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,27 @@
{
"$id": "https://schemas.ory.sh/kratos/selfservice/strategy/profile/registration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"csrf_token": {
"type": "string"
},
"traits": {
"description": "This field will be overwritten in registration.go's decoder() method. Do not add anything to this field as it has no effect."
},
"screen": {
"type": "string",
"enum": [
"credential-selection",
"previous"
]
},
"method": {
"type": "string"
},
"transient_payload": {
"type": "object",
"additionalProperties": true
}
}
}

View File

@@ -0,0 +1,24 @@
{
"$id": "https://schemas.ory.sh/kratos/selfservice/strategy/profile/settings.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [
"traits"
],
"properties": {
"method": {
"type": "string"
},
"traits": {},
"csrf_token": {
"type": "string"
},
"action": {
"type": "string"
},
"transient_payload": {
"type": "object",
"additionalProperties": true
}
}
}

View File

@@ -0,0 +1,126 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.email",
"node_type": "input",
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.stringy",
"node_type": "input",
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.numby",
"node_type": "input",
"type": "number"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.booly",
"node_type": "input",
"type": "checkbox"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_big_number",
"node_type": "input",
"type": "number"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_long_string",
"node_type": "input",
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,126 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.email",
"node_type": "input",
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.stringy",
"node_type": "input",
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.numby",
"node_type": "input",
"type": "number"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.booly",
"node_type": "input",
"type": "checkbox"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_big_number",
"node_type": "input",
"type": "number"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_long_string",
"node_type": "input",
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,106 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.booly",
"type": "checkbox",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.email",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.numby",
"type": "number",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_big_number",
"type": "number",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_long_string",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.stringy",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "method",
"type": "submit",
"value": "profile",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,87 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.booly",
"type": "checkbox",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.email",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.numby",
"type": "number",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_big_number",
"type": "number",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_long_string",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.stringy",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
}
]

View File

@@ -0,0 +1,21 @@
[
{
"type": "input",
"group": "profile",
"attributes": {
"name": "screen",
"type": "submit",
"value": "previous",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040008,
"text": "Back",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,106 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.booly",
"type": "checkbox",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.email",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.numby",
"type": "number",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_big_number",
"type": "number",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_long_string",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.stringy",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "method",
"type": "submit",
"value": "profile",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,106 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.booly",
"type": "checkbox",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.email",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.numby",
"type": "number",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_big_number",
"type": "number",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_long_string",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.stringy",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "method",
"type": "submit",
"value": "profile",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,106 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.booly",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.email",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.numby",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_big_number",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_long_string",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.stringy",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "screen",
"type": "submit",
"value": "previous",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040008,
"text": "Back",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,106 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.booly",
"type": "checkbox",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.email",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.numby",
"type": "number",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_big_number",
"type": "number",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_long_string",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.stringy",
"type": "text",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "method",
"type": "submit",
"value": "profile",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,106 @@
[
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.booly",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.email",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.numby",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_big_number",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_long_string",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.stringy",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "screen",
"type": "submit",
"value": "previous",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040008,
"text": "Back",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,153 @@
{
"method": "POST",
"nodes": [
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.email",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.stringy",
"node_type": "input",
"type": "text",
"value": "foobar"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.numby",
"node_type": "input",
"type": "number",
"value": 2.5
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.booly",
"node_type": "input",
"type": "checkbox",
"value": false
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_big_number",
"node_type": "input",
"type": "number",
"value": 2048
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_long_string",
"node_type": "input",
"type": "text",
"value": "asdfasdfasdfasdfasfdasdfasdfasdf"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
}
]
}

View File

@@ -0,0 +1,153 @@
{
"method": "POST",
"nodes": [
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.email",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.stringy",
"node_type": "input",
"type": "text",
"value": "foobar"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.numby",
"node_type": "input",
"type": "number",
"value": 2.5
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.booly",
"node_type": "input",
"type": "checkbox",
"value": false
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_big_number",
"node_type": "input",
"type": "number",
"value": 2048
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_long_string",
"node_type": "input",
"type": "text",
"value": "asdfasdfasdfasdfasfdasdfasdfasdf"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
}
]
}

View File

@@ -0,0 +1,153 @@
{
"method": "POST",
"nodes": [
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.email",
"node_type": "input",
"type": "text"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.stringy",
"node_type": "input",
"type": "text",
"value": "foobar"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.numby",
"node_type": "input",
"type": "number",
"value": 2.5
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.booly",
"node_type": "input",
"type": "checkbox",
"value": false
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_big_number",
"node_type": "input",
"type": "number",
"value": 2048
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_long_string",
"node_type": "input",
"type": "text",
"value": "asdfasdfasdfasdfasfdasdfasdfasdf"
},
"group": "profile",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"autocomplete": "new-password",
"disabled": false,
"name": "password",
"node_type": "input",
"required": true,
"type": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "password"
},
"group": "password",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
}
]
}

View File

