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

View File

@@ -0,0 +1,55 @@
{
"$id": "https://schemas.ory.sh/kratos/selfservice/strategy/password/login.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"csrf_token": {
"type": "string"
},
"provider": {
"type": "string",
"minLength": 1
},
"traits": {
"description": "DO NOT DELETE THIS FIELD. This field will be overwritten in login.go's and registration.go's decoder() method. Do not add anything to this field as it has no effect."
},
"transient_payload": {
"type": "object",
"additionalProperties": true
},
"method": {
"type": "string"
},
"upstream_parameters": {
"type": "object",
"$comment": "Only the defined parameters are allowed. This is to prevent users from sending arbitrary parameters or craft URLs that cause unexpected behavior.",
"properties": {
"hd": {
"description": "The hd (hosted domain) parameter streamlines the login process for G Suite hosted accounts. By including the domain of the G Suite user (for example, mycollege.edu), you can indicate that the account selection UI should be optimized for accounts at that domain.",
"type": "string"
},
"prompt": {
"description": "The prompt specifies whether the Authorization Server prompts the End-User for reauthentication and consent (for example, select_account).",
"type": "string"
},
"auth_type": {
"description": "The `auth_type` parameter specifies the requested authentication features (as a comma-separated list).",
"type": "string"
},
"acr_values": {
"description": "The `acr_values` parameter specifies the Authentication Context Class Reference values for the authorization request.",
"type": "string"
},
"additionalProperties": false
}
},
"id_token": {
"type": "string",
"description": "An optional id token provided by an OIDC provider"
},
"id_token_nonce": {
"type": "string",
"description": "The nonce used when requesting the id_token from the provider. Required, if an id_token is given and the provider supports it."
}
}
}

View File

@@ -0,0 +1,42 @@
{
"$id": "https://schemas.ory.sh/kratos/selfservice/strategy/oidc/.schema/settings.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"link": {
"type": "string"
},
"csrf_token": {
"type": "string"
},
"unlink": {
"type": "string"
},
"traits": {
"description": "This field will be overwritten in registration.go's decoder() method. Do not add anything to this field as it has no effect."
},
"upstream_parameters": {
"type": "object",
"$comment": "Only the defined parameters are allowed. This is to prevent users from sending arbitrary parameters or craft URLs that cause unexpected behavior.",
"properties": {
"hd": {
"description": "The hd (hosted domain) parameter streamlines the login process for G Suite hosted accounts. By including the domain of the G Suite user (for example, mycollege.edu), you can indicate that the account selection UI should be optimized for accounts at that domain.",
"type": "string"
},
"prompt": {
"description": "The prompt specifies whether the Authorization Server prompts the End-User for reauthentication and consent (for example, select_account).",
"type": "string"
},
"acr_values": {
"description": "The `acr_values` parameter specifies the Authentication Context Class Reference values for the authorization request.",
"type": "string"
},
"additionalProperties": false
}
},
"transient_payload": {
"type": "object",
"additionalProperties": true
}
}
}

View File

@@ -0,0 +1,38 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "test-provider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010002,
"text": "Sign in with test-provider",
"type": "info",
"context": {
"provider": "test-provider",
"provider_id": "test-provider"
}
}
}
}
]

View File

@@ -0,0 +1,38 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "test-provider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010002,
"text": "Sign in with test-provider",
"type": "info",
"context": {
"provider": "test-provider",
"provider_id": "test-provider"
}
}
}
}
]

View File

@@ -0,0 +1,38 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "test-provider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010002,
"text": "Sign in with test-provider",
"type": "info",
"context": {
"provider": "test-provider",
"provider_id": "test-provider"
}
}
}
}
]

View File

@@ -0,0 +1,25 @@
[
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "test-provider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010002,
"text": "Sign in with test-provider",
"type": "info",
"context": {
"provider": "test-provider",
"provider_id": "test-provider"
}
}
}
}
]

View File

@@ -0,0 +1,38 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "test-provider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010002,
"text": "Sign in with test-provider",
"type": "info",
"context": {
"provider": "test-provider",
"provider_id": "test-provider"
}
}
}
}
]

View File

@@ -0,0 +1,38 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "test-provider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010002,
"text": "Sign in with test-provider",
"type": "info",
"context": {
"provider": "test-provider",
"provider_id": "test-provider"
}
}
}
}
]

View File

@@ -0,0 +1,38 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "test-provider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010002,
"text": "Sign in with test-provider",
"type": "info",
"context": {
"provider": "test-provider",
"provider_id": "test-provider"
}
}
}
}
]

View File

@@ -0,0 +1,37 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "test-provider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010002,
"text": "Sign in with test-provider",
"type": "info",
"context": {
"provider": "test-provider"
}
}
}
}
]

View File

@@ -0,0 +1,15 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
}
]

View File

@@ -0,0 +1,38 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "providerID",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with providerID",
"type": "info",
"context": {
"provider": "providerID",
"provider_id": "providerID"
}
}
}
}
]

View File

@@ -0,0 +1,38 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "providerID",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with providerID",
"type": "info",
"context": {
"provider": "providerID",
"provider_id": "providerID"
}
}
}
}
]

View File

@@ -0,0 +1,38 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "providerID",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with providerID",
"type": "info",
"context": {
"provider": "providerID",
"provider_id": "providerID"
}
}
}
}
]

View File

@@ -0,0 +1,38 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "providerID",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with providerID",
"type": "info",
"context": {
"provider": "providerID",
"provider_id": "providerID"
}
}
}
}
]

View File

@@ -0,0 +1,38 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "providerID",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with providerID",
"type": "info",
"context": {
"provider": "providerID",
"provider_id": "providerID"
}
}
}
}
]

View File

@@ -0,0 +1,38 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "providerID",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with providerID",
"type": "info",
"context": {
"provider": "providerID",
"provider_id": "providerID"
}
}
}
}
]

View File

@@ -0,0 +1,38 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "providerID",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with providerID",
"type": "info",
"context": {
"provider": "providerID",
"provider_id": "providerID"
}
}
}
}
]

View File

@@ -0,0 +1,165 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050003,
"text": "Unlink github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050002,
"text": "Link google",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "ory"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "Ory"
},
"id": 1050003,
"text": "Unlink Ory",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,165 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050002,
"text": "Link github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050003,
"text": "Unlink google",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "ory"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "Ory"
},
"id": 1050003,
"text": "Unlink Ory",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,143 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050002,
"text": "Link github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050002,
"text": "Link google",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,165 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050002,
"text": "Link github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050002,
"text": "Link google",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "ory"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "Ory"
},
"id": 1050002,
"text": "Link Ory",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,165 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050002,
"text": "Link github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050003,
"text": "Unlink google",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "ory"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "Ory"
},
"id": 1050003,
"text": "Unlink Ory",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,165 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050003,
"text": "Unlink github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050002,
"text": "Link google",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "ory"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "Ory"
},
"id": 1050003,
"text": "Unlink Ory",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,165 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050002,
"text": "Link github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050002,
"text": "Link google",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "ory"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "Ory"
},
"id": 1050002,
"text": "Link Ory",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,143 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050002,
"text": "Link github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050002,
"text": "Link google",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,165 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050003,
"text": "Unlink github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050003,
"text": "Unlink google",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "ory"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "Ory"
},
"id": 1050003,
"text": "Unlink Ory",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,165 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050003,
"text": "Unlink github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050002,
"text": "Link google",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "ory"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "Ory"
},
"id": 1050003,
"text": "Unlink Ory",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,165 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.email",
"type": "email",
"required": true,
"autocomplete": "email",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.name",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "method",
"type": "submit",
"value": "profile",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "new-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "unlink",
"type": "submit",
"value": "github",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050003,
"text": "Unlink github",
"type": "info",
"context": {
"provider": "github"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "unlink",
"type": "submit",
"value": "google",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050003,
"text": "Unlink google",
"type": "info",
"context": {
"provider": "google"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "unlink",
"type": "submit",
"value": "ory",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050003,
"text": "Unlink Ory",
"type": "info",
"context": {
"provider": "Ory"
}
}
}
}
]

View File

@@ -0,0 +1,165 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050002,
"text": "Link github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050003,
"text": "Unlink google",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "ory"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "Ory"
},
"id": 1050002,
"text": "Link Ory",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,165 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.email",
"type": "email",
"required": true,
"autocomplete": "email",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.name",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "method",
"type": "submit",
"value": "profile",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "new-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "unlink",
"type": "submit",
"value": "github",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050003,
"text": "Unlink github",
"type": "info",
"context": {
"provider": "github"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "link",
"type": "submit",
"value": "google",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050002,
"text": "Link google",
"type": "info",
"context": {
"provider": "google"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "unlink",
"type": "submit",
"value": "ory",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050003,
"text": "Unlink Ory",
"type": "info",
"context": {
"provider": "Ory"
}
}
}
}
]

View File

@@ -0,0 +1,143 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.email",
"type": "email",
"required": true,
"autocomplete": "email",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.name",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "method",
"type": "submit",
"value": "profile",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "new-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "link",
"type": "submit",
"value": "github",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050002,
"text": "Link github",
"type": "info",
"context": {
"provider": "github"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "link",
"type": "submit",
"value": "google",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050002,
"text": "Link google",
"type": "info",
"context": {
"provider": "google"
}
}
}
}
]

View File

@@ -0,0 +1,165 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050003,
"text": "Unlink github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050002,
"text": "Link google",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "ory"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "Ory"
},
"id": 1050003,
"text": "Unlink Ory",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,165 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.email",
"type": "email",
"required": true,
"autocomplete": "email",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.name",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "method",
"type": "submit",
"value": "profile",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "new-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "unlink",
"type": "submit",
"value": "github",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050003,
"text": "Unlink github",
"type": "info",
"context": {
"provider": "github"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "link",
"type": "submit",
"value": "google",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050002,
"text": "Link google",
"type": "info",
"context": {
"provider": "google"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "unlink",
"type": "submit",
"value": "ory",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050003,
"text": "Unlink Ory",
"type": "info",
"context": {
"provider": "Ory"
}
}
}
}
]

View File

@@ -0,0 +1,165 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050003,
"text": "Unlink github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050002,
"text": "Link google",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "unlink",
"node_type": "input",
"type": "submit",
"value": "ory"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "Ory"
},
"id": 1050003,
"text": "Unlink Ory",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,165 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.email",
"type": "email",
"required": true,
"autocomplete": "email",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.name",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "method",
"type": "submit",
"value": "profile",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "new-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "unlink",
"type": "submit",
"value": "github",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050003,
"text": "Unlink github",
"type": "info",
"context": {
"provider": "github"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "link",
"type": "submit",
"value": "google",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050002,
"text": "Link google",
"type": "info",
"context": {
"provider": "google"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "unlink",
"type": "submit",
"value": "ory",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050003,
"text": "Unlink Ory",
"type": "info",
"context": {
"provider": "Ory"
}
}
}
}
]

View File

@@ -0,0 +1,143 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "email",
"disabled": false,
"name": "traits.email",
"node_type": "input",
"required": true,
"type": "email"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.name",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "github"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "github"
},
"id": 1050002,
"text": "Link github",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "link",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google"
},
"id": 1050002,
"text": "Link google",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,143 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.email",
"type": "email",
"required": true,
"autocomplete": "email",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.name",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "method",
"type": "submit",
"value": "profile",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "new-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "link",
"type": "submit",
"value": "github",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050002,
"text": "Link github",
"type": "info",
"context": {
"provider": "github"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "link",
"type": "submit",
"value": "google",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050002,
"text": "Link google",
"type": "info",
"context": {
"provider": "google"
}
}
}
}
]

View File

