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

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

View File

@@ -0,0 +1,54 @@
{
"$id": "https://schemas.ory.sh/kratos/selfservice/strategy/webauthn/login.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"csrf_token": {
"type": "string"
},
"webauthn_login": {
"type": "string"
},
"method": {
"type": "string"
},
"identifier": {
"type": "string",
"minLength": 1
},
"transient_payload": {
"type": "object",
"additionalProperties": true
}
},
"if": {
"anyOf": [
{
"properties": {
"method": {
"const": "webauthn"
}
},
"required": [
"method"
]
},
{
"properties": {
"webauthn_login": {
"type": "string",
"minLength": 1
}
},
"required": [
"webauthn_login"
]
}
]
},
"then": {
"required": [
"identifier"
]
}
}

View File

@@ -0,0 +1,42 @@
{
"$id": "https://schemas.ory.sh/kratos/selfservice/strategy/password/registration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"csrf_token": {
"type": "string"
},
"traits": {
"description": "This field will be overwritten in registration.go's decoder() method. Do not add anything to this field as it has no effect."
},
"method": {
"type": "string"
},
"webauthn_register": {
"type": "string"
},
"webauthn_register_displayname": {
"type": "string"
},
"transient_payload": {
"type": "object",
"additionalProperties": true
}
},
"oneOf": [
{
"required": [
"webauthn_register"
]
},
{
"method": {
"const": "webauthn"
},
"required": [
"identifier",
"method"
]
}
]
}

View File

@@ -0,0 +1,47 @@
{
"$id": "https://schemas.ory.sh/kratos/selfservice/strategy/webauthn/settings.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"csrf_token": {
"type": "string"
},
"method": {
"type": "string"
},
"webauthn_register": {
"type": "string"
},
"webauthn_register_displayname": {
"type": "string"
},
"webauthn_remove": {
"type": "string"
},
"transient_payload": {
"type": "object",
"additionalProperties": true
}
},
"if": {
"properties": {
"webauthn_register": {
"minLength": 1
}
},
"required": [
"webauthn_register"
]
},
"then": {
"properties": {
"webauthn_register_displayname": {
"minLength": 1
}
},
"required": [
"webauthn_register_displayname"
]
}
}

View File

@@ -0,0 +1,7 @@
[
{
"id": 4000043,
"text": "The provided web authn login is invalid, please try again.",
"type": "error"
}
]

View File

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

View File

@@ -0,0 +1,74 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "identifier",
"node_type": "input",
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login",
"node_type": "input",
"type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "script"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login_trigger",
"node_type": "input",
"onclickTrigger": "oryWebAuthnLogin",
"type": "button"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,88 @@
{
"organization_id": null,
"type": "browser",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "script",
"group": "webauthn",
"attributes": {
"async": true,
"referrerpolicy": "no-referrer",
"crossorigin": "anonymous",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"type": "text/javascript",
"node_type": "script"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_login_trigger",
"type": "button",
"disabled": false,
"onclickTrigger": "oryWebAuthnLogin",
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"text": "Continue",
"type": "info"
}
}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_login",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
}
],
"messages": [
{
"text": "Prepare your WebAuthn device (e.g. security key, biometrics scanner, ...) and press continue.",
"type": "info"
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,88 @@
{
"organization_id": null,
"type": "browser",
"ui": {
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "script",
"group": "webauthn",
"attributes": {
"async": true,
"referrerpolicy": "no-referrer",
"crossorigin": "anonymous",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"type": "text/javascript",
"node_type": "script"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_login_trigger",
"type": "button",
"disabled": false,
"onclickTrigger": "oryWebAuthnLogin",
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"text": "Continue",
"type": "info"
}
}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_login",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
}
],
"messages": [
{
"text": "Prepare your WebAuthn device (e.g. security key, biometrics scanner, ...) and press continue.",
"type": "info"
}
]
},
"refresh": false,
"requested_aal": "aal1",
"state": "choose_method"
}

View File

@@ -0,0 +1,55 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "username webauthn",
"disabled": false,
"name": "identifier",
"node_type": "input",
"required": true,
"type": "text",
"value": ""
},
"group": "default",
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "webauthn"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
},
"type": "input"
}
]

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,75 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "identifier",
"node_type": "input",
"type": "hidden",
"value": "foo@bar.com"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login",
"node_type": "input",
"type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "script"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login_trigger",
"node_type": "input",
"onclickTrigger": "oryWebAuthnLogin",
"type": "button"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,75 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "identifier",
"node_type": "input",
"type": "hidden",
"value": "foo@bar.com"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login",
"node_type": "input",
"type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "script"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login_trigger",
"node_type": "input",
"onclickTrigger": "oryWebAuthnLogin",
"type": "button"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,75 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "identifier",
"node_type": "input",
"type": "hidden",
"value": "foo@bar.com"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login",
"node_type": "input",
"type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "script"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login_trigger",
"node_type": "input",
"onclickTrigger": "oryWebAuthnLogin",
"type": "button"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,75 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "identifier",
"node_type": "input",
"type": "hidden",
"value": "foo@bar.com"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login",
"node_type": "input",
"type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "script"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login_trigger",
"node_type": "input",
"onclickTrigger": "oryWebAuthnLogin",
"type": "button"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
},
"type": "input"
}
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,75 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "identifier",
"node_type": "input",
"type": "hidden",
"value": "foo@bar.com"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login",
"node_type": "input",
"type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "script"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login_trigger",
"node_type": "input",
"onclickTrigger": "oryWebAuthnLogin",
"type": "button"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,75 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "identifier",
"node_type": "input",
"type": "hidden",
"value": "foo@bar.com"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login",
"node_type": "input",
"type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "script"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_login_trigger",
"node_type": "input",
"onclickTrigger": "oryWebAuthnLogin",
"type": "button"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,129 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_remove",
"node_type": "input",
"type": "submit",
"value": "626172626172"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"context": {
"added_at": "0001-01-01T00:00:00Z",
"added_at_unix": -62135596800,
"display_name": "bar"
},
"id": 1050018,
"text": "Remove security key \"bar\"",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_remove",
"node_type": "input",
"type": "submit",
"value": "666f6f666f6f"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"context": {
"added_at": "0001-01-01T00:00:00Z",
"added_at_unix": -62135596800,
"display_name": "foo"
},
"id": 1050018,
"text": "Remove security key \"foo\"",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_register_displayname",
"node_type": "input",
"type": "text",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1050013,
"text": "Name of the security key",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_register",
"node_type": "input",
"type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_register_trigger",
"node_type": "input",
"onclickTrigger": "oryWebAuthnRegistration",
"type": "button"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1050012,
"text": "Add security key",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "script"
}
]

View File

@@ -0,0 +1,33 @@
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_remove",
"type": "submit",
"value": "666f6f666f6f",
"disabled": true,
"node_type": "input"
},
"messages": [
{
"id": 4000001,
"text": "unable to remove this security key because it would lock you out of your account",
"type": "error",
"context": {
"reason": "unable to remove this security key because it would lock you out of your account"
}
}
],
"meta": {
"label": {
"id": 1050018,
"text": "Remove security key \"foo\"",
"type": "info",
"context": {
"added_at": "0001-01-01T00:00:00Z",
"added_at_unix": -62135596800,
"display_name": "foo"
}
}
}
}

View File

@@ -0,0 +1,11 @@
{
"webauthn_register": [
""
],
"webauthn_register_displayname": [
""
],
"webauthn_remove": [
"666f6f666f6f"
]
}

View File

@@ -0,0 +1,33 @@
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_remove",
"type": "submit",
"value": "666f6f666f6f",
"disabled": true,
"node_type": "input"
},
"messages": [
{
"id": 4000001,
"text": "unable to remove this security key because it would lock you out of your account",
"type": "error",
"context": {
"reason": "unable to remove this security key because it would lock you out of your account"
}
}
],
"meta": {
"label": {
"id": 1050018,
"text": "Remove security key \"foo\"",
"type": "info",
"context": {
"added_at": "0001-01-01T00:00:00Z",
"added_at_unix": -62135596800,
"display_name": "foo"
}
}
}
}

View File

@@ -0,0 +1,11 @@
{
"webauthn_register": [
""
],
"webauthn_register_displayname": [
""
],
"webauthn_remove": [
"666f6f666f6f"
]
}

View File

@@ -0,0 +1,81 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_register_displayname",
"node_type": "input",
"type": "text",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1050013,
"text": "Name of the security key",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_register",
"node_type": "input",
"type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_register_trigger",
"node_type": "input",
"onclickTrigger": "oryWebAuthnRegistration",
"type": "button"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1050012,
"text": "Add security key",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "script"
}
]

View File

@@ -0,0 +1,11 @@
{
"webauthn_register": [
""
],
"webauthn_register_displayname": [
""
],
"webauthn_remove": [
"666f6f666f6f"
]
}

View File

@@ -0,0 +1,11 @@
{
"webauthn_register": [
""
],
"webauthn_register_displayname": [
""
],
"webauthn_remove": [
"666f6f666f6f"
]
}

View File