@@ -0,0 +1,129 @@
[
{
"attributes": {
"disabled": false,
"name": "provider",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google",
"provider_id": "google"
},
"id": 1040002,
"text": "Sign up with google",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.email",
"node_type": "input",
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.stringy",
"node_type": "input",
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.numby",
"node_type": "input",
"type": "number"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.booly",
"node_type": "input",
"type": "checkbox"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_big_number",
"node_type": "input",
"type": "number"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_long_string",
"node_type": "input",
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,129 @@
[
{
"attributes": {
"disabled": false,
"name": "provider",
"node_type": "input",
"type": "submit",
"value": "google"
},
"group": "oidc",
"messages": [],
"meta": {
"label": {
"context": {
"provider": "google",
"provider_id": "google"
},
"id": 1040002,
"text": "Sign up with google",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.email",
"node_type": "input",
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.stringy",
"node_type": "input",
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.numby",
"node_type": "input",
"type": "number"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.booly",
"node_type": "input",
"type": "checkbox"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_big_number",
"node_type": "input",
"type": "number"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "traits.should_long_string",
"node_type": "input",
"type": "text"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"node_type": "input",
"type": "submit",
"value": "profile"
},
"group": "profile",
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
},
"type": "input"
}
]

View File

@@ -0,0 +1,154 @@
[
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "google",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with google",
"type": "info",
"context": {
"provider": "google",
"provider_id": "google"
}
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.email",
"type": "text",
"value": "browser-1-1@example.org",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.stringy",
"type": "text",
"value": "string",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.numby",
"type": "number",
"value": 1,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.booly",
"type": "checkbox",
"value": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_big_number",
"type": "number",
"value": 1000000,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_long_string",
"type": "text",
"value": "1111111111111111111111111111111111111111111111111111111111",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "method",
"type": "submit",
"value": "profile",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "method",
"type": "submit",
"value": "profile",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,252 @@
[
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "google",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with google",
"type": "info",
"context": {
"provider": "google",
"provider_id": "google"
}
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.email",
"type": "hidden",
"value": "browser-1@example.org",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.stringy",
"type": "hidden",
"value": "string",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.numby",
"type": "hidden",
"value": 1,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.booly",
"type": "hidden",
"value": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_big_number",
"type": "hidden",
"value": 1000000,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_long_string",
"type": "hidden",
"value": "1111111111111111111111111111111111111111111111111111111111",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "script",
"group": "webauthn",
"attributes": {
"async": true,
"referrerpolicy": "no-referrer",
"crossorigin": "anonymous",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"type": "text/javascript",
"id": "webauthn_script",
"node_type": "script"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "passkey",
"attributes": {
"name": "passkey_register_trigger",
"type": "button",
"disabled": false,
"onclick": "window.oryPasskeyRegistration()",
"onclickTrigger": "oryPasskeyRegistration",
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040007,
"text": "Sign up with passkey",
"type": "info"
}
}
},
{
"type": "input",
"group": "passkey",
"attributes": {
"name": "passkey_register",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "passkey",
"attributes": {
"name": "passkey_create_data",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "method",
"type": "submit",
"value": "code",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040006,
"text": "Send sign up code",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "new-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "screen",
"type": "submit",
"value": "previous",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040008,
"text": "Back",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,247 @@
[
{
"type": "input",
"group": "oidc",
"attributes": {
"name": "provider",
"type": "submit",
"value": "google",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040002,
"text": "Sign up with google",
"type": "info",
"context": {
"provider": "google",
"provider_id": "google"
}
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.email",
"type": "hidden",
"value": "browser-1-1@example.org",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.stringy",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.numby",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.booly",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_big_number",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "traits.should_long_string",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "script",
"group": "webauthn",
"attributes": {
"async": true,
"referrerpolicy": "no-referrer",
"crossorigin": "anonymous",
"integrity": "sha512-9Qlvd9zudkdfprW3Tf8FsDI2bVA667UGRVUBcima8+YLHQenHVyPaAiSTLzQ3V6MrW60xS7NpWod+UTdVa6QmQ==",
"type": "text/javascript",
"id": "webauthn_script",
"node_type": "script"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "passkey",
"attributes": {
"name": "passkey_register_trigger",
"type": "button",
"disabled": false,
"onclick": "window.oryPasskeyRegistration()",
"onclickTrigger": "oryPasskeyRegistration",
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040007,
"text": "Sign up with passkey",
"type": "info"
}
}
},
{
"type": "input",
"group": "passkey",
"attributes": {
"name": "passkey_register",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "passkey",
"attributes": {
"name": "passkey_create_data",
"type": "hidden",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "method",
"type": "submit",
"value": "code",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040006,
"text": "Send sign up code",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "new-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info"
}
}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "screen",
"type": "submit",
"value": "previous",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040008,
"text": "Back",
"type": "info"
}
}
}
]

View File

@@ -0,0 +1,27 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package profile
import (
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
)
func nodePreviousScreen() *node.Node {
return node.NewInputField(
"screen",
"previous",
node.ProfileGroup,
node.InputAttributeTypeSubmit,
).WithMetaLabel(text.NewInfoRegistrationBack())
}
func nodeSubmitProfile() *node.Node {
return node.NewInputField(
"method",
"profile",
node.ProfileGroup,
node.InputAttributeTypeSubmit,
).WithMetaLabel(text.NewInfoRegistration())
}

View File

@@ -0,0 +1,341 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package profile
import (
"context"
_ "embed"
"encoding/json"
"net/http"
"net/url"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/container"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
"github.com/ory/x/decoderx"
"github.com/ory/x/otelx"
"github.com/ory/x/otelx/semconv"
)
//go:embed .schema/registration.schema.json
var registrationSchema []byte
// The RegistrationScreen
// swagger:enum RegistrationScreen
type RegistrationScreen string
const (
//nolint:gosec // not a credential
RegistrationScreenCredentialSelection RegistrationScreen = "credential-selection"
RegistrationScreenPrevious RegistrationScreen = "previous"
)
var _ registration.Strategy = new(Strategy)
var _ registration.FormHydrator = new(Strategy)
// Update Registration Flow with Profile Method
//
// swagger:model updateRegistrationFlowWithProfileMethod
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type updateRegistrationFlowWithProfileMethod struct {
// Traits
//
// The identity's traits.
//
// required: true
Traits json.RawMessage `json:"traits"`
// Method
//
// Should be set to profile when trying to update a profile.
//
// required: true
Method string `json:"method"`
// Screen requests navigation to a previous screen.
//
// This must be set to credential-selection to go back to the credential
// selection screen.
//
// required: false
Screen RegistrationScreen `json:"screen" form:"screen"`
// FlowIDRequestID is the flow ID.
//
// swagger:ignore
FlowID string `json:"flow"`
// The Anti-CSRF Token
//
// This token is only required when performing browser flows.
CSRFToken string `json:"csrf_token"`
// Transient data to pass along to any webhooks
//
// required: false
TransientPayload json.RawMessage `json:"transient_payload,omitempty"`
}
func (s *Strategy) ID() identity.CredentialsType {
return identity.CredentialsTypeProfile
}
func (s *Strategy) PopulateRegistrationMethodCredentials(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error {
f.UI.Nodes.Append(nodePreviousScreen())
f.UI.Nodes.RemoveMatching(nodeSubmitProfile())
for _, n := range f.UI.Nodes {
if n.Group != node.DefaultGroup || n.Type != node.Input {
continue
}
if attr, ok := n.Attributes.(*node.InputAttributes); ok {
attr.Type = node.InputAttributeTypeHidden
}
}
f.UI.Messages.Add(text.NewInfoSelfServiceChooseCredentials())
return nil
}
func (s *Strategy) PopulateRegistrationMethodProfile(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error {
ds, err := f.IdentitySchema.URL(r.Context(), s.d.Config())
if err != nil {
return err
}
conf := new(registration.FormHydratorOptions)
for _, o := range options {
o(conf)
}
f.UI.Nodes.RemoveMatching(nodePreviousScreen())
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
nodes, err := container.NodesFromJSONSchema(r.Context(), node.DefaultGroup, ds.String(), "", nil, s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context()))
if err != nil {
return err
}
for _, n := range nodes {
f.UI.Nodes.Upsert(n)
}
if len(conf.WithTraits) > 0 {
f.UI.UpdateNodeValuesFromJSON(conf.WithTraits, "traits", node.DefaultGroup)
}
f.UI.Nodes.Append(nodeSubmitProfile())
return nil
}
func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error {
ds, err := f.IdentitySchema.URL(r.Context(), s.d.Config())
if err != nil {
return err
}
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
nodes, err := container.NodesFromJSONSchema(r.Context(), node.DefaultGroup, ds.String(), "", nil, s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context()))
if err != nil {
return err
}
for _, n := range nodes {
f.UI.SetNode(n)
}
return nil
}
func (s *Strategy) decode(p *updateRegistrationFlowWithProfileMethod, r *http.Request, ds *url.URL) error {
compiler, err := decoderx.HTTPRawJSONSchemaCompiler(registrationSchema)
if err != nil {
return errors.WithStack(err)
}
if err := decoderx.Decode(r, p, compiler,
decoderx.HTTPKeepRequestBody(true),
decoderx.HTTPDecoderSetValidatePayloads(false),
decoderx.HTTPDecoderJSONFollowsFormFormat(),
); err != nil {
return err
}
if p.Method != "profile" && p.Method != "profile:back" && len(p.Screen) == 0 {
return errors.WithStack(flow.ErrStrategyNotResponsible)
}
return registration.DecodeBody(p, r, registrationSchema, ds)
}
func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, i *identity.Identity) (err error) {
ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.profile.Strategy.Register")
defer otelx.End(span, &err)
if !s.d.Config().SelfServiceFlowRegistrationTwoSteps(ctx) {
return flow.ErrStrategyNotResponsible
}
var params updateRegistrationFlowWithProfileMethod
ds, err := regFlow.IdentitySchema.URL(ctx, s.d.Config())
if err != nil {
return err
}
if err = s.decode(&params, r, ds); err != nil {
return s.handleRegistrationError(r, regFlow, params, err)
}
if params.Method == "profile" || len(params.Screen) > 0 {
switch params.Screen {
case RegistrationScreenCredentialSelection:
return s.showCredentialsSelection(ctx, w, r, regFlow, i, params)
case RegistrationScreenPrevious:
return s.returnToProfileForm(ctx, w, r, regFlow, params)
default:
// FIXME In this scenario we are on the first step of the registration flow and the user clicked on "continue".
// FIXME The appropriate solution would be to also have `screen=credential-selection` available, but that
// FIXME is not the case right now. So instead, we fall back.
span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "profile:missing_screen_parameter"))
return s.showCredentialsSelection(ctx, w, r, regFlow, i, params)
}
} else if params.Method == "profile:back" {
// "profile:back" is kept for backwards compatibility.
// FIXME remove this at some point.
span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "profile:back"))
return s.returnToProfileForm(ctx, w, r, regFlow, params)
}
return flow.ErrStrategyNotResponsible
}
func (s *Strategy) returnToProfileForm(ctx context.Context, w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, params updateRegistrationFlowWithProfileMethod) error {
regFlow.UI.ResetMessages()
regFlow.OrganizationID = uuid.NullUUID{}
regFlow.UI.UpdateNodeValuesFromJSON(params.Traits, "traits", node.DefaultGroup)
for _, ls := range SortForHydration(s.d.RegistrationStrategies(ctx)) {
populator, ok := ls.(registration.FormHydrator)
if !ok {
continue
}
if err := populator.PopulateRegistrationMethodProfile(r, regFlow, registration.WithTraits(params.Traits)); err != nil {
return s.handleRegistrationError(r, regFlow, params, err)
}
}
ds, err := regFlow.IdentitySchema.URL(r.Context(), s.d.Config())
if err != nil {
return s.handleRegistrationError(r, regFlow, params, err)
}
if err := registration.SortNodes(r.Context(), regFlow.UI.Nodes, ds.String()); err != nil {
return s.handleRegistrationError(r, regFlow, params, err)
}
if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil {
return s.handleRegistrationError(r, regFlow, params, err)
}
redirectTo := regFlow.AppendTo(s.d.Config().SelfServiceFlowRegistrationUI(ctx)).String()
if x.IsJSONRequest(r) {
s.d.Writer().WriteCode(w, r, http.StatusBadRequest, regFlow)
} else {
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
}
return flow.ErrCompletedByStrategy
}
func (s *Strategy) showCredentialsSelection(ctx context.Context, w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, i *identity.Identity, params updateRegistrationFlowWithProfileMethod) error {
// Reset state-esque flow fields
regFlow.Active = ""
regFlow.State = "choose_method"
regFlow.UI.ResetMessages()
regFlow.TransientPayload = params.TransientPayload
if err := flow.EnsureCSRF(s.d, r, regFlow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, params.CSRFToken); err != nil {
return s.handleRegistrationError(r, regFlow, params, err)
}
if len(params.Traits) == 0 {
params.Traits = json.RawMessage("{}")
}
i.Traits = identity.Traits(params.Traits)
if err := s.d.IdentityValidator().Validate(ctx, i); err != nil {
return s.handleRegistrationError(r, regFlow, params, err)
}
var didPopulate bool
for _, ls := range SortForHydration(s.d.RegistrationStrategies(ctx)) {
populator, ok := ls.(registration.FormHydrator)
if !ok {
continue
}
if err := populator.PopulateRegistrationMethodCredentials(r, regFlow, registration.WithTraits([]byte(i.Traits))); errors.Is(err, registration.ErrBreakRegistrationPopulate) {
didPopulate = true
break
} else if err != nil {
return s.handleRegistrationError(r, regFlow, params, err)
} else {
didPopulate = true
}
}
// If no strategy populated, it means that the account (very likely) does not exist. We show a user not found error,
// but only if account enumeration mitigation is disabled. Otherwise, we proceed to render the rest of the form.
if !didPopulate && !s.d.Config().SecurityAccountEnumerationMitigate(ctx) {
return s.handleRegistrationError(r, regFlow, params, errors.WithStack(schema.NewNoRegistrationStrategyResponsible()))
}
regFlow.UI.UpdateNodeValuesFromJSON(json.RawMessage(i.Traits), "traits", node.DefaultGroup)
ds, err := regFlow.IdentitySchema.URL(r.Context(), s.d.Config())
if err != nil {
return s.handleRegistrationError(r, regFlow, params, err)
}
if err := registration.SortNodes(r.Context(), regFlow.UI.Nodes, ds.String()); err != nil {
return s.handleRegistrationError(r, regFlow, params, err)
}
if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil {
return s.handleRegistrationError(r, regFlow, params, err)
}
redirectTo := regFlow.AppendTo(s.d.Config().SelfServiceFlowRegistrationUI(ctx)).String()
if x.IsJSONRequest(r) {
s.d.Writer().WriteCode(w, r, http.StatusBadRequest, regFlow)
} else {
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
}
return flow.ErrCompletedByStrategy
}
func (s *Strategy) handleRegistrationError(r *http.Request, regFlow *registration.Flow, params updateRegistrationFlowWithProfileMethod, err error) error {
if regFlow != nil {
for _, n := range container.NewFromJSON("", node.DefaultGroup, params.Traits, "traits").Nodes {
// we only set the value and not the whole field because we want to keep types from the initial form generation
regFlow.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue())
}
regFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r))
}
return err
}