@@ -0,0 +1,297 @@
{
"organization_id": null,
"type": "browser",
"active": "oidc",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-acr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with aal2-acr",
"type": "info",
"context": {
"provider": "aal2-acr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-amr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with aal2-amr",
"type": "info",
"context": {
"provider": "aal2-amr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "autoPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with autoPKCE",
"type": "info",
"context": {
"provider": "autoPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "claimsViaUserInfo",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with claimsViaUserInfo",
"type": "info",
"context": {
"provider": "claimsViaUserInfo"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "forcePKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with forcePKCE",
"type": "info",
"context": {
"provider": "forcePKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "invalid-issuer",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with invalid-issuer",
"type": "info",
"context": {
"provider": "invalid-issuer"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "neverPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with neverPKCE",
"type": "info",
"context": {
"provider": "neverPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "secondProvider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with secondProvider",
"type": "info",
"context": {
"provider": "secondProvider"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "valid2",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with valid2",
"type": "info",
"context": {
"provider": "valid2"
}
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"value": "email-exist-with-oidc-strategy-lh-false@ory.sh",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "current-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010022,
"text": "Sign in with password",
"type": "info"
}
}
}
],
"messages": [
{
"id": 1010016,
"text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.",
"type": "info",
"context": {
"available_credential_types": [],
"available_providers": [],
"duplicateIdentifier": "email-exist-with-oidc-strategy-lh-false@ory.sh",
"duplicate_identifier": "email-exist-with-oidc-strategy-lh-false@ory.sh",
"provider": "generic"
}
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,297 @@
{
"organization_id": null,
"type": "browser",
"active": "oidc",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-acr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with aal2-acr",
"type": "info",
"context": {
"provider": "aal2-acr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-amr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with aal2-amr",
"type": "info",
"context": {
"provider": "aal2-amr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "autoPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with autoPKCE",
"type": "info",
"context": {
"provider": "autoPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "claimsViaUserInfo",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with claimsViaUserInfo",
"type": "info",
"context": {
"provider": "claimsViaUserInfo"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "forcePKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with forcePKCE",
"type": "info",
"context": {
"provider": "forcePKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "invalid-issuer",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with invalid-issuer",
"type": "info",
"context": {
"provider": "invalid-issuer"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "neverPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with neverPKCE",
"type": "info",
"context": {
"provider": "neverPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "secondProvider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with secondProvider",
"type": "info",
"context": {
"provider": "secondProvider"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "valid2",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with valid2",
"type": "info",
"context": {
"provider": "valid2"
}
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"value": "email-exist-with-oidc-strategy-lh-false@ory.sh",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "current-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010022,
"text": "Sign in with password",
"type": "info"
}
}
}
],
"messages": [
{
"id": 1010016,
"text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.",
"type": "info",
"context": {
"available_credential_types": [],
"available_providers": [],
"duplicateIdentifier": "email-exist-with-oidc-strategy-lh-false@ory.sh",
"duplicate_identifier": "email-exist-with-oidc-strategy-lh-false@ory.sh",
"provider": "generic"
}
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,297 @@
{
"organization_id": null,
"type": "browser",
"active": "oidc",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-acr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with aal2-acr",
"type": "info",
"context": {
"provider": "aal2-acr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-amr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with aal2-amr",
"type": "info",
"context": {
"provider": "aal2-amr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "autoPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with autoPKCE",
"type": "info",
"context": {
"provider": "autoPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "claimsViaUserInfo",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with claimsViaUserInfo",
"type": "info",
"context": {
"provider": "claimsViaUserInfo"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "forcePKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with forcePKCE",
"type": "info",
"context": {
"provider": "forcePKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "invalid-issuer",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with invalid-issuer",
"type": "info",
"context": {
"provider": "invalid-issuer"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "neverPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with neverPKCE",
"type": "info",
"context": {
"provider": "neverPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "secondProvider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with secondProvider",
"type": "info",
"context": {
"provider": "secondProvider"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "valid2",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with valid2",
"type": "info",
"context": {
"provider": "valid2"
}
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"value": "email-exist-with-oidc-strategy-lh-false@ory.sh",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "current-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010022,
"text": "Sign in with password",
"type": "info"
}
}
}
],
"messages": [
{
"id": 1010016,
"text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.",
"type": "info",
"context": {
"available_credential_types": [],
"available_providers": [],
"duplicateIdentifier": "email-exist-with-oidc-strategy-lh-false@ory.sh",
"duplicate_identifier": "email-exist-with-oidc-strategy-lh-false@ory.sh",
"provider": "generic"
}
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,297 @@
{
"organization_id": null,
"type": "browser",
"active": "oidc",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-acr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with aal2-acr",
"type": "info",
"context": {
"provider": "aal2-acr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-amr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with aal2-amr",
"type": "info",
"context": {
"provider": "aal2-amr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "autoPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with autoPKCE",
"type": "info",
"context": {
"provider": "autoPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "claimsViaUserInfo",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with claimsViaUserInfo",
"type": "info",
"context": {
"provider": "claimsViaUserInfo"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "forcePKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with forcePKCE",
"type": "info",
"context": {
"provider": "forcePKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "invalid-issuer",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with invalid-issuer",
"type": "info",
"context": {
"provider": "invalid-issuer"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "neverPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with neverPKCE",
"type": "info",
"context": {
"provider": "neverPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "secondProvider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with secondProvider",
"type": "info",
"context": {
"provider": "secondProvider"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "valid2",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with valid2",
"type": "info",
"context": {
"provider": "valid2"
}
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"value": "email-exist-with-password-strategy-lh-false@ory.sh",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "current-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010022,
"text": "Sign in with password",
"type": "info"
}
}
}
],
"messages": [
{
"id": 1010016,
"text": "You tried to sign in with \"email-exist-with-password-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.",
"type": "info",
"context": {
"available_credential_types": [],
"available_providers": [],
"duplicateIdentifier": "email-exist-with-password-strategy-lh-false@ory.sh",
"duplicate_identifier": "email-exist-with-password-strategy-lh-false@ory.sh",
"provider": "generic"
}
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,297 @@
{
"organization_id": null,
"type": "browser",
"active": "oidc",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-acr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with aal2-acr",
"type": "info",
"context": {
"provider": "aal2-acr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-amr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with aal2-amr",
"type": "info",
"context": {
"provider": "aal2-amr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "autoPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with autoPKCE",
"type": "info",
"context": {
"provider": "autoPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "claimsViaUserInfo",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with claimsViaUserInfo",
"type": "info",
"context": {
"provider": "claimsViaUserInfo"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "forcePKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with forcePKCE",
"type": "info",
"context": {
"provider": "forcePKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "invalid-issuer",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with invalid-issuer",
"type": "info",
"context": {
"provider": "invalid-issuer"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "neverPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with neverPKCE",
"type": "info",
"context": {
"provider": "neverPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "secondProvider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with secondProvider",
"type": "info",
"context": {
"provider": "secondProvider"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "valid2",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with valid2",
"type": "info",
"context": {
"provider": "valid2"
}
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"value": "email-exist-with-password-strategy-lh-false@ory.sh",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "current-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010022,
"text": "Sign in with password",
"type": "info"
}
}
}
],
"messages": [
{
"id": 1010016,
"text": "You tried to sign in with \"email-exist-with-password-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.",
"type": "info",
"context": {
"available_credential_types": [],
"available_providers": [],
"duplicateIdentifier": "email-exist-with-password-strategy-lh-false@ory.sh",
"duplicate_identifier": "email-exist-with-password-strategy-lh-false@ory.sh",
"provider": "generic"
}
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,297 @@
{
"organization_id": null,
"type": "browser",
"active": "oidc",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-acr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with aal2-acr",
"type": "info",
"context": {
"provider": "aal2-acr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-amr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with aal2-amr",
"type": "info",
"context": {
"provider": "aal2-amr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "autoPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with autoPKCE",
"type": "info",
"context": {
"provider": "autoPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "claimsViaUserInfo",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with claimsViaUserInfo",
"type": "info",
"context": {
"provider": "claimsViaUserInfo"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "forcePKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with forcePKCE",
"type": "info",
"context": {
"provider": "forcePKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "invalid-issuer",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with invalid-issuer",
"type": "info",
"context": {
"provider": "invalid-issuer"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "neverPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with neverPKCE",
"type": "info",
"context": {
"provider": "neverPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "secondProvider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with secondProvider",
"type": "info",
"context": {
"provider": "secondProvider"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "valid2",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with valid2",
"type": "info",
"context": {
"provider": "valid2"
}
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"value": "email-exist-with-password-strategy-lh-false@ory.sh",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "current-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010022,
"text": "Sign in with password",
"type": "info"
}
}
}
],
"messages": [
{
"id": 1010016,
"text": "You tried to sign in with \"email-exist-with-password-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.",
"type": "info",
"context": {
"available_credential_types": [],
"available_providers": [],
"duplicateIdentifier": "email-exist-with-password-strategy-lh-false@ory.sh",
"duplicate_identifier": "email-exist-with-password-strategy-lh-false@ory.sh",
"provider": "generic"
}
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,86 @@
{
"organization_id": null,
"type": "browser",
"active": "oidc",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "secondProvider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with secondProvider",
"type": "info",
"context": {
"provider": "secondProvider"
}
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"value": "email-exist-with-oidc-strategy-lh-true@ory.sh",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
}
],
"messages": [
{
"id": 1010016,
"text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.",
"type": "info",
"context": {
"available_credential_types": [
"oidc"
],
"available_providers": [
"secondProvider"
],
"duplicateIdentifier": "email-exist-with-oidc-strategy-lh-true@ory.sh",
"duplicate_identifier": "email-exist-with-oidc-strategy-lh-true@ory.sh",
"provider": "generic"
}
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,86 @@
{
"organization_id": null,
"type": "browser",
"active": "oidc",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "secondProvider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with secondProvider",
"type": "info",
"context": {
"provider": "secondProvider"
}
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"value": "email-exist-with-oidc-strategy-lh-true@ory.sh",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
}
],
"messages": [
{
"id": 1010016,
"text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.",
"type": "info",
"context": {
"available_credential_types": [
"oidc"
],
"available_providers": [
"secondProvider"
],
"duplicateIdentifier": "email-exist-with-oidc-strategy-lh-true@ory.sh",
"duplicate_identifier": "email-exist-with-oidc-strategy-lh-true@ory.sh",
"provider": "generic"
}
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,86 @@
{
"organization_id": null,
"type": "browser",
"active": "oidc",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "secondProvider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010018,
"text": "Confirm with secondProvider",
"type": "info",
"context": {
"provider": "secondProvider"
}
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"value": "email-exist-with-oidc-strategy-lh-true@ory.sh",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
}
],
"messages": [
{
"id": 1010016,
"text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.",
"type": "info",
"context": {
"available_credential_types": [
"oidc"
],
"available_providers": [
"secondProvider"
],
"duplicateIdentifier": "email-exist-with-oidc-strategy-lh-true@ory.sh",
"duplicate_identifier": "email-exist-with-oidc-strategy-lh-true@ory.sh",
"provider": "generic"
}
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,101 @@
{
"organization_id": null,
"type": "browser",
"active": "oidc",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"value": "email-exist-with-password-strategy-lh-true@ory.sh",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "current-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010022,
"text": "Sign in with password",
"type": "info"
}
}
}
],
"messages": [
{
"id": 1010016,
"text": "You tried to sign in with \"email-exist-with-password-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.",
"type": "info",
"context": {
"available_credential_types": [
"password"
],
"available_providers": [],
"duplicateIdentifier": "email-exist-with-password-strategy-lh-true@ory.sh",
"duplicate_identifier": "email-exist-with-password-strategy-lh-true@ory.sh",
"provider": "generic"
}
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,101 @@
{
"organization_id": null,
"type": "browser",
"active": "oidc",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"value": "email-exist-with-password-strategy-lh-true@ory.sh",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "current-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010022,
"text": "Sign in with password",
"type": "info"
}
}
}
],
"messages": [
{
"id": 1010016,
"text": "You tried to sign in with \"email-exist-with-password-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.",
"type": "info",
"context": {
"available_credential_types": [
"password"
],
"available_providers": [],
"duplicateIdentifier": "email-exist-with-password-strategy-lh-true@ory.sh",
"duplicate_identifier": "email-exist-with-password-strategy-lh-true@ory.sh",
"provider": "generic"
}
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,101 @@
{
"organization_id": null,
"type": "browser",
"active": "oidc",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"value": "email-exist-with-password-strategy-lh-true@ory.sh",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "current-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010022,
"text": "Sign in with password",
"type": "info"
}
}
}
],
"messages": [
{
"id": 1010016,
"text": "You tried to sign in with \"email-exist-with-password-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.",
"type": "info",
"context": {
"available_credential_types": [
"password"
],
"available_providers": [],
"duplicateIdentifier": "email-exist-with-password-strategy-lh-true@ory.sh",
"duplicate_identifier": "email-exist-with-password-strategy-lh-true@ory.sh",
"provider": "generic"
}
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,248 @@
{
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "valid",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with valid",
"type": "info",
"context": {
"provider": "valid",
"provider_id": "valid"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "valid2",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with valid2",
"type": "info",
"context": {
"provider": "valid2",
"provider_id": "valid2"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "secondProvider",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with secondProvider",
"type": "info",
"context": {
"provider": "secondProvider",
"provider_id": "secondProvider"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "claimsViaUserInfo",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with claimsViaUserInfo",
"type": "info",
"context": {
"provider": "claimsViaUserInfo",
"provider_id": "claimsViaUserInfo"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "neverPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with neverPKCE",
"type": "info",
"context": {
"provider": "neverPKCE",
"provider_id": "neverPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "autoPKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with autoPKCE",
"type": "info",
"context": {
"provider": "autoPKCE",
"provider_id": "autoPKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "forcePKCE",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with forcePKCE",
"type": "info",
"context": {
"provider": "forcePKCE",
"provider_id": "forcePKCE"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-acr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with aal2-acr",
"type": "info",
"context": {
"provider": "aal2-acr",
"provider_id": "aal2-acr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "aal2-amr",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with aal2-amr",
"type": "info",
"context": {
"provider": "aal2-amr",
"provider_id": "aal2-amr"
}
}
}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "invalid-issuer",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with invalid-issuer",
"type": "info",
"context": {
"provider": "invalid-issuer",
"provider_id": "invalid-issuer"
}
}
}
}
]
}

View File

@@ -0,0 +1,42 @@
{
"action": "",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "providerID",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010002,
"text": "Sign in with My Provider",
"type": "info",
"context": {
"provider": "My Provider",
"provider_id": "providerID"
}
}
}
}
]
}

View File

@@ -0,0 +1,8 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
const (
sessionName = "ory_kratos_oidc_auth_code_session"
)

View File

@@ -0,0 +1,40 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"io"
"net/http"
"github.com/pkg/errors"
"github.com/ory/herodot"
"github.com/ory/x/logrusx"
)
func ErrScopeMissing() *herodot.DefaultError {
return herodot.ErrBadRequest().
WithError("authentication failed because a required scope was not granted").
WithReasonf(`Unable to finish because one or more permissions were not granted. Please retry and accept all permissions.`)
}
func ErrIDTokenMissing() *herodot.DefaultError {
return herodot.ErrBadRequest().
WithError("authentication failed because id_token is missing").
WithReasonf(`Authentication failed because no id_token was returned. Please accept the "openid" permission and try again.`)
}
func logUpstreamError(l *logrusx.Logger, resp *http.Response) error {
if resp.StatusCode == http.StatusOK {
return nil
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024))
if err != nil {
l = l.WithError(err)
}
l.WithField("response_code", resp.StatusCode).WithField("response_body", string(body)).Error("The upstream OIDC provider returned a non 200 status code.")
return errors.WithStack(herodot.ErrUpstreamError().WithReasonf("OpenID Connect provider returned a %d status code but 200 is expected.", resp.StatusCode))
}

View File

@@ -0,0 +1,42 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"net/http"
"github.com/gofrs/uuid"
oidcv1 "github.com/ory/kratos/gen/oidc/v1"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/login"
)
// ValidateFlowForTest exposes the unexported flow resolver so tests in the
// external oidc_test package can assert that OIDC callback state dispatches
// to the correct flow kind (login, registration, settings).
func (s *Strategy) ValidateFlowForTest(ctx context.Context, r *http.Request, rid uuid.UUID, kind oidcv1.FlowKind) (flow.Flow, error) {
return s.validateFlow(ctx, r, rid, kind)
}
// ForwardErrorForTest exposes forwardError so tests can assert that
// pre-dispatch callback failures finish as a ready flow with a populated
// DebugPayload instead of panicking.
func (s *Strategy) ForwardErrorForTest(ctx context.Context, w http.ResponseWriter, r *http.Request, f flow.Flow, err error) {
s.forwardError(ctx, w, r, f, err)
}
// ProcessTestLoginForTest exposes processTestLogin so external tests can
// exercise the dry-run OIDC callback path without routing through the full
// callback HTTP pipeline.
func (s *Strategy) ProcessTestLoginForTest(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, claims *Claims, provider Provider) error {
return s.processTestLogin(ctx, w, r, f, claims, provider)
}
// FinishTestLoginForTest exposes finishTestLogin so external tests can
// assert the idempotency guard against an already-captured DebugPayload.
func (s *Strategy) FinishTestLoginForTest(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, dp *login.DebugPayload) error {
return s.finishTestLogin(ctx, w, r, f, dp)
}

View File

@@ -0,0 +1,140 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package fedcm
import "encoding/json"
type Provider struct {
// A full path of the IdP config file.
ConfigURL string `json:"config_url"`
// The RP's client identifier, issued by the IdP.
ClientID string `json:"client_id"`
// A random string to ensure the response is issued for this specific request.
// Prevents replay attacks.
Nonce string `json:"nonce"`
// By specifying one of login_hints values provided by the accounts endpoints,
// the FedCM dialog selectively shows the specified account.
LoginHint string `json:"login_hint,omitempty"`
// By specifying one of domain_hints values provided by the accounts endpoints,
// the FedCM dialog selectively shows the specified account.
DomainHint string `json:"domain_hint,omitempty"`
// Array of strings that specifies the user information ("name", " email",
// "picture") that RP needs IdP to share with them.
//
// Note: Field API is supported by Chrome 132 and later.
Fields []string `json:"fields,omitempty"`
// Custom object that allows to specify additional key-value parameters:
// - scope: A string value containing additional permissions that RP needs to
// request, for example " drive.readonly calendar.readonly"
// - nonce: A random string to ensure the response is issued for this specific
// request. Prevents replay attacks.
//
// Other custom key-value parameters.
//
// Note: parameters is supported from Chrome 132.
Parameters map[string]string `json:"parameters,omitempty"`
}
// CreateFedcmFlowResponse
//
// Contains a list of all available FedCM providers.
//
// swagger:model createFedcmFlowResponse
type CreateFedcmFlowResponse struct {
Providers []Provider `json:"providers"`
CSRFToken string `json:"csrf_token"`
}
// swagger:route GET /self-service/fed-cm/parameters frontend createFedcmFlow
//
// # Get FedCM Parameters
//
// This endpoint returns a list of all available FedCM providers. It is only supported on the Ory Network.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: createFedcmFlowResponse
// 400: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-public-high
type UpdateFedcmFlowBody struct {
// Token contains the result of `navigator.credentials.get`.
//
// required: true
Token string `json:"token"`
// Nonce is the nonce that was used in the `navigator.credentials.get` call. If
// specified, it must match the `nonce` claim in the token.
//
// required: false
Nonce string `json:"nonce"`
// CSRFToken is the anti-CSRF token.
//
// required: true
CSRFToken string `json:"csrf_token"`
// Transient data to pass along to any webhooks.
//
// required: false
TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
}
// swagger:parameters updateFedcmFlow
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type updateFedcmFlow struct {
// in: body
// required: true
Body UpdateFedcmFlowBody
}
// swagger:route POST /self-service/fed-cm/token frontend updateFedcmFlow
//
// # Submit a FedCM token
//
// Use this endpoint to submit a token from a FedCM provider through
// `navigator.credentials.get` and log the user in. The parameters from
// `navigator.credentials.get` must have come from `GET
// /self-service/fed-cm/parameters`.
//
// Consumes:
// - application/json
// - application/x-www-form-urlencoded
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Header:
// - Set-Cookie
//
// Responses:
// 200: successfulNativeLogin
// 303: emptyResponse
// 400: loginFlow
// 410: errorGeneric
// 422: errorBrowserLocationChangeRequired
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-public-low

View File

@@ -0,0 +1,41 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"encoding/json"
"dario.cat/mergo"
"github.com/ory/kratos/identity"
)
// merge merges the JSON messages in a and b. If a value is defined in both a and b, a wins.
func merge(a, b json.RawMessage) (identity.Traits, error) {
if len(a) == 0 || string(a) == "{}" {
return identity.Traits(b), nil
}
var aMap, bMap map[string]any
if err := json.Unmarshal(a, &aMap); err != nil {
return nil, err
}
if err := json.Unmarshal(b, &bMap); err != nil {
return nil, err
}
// values in aMap (src) win.
if err := mergo.Merge(&bMap, &aMap, mergo.WithOverride); err != nil {
return nil, err
}
traits, err := json.Marshal(bMap)
if err != nil {
return nil, err
}
return traits, nil
}

View File

@@ -0,0 +1,49 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMerge(t *testing.T) {
for k, tc := range []struct {
schema string
form json.RawMessage
op json.RawMessage
expect json.RawMessage
}{
{
form: json.RawMessage("{}"),
op: json.RawMessage("{}"),
expect: json.RawMessage("{}"),
},
{
form: json.RawMessage(`{"foo": "bar", "bool": true, "opv": "blubb"}`),
op: json.RawMessage(`{"baz":"bar","opv":"bla"}`),
expect: json.RawMessage(`{"foo":"bar","baz":"bar","bool":true,"opv":"blubb"}`),
},
{
form: json.RawMessage(`{"bool": true, "opv": "blubb"}`),
op: json.RawMessage(`{"foo":"bar","baz":"bar","opv":"bla"}`),
expect: json.RawMessage(`{"foo":"bar","baz":"bar","bool":true,"opv":"blubb"}`),
},
{
form: json.RawMessage(`{"bool": true, "opv": "blubb"}`),
op: json.RawMessage(`{"foo":"bar","baz":"bar","opv":"bla"}`),
expect: json.RawMessage(`{"foo":"bar","baz":"bar","bool":true,"opv":"blubb"}`),
},
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
got, err := merge(tc.form, tc.op)
require.NoError(t, err)
assert.JSONEq(t, string(tc.expect), string(got))
})
}
}

View File

@@ -0,0 +1,104 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
)
func TestJsonEqual(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
a, b json.RawMessage
expected bool
}{
{
name: "both nil",
a: nil,
b: nil,
expected: true,
},
{
name: "both empty",
a: json.RawMessage{},
b: json.RawMessage{},
expected: true,
},
{
name: "one nil one empty",
a: nil,
b: json.RawMessage{},
expected: true,
},
{
name: "one nil one non-empty",
a: nil,
b: json.RawMessage(`{}`),
expected: false,
},
{
name: "identical objects",
a: json.RawMessage(`{"name":"alice","age":30}`),
b: json.RawMessage(`{"name":"alice","age":30}`),
expected: true,
},
{
name: "same content different key order",
a: json.RawMessage(`{"name":"alice","age":30}`),
b: json.RawMessage(`{"age":30,"name":"alice"}`),
expected: true,
},
{
name: "same content different whitespace",
a: json.RawMessage(`{"name": "alice"}`),
b: json.RawMessage(`{"name":"alice"}`),
expected: true,
},
{
name: "different values",
a: json.RawMessage(`{"name":"alice"}`),
b: json.RawMessage(`{"name":"bob"}`),
expected: false,
},
{
name: "nested objects equal",
a: json.RawMessage(`{"user":{"name":"alice","groups":["admin","user"]}}`),
b: json.RawMessage(`{"user":{"groups":["admin","user"],"name":"alice"}}`),
expected: true,
},
{
name: "nested objects different",
a: json.RawMessage(`{"user":{"groups":["admin"]}}`),
b: json.RawMessage(`{"user":{"groups":["admin","user"]}}`),
expected: false,
},
{
name: "extra key",
a: json.RawMessage(`{"name":"alice"}`),
b: json.RawMessage(`{"name":"alice","age":30}`),
expected: false,
},
{
name: "invalid json a",
a: json.RawMessage(`{invalid`),
b: json.RawMessage(`{"name":"alice"}`),
expected: false,
},
{
name: "invalid json b",
a: json.RawMessage(`{"name":"alice"}`),
b: json.RawMessage(`{invalid`),
expected: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tc.expected, jsonEqual(tc.a, tc.b))
})
}
}

View File

@@ -0,0 +1,17 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
)
func NewLinkNode(providerID, providerLabel string) *node.Node {
return node.NewInputField("link", providerID, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateLinkOIDC(providerLabel))
}
func NewUnlinkNode(providerID, providerLabel string) *node.Node {
return node.NewInputField("unlink", providerID, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC(providerLabel))
}

View File

@@ -0,0 +1,75 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0
// Package oidcerr defines structured errors raised by the OIDC strategy.
//
// The Step type and its constants name the high-level callback phase that
// failed. The test login flow's debug renderer buckets failures by Step;
// non-test flows ignore it.
package oidcerr
import "errors"
// Step identifies the OIDC callback phase that produced an error.
type Step string
const (
StepStateValidate Step = "state_validate"
StepProviderDenied Step = "provider_denied"
StepTokenExchange Step = "token_exchange"
StepIDTokenVerify Step = "id_token_verify"
StepClaimsDecode Step = "claims_decode"
StepCallback Step = "callback"
)
// stepReasons crosswalks a Step to the reason string surfaced in the test
// debug payload's StepError.Reason field.
var stepReasons = map[Step]string{
StepStateValidate: "state_invalid",
StepProviderDenied: "access_denied",
StepTokenExchange: "exchange_failed",
StepIDTokenVerify: "verification_failed",
StepClaimsDecode: "claims_invalid",
StepCallback: "callback_failed",
}
// Reason returns the reason string surfaced in the test debug payload's
// StepError.Reason field. Defined on Step (not *CallbackStepError) because
// herodot.ReasonCarrier picks up any error type with a Reason() method and
// would clobber the herodot DefaultError reason on the wire.
func (s Step) Reason() string {
return stepReasons[s]
}
// CallbackStepError tags an OIDC callback error with the step it belongs to.
// It wraps the underlying error transparently: errors.As walks past it to
// find the inner *herodot.DefaultError, so existing handlers and writers
// behave identically.
type CallbackStepError struct {
Step Step
err error
}
func (e *CallbackStepError) Error() string { return e.err.Error() }
func (e *CallbackStepError) Unwrap() error { return e.err }
// Wrap tags err with a step hint. Pass-through for nil. The wrap is the
// outermost layer so callers that errors.As on *herodot.DefaultError keep
// working without changes.
func Wrap(step Step, err error) error {
if err == nil {
return nil
}
return &CallbackStepError{Step: step, err: err}
}
// Unwrap walks err's chain looking for a *CallbackStepError tag. It returns
// a pointer to the tagged error if found, or a fresh fallback tagged with
// StepCallback otherwise. The returned value is never nil — callers can
// always read e.Step / e.Step.Reason() without a nil check.
func Unwrap(err error) *CallbackStepError {
if se, ok := errors.AsType[*CallbackStepError](err); ok {
return se
}
return &CallbackStepError{Step: StepCallback, err: err}
}

View File

@@ -0,0 +1,87 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidcerr_test
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/ory/herodot"
"github.com/ory/kratos/selfservice/strategy/oidc/oidcerr"
)
func TestUnwrap(t *testing.T) {
t.Parallel()
cases := []struct {
name string
err error
wantStep oidcerr.Step
wantReason string
}{
{
name: "state validate",
err: oidcerr.Wrap(oidcerr.StepStateValidate, herodot.ErrBadRequest().WithReason("invalid state")),
wantStep: oidcerr.StepStateValidate,
wantReason: "state_invalid",
},
{
name: "provider denied",
err: oidcerr.Wrap(oidcerr.StepProviderDenied, herodot.ErrBadRequest().WithReason("access_denied: user refused")),
wantStep: oidcerr.StepProviderDenied,
wantReason: "access_denied",
},
{
name: "token exchange",
err: oidcerr.Wrap(oidcerr.StepTokenExchange, errors.New("oauth2: cannot fetch token")),
wantStep: oidcerr.StepTokenExchange,
wantReason: "exchange_failed",
},
{
name: "id_token verify",
err: oidcerr.Wrap(oidcerr.StepIDTokenVerify, herodot.ErrForbidden().WithReason("Could not verify id_token")),
wantStep: oidcerr.StepIDTokenVerify,
wantReason: "verification_failed",
},
{
name: "claims decode",
err: oidcerr.Wrap(oidcerr.StepClaimsDecode, herodot.ErrUpstreamError().WithReason("provider did not return a subject")),
wantStep: oidcerr.StepClaimsDecode,
wantReason: "claims_invalid",
},
{
name: "explicit callback bucket",
err: oidcerr.Wrap(oidcerr.StepCallback, errors.New("missing code")),
wantStep: oidcerr.StepCallback,
wantReason: "callback_failed",
},
{
name: "untagged error falls back to callback",
err: errors.New("some unrelated failure"),
wantStep: oidcerr.StepCallback,
wantReason: "callback_failed",
},
{
name: "untagged herodot error falls back to callback",
err: herodot.ErrInternalServerError().WithReason("oops"),
wantStep: oidcerr.StepCallback,
wantReason: "callback_failed",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
e := oidcerr.Unwrap(tc.err)
assert.Equal(t, tc.wantStep, e.Step)
assert.Equal(t, tc.wantReason, e.Step.Reason())
})
}
}
func TestWrap_NilPassthrough(t *testing.T) {
t.Parallel()
assert.Nil(t, oidcerr.Wrap(oidcerr.StepCallback, nil))
}

View File

@@ -0,0 +1,80 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"slices"
gooidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/pkg/errors"
"golang.org/x/oauth2"
oidcv1 "github.com/ory/kratos/gen/oidc/v1"
"github.com/ory/x/httpx"
"github.com/ory/x/logrusx"
)
type pkceDependencies interface {
logrusx.Provider
httpx.ClientProvider
}
func PKCEChallenge(s *oidcv1.State) []oauth2.AuthCodeOption {
if s.GetPkceVerifier() == "" {
return nil
}
return []oauth2.AuthCodeOption{oauth2.S256ChallengeOption(s.GetPkceVerifier())}
}
func PKCEVerifier(s *oidcv1.State) []oauth2.AuthCodeOption {
if s.GetPkceVerifier() == "" {
return nil
}
return []oauth2.AuthCodeOption{oauth2.VerifierOption(s.GetPkceVerifier())}
}
func maybePKCE(ctx context.Context, d pkceDependencies, _p Provider) (verifier string) {
if _p.Config().PKCE == "never" {
return ""
}
p, ok := _p.(OAuth2Provider)
if !ok {
return ""
}
if p.Config().PKCE != "force" {
// autodiscover PKCE support
pkceSupported, err := discoverPKCE(ctx, d, p)
if err != nil {
d.Logger().WithError(err).Warnf("Failed to autodiscover PKCE support for provider %q. Continuing without PKCE.", p.Config().ID)
return ""
}
if !pkceSupported {
d.Logger().Infof("Provider %q does not advertise support for PKCE. Continuing without PKCE.", p.Config().ID)
return ""
}
}
return oauth2.GenerateVerifier()
}
func discoverPKCE(ctx context.Context, d pkceDependencies, p OAuth2Provider) (pkceSupported bool, err error) {
if p.Config().IssuerURL == "" {
return false, errors.New("Issuer URL must be set to autodiscover PKCE support")
}
ctx = gooidc.ClientContext(ctx, d.HTTPClient(ctx).HTTPClient)
gp, err := gooidc.NewProvider(ctx, p.Config().IssuerURL)
if err != nil {
return false, errors.Wrap(err, "failed to initialize provider")
}
var claims struct {
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
if err := gp.Claims(&claims); err != nil {
return false, errors.Wrap(err, "failed to deserialize provider claims")
}
return slices.Contains(claims.CodeChallengeMethodsSupported, "S256"), nil
}

View File

@@ -0,0 +1,100 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc_test
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
oidcv1 "github.com/ory/kratos/gen/oidc/v1"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/selfservice/strategy/oidc"
"github.com/ory/kratos/x"
)
func TestPKCESupport(t *testing.T) {
t.Parallel()
supported := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, `{"issuer": "http://%s", "code_challenge_methods_supported":["S256"]}`, r.Host)
}))
t.Cleanup(supported.Close)
notSupported := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, `{"issuer": "http://%s", "code_challenge_methods_supported": ["plain"]}`, r.Host)
}))
t.Cleanup(notSupported.Close)
_, reg := pkg.NewFastRegistryWithMocks(t)
strat := oidc.NewStrategy(reg)
for _, tc := range []struct {
c *oidc.Configuration
pkce bool
}{
{c: &oidc.Configuration{IssuerURL: supported.URL, PKCE: "force"}, pkce: true},
{c: &oidc.Configuration{IssuerURL: supported.URL, PKCE: "never"}, pkce: false},
{c: &oidc.Configuration{IssuerURL: supported.URL, PKCE: "auto"}, pkce: true},
{c: &oidc.Configuration{IssuerURL: supported.URL, PKCE: ""}, pkce: true}, // same as auto
{c: &oidc.Configuration{IssuerURL: notSupported.URL, PKCE: "force"}, pkce: true},
{c: &oidc.Configuration{IssuerURL: notSupported.URL, PKCE: "never"}, pkce: false},
{c: &oidc.Configuration{IssuerURL: notSupported.URL, PKCE: "auto"}, pkce: false},
{c: &oidc.Configuration{IssuerURL: notSupported.URL, PKCE: ""}, pkce: false}, // same as auto
{c: &oidc.Configuration{IssuerURL: "", PKCE: "force"}, pkce: true},
{c: &oidc.Configuration{IssuerURL: "", PKCE: "never"}, pkce: false},
{c: &oidc.Configuration{IssuerURL: "", PKCE: "auto"}, pkce: false},
{c: &oidc.Configuration{IssuerURL: "", PKCE: ""}, pkce: false}, // same as auto
} {
provider := oidc.NewProviderGenericOIDC(tc.c, reg)
flow := &login.Flow{
ID: x.NewUUID(),
}
stateParam, pkce, err := strat.GenerateState(t.Context(), provider, flow, "https://testhost")
require.NoError(t, err)
require.NotEmpty(t, stateParam)
state, err := oidc.DecryptState(t.Context(), reg.Cipher(t.Context()), stateParam)
require.NoError(t, err)
assert.Equal(t, oidcv1.FlowKind_FLOW_KIND_LOGIN, state.FlowKind)
assert.Equal(t, "https://testhost", state.RequestBaseUrl)
if tc.pkce {
require.NotEmpty(t, pkce)
require.NotEmpty(t, oidc.PKCEVerifier(state))
} else {
require.Empty(t, pkce)
require.Empty(t, oidc.PKCEVerifier(state))
}
}
t.Run("OAuth1", func(t *testing.T) {
for _, provider := range []oidc.Provider{
oidc.NewProviderX(&oidc.Configuration{IssuerURL: supported.URL, PKCE: "force"}, reg),
oidc.NewProviderX(&oidc.Configuration{IssuerURL: supported.URL, PKCE: "never"}, reg),
oidc.NewProviderX(&oidc.Configuration{IssuerURL: supported.URL, PKCE: "auto"}, reg),
} {
stateParam, pkce, err := strat.GenerateState(t.Context(), provider, &registration.Flow{ID: x.NewUUID()}, "https://testhost")
require.NoError(t, err)
require.NotEmpty(t, stateParam)
assert.Empty(t, pkce)
state, err := oidc.DecryptState(t.Context(), reg.Cipher(t.Context()), stateParam)
require.NoError(t, err)
assert.Empty(t, oidc.PKCEVerifier(state))
assert.Equal(t, oidcv1.FlowKind_FLOW_KIND_REGISTRATION, state.FlowKind)
assert.Equal(t, "https://testhost", state.RequestBaseUrl)
}
})
}

View File

@@ -0,0 +1,165 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"encoding/json"
"net/http"
"net/url"
"strings"
"github.com/dghubble/oauth1"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/ory/herodot"
"github.com/ory/kratos/selfservice/strategy/oidc/oidcerr"
"github.com/ory/kratos/x"
)
type (
Provider interface {
Config() *Configuration
}
OAuth2Provider interface {
Provider
AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption
OAuth2(ctx context.Context) (*oauth2.Config, error)
Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error)
}
OAuth1Provider interface {
Provider
OAuth1(ctx context.Context) *oauth1.Config
AuthURL(ctx context.Context, state string) (string, error)
Claims(ctx context.Context, token *oauth1.Token) (*Claims, error)
ExchangeToken(ctx context.Context, req *http.Request) (*oauth1.Token, error)
}
)
type OAuth2TokenExchanger interface {
Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
}
type IDTokenVerifier interface {
Verify(ctx context.Context, rawIDToken string) (*Claims, error)
}
type NonceValidationSkipper interface {
CanSkipNonce(*Claims) bool
}
type Claims struct {
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Object string `json:"oid,omitempty"`
Name string `json:"name,omitempty"`
GivenName string `json:"given_name,omitempty"`
FamilyName string `json:"family_name,omitempty"`
LastName string `json:"last_name,omitempty"`
MiddleName string `json:"middle_name,omitempty"`
Nickname string `json:"nickname,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Profile string `json:"profile,omitempty"`
Picture string `json:"picture,omitempty"`
Website string `json:"website,omitempty"`
Email string `json:"email,omitempty"`
// ConvertibleBoolean is used as Apple casually sends the email_verified field as a string.
EmailVerified x.ConvertibleBoolean `json:"email_verified,omitempty"`
Gender string `json:"gender,omitempty"`
Birthdate string `json:"birthdate,omitempty"`
Zoneinfo string `json:"zoneinfo,omitempty"`
Locale Locale `json:"locale,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"`
PhoneNumberVerified bool `json:"phone_number_verified,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
HD string `json:"hd,omitempty"`
Team string `json:"team,omitempty"`
Nonce string `json:"nonce,omitempty"`
NonceSupported bool `json:"nonce_supported,omitempty"`
// ACR is the Authentication Context Class Reference reported by the
// upstream OIDC provider. See OpenID Connect Core 1.0, Section 2.
ACR string `json:"acr,omitempty"`
// AMR is the list of Authentication Methods References reported by the
// upstream OIDC provider. See OpenID Connect Core 1.0, Section 2 and
// RFC 8176.
AMR []string `json:"amr,omitempty"`
RawClaims map[string]any `json:"raw_claims,omitempty"`
}
type Locale string
func (l *Locale) UnmarshalJSON(data []byte) error {
var linkedInLocale struct {
Language string `json:"language"`
Country string `json:"country"`
}
if err := json.Unmarshal(data, &linkedInLocale); err == nil {
switch {
case linkedInLocale.Language == "":
*l = Locale(linkedInLocale.Country)
case linkedInLocale.Country == "":
*l = Locale(linkedInLocale.Language)
default:
*l = Locale(strings.Join([]string{linkedInLocale.Language, linkedInLocale.Country}, "-"))
}
return nil
}
return json.Unmarshal(data, (*string)(l))
}
// Validate checks if the claims are valid.
func (c *Claims) Validate() error {
if c.Subject == "" {
return oidcerr.Wrap(oidcerr.StepClaimsDecode, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("provider did not return a subject")))
}
if c.Issuer == "" {
return oidcerr.Wrap(oidcerr.StepClaimsDecode, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("issuer not set in claims")))
}
return nil
}
// UpstreamParameters returns a list of oauth2.AuthCodeOption based on the upstream parameters.
//
// Only allowed parameters are returned and the rest is ignored.
// Allowed parameters are also defined in the `oidc/.schema/link.schema.json` file, however,
// this function also validates the parameters to prevent any potential security issues.
//
// Allowed parameters are:
// - `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`.
// - `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`.
// - `auth_type` (string): The `auth_type` parameter specifies the requested authentication features (as a comma-separated list), e.g. `reauthenticate`.
// - `acr_values` (string): The `acr_values` specifies the Authentication Context Class Reference values for the authorization request.
//
// `login_hint` is intentionally not forwarded. A caller-supplied value cannot be
// trusted, and on IdPs that act on it (Google, Microsoft, Auth0, …) it can steer
// account, tenant, or OAuth client selection and enable a client-swap attack
// (HackerOne #3239672, ory-corp/cloud#8955).
func UpstreamParameters(upstreamParameters map[string]string) []oauth2.AuthCodeOption {
// validation of upstream parameters are already handled in the `oidc/.schema/link.schema.json` and `oidc/.schema/settings.schema.json` file.
// `upstreamParameters` will always only contain allowed parameters based on the configuration.
// we double-check the parameters here to prevent any potential security issues.
allowedParameters := map[string]struct{}{
"hd": {},
"prompt": {},
"auth_type": {},
"acr_values": {},
}
var params []oauth2.AuthCodeOption
for up, v := range upstreamParameters {
if _, ok := allowedParameters[up]; !ok {
continue
}
params = append(params, oauth2.SetAuthURLParam(up, v))
}
return params
}

View File

@@ -0,0 +1,141 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"slices"
"testing"
"github.com/hashicorp/go-retryablehttp"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"golang.org/x/oauth2/amazon"
"github.com/ory/herodot"
"github.com/ory/x/httpx"
"github.com/ory/x/otelx"
)
var _ OAuth2Provider = (*ProviderAmazon)(nil)
var amazonSupportedScopes = []string{"profile", "profile:user_id", "postal_code"}
type ProviderAmazon struct {
*ProviderGenericOIDC
amazonProfileURL string // Only overriden in tests.
}
type amazonProfileResponse struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Name string `json:"name"`
PostalCode string `json:"postal_code"`
}
func NewProviderAmazon(
config *Configuration,
reg Dependencies,
) Provider {
config.IssuerURL = amazon.Endpoint.AuthURL
const amazonProfileURL string = "https://api.amazon.com/user/profile"
return &ProviderAmazon{
ProviderGenericOIDC: &ProviderGenericOIDC{
config: config,
reg: reg,
},
amazonProfileURL: amazonProfileURL,
}
}
func (p *ProviderAmazon) SetProfileURL(t *testing.T, url string) {
p.amazonProfileURL = url
}
func (p *ProviderAmazon) Config() *Configuration {
return p.config
}
func (p *ProviderAmazon) oauth2(ctx context.Context) *oauth2.Config {
return &oauth2.Config{
ClientID: p.config.ClientID,
ClientSecret: p.config.ClientSecret,
Endpoint: amazon.Endpoint,
Scopes: p.config.Scope,
RedirectURL: p.config.Redir(p.reg.Config().OIDCRedirectURIBase(ctx)),
}
}
func (p *ProviderAmazon) validateConfiguration() error {
for _, s := range p.config.Scope {
if !slices.Contains(amazonSupportedScopes, s) {
return errors.WithStack(
herodot.ErrMisconfiguration().WithReasonf("scope %s not supported. Supported: %+v", s, amazonSupportedScopes))
}
}
if p.config.PKCE == "auto" {
return errors.WithStack(herodot.ErrMisconfiguration().WithReason("pkce:auto is not supported because Amazon does not support PKCE discovery"))
}
return nil
}
func (p *ProviderAmazon) OAuth2(ctx context.Context) (*oauth2.Config, error) {
// This is as good a place as any to validate the configuration.
if err := p.validateConfiguration(); err != nil {
return nil, err
}
return p.oauth2(ctx), nil
}
func (p *ProviderAmazon) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
return []oauth2.AuthCodeOption{}
}
func (p *ProviderAmazon) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *Claims, err error) {
ctx, span := p.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderAmazon.Claims")
defer otelx.End(span, &err)
ctx, client := httpx.SetOAuth2(ctx, p.reg.HTTPClient(ctx), p.oauth2(ctx), exchange)
req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodGet, p.amazonProfileURL, nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithReason("failed to create HTTP request").WithDetail("url", p.amazonProfileURL).WithError(err.Error()))
}
req.Header.Set("x-amz-access-token", exchange.AccessToken)
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithReason("failed to make HTTP request").WithDetail("url", p.amazonProfileURL).WithError(err.Error()))
}
defer func() { _ = resp.Body.Close() }()
body := io.LimitReader(resp.Body, 64*1024) // 64 KiB
if resp.StatusCode != http.StatusOK {
rawResponse, _ := io.ReadAll(body)
return nil, errors.WithStack(herodot.ErrUpstreamError().WithReason("non 200 response").WithDetail("url", p.amazonProfileURL).WithDetail("external_error", string(rawResponse)).
WithDetail("external_status_code", resp.StatusCode))
}
profile := amazonProfileResponse{}
if err := json.NewDecoder(body).Decode(&profile); err != nil {
rawResponse, _ := io.ReadAll(body)
return nil, errors.WithStack(herodot.ErrUpstreamError().WithDetail("url", p.amazonProfileURL).WithDetail("raw_response", rawResponse).WithError(err.Error()))
}
claims := &Claims{
Subject: profile.UserID,
Issuer: amazon.Endpoint.TokenURL,
Name: profile.Name,
Email: profile.Email,
Zoneinfo: profile.PostalCode,
}
return claims, nil
}

View File

@@ -0,0 +1,59 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"golang.org/x/oauth2/amazon"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/selfservice/strategy/oidc"
)
func TestAmazonOidcClaims(t *testing.T) {
t.Parallel()
handler := http.NewServeMux()
expectedAccessToken := "my-access-token"
handler.HandleFunc("GET /user/profile", func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("x-amz-access-token")
if token != expectedAccessToken {
w.WriteHeader(http.StatusForbidden)
return
}
// From the official docs: https://developer.amazon.com/docs/login-with-amazon/customer-profile.html .
userProfile := `
{
"user_id" : "amzn1.account.K2LI23KL2LK2",
"email" : "johndoe@gmail.com",
"name" : "John Doe",
"postal_code": "98101"
}
`
_, err := w.Write([]byte(userProfile))
require.NoError(t, err)
})
amazonAPI := httptest.NewServer(handler)
t.Cleanup(amazonAPI.Close)
_, reg := pkg.NewFastRegistryWithMocks(t)
p := oidc.NewProviderAmazon(&oidc.Configuration{}, reg).(*oidc.ProviderAmazon)
p.SetProfileURL(t, amazonAPI.URL+"/user/profile")
claims, err := p.Claims(t.Context(), &oauth2.Token{AccessToken: expectedAccessToken}, nil)
require.NoError(t, err)
require.NotNil(t, claims)
require.Equal(t, claims.Subject, "amzn1.account.K2LI23KL2LK2")
require.Equal(t, claims.Issuer, amazon.Endpoint.TokenURL)
require.Equal(t, claims.Name, "John Doe")
require.Equal(t, claims.Email, "johndoe@gmail.com")
require.Equal(t, claims.Zoneinfo, "98101")
}

View File

@@ -0,0 +1,175 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"crypto/ecdsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"net/url"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/golang-jwt/jwt/v4"
"github.com/pkg/errors"
"golang.org/x/oauth2"
)
type ProviderApple struct {
*ProviderGenericOIDC
JWKSUrl string
}
var _ OAuth2Provider = (*ProviderApple)(nil)
func NewProviderApple(
config *Configuration,
reg Dependencies,
) Provider {
config.IssuerURL = "https://appleid.apple.com"
return &ProviderApple{
ProviderGenericOIDC: &ProviderGenericOIDC{
config: config,
reg: reg,
},
JWKSUrl: "https://appleid.apple.com/auth/keys",
}
}
func (a *ProviderApple) newClientSecret() (string, error) {
// decode the pem format
block, _ := pem.Decode([]byte(a.config.PrivateKey))
if block == nil || block.Type != "PRIVATE KEY" {
return "", errors.New("failed to decode PEM block containing private key")
}
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return "", errors.Wrap(err, "Private key decoding failed")
}
privateKey, ok := parsedKey.(*ecdsa.PrivateKey)
if !ok {
return "", errors.New("Private key is not ecdsa key")
}
now := time.Now()
expirationTime := time.Now().Add(5 * time.Minute)
appleToken := jwt.NewWithClaims(jwt.SigningMethodES256,
jwt.RegisteredClaims{
Audience: []string{"https://appleid.apple.com"},
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(now),
Issuer: a.config.TeamId,
Subject: a.config.ClientID,
})
appleToken.Header["kid"] = a.config.PrivateKeyId
return appleToken.SignedString(privateKey)
}
func (a *ProviderApple) oauth2(ctx context.Context) (*oauth2.Config, error) {
// Apple requires a JWT token that acts as a client secret
secret, err := a.newClientSecret()
if err != nil {
return nil, err
}
a.config.ClientSecret = secret
endpoint := oauth2.Endpoint{
AuthURL: "https://appleid.apple.com/auth/authorize",
TokenURL: "https://appleid.apple.com/auth/token",
AuthStyle: oauth2.AuthStyleInParams,
}
return a.oauth2ConfigFromEndpoint(ctx, endpoint), nil
}
func (a *ProviderApple) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return a.oauth2(ctx)
}
func (a *ProviderApple) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
var options []oauth2.AuthCodeOption
if isForced(r) {
options = append(options, oauth2.SetAuthURLParam("prompt", "login"))
}
if len(a.config.RequestedClaims) != 0 {
options = append(options, oauth2.SetAuthURLParam("claims", string(a.config.RequestedClaims)))
}
// When requesting email or name, Apple requires the form_post response mode.
// This also means the return url will be called by Apple using POST method.
for _, scope := range a.config.Scope {
if scope == "email" || scope == "name" {
options = append(options, oauth2.SetAuthURLParam("response_mode", "form_post"))
break
}
}
return options
}
func (a *ProviderApple) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) {
claims, err := a.ProviderGenericOIDC.Claims(ctx, exchange, query)
if err != nil {
return claims, err
}
a.DecodeQuery(query, claims)
return claims, nil
}
// DecodeQuery decodes extra user info from Apple into the given `Claims`.
// The info is sent as an extra query parameter to the redirect URL.
// See https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple#3331292
// Note that there's no way to make sure the info hasn't been tampered with.
func (a *ProviderApple) DecodeQuery(query url.Values, claims *Claims) {
var user struct {
Name *struct {
FirstName *string `json:"firstName"`
LastName *string `json:"lastName"`
} `json:"name"`
}
if err := json.Unmarshal([]byte(query.Get("user")), &user); err == nil {
if name := user.Name; name != nil {
if firstName := name.FirstName; firstName != nil {
if claims.GivenName == "" {
claims.GivenName = *firstName
}
}
if lastName := name.LastName; lastName != nil {
if claims.LastName == "" {
claims.LastName = *lastName
}
if claims.FamilyName == "" {
claims.FamilyName = *lastName
}
}
}
}
}
var _ IDTokenVerifier = new(ProviderApple)
const issuerURLApple = "https://appleid.apple.com"
func (a *ProviderApple) Verify(ctx context.Context, rawIDToken string) (*Claims, error) {
keySet := oidc.NewRemoteKeySet(ctx, a.JWKSUrl)
ctx = oidc.ClientContext(ctx, a.reg.HTTPClient(ctx).HTTPClient)
return verifyToken(ctx, keySet, a.config, rawIDToken, issuerURLApple)
}
var _ NonceValidationSkipper = new(ProviderApple)
func (a *ProviderApple) CanSkipNonce(c *Claims) bool {
// Apple sets nonce_supported=true when the device requires nonce validation,
// so the nonce check may only be skipped when nonce_supported is false.
return !c.NonceSupported
}