@@ -0,0 +1,54 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "text",
"required": true,
"autocomplete": "username webauthn",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "method",
"type": "submit",
"value": "webauthn",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,54 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "text",
"required": true,
"autocomplete": "username webauthn",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "method",
"type": "submit",
"value": "webauthn",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,34 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "method",
"type": "submit",
"value": "webauthn",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,34 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "method",
"type": "submit",
"value": "webauthn",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,34 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "method",
"type": "submit",
"value": "webauthn",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,34 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "method",
"type": "submit",
"value": "webauthn",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,74 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "script",
"group": "webauthn",
"attributes": {
"async": true,
"referrerpolicy": "no-referrer",
"crossorigin": "anonymous",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"type": "text/javascript",
"id": "webauthn_script",
"node_type": "script"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_login_trigger",
"type": "button",
"disabled": false,
"onclickTrigger": "oryWebAuthnLogin",
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_login",
"type": "hidden",
"value": "",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
}
]

View File

@@ -0,0 +1,74 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "script",
"group": "webauthn",
"attributes": {
"async": true,
"referrerpolicy": "no-referrer",
"crossorigin": "anonymous",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"type": "text/javascript",
"id": "webauthn_script",
"node_type": "script"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_login_trigger",
"type": "button",
"disabled": false,
"onclickTrigger": "oryWebAuthnLogin",
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_login",
"type": "hidden",
"value": "",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
}
]

View File

@@ -0,0 +1,74 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "script",
"group": "webauthn",
"attributes": {
"async": true,
"referrerpolicy": "no-referrer",
"crossorigin": "anonymous",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"type": "text/javascript",
"id": "webauthn_script",
"node_type": "script"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_login_trigger",
"type": "button",
"disabled": false,
"onclickTrigger": "oryWebAuthnLogin",
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010008,
"text": "Sign in with a hardware key",
"type": "info"
}
}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_login",
"type": "hidden",
"value": "",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
}
]

View File

@@ -0,0 +1,81 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_register_displayname",
"type": "text",
"value": "",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050013,
"text": "Name of the security key",
"type": "info"
}
}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_register_trigger",
"type": "button",
"disabled": false,
"onclickTrigger": "oryWebAuthnRegistration",
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040004,
"text": "Sign up with security key",
"type": "info"
}
}
},
{
"type": "script",
"group": "webauthn",
"attributes": {
"async": true,
"referrerpolicy": "no-referrer",
"crossorigin": "anonymous",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"type": "text/javascript",
"id": "webauthn_script",
"node_type": "script"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_register",
"type": "hidden",
"value": "",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
}
]

View File

@@ -0,0 +1,81 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_register_displayname",
"type": "text",
"value": "",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050013,
"text": "Name of the security key",
"type": "info"
}
}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_register_trigger",
"type": "button",
"disabled": false,
"onclickTrigger": "oryWebAuthnRegistration",
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040004,
"text": "Sign up with security key",
"type": "info"
}
}
},
{
"type": "script",
"group": "webauthn",
"attributes": {
"async": true,
"referrerpolicy": "no-referrer",
"crossorigin": "anonymous",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"type": "text/javascript",
"id": "webauthn_script",
"node_type": "script"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_register",
"type": "hidden",
"value": "",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
}
]

View File

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

View File

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

View File

@@ -0,0 +1,81 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_register_displayname",
"type": "text",
"value": "",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050013,
"text": "Name of the security key",
"type": "info"
}
}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_register_trigger",
"type": "button",
"disabled": false,
"onclickTrigger": "oryWebAuthnRegistration",
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040004,
"text": "Sign up with security key",
"type": "info"
}
}
},
{
"type": "script",
"group": "webauthn",
"attributes": {
"async": true,
"referrerpolicy": "no-referrer",
"crossorigin": "anonymous",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"type": "text/javascript",
"id": "webauthn_script",
"node_type": "script"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_register",
"type": "hidden",
"value": "",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
}
]

View File

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

View File