View File

@@ -0,0 +1,355 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package profile_test
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/selfservice/strategy/oidc"
"github.com/ory/kratos/ui/node"
"github.com/ory/x/assertx"
"github.com/ory/x/contextx"
"github.com/ory/x/snapshotx"
)
func TestTwoStepRegistration(t *testing.T) {
ctx := context.Background()
conf, reg := pkg.NewFastRegistryWithMocks(t)
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true)
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeOIDC.String(), true)
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePasskey.String(), true)
conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config", &oidc.ConfigurationCollection{Providers: []oidc.Configuration{
{
ID: "google",
Provider: "google",
ClientID: "1234",
ClientSecret: "1234",
},
}})
conf.MustSet(ctx, config.ViperKeyWebAuthnPasswordless, true)
conf.MustSet(ctx, config.ViperKeyPasskeyRPID, "localhost")
conf.MustSet(ctx, config.ViperKeyPasskeyRPDisplayName, "localhost")
conf.MustSet(ctx, config.ViperKeyWebAuthnRPID, "localhost")
conf.MustSet(ctx, config.ViperKeyWebAuthnRPDisplayName, "localhost")
conf.MustSet(ctx, fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth), true)
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationFlowStyle, "profile_first")
_ = testhelpers.NewErrorTestServer(t, reg)
publicTS, _ := testhelpers.NewKratosServer(t, reg)
_ = testhelpers.NewRedirSessionEchoTS(t, reg)
ui := testhelpers.NewRegistrationUIFlowEchoServer(t, reg)
t.Run("initial form is populated with identity traits", func(t *testing.T) {
t.Run("type=browser", func(t *testing.T) {
client := testhelpers.NewClientWithCookies(t)
t.Run("empty_flow", func(t *testing.T) {
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, false, false, false)
snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths(
"1.attributes.value",
"8.attributes.nonce",
"8.attributes.src",
"10.attributes.value",
))
})
t.Run("case=multi-schema-empty_flow", func(t *testing.T) {
t.Cleanup(func() {
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
})
conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default")
conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{
{ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"},
{ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true},
})
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, false, false, false, testhelpers.InitFlowWithIdentitySchema("not-default"))
snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths(
"1.attributes.value",
"8.attributes.nonce",
"8.attributes.src",
"10.attributes.value",
))
})
t.Run("case=multi-schema-invalid-form-data", func(t *testing.T) {
t.Cleanup(func() {
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
})
conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default")
conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{
{ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"},
{ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true},
})
body := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) {
v.Set("traits.email", "invalidemail")
v.Set("traits.booly", "true")
v.Set("traits.numby", "1")
v.Set("traits.stringy", "string")
v.Set("traits.should_big_number", "1000000")
v.Set("traits.should_long_string", "1111111111111111111111111111111111111111111111111111111111")
v.Set("method", "profile")
}, false, http.StatusOK, ui.URL, testhelpers.InitFlowWithIdentitySchema("not-default"))
fmt.Println(body)
require.Equal(t, int64(4000040), gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.id").Int(), "%s", body)
require.Equal(t, "Enter a valid email address", gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.text").String(), "%s", body)
})
t.Run("select_credentials", func(t *testing.T) {
res := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) {
v.Set("traits.email", "browser-1@example.org")
v.Set("traits.booly", "true")
v.Set("traits.numby", "1")
v.Set("traits.stringy", "string")
v.Set("traits.should_big_number", "1000000")
v.Set("traits.should_long_string", "1111111111111111111111111111111111111111111111111111111111")
v.Set("method", "profile")
}, false, http.StatusOK, ui.URL)
snapshotx.SnapshotT(t, json.RawMessage(gjson.Get(res, "ui.nodes").Raw), snapshotx.ExceptPaths(
"1.attributes.value",
"8.attributes.nonce",
"8.attributes.src",
"11.attributes.value",
))
})
t.Run("return_to_profile", func(t *testing.T) {
res := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) {
v.Set("traits.email", "browser-1-1@example.org")
v.Set("traits.booly", "true")
v.Set("traits.numby", "1")
v.Set("traits.stringy", "string")
v.Set("traits.should_big_number", "1000000")
v.Set("traits.should_long_string", "1111111111111111111111111111111111111111111111111111111111")
v.Set("screen", "previous")
}, false, http.StatusOK, ui.URL)
snapshotx.SnapshotT(t, json.RawMessage(gjson.Get(res, "ui.nodes").Raw), snapshotx.ExceptPaths(
"1.attributes.value",
"8.attributes.nonce",
"8.attributes.src",
"10.attributes.value",
))
})
t.Run("select_credentials_again", func(t *testing.T) {
res := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) {
v.Set("traits.email", "browser-1-1@example.org")
v.Set("method", "profile")
}, false, http.StatusOK, ui.URL)
snapshotx.SnapshotT(t, json.RawMessage(gjson.Get(res, "ui.nodes").Raw), snapshotx.ExceptPaths(
"1.attributes.value",
"8.attributes.nonce",
"8.attributes.src",
"11.attributes.value",
))
})
})
})
}
func TestOneStepRegistration(t *testing.T) {
ctx := context.Background()
conf, reg := pkg.NewFastRegistryWithMocks(t)
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true)
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.com/")
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationFlowStyle, "unified")
_ = testhelpers.NewErrorTestServer(t, reg)
publicTS, _ := testhelpers.NewKratosServer(t, reg)
_ = testhelpers.NewRedirSessionEchoTS(t, reg)
ui := testhelpers.NewRegistrationUIFlowEchoServer(t, reg)
t.Run("initial form is populated with identity traits", func(t *testing.T) {
t.Run("type=browser", func(t *testing.T) {
client := testhelpers.NewClientWithCookies(t)
t.Run("empty_flow", func(t *testing.T) {
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, false, false, false)
snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths(
"0.attributes.value",
))
})
t.Run("case=multi-schema-empty_flow", func(t *testing.T) {
t.Cleanup(func() {
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
})
conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default")
conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{
{ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"},
{ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true},
})
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, false, false, false, testhelpers.InitFlowWithIdentitySchema("not-default"))
snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths(
"0.attributes.value",
))
})
t.Run("case=multi-schema-invalid-form-data", func(t *testing.T) {
t.Cleanup(func() {
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
})
conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default")
conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{
{ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"},
{ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true},
})
body := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) {
v.Set("traits.email", "invalidemail")
v.Set("traits.booly", "true")
v.Set("traits.numby", "1")
v.Set("traits.stringy", "string")
v.Set("traits.should_big_number", "1000000")
v.Set("traits.should_long_string", "1111111111111111111111111111111111111111111111111111111111")
v.Set("password", "password")
v.Set("method", "password")
}, false, http.StatusOK, ui.URL, testhelpers.InitFlowWithIdentitySchema("not-default"))
fmt.Println(body)
require.Equal(t, int64(4000040), gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.id").Int(), "%s", body)
require.Equal(t, "Enter a valid email address", gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.text").String(), "%s", body)
})
})
})
}
func TestPopulateRegistrationMethod(t *testing.T) {
ctx := context.Background()
conf, reg := pkg.NewFastRegistryWithMocks(t)
ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/identity.schema.json")
s, err := reg.AllRegistrationStrategies().Strategy(identity.CredentialsTypeProfile)
require.NoError(t, err)
fh, ok := s.(registration.FormHydrator)
require.True(t, ok)
toSnapshot := func(t *testing.T, f node.Nodes) {
t.Helper()
// The CSRF token has a unique value that messes with the snapshot - ignore it.
f.ResetNodes("csrf_token")
f.ResetNodes("passkey_challenge")
snapshotx.SnapshotT(t, f, snapshotx.ExceptNestedKeys("nonce", "src"))
}
newFlowInternal := func(ctx context.Context, t *testing.T, identitySchema string) (*http.Request, *registration.Flow) {
query := ""
if identitySchema != "" {
query = "?identity_schema=" + identitySchema
}
r := httptest.NewRequest("GET", "/self-service/registration/browser"+query, nil)
r = r.WithContext(ctx)
t.Helper()
f, err := registration.NewFlow(reg, r, flow.TypeBrowser)
f.UI.Nodes = make(node.Nodes, 0)
require.NoError(t, err)
return r, f
}
newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *registration.Flow) {
return newFlowInternal(ctx, t, "")
}
newFlowWithIdentitySchema := func(ctx context.Context, t *testing.T, identitySchema string) (*http.Request, *registration.Flow) {
return newFlowInternal(ctx, t, identitySchema)
}
t.Run("method=PopulateRegistrationMethod", func(t *testing.T) {
r, f := newFlow(ctx, t)
require.NoError(t, fh.PopulateRegistrationMethod(r, f))
toSnapshot(t, f.UI.Nodes)
})
t.Run("method=PopulateRegistrationMethodProfile", func(t *testing.T) {
r, f := newFlow(ctx, t)
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
toSnapshot(t, f.UI.Nodes)
})
t.Run("method=PopulateRegistrationMethodCredentials", func(t *testing.T) {
r, f := newFlow(ctx, t)
require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f))
toSnapshot(t, f.UI.Nodes)
})
t.Run("method=idempotency", func(t *testing.T) {
r, f := newFlow(ctx, t)
var snapshots []node.Nodes
t.Run("case=1", func(t *testing.T) {
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
snapshots = append(snapshots, f.UI.Nodes)
toSnapshot(t, f.UI.Nodes)
})
t.Run("case=2", func(t *testing.T) {
require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f))
snapshots = append(snapshots, f.UI.Nodes)
toSnapshot(t, f.UI.Nodes)
})
t.Run("case=3", func(t *testing.T) {
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
snapshots = append(snapshots, f.UI.Nodes)
toSnapshot(t, f.UI.Nodes)
})
t.Run("case=4", func(t *testing.T) {
require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f))
snapshots = append(snapshots, f.UI.Nodes)
toSnapshot(t, f.UI.Nodes)
})
t.Run("case=evaluate", func(t *testing.T) {
assertx.EqualAsJSON(t, snapshots[0], snapshots[2])
assertx.EqualAsJSON(t, snapshots[1], snapshots[3])
})
})
t.Run("case=multi-schema-method=PopulateRegistrationMethodProfile", func(t *testing.T) {
t.Cleanup(func() {
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
})
multiSchema := contextx.WithConfigValue(ctx, config.ViperKeyDefaultIdentitySchemaID, "default")
multiSchema = contextx.WithConfigValue(multiSchema, config.ViperKeyIdentitySchemas, config.Schemas{
{ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"},
{ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true},
})
r, f := newFlowWithIdentitySchema(multiSchema, t, "not-default")
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
toSnapshot(t, f.UI.Nodes)
})
}