View File

@@ -0,0 +1,146 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc_test
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
_ "embed"
"github.com/golang-jwt/jwt/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/selfservice/strategy/oidc"
)
func TestDecodeQuery(t *testing.T) {
query := url.Values{
"user": []string{`{"name": {"firstName": "first", "lastName": "last"}, "email": "email@email.com"}`},
}
for k, tc := range []struct {
claims *oidc.Claims
familyName string
givenName string
lastName string
}{
{claims: &oidc.Claims{}, familyName: "last", givenName: "first", lastName: "last"},
{claims: &oidc.Claims{FamilyName: "fam"}, familyName: "fam", givenName: "first", lastName: "last"},
{claims: &oidc.Claims{FamilyName: "fam", GivenName: "giv"}, familyName: "fam", givenName: "giv", lastName: "last"},
{claims: &oidc.Claims{FamilyName: "fam", GivenName: "giv", LastName: "las"}, familyName: "fam", givenName: "giv", lastName: "las"},
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
a := oidc.NewProviderApple(&oidc.Configuration{}, nil).(*oidc.ProviderApple)
a.DecodeQuery(query, tc.claims)
assert.Equal(t, tc.familyName, tc.claims.FamilyName)
assert.Equal(t, tc.givenName, tc.claims.GivenName)
assert.Equal(t, tc.lastName, tc.claims.LastName)
// Never extract email from the query, as the same info can be extracted and verified from the ID token.
assert.Empty(t, tc.claims.Email)
})
}
}
func TestAppleVerify(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
_, _ = w.Write(publicJWKS)
}))
tsOtherJWKS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
_, _ = w.Write(publicJWKS2)
}))
makeClaims := func(aud string) jwt.RegisteredClaims {
return jwt.RegisteredClaims{
Issuer: "https://appleid.apple.com",
Subject: "acme@ory.sh",
Audience: jwt.ClaimStrings{aud},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
}
}
t.Run("case=successful verification", func(t *testing.T) {
_, reg := pkg.NewFastRegistryWithMocks(t)
apple := oidc.NewProviderApple(&oidc.Configuration{
ClientID: "com.example.app",
}, reg).(*oidc.ProviderApple)
apple.JWKSUrl = ts.URL
token := createIDToken(t, makeClaims("com.example.app"))
c, err := apple.Verify(context.Background(), token)
require.NoError(t, err)
assert.Equal(t, "acme@ory.sh", c.Email)
assert.Equal(t, "acme@ory.sh", c.Subject)
assert.Equal(t, "https://appleid.apple.com", c.Issuer)
})
t.Run("case=fails due to client_id mismatch", func(t *testing.T) {
_, reg := pkg.NewFastRegistryWithMocks(t)
apple := oidc.NewProviderApple(&oidc.Configuration{
ClientID: "com.example.app",
}, reg).(*oidc.ProviderApple)
apple.JWKSUrl = ts.URL
token := createIDToken(t, makeClaims("com.different-example.app"))
_, err := apple.Verify(context.Background(), token)
require.Error(t, err)
assert.Equal(t, `token audience didn't match allowed audiences: [com.example.app] oidc: expected audience "com.example.app" got ["com.different-example.app"]`, err.Error())
})
t.Run("case=fails due to jwks mismatch", func(t *testing.T) {
_, reg := pkg.NewFastRegistryWithMocks(t)
apple := oidc.NewProviderApple(&oidc.Configuration{
ClientID: "com.example.app",
}, reg).(*oidc.ProviderApple)
apple.JWKSUrl = tsOtherJWKS.URL
token := createIDToken(t, makeClaims("com.example.app"))
_, err := apple.Verify(context.Background(), token)
require.Error(t, err)
assert.Equal(t, "failed to verify signature: failed to verify id token signature", err.Error())
})
t.Run("case=succeedes with additional id token audience", func(t *testing.T) {
_, reg := pkg.NewFastRegistryWithMocks(t)
apple := oidc.NewProviderApple(&oidc.Configuration{
ClientID: "something.else.app",
AdditionalIDTokenAudiences: []string{"com.example.app"},
}, reg).(*oidc.ProviderApple)
apple.JWKSUrl = ts.URL
token := createIDToken(t, makeClaims("com.example.app"))
_, err := apple.Verify(context.Background(), token)
require.NoError(t, err)
})
}
func TestAppleCanSkipNonce(t *testing.T) {
apple := oidc.NewProviderApple(&oidc.Configuration{}, nil).(*oidc.ProviderApple)
for _, tc := range []struct {
name string
nonceSupported bool
wantSkip bool
}{
// Apple sets nonce_supported=true when the device requires a nonce,
// so nonce validation must NOT be skipped in that case.
{name: "nonce supported means nonce is mandatory", nonceSupported: true, wantSkip: false},
// Legacy devices report nonce_supported=false, so nonce validation may be skipped.
{name: "nonce not supported means skip is allowed", nonceSupported: false, wantSkip: true},
} {
t.Run("case="+tc.name, func(t *testing.T) {
assert.Equal(t, tc.wantSkip, apple.CanSkipNonce(&oidc.Claims{NonceSupported: tc.nonceSupported}))
})
}
}