@@ -0,0 +1,81 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_register_displayname",
"type": "text",
"value": "",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1050013,
"text": "Name of the security key",
"type": "info"
}
}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_register_trigger",
"type": "button",
"disabled": false,
"onclickTrigger": "oryWebAuthnRegistration",
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040004,
"text": "Sign up with security key",
"type": "info"
}
}
},
{
"type": "script",
"group": "webauthn",
"attributes": {
"async": true,
"referrerpolicy": "no-referrer",
"crossorigin": "anonymous",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"type": "text/javascript",
"id": "webauthn_script",
"node_type": "script"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "webauthn",
"attributes": {
"name": "webauthn_register",
"type": "hidden",
"value": "",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
}
]

View File

@@ -0,0 +1,80 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.foobar",
"node_type": "input",
"required": true,
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.username",
"node_type": "input",
"required": true,
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,80 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.foobar",
"node_type": "input",
"required": true,
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.username",
"node_type": "input",
"required": true,
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,146 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.foobar",
"node_type": "input",
"required": true,
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.username",
"node_type": "input",
"required": true,
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_register_displayname",
"node_type": "input",
"type": "text",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1050013,
"text": "Name of the security key",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_register",
"node_type": "input",
"type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_register_trigger",
"node_type": "input",
"onclickTrigger": "oryWebAuthnRegistration",
"type": "button"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1040004,
"text": "Sign up with security key",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "script"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,146 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.foobar",
"node_type": "input",
"required": true,
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.username",
"node_type": "input",
"required": true,
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_register_displayname",
"node_type": "input",
"type": "text",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1050013,
"text": "Name of the security key",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_register",
"node_type": "input",
"type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "webauthn_register_trigger",
"node_type": "input",
"onclickTrigger": "oryWebAuthnRegistration",
"type": "button"
},
"group": "webauthn",
"messages": [],
"meta": {
"label": {
"id": 1040004,
"text": "Sign up with security key",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
},
"group": "webauthn",
"messages": [],
"meta": {},
"type": "script"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,13 @@
{
"id": "f5d1b6a3-a4bb-44f7-9161-f4f877efe9ad",
"schema_id": "default",
"schema_url": "http://localhost:4455/schemas/default",
"state": "active",
"state_changed_at": "2021-08-17T12:18:01.690425+02:00",
"traits": {
"email": "foo@bar.com",
"website": "https://www.ory.sh"
},
"created_at": "2021-08-17T12:18:01.690741+02:00",
"updated_at": "2021-08-17T12:18:01.690741+02:00"
}

View File

@@ -0,0 +1,11 @@
{
"id": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ",
"rawId": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ",
"type": "public-key",
"response": {
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAACg",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiV3paQ1dVTG1hcTV4VEFsQTBZWUhscW91YnFBaGUxQVdkTFJaQ0lCQU1jTSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQ1NSIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
"signature": "MEQCIHtRzzmLJrTPucNIRpPkstxR8oGJEzrm558LFe2jHTesAiAy2SGuBMDkdVdMJU4WJR2qFSpbAHQUvwG--Gv3vK8vDA",
"userHandle": "AAaaaaaaTD6lazfHs42XPg"
}
}

View File

@@ -0,0 +1,16 @@
{
"credentials": [
{
"id": "OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ==",
"public_key": "pQECAyYgASFYIPW2FsD6d/Lc7SU33hMhJUxafOA3JWpsLka8eKO+OPRkIlggkkPt8ocrupQOuvy+8HbQLSLiu899EdchJlWdMPE1tiw=",
"attestation_type": "none",
"authenticator": {
"aaguid": "AAAAAAAAAAAAAAAAAAAAAA==",
"sign_count": 3,
"clone_warning": false
},
"display_name": "some-key",
"added_at": "2021-08-17T10:18:55Z"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"webauthn_session_data": {
"challenge": "WzZCWULmaq5xTAlA0YYHlqoubqAhe1AWdLRZCIBAMcM",
"user_id": "9dG2o6S7RPeRYfT4d+/prQ==",
"allowed_credentials": [
"OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ=="
],
"userVerification": ""
}
}

View File

@@ -0,0 +1,11 @@
{
"id": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ",
"rawId": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ",
"type": "public-key",
"response": {
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAACg",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiV3paQ1dVTG1hcTV4VEFsQTBZWUhscW91YnFBaGUxQVdkTFJaQ0lCQU1jTSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQ1NSIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
"signature": "MEQCIHtRzzmLJrTPucNIRpPkstxR8oGJEzrm558LFe2jHTesAiAy2SGuBMDkdVdMJU4WJR2qFSpbAHQUvwG--Gv3vK8vDA",
"userHandle": ""
}
}

View File

@@ -0,0 +1,18 @@
{
"credentials": [
{
"id": "OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ==",
"public_key": "pQECAyYgASFYIPW2FsD6d/Lc7SU33hMhJUxafOA3JWpsLka8eKO+OPRkIlggkkPt8ocrupQOuvy+8HbQLSLiu899EdchJlWdMPE1tiw=",
"attestation_type": "none",
"authenticator": {
"aaguid": "AAAAAAAAAAAAAAAAAAAAAA==",
"sign_count": 3,
"clone_warning": false
},
"display_name": "some-key",
"added_at": "2021-08-17T10:18:55Z",
"is_passwordless": false
}
],
"user_handle": "9dG2o6S7RPeRYfT4d+/prQ=="
}

View File

@@ -0,0 +1,10 @@
{
"webauthn_session_data": {
"challenge": "WzZCWULmaq5xTAlA0YYHlqoubqAhe1AWdLRZCIBAMcM",
"user_id": "9dG2o6S7RPeRYfT4d+/prQ==",
"allowed_credentials": [
"OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ=="
],
"userVerification": ""
}
}

View File

@@ -0,0 +1,11 @@
{
"id": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ",
"rawId": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ",
"type": "public-key",
"response": {
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAACg",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiV3paQ1dVTG1hcTV4VEFsQTBZWUhscW91YnFBaGUxQVdkTFJaQ0lCQU1jTSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQ1NSIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
"signature": "MEQCIHtRzzmLJrTPucNIRpPkstxR8oGJEzrm558LFe2jHTesAiAy2SGuBMDkdVdMJU4WJR2qFSpbAHQUvwG--Gv3vK8vDA",
"userHandle": ""
}
}

View File

@@ -0,0 +1,18 @@
{
"credentials": [
{
"id": "OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ==",
"public_key": "pQECAyYgASFYIPW2FsD6d/Lc7SU33hMhJUxafOA3JWpsLka8eKO+OPRkIlggkkPt8ocrupQOuvy+8HbQLSLiu899EdchJlWdMPE1tiw=",
"attestation_type": "none",
"authenticator": {
"aaguid": "AAAAAAAAAAAAAAAAAAAAAA==",
"sign_count": 3,
"clone_warning": false
},
"display_name": "some-key",
"added_at": "2021-08-17T10:18:55Z",
"is_passwordless": false
}
],
"user_handle": "RPwiyauuTD6lazfHs42XPg=="
}

View File

@@ -0,0 +1,10 @@
{
"webauthn_session_data": {
"challenge": "WzZCWULmaq5xTAlA0YYHlqoubqAhe1AWdLRZCIBAMcM",
"user_id": "RPwiyauuTD6lazfHs42XPg==",
"allowed_credentials": [
"OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ=="
],
"userVerification": ""
}
}

View File

@@ -0,0 +1,11 @@
{
"id": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ",
"rawId": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ",
"type": "public-key",
"response": {
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAACg",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiV3paQ1dVTG1hcTV4VEFsQTBZWUhscW91YnFBaGUxQVdkTFJaQ0lCQU1jTSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQ1NSIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
"signature": "MEQCIHtRzzmLJrTPucNIRpPkstxR8oGJEzrm558LFe2jHTesAiAy2SGuBMDkdVdMJU4WJR2qFSpbAHQUvwG--Gv3vK8vDA",
"userHandle": "RPwiyauuTD6lazfHs42XPg"
}
}

View File

@@ -0,0 +1,18 @@
{
"credentials": [
{
"id": "OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ==",
"public_key": "pQECAyYgASFYIPW2FsD6d/Lc7SU33hMhJUxafOA3JWpsLka8eKO+OPRkIlggkkPt8ocrupQOuvy+8HbQLSLiu899EdchJlWdMPE1tiw=",
"attestation_type": "none",
"authenticator": {
"aaguid": "AAAAAAAAAAAAAAAAAAAAAA==",
"sign_count": 3,
"clone_warning": false
},
"display_name": "some-key",
"added_at": "2021-08-17T10:18:55Z",
"is_passwordless": true
}
],
"user_handle": "9dG2o6S7RPeRYfT4d+/prQ=="
}

View File

@@ -0,0 +1,10 @@
{
"webauthn_session_data": {
"challenge": "WzZCWULmaq5xTAlA0YYHlqoubqAhe1AWdLRZCIBAMcM",
"user_id": "9dG2o6S7RPeRYfT4d+/prQ==",
"allowed_credentials": [
"OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ=="
],
"userVerification": ""
}
}

View File

@@ -0,0 +1,11 @@
{
"id": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ",
"rawId": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ",
"type": "public-key",
"response": {
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAACg",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiV3paQ1dVTG1hcTV4VEFsQTBZWUhscW91YnFBaGUxQVdkTFJaQ0lCQU1jTSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQ1NSIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
"signature": "MEQCIHtRzzmLJrTPucNIRpPkstxR8oGJEzrm558LFe2jHTesAiAy2SGuBMDkdVdMJU4WJR2qFSpbAHQUvwG--Gv3vK8vDA",
"userHandle": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"webauthn_session_data": {
"challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY",
"user_id": "",
"userVerification": ""
}
}

View File

@@ -0,0 +1,7 @@
{
"webauthn_session_data": {
"challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY",
"user_id": "wrong",
"userVerification": ""
}
}

View File

@@ -0,0 +1,14 @@
{
"id": "6e11a9a7-62fd-4c88-871a-097f18f0306f",
"schema_id": "default",
"schema_url": "http://localhost:4455/schemas/default",
"state": "active",
"state_changed_at": "2021-08-17T11:15:59.232051+02:00",
"traits": {
"email": "foo@bar.com",
"website": "https://www.ory.sh"
},
"created_at": "2021-08-17T11:15:59.232288+02:00",
"updated_at": "2021-08-17T11:15:59.232288+02:00"
}

View File

@@ -0,0 +1,8 @@
{
"totp_url": "otpauth://totp/issuer.ory.sh:6e11a9a7-62fd-4c88-871a-097f18f0306f?algorithm=SHA1&digits=6&issuer=issuer.ory.sh&period=30&secret=2F43HRJNMUW67EDMRR7AKQYRZP3AI6IG",
"webauthn_session_data": {
"challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY",
"user_id": "bhGpp2L9TIiHGgl/GPAwbw==",
"userVerification": ""
}
}

View File

@@ -0,0 +1,9 @@
{
"id": "L1yOrxHy5Lq72lAPahaWdl0q9gsXRzV2BJ4xJmkTVH_8uuVKU-FVbJlVRwYGzPNc1IjCWUYAK0H0YSpd5hz-Pg",
"rawId": "L1yOrxHy5Lq72lAPahaWdl0q9gsXRzV2BJ4xJmkTVH_8uuVKU-FVbJlVRwYGzPNc1IjCWUYAK0H0YSpd5hz-Pg",
"type": "public-key",
"response": {
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAABAAAAAAAAAAAAAAAAAAAAAAAQC9cjq8R8uS6u9pQD2oWlnZdKvYLF0c1dgSeMSZpE1R__LrlSlPhVWyZVUcGBszzXNSIwllGACtB9GEqXeYc_j6lAQIDJiABIVggFFzdor6hBMgrpYLCds8Uu2JtPaaaxKU6LEAUT6QRZ5UiWCA24TI4vED6rrTUjykchoAln67u5GT1nwmzjvrk79HhlQ",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiVWx4SFNUa3VNdnRWRG9WOXk1bGh1OU95TlVQOFA3TVAwUllBVDZJbV9yWSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQ1NSIsImNyb3NzT3JpZ2luIjpmYWxzZX0"
}
}

View File

@@ -0,0 +1,14 @@
{
"id": "6e11a9a7-62fd-4c88-871a-097f18f0306f",
"schema_id": "default",
"schema_url": "http://localhost:4455/schemas/default",
"state": "active",
"state_changed_at": "2021-08-17T11:15:59.232051+02:00",
"traits": {
"email": "foo@bar.com",
"website": "https://www.ory.sh"
},
"created_at": "2021-08-17T11:15:59.232288+02:00",
"updated_at": "2021-08-17T11:15:59.232288+02:00"
}

View File

@@ -0,0 +1,8 @@
{
"totp_url": "otpauth://totp/issuer.ory.sh:6e11a9a7-62fd-4c88-871a-097f18f0306f?algorithm=SHA1&digits=6&issuer=issuer.ory.sh&period=30&secret=2F43HRJNMUW67EDMRR7AKQYRZP3AI6IG",
"webauthn_session_data": {
"challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY",
"user_id": "bhGpp2L9TIiHGgl/GPAwbw==",
"userVerification": ""
}
}

View File

@@ -0,0 +1,9 @@
{
"id": "L1yOrxHy5Lq72lAPahaWdl0q9gsXRzV2BJ4xJmkTVH_8uuVKU-FVbJlVRwYGzPNc1IjCWUYAK0H0YSpd5hz-Pg",
"rawId": "L1yOrxHy5Lq72lAPahaWdl0q9gsXRzV2BJ4xJmkTVH_8uuVKU-FVbJlVRwYGzPNc1IjCWUYAK0H0YSpd5hz-Pg",
"type": "public-key",
"response": {
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAABAAAAAAAAAAAAAAAAAAAAAAAQC9cjq8R8uS6u9pQD2oWlnZdKvYLF0c1dgSeMSZpE1R__LrlSlPhVWyZVUcGBszzXNSIwllGACtB9GEqXeYc_j6lAQIDJiABIVggFFzdor6hBMgrpYLCds8Uu2JtPaaaxKU6LEAUT6QRZ5UiWCA24TI4vED6rrTUjykchoAln67u5GT1nwmzjvrk79HhlQ",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiVWx4SFNUa3VNdnRWRG9WOXk1bGh1OU95TlVQOFA3TVAwUllBVDZJbV9yWSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQ1NSIsImNyb3NzT3JpZ2luIjpmYWxzZX0"
}
}

View File

@@ -0,0 +1,444 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package webauthn
import (
"context"
"encoding/json"
"net/http"
"strings"
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/ory/x/otelx"
"github.com/ory/kratos/selfservice/strategy/idfirst"
"github.com/ory/kratos/selfservice/flowhelpers"
"github.com/ory/kratos/session"
"github.com/ory/kratos/x/webauthnx"
"github.com/gofrs/uuid"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"github.com/pkg/errors"
"github.com/ory/herodot"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/x/decoderx"
)
var (
_ login.AAL1FormHydrator = new(Strategy)
_ login.AAL2FormHydrator = new(Strategy)
)
func (s *Strategy) populateLoginMethodForPasswordless(r *http.Request, sr *login.Flow) error {
sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
sr.UI.GetNodes().Append(node.NewInputField("method", "webauthn", node.WebAuthnGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginWebAuthn()))
return nil
}
func (s *Strategy) populateLoginMethod(r *http.Request, sr *login.Flow, i *identity.Identity, label *text.Message, aal identity.AuthenticatorAssuranceLevel) error {
id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), i.ID)
if err != nil {
return err
}
cred, ok := id.GetCredentials(s.ID())
if !ok {
// Identity has no webauth
return nil
}
var conf identity.CredentialsWebAuthnConfig
if err := json.Unmarshal(cred.Config, &conf); err != nil {
return errors.WithStack(err)
}
webAuthCreds := conf.Credentials.ToWebAuthnFiltered(aal, nil)
if len(webAuthCreds) == 0 {
// Identity has no webauthn
return webauthnx.ErrNoCredentials
}
web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context()))
if err != nil {
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to initiate WebAuth.").WithDebug(err.Error()))
}
options, sessionData, err := web.BeginLogin(webauthnx.NewUser(conf.UserHandle, webAuthCreds, web.Config))
if err != nil {
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to initiate WebAuth login.").WithDebug(err.Error()))
}
// Remove the WebAuthn URL from the internal context now that it is set!
sr.InternalContext, err = sjson.SetBytes(sr.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), sessionData)
if err != nil {
return errors.WithStack(err)
}
injectWebAuthnOptions, err := json.Marshal(options)
if err != nil {
return errors.WithStack(err)
}
if len(cred.Identifiers) > 0 {
sr.UI.SetNode(node.NewInputField("identifier", cred.Identifiers[0], node.DefaultGroup, node.InputAttributeTypeHidden))
}
sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
sr.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(r.Context())))
sr.UI.SetNode(webauthnx.NewWebAuthnLoginTrigger(string(injectWebAuthnOptions)).
WithMetaLabel(label))
sr.UI.Nodes.Upsert(webauthnx.NewWebAuthnLoginInput())
return nil
}
func (s *Strategy) handleLoginError(r *http.Request, f *login.Flow, err error) error {
if f != nil {
f.UI.Nodes.ResetNodes("webauthn_login")
if f.Type == flow.TypeBrowser {
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
}
}
return err
}
// Update Login Flow with WebAuthn Method
//
// swagger:model updateLoginFlowWithWebAuthnMethod
type updateLoginFlowWithWebAuthnMethod struct {
// Identifier is the email or username of the user trying to log in.
//
// required: true
Identifier string `json:"identifier"`
// Method should be set to "webAuthn" when logging in using the WebAuthn strategy.
//
// required: true
Method string `json:"method"`
// Sending the anti-csrf token is only required for browser login flows.
CSRFToken string `json:"csrf_token"`
// Login a WebAuthn Security Key
//
// This must contain the ID of the WebAuthN connection.
Login string `json:"webauthn_login"`
// Transient data to pass along to any webhooks
//
// required: false
TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
}
func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, sess *session.Session) (i *identity.Identity, err error) {
ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.webauthn.Strategy.Login")
defer otelx.End(span, &err)
if f.Type != flow.TypeBrowser {
span.SetAttributes(attribute.String("not_responsible_reason", "flow type is not browser"))
return nil, flow.ErrStrategyNotResponsible
}
var p updateLoginFlowWithWebAuthnMethod
if err := decoderx.Decode(r, &p,
decoderx.HTTPKeepRequestBody(true),
decoderx.HTTPDecoderSetValidatePayloads(true),
decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema),
decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil {
return nil, s.handleLoginError(r, f, err)
}
f.TransientPayload = p.TransientPayload
if len(p.Login) > 0 || p.Method == s.SettingsStrategyID() {
// This method has only two submit buttons
p.Method = s.SettingsStrategyID()
} else {
span.SetAttributes(attribute.String("not_responsible_reason", "login is not provided and method is not webauthn"))
return nil, flow.ErrStrategyNotResponsible
}
if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil {
return nil, s.handleLoginError(r, f, err)
}
if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil {
return nil, s.handleLoginError(r, f, err)
}
if s.d.Config().WebAuthnForPasswordless(ctx) || f.IsRefresh() && f.RequestedAAL == identity.AuthenticatorAssuranceLevel1 {
return s.loginPasswordless(ctx, w, r, f, &p)
}
return s.loginMultiFactor(ctx, r, f, sess.IdentityID, &p)
}
func (s *Strategy) loginPasswordless(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, p *updateLoginFlowWithWebAuthnMethod) (i *identity.Identity, err error) {
ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.webauthn.Strategy.loginPasswordless")
defer otelx.End(span, &err)
if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil {
span.SetAttributes(attribute.String("not_responsible_reason", "requested AAL is not AAL1"))
return nil, s.handleLoginError(r, f, err)
}
if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil {
return nil, s.handleLoginError(r, f, err)
}
if p.Identifier == "" {
return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrBadRequest().WithReason("identifier is required")))
}
i, _, err = s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), p.Identifier)
if err != nil {
time.Sleep(x.RandomDelay(s.d.Config().HasherArgon2(ctx).ExpectedDuration, s.d.Config().HasherArgon2(ctx).ExpectedDeviation))
return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewNoWebAuthnCredentials()))
}
if len(p.Login) == 0 {
// Reset all nodes to not confuse users.
// This is kinda hacky and will probably need to be updated at some point.
previousNodes := f.UI.Nodes
f.UI.Nodes = node.Nodes{}
if err := s.populateLoginMethod(r, f, i, text.NewInfoSelfServiceLoginContinue(), identity.AuthenticatorAssuranceLevel1); errors.Is(err, webauthnx.ErrNoCredentials) {
f.UI.Nodes = previousNodes
return nil, s.handleLoginError(r, f, schema.NewNoWebAuthnCredentials())
} else if err != nil {
return nil, s.handleLoginError(r, f, err)
}
// Adds the "Continue" button
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
f.UI.Messages.Add(text.NewInfoLoginWebAuthnPasswordless())
f.UI.SetNode(node.NewInputField("identifier", p.Identifier, node.DefaultGroup, node.InputAttributeTypeHidden, node.WithRequiredInputAttribute))
if err := s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil {
return nil, s.handleLoginError(r, f, err)
}
redirectTo := f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(ctx)).String()
if x.IsJSONRequest(r) {
s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(redirectTo))
} else {
http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(ctx)).String(), http.StatusSeeOther)
}
return nil, errors.WithStack(flow.ErrCompletedByStrategy)
}
return s.loginAuthenticate(ctx, r, f, i.ID, p, identity.AuthenticatorAssuranceLevel1)
}
func (s *Strategy) loginAuthenticate(ctx context.Context, r *http.Request, f *login.Flow, identityID uuid.UUID, p *updateLoginFlowWithWebAuthnMethod, aal identity.AuthenticatorAssuranceLevel) (_ *identity.Identity, err error) {
ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.webauthn.Strategy.loginAuthenticate")
defer otelx.End(span, &err)
i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, identityID)
if err != nil {
return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewNoWebAuthnRegistered()))
}
c, ok := i.GetCredentials(s.ID())
if !ok {
return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewNoWebAuthnRegistered()))
}
var o identity.CredentialsWebAuthnConfig
if err := json.Unmarshal(c.Config, &o); err != nil {
return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError().WithReason("The WebAuthn credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err)))
}
web, err := webauthn.New(s.d.Config().WebAuthnConfig(ctx))
if err != nil {
return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error())))
}
webAuthnResponse, err := protocol.ParseCredentialRequestResponseBody(strings.NewReader(p.Login))
if err != nil {
return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to parse WebAuthn response.").WithDebug(err.Error())))
}
var webAuthnSess webauthn.SessionData
if err := json.Unmarshal([]byte(gjson.GetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)).Raw), &webAuthnSess); err != nil {
return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err)))
}
webAuthCreds := o.Credentials.ToWebAuthnFiltered(aal, &webAuthnResponse.Response.AuthenticatorData.Flags)
if f.IsRefresh() {
webAuthCreds = o.Credentials.ToWebAuthn()
}
validatedCredential, err := web.ValidateLogin(webauthnx.NewUser(o.UserHandle, webAuthCreds, web.Config), webAuthnSess, webAuthnResponse)
if err != nil {
return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewWebAuthnVerifierWrongError("#/")))
}
// Persist the updated signature counter and clone warning for W3C WebAuthn
// clone detection. This must not block the login: the user already proved
// possession of the authenticator.
webauthnx.UpdateSignCountAfterLogin(ctx, s.d, i, identity.CredentialsTypeWebAuthn, validatedCredential)
// Remove the WebAuthn URL from the internal context now that it is set!
f.InternalContext, err = sjson.DeleteBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData))
if err != nil {
return nil, s.handleLoginError(r, f, errors.WithStack(err))
}
f.Active = s.ID()
if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil {
return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError().WithReason("Could not update flow").WithDebug(err.Error())))
}
return i, nil
}
func (s *Strategy) loginMultiFactor(ctx context.Context, r *http.Request, f *login.Flow, identityID uuid.UUID, p *updateLoginFlowWithWebAuthnMethod) (*identity.Identity, error) {
if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel2); err != nil {
trace.SpanFromContext(ctx).SetAttributes(attribute.String("not_responsible_reason", "requested AAL is not AAL2"))
return nil, err
}
return s.loginAuthenticate(ctx, r, f, identityID, p, identity.AuthenticatorAssuranceLevel2)
}
func (s *Strategy) populateLoginMethodRefresh(r *http.Request, sr *login.Flow) error {
if sr.Type != flow.TypeBrowser {
return nil
}
identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID())
if identifier == "" {
return nil
}
if err := s.populateLoginMethod(r, sr, id, text.NewInfoSelfServiceLoginWebAuthn(), sr.RequestedAAL); errors.Is(err, webauthnx.ErrNoCredentials) {
return nil
} else if err != nil {
return err
}
sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden))
return nil
}
func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *login.Flow, _ *session.Session) error {
return s.populateLoginMethodRefresh(r, sr)
}
func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error {
return s.populateLoginMethodRefresh(r, sr)
}
func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error {
if sr.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) {
return nil
}
ds, err := sr.IdentitySchema.URL(r.Context(), s.d.Config())
if err != nil {
return err
}
identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String(), s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context()))
if err != nil {
return err
}
sr.UI.SetNode(node.NewInputField(
"identifier",
"",
node.DefaultGroup,
node.InputAttributeTypeText,
node.WithRequiredInputAttribute,
func(attributes *node.InputAttributes) {
attributes.Autocomplete = node.InputAttributeAutocompleteUsernameWebauthn
},
).WithMetaLabel(identifierLabel))
if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) {
return nil
} else if err != nil {
return err
}
return nil
}
func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error {
if sr.Type != flow.TypeBrowser || s.d.Config().WebAuthnForPasswordless(r.Context()) {
return nil
}
// We have done proper validation before so this should never error.
// The full expansion is required: the form hydrator reads the identity's credentials.
sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandEverything, identity.ExpandEverything)
if err != nil {
return err
}
if err := s.populateLoginMethod(r, sr, sess.Identity, text.NewInfoSelfServiceLoginWebAuthn(), identity.AuthenticatorAssuranceLevel2); errors.Is(err, webauthnx.ErrNoCredentials) {
return nil
} else if err != nil {
return err
}
return nil
}
func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error {
if sr.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) {
return errors.WithStack(idfirst.ErrNoCredentialsFound)
}
o := login.NewFormHydratorOptions(opts)
var count int
if o.IdentityHint != nil {
var err error
// If we have an identity hint we can perform identity credentials discovery and
// hide this credential if it should not be included.
if count, err = s.CountActiveFirstFactorCredentials(r.Context(), o.IdentityHint.Credentials); err != nil {
return err
}
}
if count > 0 || s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) {
if !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
return errors.WithStack(idfirst.ErrNoCredentialsFound)
}
return nil
} else if err != nil {
return err
}
}
if count == 0 {
return errors.WithStack(idfirst.ErrNoCredentialsFound)
}
return nil
}
func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *login.Flow) error {
return nil
}