View File

@@ -0,0 +1,11 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package profile
import (
_ "embed"
)
//go:embed .schema/settings.schema.json
var settingsSchema []byte

View File

@@ -0,0 +1,322 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package profile
import (
"context"
"encoding/json"
"net/http"
"github.com/ory/kratos/x/nosurfx"
"github.com/ory/x/httpx"
"github.com/ory/x/logrusx"
"github.com/ory/x/otelx"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/text"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/tidwall/sjson"
"github.com/ory/herodot"
"github.com/ory/kratos/continuity"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/selfservice/errorx"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/session"
"github.com/ory/kratos/ui/container"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
"github.com/ory/x/decoderx"
)
var _ settings.Strategy = new(Strategy)
type (
strategyDependencies interface {
nosurfx.CSRFProvider
nosurfx.CSRFTokenGeneratorProvider
httpx.WriterProvider
logrusx.Provider
otelx.Provider
config.Provider
continuity.ManagementProvider
x.CookieProvider
session.HandlerProvider
session.ManagementProvider
identity.ValidationProvider
identity.ManagementProvider
identity.PrivilegedPoolProvider
errorx.ManagementProvider
settings.HookExecutorProvider
settings.ErrorHandlerProvider
settings.FlowPersistenceProvider
settings.StrategyProvider
settings.HooksProvider
registration.FlowPersistenceProvider
registration.StrategyProvider
schema.IdentitySchemaProvider
}
Strategy struct{ d strategyDependencies }
)
func NewStrategy(d strategyDependencies) *Strategy { return &Strategy{d: d} }
func (s *Strategy) SettingsStrategyID() string {
return settings.StrategyProfile
}
func (s *Strategy) PopulateSettingsMethod(ctx context.Context, r *http.Request, id *identity.Identity, f *settings.Flow) (err error) {
ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.profile.Strategy.PopulateSettingsMethod")
defer otelx.End(span, &err)
schemas, err := s.d.IdentityTraitsSchemas(ctx)
if err != nil {
return err
}
traitsSchema, err := schemas.GetByID(id.SchemaID)
if err != nil {
return err
}
// Use a schema compiler that disables identifiers. NewCompilerWithURL
// pre-registers the top-level schema via the trusted global loader; the
// `security.disallow_ref_in_identity_schemas` flag controls whether
// `$ref` resolution rejects the `file` scheme.
disallowRefs := s.d.Config().SecurityDisallowRefInIdentitySchemas(ctx)
schemaCompiler, err := schema.NewCompilerWithURL(ctx, traitsSchema.URL.String(), disallowRefs)
if err != nil {
return err
}
nodes, err := container.NodesFromJSONSchema(ctx, node.ProfileGroup, traitsSchema.URL.String(), "", schemaCompiler, disallowRefs)
if err != nil {
return err
}
for _, n := range nodes {
f.UI.SetNode(n)
}
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
f.UI.UpdateNodeValuesFromJSON(json.RawMessage(id.Traits), "traits", node.ProfileGroup)
f.UI.Nodes.Append(node.NewInputField("method", "profile", node.ProfileGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSave()))
return nil
}
func (s *Strategy) Settings(ctx context.Context, w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) {
ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.profile.Strategy.Settings")
defer otelx.End(span, &err)
var p updateSettingsFlowWithProfileMethod
ctxUpdate, err := settings.PrepareUpdate(s.d, w, r, f, ss, settings.ContinuityKey(s.SettingsStrategyID()), &p)
if errors.Is(err, settings.ErrContinuePreviousAction) {
return ctxUpdate, s.continueFlow(ctx, r, ctxUpdate, p)
} else if err != nil {
return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, nil, p, err)
}
if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.SettingsStrategyID(), s.d); err != nil {
return ctxUpdate, err
}
option, err := s.newSettingsProfileDecoder(ctx, ctxUpdate.GetSessionIdentity())
if err != nil {
return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, nil, p, err)
}
if err := decoderx.Decode(r, &p, option,
decoderx.HTTPDecoderAllowedMethods("POST", "GET"),
decoderx.HTTPDecoderSetValidatePayloads(true),
decoderx.HTTPDecoderJSONFollowsFormFormat(),
); err != nil {
return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, nil, p, err)
}
// Reset after decoding form
p.SetFlowID(ctxUpdate.Flow.ID)
if err := s.continueFlow(ctx, r, ctxUpdate, p); err != nil {
return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, nil, p, err)
}
return ctxUpdate, nil
}
func (s *Strategy) continueFlow(ctx context.Context, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithProfileMethod) error {
if err := flow.MethodEnabledAndAllowed(ctx, flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil {
return err
}
if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil {
return err
}
if len(p.Traits) == 0 {
return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Did not receive any value changes."))
}
if err := s.hydrateForm(r, ctxUpdate.Flow, p.Traits); err != nil {
return err
}
options := []identity.ManagerOption{identity.ManagerExposeValidationErrorsForInternalTypeAssertion}
if s.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) {
options = append(options, identity.ManagerAllowWriteProtectedTraits)
}
update, err := s.d.IdentityManager().SetTraits(ctx, ctxUpdate.GetSessionIdentity().ID, identity.Traits(p.Traits), options...)
if err != nil {
if errors.Is(err, identity.ErrProtectedFieldModified()) {
return settings.NewFlowNeedsReAuth()
}
return err
}
ctxUpdate.UpdateIdentity(update)
return nil
}
// Update Settings Flow with Profile Method
//
// swagger:model updateSettingsFlowWithProfileMethod
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type updateSettingsFlowWithProfileMethod struct {
// Traits
//
// The identity's traits.
//
// required: true
Traits json.RawMessage `json:"traits"`
// Method
//
// Should be set to profile when trying to update a profile.
//
// required: true
Method string `json:"method"`
// FlowIDRequestID is the flow ID.
//
// swagger:ignore
FlowID string `json:"flow"`
// The Anti-CSRF Token
//
// This token is only required when performing browser flows.
CSRFToken string `json:"csrf_token"`
// Transient data to pass along to any webhooks
//
// required: false
TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
}
func (p *updateSettingsFlowWithProfileMethod) GetFlowID() uuid.UUID {
return x.ParseUUID(p.FlowID)
}
func (p *updateSettingsFlowWithProfileMethod) SetFlowID(rid uuid.UUID) {
p.FlowID = rid.String()
}
func (s *Strategy) hydrateForm(r *http.Request, ar *settings.Flow, traits json.RawMessage) error {
if traits != nil {
ar.UI.Nodes.ResetNodesWithPrefix("traits.")
ar.UI.UpdateNodeValuesFromJSON(traits, "traits", node.ProfileGroup)
}
ar.UI.SetCSRF(s.d.GenerateCSRFToken(r))
return nil
}
// handleSettingsError is a convenience function for handling all types of errors that may occur (e.g. validation error)
// during a settings request.
func (s *Strategy) handleSettingsError(ctx context.Context, w http.ResponseWriter, r *http.Request, puc *settings.UpdateContext, traits json.RawMessage, p updateSettingsFlowWithProfileMethod, err error) error {
if e := new(settings.FlowNeedsReAuth); errors.As(err, &e) {
if _, err := s.d.ContinuityManager().Pause(ctx, w, r,
settings.ContinuityKey(s.SettingsStrategyID()),
continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(ctx)),
settings.ContinuityOptions(p, puc.GetSessionIdentity())...); err != nil {
return err
}
}
if puc.Flow != nil {
if traits == nil {
if len(p.Traits) >= 0 {
traits = p.Traits
} else {
traits = json.RawMessage(puc.GetSessionIdentity().Traits)
}
}
if err := s.hydrateForm(r, puc.Flow, traits); err != nil {
return err
}
}
return err
}
// newSettingsProfileDecoder returns a decoderx.HTTPDecoderOption with a JSON Schema for type assertion and
// validation.
func (s *Strategy) newSettingsProfileDecoder(ctx context.Context, i *identity.Identity) (decoderx.HTTPDecoderOption, error) {
schemas, err := s.d.IdentityTraitsSchemas(ctx)
if err != nil {
return nil, err
}
ss, err := schemas.GetByID(i.SchemaID)
if err != nil {
return nil, err
}
raw, err := sjson.SetBytes(settingsSchema,
"properties.traits.$ref", ss.URL.String()+"#/properties/traits")
if err != nil {
return nil, errors.WithStack(err)
}
o, err := decoderx.HTTPRawJSONSchemaCompiler(raw)
if err != nil {
return nil, errors.WithStack(err)
}
return o, nil
}
func (s *Strategy) NodeGroup() node.UiNodeGroup {
return node.ProfileGroup
}
// SortForHydration sorts the strategies so that the profile strategy is always first.
func SortForHydration(strats registration.Strategies) registration.Strategies {
sorted := make(registration.Strategies, len(strats))
copy(sorted, strats)
for i, strat := range sorted {
if strat.ID() == identity.CredentialsTypeProfile {
sorted = append([]registration.Strategy{strat}, append(sorted[:i], sorted[i+1:]...)...)
break
}
}
return sorted
}