View File

@@ -0,0 +1,144 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"cmp"
"context"
"encoding/json"
"io"
"net/url"
"path"
"time"
"github.com/ory/x/httpx"
"github.com/tidwall/sjson"
"github.com/hashicorp/go-retryablehttp"
"github.com/pkg/errors"
"github.com/tidwall/gjson"
"golang.org/x/oauth2"
"github.com/ory/herodot"
)
type ProviderAuth0 struct {
*ProviderGenericOIDC
}
var _ OAuth2Provider = (*ProviderAuth0)(nil)
func NewProviderAuth0(
config *Configuration,
reg Dependencies,
) Provider {
return &ProviderAuth0{
ProviderGenericOIDC: &ProviderGenericOIDC{
config: config,
reg: reg,
},
}
}
func (g *ProviderAuth0) oauth2(ctx context.Context) (*oauth2.Config, error) {
endpoint, err := url.Parse(g.config.IssuerURL)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
authURL := *endpoint
tokenURL := *endpoint
authURL.Path = path.Join(authURL.Path, "/authorize")
tokenURL.Path = path.Join(tokenURL.Path, "/oauth/token")
c := &oauth2.Config{
ClientID: g.config.ClientID,
ClientSecret: g.config.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: authURL.String(),
TokenURL: tokenURL.String(),
},
Scopes: g.config.Scope,
RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)),
}
return c, nil
}
func (g *ProviderAuth0) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return g.oauth2(ctx)
}
func (g *ProviderAuth0) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) {
o, err := g.OAuth2(ctx)
if err != nil {
return nil, err
}
u, err := url.Parse(g.config.IssuerURL)
if err != nil {
return nil, errors.WithStack(herodot.ErrMisconfiguration().WithWrap(err).WithReasonf("%s", err))
}
u.Path = path.Join(u.Path, "/userinfo")
ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), o, exchange)
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", u.String(), nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
defer func() { _ = resp.Body.Close() }()
if err := logUpstreamError(g.reg.Logger(), resp); err != nil {
return nil, err
}
// Once auth0 fixes this bug, all this workaround can be removed.
b, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
b, err = authZeroUpdatedAtWorkaround(b)
if err != nil {
return nil, err
}
// Once we get here, we know that if there is an updated_at field in the json, it is the correct type.
var claims Claims
if err := json.Unmarshal(b, &claims); err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
claims.Issuer = cmp.Or(claims.Issuer, g.config.IssuerURL)
return &claims, nil
}
// There is a bug in the response from Auth0. The updated_at field may be a string and not an int64.
// https://community.auth0.com/t/oidc-id-token-claim-updated-at-violates-oidc-specification-breaks-rp-implementations/24098
// We work around this by reading the json generically (as map[string]inteface{} and looking at the updated_at field
// if it exists. If it's the wrong type (string), we fill out the claims by hand.
func authZeroUpdatedAtWorkaround(body []byte) ([]byte, error) {
// Force updatedAt to be an int if given as a string in the response.
if updatedAtField := gjson.GetBytes(body, "updated_at"); updatedAtField.Exists() && updatedAtField.Type == gjson.String {
t, err := time.Parse(time.RFC3339, updatedAtField.String())
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("bad time format in updated_at"))
}
body, err = sjson.SetBytes(body, "updated_at", t.Unix())
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
}
return body, nil
}

View File

@@ -0,0 +1,33 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAuthZeroUpdatedAtWorkaround(t *testing.T) {
actual, err := authZeroUpdatedAtWorkaround([]byte("{}"))
require.NoError(t, err)
assert.Equal(t, "{}", string(actual))
actual, err = authZeroUpdatedAtWorkaround([]byte(`{"updated_at":1234}`))
require.NoError(t, err)
assert.Equal(t, `{"updated_at":1234}`, string(actual))
timestamp := time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
input, err := json.Marshal(map[string]interface{}{
"updated_at": timestamp,
})
require.NoError(t, err)
actual, err = authZeroUpdatedAtWorkaround(input)
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf(`{"updated_at":%d}`, timestamp.Unix()), string(actual))
}

View File

@@ -0,0 +1,280 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"encoding/json"
"maps"
"net/url"
"slices"
"strings"
"github.com/pkg/errors"
"github.com/ory/herodot"
"github.com/ory/kratos/identity"
"github.com/ory/x/urlx"
)
const (
// AccountLinkingModeVerifyWithExistingCredential requires the user to verify their identity
// with an existing credential before accounts are linked.
AccountLinkingModeVerifyWithExistingCredential = "confirm_with_existing_credential"
// AccountLinkingModeAutomatic silently links accounts if the provider verifies email ownership.
AccountLinkingModeAutomatic = "automatic"
// ProviderTypeApple is the provider type for Sign in with Apple.
ProviderTypeApple = "apple"
// ProviderTypeGoogle is the provider type for Sign in with Google.
ProviderTypeGoogle = "google"
// UpdateIdentityOnLoginNever disables identity updates on login (default).
UpdateIdentityOnLoginNever = "never"
// UpdateIdentityOnLoginAutomatic re-runs the Jsonnet claims mapper on every
// OIDC login and updates the identity's traits and metadata automatically.
UpdateIdentityOnLoginAutomatic = "automatic"
)
type Configuration struct {
// ID is the provider's ID
ID string `json:"id"`
// Provider is either "generic" for a generic OAuth 2.0 / OpenID Connect Provider or one of:
// - generic
// - google
// - github
// - github-app
// - gitlab
// - microsoft
// - discord
// - salesforce
// - slack
// - facebook
// - auth0
// - vk
// - yandex
// - apple
// - spotify
// - netid
// - dingtalk
// - linkedin
// - patreon
// - amazon
// - uaepass
Provider string `json:"provider"`
// Label represents an optional label which can be used in the UI generation.
Label string `json:"label"`
// ClientID is the application's Client ID.
ClientID string `json:"client_id"`
// ClientSecret is the application's secret.
ClientSecret string `json:"client_secret"`
// IssuerURL is the OpenID Connect Server URL. You can leave this empty if `provider` is not set to `generic`.
// If set, neither `auth_url` nor `token_url` are required.
IssuerURL string `json:"issuer_url"`
// AuthURL is the authorize url, typically something like: https://example.org/oauth2/auth
// Should only be used when the OAuth2 / OpenID Connect server is not supporting OpenID Connect Discovery and when
// `provider` is set to `generic`.
AuthURL string `json:"auth_url"`
// TokenURL is the token url, typically something like: https://example.org/oauth2/token
// Should only be used when the OAuth2 / OpenID Connect server is not supporting OpenID Connect Discovery and when
// `provider` is set to `generic`.
TokenURL string `json:"token_url"`
// Tenant is the Azure AD Tenant to use for authentication, and must be set when `provider` is set to `microsoft`.
// Can be either `common`, `organizations`, `consumers` for a multitenant application or a specific tenant like
// `8eaef023-2b34-4da1-9baa-8bc8c9d6a490` or `contoso.onmicrosoft.com`.
Tenant string `json:"microsoft_tenant"`
// SubjectSource is a flag which controls from which endpoint the subject identifier is taken by microsoft provider.
// Can be either `userinfo` or `me` or `oid`.
// If the value is `userinfo` then the subject identifier is taken from sub field of userinfo standard endpoint response.
// If the value is `me` then the `id` field of https://graph.microsoft.com/v1.0/me response is taken as subject.
// If the value is `oid` then the the oid (Object ID) is taken to identify users across different services.
// The default is `userinfo`.
SubjectSource string `json:"subject_source"`
// TeamId is the Apple Developer Team ID that's needed for the `apple` `provider` to work.
// It can be found Apple Developer website and combined with `apple_private_key` and `apple_private_key_id`
// is used to generate `client_secret`
TeamId string `json:"apple_team_id"`
// PrivateKeyId is the private Apple key identifier. Keys can be generated via developer.apple.com.
// This key should be generated with the `Sign In with Apple` option checked.
// This is needed when `provider` is set to `apple`
PrivateKeyId string `json:"apple_private_key_id"`
// PrivateKeyId is the Apple private key identifier that can be downloaded during key generation.
// This is needed when `provider` is set to `apple`
PrivateKey string `json:"apple_private_key"`
// Scope specifies optional requested permissions.
Scope []string `json:"scope"`
// Mapper specifies the JSONNet code snippet which uses the OpenID Connect Provider's data (e.g. GitHub or Google
// profile information) to hydrate the identity's data.
//
// It can be either a URL (file://, http(s)://, base64://) or an inline JSONNet code snippet.
Mapper string `json:"mapper_url"`
// RequestedClaims is a string encoded json object that specifies claims and optionally their properties that should be
// included in the id_token or returned from the UserInfo Endpoint.
//
// More information: https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
RequestedClaims json.RawMessage `json:"requested_claims"`
// An optional organization ID that this provider belongs to.
// This parameter is only effective in the Ory Network.
OrganizationID string `json:"organization_id"`
// AdditionalIDTokenAudiences is a list of additional audiences allowed in the ID Token.
// This is only relevant in OIDC flows that submit an IDToken instead of using the callback from the OIDC provider.
AdditionalIDTokenAudiences []string `json:"additional_id_token_audiences"`
// ClaimsSource is a flag which controls where the claims are taken from when
// using the generic provider. Can be either `userinfo` (calls the userinfo
// endpoint to get the claims) or `id_token` (takes the claims from the id
// token). It defaults to `id_token`.
ClaimsSource string `json:"claims_source"`
// PKCE controls if the OpenID Connect OAuth2 flow should use PKCE (Proof Key for Code Exchange).
// Possible values are: `auto` (default), `never`, `force`.
// - `auto`: PKCE is used if the provider supports it. Requires setting `issuer_url`.
// - `never`: Disable PKCE entirely for this provider, even if the provider advertises support for it.
// - `force`: Always use PKCE, even if the provider does not advertise support for it. OAuth2 flows will fail if the provider does not support PKCE.
// IMPORTANT: If you set this to `force`, you must whitelist a different return URL for your OAuth2 client in the provider's configuration.
// Instead of <base-url>/self-service/methods/oidc/callback/<provider>, you must use <base-url>/self-service/methods/oidc/callback
// (Note the missing <provider> path segment and no trailing slash).
PKCE string `json:"pkce"`
// FedCMConfigURL is the URL to the FedCM IdP configuration file.
// This is only effective in the Ory Network.
FedCMConfigURL string `json:"fedcm_config_url"`
// NetIDTokenOriginHeader contains the orgin header to be used when exchanging a
// NetID FedCM token for an ID token.
NetIDTokenOriginHeader string `json:"net_id_token_origin_header"`
// AccountLinkingMode controls how account conflicts are resolved for this provider.
// Possible values: "confirm_with_existing_credential" (default), "automatic".
// This is only effective in the Ory Network.
AccountLinkingMode string `json:"account_linking_mode,omitempty"`
// UpdateIdentityOnLogin controls whether the identity is updated from
// OIDC claims on each login.
//
// Possible values:
// - "never" (default): the identity is not updated on login.
// - "automatic": re-runs the Jsonnet claims mapper on every OIDC login
// and updates the identity's traits and metadata automatically.
UpdateIdentityOnLogin string `json:"update_identity_on_login,omitempty"`
// AAL2ACRValues lists upstream OIDC `acr` claim values that elevate
// the resulting Kratos session to AAL2. When empty, the session AAL
// is always AAL1 regardless of the upstream `acr` claim.
AAL2ACRValues []string `json:"aal2_acr_values,omitempty"`
// AAL2AMRValues lists upstream OIDC `amr` claim values that elevate
// the resulting Kratos session to AAL2 when any of them appears in
// the upstream `amr` array. When empty, `amr` does not influence the
// session AAL.
AAL2AMRValues []string `json:"aal2_amr_values,omitempty"`
}
// AALForClaims returns the session AuthenticatorAssuranceLevel that the
// upstream OIDC response should be mapped to, based on the provider's
// AAL2ACRValues / AAL2AMRValues allowlists and the ACR / AMR claims
// reported by the upstream provider.
//
// The default is AAL1. AAL2 is only returned when either the upstream
// `acr` claim matches one of the configured AAL2ACRValues, or at least
// one of the upstream `amr` entries matches one of the configured
// AAL2AMRValues. An empty `acr` never matches, even if the allowlist
// contains an empty string.
func (c *Configuration) AALForClaims(claims *Claims) identity.AuthenticatorAssuranceLevel {
if claims == nil {
return identity.AuthenticatorAssuranceLevel1
}
if claims.ACR != "" && slices.Contains(c.AAL2ACRValues, claims.ACR) {
return identity.AuthenticatorAssuranceLevel2
}
for _, want := range c.AAL2AMRValues {
if slices.Contains(claims.AMR, want) {
return identity.AuthenticatorAssuranceLevel2
}
}
return identity.AuthenticatorAssuranceLevel1
}
func (p Configuration) Redir(public *url.URL) string {
if p.PKCE == "force" {
return urlx.AppendPaths(public, RouteCallbackGeneric).String()
}
if p.OrganizationID != "" {
route := RouteOrganizationCallback
route = strings.Replace(route, "{provider}", p.ID, 1)
route = strings.Replace(route, "{organization}", p.OrganizationID, 1)
return urlx.AppendPaths(public, route).String()
}
return urlx.AppendPaths(public, strings.Replace(RouteCallback, "{provider}", p.ID, 1)).String()
}
type ConfigurationCollection struct {
BaseRedirectURI string `json:"base_redirect_uri"`
Providers []Configuration `json:"providers"`
}
// !!! WARNING !!!
//
// If you add a provider here, please also add a test to
// provider_private_net_test.go
var supportedProviders = map[string]func(config *Configuration, reg Dependencies) Provider{
"generic": NewProviderGenericOIDC,
"google": NewProviderGoogle,
"github": NewProviderGitHub,
"github-app": NewProviderGitHubApp,
"gitlab": NewProviderGitLab,
"microsoft": NewProviderMicrosoft,
"discord": NewProviderDiscord,
"salesforce": NewProviderSalesforce,
"slack": NewProviderSlack,
"facebook": NewProviderFacebook,
"auth0": NewProviderAuth0,
"vk": NewProviderVK,
"yandex": NewProviderYandex,
"apple": NewProviderApple,
"spotify": NewProviderSpotify,
"netid": NewProviderNetID,
"dingtalk": NewProviderDingTalk,
"linkedin": NewProviderLinkedIn,
"linkedin_v2": NewProviderLinkedInV2,
"patreon": NewProviderPatreon,
"lark": NewProviderLark,
"x": NewProviderX,
"line": NewProviderLineV21,
"jackson": NewProviderJackson,
"fedcm-test": NewProviderTestFedcm,
"amazon": NewProviderAmazon,
"uaepass": NewProviderUAEPass,
}
func (c ConfigurationCollection) Provider(id string, reg Dependencies) (Provider, error) {
for _, p := range c.Providers {
if p.ID == id {
if f, ok := supportedProviders[p.Provider]; ok {
return f(&p, reg), nil
}
return nil, errors.Errorf("provider type %s is not supported, supported are: %v", p.Provider, maps.Keys(supportedProviders))
}
}
return nil, errors.WithStack(herodot.ErrNotFound().WithReasonf(`OpenID Connect Provider "%s" is unknown or has not been configured`, id))
}