View File

@@ -0,0 +1,137 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package webauthn_test
import (
"encoding/json"
"net/http"
"net/url"
"testing"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/x/configx"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/pkg"
kratos "github.com/ory/kratos/pkg/httpclient"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/ui/node"
)
// TestLoginUpdatesSignCount is a regression test for HackerOne #3780149.
//
// The go-webauthn library returns a *webauthn.Credential from ValidateLogin
// whose Authenticator.SignCount reflects the signature counter reported by the
// authenticator during the assertion. W3C WebAuthn clone detection relies on
// the relying party persisting this incremented counter so that a replayed or
// cloned authenticator (which reports a stale counter) can be detected.
//
// Before the fix, Kratos discarded the returned credential, so the stored
// SignCount stayed at its registration value forever and clone detection was
// inert.
//
// The MFA v1 fixtures store sign_count = 3 and the assertion response reports
// counter = 10, so a correct implementation persists SignCount = 10.
func TestLoginUpdatesSignCount(t *testing.T) {
t.Parallel()
conf, reg := pkg.NewFastRegistryWithMocks(t,
configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", false),
enabledWebauthn,
configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/login.schema.json")),
)
publicTS, _ := testhelpers.NewKratosServer(t, reg)
_ = testhelpers.NewErrorTestServer(t, reg)
_ = testhelpers.NewLoginUIFlowEchoServer(t, reg)
_ = testhelpers.NewRedirSessionEchoTS(t, reg)
createIdentityWithWebAuthn := func(t *testing.T, c identity.Credentials) *identity.Identity {
var id identity.Identity
require.NoError(t, json.Unmarshal(loginFixtureSuccessIdentity, &id))
id.SetCredentials(identity.CredentialsTypeWebAuthn, identity.Credentials{
Identifiers: []string{loginFixtureSuccessEmail},
Config: c.Config,
Type: identity.CredentialsTypeWebAuthn,
Version: c.Version,
})
_ = reg.PrivilegedIdentityPool().DeleteIdentity(t.Context(), id.ID)
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), &id))
return &id
}
submitWebAuthnLogin := func(t *testing.T, isSPA bool, id *identity.Identity, contextFixture []byte, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) {
client := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, client, publicTS, false, isSPA, false, false, opts...)
interim, err := reg.LoginFlowPersister().GetLoginFlow(t.Context(), uuid.FromStringOrNil(f.Id))
require.NoError(t, err)
interim.InternalContext = contextFixture
require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(t.Context(), interim))
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
cb(values)
body, res := testhelpers.LoginMakeRequest(t, false, isSPA, f, client, values.Encode())
return body, res, f
}
// Reads the stored first webauthn credential: the outer row (for id and
// created_at) and the decoded authenticator state.
storedCredential := func(t *testing.T, idID uuid.UUID) (*identity.Credentials, *identity.AuthenticatorWebAuthn) {
stored, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), idID)
require.NoError(t, err)
c, ok := stored.GetCredentials(identity.CredentialsTypeWebAuthn)
require.True(t, ok)
var conf identity.CredentialsWebAuthnConfig
require.NoError(t, json.Unmarshal(c.Config, &conf))
require.NotEmpty(t, conf.Credentials)
require.NotNil(t, conf.Credentials[0].Authenticator)
return c, conf.Credentials[0].Authenticator
}
conf.MustSet(t.Context(), config.ViperKeySessionWhoAmIAAL, "aal1")
id := createIdentityWithWebAuthn(t, identity.Credentials{
Config: loginFixtureSuccessV1Credentials,
Version: 1,
})
// Sanity: the registered credential starts at the fixture's sign_count = 3.
credBefore, authBefore := storedCredential(t, id.ID)
require.EqualValues(t, 3, authBefore.SignCount)
body, _, _ := submitWebAuthnLogin(t, true, id, loginFixtureSuccessV1Context, func(values url.Values) {
values.Set("identifier", loginFixtureSuccessEmail)
values.Set(node.WebAuthnLogin, string(loginFixtureSuccessV1Response))
}, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
// The login must succeed.
require.True(t, gjson.Get(body, "session.active").Bool(), "%s", body)
// The stored SignCount must be updated to the value reported by the
// authenticator assertion (10), not left at the registration value (3).
credAfter, auth := storedCredential(t, id.ID)
assert.EqualValues(t, 10, auth.SignCount,
"stored SignCount must be updated from the login assertion for clone detection to work")
// The assertion counter advanced (3 -> 10), so this is not a clone: the
// persisted clone warning must round-trip as false rather than being set
// spuriously on a normal login.
assert.False(t, auth.CloneWarning,
"a normally advancing counter must not set the clone warning")
// The credential row must be updated in place, not deleted and recreated:
// its id and created_at must be stable across the login.
assert.Equal(t, credBefore.ID, credAfter.ID,
"the credential row must be updated in place (stable id), not recreated")
assert.Equal(t, credBefore.CreatedAt, credAfter.CreatedAt,
"persisting the sign counter must not reset the credential's created_at")
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package webauthn
import (
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x/webauthnx"
)
func nodeWebauthnRegistrationOptions(opts []byte) *node.Node {
return webauthnx.NewWebAuthnConnectionTrigger(string(opts)).WithMetaLabel(text.NewInfoSelfServiceRegistrationRegisterWebAuthn())
}
func nodeDisplayName() *node.Node { return webauthnx.NewWebAuthnConnectionName() }
func nodeConnectionInput() *node.Node { return webauthnx.NewWebAuthnConnectionInput() }

View File

@@ -0,0 +1,292 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package webauthn
import (
"encoding/json"
"net/http"
"net/url"
"strings"
"go.opentelemetry.io/otel/attribute"
"github.com/ory/x/otelx"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/pkg/errors"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"github.com/ory/herodot"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/ui/container"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
"github.com/ory/kratos/x/webauthnx"
)
var _ registration.FormHydrator = new(Strategy)
// Update Registration Flow with WebAuthn Method
//
// swagger:model updateRegistrationFlowWithWebAuthnMethod
type updateRegistrationFlowWithWebAuthnMethod struct {
// Register a WebAuthn Security Key
//
// It is expected that the JSON returned by the WebAuthn registration process
// is included here.
Register string `json:"webauthn_register"`
// Name of the WebAuthn Security Key to be Added
//
// A human-readable name for the security key which will be added.
RegisterDisplayName string `json:"webauthn_register_displayname"`
// CSRFToken is the anti-CSRF token
CSRFToken string `json:"csrf_token"`
// The identity's traits
//
// required: true
Traits json.RawMessage `json:"traits"`
// Method
//
// Should be set to "webauthn" when trying to add, update, or remove a webAuthn pairing.
//
// required: true
Method string `json:"method"`
// Flow is flow ID.
//
// swagger:ignore
Flow string `json:"flow"`
// Transient data to pass along to any webhooks
//
// required: false
TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
}
func (s *Strategy) handleRegistrationError(r *http.Request, f *registration.Flow, p updateRegistrationFlowWithWebAuthnMethod, err error) error {
if f != nil {
for _, n := range container.NewFromJSON("", node.DefaultGroup, p.Traits, "traits").Nodes {
// we only set the value and not the whole field because we want to keep types from the initial form generation
f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue())
}
f.UI.Nodes.SetValueAttribute(node.WebAuthnRegisterDisplayName, p.RegisterDisplayName)
if f.Type == flow.TypeBrowser {
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
}
}
return err
}
func (s *Strategy) decode(p *updateRegistrationFlowWithWebAuthnMethod, r *http.Request, ds *url.URL) error {
return registration.DecodeBody(p, r, registrationSchema, ds)
}
func (s *Strategy) Register(_ http.ResponseWriter, r *http.Request, regFlow *registration.Flow, i *identity.Identity) (err error) {
ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.webauthn.Strategy.Register")
defer otelx.End(span, &err)
if regFlow.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(ctx) {
span.SetAttributes(attribute.String("not_responsible_reason", "registration flow is not a browser flow or WebAuthn is not enabled"))
return flow.ErrStrategyNotResponsible
}
ds, err := regFlow.IdentitySchema.URL(ctx, s.d.Config())
if err != nil {
return err
}
var p updateRegistrationFlowWithWebAuthnMethod
if err := s.decode(&p, r, ds); err != nil {
return s.handleRegistrationError(r, regFlow, p, err)
}
regFlow.TransientPayload = p.TransientPayload
if err := flow.EnsureCSRF(s.d, r, regFlow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil {
return s.handleRegistrationError(r, regFlow, p, err)
}
if len(p.Register) == 0 {
span.SetAttributes(attribute.String("not_responsible_reason", "register field is empty"))
return flow.ErrStrategyNotResponsible
}
p.Method = s.SettingsStrategyID()
if err := flow.MethodEnabledAndAllowed(ctx, regFlow.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil {
return s.handleRegistrationError(r, regFlow, p, err)
}
if len(p.Traits) == 0 {
p.Traits = json.RawMessage("{}")
}
i.Traits = identity.Traits(p.Traits)
webAuthnSession := gjson.GetBytes(regFlow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData))
if !webAuthnSession.IsObject() {
return s.handleRegistrationError(r, regFlow, p, errors.WithStack(
herodot.ErrInternalServerError().WithReasonf("Expected WebAuthN in internal context to be an object.")))
}
var webAuthnSess webauthn.SessionData
if err := json.Unmarshal([]byte(webAuthnSession.Raw), &webAuthnSess); err != nil {
return s.handleRegistrationError(r, regFlow, p, errors.WithStack(
herodot.ErrInternalServerError().WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err)))
}
webAuthnResponse, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(p.Register))
if err != nil {
return s.handleRegistrationError(r, regFlow, p, errors.WithStack(
herodot.ErrBadRequest().WithReasonf("Unable to parse WebAuthn response: %s", err)))
}
web, err := webauthn.New(s.d.Config().WebAuthnConfig(ctx))
if err != nil {
return s.handleRegistrationError(r, regFlow, p, errors.WithStack(
herodot.ErrInternalServerError().WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error())))
}
credential, err := web.CreateCredential(webauthnx.NewUser(webAuthnSess.UserID, nil, web.Config), webAuthnSess, webAuthnResponse)
if err != nil {
if devErr := new(protocol.Error); errors.As(err, &devErr) {
s.d.Logger().WithError(err).WithField("error_devinfo", devErr.DevInfo).Error("Failed to create WebAuthn credential")
}
return s.handleRegistrationError(r, regFlow, p, errors.WithStack(
herodot.ErrInternalServerError().WithReasonf("Unable to create WebAuthn credential: %s", err)))
}
credentialWebAuthn := identity.CredentialFromWebAuthn(credential, true)
credentialWebAuthn.DisplayName = p.RegisterDisplayName
credentialWebAuthnConfig, err := json.Marshal(identity.CredentialsWebAuthnConfig{
Credentials: identity.CredentialsWebAuthn{*credentialWebAuthn},
UserHandle: webAuthnSess.UserID,
})
if err != nil {
return s.handleRegistrationError(r, regFlow, p, errors.WithStack(
herodot.ErrInternalServerError().WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())))
}
i.UpsertCredentialsConfig(s.ID(), credentialWebAuthnConfig, 1)
if err := s.validateCredentials(ctx, i); err != nil {
return s.handleRegistrationError(r, regFlow, p, err)
}
// Remove the WebAuthn URL from the internal context now that it is set!
regFlow.InternalContext, err = sjson.DeleteBytes(regFlow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData))
if err != nil {
return s.handleRegistrationError(r, regFlow, p, err)
}
if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil {
return s.handleRegistrationError(r, regFlow, p, err)
}
return nil
}
func (s *Strategy) injectWebauthnRegistrationOptions(r *http.Request, f *registration.Flow) ([]byte, error) {
ctx := r.Context()
if options := gjson.GetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeyWebauthnOptions)); options.IsObject() {
return []byte(options.Raw), nil
}
web, err := webauthn.New(s.d.Config().WebAuthnConfig(ctx))
if err != nil {
return nil, errors.WithStack(err)
}
webauthID := x.NewUUID()
user := webauthnx.NewUser(webauthID[:], nil, s.d.Config().WebAuthnConfig(ctx))
option, sessionData, err := web.BeginRegistration(user)
if err != nil {
return nil, errors.WithStack(err)
}
injectWebAuthnOptions, err := json.Marshal(option)
if err != nil {
return nil, errors.WithStack(err)
}
f.InternalContext, err = sjson.SetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), sessionData)
if err != nil {
return nil, errors.WithStack(err)
}
f.InternalContext, err = sjson.SetRawBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeyWebauthnOptions), injectWebAuthnOptions)
if err != nil {
return nil, errors.WithStack(err)
}
return injectWebAuthnOptions, nil
}
func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error {
ctx := r.Context()
if f.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(ctx) {
return nil
}
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
opts, err := s.injectWebauthnRegistrationOptions(r, f)
if err != nil {
return nil
}
f.UI.Nodes.Upsert(nodeDisplayName())
f.UI.Nodes.Upsert(nodeWebauthnRegistrationOptions(opts))
f.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx)))
f.UI.Nodes.Upsert(nodeConnectionInput())
return nil
}
func (s *Strategy) PopulateRegistrationMethodProfile(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error {
ctx := r.Context()
if f.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(ctx) {
return nil
}
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
opts, err := s.injectWebauthnRegistrationOptions(r, f)
if err != nil {
return nil
}
f.UI.Nodes.RemoveMatching(nodeDisplayName())
f.UI.Nodes.RemoveMatching(nodeWebauthnRegistrationOptions(opts))
f.UI.Nodes.RemoveMatching(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx)))
f.UI.Nodes.RemoveMatching(nodeConnectionInput())
return nil
}
func (s *Strategy) PopulateRegistrationMethodCredentials(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error {
ctx := r.Context()
if f.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(ctx) {
return nil
}
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
opts, err := s.injectWebauthnRegistrationOptions(r, f)
if err != nil {
return nil
}
f.UI.Nodes.Upsert(nodeDisplayName())
f.UI.Nodes.Upsert(nodeWebauthnRegistrationOptions(opts))
f.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx)))
f.UI.Nodes.Upsert(nodeConnectionInput())
return nil
}

