snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
24
selfservice/strategy/profile/.schema/settings.schema.json
Normal file
24
selfservice/strategy/profile/.schema/settings.schema.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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": {}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
27
selfservice/strategy/profile/nodes.go
Normal file
27
selfservice/strategy/profile/nodes.go
Normal 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())
|
||||
}
|
||||
341
selfservice/strategy/profile/registration.go
Normal file
341
selfservice/strategy/profile/registration.go
Normal 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(¶ms, 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
|
||||
}
|
||||
355
selfservice/strategy/profile/registration_test.go
Normal file
355
selfservice/strategy/profile/registration_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
11
selfservice/strategy/profile/schema.go
Normal file
11
selfservice/strategy/profile/schema.go
Normal 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
|
||||
322
selfservice/strategy/profile/strategy.go
Normal file
322
selfservice/strategy/profile/strategy.go
Normal 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
|
||||
}
|
||||
684
selfservice/strategy/profile/strategy_test.go
Normal file
684
selfservice/strategy/profile/strategy_test.go
Normal 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)
|
||||
}
|
||||
43
selfservice/strategy/profile/stub/identity.schema.json
Normal file
43
selfservice/strategy/profile/stub/identity.schema.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user