View File

@@ -0,0 +1,113 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/selfservice/strategy/oidc"
"github.com/ory/x/configx"
)
func TestConfig(t *testing.T) {
_, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue(
config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config",
map[string]any{"providers": []map[string]any{{"provider": "generic"}}}))
s := oidc.NewStrategy(reg)
collection, err := s.Config(t.Context())
require.NoError(t, err)
require.Len(t, collection.Providers, 1)
assert.Equal(t, "generic", collection.Providers[0].Provider)
}
func TestConfiguration_AALForClaims(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
config oidc.Configuration
claims *oidc.Claims
want identity.AuthenticatorAssuranceLevel
}{
{
name: "nil claims default to aal1",
config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}, AAL2AMRValues: []string{"mfa"}},
claims: nil,
want: identity.AuthenticatorAssuranceLevel1,
},
{
name: "empty config with claims stays aal1",
config: oidc.Configuration{},
claims: &oidc.Claims{ACR: "urn:mfa", AMR: []string{"mfa", "pwd"}},
want: identity.AuthenticatorAssuranceLevel1,
},
{
name: "acr match elevates to aal2",
config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa", "urn:strong"}},
claims: &oidc.Claims{ACR: "urn:mfa"},
want: identity.AuthenticatorAssuranceLevel2,
},
{
name: "acr mismatch stays aal1",
config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}},
claims: &oidc.Claims{ACR: "urn:basic"},
want: identity.AuthenticatorAssuranceLevel1,
},
{
name: "empty acr does not accidentally match empty configured value",
config: oidc.Configuration{AAL2ACRValues: []string{""}},
claims: &oidc.Claims{ACR: ""},
want: identity.AuthenticatorAssuranceLevel1,
},
{
name: "amr match elevates to aal2",
config: oidc.Configuration{AAL2AMRValues: []string{"mfa"}},
claims: &oidc.Claims{AMR: []string{"pwd", "mfa"}},
want: identity.AuthenticatorAssuranceLevel2,
},
{
name: "amr mismatch stays aal1",
config: oidc.Configuration{AAL2AMRValues: []string{"mfa"}},
claims: &oidc.Claims{AMR: []string{"pwd"}},
want: identity.AuthenticatorAssuranceLevel1,
},
{
name: "any configured amr value is sufficient",
config: oidc.Configuration{AAL2AMRValues: []string{"otp", "hwk", "mfa"}},
claims: &oidc.Claims{AMR: []string{"pwd", "hwk"}},
want: identity.AuthenticatorAssuranceLevel2,
},
{
name: "both acr and amr configured, acr matches",
config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}, AAL2AMRValues: []string{"mfa"}},
claims: &oidc.Claims{ACR: "urn:mfa", AMR: []string{"pwd"}},
want: identity.AuthenticatorAssuranceLevel2,
},
{
name: "both acr and amr configured, amr matches",
config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}, AAL2AMRValues: []string{"mfa"}},
claims: &oidc.Claims{ACR: "urn:basic", AMR: []string{"pwd", "mfa"}},
want: identity.AuthenticatorAssuranceLevel2,
},
{
name: "both configured, neither matches",
config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}, AAL2AMRValues: []string{"mfa"}},
claims: &oidc.Claims{ACR: "urn:basic", AMR: []string{"pwd"}},
want: identity.AuthenticatorAssuranceLevel1,
},
} {
t.Run("case="+tc.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tc.want, tc.config.AALForClaims(tc.claims))
})
}
}

View File

@@ -0,0 +1,173 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"encoding/json"
"net/url"
"strings"
"time"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/ory/x/httpx"
"github.com/hashicorp/go-retryablehttp"
"github.com/ory/herodot"
)
type ProviderDingTalk struct {
config *Configuration
reg Dependencies
}
var _ OAuth2Provider = (*ProviderDingTalk)(nil)
func NewProviderDingTalk(
config *Configuration,
reg Dependencies,
) Provider {
return &ProviderDingTalk{
config: config,
reg: reg,
}
}
func (g *ProviderDingTalk) Config() *Configuration {
return g.config
}
func (g *ProviderDingTalk) oauth2(ctx context.Context) *oauth2.Config {
endpoint := oauth2.Endpoint{
AuthURL: "https://login.dingtalk.com/oauth2/auth",
TokenURL: "https://api.dingtalk.com/v1.0/oauth2/userAccessToken",
}
return &oauth2.Config{
ClientID: g.config.ClientID,
ClientSecret: g.config.ClientSecret,
Endpoint: endpoint,
// DingTalk only allow to set scopes: openid or openid corpid
Scopes: g.config.Scope,
RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)),
}
}
func (g *ProviderDingTalk) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
return []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("prompt", "consent"),
}
}
func (g *ProviderDingTalk) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return g.oauth2(ctx), nil
}
func (g *ProviderDingTalk) ExchangeOAuth2Token(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
conf, err := g.OAuth2(ctx)
if err != nil {
return nil, err
}
pTokenParams := &struct {
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
Code string `json:"code"`
GrantType string `json:"grantType"`
}{conf.ClientID, conf.ClientSecret, code, "authorization_code"}
bs, err := json.Marshal(pTokenParams)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
r := strings.NewReader(string(bs))
client := g.reg.HTTPClient(ctx, httpx.ResilientClientDisallowInternalIPs())
req, err := retryablehttp.NewRequest("POST", conf.Endpoint.TokenURL, r)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
req.Header.Add("Content-Type", "application/json;charset=UTF-8")
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
defer func() { _ = resp.Body.Close() }()
if err := logUpstreamError(g.reg.Logger(), resp); err != nil {
return nil, err
}
var dToken struct {
ErrCode int `json:"code"`
ErrMsg string `json:"message"`
AccessToken string `json:"accessToken"` // Interface call credentials
ExpiresIn int64 `json:"expireIn"` // access_token interface call credential timeout time, unit (seconds)
}
if err := json.NewDecoder(resp.Body).Decode(&dToken); err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
if dToken.ErrCode != 0 {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("dToken.ErrCode = %d, dToken.ErrMsg = %s", dToken.ErrCode, dToken.ErrMsg))
}
token := &oauth2.Token{
AccessToken: dToken.AccessToken,
Expiry: time.Unix(time.Now().Unix()+int64(dToken.ExpiresIn), 0),
}
return token, nil
}
func (g *ProviderDingTalk) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) {
userInfoURL := "https://api.dingtalk.com/v1.0/contact/users/me"
accessToken := exchange.AccessToken
client := g.reg.HTTPClient(ctx, httpx.ResilientClientDisallowInternalIPs())
req, err := retryablehttp.NewRequest("GET", userInfoURL, nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
req.Header.Add("x-acs-dingtalk-access-token", accessToken)
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
defer func() { _ = resp.Body.Close() }()
if err := logUpstreamError(g.reg.Logger(), resp); err != nil {
return nil, err
}
var user struct {
Nick string `json:"nick"`
OpenID string `json:"openId"`
AvatarURL string `json:"avatarUrl"`
Email string `json:"email"`
ErrMsg string `json:"message"`
ErrCode string `json:"code"`
}
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
if user.ErrMsg != "" {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("userResp.ErrCode = %s, userResp.ErrMsg = %s", user.ErrCode, user.ErrMsg))
}
return &Claims{
Issuer: userInfoURL,
Subject: user.OpenID,
Nickname: user.Nick,
Name: user.Nick,
Picture: user.AvatarURL,
Email: user.Email,
}, nil
}

View File

@@ -0,0 +1,103 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"fmt"
"net/url"
"slices"
"github.com/ory/kratos/x"
"github.com/bwmarrin/discordgo"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/ory/herodot"
"github.com/ory/x/stringsx"
)
var _ OAuth2Provider = (*ProviderDiscord)(nil)
type ProviderDiscord struct {
config *Configuration
reg Dependencies
}
func NewProviderDiscord(
config *Configuration,
reg Dependencies,
) Provider {
return &ProviderDiscord{
config: config,
reg: reg,
}
}
func (d *ProviderDiscord) Config() *Configuration {
return d.config
}
func (d *ProviderDiscord) oauth2(ctx context.Context) *oauth2.Config {
return &oauth2.Config{
ClientID: d.config.ClientID,
ClientSecret: d.config.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: discordgo.EndpointOauth2 + "authorize",
TokenURL: discordgo.EndpointOauth2 + "token",
},
RedirectURL: d.config.Redir(d.reg.Config().OIDCRedirectURIBase(ctx)),
Scopes: d.config.Scope,
}
}
func (d *ProviderDiscord) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return d.oauth2(ctx), nil
}
func (d *ProviderDiscord) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
if isForced(r) {
return []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("prompt", "consent"),
}
}
return []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("prompt", "none"),
}
}
func (d *ProviderDiscord) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) {
grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), " ")
for _, check := range d.Config().Scope {
if !slices.Contains(grantedScopes, check) {
return nil, errors.WithStack(ErrScopeMissing())
}
}
dg, err := discordgo.New(fmt.Sprintf("Bearer %s", exchange.AccessToken))
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("%s", err))
}
user, err := dg.User("@me")
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
claims := &Claims{
Issuer: discordgo.EndpointOauth2,
Subject: user.ID,
Name: fmt.Sprintf("%s#%s", user.Username, user.Discriminator),
Nickname: user.Username,
PreferredUsername: user.Username,
Picture: user.AvatarURL(""),
Email: user.Email,
EmailVerified: x.ConvertibleBoolean(user.Verified),
Locale: Locale(user.Locale),
}
return claims, nil
}

View File

