snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:19:12 +05:00
commit c60cc7ca75
8092 changed files with 394160 additions and 0 deletions

35
.docker/Dockerfile-alpine Normal file
View File

@@ -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"]

46
.docker/Dockerfile-build Normal file
View File

@@ -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"]

18
.docker/Dockerfile-debug Normal file
View File

@@ -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"]

View File

@@ -0,0 +1,7 @@
FROM gcr.io/distroless/static-debian12:nonroot
COPY kratos /usr/bin/kratos
EXPOSE 4433 4434
ENTRYPOINT ["kratos"]
CMD ["serve"]

View File

@@ -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

16
.dockerignore Normal file
View File

@@ -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

11
.editorconfig Normal file
View File

@@ -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

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @aeneasr @ory/product-development

8
.github/FUNDING.yml vendored Normal file
View File

@@ -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

122
.github/ISSUE_TEMPLATE/BUG-REPORT.yml vendored Normal file
View File

@@ -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://<your-project-slug>.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

125
.github/ISSUE_TEMPLATE/DESIGN-DOC.yml vendored Normal file
View File

@@ -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://<your-project-slug>.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

View File

@@ -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://<your-project-slug>.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

14
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -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.

16
.github/auto_assign.yml vendored Normal file
View File

@@ -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

8
.github/codeql/codeql-config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
name: "CodeQL config"
queries:
- uses: security-and-quality
paths-ignore:
- "/test/"
- "/pkg/testhelpers"

6
.github/config.yml vendored Normal file
View File

@@ -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

5
.github/conventional_commits.json vendored Normal file
View File

@@ -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"]
}

67
.github/labels.json vendored Normal file
View File

@@ -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"]
}
]

60
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,60 @@
<!--
Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.
This text will be included in the changelog. If applicable, include links to documentation or pieces of code.
If your change includes breaking changes please add a code block documenting the breaking change:
```
BREAKING CHANGES: This patch changes the behavior of configuration item `foo` to do bar. To keep the existing
behavior please do baz.
```
-->
## Related issue(s)
<!--
If this pull request
1. is a fix for a known bug, link the issue where the bug was reported in the format of `#1234`;
2. is a fix for a previously unknown bug, explain the bug and how to reproduce it in this pull request;
3. implements a new feature, link the issue containing the design document in the format of `#1234`;
4. improves the documentation, no issue reference is required.
Pull requests introducing new features, which do not have a design document linked are more likely to be rejected and take on average 2-8 weeks longer to
get merged.
You can discuss changes with maintainers either in the Github Discussions in this repository or
join the [Ory Chat](https://www.ory.com/chat).
-->
## Checklist
<!--
Put an `x` in the boxes that apply. You can also fill these out after creating the PR.
Please be aware that pull requests must have all boxes ticked in order to be merged.
If you're unsure about any of them, don't hesitate to ask. We're here to help!
-->
- [ ] 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
<!--
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution
you did and what alternatives you considered, etc...
-->

385
.github/workflows/ci.yaml vendored Normal file
View File

@@ -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 }}

30
.github/workflows/closed_references.yml vendored Normal file
View File

@@ -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' }}

68
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -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

View File

@@ -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}"

114
.github/workflows/cve-scan.yaml vendored Normal file
View File

@@ -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::"

21
.github/workflows/format.yml vendored Normal file
View File

@@ -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

25
.github/workflows/labels.yml vendored Normal file
View File

@@ -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

33
.github/workflows/milestone.yml vendored Normal file
View File

@@ -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 }}

30
.github/workflows/pm.yml vendored Normal file
View File

@@ -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

47
.github/workflows/stale.yml vendored Normal file
View File

@@ -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"

68
.gitignore vendored Normal file
View File

@@ -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

39
.golangci.yml Normal file
View File

@@ -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$"

23
.goreleaser.yml Normal file
View File

@@ -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 }}"

9
.grype.yaml Normal file
View File

@@ -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

13
.mailmap Normal file
View File