View File

@@ -0,0 +1,628 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package webauthn_test
import (
"context"
_ "embed"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/x/configx"
"github.com/ory/kratos/driver"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/pkg"
kratos "github.com/ory/kratos/pkg/httpclient"
"github.com/ory/kratos/pkg/registrationhelpers"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/selfservice/strategy/webauthn"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
"github.com/ory/x/assertx"
"github.com/ory/x/contextx"
"github.com/ory/x/snapshotx"
)
var (
flows = []string{"spa", "browser"}
//go:embed fixtures/registration/success/response.json
registrationFixtureSuccessResponse []byte
//go:embed fixtures/registration/success/internal_context.json
registrationFixtureSuccessInternalContext []byte
//go:embed fixtures/registration/failure/internal_context_wrong_user_id.json
registrationFixtureFailureInternalContextWrongUserID []byte
)
func flowToIsSPA(flow string) bool {
return flow == "spa"
}
func newRegistrationRegistry(t *testing.T, cfgOpts ...configx.OptionModifier) *driver.RegistryDefault {
_, reg := pkg.NewFastRegistryWithMocks(t, append([]configx.OptionModifier{
configx.WithValues(map[string]any{
config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePassword) + ".enabled": true,
config.ViperKeyWebAuthnPasswordless: true,
config.ViperKeySelfServiceRegistrationLoginHints: true,
config.ViperKeySelfServiceRegistrationEnableLegacyOneStep: true,
}),
enabledWebauthn,
}, cfgOpts...)...)
return reg
}
func TestRegistration(t *testing.T) {
reg := newRegistrationRegistry(t, configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/registration.schema.json")))
conf := reg.Config()
publicTS, _ := testhelpers.NewKratosServer(t, reg)
_ = testhelpers.NewErrorTestServer(t, reg)
_ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg)
_ = testhelpers.NewRedirSessionEchoTS(t, reg)
redirTS := testhelpers.NewRedirSessionEchoTS(t, reg)
redirNoSessionTS := testhelpers.NewRedirNoSessionTS(t, reg)
// set the "return to" server, which will assert the session state
// (redirTS: enforce that a session exists, redirNoSessionTS: enforce that no session exists)
useReturnToFromTS := func(ts *httptest.Server) {
conf.MustSet(t.Context(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/default-return-to")
conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, ts.URL+"/registration-return-ts")
}
useReturnToFromTS(redirTS)
//checkURL := func(t *testing.T, shouldRedirect bool, res *http.Response) {
// if shouldRedirect {
// assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/registration-ts")
// } else {
// assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow)
// }
//}
t.Run("AssertCommonErrorCases", func(t *testing.T) {
registrationhelpers.AssertCommonErrorCases(t, flows)
})
t.Run("AssertRegistrationRespectsValidation", func(t *testing.T) {
reg := newRegistrationRegistry(t)
registrationhelpers.AssertRegistrationRespectsValidation(t, reg, flows, func(v url.Values) {
v.Del("traits.foobar")
v.Set(node.WebAuthnRegister, "{}")
v.Del("method")
})
})
t.Run("AssertCSRFFailures", func(t *testing.T) {
reg := newRegistrationRegistry(t)
registrationhelpers.AssertCSRFFailures(t, reg, flows, func(v url.Values) {
v.Set(node.WebAuthnRegister, "{}")
v.Del("method")
})
})
t.Run("AssertSchemaDoesNotExist", func(t *testing.T) {
reg := newRegistrationRegistry(t)
registrationhelpers.AssertSchemaDoesNotExist(t, reg, flows, func(v url.Values) {
v.Set(node.WebAuthnRegister, "{}")
v.Del("method")
})
})
t.Run("case=webauthn button does not exist when passwordless is disabled", func(t *testing.T) {
conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, false)
t.Cleanup(func() {
conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, true)
})
for _, f := range flows {
t.Run(f, func(t *testing.T) {
client := testhelpers.NewClientWithCookies(t)
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, flowToIsSPA(f), false, false)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
})
})
}
})
t.Run("case=webauthn button exists", func(t *testing.T) {
for _, f := range flows {
t.Run(f, func(t *testing.T) {
client := testhelpers.NewClientWithCookies(t)
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, flowToIsSPA(f), false, false)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
"5.attributes.onclick",
"5.attributes.value",
"6.attributes.nonce",
"6.attributes.src",
})
})
}
})
t.Run("case=should return an error because not passing validation", func(t *testing.T) {
email := testhelpers.RandomEmail()
values := func(v url.Values) {
v.Set("traits.username", email)
v.Del("traits.foobar")
v.Set(node.WebAuthnRegister, "{}")
v.Del("method")
}
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
actual := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, f, values)
assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual)
assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual)
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username", "traits.foobar")
assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0").String(), `Property foobar is missing`, "%s", actual)
assert.Equal(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual)
})
}
})
t.Run("case=should reject invalid transient payload", func(t *testing.T) {
email := testhelpers.RandomEmail()
values := func(v url.Values) {
v.Set("traits.username", email)
v.Set("traits.foobar", "bar")
v.Set("transient_payload", "42")
v.Set(node.WebAuthnRegister, "{}")
v.Del("method")
}
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
actual := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, f, values)
assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual)
assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual)
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username", "traits.foobar")
assert.Equal(t, "bar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).attributes.value").String(), "%s", actual)
assert.Equal(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual)
assert.Equal(t, int64(4000026), gjson.Get(actual, "ui.nodes.#(attributes.name==transient_payload).messages.0.id").Int(), "%s", actual)
})
}
})
t.Run("case=should return an error because webauthn response is invalid", func(t *testing.T) {
email := testhelpers.RandomEmail()
values := func(v url.Values) {
v.Set("traits.username", email)
v.Set("traits.foobar", "bazbar")
v.Set(node.WebAuthnRegister, "{}")
v.Del("method")
}
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
actual := registrationhelpers.ExpectValidationError(t.Context(), t, publicTS, conf, f, values)
assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual)
assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual)
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username", "traits.foobar")
assert.Equal(t, "bazbar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).attributes.value").String(), "%s", actual)
assert.Equal(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual)
assert.Contains(t, gjson.Get(actual, "ui.messages.0").String(), `Unable to parse WebAuthn response: Parse error for Registration`, "%s", actual)
})
}
})
submitWebAuthnRegistrationWithClient := func(t *testing.T, flow string, contextFixture []byte, client *http.Client, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.RegistrationFlow) {
isSPA := flow == "spa"
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, isSPA, false, false, opts...)
// We inject the session to replay
interim, err := reg.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), uuid.FromStringOrNil(f.Id))
require.NoError(t, err)
interim.InternalContext = contextFixture
require.NoError(t, reg.RegistrationFlowPersister().UpdateRegistrationFlow(context.Background(), interim))
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
cb(values)
// We use the response replay
body, res := testhelpers.RegistrationMakeRequest(t, false, isSPA, f, client, values.Encode())
return body, res, f
}
t.Run("case=should return an error because internal context is invalid", func(t *testing.T) {
email := testhelpers.RandomEmail()
for _, tc := range []struct {
name string
internalContext string
}{{
name: "invalid json",
internalContext: "invalid",
}, {
name: "wrong user ID",
internalContext: string(registrationFixtureFailureInternalContextWrongUserID),
}} {
tc := tc
t.Run("context="+tc.name, func(t *testing.T) {
values := func(v url.Values) {
v.Set("traits.username", email)
v.Set("traits.foobar", "bazbar")
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
}
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
actual, _, _ := submitWebAuthnRegistrationWithClient(t, f,
[]byte(tc.internalContext),
testhelpers.NewClientWithCookies(t),
values,
)
if f == "spa" {
assert.Equal(t, "Internal Server Error", gjson.Get(actual, "error.status").String(), "%s", actual)
} else {
assert.Equal(t, "Internal Server Error", gjson.Get(actual, "status").String(), "%s", actual)
}
})
}
})
}
})
t.Run("case=should fail to create identity if schema is missing the identifier", func(t *testing.T) {
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/noid.schema.json")
t.Cleanup(func() {
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json")
})
email := testhelpers.RandomEmail()
values := func(v url.Values) {
v.Set("traits.email", email)
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
}
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
actual, _, _ := submitWebAuthnRegistrationWithClient(t, f, registrationFixtureSuccessInternalContext, testhelpers.NewClientWithCookies(t), values)
assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual)
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.email")
assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual)
})
}
})
makeRegistration := func(t *testing.T, f string, values func(v url.Values), opts ...testhelpers.InitFlowWithOption) (actual string, res *http.Response, fetchedFlow *registration.Flow) {
actual, res, actualFlow := submitWebAuthnRegistrationWithClient(t, f, registrationFixtureSuccessInternalContext, testhelpers.NewClientWithCookies(t), values, opts...)
fetchedFlow, err := reg.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), uuid.FromStringOrNil(actualFlow.Id))
require.NoError(t, err)
return actual, res, fetchedFlow
}
makeSuccessfulRegistration := func(t *testing.T, f string, expectReturnTo string, values func(v url.Values), opts ...testhelpers.InitFlowWithOption) (actual string) {
actual, res, fetchedFlow := makeRegistration(t, f, values, opts...)
assert.Empty(t, gjson.GetBytes(fetchedFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeWebAuthn, webauthn.InternalContextKeySessionData)), "has cleaned up the internal context after success")
if f == "spa" {
expectReturnTo = publicTS.URL
}
assert.Contains(t, res.Request.URL.String(), expectReturnTo, "%+v\n\t%s", res.Request, assertx.PrettifyJSONPayload(t, actual))
return actual
}
getPrefix := func(f string) (prefix string) {
if f == "spa" {
prefix = "session."
}
return
}
t.Run("successful registration", func(t *testing.T) {
t.Cleanup(func() {
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), nil)
})
values := func(email string) func(v url.Values) {
return func(v url.Values) {
v.Set("traits.username", email)
v.Set("traits.foobar", "bazbar")
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
}
}
t.Run("case=should create the identity but not a session", func(t *testing.T) {
useReturnToFromTS(redirNoSessionTS)
t.Cleanup(func() {
useReturnToFromTS(redirTS)
})
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil)
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
email := testhelpers.RandomEmail()
actual := makeSuccessfulRegistration(t, f, redirNoSessionTS.URL+"/registration-return-ts", values(email))
if f == "spa" {
assert.Equal(t, email, gjson.Get(actual, "identity.traits.username").String(), "%s", actual)
assert.False(t, gjson.Get(actual, "session").Exists(), "because the registration yielded no session, the user is not expected to be signed in: %s", actual)
} else {
assert.Equal(t, "null\n", actual, "because the registration yielded no session, the user is not expected to be signed in: %s", actual)
}
i, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email)
require.NoError(t, err)
assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual)
if f == "spa" {
assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), redirNoSessionTS.URL+"/registration-return-ts", "%s", actual)
} else {
assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
}
})
}
})
t.Run("case=should accept valid transient payload", func(t *testing.T) {
useReturnToFromTS(redirNoSessionTS)
t.Cleanup(func() {
useReturnToFromTS(redirTS)
})
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil)
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
email := testhelpers.RandomEmail()
actual := makeSuccessfulRegistration(t, f, redirNoSessionTS.URL+"/registration-return-ts", func(v url.Values) {
values(email)(v)
v.Set("transient_payload.stuff", "42")
})
if f == "spa" {
assert.Equal(t, email, gjson.Get(actual, "identity.traits.username").String(), "%s", actual)
assert.False(t, gjson.Get(actual, "session").Exists(), "because the registration yielded no session, the user is not expected to be signed in: %s", actual)
} else {
assert.Equal(t, "null\n", actual, "because the registration yielded no session, the user is not expected to be signed in: %s", actual)
}
i, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email)
require.NoError(t, err)
assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual)
})
}
})
t.Run("case=should create the identity and a session and use the correct schema", func(t *testing.T) {
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), []config.SelfServiceHook{{Name: "session"}})
conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "advanced-user")
conf.MustSet(t.Context(), config.ViperKeyIdentitySchemas, config.Schemas{
{ID: "does-not-exist", URL: "file://./stub/profile.schema.json"},
{ID: "advanced-user", URL: "file://./stub/registration.schema.json"},
})
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
email := testhelpers.RandomEmail()
actual := makeSuccessfulRegistration(t, f, redirTS.URL+"/registration-return-ts", values(email))
prefix := getPrefix(f)
assert.Equal(t, email, gjson.Get(actual, prefix+"identity.traits.username").String(), "%s", actual)
assert.True(t, gjson.Get(actual, prefix+"active").Bool(), "%s", actual)
i, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email)
require.NoError(t, err)
assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual)
})
}
})
t.Run("case=not able to create the same account twice", func(t *testing.T) {
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), []config.SelfServiceHook{{Name: "session"}})
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json")
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
email := testhelpers.RandomEmail()
actual := makeSuccessfulRegistration(t, f, redirTS.URL+"/registration-return-ts", values(email))
assert.True(t, gjson.Get(actual, getPrefix(f)+"active").Bool(), "%s", actual)
actual, _, _ = makeRegistration(t, f, values(email))
assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual)
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username")
assert.Equal(t, "You tried signing in with "+email+" which is already in use by another account. You can sign in using your passkey or a security key.", gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual)
})
}
})
t.Run("case=reset previous form errors", func(t *testing.T) {
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), []config.SelfServiceHook{{Name: "session"}})
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json")
for _, f := range flows {
t.Run("type="+f, func(t *testing.T) {
email := testhelpers.RandomEmail()
actual, _, _ := makeRegistration(t, f, func(v url.Values) {
v.Del("traits.username")
v.Set("traits.foobar", "bazbar")
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
})
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username", "traits.foobar")
assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).messages.0").String(), `Property username is missing`, "%s", actual)
actual, _, _ = makeRegistration(t, f, func(v url.Values) {
v.Set("traits.username", email)
v.Del("traits.foobar")
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
})
registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username", "traits.foobar")
assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0").String(), `Property foobar is missing`, "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).messages").Array())
assert.Empty(t, gjson.Get(actual, "ui.nodes.messages").Array())
})
}
})
t.Run("case=multi-schema should create the identity and a session and use the correct schema", func(t *testing.T) {
conf.MustSet(t.Context(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeWebAuthn.String()), []config.SelfServiceHook{{Name: "session"}})
conf.MustSet(t.Context(), config.ViperKeyDefaultIdentitySchemaID, "does-not-exist")
conf.MustSet(t.Context(), config.ViperKeyIdentitySchemas, config.Schemas{
{ID: "does-not-exist", URL: "file://./stub/profile.schema.json"},
{ID: "advanced-user", URL: "file://./stub/registration.schema.json", SelfserviceSelectable: true},
})
for _, f := range flows {
t.Run("type="+f+" registration success", func(t *testing.T) {
email := testhelpers.RandomEmail()
actual := makeSuccessfulRegistration(t, f, redirTS.URL+"/registration-return-ts", values(email), testhelpers.InitFlowWithIdentitySchema("advanced-user"))
prefix := getPrefix(f)
assert.Equal(t, email, gjson.Get(actual, prefix+"identity.traits.username").String(), "%s", actual)
assert.True(t, gjson.Get(actual, prefix+"active").Bool(), "%s", actual)
i, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email)
require.NoError(t, err)
assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual)
})
t.Run("type="+f+" registration failure due to invalid form data", func(t *testing.T) {
invalidValues := func(v url.Values) {
v.Set("traits.username", testhelpers.RandomEmail())
v.Set("traits.foobar", "b")
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
}
actual, res, _ := submitWebAuthnRegistrationWithClient(t, f, registrationFixtureSuccessInternalContext, testhelpers.NewClientWithCookies(t), invalidValues, testhelpers.InitFlowWithIdentitySchema("advanced-user"))
if f == "browser" {
assert.Equal(t, http.StatusOK, res.StatusCode, "%s", actual)
} else {
assert.Equal(t, http.StatusBadRequest, res.StatusCode, "%s", actual)
}
assert.Equal(t, int64(4000003), gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.id").Int(), "%s", actual)
assert.Equal(t, "length must be \u003e= 2, but got 1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0.text").String(), "%s", actual)
})
}
})
})
t.Run("case=should fail if no identifier was set in the schema", func(t *testing.T) {
testhelpers.SetDefaultIdentitySchema(conf, "file://stub/missing-identifier.schema.json")
for _, f := range []string{"spa", "api", "browser"} {
t.Run("type="+f, func(t *testing.T) {
actual, _, _ := makeRegistration(t, f, func(v url.Values) {
v.Set("traits.email", testhelpers.RandomEmail())
v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse))
v.Del("method")
})
assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual)
})
}
})
}
func TestPopulateRegistrationMethod(t *testing.T) {
ctx := context.Background()
_, reg := pkg.NewFastRegistryWithMocks(t)
ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/registration.schema.json")
ctx = contextx.WithConfigValue(ctx, config.ViperKeyWebAuthnRPID, "localhost")
ctx = contextx.WithConfigValue(ctx, config.ViperKeyWebAuthnRPDisplayName, "localhost")
ctx = contextx.WithConfigValue(ctx, config.ViperKeyWebAuthnPasswordless, true)
s, err := reg.AllRegistrationStrategies().Strategy(identity.CredentialsTypeWebAuthn)
require.NoError(t, err)
fh, ok := s.(registration.FormHydrator)
require.True(t, ok)
toSnapshot := func(t *testing.T, f node.Nodes, except ...snapshotx.Opt) {
t.Helper()
// The CSRF token has a unique value that messes with the snapshot - ignore it.
f.ResetNodes("csrf_token")
snapshotx.SnapshotT(t, f, append(except, snapshotx.ExceptNestedKeys("nonce", "src", "onclick"))...)
}
newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *registration.Flow) {
r := httptest.NewRequest("GET", "/self-service/registration/browser", nil)
r = r.WithContext(ctx)
t.Helper()
f, err := registration.NewFlow(reg, r, flow.TypeBrowser)
f.UI.Nodes = make(node.Nodes, 0)
require.NoError(t, err)
return r, f
}
t.Run("method=PopulateRegistrationMethod", func(t *testing.T) {
r, f := newFlow(ctx, t)
require.NoError(t, fh.PopulateRegistrationMethod(r, f))
toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("2.attributes.value"))
})
t.Run("method=PopulateRegistrationMethodProfile", func(t *testing.T) {
r, f := newFlow(ctx, t)
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
toSnapshot(t, f.UI.Nodes)
})
t.Run("method=PopulateRegistrationMethodCredentials", func(t *testing.T) {
r, f := newFlow(ctx, t)
require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f))
toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("2.attributes.value"))
})
t.Run("method=idempotency", func(t *testing.T) {
r, f := newFlow(ctx, t)
var snapshots []node.Nodes
t.Run("case=1", func(t *testing.T) {
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
snapshots = append(snapshots, f.UI.Nodes)
toSnapshot(t, f.UI.Nodes)
})
t.Run("case=2", func(t *testing.T) {
require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f))
snapshots = append(snapshots, f.UI.Nodes)
toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("2.attributes.value"))
})
t.Run("case=3", func(t *testing.T) {
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
snapshots = append(snapshots, f.UI.Nodes)
toSnapshot(t, f.UI.Nodes)
})
t.Run("case=4", func(t *testing.T) {
require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f))
snapshots = append(snapshots, f.UI.Nodes)
toSnapshot(t, f.UI.Nodes, snapshotx.ExceptPaths("2.attributes.value"))
})
t.Run("case=evaluate", func(t *testing.T) {
assertx.EqualAsJSON(t, snapshots[0], snapshots[2])
assertx.EqualAsJSONExcept(t, snapshots[1], snapshots[3], []string{"3.attributes.nonce"})
})
})
}

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