@@ -0,0 +1,139 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/url"
"github.com/hashicorp/go-retryablehttp"
"github.com/ory/x/httpx"
"github.com/ory/kratos/x"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/ory/herodot"
)
var _ OAuth2Provider = (*ProviderFacebook)(nil)
type ProviderFacebook struct {
*ProviderGenericOIDC
}
func NewProviderFacebook(
config *Configuration,
reg Dependencies,
) Provider {
config.IssuerURL = "https://www.facebook.com"
return &ProviderFacebook{
ProviderGenericOIDC: &ProviderGenericOIDC{
config: config,
reg: reg,
},
}
}
func (g *ProviderFacebook) generateAppSecretProof(token *oauth2.Token) string {
secret := g.config.ClientSecret
data := token.AccessToken
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
func (g *ProviderFacebook) OAuth2(ctx context.Context) (*oauth2.Config, error) {
p, err := g.provider(ctx)
if err != nil {
return nil, err
}
endpoint := p.Endpoint()
endpoint.AuthURL = "https://facebook.com/dialog/oauth"
endpoint.TokenURL = "https://graph.facebook.com/oauth/access_token"
return g.oauth2ConfigFromEndpoint(ctx, endpoint), nil
}
func (g *ProviderFacebook) Claims(ctx context.Context, token *oauth2.Token, query url.Values) (*Claims, error) {
o, err := g.OAuth2(ctx)
if err != nil {
return nil, err
}
appSecretProof := g.generateAppSecretProof(token)
// Do not use the versioned Graph API here. If you do, it will break once the version is deprecated. See also:
//
// When you use https://graph.facebook.com/me without specifying a version, Facebook defaults to the oldest
// available version your app supports. This behavior ensures backward compatibility but can lead to unintended
// issues if that version becomes deprecated.
u, err := url.Parse(fmt.Sprintf("https://graph.facebook.com/me?fields=id,name,first_name,last_name,middle_name,email,picture,birthday,gender&appsecret_proof=%s", appSecretProof))
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), o, token)
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", u.String(), nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
defer func() { _ = resp.Body.Close() }()
if err := logUpstreamError(g.reg.Logger(), resp); err != nil {
return nil, err
}
var user struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
MiddleName string `json:"middle_name,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified bool
Picture struct {
Data struct {
URL string `json:"url,omitempty"`
} `json:"data,omitempty"`
} `json:"picture,omitempty"`
BirthDay string `json:"birthday,omitempty"`
Gender string `json:"gender,omitempty"`
}
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("%s", err))
}
if user.Email != "" {
user.EmailVerified = true
}
return &Claims{
Issuer: u.String(),
Subject: user.ID,
Name: user.Name,
GivenName: user.FirstName,
FamilyName: user.LastName,
MiddleName: user.MiddleName,
Nickname: user.Name,
PreferredUsername: user.Name,
Picture: user.Picture.Data.URL,
Email: user.Email,
EmailVerified: x.ConvertibleBoolean(user.EmailVerified),
Gender: user.Gender,
Birthdate: user.BirthDay,
}, nil
}

View File

@@ -0,0 +1,219 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"net/url"
"slices"
"time"
gooidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/ory/herodot"
"github.com/ory/x/reqlog"
)
var _ OAuth2Provider = (*ProviderGenericOIDC)(nil)
type ProviderGenericOIDC struct {
p *gooidc.Provider
config *Configuration
reg Dependencies
}
func NewProviderGenericOIDC(
config *Configuration,
reg Dependencies,
) Provider {
return &ProviderGenericOIDC{
config: config,
reg: reg,
}
}
const (
ClaimsSourceIDToken = "id_token"
ClaimsSourceUserInfo = "userinfo"
)
func (g *ProviderGenericOIDC) Config() *Configuration {
return g.config
}
func (g *ProviderGenericOIDC) withHTTPClientContext(ctx context.Context) context.Context {
return gooidc.ClientContext(ctx, g.reg.HTTPClient(ctx).HTTPClient)
}
func (g *ProviderGenericOIDC) provider(ctx context.Context) (*gooidc.Provider, error) {
if g.p == nil {
p, err := gooidc.NewProvider(g.withHTTPClientContext(ctx), g.config.IssuerURL)
if err != nil {
return nil, errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("Unable to initialize OpenID Connect Provider: %s", err))
}
g.p = p
}
return g.p, nil
}
func (g *ProviderGenericOIDC) oauth2ConfigFromEndpoint(ctx context.Context, endpoint oauth2.Endpoint) *oauth2.Config {
scope := g.config.Scope
if !slices.Contains(scope, gooidc.ScopeOpenID) {
scope = append(scope, gooidc.ScopeOpenID)
}
return &oauth2.Config{
ClientID: g.config.ClientID,
ClientSecret: g.config.ClientSecret,
Endpoint: endpoint,
Scopes: scope,
RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)),
}
}
func (g *ProviderGenericOIDC) OAuth2(ctx context.Context) (*oauth2.Config, error) {
p, err := g.provider(ctx)
if err != nil {
return nil, err
}
endpoint := p.Endpoint()
return g.oauth2ConfigFromEndpoint(ctx, endpoint), nil
}
func (g *ProviderGenericOIDC) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
var options []oauth2.AuthCodeOption
if isForced(r) {
options = append(options, oauth2.SetAuthURLParam("prompt", "login"))
}
if len(g.config.RequestedClaims) != 0 {
options = append(options, oauth2.SetAuthURLParam("claims", string(g.config.RequestedClaims)))
}
return options
}
func (g *ProviderGenericOIDC) verifyAndDecodeClaimsWithProvider(ctx context.Context, provider *gooidc.Provider, raw string) (*Claims, error) {
token, err := provider.VerifierContext(g.withHTTPClientContext(ctx), &gooidc.Config{ClientID: g.config.ClientID}).Verify(ctx, raw)
if err != nil {
return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err))
}
var claims Claims
if err := token.Claims(&claims); err != nil {
return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err))
}
var rawClaims map[string]interface{}
if err := token.Claims(&rawClaims); err != nil {
return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err))
}
claims.RawClaims = rawClaims
return &claims, nil
}
func (g *ProviderGenericOIDC) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) {
switch g.config.ClaimsSource {
case ClaimsSourceIDToken, "":
return g.claimsFromIDToken(ctx, exchange)
case ClaimsSourceUserInfo:
return g.claimsFromUserInfo(ctx, exchange)
}
return nil, errors.WithStack(herodot.ErrMisconfiguration().
WithReasonf("Unknown claims source: %q", g.config.ClaimsSource))
}
func (g *ProviderGenericOIDC) claimsFromUserInfo(ctx context.Context, exchange *oauth2.Token) (*Claims, error) {
p, err := g.provider(ctx)
if err != nil {
return nil, err
}
t0 := time.Now()
userInfo, err := p.UserInfo(g.withHTTPClientContext(ctx), oauth2.StaticTokenSource(exchange))
reqlog.AccumulateExternalLatency(ctx, time.Since(t0))
if err != nil {
return nil, err
}
var claims Claims
if err = userInfo.Claims(&claims); err != nil {
return nil, err
}
var rawClaims map[string]interface{}
if err := userInfo.Claims(&rawClaims); err != nil {
return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err))
}
claims.RawClaims = rawClaims
// NOTE: Due to the possibility of token substitution attacks (see Section
// 16.11), the UserInfo Response is not guaranteed to be about the End-User
// identified by the sub (subject) element of the ID Token. The sub Claim in the
// UserInfo Response MUST be verified to exactly match the sub Claim in the ID
// Token; if they do not match, the UserInfo Response values MUST NOT be used.
// See https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
idToken, err := g.verifiedIDToken(ctx, exchange)
if err != nil {
return nil, err
}
if idToken.Subject != claims.Subject {
return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("sub (Subject) claim mismatch between ID token and UserInfo endpoint"))
}
// If signed, the UserInfo Response MUST contain the Claims iss (issuer) and aud
// (audience) as members. The iss value MUST be the OP's Issuer Identifier URL.
// The aud value MUST be or include the RP's Client ID value.
// See https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
//
// Consequently, the issuer might not be present in the UserInfo response and we
// need to set it here.
if claims.Issuer == "" {
claims.Issuer = idToken.Issuer
}
return &claims, nil
}
func (g *ProviderGenericOIDC) claimsFromIDToken(ctx context.Context, exchange *oauth2.Token) (*Claims, error) {
p, raw, err := g.idTokenAndProvider(ctx, exchange)
if err != nil {
return nil, err
}
return g.verifyAndDecodeClaimsWithProvider(ctx, p, raw)
}
func (g *ProviderGenericOIDC) idTokenAndProvider(ctx context.Context, exchange *oauth2.Token) (*gooidc.Provider, string, error) {
raw, ok := exchange.Extra("id_token").(string)
if !ok || len(raw) == 0 {
return nil, "", errors.WithStack(ErrIDTokenMissing())
}
p, err := g.provider(ctx)
if err != nil {
return nil, "", err
}
return p, raw, nil
}
func (g *ProviderGenericOIDC) verifiedIDToken(ctx context.Context, exchange *oauth2.Token) (*gooidc.IDToken, error) {
p, raw, err := g.idTokenAndProvider(ctx, exchange)
if err != nil {
return nil, err
}
token, err := p.VerifierContext(g.withHTTPClientContext(ctx), &gooidc.Config{ClientID: g.config.ClientID}).Verify(ctx, raw)
if err != nil {
return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err))
}
return token, nil
}

View File

@@ -0,0 +1,94 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc_test
import (
"encoding/json"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/kratos/driver"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/selfservice/strategy/oidc"
"github.com/ory/kratos/x"
"github.com/ory/x/configx"
)
func makeOIDCClaims() json.RawMessage {
claims, _ := json.Marshal(map[string]interface{}{
"id_token": map[string]interface{}{
"email": map[string]bool{
"essential": true,
},
"email_verified": map[string]bool{
"essential": true,
},
},
})
return claims
}
func makeAuthCodeURL(t *testing.T, r *login.Flow, reg *driver.RegistryDefault) string {
p := oidc.NewProviderGenericOIDC(&oidc.Configuration{
Provider: "generic",
ID: "valid",
ClientID: "client",
ClientSecret: "secret",
IssuerURL: "https://accounts.google.com",
Mapper: "file://./stub/hydra.schema.json",
RequestedClaims: makeOIDCClaims(),
}, reg)
c, err := p.(oidc.OAuth2Provider).OAuth2(t.Context())
require.NoError(t, err)
return c.AuthCodeURL("state", p.(oidc.OAuth2Provider).AuthCodeURLOptions(r)...)
}
func TestProviderGenericOIDC_AddAuthCodeURLOptions(t *testing.T) {
conf, reg := pkg.NewFastRegistryWithMocks(t,
configx.WithValue(config.ViperKeyPublicBaseURL, "https://ory.sh"))
t.Run("case=redirectURI is public base url", func(t *testing.T) {
r := &login.Flow{ID: x.NewUUID(), Refresh: true}
actual, err := url.ParseRequestURI(makeAuthCodeURL(t, r, reg))
require.NoError(t, err)
assert.Contains(t, actual.Query().Get("redirect_uri"), "https://ory.sh")
})
t.Run("case=redirectURI is public base url", func(t *testing.T) {
conf.MustSet(t.Context(), config.ViperKeyOIDCBaseRedirectURL, "https://example.org")
t.Cleanup(func() {
conf.MustSet(t.Context(), config.ViperKeyOIDCBaseRedirectURL, nil)
})
r := &login.Flow{ID: x.NewUUID(), Refresh: true}
actual, err := url.ParseRequestURI(makeAuthCodeURL(t, r, reg))
require.NoError(t, err)
assert.Contains(t, actual.Query().Get("redirect_uri"), "https://example.org")
})
t.Run("case=expect prompt to be login with forced flag", func(t *testing.T) {
r := &login.Flow{
ID: x.NewUUID(),
Refresh: true,
}
assert.Contains(t, makeAuthCodeURL(t, r, reg), "prompt=login")
})
t.Run("case=expect prompt to not be login without forced flag", func(t *testing.T) {
r := &login.Flow{
ID: x.NewUUID(),
}
assert.NotContains(t, makeAuthCodeURL(t, r, reg), "prompt=login")
})
t.Run("case=expect requested claims to be set", func(t *testing.T) {
r := &login.Flow{
ID: x.NewUUID(),
}
assert.Contains(t, makeAuthCodeURL(t, r, reg), "claims="+url.QueryEscape(string(makeOIDCClaims())))
})
}

View File

@@ -0,0 +1,114 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"fmt"
"net/url"
"slices"
"github.com/ory/kratos/x"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
"github.com/ory/x/httpx"
"github.com/ory/x/stringsx"
ghapi "github.com/google/go-github/v89/github"
"github.com/ory/herodot"
)
var _ OAuth2Provider = (*ProviderGitHub)(nil)
type ProviderGitHub struct {
config *Configuration
reg Dependencies
}
func NewProviderGitHub(
config *Configuration,
reg Dependencies,
) Provider {
return &ProviderGitHub{
config: config,
reg: reg,
}
}
func (g *ProviderGitHub) Config() *Configuration {
return g.config
}
func (g *ProviderGitHub) oauth2(ctx context.Context) *oauth2.Config {
return &oauth2.Config{
ClientID: g.config.ClientID,
ClientSecret: g.config.ClientSecret,
Endpoint: github.Endpoint,
Scopes: g.config.Scope,
RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)),
}
}
func (g *ProviderGitHub) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return g.oauth2(ctx), nil
}
func (g *ProviderGitHub) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
return []oauth2.AuthCodeOption{}
}
func (g *ProviderGitHub) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) {
grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), ",")
for _, check := range g.Config().Scope {
if !slices.Contains(grantedScopes, check) {
return nil, errors.WithStack(ErrScopeMissing())
}
}
ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), g.oauth2(ctx), exchange)
gh, err := ghapi.NewClient(ghapi.WithHTTPClient(client.HTTPClient))
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
user, _, err := gh.Users.Get(ctx, "")
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
claims := &Claims{
Subject: fmt.Sprintf("%d", user.GetID()),
Issuer: github.Endpoint.TokenURL,
Name: user.GetName(),
Nickname: user.GetLogin(),
Website: user.GetBlog(),
Picture: user.GetAvatarURL(),
Profile: user.GetHTMLURL(),
UpdatedAt: user.GetUpdatedAt().Unix(),
}
// GitHub does not provide the user's private emails in the call to `/user`. Therefore, if scope "user:email" is set,
// we want to make another request to `/user/emails` and merge that with our claims.
if slices.Contains(grantedScopes, "user:email") {
emails, _, err := gh.Users.ListEmails(ctx, nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
for k, e := range emails {
// If it is the primary email or it's the last email (no primary email set?), set the email.
if e.GetPrimary() || k == len(emails) {
claims.Email = e.GetEmail()
claims.EmailVerified = x.ConvertibleBoolean(e.GetVerified())
break
}
}
}
return claims, nil
}

View File

@@ -0,0 +1,100 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"fmt"
"net/url"
"github.com/ory/kratos/x"
"github.com/ory/x/httpx"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
ghapi "github.com/google/go-github/v89/github"
"github.com/ory/herodot"
)
type ProviderGitHubApp struct {
config *Configuration
reg Dependencies
}
func NewProviderGitHubApp(
config *Configuration,
reg Dependencies,
) Provider {
return &ProviderGitHubApp{
config: config,
reg: reg,
}
}
func (g *ProviderGitHubApp) Config() *Configuration {
return g.config
}
func (g *ProviderGitHubApp) oauth2(ctx context.Context) *oauth2.Config {
return &oauth2.Config{
ClientID: g.config.ClientID,
ClientSecret: g.config.ClientSecret,
Endpoint: github.Endpoint,
Scopes: g.config.Scope,
RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)),
}
}
func (g *ProviderGitHubApp) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return g.oauth2(ctx), nil
}
func (g *ProviderGitHubApp) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
return []oauth2.AuthCodeOption{}
}
func (g *ProviderGitHubApp) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) {
ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), g.oauth2(ctx), exchange)
gh, err := ghapi.NewClient(ghapi.WithHTTPClient(client.HTTPClient))
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
user, _, err := gh.Users.Get(ctx, "")
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
claims := &Claims{
Subject: fmt.Sprintf("%d", user.GetID()),
Issuer: github.Endpoint.TokenURL,
Name: user.GetName(),
Nickname: user.GetLogin(),
Website: user.GetBlog(),
Picture: user.GetAvatarURL(),
Profile: user.GetHTMLURL(),
UpdatedAt: user.GetUpdatedAt().Unix(),
}
// GitHub does not provide the user's private emails in the call to `/user`. Therefore, if scope "user:email" is set,
// we want to make another request to `/user/emails` and merge that with our claims.
emails, _, err := gh.Users.ListEmails(ctx, nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
for k, e := range emails {
// If it is the primary email or it's the last email (no primary email set?), set the email.
if e.GetPrimary() || k == len(emails)-1 {
claims.Email = e.GetEmail()
claims.EmailVerified = x.ConvertibleBoolean(e.GetVerified())
break
}
}
return claims, nil
}

View File

@@ -0,0 +1,117 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"cmp"
"context"
"encoding/json"
"net/url"
"path"
"github.com/hashicorp/go-retryablehttp"
"github.com/ory/x/httpx"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/ory/herodot"
)
const (
defaultEndpoint = "https://gitlab.com"
)
var _ OAuth2Provider = (*ProviderGitLab)(nil)
type ProviderGitLab struct {
*ProviderGenericOIDC
}
func NewProviderGitLab(
config *Configuration,
reg Dependencies,
) Provider {
return &ProviderGitLab{
ProviderGenericOIDC: &ProviderGenericOIDC{
config: config,
reg: reg,
},
}
}
func (g *ProviderGitLab) oauth2(ctx context.Context) (*oauth2.Config, error) {
endpoint, err := g.endpoint()
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
authURL := *endpoint
tokenURL := *endpoint
authURL.Path = path.Join(authURL.Path, "/oauth/authorize")
tokenURL.Path = path.Join(tokenURL.Path, "/oauth/token")
return &oauth2.Config{
ClientID: g.config.ClientID,
ClientSecret: g.config.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: authURL.String(),
TokenURL: tokenURL.String(),
},
Scopes: g.config.Scope,
RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)),
}, nil
}
func (g *ProviderGitLab) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return g.oauth2(ctx)
}
func (g *ProviderGitLab) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) {
o, err := g.OAuth2(ctx)
if err != nil {
return nil, err
}
u, err := g.endpoint()
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
u.Path = path.Join(u.Path, "/oauth/userinfo")
ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), o, exchange)
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", u.String(), nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
defer func() { _ = resp.Body.Close() }()
if err := logUpstreamError(g.reg.Logger(), resp); err != nil {
return nil, err
}
var claims Claims
if err := json.NewDecoder(resp.Body).Decode(&claims); err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
claims.Issuer = cmp.Or(claims.Issuer, g.config.IssuerURL)
return &claims, nil
}
func (g *ProviderGitLab) endpoint() (*url.URL, error) {
e := defaultEndpoint
if len(g.config.IssuerURL) > 0 {
e = g.config.IssuerURL
}
return url.Parse(e)
}

View File

@@ -0,0 +1,89 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"slices"
gooidc "github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
var _ OAuth2Provider = (*ProviderGoogle)(nil)
type ProviderGoogle struct {
*ProviderGenericOIDC
JWKSUrl string
}
func NewProviderGoogle(
config *Configuration,
reg Dependencies,
) Provider {
config.IssuerURL = "https://accounts.google.com"
return &ProviderGoogle{
ProviderGenericOIDC: &ProviderGenericOIDC{
config: config,
reg: reg,
},
JWKSUrl: "https://www.googleapis.com/oauth2/v3/certs",
}
}
func (g *ProviderGoogle) oauth2ConfigFromEndpoint(ctx context.Context, endpoint oauth2.Endpoint) *oauth2.Config {
scope := g.config.Scope
if !slices.Contains(scope, gooidc.ScopeOpenID) {
scope = append(scope, gooidc.ScopeOpenID)
}
scope = slices.DeleteFunc(slices.Clone(scope), func(s string) bool { return s == gooidc.ScopeOfflineAccess })
return &oauth2.Config{
ClientID: g.config.ClientID,
ClientSecret: g.config.ClientSecret,
Endpoint: endpoint,
Scopes: scope,
RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)),
}
}
func (g *ProviderGoogle) OAuth2(ctx context.Context) (*oauth2.Config, error) {
p, err := g.provider(ctx)
if err != nil {
return nil, err
}
endpoint := p.Endpoint()
return g.oauth2ConfigFromEndpoint(ctx, endpoint), nil
}
func (g *ProviderGoogle) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
scope := g.config.Scope
options := g.ProviderGenericOIDC.AuthCodeURLOptions(r)
if slices.Contains(scope, gooidc.ScopeOfflineAccess) {
options = append(options, oauth2.AccessTypeOffline)
}
return options
}
var _ IDTokenVerifier = new(ProviderGoogle)
const issuerURLGoogle = "https://accounts.google.com"
func (g *ProviderGoogle) Verify(ctx context.Context, rawIDToken string) (*Claims, error) {
keySet := gooidc.NewRemoteKeySet(ctx, g.JWKSUrl)
ctx = gooidc.ClientContext(ctx, g.reg.HTTPClient(ctx).HTTPClient)
return verifyToken(ctx, keySet, g.config, rawIDToken, issuerURLGoogle)
}
var _ NonceValidationSkipper = new(ProviderGoogle)
func (g *ProviderGoogle) CanSkipNonce(c *Claims) bool {
// Not all SDKs support nonce validation, so we skip it if no nonce is present in the claims of the ID Token.
return c.Nonce == ""
}

View File

@@ -0,0 +1,138 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/selfservice/strategy/oidc"
"github.com/ory/kratos/x"
)
func TestProviderGoogle_Scope(t *testing.T) {
_, reg := pkg.NewVeryFastRegistryWithoutDB(t)
p := oidc.NewProviderGoogle(&oidc.Configuration{
Provider: "google",
ID: "valid",
ClientID: "client",
ClientSecret: "secret",
Mapper: "file://./stub/hydra.schema.json",
RequestedClaims: nil,
Scope: []string{"email", "profile", "offline_access"},
}, reg)
c, _ := p.(oidc.OAuth2Provider).OAuth2(context.Background())
assert.NotContains(t, c.Scopes, "offline_access")
}
func TestProviderGoogle_AccessType(t *testing.T) {
_, reg := pkg.NewVeryFastRegistryWithoutDB(t)
p := oidc.NewProviderGoogle(&oidc.Configuration{
Provider: "google",
ID: "valid",
ClientID: "client",
ClientSecret: "secret",
Mapper: "file://./stub/hydra.schema.json",
RequestedClaims: nil,
Scope: []string{"email", "profile", "offline_access"},
}, reg)
r := &login.Flow{
ID: x.NewUUID(),
}
options := p.(oidc.OAuth2Provider).AuthCodeURLOptions(r)
assert.Contains(t, options, oauth2.AccessTypeOffline)
}
func TestGoogleVerify(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
_, _ = w.Write(publicJWKS)
}))
tsOtherJWKS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
_, _ = w.Write(publicJWKS2)
}))
makeClaims := func(aud string) jwt.RegisteredClaims {
return jwt.RegisteredClaims{
Issuer: "https://accounts.google.com",
Subject: "acme@ory.sh",
Audience: jwt.ClaimStrings{aud},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
}
}
createProvider := func(jwksUrl string) *oidc.ProviderGoogle {
_, reg := pkg.NewVeryFastRegistryWithoutDB(t)
p := oidc.NewProviderGoogle(&oidc.Configuration{
Provider: "google",
ID: "valid",
ClientID: "com.example.app",
ClientSecret: "secret",
Mapper: "file://./stub/hydra.schema.json",
RequestedClaims: nil,
Scope: []string{"email", "profile", "offline_access"},
}, reg).(*oidc.ProviderGoogle)
p.JWKSUrl = jwksUrl
return p
}
t.Run("case=successful verification", func(t *testing.T) {
p := createProvider(ts.URL)
token := createIDToken(t, makeClaims("com.example.app"))
c, err := p.Verify(context.Background(), token)
require.NoError(t, err)
assert.Equal(t, "acme@ory.sh", c.Email)
assert.Equal(t, "acme@ory.sh", c.Subject)
assert.Equal(t, "https://accounts.google.com", c.Issuer)
})
t.Run("case=fails due to client_id mismatch", func(t *testing.T) {
p := createProvider(ts.URL)
token := createIDToken(t, makeClaims("com.different-example.app"))
_, err := p.Verify(context.Background(), token)
require.Error(t, err)
assert.Equal(t, `token audience didn't match allowed audiences: [com.example.app] oidc: expected audience "com.example.app" got ["com.different-example.app"]`, err.Error())
})
t.Run("case=fails due to jwks mismatch", func(t *testing.T) {
p := createProvider(tsOtherJWKS.URL)
token := createIDToken(t, makeClaims("com.example.app"))
_, err := p.Verify(context.Background(), token)
require.Error(t, err)
assert.Equal(t, "failed to verify signature: failed to verify id token signature", err.Error())
})
t.Run("case=succeedes with additional id token audience", func(t *testing.T) {
_, reg := pkg.NewFastRegistryWithMocks(t)
google := oidc.NewProviderGoogle(&oidc.Configuration{
ClientID: "something.else.app",
AdditionalIDTokenAudiences: []string{"com.example.app"},
}, reg).(*oidc.ProviderGoogle)
google.JWKSUrl = ts.URL
token := createIDToken(t, makeClaims("com.example.app"))
_, err := google.Verify(context.Background(), token)
require.NoError(t, err)
})
}

View File

@@ -0,0 +1,118 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"net/url"
"os"
"slices"
"strings"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/ory/herodot"
"github.com/ory/x/urlx"
)
// jacksonTrustedIssuersEnv is a comma-separated list of token issuers accepted
// in addition to the configured issuer URL. It keeps ID tokens verifiable while
// the issuer URL migrates from one domain to another: set it to the old and new
// issuers for the duration of the migration, then unset it.
const jacksonTrustedIssuersEnv = "SAML_SP_TRUSTED_ISSUERS"
type ProviderJackson struct {
*ProviderGenericOIDC
}
func NewProviderJackson(
config *Configuration,
reg Dependencies,
) Provider {
return &ProviderJackson{
ProviderGenericOIDC: &ProviderGenericOIDC{
config: config,
reg: reg,
},
}
}
func (j *ProviderJackson) setProvider(ctx context.Context) {
if j.p == nil {
internalHost := strings.TrimSuffix(j.config.TokenURL, "/api/oauth/token")
config := oidc.ProviderConfig{
IssuerURL: j.config.IssuerURL,
AuthURL: j.config.AuthURL,
TokenURL: j.config.TokenURL,
DeviceAuthURL: "",
UserInfoURL: internalHost + "/api/oauth/userinfo",
JWKSURL: internalHost + "/oauth/jwks",
Algorithms: []string{"RS256"},
}
j.p = config.NewProvider(j.withHTTPClientContext(ctx))
}
}
func (j *ProviderJackson) OAuth2(ctx context.Context) (*oauth2.Config, error) {
j.setProvider(ctx)
endpoint := j.p.Endpoint()
config := j.oauth2ConfigFromEndpoint(ctx, endpoint)
config.RedirectURL = urlx.AppendPaths(
j.reg.Config().SAMLRedirectURIBase(ctx),
"/self-service/methods/saml/callback/"+j.config.ID,
).String()
return config, nil
}
func (j *ProviderJackson) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) {
j.setProvider(ctx)
raw, ok := exchange.Extra("id_token").(string)
if !ok || len(raw) == 0 {
return nil, errors.WithStack(ErrIDTokenMissing())
}
// go-oidc's SkipIssuerCheck defers issuer validation to the caller. The
// issuer is verified below against the configured issuer plus the
// JacksonTrustedIssuersEnv allowlist.
token, err := j.p.VerifierContext(
j.withHTTPClientContext(ctx),
&oidc.Config{ClientID: j.config.ClientID, SkipIssuerCheck: true},
).Verify(ctx, raw)
if err != nil {
return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err))
}
if token.Issuer != j.config.IssuerURL && !slices.Contains(jacksonTrustedIssuers(), token.Issuer) {
return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf(
"oidc: id token issued by a different provider, expected %q got %q", j.config.IssuerURL, token.Issuer,
))
}
var claims Claims
if err := token.Claims(&claims); err != nil {
return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err))
}
var rawClaims map[string]interface{}
if err := token.Claims(&rawClaims); err != nil {
return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err))
}
claims.RawClaims = rawClaims
return &claims, nil
}
func jacksonTrustedIssuers() []string {
var issuers []string
for iss := range strings.SplitSeq(os.Getenv(jacksonTrustedIssuersEnv), ",") {
if iss = strings.TrimSpace(iss); iss != "" {
issuers = append(issuers, iss)
}
}
return issuers
}

View File