@@ -0,0 +1,13 @@
Aeneas Rekkas <aeneas@ory.sh> <arekkas@users.noreply.github.com>
Aeneas Rekkas <aeneas@ory.sh> <3372410+aeneasr@users.noreply.github.com>
Aeneas Rekkas <aeneas@ory.sh> <aeneasr@users.noreply.github.com>
Aeneas Rekkas <aeneas@ory.sh> <aeneas@ory.sh>
Ajay Kelkar <ajay.kelkar@ory.sh> <ajaykelkar@outlook.com>
Alano Terblanche <alano@ory.sh> <18033717+Benehiko@users.noreply.github.com>
Alano Terblanche <alano@ory.sh> <alanoterblanche@gmail.com>
Jonas Hungershausen <jonas.hungershausen@ory.sh> <jonas.hungershausen@gmail.com>
Matt Bonnell <matt.bonnell@icloud.com> <64976795+mbonnell-wish@users.noreply.github.com>
Nick Ufer <nick@ufer.dev> <me@nick-ufer.de>
Nick Ufer <nick@ufer.dev> <nick@ufer.dev>
Patrik Neu <patrik@ory.sh> <zepatrik@users.noreply.github.com>
Patrik Neu <patrik@ory.sh> <patrik@ory.sh>

15
.nancy-ignore Normal file
View File

@@ -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

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v16.19.0

7
.orycli.yml Normal file
View File

@@ -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

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
.schema/
.github/ISSUE_TEMPLATE
oryx

3
.reference-ignore Normal file
View File

@@ -0,0 +1,3 @@
**/node_modules
docs
CHANGELOG.md

View File

@@ -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"
1 module name licenses
2 github.com/arbovm/levenshtein BSD-3-Clause
3 github.com/ory/x Apache-2.0
4 github.com/stretchr/testify MIT
5 go.opentelemetry.io/otel/sdk Apache-2.0
6 golang.org/x/text BSD-3-Clause

3239
.schema/api.openapi.json Normal file

File diff suppressed because it is too large Load Diff

3285
.schema/openapi.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
disallowAdditionalPropertiesIfNotPresent: false
packageName: client
generateInterfaces: true
structPrefix: true
enumClassPrefix: true
useOneOfDiscriminatorLookup: true
isGoSubmodule: false

View File

@@ -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

View File

@@ -0,0 +1 @@
[]

View File

@@ -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"

View File

@@ -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==

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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"

View File

@@ -0,0 +1,3 @@
- op: replace
path: /components/schemas/genericError/properties/details/additionalProperties
value: false

View File

@@ -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"

View File

@@ -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

601
.schema/version.schema.json Normal file
View File

@@ -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"
}

3
.schemastore/README.md Normal file
View File

@@ -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.

File diff suppressed because it is too large Load Diff

34
.vscode/launch.json vendored Normal file
View File

@@ -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"
}
}
]
}

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"gopls": {
"formatting.gofumpt": true,
"formatting.local": "github.com/ory"
}
}

58
.vscode/tasks.json vendored Normal file
View File

@@ -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"
}
]
}

198
AUTHORS Normal file
View File

