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
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:
55
selfservice/strategy/oidc/.schema/link.schema.json
Normal file
55
selfservice/strategy/oidc/.schema/link.schema.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
42
selfservice/strategy/oidc/.schema/settings.schema.json
Normal file
42
selfservice/strategy/oidc/.schema/settings.schema.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
null
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
8
selfservice/strategy/oidc/const.go
Normal file
8
selfservice/strategy/oidc/const.go
Normal 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"
|
||||
)
|
||||
40
selfservice/strategy/oidc/error.go
Normal file
40
selfservice/strategy/oidc/error.go
Normal 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))
|
||||
}
|
||||
42
selfservice/strategy/oidc/export_test.go
Normal file
42
selfservice/strategy/oidc/export_test.go
Normal 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)
|
||||
}
|
||||
140
selfservice/strategy/oidc/fedcm/definitions.go
Normal file
140
selfservice/strategy/oidc/fedcm/definitions.go
Normal 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
|
||||
41
selfservice/strategy/oidc/form.go
Normal file
41
selfservice/strategy/oidc/form.go
Normal 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
|
||||
}
|
||||
49
selfservice/strategy/oidc/form_test.go
Normal file
49
selfservice/strategy/oidc/form_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
104
selfservice/strategy/oidc/json_equal_test.go
Normal file
104
selfservice/strategy/oidc/json_equal_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
17
selfservice/strategy/oidc/nodes.go
Normal file
17
selfservice/strategy/oidc/nodes.go
Normal 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))
|
||||
}
|
||||
75
selfservice/strategy/oidc/oidcerr/oidcerr.go
Normal file
75
selfservice/strategy/oidc/oidcerr/oidcerr.go
Normal 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}
|
||||
}
|
||||
87
selfservice/strategy/oidc/oidcerr/oidcerr_test.go
Normal file
87
selfservice/strategy/oidc/oidcerr/oidcerr_test.go
Normal 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))
|
||||
}
|
||||
80
selfservice/strategy/oidc/pkce.go
Normal file
80
selfservice/strategy/oidc/pkce.go
Normal 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
|
||||
}
|
||||
100
selfservice/strategy/oidc/pkce_test.go
Normal file
100
selfservice/strategy/oidc/pkce_test.go
Normal 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, ®istration.Flow{ID: x.NewUUID()}, "https://testhost")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, stateParam)
|
||||
assert.Empty(t, pkce)
|
||||
|
||||
state, err := oidc.DecryptState(t.Context(), reg.Cipher(t.Context()), stateParam)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, oidc.PKCEVerifier(state))
|
||||
assert.Equal(t, oidcv1.FlowKind_FLOW_KIND_REGISTRATION, state.FlowKind)
|
||||
assert.Equal(t, "https://testhost", state.RequestBaseUrl)
|
||||
}
|
||||
})
|
||||
}
|
||||
165
selfservice/strategy/oidc/provider.go
Normal file
165
selfservice/strategy/oidc/provider.go
Normal 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
|
||||
}
|
||||
141
selfservice/strategy/oidc/provider_amazon.go
Normal file
141
selfservice/strategy/oidc/provider_amazon.go
Normal 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
|
||||
}
|
||||
59
selfservice/strategy/oidc/provider_amazon_test.go
Normal file
59
selfservice/strategy/oidc/provider_amazon_test.go
Normal 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")
|
||||
}
|
||||
175
selfservice/strategy/oidc/provider_apple.go
Normal file
175
selfservice/strategy/oidc/provider_apple.go
Normal 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
|
||||
}
|
||||
146
selfservice/strategy/oidc/provider_apple_test.go
Normal file
146
selfservice/strategy/oidc/provider_apple_test.go
Normal 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}))
|
||||
})
|
||||
}
|
||||
}
|
||||
144
selfservice/strategy/oidc/provider_auth0.go
Normal file
144
selfservice/strategy/oidc/provider_auth0.go
Normal 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
|
||||
}
|
||||
33
selfservice/strategy/oidc/provider_auth0_unit_test.go
Normal file
33
selfservice/strategy/oidc/provider_auth0_unit_test.go
Normal 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))
|
||||
}
|
||||
280
selfservice/strategy/oidc/provider_config.go
Normal file
280
selfservice/strategy/oidc/provider_config.go
Normal 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))
|
||||
}
|
||||
113
selfservice/strategy/oidc/provider_config_test.go
Normal file
113
selfservice/strategy/oidc/provider_config_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
173
selfservice/strategy/oidc/provider_dingtalk.go
Normal file
173
selfservice/strategy/oidc/provider_dingtalk.go
Normal 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
|
||||
}
|
||||
103
selfservice/strategy/oidc/provider_discord.go
Normal file
103
selfservice/strategy/oidc/provider_discord.go
Normal 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
|
||||
}
|
||||
139
selfservice/strategy/oidc/provider_facebook.go
Normal file
139
selfservice/strategy/oidc/provider_facebook.go
Normal 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
|
||||
}
|
||||
219
selfservice/strategy/oidc/provider_generic_oidc.go
Normal file
219
selfservice/strategy/oidc/provider_generic_oidc.go
Normal 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
|
||||
}
|
||||
94
selfservice/strategy/oidc/provider_generic_test.go
Normal file
94
selfservice/strategy/oidc/provider_generic_test.go
Normal 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())))
|
||||
})
|
||||
}
|
||||
114
selfservice/strategy/oidc/provider_github.go
Normal file
114
selfservice/strategy/oidc/provider_github.go
Normal 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
|
||||
}
|
||||
100
selfservice/strategy/oidc/provider_github_app.go
Normal file
100
selfservice/strategy/oidc/provider_github_app.go
Normal 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
|
||||
}
|
||||
117
selfservice/strategy/oidc/provider_gitlab.go
Normal file
117
selfservice/strategy/oidc/provider_gitlab.go
Normal 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)
|
||||
}
|
||||
89
selfservice/strategy/oidc/provider_google.go
Normal file
89
selfservice/strategy/oidc/provider_google.go
Normal 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 == ""
|
||||
}
|
||||
138
selfservice/strategy/oidc/provider_google_test.go
Normal file
138
selfservice/strategy/oidc/provider_google_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
118
selfservice/strategy/oidc/provider_jackson.go
Normal file
118
selfservice/strategy/oidc/provider_jackson.go
Normal 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
|
||||
}
|
||||
137
selfservice/strategy/oidc/provider_jackson_test.go
Normal file
137
selfservice/strategy/oidc/provider_jackson_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
117
selfservice/strategy/oidc/provider_lark.go
Normal file
117
selfservice/strategy/oidc/provider_lark.go
Normal 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{}
|
||||
}
|
||||
39
selfservice/strategy/oidc/provider_line_2_1.go
Normal file
39
selfservice/strategy/oidc/provider_line_2_1.go
Normal 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...)
|
||||
}
|
||||
200
selfservice/strategy/oidc/provider_linkedin.go
Normal file
200
selfservice/strategy/oidc/provider_linkedin.go
Normal 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
|
||||
}
|
||||
209
selfservice/strategy/oidc/provider_linkedin_test.go
Normal file
209
selfservice/strategy/oidc/provider_linkedin_test.go
Normal 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)
|
||||
}
|
||||
17
selfservice/strategy/oidc/provider_linkedin_v2.go
Normal file
17
selfservice/strategy/oidc/provider_linkedin_v2.go
Normal 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,
|
||||
}
|
||||
}
|
||||
33
selfservice/strategy/oidc/provider_linkedin_v2_test.go
Normal file
33
selfservice/strategy/oidc/provider_linkedin_v2_test.go
Normal 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)
|
||||
}
|
||||
132
selfservice/strategy/oidc/provider_microsoft.go
Normal file
132
selfservice/strategy/oidc/provider_microsoft.go
Normal 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
|
||||
}
|
||||
174
selfservice/strategy/oidc/provider_netid.go
Normal file
174
selfservice/strategy/oidc/provider_netid.go
Normal 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}
|
||||
}
|
||||
29
selfservice/strategy/oidc/provider_netid_test.go
Normal file
29
selfservice/strategy/oidc/provider_netid_test.go
Normal 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)
|
||||
}
|
||||
124
selfservice/strategy/oidc/provider_patreon.go
Normal file
124
selfservice/strategy/oidc/provider_patreon.go
Normal 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
|
||||
}
|
||||
108
selfservice/strategy/oidc/provider_private_net_test.go
Normal file
108
selfservice/strategy/oidc/provider_private_net_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
144
selfservice/strategy/oidc/provider_salesforce.go
Normal file
144
selfservice/strategy/oidc/provider_salesforce.go
Normal 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
|
||||
}
|
||||
33
selfservice/strategy/oidc/provider_salesforce_unit_test.go
Normal file
33
selfservice/strategy/oidc/provider_salesforce_unit_test.go
Normal 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
Reference in New Issue
Block a user