@@ -0,0 +1,137 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc_test
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/ory/herodot"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/selfservice/strategy/oidc"
)
func TestProviderJackson(t *testing.T) {
_, reg := pkg.NewVeryFastRegistryWithoutDB(t)
j := oidc.NewProviderJackson(&oidc.Configuration{
Provider: "jackson",
IssuerURL: "https://www.jackson.com/oauth",
AuthURL: "https://www.jackson.com/oauth/auth",
TokenURL: "https://www.jackson.com/api/oauth/token",
Mapper: "file://./stub/hydra.schema.json",
Scope: []string{"email", "profile"},
ID: "some-id",
}, reg)
assert.NotNil(t, j)
c, err := j.(oidc.OAuth2Provider).OAuth2(context.Background())
require.NoError(t, err)
assert.True(t, strings.HasSuffix(c.RedirectURL, "/self-service/methods/saml/callback/some-id"))
}
func TestProviderJacksonClaims(t *testing.T) {
jwks := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
_, _ = w.Write(publicJWKS)
}))
t.Cleanup(jwks.Close)
otherJWKS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
_, _ = w.Write(publicJWKS2)
}))
t.Cleanup(otherJWKS.Close)
const issuer = "https://sp.example.org/saml"
newJackson := func(t *testing.T, upstream *httptest.Server) oidc.OAuth2Provider {
_, reg := pkg.NewVeryFastRegistryWithoutDB(t)
return oidc.NewProviderJackson(&oidc.Configuration{
Provider: "jackson",
ID: "jackson",
ClientID: "client-id",
IssuerURL: issuer,
AuthURL: upstream.URL + "/api/oauth/authorize",
TokenURL: upstream.URL + "/api/oauth/token",
Mapper: "file://./stub/hydra.schema.json",
}, reg).(oidc.OAuth2Provider)
}
makeExchange := func(t *testing.T, iss string) *oauth2.Token {
raw := createIDToken(t, jwt.RegisteredClaims{
Issuer: iss,
Subject: "acme@ory.sh",
Audience: jwt.ClaimStrings{"client-id"},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
})
return (&oauth2.Token{}).WithExtra(map[string]interface{}{"id_token": raw})
}
t.Run("case=accepts the configured issuer", func(t *testing.T) {
claims, err := newJackson(t, jwks).Claims(t.Context(), makeExchange(t, issuer), url.Values{})
require.NoError(t, err)
assert.Equal(t, issuer, claims.Issuer)
assert.Equal(t, "acme@ory.sh", claims.Subject)
assert.Equal(t, "acme@ory.sh", claims.Email)
})
requireReasonContains := func(t *testing.T, err error, substr string) {
t.Helper()
he := new(herodot.DefaultError)
require.ErrorAs(t, err, &he)
assert.Contains(t, he.ReasonField, substr)
}
t.Run("case=rejects an unknown issuer", func(t *testing.T) {
_, err := newJackson(t, jwks).Claims(t.Context(), makeExchange(t, "https://old.example.org/saml"), url.Values{})
requireReasonContains(t, err, "https://old.example.org/saml")
})
t.Run("case=accepts an additional trusted issuer", func(t *testing.T) {
t.Setenv("SAML_SP_TRUSTED_ISSUERS", "https://old.example.org/saml, https://other.example.org/saml")
claims, err := newJackson(t, jwks).Claims(t.Context(), makeExchange(t, "https://old.example.org/saml"), url.Values{})
require.NoError(t, err)
assert.Equal(t, "https://old.example.org/saml", claims.Issuer)
claims, err = newJackson(t, jwks).Claims(t.Context(), makeExchange(t, "https://other.example.org/saml"), url.Values{})
require.NoError(t, err)
assert.Equal(t, "https://other.example.org/saml", claims.Issuer)
claims, err = newJackson(t, jwks).Claims(t.Context(), makeExchange(t, issuer), url.Values{})
require.NoError(t, err)
assert.Equal(t, issuer, claims.Issuer)
})
t.Run("case=rejects an issuer outside the trusted list", func(t *testing.T) {
t.Setenv("SAML_SP_TRUSTED_ISSUERS", "https://old.example.org/saml")
_, err := newJackson(t, jwks).Claims(t.Context(), makeExchange(t, "https://evil.example.org/saml"), url.Values{})
requireReasonContains(t, err, "https://evil.example.org/saml")
})
t.Run("case=rejects a token signed by an unknown key even for a trusted issuer", func(t *testing.T) {
t.Setenv("SAML_SP_TRUSTED_ISSUERS", "https://old.example.org/saml")
_, err := newJackson(t, otherJWKS).Claims(t.Context(), makeExchange(t, "https://old.example.org/saml"), url.Values{})
requireReasonContains(t, err, "failed to verify signature")
})
t.Run("case=rejects a missing id token", func(t *testing.T) {
_, err := newJackson(t, jwks).Claims(t.Context(), &oauth2.Token{}, url.Values{})
require.Error(t, err)
})
}

View File

@@ -0,0 +1,117 @@
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"encoding/json"
"net/url"
"github.com/hashicorp/go-retryablehttp"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/ory/herodot"
"github.com/ory/x/httpx"
)
var _ OAuth2Provider = (*ProviderLark)(nil)
type ProviderLark struct {
*ProviderGenericOIDC
}
var (
larkAuthEndpoint = oauth2.Endpoint{
AuthURL: "https://passport.feishu.cn/suite/passport/oauth/authorize",
TokenURL: "https://passport.feishu.cn/suite/passport/oauth/token",
AuthStyle: oauth2.AuthStyleInParams,
}
larkUserEndpoint = "https://passport.feishu.cn/suite/passport/oauth/userinfo"
)
func NewProviderLark(
config *Configuration,
reg Dependencies,
) Provider {
return &ProviderLark{
&ProviderGenericOIDC{
config: config,
reg: reg,
},
}
}
func (pl *ProviderLark) Config() *Configuration {
return pl.config
}
func (pl *ProviderLark) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return &oauth2.Config{
ClientID: pl.config.ClientID,
ClientSecret: pl.config.ClientSecret,
Endpoint: larkAuthEndpoint,
// Lark uses fixed scope that can not be configured in runtime
Scopes: pl.config.Scope,
RedirectURL: pl.config.Redir(pl.reg.Config().OIDCRedirectURIBase(ctx)),
}, nil
}
func (pl *ProviderLark) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) {
// larkClaim is defined in the https://open.feishu.cn/document/common-capabilities/sso/api/get-user-info
type larkClaim struct {
Sub string `json:"sub"`
Name string `json:"name"`
Picture string `json:"picture"`
OpenID string `json:"open_id"`
UnionID string `json:"union_id"`
EnName string `json:"en_name"`
TenantKey string `json:"tenant_key"`
AvatarURL string `json:"avatar_url"`
AvatarThumb string `json:"avatar_thumb"`
AvatarMiddle string `json:"avatar_middle"`
AvatarBig string `json:"avatar_big"`
Email string `json:"email"`
UserID string `json:"user_id"`
Mobile string `json:"mobile"`
}
var (
client = pl.reg.HTTPClient(ctx, httpx.ResilientClientDisallowInternalIPs())
user larkClaim
)
req, err := retryablehttp.NewRequest("GET", larkUserEndpoint, nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
exchange.SetAuthHeader(req.Request)
res, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
defer func() { _ = res.Body.Close() }()
if err := logUpstreamError(pl.reg.Logger(), res); err != nil {
return nil, err
}
if err := json.NewDecoder(res.Body).Decode(&user); err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
return &Claims{
Issuer: larkUserEndpoint,
Subject: user.OpenID,
Name: user.Name,
Nickname: user.Name,
Picture: user.AvatarURL,
Email: user.Email,
PhoneNumber: user.Mobile,
}, nil
}
func (pl *ProviderLark) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
return []oauth2.AuthCodeOption{}
}

View File

@@ -0,0 +1,39 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"golang.org/x/oauth2"
)
type ProviderLineV21 struct {
*ProviderGenericOIDC
}
func NewProviderLineV21(
config *Configuration,
reg Dependencies,
) Provider {
config.IssuerURL = "https://access.line.me"
return &ProviderLineV21{
&ProviderGenericOIDC{
config: config,
reg: reg,
},
}
}
func (g *ProviderLineV21) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
o, err := g.OAuth2(ctx)
if err != nil {
return nil, err
}
// Line login requires adding id_token_key_type=JWK when getting the token in order to issue an ES256 token.
// https://blog.miniasp.com/post/2022/04/08/LINE-Login-with-OpenID-Connect-in-ASPNET-Core (Chinese)
opts = append(opts, oauth2.SetAuthURLParam("id_token_key_type", "JWK"))
return o.Exchange(ctx, code, opts...)
}

View File

@@ -0,0 +1,200 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/ory/x/otelx"
"github.com/hashicorp/go-retryablehttp"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"golang.org/x/oauth2/linkedin"
"github.com/ory/herodot"
"github.com/ory/x/httpx"
)
type LinkedInProfile struct {
LocalizedLastName string `json:"localizedLastName"`
LocalizedFirstName string `json:"localizedFirstName"`
ProfilePicture *struct {
DisplayImage struct {
Elements []struct {
Identifiers []struct {
Identifier string `json:"identifier"`
} `json:"identifiers"`
} `json:"elements"`
} `json:"displayImage~"`
} `json:"profilePicture,omitempty"`
ID string `json:"id"`
}
type LinkedInEmail struct {
Elements []struct {
Handle struct {
EmailAddress string `json:"emailAddress"`
} `json:"handle~"`
HandleUrn string `json:"handle"`
} `json:"elements"`
}
type LinkedInIntrospection struct {
Active bool `json:"active"`
ClientID string `json:"client_id"`
AuthorizedAt uint32 `json:"authorized_at"`
CreatedAt uint32 `json:"created_at"`
ExpiresAt uint32 `json:"expires_at"`
Status string `json:"status"`
Scope string `json:"scope"`
AuthType string `json:"auth_type"`
}
// type APIUrl string
const (
ProfileURL string = "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,profilePicture(displayImage~digitalmediaAsset:playableStreams))"
EmailURL string = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))"
IntrospectionURL string = "https://www.linkedin.com/oauth/v2/introspectToken"
)
var _ OAuth2Provider = (*ProviderLinkedIn)(nil)
type ProviderLinkedIn struct {
config *Configuration
reg Dependencies
}
func NewProviderLinkedIn(
config *Configuration,
reg Dependencies,
) Provider {
return &ProviderLinkedIn{
config: config,
reg: reg,
}
}
func (l *ProviderLinkedIn) Config() *Configuration {
return l.config
}
func (l *ProviderLinkedIn) oauth2(ctx context.Context) *oauth2.Config {
return &oauth2.Config{
ClientID: l.config.ClientID,
ClientSecret: l.config.ClientSecret,
Endpoint: linkedin.Endpoint,
Scopes: l.config.Scope,
RedirectURL: l.config.Redir(l.reg.Config().OIDCRedirectURIBase(ctx)),
}
}
func (l *ProviderLinkedIn) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return l.oauth2(ctx), nil
}
func (l *ProviderLinkedIn) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
return []oauth2.AuthCodeOption{}
}
func (l *ProviderLinkedIn) fetch(ctx context.Context, client *retryablehttp.Client, url string, result interface{}) (err error) {
ctx, span := l.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderLinkedIn.fetch")
defer otelx.End(span, &err)
req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return errors.WithStack(err)
}
res, err := client.Do(req)
if err != nil {
return errors.WithStack(err)
}
defer func() { _ = res.Body.Close() }()
if err := logUpstreamError(l.reg.Logger(), res); err != nil {
return err
}
if err := json.NewDecoder(res.Body).Decode(result); err != nil {
return errors.WithStack(err)
}
return nil
}
func (l *ProviderLinkedIn) Profile(ctx context.Context, client *retryablehttp.Client) (*LinkedInProfile, error) {
var result LinkedInProfile
if err := l.fetch(ctx, client, ProfileURL, &result); err != nil {
return nil, errors.WithStack(err)
}
return &result, nil
}
func (l *ProviderLinkedIn) Email(ctx context.Context, client *retryablehttp.Client) (*LinkedInEmail, error) {
var result LinkedInEmail
if err := l.fetch(ctx, client, EmailURL, &result); err != nil {
return nil, errors.WithStack(err)
}
return &result, nil
}
func (l *ProviderLinkedIn) ProfilePicture(profile *LinkedInProfile) string {
if profile.ProfilePicture == nil {
return ""
}
elements := profile.ProfilePicture.DisplayImage.Elements
i := len(elements)
if i == 0 {
return ""
} else if i > 3 {
i = 3
}
identifiers := elements[i-1].Identifiers
if len(identifiers) == 0 {
return ""
}
return identifiers[0].Identifier
}
func (l *ProviderLinkedIn) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *Claims, err error) {
ctx, span := l.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderLinkedIn.Claims")
defer otelx.End(span, &err)
o, err := l.OAuth2(ctx)
if err != nil {
return nil, err
}
ctx, client := httpx.SetOAuth2(ctx, l.reg.HTTPClient(ctx), o, exchange)
profile, err := l.Profile(ctx, client)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
email, err := l.Email(ctx, client)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
claims := &Claims{
Subject: profile.ID,
Issuer: "https://login.linkedin.com/",
Email: email.Elements[0].Handle.EmailAddress,
GivenName: profile.LocalizedFirstName,
LastName: profile.LocalizedLastName,
Picture: l.ProfilePicture(profile),
}
return claims, nil
}

View File

@@ -0,0 +1,209 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc_test
import (
"context"
"net/http"
"net/url"
"testing"
"time"
"github.com/hashicorp/go-retryablehttp"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/selfservice/strategy/oidc"
)
func TestProviderLinkedin_Claims(t *testing.T) {
_, base := pkg.NewFastRegistryWithMocks(t)
reg := &mockRegistry{base, retryablehttp.NewClient()}
httpmock.ActivateNonDefault(reg.cl.HTTPClient)
t.Cleanup(httpmock.DeactivateAndReset)
httpmock.RegisterResponder("GET", "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,profilePicture(displayImage~digitalmediaAsset:playableStreams))",
func(req *http.Request) (*http.Response, error) {
resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{
"localizedLastName": "Doe",
"profilePicture": map[string]interface{}{
"displayImage": "urn:li:digitalmediaAsset:D5F04AQG1h-anJtsLeA",
"displayImage~": map[string]interface{}{
"elements": []map[string]interface{}{
{
"identifiers": []map[string]interface{}{
{
"identifier": "https://media-exp1.licdn.com/dms/image/D5F04AQG1h-anJtsLeA/profile-displayphoto-shrink_100_100/0/1661502148285?e=1674086400&v=beta&t=5zdsvAqGS1idrfNAai-zwp01K4hApGeJyRV8zBTjZck",
},
},
},
{
"identifiers": []map[string]interface{}{
{
"identifier": "https://media-exp1.licdn.com/dms/image/D5F04AQG1h-anJtsLeA/profile-displayphoto-shrink_200_200/0/1661502148285?e=1674086400&v=beta&t=fbHprGci4vEpeQdzRTyt9YJjNXTgv-14h9ROr_DE5R4",
},
},
},
{
"identifiers": []map[string]interface{}{
{
"identifier": "https://media-exp1.licdn.com/dms/image/D5F04AQG1h-anJtsLeA/profile-displayphoto-shrink_400_400/0/1661502148285?e=1674086400&v=beta&t=SMgXuiZ5DiiPf4iMrTPQWoCnFRh4Y-DfoWjopUZ0qCo",
},
},
},
{
"identifiers": []map[string]interface{}{
{
"identifier": "https://media-exp1.licdn.com/dms/image/D5F04AQG1h-anJtsLeA/profile-displayphoto-shrink_800_800/0/1661502148285?e=1674086400&v=beta&t=QzC9b32DmLRIwU7P27LfJUqglXsJm056WkA_TTOHxHU",
},
},
},
},
},
},
"localizedFirstName": "John",
"id": "5foOWOiYXD",
})
return resp, err
})
httpmock.RegisterResponder("GET", "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))",
func(req *http.Request) (*http.Response, error) {
resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{
"elements": []map[string]interface{}{
{
"handle~": map[string]interface{}{
"emailAddress": "john.doe@gmail.com",
},
"handle": "urn:li:emailAddress:-1810278278",
},
},
})
return resp, err
},
)
httpmock.RegisterResponder("POST", "https://www.linkedin.com/oauth/v2/introspectToken",
func(req *http.Request) (*http.Response, error) {
resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{
"active": true,
"client_id": "78e533g1dq0rs5",
"authorized_at": 1668518671,
"created_at": 1668518671,
"status": "active",
"expires_at": 2673702676,
"scope": "r_emailaddress,r_liteprofile",
"auth_type": "3L",
})
return resp, err
},
)
c := &oidc.Configuration{
ID: "linkedin",
Provider: "linkedin",
ClientID: "abcd",
ClientSecret: "secret",
Mapper: "file://./stub/oidc.linkedin.jsonnet",
Scope: []string{"r_emailaddress", "r_liteprofile"},
}
linkedin := oidc.NewProviderLinkedIn(c, reg)
const fakeLinkedinIDToken = "id_token_mock_" // #nosec G101 test code
actual, err := linkedin.(oidc.OAuth2Provider).Claims(
context.Background(),
(&oauth2.Token{AccessToken: "foo", Expiry: time.Now().Add(time.Hour)}).WithExtra(map[string]interface{}{"id_token": fakeLinkedinIDToken}),
url.Values{},
)
require.NoError(t, err)
assert.Equal(t, &oidc.Claims{
Issuer: "https://login.linkedin.com/",
Subject: "5foOWOiYXD",
GivenName: "John",
LastName: "Doe",
Email: "john.doe@gmail.com",
Picture: "https://media-exp1.licdn.com/dms/image/D5F04AQG1h-anJtsLeA/profile-displayphoto-shrink_400_400/0/1661502148285?e=1674086400&v=beta&t=SMgXuiZ5DiiPf4iMrTPQWoCnFRh4Y-DfoWjopUZ0qCo",
}, actual)
}
func TestProviderLinkedin_No_Picture(t *testing.T) {
_, base := pkg.NewFastRegistryWithMocks(t)
reg := &mockRegistry{base, retryablehttp.NewClient()}
httpmock.ActivateNonDefault(reg.cl.HTTPClient)
t.Cleanup(httpmock.DeactivateAndReset)
httpmock.RegisterResponder("GET", "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,profilePicture(displayImage~digitalmediaAsset:playableStreams))",
func(req *http.Request) (*http.Response, error) {
resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{
"localizedLastName": "Doe",
"localizedFirstName": "John",
"id": "5foOWOiYXD",
})
return resp, err
})
httpmock.RegisterResponder("GET", "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))",
func(req *http.Request) (*http.Response, error) {
resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{
"elements": []map[string]interface{}{
{
"handle~": map[string]interface{}{
"emailAddress": "john.doe@gmail.com",
},
"handle": "urn:li:emailAddress:-1810278278",
},
},
})
return resp, err
},
)
httpmock.RegisterResponder("POST", "https://www.linkedin.com/oauth/v2/introspectToken",
func(req *http.Request) (*http.Response, error) {
resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{
"active": true,
"client_id": "78e533g1dq0rs5",
"authorized_at": 1668518671,
"created_at": 1668518671,
"status": "active",
"expires_at": 2673702676,
"scope": "r_emailaddress,r_liteprofile",
"auth_type": "3L",
})
return resp, err
},
)
c := &oidc.Configuration{
ID: "linkedin",
Provider: "linkedin",
ClientID: "abcd",
ClientSecret: "secret",
Mapper: "file://./stub/oidc.linkedin.jsonnet",
Scope: []string{"r_emailaddress", "r_liteprofile"},
}
linkedin := oidc.NewProviderLinkedIn(c, reg)
const fakeLinkedinIDToken = "id_token_mock_" // #nosec G101 test code
actual, err := linkedin.(oidc.OAuth2Provider).Claims(
context.Background(),
(&oauth2.Token{AccessToken: "foo", Expiry: time.Now().Add(time.Hour)}).WithExtra(map[string]interface{}{"id_token": fakeLinkedinIDToken}),
url.Values{},
)
require.NoError(t, err)
assert.Equal(t, &oidc.Claims{
Issuer: "https://login.linkedin.com/",
Subject: "5foOWOiYXD",
GivenName: "John",
LastName: "Doe",
Email: "john.doe@gmail.com",
Picture: "",
}, actual)
}

View File

@@ -0,0 +1,17 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
func NewProviderLinkedInV2(
config *Configuration,
reg Dependencies,
) Provider {
config.ClaimsSource = ClaimsSourceUserInfo
config.IssuerURL = "https://www.linkedin.com/oauth"
return &ProviderGenericOIDC{
config: config,
reg: reg,
}
}

View File

@@ -0,0 +1,33 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/selfservice/strategy/oidc"
)
func TestProviderLinkedInV2_Discovery(t *testing.T) {
_, reg := pkg.NewVeryFastRegistryWithoutDB(t)
p := oidc.NewProviderLinkedInV2(&oidc.Configuration{
Provider: "linkedin_v2",
ID: "valid",
ClientID: "client",
ClientSecret: "secret",
Mapper: "file://./stub/hydra.schema.json",
RequestedClaims: nil,
Scope: []string{"email", "profile", "offline_access"},
}, reg)
c, err := p.(oidc.OAuth2Provider).OAuth2(t.Context())
require.NoError(t, err)
assert.Contains(t, c.Scopes, "openid")
assert.Equal(t, "https://www.linkedin.com/oauth/v2/accessToken", c.Endpoint.TokenURL)
}