@@ -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 <adam.schepis@gmail.com>
Addison Snelling <addison@asnell.io>
adrian5 <adrian5@users.noreply.github.com>
Aeneas Rekkas <aeneas@ory.sh>
aenoralanar <38026322+aenoralanar@users.noreply.github.com>
Aidan Holland <thehappydinoa@gmail.com>
Ajay Kelkar <ajay.kelkar@ory.sh>
Alano Terblanche <alano@ory.sh>
Alexander Probst <probstal.hamm@gmail.com>
Alexandre Burgoni <burgoni.alexandre@protonmail.com>
Alexandre Gagneux <46828169+alexGNX@users.noreply.github.com>
Alexey Reshetnik <getupandgo11@gmail.com>
Andreas <AndreasGB@users.noreply.github.com>
Andreas Bucksteeg <tricky42@users.noreply.github.com>
Andrew Banchich <13824577+andrewbanchich@users.noreply.github.com>
Andrew Minkin <minkin.andrew@gmail.com>
Andrew Tan <origandrew@gmail.com>
Andrey <weivall@gmail.com>
Andy Steele <andy@spacemeld.com>
angryPopcorn <me@renyijiu.com>
Anirudh Oppiliappan <x@icyphox.sh>
Antoine Beyet <Stoakes@users.noreply.github.com>
Anuar Ustayev <anuar.ustayev@gmail.com>
Arne <a.luenser@gmail.com>
Arne <arne.luenser@ory.sh>
aspeteRakete <fdubrownik@gmail.com>
Atreya <44151328+atreya2011@users.noreply.github.com>
AVA Monitoring <56961522+avamonitoring@users.noreply.github.com>
Avinash Dwarapu <d.nash.avi@gmail.com>
b3j0f <b3j0f@users.noreply.github.com>
Bengt Hagemeister <30391176+BengtHagemeister@users.noreply.github.com>
Bengt Hagemeister <bengt@localticketing.de>
Benjamin Freeman <b.t.freeman16@gmail.com>
BIKI DAS <bikid475@gmail.com>
Bill Monkman <bmonkman@gmail.com>
Bipin Paul Bedi <bipinpaulbedi@gmail.com>
Brahm Lower <bplower@gmail.com>
Brahm Lower <brahm@censys.io>
BrentChesny <bchesny@fb.com>
CaptainStandby <18215579+CaptainStandby@users.noreply.github.com>
Carlos Chida <carlos.chida@gmail.com>
chlasch <72044019+chlasch@users.noreply.github.com>
cwei-wish <cwei@wish.com>
Cλctysman <git@cactys.dev>
Daniel Hobbs <daniel.b.hobbs@gmail.com>
David ALEXANDRE <9482408+david972@users.noreply.github.com>
David Cheung <davidcheung@live.ca>
David Laban <alsuren@gmail.com>
David Laban <david.laban@red-badger.com>
David San <davidsanfr@gmail.com>
David van der Sluijs <david@pexlab.net>
debrutal <duennes@gmail.com>
Dejan Filipovic <dejan.filipovic@gmail.com>
Dejan Filipovic <dejan@gensdeconfiance.com>
Denis Palnitsky <palnitsky@gmail.com>
Dibyajyoti Behera <dibyajyotibehera@gmail.com>
Dimitrij Drus <dadrus@gmx.de>
Dimitrij Drus <dimitrij.drus@innoq.com>
Eifoen <35534229+Eifoen@users.noreply.github.com>
emrah <emrah.com@gmail.com>
emtammaru <emtammaru@gmail.com>
Eric B <ericbellotti@gmail.com>
ernax78 <65963237+ernax78@users.noreply.github.com>
Erol Keskin <erolkeskin.dev@gmail.com>
Erol Keskin <erolmon.kskn@gmail.com>
Fabian Meyer <3982806+meyfa@users.noreply.github.com>
Felix Beuke <felix.j.beuke@gmail.com>
Ferdi <ferdina.kusumah@gmail.com>
Florian Kramer <flo.kramer@outlook.com>
Florian Nagel <nagelflorian@users.noreply.github.com>
Florian Wiech <florian.wiech@gmail.com>
frederikhors <41120635+frederikhors@users.noreply.github.com>
Gajewski Dmitriy <dmit8815@gmail.com>
Geoff Lawler <geoff.lawler@gmail.com>
Giacomo Mazzamuto <g.mazzamuto@gmail.com>
Gibheer <gibheer+github@zero-knowledge.org>
goughjo02 <goughjo02@gmail.com>
Grant Zvolský <GrantZvolsky@gmail.com>
Harsimran Singh Maan <maan.harry@gmail.com>
Henning Perl <henning.perl@gmail.com>
Jacob Lehr <jacoblehr@gmail.com>
Jakob Høgenes <1014990+jakhog@users.noreply.github.com>
Jakob Sinclair <sinclair.jakob@mailbox.org>
Jakub Błaszczyk <daemonsthere@gmail.com>
James D. Nurmi <github@qwe.cc>
James D. Nurmi <jdnurmi@qwe.cc>
Jason Ertel <jertel@users.noreply.github.com>
JeffreyThijs <jeffrey.thijs@hotmail.com>
Jelle Besseling <jelle@pingiun.com>
Jhonatan Hulse <jhonatan.hulse@gmail.com>
JiggyDown <84430646+JiggyDown@users.noreply.github.com>
jingkai <contact@jingk.ai>
jld3103 <jld3103@users.noreply.github.com>
Joe Krill <joekrill@users.noreply.github.com>
Johan Forssell <johanforssell@users.noreply.github.com>
John Binstead <67282336+Bin-fluence@users.noreply.github.com>
John <jfcurran@users.noreply.github.com>
Jonas Hungershausen <jonas.hungershausen@ory.sh>
Jonathon Sheffield <samsmug@gmail.com>
Jordan <jdisaacs@hotmail.com>
Jordan May <jwmay2012@gmail.com>
Juri <1773075+2mol@users.noreply.github.com>
Kevin Goslar <kevin.goslar@gmail.com>
Kim Neunert <kneunert@gmail.com>
Klaus Herrmann <106238709+kmherrmann@users.noreply.github.com>
koenmtb1 <koenmtb1@users.noreply.github.com>
Koen van Marrewijk <koenmtb1@users.noreply.github.com>
kszafran <k.p.szafranski@gmail.com>
Kun Chong <kun@refs.cn>
Landon Pattison <67596936+LandonPattison@users.noreply.github.com>
Lan Phan <quoclan@gmail.com>
Li Ming <lmcl90@hotmail.com>
Lorenzo Baldassarri <lorenzobaldassarri16@gmail.com>
Łukasz Harasimowicz <dev@harnash.eu>
Łukasz Szcześniak <viters@student.agh.edu.pl>
Márk Bartos <Jackneill@users.noreply.github.com>
Mart Aarma <mart.aarma@nortal.com>
Martin Eigenbrodt <martin.eigenbrodt@googlemail.com>
Martin Eigenbrodt <martin.eigenbrodt@innoq.com>
Martin <martin.boehm@1mbsoftware.net>
Mathias Polligkeit <13847569+woylie@users.noreply.github.com>
Matt Bonnell <matt.bonnell@icloud.com>
Matthieu Jacquot <matthieu.jacquot.tech@protonmail.ch>
Matúš Múčka <zippersk.mm@gmail.com>
Maurice Freitag <maurice.freitag@hotmail.de>
Mike Milano <coder1@gmail.com>
Mike <perryao@users.noreply.github.com>
Mikołaj Meller <52668809+mmeller-wikia@users.noreply.github.com>
Miłosz <12242002+mszekiel@users.noreply.github.com>
MiniDigger | Martin <admin@benndorf.dev>
Mitar <mitar.github@tnode.com>
Mitsuo Heijo <25817501+johejo@users.noreply.github.com>
Nanik <nanikjava@gmail.com>
Natanael Oliva <natanael.og@gmail.com>
Neil Rutherford <374275+nrutherford@users.noreply.github.com>
Nichlas Lendal <62498670+pr1ze@users.noreply.github.com>
Nicholas Bush <nickdbush@hotmail.co.uk>
Nick Campbell <nickcampbell18@gmail.com>
Nick Malcev <41836660+nmlc@users.noreply.github.com>
Nick Ufer <nick@ufer.dev>
Nikita Puzankov <humb1t@yandex.ru>
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 <contact@omarhachach.com>
Patrik Neu <patrik@ory.sh>
Paweł Hemperek <pawel+github@hemperek.pl>
Piotr Mścichowski <piotr.mscichowski@gmail.com>
Przemysław Czaus <przemyslaw@czaus.pl>
Quentin Gliech <quentingliech@gmail.com>
qvamatel <qvamatel@freemail.hu>
Radek Gruchalski <radekg@users.noreply.github.com>
RamiBerm <54766858+RamiBerm@users.noreply.github.com>
Rauno Viskus <Rauno56@users.noreply.github.com>
reshetnik-alexey <oleksii.reshetnik@gmail.com>
Rodrigo Queiro <overdrigzed@gmail.com>
rvo <ryanvanoss@users.noreply.github.com>
sawadashota <shota@sslife.tech>
sawadashota <xiootas@gmail.com>
Seb <git@erhardt.net>
seremenko-wish <60801091+seremenko-wish@users.noreply.github.com>
Sergey Plaunov <splaunov@users.noreply.github.com>
sherbang <brian@sherbang.com>
shivam <86538330+shivam-909@users.noreply.github.com>
Simon Lipp <sloonz@gmail.com>
Simon-Pierre Gingras <892367+spg@users.noreply.github.com>
Steffen Heidel <kontakt@sthh.eu>
TannerGabriel <40315960+TannerGabriel@users.noreply.github.com>
Theodor Brandt <teddy@brandts.se>
theotherian <ian.g.simpson@gmail.com>
Thomas Aidan Curran <2030403+tacurran@users.noreply.github.com>
Thomas Guillet <guillet.thomas@gmail.com>
Thomas Ruiz <contact@thomasruiz.eu>
Tomasz Tomalak <12939493+t-tomalak@users.noreply.github.com>
Tom Fenech <tomjwfenech@gmail.com>
Toon van Strijp <toonvanstrijp@gmail.com>
Tsirkin Evgeny <tsirkin@gmail.com>
ttimonen <toni.timonen@iki.fi>
Tyler Battle <tyler@battle.ca>
VeenaInd <104088519+VeenaInd@users.noreply.github.com>
Victor Duarte <zvictor@users.noreply.github.com>
Vincent <vincent@ory.sh>
vinckr <vincent@ory.sh>
Viz <viz@linux.com>
WeiQi <17816875364@163.com>
wezzle <wesley@black-sky.nl>
Yagiz Nizipli <yagiz@nizipli.com>
yon <38630464+yonbh@users.noreply.github.com>
Yorick Holkamp <github@nextpulse.nl>
you1996 <45292366+you1996@users.noreply.github.com>
Yuvraj <10830562+evalsocket@users.noreply.github.com>
Zageron <hello@adambryant.ca>
Zhiming Guo <guo.zhming@gmail.com>
znerol <lo+github@znerol.ch>
好风 <gwindfree@gmail.com>