View File

@@ -0,0 +1,684 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package profile_test
import (
"bytes"
"context"
_ "embed"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/ory/kratos/x/nosurfx"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/selfservice/strategy/code"
"github.com/ory/kratos/selfservice/strategy/oidc"
"github.com/ory/kratos/selfservice/strategy/passkey"
"github.com/ory/kratos/selfservice/strategy/password"
"github.com/ory/kratos/selfservice/strategy/webauthn"
"github.com/ory/kratos/selfservice/strategy/profile"
"github.com/ory/x/jsonx"
kratos "github.com/ory/kratos/pkg/httpclient"
"github.com/ory/kratos/corpx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/x"
"github.com/ory/x/assertx"
"github.com/ory/x/httpx"
"github.com/ory/x/ioutilx"
"github.com/ory/x/sqlxx"
)
func init() {
corpx.RegisterFakes()
}
func newIdentityWithPassword(email string) *identity.Identity {
return &identity.Identity{
ID: x.NewUUID(),
Credentials: map[identity.CredentialsType]identity.Credentials{
"password": {Type: "password", Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)},
},
Traits: identity.Traits(`{"email":"` + email + `","stringy":"foobar","booly":false,"numby":2.5,"should_long_string":"asdfasdfasdfasdfasfdasdfasdfasdf","should_big_number":2048}`),
SchemaID: config.DefaultIdentityTraitsSchemaID,
State: identity.StateActive,
VerifiableAddresses: []identity.VerifiableAddress{{Value: email, Via: identity.AddressTypeEmail}},
// TO ADD - RECOVERY EMAIL,
}
}
func TestStrategyTraits(t *testing.T) {
ctx := context.Background()
conf, reg := pkg.NewFastRegistryWithMocks(t)
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.com/")
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true)
testhelpers.StrategyEnable(t, conf, settings.StrategyProfile, true)
setPrivilegedTime := func(t *testing.T, duration time.Duration) {
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, duration.String())
t.Cleanup(func() {
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns")
})
}
setPrivileged := func(t *testing.T) {
setPrivilegedTime(t, time.Minute*10)
}
setUnprivileged := func(t *testing.T) {
setPrivilegedTime(t, time.Nanosecond)
}
ui := testhelpers.NewSettingsUIEchoServer(t, reg)
_ = testhelpers.NewErrorTestServer(t, reg)
publicTS, _ := testhelpers.NewKratosServer(t, reg)
browserIdentity1 := newIdentityWithPassword("john-browser@doe.com")
apiIdentity1 := newIdentityWithPassword("john-api@doe.com")
browserID2 := x.NewUUID()
browserIdentity2 := &identity.Identity{ID: browserID2, Traits: identity.Traits(`{}`), State: identity.StateActive, Credentials: map[identity.CredentialsType]identity.Credentials{
identity.CredentialsTypePassword: {Type: "password", Identifiers: []string{browserID2.String()}, Config: []byte(`{"hashed_password":"$2a$04$zvZz1zV"}`)},
}}
apiID2 := x.NewUUID()
apiIdentity2 := &identity.Identity{ID: apiID2, Traits: identity.Traits(`{}`), State: identity.StateActive, Credentials: map[identity.CredentialsType]identity.Credentials{
identity.CredentialsTypePassword: {Type: "password", Identifiers: []string{apiID2.String()}, Config: []byte(`{"hashed_password":"$2a$04$zvZz1zV"}`)},
}}
browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, browserIdentity1)
browserUser1.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL))
browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, browserIdentity2)
browserUser2.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL))
apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, apiIdentity1)
apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, apiIdentity2)
t.Run("description=not authorized to call endpoints without a session", func(t *testing.T) {
setUnprivileged(t)
t.Run("type=browser", func(t *testing.T) {
res, err := testhelpers.NewTestClient(t).Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(url.Values{"foo": {"bar"}}.Encode()), "application/x-www-form-urlencoded"))
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode, "%+v", res.Request)
assert.Contains(t, res.Request.URL.String(), conf.GetProvider(ctx).String(config.ViperKeySelfServiceLoginUI))
})
t.Run("type=api/spa", func(t *testing.T) {
res, err := testhelpers.NewTestClient(t).Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(`{"foo":"bar"}`), "application/json"))
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode)
})
})
t.Run("description=should fail to post data if CSRF is invalid/type=browser", func(t *testing.T) {
setUnprivileged(t)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS)
actual, res := testhelpers.SettingsMakeRequest(t, false, false, f, browserUser1,
url.Values{"traits.booly": {"true"}, "csrf_token": {"invalid"}, "method": {"profile"}}.Encode())
assert.EqualValues(t, http.StatusOK, res.StatusCode, "should return a 400 error because CSRF token is not set\n\t%s", actual)
assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenServerTokenMismatch(), json.RawMessage(actual), "%s", actual)
})
t.Run("description=should fail to post data if CSRF is invalid/type=spa", func(t *testing.T) {
setUnprivileged(t)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, true, publicTS)
actual, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserUser1,
testhelpers.EncodeFormAsJSON(t, true, url.Values{"traits.booly": {"true"}, "csrf_token": {"invalid"}, "method": {"profile"}}))
assert.EqualValues(t, http.StatusForbidden, res.StatusCode, "should return a 400 error because CSRF token is not set\n\t%s", actual)
assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch(), json.RawMessage(gjson.Get(actual, "error").Raw), "%s", actual)
})
t.Run("description=should not fail because of CSRF token but because of unprivileged/type=api", func(t *testing.T) {
setUnprivileged(t)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiUser1, `{"traits.booly":true,"method":"profile","csrf_token":"`+nosurfx.FakeCSRFToken+`"}`)
assert.EqualValues(t, http.StatusForbidden, res.StatusCode, "should return a 403 error because the session is unprivileged\n\t%s", actual)
require.Len(t, res.Cookies(), 1)
assert.Equal(t, "ory_kratos_continuity", res.Cookies()[0].Name)
assert.Contains(t, gjson.Get(actual, "error.reason").String(), "login session is too old", actual)
})
t.Run("case=should fail with correct CSRF error cause/type=api", func(t *testing.T) {
setPrivileged(t)
for k, tc := range []struct {
mod func(http.Header)
exp string
}{
{
mod: func(h http.Header) {
h.Add("Cookie", "name=bar")
},
exp: "The HTTP Request Header included the \\\"Cookie\\\" key",
},
{
mod: func(h http.Header) {
h.Add("Origin", "www.bar.com")
},
exp: "The HTTP Request Header included the \\\"Origin\\\" key",
},
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
req := testhelpers.NewPostRequest(t, true, f.Ui.Action, bytes.NewBufferString(`{"traits.booly":true,"method":"profile","csrf_token":"invalid"}`))
tc.mod(req.Header)
res, err := apiUser1.Do(req)
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
actual := string(ioutilx.MustReadAll(res.Body))
assert.EqualValues(t, http.StatusBadRequest, res.StatusCode)
assert.Contains(t, actual, tc.exp, "%s", actual)
})
}
})
t.Run("description=hydrate the proper fields", func(t *testing.T) {
setPrivileged(t)
run := func(t *testing.T, id *identity.Identity, payload *kratos.SettingsFlow, route string) {
assert.NotEmpty(t, payload.Identity)
assert.Equal(t, id.ID.String(), string(payload.Identity.Id))
assert.JSONEq(t, string(id.Traits), x.MustEncodeJSON(t, payload.Identity.Traits))
assert.Equal(t, id.SchemaID, payload.Identity.SchemaId)
assert.Equal(t, publicTS.URL+route, payload.RequestUrl)
actual := jsonx.TestMarshalJSONString(t, payload.Ui)
assert.EqualValues(t, payload.Identity.Traits.(map[string]interface{})["email"], gjson.Get(actual, "nodes.#(attributes.name==traits.email).attributes.value").String())
assert.NotEmpty(t, gjson.Get(actual, "nodes.#(attributes.name==csrf_token).attributes.value").String(), "csrf token missing")
testhelpers.SnapshotTExcept(t, payload.Ui, []string{"action", "nodes.0.attributes.value", "nodes.1.attributes.value"})
}
t.Run("type=api", func(t *testing.T) {
pr, _, err := testhelpers.NewSDKCustomClient(publicTS, apiUser1).FrontendAPI.CreateNativeSettingsFlow(context.Background()).Execute()
require.NoError(t, err)
run(t, apiIdentity1, pr, settings.RouteInitAPIFlow)
})
t.Run("type=spa", func(t *testing.T) {
pr, _, err := testhelpers.NewSDKCustomClient(publicTS, browserUser1).FrontendAPI.CreateBrowserSettingsFlow(context.Background()).Execute()
require.NoError(t, err)
run(t, browserIdentity1, pr, settings.RouteInitBrowserFlow)
})
t.Run("type=browser", func(t *testing.T) {
res, err := browserUser1.Get(publicTS.URL + settings.RouteInitBrowserFlow)
require.NoError(t, err)
assert.Contains(t, res.Request.URL.String(), ui.URL+"/settings?flow")
rid := res.Request.URL.Query().Get("flow")
require.NotEmpty(t, rid)
pr, _, err := testhelpers.NewSDKCustomClient(publicTS, browserUser1).FrontendAPI.GetSettingsFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute()
require.NoError(t, err, "%s", rid)
run(t, browserIdentity1, pr, settings.RouteInitBrowserFlow)
})
})
expectValidationError := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
return testhelpers.SubmitSettingsForm(t, isAPI, isSPA, hc, publicTS, values,
testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK),
testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+settings.RouteSubmitFlow, conf.SelfServiceFlowSettingsUI(ctx).String()))
}
t.Run("description=should come back with form errors if some profile data is invalid", func(t *testing.T) {
setPrivileged(t)
check := func(t *testing.T, actual string) {
assert.NotEmpty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", actual)
assert.Equal(t, "too-short", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.value").String(), "%s", actual)
assert.Equal(t, "bazbar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual)
assert.Equal(t, "2.5", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.value").String(), "%s", actual)
assert.Equal(t, "length must be >= 25, but got 9", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).messages.0.text").String(), "%s", actual)
}
payload := func(v url.Values) {
v.Set("method", "profile")
v.Set("traits.should_long_string", "too-short")
v.Set("traits.stringy", "bazbar")
}
t.Run("type=api", func(t *testing.T) {
check(t, expectValidationError(t, true, false, apiUser1, payload))
})
t.Run("type=spa", func(t *testing.T) {
check(t, expectValidationError(t, false, true, browserUser1, payload))
})
t.Run("type=browser", func(t *testing.T) {
check(t, expectValidationError(t, false, false, browserUser1, payload))
})
})
t.Run("description=should not be able to make requests for another user", func(t *testing.T) {
setUnprivileged(t)
t.Run("type=api", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiUser2, testhelpers.EncodeFormAsJSON(t, true, values))
assert.Equal(t, http.StatusForbidden, res.StatusCode)
assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual)
})
t.Run("type=spa", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaAPI(t, browserUser1, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
actual, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserUser2, testhelpers.EncodeFormAsJSON(t, true, values))
assert.Equal(t, http.StatusForbidden, res.StatusCode)
assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual)
})
t.Run("type=browser", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
actual, res := testhelpers.SettingsMakeRequest(t, false, false, f, browserUser2, values.Encode())
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Contains(t, gjson.Get(actual, "reason").String(), "initiated by someone else", "%s", actual)
})
})
t.Run("description=should end up at the login endpoint if trying to update protected field without sudo mode", func(t *testing.T) {
run := func(t *testing.T, config *kratos.SettingsFlow, isAPI bool, c *http.Client) *http.Response {
time.Sleep(time.Millisecond)
values := testhelpers.SDKFormFieldsToURLValues(config.Ui.Nodes)
values.Set("method", "profile")
values.Set("traits.email", "not-john-doe@foo.bar")
res, err := c.PostForm(config.Ui.Action, values)
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
return res
}
t.Run("type=api", func(t *testing.T) {
setUnprivileged(t)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
res := run(t, f, true, apiUser1)
assert.EqualValues(t, http.StatusForbidden, res.StatusCode)
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
})
t.Run("type=sqa", func(t *testing.T) {
setUnprivileged(t)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, true, publicTS)
res := run(t, f, true, browserUser1)
assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode)
assert.Contains(t, res.Request.URL.String(), conf.GetProvider(ctx).String(config.ViperKeySelfServiceLoginUI))
})
t.Run("type=browser", func(t *testing.T) {
setUnprivileged(t)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS)
res := run(t, f, false, browserUser1)
assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode)
assert.Contains(t, res.Request.URL.String(), conf.GetProvider(ctx).String(config.ViperKeySelfServiceLoginUI))
t.Run("should update when signed back in", func(t *testing.T) {
setPrivileged(t)
res, err := browserUser1.Get(f.Ui.Action)
require.NoError(t, err)
body := ioutilx.MustReadAll(res.Body)
defer func() { _ = res.Body.Close() }()
assert.EqualValues(t, http.StatusOK, res.StatusCode, "%s", body)
assert.EqualValues(t, flow.StateSuccess, gjson.GetBytes(body, "state").String(), "%s", body)
})
})
})
t.Run("flow=fail first update", func(t *testing.T) {
setPrivileged(t)
check := func(t *testing.T, actual string) {
assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual)
assert.Equal(t, "1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.value").String(), "%s", actual)
assert.Equal(t, "must be >= 1200 but found 1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).messages.0.text").String(), "%s", actual)
assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here
}
payload := func(v url.Values) {
v.Set("method", settings.StrategyProfile)
v.Set("traits.should_big_number", "1")
}
t.Run("type=api", func(t *testing.T) {
check(t, expectValidationError(t, true, false, apiUser1, payload))
})
t.Run("type=sqa", func(t *testing.T) {
check(t, expectValidationError(t, false, true, browserUser1, payload))
})
t.Run("type=browser", func(t *testing.T) {
check(t, expectValidationError(t, false, false, browserUser1, payload))
})
})
t.Run("flow=fail second update", func(t *testing.T) {
setPrivileged(t)
check := func(t *testing.T, actual string) {
assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).messages.0.text").String(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.value").String(), "%s", actual)
assert.Equal(t, "short", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.value").String(), "%s", actual)
assert.Equal(t, "length must be >= 25, but got 5", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).messages.0.text").String(), "%s", actual)
assert.Equal(t, "this-is-not-a-number", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.value").String(), "%s", actual)
assert.Equal(t, "expected number, but got string", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).messages.0.text").String(), "%s", actual)
assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here
}
payload := func(v url.Values) {
v.Set("method", settings.StrategyProfile)
v.Del("traits.should_big_number")
v.Set("traits.should_long_string", "short")
v.Set("traits.numby", "this-is-not-a-number")
}
t.Run("type=api", func(t *testing.T) {
check(t, expectValidationError(t, true, false, apiUser1, payload))
})
t.Run("type=spa", func(t *testing.T) {
check(t, expectValidationError(t, false, true, browserUser1, payload))
})
t.Run("type=browser", func(t *testing.T) {
check(t, expectValidationError(t, false, false, browserUser1, payload))
})
})
expectSuccess := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
return testhelpers.SubmitSettingsForm(t, isAPI, isSPA, hc, publicTS, values,
http.StatusOK,
testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+settings.RouteSubmitFlow, conf.SelfServiceFlowSettingsUI(ctx).String()))
}
t.Run("flow=succeed with final request", func(t *testing.T) {
setPrivileged(t)
check := func(t *testing.T, actual string) {
assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.errors").Value(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.errors").Value(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.errors").Value(), "%s", actual)
assert.Equal(t, 15.0, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.value").Value(), "%s", actual)
assert.Equal(t, 9001.0, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.value").Value(), "%s", actual)
assert.Equal(t, "this is such a long string, amazing stuff!", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.value").Value(), "%s", actual)
}
payload := func(newEmail string) func(v url.Values) {
return func(v url.Values) {
v.Set("method", settings.StrategyProfile)
v.Set("traits.email", newEmail)
v.Set("traits.numby", "15")
v.Set("traits.should_big_number", "9001")
v.Set("traits.should_long_string", "this is such a long string, amazing stuff!")
}
}
t.Run("type=api", func(t *testing.T) {
actual := expectSuccess(t, true, false, apiUser1, payload("not-john-doe-api@mail.com"))
check(t, actual)
assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
t.Run("type=sqa", func(t *testing.T) {
actual := expectSuccess(t, false, true, browserUser1, payload("not-john-doe-browser@mail.com"))
check(t, actual)
assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), ui.URL, "%s", actual)
})
t.Run("type=browser", func(t *testing.T) {
actual := expectSuccess(t, false, false, browserUser1, payload("not-john-doe-browser@mail.com"))
check(t, actual)
assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
})
t.Run("flow=try another update with invalid data", func(t *testing.T) {
setPrivileged(t)
check := func(t *testing.T, actual string) {
assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual)
}
payload := func(v url.Values) {
v.Set("method", settings.StrategyProfile)
v.Set("traits.should_long_string", "short")
}
t.Run("type=api", func(t *testing.T) {
check(t, expectValidationError(t, true, false, apiUser1, payload))
})
t.Run("type=sqa", func(t *testing.T) {
check(t, expectValidationError(t, false, true, browserUser1, payload))
})
t.Run("type=browser", func(t *testing.T) {
check(t, expectValidationError(t, false, false, browserUser1, payload))
})
})
t.Run("description=ensure that hooks are running", func(t *testing.T) {
setPrivileged(t)
var returned bool
router := http.NewServeMux()
router.HandleFunc("GET /return-ts", func(w http.ResponseWriter, r *http.Request) {
returned = true
})
rts := httptest.NewServer(router)
t.Cleanup(rts.Close)
testhelpers.SelfServiceHookSettingsSetDefaultRedirectTo(t, conf, rts.URL+"/return-ts")
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", settings.StrategyProfile)
values.Set("traits.should_big_number", "9001")
res, err := browserUser1.PostForm(f.Ui.Action, values)
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
assert.True(t, returned, "%d - %s", res.StatusCode, body)
})
// Update the login endpoint to auto-accept any incoming login request!
_ = testhelpers.NewSettingsLoginAcceptAPIServer(t, testhelpers.NewSDKCustomClient(publicTS, browserUser1), conf)
t.Run("description=should send email with verifiable address", func(t *testing.T) {
setPrivileged(t)
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true)
conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo:bar@irrelevant.com/")
t.Cleanup(func() {
conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceSettingsAfter, settings.StrategyProfile), nil)
})
check := func(t *testing.T, actual, newEmail string) {
assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual)
assert.Equal(t, newEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.email).attributes.value").Value(), "%s", actual)
m, err := reg.CourierPersister().LatestQueuedMessage(context.Background())
require.NoError(t, err)
assert.Contains(t, m.Subject, "Use code")
}
payload := func(newEmail string) func(v url.Values) {
return func(v url.Values) {
v.Set("method", settings.StrategyProfile)
v.Set("traits.email", newEmail)
}
}
t.Run("type=api", func(t *testing.T) {
newEmail := "update-verify-api@mail.com"
actual := expectSuccess(t, true, false, apiUser1, payload(newEmail))
check(t, actual, newEmail)
})
t.Run("type=spa", func(t *testing.T) {
newEmail := "update-verify-browser@mail.com"
actual := expectSuccess(t, false, true, browserUser1, payload(newEmail))
check(t, actual, newEmail)
})
t.Run("type=browser", func(t *testing.T) {
newEmail := "update-verify-browser@mail.com"
actual := expectSuccess(t, false, false, browserUser1, payload(newEmail))
check(t, actual, newEmail)
})
})
t.Run("description=should update protected field with sudo mode", func(t *testing.T) {
setPrivileged(t)
check := func(t *testing.T, newEmail string, actual string) {
assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.errors").Value(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.errors").Value(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.errors").Value(), "%s", actual)
assert.Equal(t, newEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.email).attributes.value").Value(), "%s", actual)
assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here
}
payload := func(email string) func(v url.Values) {
return func(v url.Values) {
v.Set("method", settings.StrategyProfile)
v.Set("traits.email", email)
}
}
t.Run("type=api", func(t *testing.T) {
email := "not-john-doe-api@mail.com"
actual := expectSuccess(t, true, false, apiUser1, payload(email))
check(t, email, actual)
})
t.Run("type=sqa", func(t *testing.T) {
email := "not-john-doe-browser@mail.com"
actual := expectSuccess(t, false, true, browserUser1, payload(email))
check(t, email, actual)
})
t.Run("type=browser", func(t *testing.T) {
email := "not-john-doe-browser@mail.com"
actual := expectSuccess(t, false, false, browserUser1, payload(email))
check(t, email, actual)
})
})
}
func TestDisabledEndpoint(t *testing.T) {
conf, reg := pkg.NewFastRegistryWithMocks(t)
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
testhelpers.StrategyEnable(t, conf, settings.StrategyProfile, false)
errTS := testhelpers.NewErrorTestServer(t, reg)
publicTS, _ := testhelpers.NewKratosServer(t, reg)
browserIdentity1 := newIdentityWithPassword("john-browser@doe.com")
browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(context.Background(), t, reg, browserIdentity1)
t.Run("case=should not submit when profile method is disabled", func(t *testing.T) {
t.Run("method=GET", func(t *testing.T) {
t.Skip("GET is not supported here")
})
t.Run("method=POST", func(t *testing.T) {
b := testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, func(v url.Values) {
v.Set("method", settings.StrategyProfile)
},
http.StatusOK,
testhelpers.ExpectURL(false, publicTS.URL+settings.RouteSubmitFlow, errTS.URL))
assert.Contains(t, string(b), "404")
assert.Contains(t, string(b), "This endpoint was disabled by system administrator")
})
})
}
func TestSortedForHydration(t *testing.T) {
_, reg := pkg.NewFastRegistryWithMocks(t)
// Get a reference to all registration strategies
allStrategies := []registration.Strategy{
password.NewStrategy(reg),
code.NewStrategy(reg),
oidc.NewStrategy(reg),
code.NewStrategy(reg),
passkey.NewStrategy(reg),
passkey.NewStrategy(reg),
profile.NewStrategy(reg),
webauthn.NewStrategy(reg),
}
var originalOrder []string
for _, s := range allStrategies {
if s.ID().String() == "profile" {
continue
}
originalOrder = append(originalOrder, s.ID().String())
}
var actual []string
for _, s := range profile.SortForHydration(allStrategies) {
actual = append(actual, s.ID().String())
}
assert.EqualValues(t, append([]string{"profile"}, originalOrder...), actual)
}

View File

@@ -0,0 +1,43 @@
{
"$id": "https://example.com/registration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"verification": {
"via": "email"
}
}
},
"stringy": {
"type": "string"
},
"numby": {
"type": "number"
},
"booly": {
"type": "boolean"
},
"should_big_number": {
"type": "number",
"minimum": 1200
},
"should_long_string": {
"type": "string",
"minLength": 25
}
}
}
}
}