View File

@@ -0,0 +1,132 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"encoding/json"
"net/url"
"strings"
gooidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/gofrs/uuid"
"github.com/golang-jwt/jwt/v4"
"github.com/hashicorp/go-retryablehttp"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/ory/herodot"
"github.com/ory/x/httpx"
)
var _ OAuth2Provider = (*ProviderMicrosoft)(nil)
type ProviderMicrosoft struct {
*ProviderGenericOIDC
}
func NewProviderMicrosoft(
config *Configuration,
reg Dependencies,
) Provider {
return &ProviderMicrosoft{
ProviderGenericOIDC: &ProviderGenericOIDC{
config: config,
reg: reg,
},
}
}
func (m *ProviderMicrosoft) OAuth2(ctx context.Context) (*oauth2.Config, error) {
if strings.TrimSpace(m.config.Tenant) == "" {
return nil, errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("No Tenant specified for the `microsoft` oidc provider %s", m.config.ID))
}
endpointPrefix := "https://login.microsoftonline.com/" + m.config.Tenant
endpoint := oauth2.Endpoint{
AuthURL: endpointPrefix + "/oauth2/v2.0/authorize",
TokenURL: endpointPrefix + "/oauth2/v2.0/token",
}
return m.oauth2ConfigFromEndpoint(ctx, endpoint), nil
}
func (m *ProviderMicrosoft) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) {
raw, ok := exchange.Extra("id_token").(string)
if !ok || len(raw) == 0 {
return nil, errors.WithStack(ErrIDTokenMissing())
}
parser := new(jwt.Parser)
unverifiedClaims := microsoftUnverifiedClaims{}
if _, _, err := parser.ParseUnverified(raw, &unverifiedClaims); err != nil {
return nil, err
}
if _, err := uuid.FromString(unverifiedClaims.TenantID); err != nil {
return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("TenantID claim is not a valid UUID: %s", err))
}
issuer := "https://login.microsoftonline.com/" + unverifiedClaims.TenantID + "/v2.0"
ctx = context.WithValue(ctx, oauth2.HTTPClient, m.reg.HTTPClient(ctx).HTTPClient)
p, err := gooidc.NewProvider(ctx, issuer)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("Unable to initialize OpenID Connect Provider: %s", err))
}
claims, err := m.verifyAndDecodeClaimsWithProvider(ctx, p, raw)
if err != nil {
return nil, err
}
return m.updateSubject(ctx, claims, exchange)
}
func (m *ProviderMicrosoft) updateSubject(ctx context.Context, claims *Claims, exchange *oauth2.Token) (*Claims, error) {
if m.config.SubjectSource == "me" {
o, err := m.OAuth2(ctx)
if err != nil {
return nil, err
}
ctx, client := httpx.SetOAuth2(ctx, m.reg.HTTPClient(ctx), o, exchange)
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", "https://graph.microsoft.com/v1.0/me", nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("Unable to fetch from `https://graph.microsoft.com/v1.0/me`: %s", err))
}
defer func() { _ = resp.Body.Close() }()
if err := logUpstreamError(m.reg.Logger(), resp); err != nil {
return nil, err
}
var user struct {
ID string `json:"id"`
}
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("Unable to decode JSON from `https://graph.microsoft.com/v1.0/me`: %s", err))
}
claims.Subject = user.ID
}
if m.config.SubjectSource == "oid" {
claims.Subject = claims.Object
}
return claims, nil
}
type microsoftUnverifiedClaims struct {
TenantID string `json:"tid,omitempty"`
}
func (c *microsoftUnverifiedClaims) Valid() error {
return nil
}

View File

@@ -0,0 +1,174 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"encoding/json"
"fmt"
"net/url"
"slices"
"strings"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/hashicorp/go-retryablehttp"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/ory/herodot"
"github.com/ory/x/httpx"
"github.com/ory/x/urlx"
)
const (
defaultBrokerScheme = "https"
defaultBrokerHost = "broker.netid.de"
)
var _ OAuth2Provider = (*ProviderNetID)(nil)
type ProviderNetID struct {
*ProviderGenericOIDC
}
func NewProviderNetID(
config *Configuration,
reg Dependencies,
) Provider {
config.IssuerURL = fmt.Sprintf("%s://%s/", defaultBrokerScheme, defaultBrokerHost)
if !slices.Contains(config.Scope, oidc.ScopeOpenID) {
config.Scope = append(config.Scope, oidc.ScopeOpenID)
}
return &ProviderNetID{
ProviderGenericOIDC: &ProviderGenericOIDC{
config: config,
reg: reg,
},
}
}
func (n *ProviderNetID) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return n.oAuth2(ctx)
}
func (n *ProviderNetID) oAuth2(ctx context.Context) (*oauth2.Config, error) {
u := n.brokerURL()
authURL := urlx.AppendPaths(u, "/authorize")
tokenURL := urlx.AppendPaths(u, "/token")
return &oauth2.Config{
ClientID: n.config.ClientID,
ClientSecret: n.config.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: authURL.String(),
TokenURL: tokenURL.String(),
},
Scopes: n.config.Scope,
RedirectURL: n.config.Redir(n.reg.Config().OIDCRedirectURIBase(ctx)),
}, nil
}
func (n *ProviderNetID) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) {
o, err := n.OAuth2(ctx)
if err != nil {
return nil, err
}
ctx, client := httpx.SetOAuth2(ctx, n.reg.HTTPClient(ctx), o, exchange)
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", urlx.AppendPaths(n.brokerURL(), "/userinfo").String(), nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("%s", err))
}
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
defer func() { _ = resp.Body.Close() }()
if err := logUpstreamError(n.reg.Logger(), resp); err != nil {
return nil, err
}
p, err := n.provider(ctx)
if err != nil {
return nil, err
}
raw, ok := exchange.Extra("id_token").(string)
if !ok || len(raw) == 0 {
return nil, errors.WithStack(ErrIDTokenMissing())
}
claims, err := n.verifyAndDecodeClaimsWithProvider(ctx, p, raw)
if err != nil {
return nil, err
}
var userinfo Claims
if err := json.NewDecoder(resp.Body).Decode(&userinfo); err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
userinfo.Issuer = claims.Issuer
userinfo.Subject = claims.Subject
return &userinfo, nil
}
func (n *ProviderNetID) Verify(ctx context.Context, rawIDToken string) (*Claims, error) {
provider, err := n.provider(ctx)
if err != nil {
return nil, err
}
req, err := retryablehttp.NewRequestWithContext(ctx, "POST", urlx.AppendPaths(n.brokerURL(), "/token").String(), strings.NewReader(url.Values{
"grant_type": {"netid_fedcm"},
"fedcm_token": {rawIDToken},
}.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Origin", n.config.NetIDTokenOriginHeader)
res, err := n.reg.HTTPClient(ctx).Do(req)
if err != nil {
return nil, err
}
token := struct {
IDToken string `json:"id_token"`
}{}
if err := json.NewDecoder(res.Body).Decode(&token); err != nil {
return nil, err
}
idToken, err := provider.VerifierContext(
n.withHTTPClientContext(ctx),
&oidc.Config{ClientID: n.config.ClientID},
).Verify(ctx, token.IDToken)
if err != nil {
return nil, err
}
var (
claims Claims
rawClaims map[string]any
)
if err = idToken.Claims(&claims); err != nil {
return nil, err
}
if err = idToken.Claims(&rawClaims); err != nil {
return nil, err
}
claims.RawClaims = rawClaims
return &claims, nil
}
func (n *ProviderNetID) brokerURL() *url.URL {
return &url.URL{Scheme: defaultBrokerScheme, Host: defaultBrokerHost}
}

View File

@@ -0,0 +1,29 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/selfservice/strategy/oidc"
)
func TestNetidProvider(t *testing.T) {
t.Skip("can't test this automatically, because the token is only valid for a short time")
_, reg := pkg.NewVeryFastRegistryWithoutDB(t)
p := oidc.NewProviderNetID(&oidc.Configuration{
ClientID: "9b56b26a-e93d-4fce-8f16-951a9858f23e",
}, reg)
rawToken := `...`
claims, err := p.(oidc.IDTokenVerifier).Verify(context.Background(), rawToken)
require.NoError(t, err)
require.NotNil(t, claims)
}

View File

@@ -0,0 +1,124 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"encoding/json"
"net/url"
"github.com/hashicorp/go-retryablehttp"
"github.com/ory/x/httpx"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/ory/herodot"
)
var _ OAuth2Provider = (*ProviderPatreon)(nil)
type ProviderPatreon struct {
config *Configuration
reg Dependencies
}
type PatreonIdentityResponse struct {
Data struct {
Attributes struct {
Email string `json:"email"`
FirstName string `json:"first_name"`
FullName string `json:"full_name"`
ImageURL string `json:"image_url"`
LastName string `json:"last_name"`
} `json:"attributes"`
ID string `json:"id"`
Type string `json:"type"`
} `json:"data"`
}
func NewProviderPatreon(
config *Configuration,
reg Dependencies,
) Provider {
return &ProviderPatreon{
config: config,
reg: reg,
}
}
func (d *ProviderPatreon) Config() *Configuration {
return d.config
}
func (d *ProviderPatreon) oauth2(ctx context.Context) *oauth2.Config {
return &oauth2.Config{
ClientID: d.config.ClientID,
ClientSecret: d.config.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: "https://www.patreon.com/oauth2/authorize",
TokenURL: "https://www.patreon.com/api/oauth2/token",
},
RedirectURL: d.config.Redir(d.reg.Config().OIDCRedirectURIBase(ctx)),
Scopes: d.config.Scope,
}
}
func (d *ProviderPatreon) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return d.oauth2(ctx), nil
}
func (d *ProviderPatreon) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
if isForced(r) {
return []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("prompt", "consent"),
}
}
return []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("prompt", "none"),
}
}
func (d *ProviderPatreon) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) {
identityURL := "https://www.patreon.com/api/oauth2/v2/identity?fields%5Buser%5D=first_name,last_name,url,full_name,email,image_url"
o := d.oauth2(ctx)
ctx, client := httpx.SetOAuth2(ctx, d.reg.HTTPClient(ctx), o, exchange)
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", identityURL, nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("%s", err))
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+exchange.AccessToken)
res, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
defer func() { _ = res.Body.Close() }()
if err := logUpstreamError(d.reg.Logger(), res); err != nil {
return nil, err
}
data := PatreonIdentityResponse{}
jsonErr := json.NewDecoder(res.Body).Decode(&data)
if jsonErr != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(jsonErr).WithReasonf("%s", jsonErr))
}
claims := &Claims{
Issuer: "https://www.patreon.com/",
Subject: data.Data.ID,
Name: data.Data.Attributes.FullName,
Email: data.Data.Attributes.Email,
GivenName: data.Data.Attributes.FirstName,
FamilyName: data.Data.Attributes.LastName,
LastName: data.Data.Attributes.LastName,
Picture: data.Data.Attributes.ImageURL,
}
return claims, nil
}

View File

@@ -0,0 +1,108 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc_test
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/selfservice/strategy/oidc"
"github.com/ory/x/configx"
)
const (
wellknownJWKs = "https://raw.githubusercontent.com/aeneasr/private-oidc/master/jwks"
fakeJWTJWKS = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTksImF1ZCI6ImFiY2QiLCJpc3MiOiJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vYWVuZWFzci9wcml2YXRlLW9pZGMvbWFzdGVyL2p3a3MifQ.RLR3dSRGGIjbRqyOMGMYFGTzcVHi7hPuFs_IKYywVWJ_XMyzWozTW4M8uuvBUPiVoNDNs7osm-AkRl7cBfw0by1XEcnEKZStCjdEh7Q0IGGb4hgq8rRqm1d3uJwNIGU5h7-s7tMnDED2ZTZhp304U99YWz7Ozl_TA9tqolBLLZEmIfXSY_RR3rMoDwtHZvWhI0OZtPdcBh86vWS9zG6QPHM5qGtRMMIs-ljXrrgS8LulUI5CAVEeHlQLXroBIe9v89IkKi07A7YRrk1SxFxlojcZ2v0z-0iTI3WL8mUoocF-RYy1RgJTK_dPYkSJebaN0R5MmBax5MXLKy4baNHKsg"
)
func TestProviderPrivateIP(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("the request should not have reached the test server")
w.WriteHeader(http.StatusOK)
}))
t.Cleanup(ts.Close)
_, reg := pkg.NewFastRegistryWithMocks(t,
configx.WithValue(config.ViperKeyClientHTTPNoPrivateIPRanges, true))
generic := func(c *oidc.Configuration) oidc.Provider {
return oidc.NewProviderGenericOIDC(c, reg)
}
auth0 := func(c *oidc.Configuration) oidc.Provider {
return oidc.NewProviderAuth0(c, reg)
}
gitlab := func(c *oidc.Configuration) oidc.Provider {
return oidc.NewProviderGitLab(c, reg)
}
// We do not test the Auth URL as the Auth URL is not vulnerable to SSRF attacks.
// The AuthURL is only given to the user's browser, thus it is not possible to cause SSRF.
// We only care about Token URLs and Issuer URLs.
for k, tc := range []struct {
c *oidc.Configuration
e string
p func(c *oidc.Configuration) oidc.Provider
id string
}{
// Apple uses a fixed token URL and does not use the issuer.
{p: auth0, c: &oidc.Configuration{IssuerURL: ts.URL}, e: "no route to host"},
// The TokenURL is fixed in Auth0 to {issuer_url}/token. Since the issuer is called first, any local token fails also.
// If the issuer URL is local, we fail
{p: generic, c: &oidc.Configuration{IssuerURL: ts.URL}, e: "no route to host", id: fakeJWTJWKS},
{p: generic, c: &oidc.Configuration{IssuerURL: ts.URL, ClaimsSource: "userinfo"}, e: "no route to host", id: fakeJWTJWKS},
{p: generic, c: &oidc.Configuration{IssuerURL: ts.URL, ClaimsSource: "invalid"}, e: "Unknown claims source: \"invalid\"", id: fakeJWTJWKS},
// If the issuer URL has a local JWKs URL, we fail
{p: generic, c: &oidc.Configuration{ClientID: "abcd", IssuerURL: wellknownJWKs}, e: "no route to host", id: fakeJWTJWKS},
{p: generic, c: &oidc.Configuration{ClientID: "abcd", IssuerURL: wellknownJWKs, ClaimsSource: "userinfo"}, e: "no route to host", id: fakeJWTJWKS},
// The next call does not fail because the provider uses only the ID JSON Web Token to verify this call and does
// not use the TokenURL at all!
// {p: generic, c: &oidc.Configuration{ClientID: "abcd", IssuerURL: wellknownToken, TokenURL: "http://127.0.0.3/"}, e: "127.0.0.3 is not a public IP address", id: fakeJWTToken},
// Discord uses a fixed token URL and does not use the issuer.
// Facebook uses a fixed token URL and does not use the issuer.
// GitHub uses a fixed token URL and does not use the issuer.
// GitHub App uses a fixed token URL and does not use the issuer.
// GitHub App uses a fixed token URL and does not use the issuer.
// LinkedInV2 uses a fixed token URL and does not use the issuer.
{p: gitlab, c: &oidc.Configuration{IssuerURL: ts.URL}, e: "no route to host"},
// The TokenURL is fixed in GitLab to {issuer_url}/token. Since the issuer is called first, any local token fails also.
// Google uses a fixed token URL and does not use the issuer.
// Microsoft uses a fixed token URL and does not use the issuer.
// Slack uses a fixed token URL and does not use the issuer.
// Spotify uses a fixed token URL and does not use the issuer.
// VK uses a fixed token URL and does not use the issuer.
// Yandex uses a fixed token URL and does not use the issuer.
// NetID uses a fixed token URL and does not use the issuer.
// X uses a fixed token URL and userinfoRL and does not use the issuer value.
// Line v2.1 uses a fixed token URL and does not use the issuer.
// UAE PASS uses fixed token URL and userinfo URL and does not use the issuer value.
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
p := tc.p(tc.c)
_, err := p.(oidc.OAuth2Provider).Claims(t.Context(), (&oauth2.Token{RefreshToken: "foo", Expiry: time.Now().Add(-time.Hour)}).WithExtra(map[string]interface{}{
"id_token": tc.id,
}), url.Values{})
require.Error(t, err)
assert.Contains(t, fmt.Sprintf("%+v", err), tc.e)
})
}
}

View File

@@ -0,0 +1,144 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"cmp"
"context"
"encoding/json"
"io"
"net/url"
"path"
"time"
"github.com/ory/x/httpx"
"github.com/tidwall/sjson"
"github.com/hashicorp/go-retryablehttp"
"github.com/pkg/errors"
"github.com/tidwall/gjson"
"golang.org/x/oauth2"
"github.com/ory/herodot"
)
var _ OAuth2Provider = (*ProviderSalesforce)(nil)
type ProviderSalesforce struct {
*ProviderGenericOIDC
}
func NewProviderSalesforce(
config *Configuration,
reg Dependencies,
) Provider {
return &ProviderSalesforce{
ProviderGenericOIDC: &ProviderGenericOIDC{
config: config,
reg: reg,
},
}
}
func (g *ProviderSalesforce) oauth2(ctx context.Context) (*oauth2.Config, error) {
endpoint, err := url.Parse(g.config.IssuerURL)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
authURL := *endpoint
tokenURL := *endpoint
authURL.Path = path.Join(authURL.Path, "/services/oauth2/authorize")
tokenURL.Path = path.Join(tokenURL.Path, "/services/oauth2/token")
c := &oauth2.Config{
ClientID: g.config.ClientID,
ClientSecret: g.config.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: authURL.String(),
TokenURL: tokenURL.String(),
},
Scopes: g.config.Scope,
RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)),
}
return c, nil
}
func (g *ProviderSalesforce) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return g.oauth2(ctx)
}
func (g *ProviderSalesforce) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) {
o, err := g.OAuth2(ctx)
if err != nil {
return nil, err
}
u, err := url.Parse(g.config.IssuerURL)
if err != nil {
return nil, errors.WithStack(herodot.ErrMisconfiguration().WithWrap(err).WithReasonf("%s", err))
}
u.Path = path.Join(u.Path, "/services/oauth2/userinfo")
ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), o, exchange)
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", u.String(), nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
defer func() { _ = resp.Body.Close() }()
if err := logUpstreamError(g.reg.Logger(), resp); err != nil {
return nil, err
}
// Once Salesforce fixes this bug, all this workaround can be removed.
b, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
}
b, err = salesforceUpdatedAtWorkaround(b)
if err != nil {
return nil, err
}
// Once we get here, we know that if there is an updated_at field in the json, it is the correct type.
var claims Claims
if err := json.Unmarshal(b, &claims); err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
claims.Issuer = cmp.Or(claims.Issuer, g.config.IssuerURL)
return &claims, nil
}
// There is a bug in the response from Salesforce. The updated_at field may be a string and not an int64.
// https://help.salesforce.com/s/articleView?id=sf.remoteaccess_using_userinfo_endpoint.htm&type=5
// We work around this by reading the json generically (as map[string]inteface{} and looking at the updated_at field
// if it exists. If it's the wrong type (string), we fill out the claims by hand.
func salesforceUpdatedAtWorkaround(body []byte) ([]byte, error) {
// Force updatedAt to be an int if given as a string in the response.
if updatedAtField := gjson.GetBytes(body, "updated_at"); updatedAtField.Exists() && updatedAtField.Type == gjson.String {
t, err := time.Parse(time.RFC3339, updatedAtField.String())
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("bad time format in updated_at"))
}
body, err = sjson.SetBytes(body, "updated_at", t.Unix())
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err))
}
}
return body, nil
}

View File

@@ -0,0 +1,33 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSalesforceUpdatedAtWorkaround(t *testing.T) {
actual, err := salesforceUpdatedAtWorkaround([]byte("{}"))
require.NoError(t, err)
assert.Equal(t, "{}", string(actual))
actual, err = salesforceUpdatedAtWorkaround([]byte(`{"updated_at":1234}`))
require.NoError(t, err)
assert.Equal(t, `{"updated_at":1234}`, string(actual))
timestamp := time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
input, err := json.Marshal(map[string]interface{}{
"updated_at": timestamp,
})
require.NoError(t, err)
actual, err = salesforceUpdatedAtWorkaround(input)
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf(`{"updated_at":%d}`, timestamp.Unix()), string(actual))
}

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