145
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,145 @@
<!-- AUTO-GENERATED, DO NOT EDIT! -->
<!-- Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/CODE_OF_CONDUCT.md -->
# 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

293
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,293 @@
<!-- AUTO-GENERATED, DO NOT EDIT! -->
<!-- Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/CONTRIBUTING.md -->
# Contribute to Ory Kratos<!-- omit in toc -->
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [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)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 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:<YOUR-GITHUB-USERNAME-HERE>/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).

215
DEVELOP.md Normal file
View File

@@ -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
```

201
LICENSE Normal file
View File

@@ -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.

214
Makefile Normal file
View File

@@ -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

517
README.md Normal file
View File

@@ -0,0 +1,517 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/ory/meta/master/static/banners/kratos.svg" alt="Ory Kratos - Cloud native identity and user management">
</h1>
<h4 align="center">
<a href="https://www.ory.com/chat">Chat</a> ·
<a href="https://github.com/ory/kratos/discussions">Discussions</a> ·
<a href="https://www.ory.com/l/sign-up-newsletter">Newsletter</a> ·
<a href="https://www.ory.com/docs/">Docs</a> ·
<a href="https://console.ory.sh/">Try Ory Network</a> ·
<a href="https://www.ory.com/jobs/">Jobs</a>
</h4>
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.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**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)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 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?
<!--BEGIN ADOPTERS-->
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
<a href="mailto:office@ory.com">office@ory.com</a> now_!
<table>
<thead>
<tr>
<th>Name</th>
<th>Logo</th>
<th>Website</th>
<th>Case Study</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenAI</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/openai.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/openai.svg" alt="OpenAI">
</picture>
</td>
<td><a href="https://openai.com/">openai.com</a></td>
<td><a href="https://www.ory.com/case-studies/openai">OpenAI Case Study</a></td>
</tr>
<tr>
<td>Fandom</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/fandom.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/fandom.svg" alt="Fandom">
</picture>
</td>
<td><a href="https://www.fandom.com/">fandom.com</a></td>
<td><a href="https://www.ory.com/case-studies/fandom">Fandom Case Study</a></td>
</tr>
<tr>
<td>Lumin</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/lumin.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/lumin.svg" alt="Lumin">
</picture>
</td>
<td><a href="https://www.luminpdf.com/">luminpdf.com</a></td>
<td><a href="https://www.ory.com/case-studies/lumin">Lumin Case Study</a></td>
</tr>
<tr>
<td>Sencrop</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/sencrop.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/sencrop.svg" alt="Sencrop">
</picture>
</td>
<td><a href="https://sencrop.com/">sencrop.com</a></td>
<td><a href="https://www.ory.com/case-studies/sencrop">Sencrop Case Study</a></td>
</tr>
<tr>
<td>OSINT Industries</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/osint.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/osint.svg" alt="OSINT Industries">
</picture>
</td>
<td><a href="https://www.osint.industries/">osint.industries</a></td>
<td><a href="https://www.ory.com/case-studies/osint">OSINT Industries Case Study</a></td>
</tr>
<tr>
<td>HGV</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/hgv.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/hgv.svg" alt="HGV">
</picture>
</td>
<td><a href="https://www.hgv.it/">hgv.it</a></td>
<td><a href="https://www.ory.com/case-studies/hgv">HGV Case Study</a></td>
</tr>
<tr>
<td>Maxroll</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/maxroll.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/maxroll.svg" alt="Maxroll">
</picture>
</td>
<td><a href="https://maxroll.gg/">maxroll.gg</a></td>
<td><a href="https://www.ory.com/case-studies/maxroll">Maxroll Case Study</a></td>
</tr>
<tr>
<td>Zezam</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/zezam.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/zezam.svg" alt="Zezam">
</picture>
</td>
<td><a href="https://www.zezam.io/">zezam.io</a></td>
<td><a href="https://www.ory.com/case-studies/zezam">Zezam Case Study</a></td>
</tr>
<tr>
<td>T.RowePrice</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/troweprice.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/troweprice.svg" alt="T.RowePrice">
</picture>
</td>
<td><a href="https://www.troweprice.com/">troweprice.com</a></td>
</tr>
<tr>
<td>Mistral</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/mistral.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/mistral.svg" alt="Mistral">
</picture>
</td>
<td><a href="https://www.mistral.ai/">mistral.ai</a></td>
</tr>
<tr>
<td>Axel Springer</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/axelspringer.svg" />
<img height="22px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/axelspringer.svg" alt="Axel Springer">
</picture>
</td>
<td><a href="https://www.axelspringer.com/">axelspringer.com</a></td>
</tr>
<tr>
<td>Hemnet</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/hemnet.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/hemnet.svg" alt="Hemnet">
</picture>
</td>
<td><a href="https://www.hemnet.se/">hemnet.se</a></td>
</tr>
<tr>
<td>Cisco</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/cisco.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/cisco.svg" alt="Cisco">
</picture>
</td>
<td><a href="https://www.cisco.com/">cisco.com</a></td>
</tr>
<tr>
<td>Presidencia de la República Dominicana</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/republica-dominicana.svg" />
<img height="42px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/republica-dominicana.svg" alt="Presidencia de la República Dominicana">
</picture>
</td>
<td><a href="https://www.presidencia.gob.do/">presidencia.gob.do</a></td>
</tr>
<tr>
<td>Moonpig</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/moonpig.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/moonpig.svg" alt="Moonpig">
</picture>
</td>
<td><a href="https://www.moonpig.com/">moonpig.com</a></td>
</tr>
<tr>
<td>Booster</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/booster.svg" />
<img height="18px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/booster.svg" alt="Booster">
</picture>
</td>
<td><a href="https://www.choosebooster.com/">choosebooster.com</a></td>
</tr>
<tr>
<td>Zaptec</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/zaptec.svg" />
<img height="24px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/zaptec.svg" alt="Zaptec">
</picture>
</td>
<td><a href="https://www.zaptec.com/">zaptec.com</a></td>
</tr>
<tr>
<td>Klarna</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/klarna.svg" />
<img height="24px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/klarna.svg" alt="Klarna">
</picture>
</td>
<td><a href="https://www.klarna.com/">klarna.com</a></td>
</tr>
<tr>
<td>Raspberry PI Foundation</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/raspi.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/raspi.svg" alt="Raspberry PI Foundation">
</picture>
</td>
<td><a href="https://www.raspberrypi.org/">raspberrypi.org</a></td>
</tr>
<tr>
<td>Tulip</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/tulip.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/tulip.svg" alt="Tulip Retail">
</picture>
</td>
<td><a href="https://tulip.com/">tulip.com</a></td>
</tr>
<tr>
<td>Hootsuite</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/hootsuite.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/hootsuite.svg" alt="Hootsuite">
</picture>
</td>
<td><a href="https://hootsuite.com/">hootsuite.com</a></td>
</tr>
<tr>
<td>Segment</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/segment.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/segment.svg" alt="Segment">
</picture>
</td>
<td><a href="https://segment.com/">segment.com</a></td>
</tr>
<tr>
<td>Arduino</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/arduino.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/arduino.svg" alt="Arduino">
</picture>
</td>
<td><a href="https://www.arduino.cc/">arduino.cc</a></td>
</tr>
<tr>
<td>Sainsbury's</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/sainsburys.svg" />
<img height="24px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/sainsburys.svg" alt="Sainsbury's">
</picture>
</td>
<td><a href="https://www.sainsburys.co.uk/">sainsburys.co.uk</a></td>
</tr>
<tr>
<td>Contraste</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/contraste.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/contraste.svg" alt="Contraste">
</picture>
</td>
<td><a href="https://www.contraste.com/en">contraste.com</a></td>
</tr>
<tr>
<td>inMusic</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/inmusic.svg" />
<img height="24px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/inmusic.svg" alt="InMusic">
</picture>
</td>
<td><a href="https://inmusicbrands.com/">inmusicbrands.com</a></td>
</tr>
<tr>
<td>Buhta</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/buhta.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/buhta.svg" alt="Buhta">
</picture>
</td>
<td><a href="https://buhta.com/">buhta.com</a></td>
</tr>
</tr>
<tr>
<td>Amplitude</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/amplitude.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/amplitude.svg" alt="amplitude.com">
</picture>
</td>
<td><a href="https://amplitude.com/">amplitude.com</a></td>
</tr>
<tr>
<td align="center"><a href="https://tier4.jp/en/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/tieriv.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/tieriv.svg" alt="TIER IV"></picture></a></td>
<td align="center"><a href="https://kyma-project.io"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/kyma.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/kyma.svg" alt="Kyma Project"></picture></a></td>
<td align="center"><a href="https://serlo.org/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/serlo.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/serlo.svg" alt="Serlo"></picture></a></td>
<td align="center"><a href="https://padis.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/padis.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/padis.svg" alt="Padis"></picture></a></td>
</tr>
<tr>
<td align="center"><a href="https://cloudbear.eu/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/cloudbear.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/cloudbear.svg" alt="Cloudbear"></picture></a></td>
<td align="center"><a href="https://securityonionsolutions.com/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/securityonion.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/securityonion.svg" alt="Security Onion Solutions"></picture></a></td>
<td align="center"><a href="https://factlylabs.com/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/factly.svg" /><img height="24px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/factly.svg" alt="Factly"></picture></a></td>
<td align="center"><a href="https://cashdeck.com.au/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/allmyfunds.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/allmyfunds.svg" alt="All My Funds"></picture></a></td>
</tr>
<tr>
<td align="center"><a href="https://nortal.com/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/nortal.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/nortal.svg" alt="Nortal"></picture></a></td>
<td align="center"><a href="https://www.ordermygear.com/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/ordermygear.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/ordermygear.svg" alt="OrderMyGear"></picture></a></td>
<td align="center"><a href="https://r2devops.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/r2devops.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/r2devops.svg" alt="R2Devops"></picture></a></td>
<td align="center"><a href="https://www.paralus.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/paralus.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/paralus.svg" alt="Paralus"></picture></a></td>
</tr>
<tr>
<td align="center"><a href="https://dyrector.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/dyrector_io.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/dyrector_io.svg" alt="dyrector.io"></picture></a></td>
<td align="center"><a href="https://pinniped.dev/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/pinniped.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/pinniped.svg" alt="pinniped.dev"></picture></a></td>
<td align="center"><a href="https://pvotal.tech/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/pvotal.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/pvotal.svg" alt="pvotal.tech"></picture></a></td>
<td></td>
</tr>
</tbody>
</table>
Many thanks to all individual contributors
<a href="https://opencollective.com/ory" target="_blank"><img src="https://opencollective.com/ory/contributors.svg?width=890&limit=714&button=false" /></a>
<!--END ADOPTERS-->

56
SECURITY.md Normal file
View File

@@ -0,0 +1,56 @@
<!-- AUTO-GENERATED, DO NOT EDIT! -->
<!-- Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/SECURITY.md -->
# 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).

12
buf.gen.yaml Normal file
View File

@@ -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

9
buf.yaml Normal file
View File

@@ -0,0 +1,9 @@
version: v2
modules:
- path: proto
lint:
use:
- DEFAULT
breaking:
use:
- FILE

84
cipher/aes.go Normal file
View File

@@ -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."))
}

94
cipher/chacha20.go Normal file
View File

@@ -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"))
}

29
cipher/cipher.go Normal file
View File

@@ -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
}

136
cipher/cipher_test.go Normal file
View File

@@ -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)
}

26
cipher/noop.go Normal file
View File

@@ -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)
}

67
cmd/cleanup/handler.go Normal file
View File

@@ -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
}

25
cmd/cleanup/root.go Normal file
View File

@@ -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())
}

49
cmd/cleanup/sql.go Normal file
View File

@@ -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 <database-url>",
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
}

27
cmd/cleanup/sql_test.go Normal file
View File

@@ -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()
}

82
cmd/cliclient/client.go Normal file
View File

@@ -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))
}

431
cmd/clidoc/main.go Normal file
View File

@@ -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)<!-- START MESSAGE TABLE -->(.*?)<!-- END MESSAGE TABLE -->`)
result := r.ReplaceAllString(string(content), "<!-- START MESSAGE TABLE -->\n"+w.String()+"\n<!-- END MESSAGE TABLE -->")
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
}

14
cmd/clidoc/main_test.go Normal file
View File

@@ -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"))
}

27
cmd/courier/root.go Normal file
View File

@@ -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))
}

108
cmd/courier/watch.go Normal file
View File

@@ -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
}

47
cmd/courier/watch_test.go Normal file
View File

@@ -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)
})
}

371
cmd/daemon/serve.go Normal file
View File

@@ -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()
}
}

76
cmd/daemon/serve_test.go Normal file
View File

@@ -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)
}

View File

@@ -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 <requests-per-minute>",
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
}

View File

@@ -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 <password1> [<password2> ...]",
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
}

View File

@@ -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 <authentication-requests-per-minute>",
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
}

177
cmd/hashers/argon2/root.go Normal file
View File

@@ -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
}

25
cmd/hashers/root.go Normal file
View File

@@ -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)
}

View File

@@ -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)
}

72
cmd/identities/delete.go Normal file
View File

@@ -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
},
}
}

View File

@@ -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)
})
}

116
cmd/identities/get.go Normal file
View File

@@ -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
}

132
cmd/identities/get_test.go Normal file
View File

@@ -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)
})
}

53
cmd/identities/helpers.go Normal file
View File

@@ -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
}

View File

@@ -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
}

98
cmd/identities/import.go Normal file
View File

@@ -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 <<EOF
{
"schema_id": "default",
"traits": {
"email": "foo@example.com"
}
}
EOF
{{ .CommandPath }} file.json
Alternatively:
cat file.json | {{ .CommandPath }}`,
Long: `Import identities from files or STD_IN.
Files can contain only a single or an array of identities. The validity of files can be tested beforehand using "... identities validate".`,
RunE: func(cmd *cobra.Command, args []string) error {
c, err := cliclient.NewClient(cmd)
if err != nil {
return err
}
imported := make([]kratos.Identity, 0, len(args))
failed := make(map[string]error)
is, err := readIdentities(cmd, args)
if err != nil {
return err
}
for src, i := range is {
var params kratos.CreateIdentityBody
err = json.Unmarshal([]byte(i), &params)
if err != nil {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "STD_IN: Could not parse identity")
return cmdx.FailSilently(cmd)
}
ident, _, err := c.IdentityAPI.CreateIdentity(cmd.Context()).CreateIdentityBody(params).Execute()
if err != nil {
failed[src] = cmdx.PrintOpenAPIError(cmd, err)
} else {
imported = append(imported, *ident)
}
}
if len(imported) == 1 {
cmdx.PrintRow(cmd, (*outputIdentity)(&imported[0]))
} else {
cmdx.PrintTable(cmd, &outputIdentityCollection{Identities: imported})
}
cmdx.PrintErrors(cmd, failed)
if len(failed) != 0 {
return cmdx.FailSilently(cmd)
}
return nil
},
}
}

Some files were not shown because too many files have changed in this diff Show More