From 1bc4dc5b24c72e26c2b1022edee0312a357bea8d Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Tue, 11 Jun 2024 13:12:53 +0200 Subject: [PATCH 01/71] chore: move b2b config to selfservice section (#3949) --- embedx/config.schema.json | 661 ++++++++++++++++++++++++++++++-------- internal/client-go/go.sum | 1 + 2 files changed, 527 insertions(+), 135 deletions(-) diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 5349164b6f9b..5ebbdb1241ee 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -43,7 +43,10 @@ "description": "Ory Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/dashboard", "/dashboard"] + "examples": [ + "https://my-app.com/dashboard", + "/dashboard" + ] }, "selfServiceSessionRevokerHook": { "type": "object", @@ -53,7 +56,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "selfServiceSessionIssuerHook": { "type": "object", @@ -63,7 +68,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "selfServiceRequireVerifiedAddressHook": { "type": "object", @@ -73,7 +80,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "selfServiceVerificationHook": { "type": "object", @@ -83,7 +92,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "selfServiceShowVerificationUIHook": { "type": "object", @@ -93,7 +104,9 @@ } }, "additionalProperties": false, - "required": ["hook"] + "required": [ + "hook" + ] }, "b2bSSOHook": { "type": "object", @@ -107,7 +120,10 @@ } }, "additionalProperties": false, - "required": ["hook", "config"] + "required": [ + "hook", + "config" + ] }, "webHookAuthBasicAuthProperties": { "properties": { @@ -127,11 +143,17 @@ } }, "additionalProperties": false, - "required": ["user", "password"] + "required": [ + "user", + "password" + ] } }, "additionalProperties": false, - "required": ["type", "config"] + "required": [ + "type", + "config" + ] }, "httpRequestConfig": { "type": "object", @@ -139,7 +161,9 @@ "url": { "title": "HTTP address of API endpoint", "description": "This URL will be used to send the emails to.", - "examples": ["https://example.com/api/v1/email"], + "examples": [ + "https://example.com/api/v1/email" + ], "type": "string", "pattern": "^https?://" }, @@ -204,15 +228,25 @@ "in": { "type": "string", "description": "How the api key should be transferred", - "enum": ["header", "cookie"] + "enum": [ + "header", + "cookie" + ] } }, "additionalProperties": false, - "required": ["name", "value", "in"] + "required": [ + "name", + "value", + "in" + ] } }, "additionalProperties": false, - "required": ["type", "config"] + "required": [ + "type", + "config" + ] }, "selfServiceWebHook": { "type": "object", @@ -251,7 +285,10 @@ "const": true } }, - "required": ["ignore", "parse"] + "required": [ + "ignore", + "parse" + ] } }, "url": { @@ -324,30 +361,46 @@ "response": { "properties": { "ignore": { - "enum": [true] + "enum": [ + true + ] } }, - "required": ["ignore"] + "required": [ + "ignore" + ] } }, - "required": ["response"] + "required": [ + "response" + ] } }, { "properties": { "can_interrupt": { - "enum": [false] + "enum": [ + false + ] } }, - "require": ["can_interrupt"] + "require": [ + "can_interrupt" + ] } ], "additionalProperties": false, - "required": ["url", "method"] + "required": [ + "url", + "method" + ] } }, "additionalProperties": false, - "required": ["hook", "config"] + "required": [ + "hook", + "config" + ] }, "OIDCClaims": { "title": "OpenID Connect claims", @@ -380,7 +433,9 @@ "essential": true }, "acr": { - "values": ["urn:mace:incommon:iap:silver"] + "values": [ + "urn:mace:incommon:iap:silver" + ] } } } @@ -428,7 +483,9 @@ "properties": { "id": { "type": "string", - "examples": ["google"] + "examples": [ + "google" + ] }, "provider": { "title": "Provider", @@ -457,7 +514,9 @@ "lark", "x" ], - "examples": ["google"] + "examples": [ + "google" + ] }, "label": { "title": "Optional string which will be used when generating labels for UI buttons.", @@ -472,17 +531,23 @@ "issuer_url": { "type": "string", "format": "uri", - "examples": ["https://accounts.google.com"] + "examples": [ + "https://accounts.google.com" + ] }, "auth_url": { "type": "string", "format": "uri", - "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] + "examples": [ + "https://accounts.google.com/o/oauth2/v2/auth" + ] }, "token_url": { "type": "string", "format": "uri", - "examples": ["https://www.googleapis.com/oauth2/v4/token"] + "examples": [ + "https://www.googleapis.com/oauth2/v4/token" + ] }, "mapper_url": { "title": "Jsonnet Mapper URL", @@ -499,7 +564,10 @@ "type": "array", "items": { "type": "string", - "examples": ["offline_access", "profile"] + "examples": [ + "offline_access", + "profile" + ] } }, "microsoft_tenant": { @@ -518,21 +586,30 @@ "title": "Microsoft subject source", "description": "Controls which source the subject identifier is taken from by microsoft provider. If set to `userinfo` (the default) then the identifier is taken from the `sub` field of OIDC ID token or data received from `/userinfo` standard OIDC endpoint. If set to `me` then the `id` field of data structure received from `https://graph.microsoft.com/v1.0/me` is taken as an identifier.", "type": "string", - "enum": ["userinfo", "me"], + "enum": [ + "userinfo", + "me" + ], "default": "userinfo", - "examples": ["userinfo"] + "examples": [ + "userinfo" + ] }, "apple_team_id": { "title": "Apple Developer Team ID", "description": "Apple Developer Team ID needed for generating a JWT token for client secret", "type": "string", - "examples": ["KP76DQS54M"] + "examples": [ + "KP76DQS54M" + ] }, "apple_private_key_id": { "title": "Apple Private Key Identifier", "description": "Sign In with Apple Private Key Identifier needed for generating a JWT token for client secret", "type": "string", - "examples": ["UX56C66723"] + "examples": [ + "UX56C66723" + ] }, "apple_private_key": { "title": "Apple Private Key", @@ -549,27 +626,42 @@ "title": "Organization ID", "description": "The ID of the organization that this provider belongs to. Only effective in the Ory Network.", "type": "string", - "examples": ["12345678-1234-1234-1234-123456789012"] + "examples": [ + "12345678-1234-1234-1234-123456789012" + ] }, "additional_id_token_audiences": { "title": "Additional client ids allowed when using ID token submission", "type": "array", "items": { "type": "string", - "examples": ["12345678-1234-1234-1234-123456789012"] + "examples": [ + "12345678-1234-1234-1234-123456789012" + ] } }, "claims_source": { "title": "Claims source", "description": "Can be either `userinfo` (calls the userinfo endpoint to get the claims) or `id_token` (takes the claims from the id token). It defaults to `id_token`", "type": "string", - "enum": ["id_token", "userinfo"], + "enum": [ + "id_token", + "userinfo" + ], "default": "id_token", - "examples": ["id_token", "userinfo"] + "examples": [ + "id_token", + "userinfo" + ] } }, "additionalProperties": false, - "required": ["id", "provider", "client_id", "mapper_url"], + "required": [ + "id", + "provider", + "client_id", + "mapper_url" + ], "allOf": [ { "if": { @@ -578,17 +670,23 @@ "const": "microsoft" } }, - "required": ["provider"] + "required": [ + "provider" + ] }, "then": { - "required": ["microsoft_tenant"] + "required": [ + "microsoft_tenant" + ] }, "else": { "not": { "properties": { "microsoft_tenant": {} }, - "required": ["microsoft_tenant"] + "required": [ + "microsoft_tenant" + ] } } }, @@ -599,7 +697,9 @@ "const": "apple" } }, - "required": ["provider"] + "required": [ + "provider" + ] }, "then": { "not": { @@ -609,7 +709,9 @@ "minLength": 1 } }, - "required": ["client_secret"] + "required": [ + "client_secret" + ] }, "required": [ "apple_private_key_id", @@ -618,7 +720,9 @@ ] }, "else": { - "required": ["client_secret"], + "required": [ + "client_secret" + ], "allOf": [ { "not": { @@ -628,7 +732,9 @@ "minLength": 1 } }, - "required": ["apple_team_id"] + "required": [ + "apple_team_id" + ] } }, { @@ -639,7 +745,9 @@ "minLength": 1 } }, - "required": ["apple_private_key_id"] + "required": [ + "apple_private_key_id" + ] } }, { @@ -650,7 +758,9 @@ "minLength": 1 } }, - "required": ["apple_private_key"] + "required": [ + "apple_private_key" + ] } } ] @@ -830,7 +940,10 @@ "title": "Required Authenticator Assurance Level", "description": "Sets what Authenticator Assurance Level (used for 2FA) is required to access this feature. If set to `highest_available` then this endpoint requires the highest AAL the identity has set up. If set to `aal1` then the identity can access this feature without 2FA.", "type": "string", - "enum": ["aal1", "highest_available"], + "enum": [ + "aal1", + "highest_available" + ], "default": "highest_available" }, "selfServiceAfterSettings": { @@ -1026,7 +1139,9 @@ "path": { "title": "Path to PEM-encoded Fle", "type": "string", - "examples": ["path/to/file.pem"] + "examples": [ + "path/to/file.pem" + ] }, "base64": { "title": "Base64 Encoded Inline", @@ -1074,7 +1189,9 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": ["email"] + "required": [ + "email" + ] }, "valid": { "additionalProperties": false, @@ -1087,7 +1204,9 @@ "$ref": "#/definitions/smsCourierTemplate" } }, - "required": ["email"] + "required": [ + "email" + ] } } }, @@ -1158,7 +1277,9 @@ "selfservice": { "type": "object", "additionalProperties": false, - "required": ["default_browser_return_url"], + "required": [ + "default_browser_return_url" + ], "properties": { "default_browser_return_url": { "$ref": "#/definitions/defaultReturnTo" @@ -1193,20 +1314,30 @@ "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/user/settings"], + "examples": [ + "https://my-app.com/user/settings" + ], "default": "https://www.ory.sh/kratos/docs/fallback/settings" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] }, "privileged_session_max_age": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] }, "required_aal": { "$ref": "#/definitions/featureRequiredAal" @@ -1255,14 +1386,20 @@ "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/signup"], + "examples": [ + "https://my-app.com/signup" + ], "default": "https://www.ory.sh/kratos/docs/fallback/registration" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] }, "before": { "$ref": "#/definitions/selfServiceBeforeRegistration" @@ -1287,14 +1424,30 @@ "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/login"], + "examples": [ + "https://my-app.com/login" + ], "default": "https://www.ory.sh/kratos/docs/fallback/login" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] + }, + "style": { + "title": "Login Flow Style", + "description": "The style of the login flow. If set to `one_step` the login flow will be a one-step process. If set to `identifier_first` (experimental!) the login flow will first ask for the identifier and then the credentials.", + "type": "string", + "enum": [ + "one_step", + "identifier_first" + ], + "default": "one_step" }, "before": { "$ref": "#/definitions/selfServiceBeforeLogin" @@ -1320,7 +1473,9 @@ "description": "URL where the Ory Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/verify"], + "examples": [ + "https://my-app.com/verify" + ], "default": "https://www.ory.sh/kratos/docs/fallback/verification" }, "after": { @@ -1332,7 +1487,11 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] }, "before": { "$ref": "#/definitions/selfServiceBeforeVerification" @@ -1341,7 +1500,10 @@ "title": "Verification Strategy", "description": "The strategy to use for verification requests", "type": "string", - "enum": ["link", "code"], + "enum": [ + "link", + "code" + ], "default": "code" }, "notify_unknown_recipients": { @@ -1368,7 +1530,9 @@ "description": "URL where the Ory Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/verify"], + "examples": [ + "https://my-app.com/verify" + ], "default": "https://www.ory.sh/kratos/docs/fallback/recovery" }, "after": { @@ -1380,7 +1544,11 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] }, "before": { "$ref": "#/definitions/selfServiceBeforeRecovery" @@ -1389,7 +1557,10 @@ "title": "Recovery Strategy", "description": "The strategy to use for recovery requests", "type": "string", - "enum": ["link", "code"], + "enum": [ + "link", + "code" + ], "default": "code" }, "notify_unknown_recipients": { @@ -1409,7 +1580,9 @@ "description": "URL where the Ory Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": ["https://my-app.com/kratos-error"], + "examples": [ + "https://my-app.com/kratos-error" + ], "default": "https://www.ory.sh/kratos/docs/fallback/error" } } @@ -1420,6 +1593,54 @@ "type": "object", "additionalProperties": false, "properties": { + "b2b": { + "title": "Single Sign-On for B2B", + "description": "Single Sign-On for B2B allows your customers to bring their own (workforce) identity server (e.g. OneLogin). This feature is not available in the open source licensed code.", + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "organizations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the organization.", + "format": "uuid", + "examples": [ + "00000000-0000-0000-0000-000000000000" + ] + }, + "label": { + "type": "string", + "description": "The label of the organization.", + "examples": [ + "ACME SSO" + ] + }, + "domains": { + "type": "array", + "items": { + "type": "string", + "format": "hostname", + "examples": [ + "my-app.com" + ], + "description": "If this domain matches the email's domain, this provider is shown." + } + } + } + } + } + } + } + }, + "additionalProperties": false + }, "profile": { "type": "object", "additionalProperties": false, @@ -1448,14 +1669,20 @@ "base_url": { "title": "Override the base URL which should be used as the base for recovery and verification links.", "type": "string", - "examples": ["https://my-app.com"] + "examples": [ + "https://my-app.com" + ] }, "lifespan": { "title": "How long a link is valid for", "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] } } } @@ -1463,24 +1690,36 @@ }, "code": { "type": "object", - "additionalProperties": false, + "additionalProperties": true, "anyOf": [ { "properties": { - "passwordless_enabled": { "const": true }, - "mfa_enabled": { "const": false } + "passwordless_enabled": { + "const": true + }, + "mfa_enabled": { + "const": false + } } }, { "properties": { - "mfa_enabled": { "const": true }, - "passwordless_enabled": { "const": false } + "mfa_enabled": { + "const": true + }, + "passwordless_enabled": { + "const": false + } } }, { "properties": { - "mfa_enabled": { "const": false }, - "passwordless_enabled": { "const": false } + "mfa_enabled": { + "const": false + }, + "passwordless_enabled": { + "const": false + } } } ], @@ -1517,7 +1756,11 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] } } } @@ -1640,13 +1883,17 @@ "type": "string", "title": "Relying Party Display Name", "description": "An name to help the user identify this RP.", - "examples": ["Ory Foundation"] + "examples": [ + "Ory Foundation" + ] }, "id": { "type": "string", "title": "Relying Party Identifier", "description": "The id must be a subset of the domain currently in the browser.", - "examples": ["ory.sh"] + "examples": [ + "ory.sh" + ] }, "origin": { "type": "string", @@ -1654,7 +1901,9 @@ "description": "An explicit RP origin. If left empty, this defaults to `id`, prepended with the current protocol schema (HTTP or HTTPS).", "format": "uri", "deprecationMessage": "This field is deprecated. Use `origins` instead.", - "examples": ["https://www.ory.sh"] + "examples": [ + "https://www.ory.sh" + ] }, "origins": { "type": "array", @@ -1675,13 +1924,18 @@ "description": "An icon to help the user identify this RP.", "format": "uri", "deprecationMessage": "This field is deprecated and ignored due to security considerations.", - "examples": ["https://www.ory.sh/an-icon.png"] + "examples": [ + "https://www.ory.sh/an-icon.png" + ] } }, "type": "object", "oneOf": [ { - "required": ["id", "display_name"], + "required": [ + "id", + "display_name" + ], "properties": { "origin": { "not": {} @@ -1692,7 +1946,11 @@ } }, { - "required": ["id", "display_name", "origin"], + "required": [ + "id", + "display_name", + "origin" + ], "properties": { "origin": { "type": "string" @@ -1703,7 +1961,11 @@ } }, { - "required": ["id", "display_name", "origins"], + "required": [ + "id", + "display_name", + "origins" + ], "properties": { "origin": { "not": {} @@ -1728,10 +1990,14 @@ "const": true } }, - "required": ["enabled"] + "required": [ + "enabled" + ] }, "then": { - "required": ["config"] + "required": [ + "config" + ] } }, "passkey": { @@ -1754,13 +2020,17 @@ "type": "string", "title": "Relying Party Display Name", "description": "A name to help the user identify this RP.", - "examples": ["Ory Foundation"] + "examples": [ + "Ory Foundation" + ] }, "id": { "type": "string", "title": "Relying Party Identifier", "description": "The id must be a subset of the domain currently in the browser.", - "examples": ["ory.sh"] + "examples": [ + "ory.sh" + ] }, "origins": { "type": "array", @@ -1777,7 +2047,10 @@ } }, "type": "object", - "required": ["display_name", "id"] + "required": [ + "display_name", + "id" + ] } }, "additionalProperties": false @@ -1789,10 +2062,14 @@ "const": true } }, - "required": ["enabled"] + "required": [ + "enabled" + ] }, "then": { - "required": ["config"] + "required": [ + "config" + ] } }, "oidc": { @@ -1815,7 +2092,9 @@ "title": "Base URL for OAuth2 Redirect URIs", "description": "Can be used to modify the base URL for OAuth2 Redirect URLs. If unset, the Public Base URL will be used.", "format": "uri", - "examples": ["https://auth.myexample.org/"] + "examples": [ + "https://auth.myexample.org/" + ] }, "providers": { "title": "OpenID Connect and OAuth2 Providers", @@ -1920,7 +2199,9 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": ["email"] + "required": [ + "email" + ] } } }, @@ -1939,7 +2220,9 @@ "$ref": "#/definitions/smsCourierTemplate" } }, - "required": ["email"] + "required": [ + "email" + ] } } } @@ -1949,13 +2232,18 @@ "type": "string", "title": "Override message templates", "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.", - "examples": ["/conf/courier-templates"] + "examples": [ + "/conf/courier-templates" + ] }, "message_retries": { "description": "Defines the maximum number of times the sending of a message is retried after it failed before it is marked as abandoned", "type": "integer", "default": 5, - "examples": [10, 60] + "examples": [ + 10, + 60 + ] }, "worker": { "description": "Configures the dispatch worker.", @@ -1978,7 +2266,10 @@ "title": "Delivery Strategy", "description": "Defines how emails will be sent, either through SMTP (default) or HTTP.", "type": "string", - "enum": ["smtp", "http"], + "enum": [ + "smtp", + "http" + ], "default": "smtp" }, "http": { @@ -2035,7 +2326,9 @@ "title": "SMTP Sender Name", "description": "The recipient of an email will see this as the sender name.", "type": "string", - "examples": ["Bob"] + "examples": [ + "Bob" + ] }, "headers": { "title": "SMTP Headers", @@ -2083,7 +2376,9 @@ "url": { "title": "HTTP address of API endpoint", "description": "This URL will be used to connect to the SMS provider.", - "examples": ["https://api.twillio.com/sms/send"], + "examples": [ + "https://api.twillio.com/sms/send" + ], "type": "string", "pattern": "^https?:\\/\\/.*" }, @@ -2125,7 +2420,10 @@ }, "additionalProperties": false }, - "required": ["url", "method"], + "required": [ + "url", + "method" + ], "additionalProperties": false } }, @@ -2142,19 +2440,26 @@ "title": "Channel id", "description": "The channel id. Corresponds to the .via property of the identity schema for recovery, verification, etc. Currently only phone is supported.", "maxLength": 32, - "enum": ["sms"] + "enum": [ + "sms" + ] }, "type": { "type": "string", "title": "Channel type", "description": "The channel type. Currently only http is supported.", - "enum": ["http"] + "enum": [ + "http" + ] }, "request_config": { "$ref": "#/definitions/httpRequestConfig" } }, - "required": ["id", "request_config"], + "required": [ + "id", + "request_config" + ], "additionalProperties": false } } @@ -2205,7 +2510,10 @@ "type": "string", "title": "Default Read Consistency Level", "description": "The default consistency level to use when reading from the database. Defaults to `strong` to not break existing API contracts. Only set this to `eventual` if you can accept that other read APIs will suddenly return eventually consistent results. It is only effective in Ory Network.", - "enum": ["strong", "eventual"], + "enum": [ + "strong", + "eventual" + ], "default": "strong" } } @@ -2233,7 +2541,9 @@ "description": "The URL where the admin endpoint is exposed at.", "type": "string", "format": "uri", - "examples": ["https://kratos.private-network:4434/"] + "examples": [ + "https://kratos.private-network:4434/" + ] }, "host": { "title": "Admin Host", @@ -2247,7 +2557,9 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [4434], + "examples": [ + 4434 + ], "default": 4434 }, "socket": { @@ -2306,7 +2618,9 @@ ] }, "uniqueItems": true, - "default": ["*"], + "default": [ + "*" + ], "examples": [ [ "https://example.com", @@ -2318,7 +2632,13 @@ "allowed_methods": { "type": "array", "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", - "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "default": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE" + ], "items": { "type": "string", "enum": [ @@ -2352,7 +2672,9 @@ "exposed_headers": { "type": "array", "description": "Sets which headers are safe to expose to the API of a CORS API specification.", - "default": ["Content-Type"], + "default": [ + "Content-Type" + ], "items": { "type": "string" } @@ -2395,7 +2717,9 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [4433], + "examples": [ + 4433 + ], "default": 4433 }, "socket": { @@ -2445,7 +2769,10 @@ "format": { "description": "The log format can either be text or JSON.", "type": "string", - "enum": ["json", "text"] + "enum": [ + "json", + "text" + ] } }, "additionalProperties": false @@ -2486,7 +2813,9 @@ "id": { "title": "The schema's ID.", "type": "string", - "examples": ["employee"] + "examples": [ + "employee" + ] }, "url": { "type": "string", @@ -2500,11 +2829,16 @@ ] } }, - "required": ["id", "url"] + "required": [ + "id", + "url" + ] } } }, - "required": ["schemas"], + "required": [ + "schemas" + ], "additionalProperties": false }, "secrets": { @@ -2553,7 +2887,10 @@ "description": "One of the values: argon2, bcrypt.\nAny other hashes will be migrated to the set algorithm once an identity authenticates using their password.", "type": "string", "default": "bcrypt", - "enum": ["argon2", "bcrypt"] + "enum": [ + "argon2", + "bcrypt" + ] }, "argon2": { "title": "Configuration for the Argon2id hasher.", @@ -2609,7 +2946,9 @@ "title": "Configuration for the Bcrypt hasher. Minimum is 4 when --dev flag is used and 12 otherwise.", "type": "object", "additionalProperties": false, - "required": ["cost"], + "required": [ + "cost" + ], "properties": { "cost": { "type": "integer", @@ -2631,7 +2970,11 @@ "description": "One of the values: noop, aes, xchacha20-poly1305", "type": "string", "default": "noop", - "enum": ["noop", "aes", "xchacha20-poly1305"] + "enum": [ + "noop", + "aes", + "xchacha20-poly1305" + ] } } }, @@ -2655,7 +2998,11 @@ "title": "HTTP Cookie Same Site Configuration", "description": "Sets the session and CSRF cookie SameSite.", "type": "string", - "enum": ["Strict", "Lax", "None"], + "enum": [ + "Strict", + "Lax", + "None" + ], "default": "Lax" } }, @@ -2685,7 +3032,9 @@ "patternProperties": { "[a-zA-Z0-9-_.]+": { "type": "object", - "required": ["jwks_url"], + "required": [ + "jwks_url" + ], "properties": { "ttl": { "type": "string", @@ -2718,7 +3067,11 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "24h", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] }, "cookie": { "type": "object", @@ -2749,7 +3102,11 @@ "title": "Session Cookie SameSite Configuration", "description": "Sets the session cookie SameSite. Overrides `cookies.same_site`.", "type": "string", - "enum": ["Strict", "Lax", "None"] + "enum": [ + "Strict", + "Lax", + "None" + ] } }, "additionalProperties": false @@ -2759,7 +3116,11 @@ "description": "Sets when a session can be extended. Settings this value to `24h` will prevent the session from being extended before until 24 hours before it expires. This setting prevents excessive writes to the database. We highly recommend setting this value.", "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", - "examples": ["1h", "1m", "1s"] + "examples": [ + "1h", + "1m", + "1s" + ] } } }, @@ -2768,7 +3129,9 @@ "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", "type": "string", "pattern": "^(v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|$", - "examples": ["v0.5.0-alpha.1"] + "examples": [ + "v0.5.0-alpha.1" + ] }, "dev": { "type": "boolean" @@ -2792,7 +3155,9 @@ "type": "integer", "minimum": 0, "maximum": 65535, - "examples": [4434], + "examples": [ + 4434 + ], "default": 0 }, "config": { @@ -2864,7 +3229,7 @@ }, "organizations": { "title": "Organizations", - "description": "Secifies which organizations are available. Only effective in the Ory Network.", + "description": "Please use selfservice.methods.b2b instead. This key will be removed. Only effective in the Ory Network.", "type": "array", "default": [] }, @@ -2898,10 +3263,14 @@ "const": true } }, - "required": ["enabled"] + "required": [ + "enabled" + ] } }, - "required": ["verification"] + "required": [ + "verification" + ] }, { "properties": { @@ -2911,21 +3280,31 @@ "const": true } }, - "required": ["enabled"] + "required": [ + "enabled" + ] } }, - "required": ["recovery"] + "required": [ + "recovery" + ] } ] } }, - "required": ["flows"] + "required": [ + "flows" + ] } }, - "required": ["selfservice"] + "required": [ + "selfservice" + ] }, "then": { - "required": ["courier"] + "required": [ + "courier" + ] } }, { @@ -2944,21 +3323,33 @@ ] } }, - "required": ["algorithm"] + "required": [ + "algorithm" + ] } }, - "required": ["ciphers"] + "required": [ + "ciphers" + ] }, "then": { - "required": ["secrets"], + "required": [ + "secrets" + ], "properties": { "secrets": { - "required": ["cipher"] + "required": [ + "cipher" + ] } } } } ], - "required": ["identity", "dsn", "selfservice"], + "required": [ + "identity", + "dsn", + "selfservice" + ], "additionalProperties": false } diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From b29dff3850ba3d8ce04a32d9327222038629925d Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:14:13 +0000 Subject: [PATCH 02/71] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From b192c92d6c969d470d6479bc33dbc351d327c1f9 Mon Sep 17 00:00:00 2001 From: Patrik Date: Thu, 13 Jun 2024 15:25:25 +0200 Subject: [PATCH 03/71] test: deflake session extend config side-effect (#3950) --- .golangci.yml | 6 + cipher/cipher_test.go | 72 ++-- cmd/courier/watch_test.go | 4 +- cmd/hashers/argon2/root.go | 3 + courier/template/load_template_test.go | 2 +- driver/config/config.go | 16 +- driver/config/config_test.go | 100 ++--- driver/config/test_config.go | 75 ++++ driver/factory.go | 2 +- driver/registry_default_test.go | 534 ++++++++++++------------- go.mod | 2 +- hydra/hydra_test.go | 4 +- internal/driver.go | 28 +- internal/testhelpers/config.go | 44 +- internal/testhelpers/network.go | 2 +- persistence/sql/persister_hmac_test.go | 2 +- session/test/persistence.go | 31 +- x/redir_test.go | 12 +- 18 files changed, 525 insertions(+), 414 deletions(-) create mode 100644 driver/config/test_config.go diff --git a/.golangci.yml b/.golangci.yml index 079e952252ba..374c9204ed1b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -25,3 +25,9 @@ run: skip-files: - ".+_test.go" - "corpx/faker.go" + +issues: + exclude: + - "Set is deprecated: use context-based WithConfigValue instead" + - "SetDefaultIdentitySchemaFromRaw is deprecated: Use context-based WithDefaultIdentitySchemaFromRaw instead" + - "SetDefaultIdentitySchema is deprecated: Use context-based WithDefaultIdentitySchema instead" diff --git a/cipher/cipher_test.go b/cipher/cipher_test.go index 8cdb0ed0e2ac..eb8ba7e1ba7b 100644 --- a/cipher/cipher_test.go +++ b/cipher/cipher_test.go @@ -9,6 +9,8 @@ import ( "fmt" "testing" + "github.com/ory/x/configx" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -18,10 +20,11 @@ import ( "github.com/ory/kratos/internal" ) +var goodSecret = []string{"secret-thirty-two-character-long"} + func TestCipher(t *testing.T) { ctx := context.Background() - cfg, reg := internal.NewFastRegistryWithMocks(t) - goodSecret := []string{"secret-thirty-two-character-long"} + _, reg := internal.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeySecretsDefault, goodSecret)) ciphers := []cipher.Cipher{ cipher.NewCryptAES(reg), @@ -30,82 +33,71 @@ func TestCipher(t *testing.T) { for _, c := range ciphers { t.Run(fmt.Sprintf("cipher=%T", c), func(t *testing.T) { + t.Parallel() t.Run("case=all_work", func(t *testing.T) { - cfg.MustSet(ctx, config.ViperKeySecretsCipher, goodSecret) - testAllWork(t, c, cfg) + t.Parallel() + + testAllWork(ctx, t, c) }) t.Run("case=encryption_failed", func(t *testing.T) { - // unset secret - err := cfg.Set(ctx, config.ViperKeySecretsCipher, []string{}) - require.NoError(t, err) + t.Parallel() + + ctx := config.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}) // secret have to be set - _, err = c.Encrypt(context.Background(), []byte("not-empty")) + _, err := c.Encrypt(ctx, []byte("not-empty")) require.Error(t, err) + var hErr *herodot.DefaultError + require.ErrorAs(t, err, &hErr) + assert.Equal(t, "Unable to encrypt message because no cipher secrets were configured.", hErr.Reason()) - // unset secret - err = cfg.Set(ctx, config.ViperKeySecretsCipher, []string{"bad-length"}) - require.NoError(t, err) + ctx = config.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{"bad-length"}) // bad secret length - _, err = c.Encrypt(context.Background(), []byte("not-empty")) - if e, ok := err.(*herodot.DefaultError); ok { - t.Logf("reason contains: %s", e.Reason()) - } - t.Logf("err type %T contains: %s", err, err.Error()) - require.Error(t, err) + _, err = c.Encrypt(ctx, []byte("not-empty")) + require.ErrorAs(t, err, &hErr) + assert.Equal(t, "Unable to encrypt message because no cipher secrets were configured.", hErr.Reason()) }) t.Run("case=decryption_failed", func(t *testing.T) { - // set secret - err := cfg.Set(ctx, config.ViperKeySecretsCipher, goodSecret) - require.NoError(t, err) + t.Parallel() - // - _, err = c.Decrypt(context.Background(), hex.EncodeToString([]byte("bad-data"))) + _, err := c.Decrypt(ctx, hex.EncodeToString([]byte("bad-data"))) require.Error(t, err) - _, err = c.Decrypt(context.Background(), "not-empty") + _, err = c.Decrypt(ctx, "not-empty") require.Error(t, err) - // unset secret - err = cfg.Set(ctx, config.ViperKeySecretsCipher, []string{}) - require.NoError(t, err) - - _, err = c.Decrypt(context.Background(), "not-empty") + _, err = c.Decrypt(config.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}), "not-empty") require.Error(t, err) }) }) } + c := cipher.NewNoop(reg) t.Run(fmt.Sprintf("cipher=%T", c), func(t *testing.T) { - cfg.MustSet(ctx, config.ViperKeySecretsCipher, goodSecret) - testAllWork(t, c, cfg) + t.Parallel() + testAllWork(ctx, t, c) }) } -func testAllWork(t *testing.T, c cipher.Cipher, cfg *config.Config) { - ctx := context.Background() - - goodSecret := []string{"secret-thirty-two-character-long"} - cfg.MustSet(ctx, config.ViperKeySecretsCipher, goodSecret) - +func testAllWork(ctx context.Context, t *testing.T, c cipher.Cipher) { message := "my secret message!" - encryptedSecret, err := c.Encrypt(context.Background(), []byte(message)) + encryptedSecret, err := c.Encrypt(ctx, []byte(message)) require.NoError(t, err) - decryptedSecret, err := c.Decrypt(context.Background(), encryptedSecret) + decryptedSecret, err := c.Decrypt(ctx, encryptedSecret) require.NoError(t, err, "encrypted", encryptedSecret) assert.Equal(t, message, string(decryptedSecret)) // data to encrypt return blank result - _, err = c.Encrypt(context.Background(), []byte("")) + _, err = c.Encrypt(ctx, []byte("")) require.NoError(t, err) // empty encrypted data return blank - _, err = c.Decrypt(context.Background(), "") + _, err = c.Decrypt(ctx, "") require.NoError(t, err) } diff --git a/cmd/courier/watch_test.go b/cmd/courier/watch_test.go index 48fd6515f53b..b521e9119a97 100644 --- a/cmd/courier/watch_test.go +++ b/cmd/courier/watch_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ory/kratos/internal" + "github.com/ory/x/configx" ) func TestStartCourier(t *testing.T) { @@ -27,10 +28,9 @@ func TestStartCourier(t *testing.T) { t.Run("case=with metrics", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - _, r := internal.NewFastRegistryWithMocks(t) port, err := freeport.GetFreePort() require.NoError(t, err) - r.Config().Set(ctx, "expose-metrics-port", port) + _, r := internal.NewFastRegistryWithMocks(t, configx.WithValue("expose-metrics-port", port)) go StartCourier(ctx, r) time.Sleep(time.Second) res, err := http.Get("http://" + r.Config().MetricsListenOn(ctx) + "/metrics/prometheus") diff --git a/cmd/hashers/argon2/root.go b/cmd/hashers/argon2/root.go index c5cb76581590..2282f0404d4a 100644 --- a/cmd/hashers/argon2/root.go +++ b/cmd/hashers/argon2/root.go @@ -9,6 +9,8 @@ import ( "reflect" "strings" + "github.com/ory/x/contextx" + "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -70,6 +72,7 @@ func configProvider(cmd *cobra.Command, flagConf *argon2Config) (*argon2Config, cmd.Context(), l, cmd.ErrOrStderr(), + &contextx.Default{}, configx.WithFlags(cmd.Flags()), configx.SkipValidation(), configx.WithContext(cmd.Context()), diff --git a/courier/template/load_template_test.go b/courier/template/load_template_test.go index e6b043aa0c54..1fd245497ca9 100644 --- a/courier/template/load_template_test.go +++ b/courier/template/load_template_test.go @@ -182,7 +182,7 @@ func TestLoadTextTemplate(t *testing.T) { }) t.Run("case=disallowed resources", func(t *testing.T) { - require.NoError(t, reg.Config().GetProvider(ctx).Set(config.ViperKeyClientHTTPNoPrivateIPRanges, true)) + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyClientHTTPNoPrivateIPRanges, true)) reg.HTTPClient(ctx).RetryMax = 1 reg.HTTPClient(ctx).RetryWaitMax = time.Millisecond diff --git a/driver/config/config.go b/driver/config/config.go index 05d7ddef52a7..9f3c1b38938b 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -367,13 +367,13 @@ func (s Schemas) FindSchemaByID(id string) (*Schema, error) { return nil, errors.Errorf("unable to find identity schema with id: %s", id) } -func MustNew(t testing.TB, l *logrusx.Logger, stdOutOrErr io.Writer, opts ...configx.OptionModifier) *Config { - p, err := New(context.TODO(), l, stdOutOrErr, opts...) +func MustNew(t testing.TB, l *logrusx.Logger, stdOutOrErr io.Writer, ctxer contextx.Contextualizer, opts ...configx.OptionModifier) *Config { + p, err := New(context.TODO(), l, stdOutOrErr, ctxer, opts...) require.NoError(t, err) return p } -func New(ctx context.Context, l *logrusx.Logger, stdOutOrErr io.Writer, opts ...configx.OptionModifier) (*Config, error) { +func New(ctx context.Context, l *logrusx.Logger, stdOutOrErr io.Writer, ctxer contextx.Contextualizer, opts ...configx.OptionModifier) (*Config, error) { var c *Config opts = append([]configx.OptionModifier{ @@ -402,7 +402,7 @@ func New(ctx context.Context, l *logrusx.Logger, stdOutOrErr io.Writer, opts ... l.UseConfig(p) - c = NewCustom(l, p, stdOutOrErr, &contextx.Default{}) + c = NewCustom(l, p, stdOutOrErr, ctxer) if !p.SkipValidation() { if err := c.validateIdentitySchemas(ctx); err != nil { @@ -518,12 +518,14 @@ func (p *Config) cors(ctx context.Context, prefix string) (cors.Options, bool) { }) } +// Deprecatd: use context-based WithConfigValue instead func (p *Config) Set(ctx context.Context, key string, value interface{}) error { - return p.GetProvider(ctx).Set(key, value) + return p.p.Set(key, value) } +// Deprecated: use context-based WithConfigValue instead func (p *Config) MustSet(ctx context.Context, key string, value interface{}) { - if err := p.GetProvider(ctx).Set(key, value); err != nil { + if err := p.p.Set(key, value); err != nil { p.l.WithError(err).Fatalf("Unable to set \"%s\" to \"%s\".", key, value) } } @@ -859,7 +861,7 @@ func (p *Config) SecretsCipher(ctx context.Context) [][32]byte { result := make([][32]byte, len(cleanSecrets)) for n, s := range secrets { for k, v := range []byte(s) { - result[n][k] = byte(v) + result[n][k] = v } } return result diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 6cb37f100850..dc276eb3a171 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -18,6 +18,8 @@ import ( "testing" "time" + "github.com/ory/x/contextx" + "github.com/ory/x/httpx" "github.com/ory/x/randx" @@ -51,6 +53,7 @@ func TestViperProvider(t *testing.T) { t.Run("suite=loaders", func(t *testing.T) { p := config.MustNew(t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml"), configx.WithContext(ctx), ) @@ -89,6 +92,7 @@ func TestViperProvider(t *testing.T) { pWithFragments := config.MustNew(t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.WithValues(map[string]interface{}{ config.ViperKeySelfServiceLoginUI: "http://test.kratos.ory.sh/#/login", config.ViperKeySelfServiceSettingsURL: "http://test.kratos.ory.sh/#/settings", @@ -105,6 +109,7 @@ func TestViperProvider(t *testing.T) { pWithRelativeFragments := config.MustNew(t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.WithValues(map[string]interface{}{ config.ViperKeySelfServiceLoginUI: "/login", config.ViperKeySelfServiceSettingsURL: "/settings", @@ -130,6 +135,7 @@ func TestViperProvider(t *testing.T) { pWithIncorrectUrls := config.MustNew(t, logger, os.Stderr, + &contextx.Default{}, configx.WithValues(map[string]interface{}{ config.ViperKeySelfServiceLoginUI: v, }), @@ -161,6 +167,7 @@ func TestViperProvider(t *testing.T) { t.Run("group=identity", func(t *testing.T) { c := config.MustNew(t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.mock.identities.yaml"), configx.SkipValidation()) @@ -198,7 +205,7 @@ func TestViperProvider(t *testing.T) { }, p.SecretsSession(ctx)) var cipherExpected [32]byte for k, v := range []byte("secret-thirty-two-character-long") { - cipherExpected[k] = byte(v) + cipherExpected[k] = v } assert.Equal(t, [][32]byte{ cipherExpected, @@ -400,7 +407,7 @@ func TestViperProvider(t *testing.T) { func TestBcrypt(t *testing.T) { t.Parallel() ctx := context.Background() - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) require.NoError(t, p.Set(ctx, config.ViperKeyHasherBcryptCost, 4)) require.NoError(t, p.Set(ctx, "dev", false)) @@ -418,7 +425,7 @@ func TestProviderBaseURLs(t *testing.T) { machineHostname = "127.0.0.1" } - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "https://"+machineHostname+":4433/", p.SelfPublicURL(ctx).String()) assert.Equal(t, "https://"+machineHostname+":4434/", p.SelfAdminURL(ctx).String()) @@ -446,7 +453,7 @@ func TestProviderSelfServiceLinkMethodBaseURL(t *testing.T) { machineHostname = "127.0.0.1" } - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "https://"+machineHostname+":4433/", p.SelfServiceLinkMethodBaseURL(ctx).String()) p.MustSet(ctx, config.ViperKeyLinkBaseURL, "https://example.org/bar") @@ -456,7 +463,7 @@ func TestProviderSelfServiceLinkMethodBaseURL(t *testing.T) { func TestViperProvider_Secrets(t *testing.T) { t.Parallel() ctx := context.Background() - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) def := p.SecretsDefault(ctx) assert.NotEmpty(t, def) @@ -479,24 +486,25 @@ func TestViperProvider_Defaults(t *testing.T) { }{ { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) }, }, { init: func() *config.Config { return config.MustNew(t, l, os.Stderr, + &contextx.Default{}, configx.WithConfigFiles("stub/.defaults.yml"), configx.SkipValidation()) }, }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("stub/.defaults-password.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.defaults-password.yml"), configx.SkipValidation()) }, }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("../../test/e2e/profiles/recovery/.kratos.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/recovery/.kratos.yml"), configx.SkipValidation()) }, expect: func(t *testing.T, p *config.Config) { assert.True(t, p.SelfServiceFlowRecoveryEnabled(ctx)) @@ -512,7 +520,7 @@ func TestViperProvider_Defaults(t *testing.T) { }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("../../test/e2e/profiles/verification/.kratos.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/verification/.kratos.yml"), configx.SkipValidation()) }, expect: func(t *testing.T, p *config.Config) { assert.False(t, p.SelfServiceFlowRecoveryEnabled(ctx)) @@ -528,7 +536,7 @@ func TestViperProvider_Defaults(t *testing.T) { }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("../../test/e2e/profiles/oidc/.kratos.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/oidc/.kratos.yml"), configx.SkipValidation()) }, expect: func(t *testing.T, p *config.Config) { assert.False(t, p.SelfServiceFlowRecoveryEnabled(ctx)) @@ -543,7 +551,7 @@ func TestViperProvider_Defaults(t *testing.T) { }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("stub/.kratos.notify-unknown-recipients.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.notify-unknown-recipients.yml"), configx.SkipValidation()) }, expect: func(t *testing.T, p *config.Config) { assert.True(t, p.SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx)) @@ -572,7 +580,7 @@ func TestViperProvider_Defaults(t *testing.T) { } t.Run("suite=ui_url", func(t *testing.T) { - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "https://www.ory.sh/kratos/docs/fallback/login", p.SelfServiceFlowLoginUI(ctx).String()) assert.Equal(t, "https://www.ory.sh/kratos/docs/fallback/settings", p.SelfServiceFlowSettingsUI(ctx).String()) assert.Equal(t, "https://www.ory.sh/kratos/docs/fallback/registration", p.SelfServiceFlowRegistrationUI(ctx).String()) @@ -585,7 +593,7 @@ func TestViperProvider_ReturnTo(t *testing.T) { t.Parallel() ctx := context.Background() l := logrusx.New("", "") - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) p.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh/") assert.Equal(t, "https://www.ory.sh/", p.SelfServiceFlowVerificationReturnTo(ctx, urlx.ParseOrPanic("https://www.ory.sh/")).String()) @@ -602,7 +610,7 @@ func TestSession(t *testing.T) { t.Parallel() ctx := context.Background() l := logrusx.New("", "") - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "ory_kratos_session", p.SessionName(ctx)) p.MustSet(ctx, config.ViperKeySessionName, "ory_session") @@ -629,7 +637,7 @@ func TestCookies(t *testing.T) { t.Parallel() ctx := context.Background() l := logrusx.New("", "") - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) t.Run("path", func(t *testing.T) { assert.Equal(t, "/", p.CookiePath(ctx)) @@ -676,14 +684,14 @@ func TestViperProvider_DSN(t *testing.T) { ctx := context.Background() t.Run("case=dsn: memory", func(t *testing.T) { - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) p.MustSet(ctx, config.ViperKeyDSN, "memory") assert.Equal(t, config.DefaultSQLiteMemoryDSN, p.DSN(ctx)) }) t.Run("case=dsn: not memory", func(t *testing.T) { - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) dsn := "sqlite://foo.db?_fk=true" p.MustSet(ctx, config.ViperKeyDSN, dsn) @@ -698,7 +706,7 @@ func TestViperProvider_DSN(t *testing.T) { l := logrusx.New("", "", logrusx.WithExitFunc(func(i int) { exitCode = i })) - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, dsn, p.DSN(ctx)) assert.NotEqual(t, 0, exitCode) @@ -714,7 +722,7 @@ func TestViperProvider_ParseURIOrFail(t *testing.T) { l := logrusx.New("", "", logrusx.WithExitFunc(func(i int) { exitCode = i })) - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) require.Zero(t, exitCode) const testKey = "testKeyNotUsedInTheRealSchema" @@ -768,7 +776,7 @@ func TestViperProvider_HaveIBeenPwned(t *testing.T) { t.Parallel() ctx := context.Background() - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) t.Run("case=hipb: host", func(t *testing.T) { p.MustSet(ctx, config.ViperKeyPasswordHaveIBeenPwnedHost, "foo.bar") assert.Equal(t, "foo.bar", p.PasswordPolicyConfig(ctx).HaveIBeenPwnedHost) @@ -806,7 +814,7 @@ func newTestConfig(t *testing.T) (_ *config.Config, _ *test.Hook, exited *bool) exited = new(bool) l.Logger.Hooks.Add(h) l.Logger.ExitFunc = func(code int) { *exited = true } - config := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + config := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) return config, h, exited } @@ -972,7 +980,7 @@ func TestIdentitySchemaValidation(t *testing.T) { l := logrusx.New("kratos-"+tmpConfig.Name(), "test") hook := test.NewLocal(l.Logger) - conf, err := config.New(ctx, l, os.Stderr, configx.WithConfigFiles(tmpConfig.Name())) + conf, err := config.New(ctx, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles(tmpConfig.Name())) assert.NoError(t, err) // clean the hooks since it will throw an event on first boot @@ -986,7 +994,7 @@ func TestIdentitySchemaValidation(t *testing.T) { t.Run("case=skip invalid schema validation", func(t *testing.T) { ctx := ctx - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.invalid.identities.yaml"), configx.SkipValidation()) assert.NoError(t, err) @@ -995,7 +1003,7 @@ func TestIdentitySchemaValidation(t *testing.T) { t.Run("case=invalid schema should throw error", func(t *testing.T) { ctx := ctx var stdErr bytes.Buffer - _, err := config.New(ctx, logrusx.New("", ""), &stdErr, + _, err := config.New(ctx, logrusx.New("", ""), &stdErr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.invalid.identities.yaml")) assert.Error(t, err) assert.Contains(t, err.Error(), "minimum 1 properties allowed, but found 0") @@ -1013,7 +1021,7 @@ func TestIdentitySchemaValidation(t *testing.T) { err := make(chan error, 1) go func(err chan error) { - _, e := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, e := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.mock.identities.yaml")) err <- e }(err) @@ -1068,7 +1076,7 @@ func TestPasswordless(t *testing.T) { t.Parallel() ctx := context.Background() - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation(), configx.WithValue(config.ViperKeyWebAuthnPasswordless, true)) require.NoError(t, err) @@ -1083,7 +1091,7 @@ func TestPasswordlessCode(t *testing.T) { ctx := context.Background() - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation(), configx.WithValue(config.ViperKeySelfServiceStrategyConfig+".code", map[string]interface{}{ "passwordless_enabled": true, @@ -1100,7 +1108,7 @@ func TestChangeMinPasswordLength(t *testing.T) { t.Run("case=must fail on minimum password length below enforced minimum", func(t *testing.T) { ctx := context.Background() - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml"), configx.WithValue(config.ViperKeyPasswordMinLength, 5)) @@ -1110,7 +1118,7 @@ func TestChangeMinPasswordLength(t *testing.T) { t.Run("case=must not fail on minimum password length above enforced minimum", func(t *testing.T) { ctx := context.Background() - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml"), configx.WithValue(config.ViperKeyPasswordMinLength, 9)) @@ -1123,14 +1131,14 @@ func TestCourierEmailHTTP(t *testing.T) { ctx := context.Background() t.Run("case=configs set", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.email.http.yaml"), configx.SkipValidation()) assert.Equal(t, "http", conf.CourierEmailStrategy(ctx)) snapshotx.SnapshotT(t, conf.CourierEmailRequestConfig(ctx)) }) t.Run("case=defaults", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "smtp", conf.CourierEmailStrategy(ctx)) }) @@ -1140,7 +1148,7 @@ func TestCourierChannels(t *testing.T) { t.Parallel() ctx := context.Background() t.Run("case=configs set", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.WithConfigFiles("stub/.kratos.courier.channels.yaml"), configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.channels.yaml"), configx.SkipValidation()) channelConfig, err := conf.CourierChannels(ctx) require.NoError(t, err) @@ -1152,7 +1160,7 @@ func TestCourierChannels(t *testing.T) { }) t.Run("case=defaults", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) channelConfig, err := conf.CourierChannels(ctx) require.NoError(t, err) @@ -1171,7 +1179,7 @@ func TestCourierChannels(t *testing.T) { "smtp://username:pass%2Fword@email-smtp.eu-west-3.amazonaws.com:587/", } { t.Run("case="+tc, func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.WithValue(config.ViperKeyCourierSMTPURL, tc), configx.SkipValidation()) + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithValue(config.ViperKeyCourierSMTPURL, tc), configx.SkipValidation()) require.NoError(t, err) cs, err := conf.CourierChannels(ctx) require.NoError(t, err) @@ -1187,13 +1195,13 @@ func TestCourierMessageTTL(t *testing.T) { ctx := context.Background() t.Run("case=configs set", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.message_retries.yaml"), configx.SkipValidation()) assert.Equal(t, conf.CourierMessageRetries(ctx), 10) }) t.Run("case=defaults", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, conf.CourierMessageRetries(ctx), 5) }) } @@ -1203,7 +1211,7 @@ func TestOAuth2Provider(t *testing.T) { ctx := context.Background() t.Run("case=configs set", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.oauth2_provider.yaml"), configx.SkipValidation()) assert.Equal(t, "https://oauth2_provider/", conf.OAuth2ProviderURL(ctx).String()) assert.Equal(t, http.Header{"Authorization": {"Basic"}}, conf.OAuth2ProviderHeader(ctx)) @@ -1211,7 +1219,7 @@ func TestOAuth2Provider(t *testing.T) { }) t.Run("case=defaults", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Empty(t, conf.OAuth2ProviderURL(ctx)) assert.Empty(t, conf.OAuth2ProviderHeader(ctx)) assert.False(t, conf.OAuth2ProviderOverrideReturnTo(ctx)) @@ -1223,7 +1231,7 @@ func TestWebauthn(t *testing.T) { ctx := context.Background() t.Run("case=multiple origins", func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.webauthn.origins.yaml")) require.NoError(t, err) webAuthnConfig := conf.WebAuthnConfig(ctx) @@ -1236,7 +1244,7 @@ func TestWebauthn(t *testing.T) { }) t.Run("case=one origin", func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.webauthn.origin.yaml")) require.NoError(t, err) webAuthnConfig := conf.WebAuthnConfig(ctx) @@ -1247,7 +1255,7 @@ func TestWebauthn(t *testing.T) { }) t.Run("case=id as origin", func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml")) require.NoError(t, err) webAuthnConfig := conf.WebAuthnConfig(ctx) @@ -1258,7 +1266,7 @@ func TestWebauthn(t *testing.T) { }) t.Run("case=invalid", func(t *testing.T) { - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.webauthn.invalid.yaml")) assert.Error(t, err) }) @@ -1269,19 +1277,19 @@ func TestCourierTemplatesConfig(t *testing.T) { ctx := context.Background() t.Run("case=partial template update allowed", func(t *testing.T) { - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.remote.partial.templates.yaml")) assert.NoError(t, err) }) t.Run("case=load remote template with fallback template overrides path", func(t *testing.T) { - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.remote.templates.yaml")) assert.NoError(t, err) }) t.Run("case=courier template helper", func(t *testing.T) { - c, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + c, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.remote.templates.yaml")) assert.NoError(t, err) @@ -1323,7 +1331,7 @@ func TestCleanup(t *testing.T) { t.Parallel() ctx := context.Background() - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml")) t.Run("group=cleanup config", func(t *testing.T) { diff --git a/driver/config/test_config.go b/driver/config/test_config.go new file mode 100644 index 000000000000..459ae15ac89c --- /dev/null +++ b/driver/config/test_config.go @@ -0,0 +1,75 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "context" + "strings" + + "github.com/knadh/koanf/maps" + + "github.com/ory/kratos/embedx" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" +) + +type ( + TestConfigProvider struct { + contextx.Contextualizer + Options []configx.OptionModifier + } + contextKey int + mapProvider map[string]any +) + +func (t *TestConfigProvider) NewProvider(ctx context.Context, opts ...configx.OptionModifier) (*configx.Provider, error) { + return configx.New(ctx, []byte(embedx.ConfigSchema), append(t.Options, opts...)...) +} + +func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider { + config = t.Contextualizer.Config(ctx, config) + values, ok := ctx.Value(contextConfigKey).(mapProvider) + if !ok { + return config + } + config, err := t.NewProvider(ctx, configx.WithValues(values)) + if err != nil { + // This is not production code. The provider is only used in tests. + panic(err) + } + return config +} + +const contextConfigKey contextKey = 1 + +var ( + _ contextx.Contextualizer = (*TestConfigProvider)(nil) +) + +func WithConfigValue(ctx context.Context, key string, value any) context.Context { + return WithConfigValues(ctx, map[string]any{key: value}) +} + +func WithConfigValues(ctx context.Context, newValues map[string]any) context.Context { + values, ok := ctx.Value(contextConfigKey).(mapProvider) + if !ok { + values = make(mapProvider) + } + expandedValues := make([]map[string]any, 0, len(newValues)) + for k, v := range newValues { + parts := strings.Split(k, ".") + val := map[string]any{parts[len(parts)-1]: v} + if len(parts) > 1 { + for i := len(parts) - 2; i >= 0; i-- { + val = map[string]any{parts[i]: val} + } + } + expandedValues = append(expandedValues, val) + } + for _, v := range expandedValues { + maps.Merge(v, values) + } + + return context.WithValue(ctx, contextConfigKey, values) +} diff --git a/driver/factory.go b/driver/factory.go index e3470d3cffd9..da0dd5601e2b 100644 --- a/driver/factory.go +++ b/driver/factory.go @@ -38,7 +38,7 @@ func NewWithoutInit(ctx context.Context, stdOutOrErr io.Writer, sl *servicelocat c := newOptions(dOpts).config if c == nil { var err error - c, err = config.New(ctx, l, stdOutOrErr, opts...) + c, err = config.New(ctx, l, stdOutOrErr, sl.Contextualizer(), opts...) if err != nil { l.WithError(err).Error("Unable to instantiate configuration.") return nil, err diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go index 009dd76173d8..020517a41159 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -10,6 +10,8 @@ import ( "os" "testing" + "github.com/ory/x/contextx" + "github.com/ory/kratos/selfservice/flow/recovery" "github.com/ory/kratos/selfservice/flow/verification" @@ -34,26 +36,27 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Parallel() ctx := context.Background() + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + t.Run("type=verification", func(t *testing.T) { t.Parallel() // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []verification.PreHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []verification.PreHookExecutor { return nil }, }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceVerificationBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []verification.PreHookExecutor { return []verification.PreHookExecutor{ @@ -64,8 +67,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PreVerificationHooks(ctx) @@ -79,6 +83,7 @@ func TestDriverDefault_Hooks(t *testing.T) { for _, tc := range []struct { uc string prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []verification.PostHookExecutor }{ { @@ -88,11 +93,11 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Multiple web_hooks configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceVerificationAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []verification.PostHookExecutor { return []verification.PostHookExecutor{ @@ -103,8 +108,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PostVerificationHooks(ctx) @@ -120,21 +126,20 @@ func TestDriverDefault_Hooks(t *testing.T) { // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []recovery.PreHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []recovery.PreHookExecutor { return nil }, }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceRecoveryBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []recovery.PreHookExecutor { return []recovery.PreHookExecutor{ @@ -145,8 +150,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PreRecoveryHooks(ctx) @@ -159,21 +165,20 @@ func TestDriverDefault_Hooks(t *testing.T) { // AFTER hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []recovery.PostHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []recovery.PostHookExecutor { return nil }, }, { uc: "Multiple web_hooks configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceRecoveryAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []recovery.PostHookExecutor { return []recovery.PostHookExecutor{ @@ -184,8 +189,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PostRecoveryHooks(ctx) @@ -201,12 +207,11 @@ func TestDriverDefault_Hooks(t *testing.T) { // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []registration.PreHookExecutor }{ { - uc: "No hooks configured", - prep: func(conf *config.Config) {}, + uc: "No hooks configured", expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor { return []registration.PreHookExecutor{ hook.NewTwoStepRegistration(reg), @@ -215,11 +220,11 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceRegistrationBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor { return []registration.PreHookExecutor{ @@ -231,8 +236,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PreRegistrationHooks(ctx) @@ -245,21 +251,20 @@ func TestDriverDefault_Hooks(t *testing.T) { // AFTER hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return nil }, }, { uc: "Only session hook configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{ + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ {"hook": "session"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ @@ -270,12 +275,12 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "A session hook and a web_hook are configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, {"hook": "session"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ @@ -287,11 +292,11 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Two web_hooks are configured on a global level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ @@ -302,15 +307,15 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Hooks are configured on a global level, as well as on a strategy level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + config: map[string]any{ + config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "session"}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + }, + config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, + config.ViperKeySelfServiceVerificationEnabled: true, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ @@ -322,10 +327,10 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "show_verification_ui is configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ + config: map[string]any{ + config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{ {"hook": "show_verification_ui"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ @@ -335,8 +340,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PostRegistrationPostPersistHooks(ctx, identity.CredentialsTypePassword) @@ -352,21 +358,20 @@ func TestDriverDefault_Hooks(t *testing.T) { // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []login.PreHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []login.PreHookExecutor { return nil }, }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceLoginBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []login.PreHookExecutor { return []login.PreHookExecutor{ @@ -377,8 +382,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PreLoginHooks(ctx) @@ -391,20 +397,19 @@ func TestDriverDefault_Hooks(t *testing.T) { // AFTER hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []login.PostHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return nil }, }, { uc: "Only revoke_active_sessions hook configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ {"hook": "revoke_active_sessions"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ @@ -414,10 +419,10 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Only require_verified_address hook configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ {"hook": "require_verified_address"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ @@ -427,12 +432,12 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "A revoke_active_sessions hook, require_verified_address hook and a web_hook are configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, {"hook": "require_verified_address"}, {"hook": "revoke_active_sessions"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ @@ -444,11 +449,11 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Two web_hooks are configured on a global level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ @@ -459,15 +464,15 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Hooks are configured on a global level, as well as on a strategy level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "revoke_active_sessions"}, {"hook": "require_verified_address"}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + }, + config.ViperKeySelfServiceLoginAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ @@ -479,8 +484,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PostLoginHooks(ctx, identity.CredentialsTypePassword) @@ -496,21 +502,20 @@ func TestDriverDefault_Hooks(t *testing.T) { // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []settings.PreHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []settings.PreHookExecutor { return nil }, }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceSettingsBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []settings.PreHookExecutor { return []settings.PreHookExecutor{ @@ -521,8 +526,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PreSettingsHooks(ctx) @@ -535,18 +541,17 @@ func TestDriverDefault_Hooks(t *testing.T) { // AFTER hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return nil }, }, { uc: "Only verify hook configured for the strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, // I think this is a bug as there is a hook named verify defined for both profile and password // strategies. Instead of using it, the code makes use of the property used above and which // is defined in an entirely different flow (verification). @@ -559,11 +564,11 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "A verify hook and a web_hook are configured for profile strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".profile.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"headers": []map[string]string{{"X-Custom-Header": "test"}}, "url": "foo", "method": "POST", "body": "bar"}}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + config: map[string]any{ + config.ViperKeySelfServiceSettingsAfter + ".profile.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"headers": []map[string]string{{"X-Custom-Header": "test"}}, "url": "foo", "method": "POST", "body": "bar"}}, + }, + config.ViperKeySelfServiceVerificationEnabled: true, }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ @@ -574,11 +579,11 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Two web_hooks are configured on a global level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceSettingsAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ @@ -589,14 +594,14 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Hooks are configured on a global level, as well as on a strategy level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".profile.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeySelfServiceSettingsAfter + ".profile.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, + config.ViperKeySelfServiceSettingsAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ @@ -607,8 +612,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) h := reg.PostSettingsPostPersistHooks(ctx, "profile") @@ -623,62 +629,64 @@ func TestDriverDefault_Hooks(t *testing.T) { func TestDriverDefault_Strategies(t *testing.T) { t.Parallel() ctx := context.Background() + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + t.Run("case=registration", func(t *testing.T) { t.Parallel() for _, tc := range []struct { name string - prep func(conf *config.Config) + config map[string]any expect []string }{ { name: "no strategies", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"profile"}, }, { name: "only password", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "profile"}, }, { name: "oidc and password", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "oidc", "profile"}, }, { name: "oidc, password and totp", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".totp.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "oidc", "profile"}, }, { name: "password and code", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true, }, expect: []string{"password", "profile", "code"}, }, } { t.Run(fmt.Sprintf("subcase=%s", tc.name), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() - s := reg.RegistrationStrategies(context.Background()) + ctx := config.WithConfigValues(ctx, tc.config) + s := reg.RegistrationStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { assert.Equal(t, e, s[k].ID().String()) @@ -689,68 +697,69 @@ func TestDriverDefault_Strategies(t *testing.T) { t.Run("case=login", func(t *testing.T) { t.Parallel() + for _, tc := range []struct { name string - prep func(conf *config.Config) + config map[string]any expect []string }{ { name: "no strategies", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, }, { name: "only password", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password"}, }, { name: "oidc and password", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "oidc"}, }, { name: "oidc, password and totp", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".totp.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "oidc", "totp"}, }, { name: "password and code", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true, }, expect: []string{"password", "code"}, }, { name: "code is enabled if passwordless_enabled is true", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.passwordless_enabled": true, }, expect: []string{"code"}, }, } { t.Run(fmt.Sprintf("run=%s", tc.name), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() - s := reg.LoginStrategies(context.Background()) + ctx := config.WithConfigValues(ctx, tc.config) + s := reg.LoginStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { assert.Equal(t, e, s[k].ID().String()) @@ -762,27 +771,28 @@ func TestDriverDefault_Strategies(t *testing.T) { t.Run("case=recovery", func(t *testing.T) { t.Parallel() for k, tc := range []struct { - prep func(conf *config.Config) + config map[string]any expect []string }{ { - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".link.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".link.enabled": false, }, }, { - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".link.enabled", true) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".link.enabled": true, }, expect: []string{"code", "link"}, }, } { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := config.WithConfigValues(ctx, tc.config) - s := reg.RecoveryStrategies(context.Background()) + s := reg.RecoveryStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { assert.Equal(t, e, s[k].RecoveryStrategyID()) @@ -796,81 +806,55 @@ func TestDriverDefault_Strategies(t *testing.T) { l := logrusx.New("", "") for k, tc := range []struct { - prep func(t *testing.T) *config.Config - expect []string + configOptions []configx.OptionModifier + expect []string }{ { - prep: func(t *testing.T) *config.Config { - c := config.MustNew(t, l, - os.Stderr, - configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, - config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, - config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": false, - config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": false, - }), - configx.SkipValidation()) - return c - }, - }, - { - prep: func(t *testing.T) *config.Config { - c := config.MustNew(t, l, - os.Stderr, - configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, - config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, - config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, - }), - configx.SkipValidation()) - return c - }, + configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ + config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": false, + })}, + }, + { + configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ + config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, + config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + })}, expect: []string{"profile"}, }, { - prep: func(t *testing.T) *config.Config { - c := config.MustNew(t, l, - os.Stderr, - configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, - config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, - config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, - config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, - }), - configx.SkipValidation()) - return c - }, + configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ + config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, + config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, + })}, expect: []string{"profile", "totp"}, }, { - prep: func(t *testing.T) *config.Config { - return config.MustNew(t, l, - os.Stderr, - configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, - }), - configx.SkipValidation()) - }, + configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ + config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, + })}, expect: []string{"password", "profile"}, }, { - prep: func(t *testing.T) *config.Config { - return config.MustNew(t, l, - os.Stderr, - configx.WithConfigFiles("../test/e2e/profiles/verification/.kratos.yml"), - configx.WithValue(config.ViperKeyDSN, config.DefaultSQLiteMemoryDSN), - configx.SkipValidation()) + configOptions: []configx.OptionModifier{ + configx.WithConfigFiles("../test/e2e/profiles/verification/.kratos.yml"), + configx.WithValue(config.ViperKeyDSN, config.DefaultSQLiteMemoryDSN), }, expect: []string{"password", "profile"}, }, } { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { - conf := tc.prep(t) + conf := config.MustNew(t, l, os.Stderr, &contextx.Default{}, append(tc.configOptions, configx.SkipValidation())...) - reg, err := driver.NewRegistryFromDSN(ctx, conf, logrusx.New("", "")) + reg, err := driver.NewRegistryFromDSN(ctx, conf, l) require.NoError(t, err) - s := reg.SettingsStrategies(context.Background()) + s := reg.SettingsStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { @@ -924,12 +908,16 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { func TestGetActiveRecoveryStrategy(t *testing.T) { t.Parallel() - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + ctx := context.Background() + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + t.Run("returns error if active strategy is disabled", func(t *testing.T) { - conf.Set(context.Background(), "selfservice.methods.code.enabled", false) - conf.Set(context.Background(), config.ViperKeySelfServiceRecoveryUse, "code") + ctx := config.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.enabled": false, + config.ViperKeySelfServiceRecoveryUse: "code", + }) - _, err := reg.GetActiveRecoveryStrategy(context.Background()) + _, err := reg.GetActiveRecoveryStrategy(ctx) require.Error(t, err) }) @@ -938,10 +926,12 @@ func TestGetActiveRecoveryStrategy(t *testing.T) { "code", "link", } { t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { - conf.Set(context.Background(), fmt.Sprintf("selfservice.methods.%s.enabled", sID), true) - conf.Set(context.Background(), config.ViperKeySelfServiceRecoveryUse, sID) + ctx := config.WithConfigValues(ctx, map[string]any{ + fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, + config.ViperKeySelfServiceRecoveryUse: sID, + }) - s, err := reg.GetActiveRecoveryStrategy(context.Background()) + s, err := reg.GetActiveRecoveryStrategy(ctx) require.NoError(t, err) require.Equal(t, sID, s.RecoveryStrategyID()) }) @@ -951,12 +941,14 @@ func TestGetActiveRecoveryStrategy(t *testing.T) { func TestGetActiveVerificationStrategy(t *testing.T) { t.Parallel() - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + ctx := context.Background() + _, reg := internal.NewVeryFastRegistryWithoutDB(t) t.Run("returns error if active strategy is disabled", func(t *testing.T) { - conf.Set(context.Background(), "selfservice.methods.code.enabled", false) - conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUse, "code") - - _, err := reg.GetActiveVerificationStrategy(context.Background()) + ctx := config.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.enabled": false, + config.ViperKeySelfServiceVerificationUse: "code", + }) + _, err := reg.GetActiveVerificationStrategy(ctx) require.Error(t, err) }) @@ -965,10 +957,12 @@ func TestGetActiveVerificationStrategy(t *testing.T) { "code", "link", } { t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { - conf.Set(context.Background(), fmt.Sprintf("selfservice.methods.%s.enabled", sID), true) - conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUse, sID) + ctx := config.WithConfigValues(ctx, map[string]any{ + fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, + config.ViperKeySelfServiceVerificationUse: sID, + }) - s, err := reg.GetActiveVerificationStrategy(context.Background()) + s, err := reg.GetActiveVerificationStrategy(ctx) require.NoError(t, err) require.Equal(t, sID, s.VerificationStrategyID()) }) diff --git a/go.mod b/go.mod index 537942ebacbd..67e7a524c134 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ory/kratos -go 1.21 +go 1.22 replace ( github.com/go-sql-driver/mysql => github.com/go-sql-driver/mysql v1.7.2-0.20231005084435-37980127edfb diff --git a/hydra/hydra_test.go b/hydra/hydra_test.go index b2e252be5b18..d022ae6021cf 100644 --- a/hydra/hydra_test.go +++ b/hydra/hydra_test.go @@ -13,6 +13,7 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/hydra" "github.com/ory/x/configx" + "github.com/ory/x/contextx" "github.com/ory/x/logrusx" "github.com/ory/x/sqlxx" "github.com/ory/x/urlx" @@ -25,11 +26,12 @@ func requestFromChallenge(s string) *http.Request { func TestGetLoginChallengeID(t *testing.T) { uuidChallenge := "b346a452-e8fb-4828-8ef8-a4dbc98dc23a" blobChallenge := "1337deadbeefcafe" - defaultConfig := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + defaultConfig := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) configWithHydra := config.MustNew( t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.SkipValidation(), configx.WithValues(map[string]interface{}{ config.ViperKeyOAuth2ProviderURL: "https://hydra", diff --git a/internal/driver.go b/internal/driver.go index a6f1f13d7954..3499a83b5b9b 100644 --- a/internal/driver.go +++ b/internal/driver.go @@ -9,9 +9,10 @@ import ( "runtime" "testing" + "github.com/ory/x/contextx" + "github.com/sirupsen/logrus" - "github.com/ory/x/contextx" "github.com/ory/x/jsonnetsecure" "github.com/gofrs/uuid" @@ -36,9 +37,8 @@ func init() { }) } -func NewConfigurationWithDefaults(t testing.TB) *config.Config { - c := config.MustNew(t, logrusx.New("", ""), - os.Stderr, +func NewConfigurationWithDefaults(t testing.TB, opts ...configx.OptionModifier) *config.Config { + configOpts := append([]configx.OptionModifier{ configx.WithValues(map[string]interface{}{ "log.level": "error", config.ViperKeyDSN: dbal.NewSQLiteTestDatabase(t), @@ -53,14 +53,19 @@ func NewConfigurationWithDefaults(t testing.TB) *config.Config { config.ViperKeySecretsCipher: []string{"secret-thirty-two-character-long"}, }), configx.SkipValidation(), + }, opts...) + c := config.MustNew(t, logrusx.New("", ""), + os.Stderr, + &config.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: configOpts}, + configOpts..., ) return c } // NewFastRegistryWithMocks returns a registry with several mocks and an SQLite in memory database that make testing // easier and way faster. This suite does not work for e2e or advanced integration tests. -func NewFastRegistryWithMocks(t *testing.T) (*config.Config, *driver.RegistryDefault) { - conf, reg := NewRegistryDefaultWithDSN(t, "") +func NewFastRegistryWithMocks(t *testing.T, opts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault) { + conf, reg := NewRegistryDefaultWithDSN(t, "", opts...) reg.WithCSRFTokenGenerator(x.FakeCSRFTokenGenerator) reg.WithCSRFHandler(x.NewFakeCSRFHandler("")) reg.WithHooks(map[string]func(config.SelfServiceHook) interface{}{ @@ -76,16 +81,17 @@ func NewFastRegistryWithMocks(t *testing.T) (*config.Config, *driver.RegistryDef } // NewRegistryDefaultWithDSN returns a more standard registry without mocks. Good for e2e and advanced integration testing! -func NewRegistryDefaultWithDSN(t testing.TB, dsn string) (*config.Config, *driver.RegistryDefault) { +func NewRegistryDefaultWithDSN(t testing.TB, dsn string, opts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault) { ctx := context.Background() - c := NewConfigurationWithDefaults(t) - c.MustSet(ctx, config.ViperKeyDSN, stringsx.Coalesce(dsn, dbal.NewSQLiteTestDatabase(t))) + c := NewConfigurationWithDefaults(t, append(opts, configx.WithValues(map[string]interface{}{ + config.ViperKeyDSN: stringsx.Coalesce(dsn, dbal.NewSQLiteTestDatabase(t)), + "dev": true, + }))...) reg, err := driver.NewRegistryFromDSN(ctx, c, logrusx.New("", "", logrusx.ForceLevel(logrus.ErrorLevel))) require.NoError(t, err) - reg.Config().MustSet(ctx, "dev", true) pool := jsonnetsecure.NewProcessPool(runtime.GOMAXPROCS(0)) t.Cleanup(pool.Close) - require.NoError(t, reg.Init(context.Background(), &contextx.Default{}, driver.SkipNetworkInit, driver.WithDisabledMigrationLogging(), driver.WithJsonnetPool(pool))) + require.NoError(t, reg.Init(context.Background(), &config.TestConfigProvider{Contextualizer: &contextx.Default{}}, driver.SkipNetworkInit, driver.WithDisabledMigrationLogging(), driver.WithJsonnetPool(pool))) require.NoError(t, reg.Persister().MigrateUp(context.Background())) // always migrate up actual, err := reg.Persister().DetermineNetwork(context.Background()) diff --git a/internal/testhelpers/config.go b/internal/testhelpers/config.go index 2a24709c0745..8e17a6ab3a12 100644 --- a/internal/testhelpers/config.go +++ b/internal/testhelpers/config.go @@ -8,11 +8,10 @@ import ( "encoding/base64" "testing" - "github.com/ory/kratos/driver/config" - "github.com/spf13/pflag" "github.com/stretchr/testify/require" + "github.com/ory/kratos/driver/config" "github.com/ory/x/configx" "github.com/ory/x/randx" ) @@ -24,6 +23,20 @@ func UseConfigFile(t *testing.T, path string) *pflag.FlagSet { return flags } +func DefaultIdentitySchemaConfig(url string) map[string]any { + return map[string]any{ + config.ViperKeyDefaultIdentitySchemaID: "default", + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "default", URL: url}, + }, + } +} + +func WithDefaultIdentitySchema(ctx context.Context, url string) context.Context { + return config.WithConfigValues(ctx, DefaultIdentitySchemaConfig(url)) +} + +// Deprecated: Use context-based WithDefaultIdentitySchema instead func SetDefaultIdentitySchema(conf *config.Config, url string) func() { schemaUrl, _ := conf.DefaultIdentityTraitsSchemaURL(context.Background()) conf.MustSet(context.Background(), config.ViperKeyDefaultIdentitySchemaID, "default") @@ -37,13 +50,29 @@ func SetDefaultIdentitySchema(conf *config.Config, url string) func() { } } -// UseIdentitySchema registeres an identity schema in the config with a random ID and returns the ID +// WithAddIdentitySchema registers an identity schema in the config with a random ID and returns the ID +// +// It also registers a test cleanup function, to reset the schemas to the original values, after the test finishes +func WithAddIdentitySchema(ctx context.Context, t *testing.T, conf *config.Config, url string) (context.Context, string) { + id := randx.MustString(16, randx.Alpha) + schemas, err := conf.IdentityTraitsSchemas(ctx) + require.NoError(t, err) + + return config.WithConfigValue(ctx, config.ViperKeyIdentitySchemas, append(schemas, config.Schema{ + ID: id, + URL: url, + })), id +} + +// UseIdentitySchema registers an identity schema in the config with a random ID and returns the ID // -// It also registeres a test cleanup function, to reset the schemas to the original values, after the test finishes +// It also registers a test cleanup function, to reset the schemas to the original values, after the test finishes +// Deprecated: Use context-based WithAddIdentitySchema instead func UseIdentitySchema(t *testing.T, conf *config.Config, url string) (id string) { id = randx.MustString(16, randx.Alpha) schemas, err := conf.IdentityTraitsSchemas(context.Background()) require.NoError(t, err) + conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, append(schemas, config.Schema{ ID: id, URL: url, @@ -54,7 +83,12 @@ func UseIdentitySchema(t *testing.T, conf *config.Config, url string) (id string return id } -// SetDefaultIdentitySchemaFromRaw allows setting the default identity schema from a raw JSON string. +// WithDefaultIdentitySchemaFromRaw allows setting the default identity schema from a raw JSON string. +func WithDefaultIdentitySchemaFromRaw(ctx context.Context, schema []byte) context.Context { + return WithDefaultIdentitySchema(ctx, "base64://"+base64.URLEncoding.EncodeToString(schema)) +} + +// Deprecated: Use context-based WithDefaultIdentitySchemaFromRaw instead func SetDefaultIdentitySchemaFromRaw(conf *config.Config, schema []byte) { conf.MustSet(context.Background(), config.ViperKeyDefaultIdentitySchemaID, "default") conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, config.Schemas{ diff --git a/internal/testhelpers/network.go b/internal/testhelpers/network.go index 888f46b583f5..10978dba6b05 100644 --- a/internal/testhelpers/network.go +++ b/internal/testhelpers/network.go @@ -20,7 +20,7 @@ func NewNetworkUnlessExisting(t *testing.T, ctx context.Context, p persistence.P } n := networkx.NewNetwork() - require.NoError(t, p.GetConnection(context.Background()).Create(n)) + require.NoError(t, p.GetConnection(ctx).Create(n)) return n.ID, p.WithNetworkID(n.ID) } diff --git a/persistence/sql/persister_hmac_test.go b/persistence/sql/persister_hmac_test.go index c7adcdce3a1e..7b8cc8575368 100644 --- a/persistence/sql/persister_hmac_test.go +++ b/persistence/sql/persister_hmac_test.go @@ -64,7 +64,7 @@ var _ persisterDependencies = &logRegistryOnly{} func TestPersisterHMAC(t *testing.T) { ctx := context.Background() - conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"foobarbaz"}) c, err := pop.NewConnection(&pop.ConnectionDetails{URL: "sqlite://foo?mode=memory"}) require.NoError(t, err) diff --git a/session/test/persistence.go b/session/test/persistence.go index 8e8cbfeb18b2..0db6964468d8 100644 --- a/session/test/persistence.go +++ b/session/test/persistence.go @@ -42,7 +42,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { return func(t *testing.T) { _, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + ctx := testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/identity.schema.json") t.Run("case=not found", func(t *testing.T) { _, err := p.GetSession(ctx, x.NewUUID(), session.ExpandNothing) @@ -611,10 +611,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { }) t.Run("extend session lifespan but min time is not yet reached", func(t *testing.T) { - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour*2) - t.Cleanup(func() { - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, nil) - }) + ctx := config.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) var expected session.Session require.NoError(t, faker.FakeData(&expected)) @@ -629,23 +626,19 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { }) t.Run("extend session lifespan", func(t *testing.T) { - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour) - t.Cleanup(func() { - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, nil) - }) + ctx := config.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour*2) var expected session.Session require.NoError(t, faker.FakeData(&expected)) expected.ExpiresAt = time.Now().Add(time.Hour).UTC() require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) require.NoError(t, p.UpsertSession(ctx, &expected)) - expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt.Round(time.Minute) + expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt require.NoError(t, p.ExtendSession(ctx, expected.ID)) actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) require.NoError(t, err) - assert.Equal(t, expectedExpiry, actual.ExpiresAt.Round(time.Minute)) + assert.GreaterOrEqual(t, 10*time.Second, expectedExpiry.Sub(actual.ExpiresAt).Abs()) }) t.Run("extend session lifespan on CockroachDB", func(t *testing.T) { @@ -653,23 +646,19 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { t.Skip("Skipping test because driver is not CockroachDB") } - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour) - t.Cleanup(func() { - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, nil) - }) + ctx := config.WithConfigValue(ctx, config.ViperKeySessionRefreshMinTimeLeft, 2*time.Hour) - conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour*2) var expected session.Session require.NoError(t, faker.FakeData(&expected)) expected.ExpiresAt = time.Now().Add(time.Hour).UTC() require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) require.NoError(t, p.UpsertSession(ctx, &expected)) - expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt.Round(time.Minute) + expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt - var foundExpectedCockroachError bool + foundExpectedCockroachError := false g := errgroup.Group{} - for i := 0; i < 10; i++ { + for range 10 { g.Go(func() error { err := p.ExtendSession(ctx, expected.ID) if errors.Is(err, sqlcon.ErrNoRows) { @@ -683,7 +672,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) require.NoError(t, err) - assert.Equal(t, expectedExpiry, actual.ExpiresAt.Round(time.Minute)) + assert.LessOrEqual(t, expectedExpiry.Sub(actual.ExpiresAt).Abs(), 10*time.Second) assert.True(t, foundExpectedCockroachError, "We expect to find a not found error caused by ... FOR UPDATE SKIP LOCKED") }) } diff --git a/x/redir_test.go b/x/redir_test.go index bbb8417f8b91..1c4a191b429b 100644 --- a/x/redir_test.go +++ b/x/redir_test.go @@ -4,7 +4,6 @@ package x_test import ( - "context" "fmt" "io" "net/http" @@ -12,6 +11,8 @@ import ( "strings" "testing" + "github.com/ory/x/configx" + "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,17 +23,16 @@ import ( ) func TestRedirectToPublicAdminRoute(t *testing.T) { - ctx := context.Background() - conf, reg := internal.NewFastRegistryWithMocks(t) pub := x.NewRouterPublic() adm := x.NewRouterAdmin() adminTS := httptest.NewServer(adm) pubTS := httptest.NewServer(pub) t.Cleanup(pubTS.Close) t.Cleanup(adminTS.Close) - - conf.MustSet(ctx, config.ViperKeyAdminBaseURL, adminTS.URL) - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, pubTS.URL) + _, reg := internal.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyAdminBaseURL: adminTS.URL, + config.ViperKeyPublicBaseURL: pubTS.URL, + })) pub.POST("/privileged", x.RedirectToAdminRoute(reg)) pub.POST("/admin/privileged", x.RedirectToAdminRoute(reg)) From 61f87d90bd67e5bb1f00ee110d986e4f72fc4c91 Mon Sep 17 00:00:00 2001 From: Patrik Date: Mon, 17 Jun 2024 12:24:31 +0200 Subject: [PATCH 04/71] test: deflake and parallelize persister tests (#3953) --- driver/config/test_config.go | 39 ++--- identity/manager_test.go | 133 +++++++++--------- identity/test/pool.go | 42 +++--- internal/client-go/go.sum | 1 + internal/driver.go | 2 +- persistence/sql/persister_cleanup_test.go | 18 +++ persistence/sql/persister_code.go | 2 +- persistence/sql/persister_errorx.go | 36 ++--- persistence/sql/persister_hmac.go | 18 +-- persistence/sql/persister_hmac_test.go | 50 ++++--- persistence/sql/persister_recovery.go | 2 +- persistence/sql/persister_test.go | 77 +++++----- persistence/sql/persister_verification.go | 2 +- selfservice/flow/recovery/test/persistence.go | 4 +- selfservice/flow/settings/test/persistence.go | 5 +- .../flow/verification/test/persistence.go | 5 +- .../sessiontokenexchange/test/persistence.go | 4 +- selfservice/strategy/code/test/persistence.go | 5 +- selfservice/strategy/link/test/persistence.go | 5 +- session/test/persistence.go | 2 - 20 files changed, 226 insertions(+), 226 deletions(-) diff --git a/driver/config/test_config.go b/driver/config/test_config.go index 459ae15ac89c..c95fba7b7876 100644 --- a/driver/config/test_config.go +++ b/driver/config/test_config.go @@ -5,9 +5,6 @@ package config import ( "context" - "strings" - - "github.com/knadh/koanf/maps" "github.com/ory/kratos/embedx" "github.com/ory/x/configx" @@ -19,8 +16,7 @@ type ( contextx.Contextualizer Options []configx.OptionModifier } - contextKey int - mapProvider map[string]any + contextKey int ) func (t *TestConfigProvider) NewProvider(ctx context.Context, opts ...configx.OptionModifier) (*configx.Provider, error) { @@ -29,11 +25,15 @@ func (t *TestConfigProvider) NewProvider(ctx context.Context, opts ...configx.Op func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider { config = t.Contextualizer.Config(ctx, config) - values, ok := ctx.Value(contextConfigKey).(mapProvider) + values, ok := ctx.Value(contextConfigKey).([]map[string]any) if !ok { return config } - config, err := t.NewProvider(ctx, configx.WithValues(values)) + opts := make([]configx.OptionModifier, 0, len(values)) + for _, v := range values { + opts = append(opts, configx.WithValues(v)) + } + config, err := t.NewProvider(ctx, opts...) if err != nil { // This is not production code. The provider is only used in tests. panic(err) @@ -51,25 +51,14 @@ func WithConfigValue(ctx context.Context, key string, value any) context.Context return WithConfigValues(ctx, map[string]any{key: value}) } -func WithConfigValues(ctx context.Context, newValues map[string]any) context.Context { - values, ok := ctx.Value(contextConfigKey).(mapProvider) +func WithConfigValues(ctx context.Context, setValues map[string]any) context.Context { + values, ok := ctx.Value(contextConfigKey).([]map[string]any) if !ok { - values = make(mapProvider) - } - expandedValues := make([]map[string]any, 0, len(newValues)) - for k, v := range newValues { - parts := strings.Split(k, ".") - val := map[string]any{parts[len(parts)-1]: v} - if len(parts) > 1 { - for i := len(parts) - 2; i >= 0; i-- { - val = map[string]any{parts[i]: val} - } - } - expandedValues = append(expandedValues, val) - } - for _, v := range expandedValues { - maps.Merge(v, values) + values = make([]map[string]any, 0) } + newValues := make([]map[string]any, len(values), len(values)+1) + copy(newValues, values) + newValues = append(newValues, setValues) - return context.WithValue(ctx, contextConfigKey, values) + return context.WithValue(ctx, contextConfigKey, newValues) } diff --git a/identity/manager_test.go b/identity/manager_test.go index e0346b8ee0c0..f45a3f05e4fc 100644 --- a/identity/manager_test.go +++ b/identity/manager_test.go @@ -4,11 +4,11 @@ package identity_test import ( - "context" "fmt" "testing" "time" + "github.com/ory/x/configx" "github.com/ory/x/pointerx" "github.com/ory/x/sqlcon" @@ -29,17 +29,17 @@ import ( ) func TestManager(t *testing.T) { - conf, reg := internal.NewFastRegistryWithMocks(t) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/manager.schema.json") - extensionSchemaID := testhelpers.UseIdentitySchema(t, conf, "file://./stub/extension.schema.json") - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") - conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) + conf, reg := internal.NewFastRegistryWithMocks(t, configx.WithValues(map[string]interface{}{ + config.ViperKeyPublicBaseURL: "https://www.ory.sh/", + config.ViperKeyCourierSMTPURL: "smtp://foo@bar@dev.null/", + config.ViperKeySelfServiceRegistrationLoginHints: true, + }), configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/manager.schema.json"))) + ctx, extensionSchemaID := testhelpers.WithAddIdentitySchema(ctx, t, conf, "file://./stub/extension.schema.json") t.Run("case=should fail to create because validation fails", func(t *testing.T) { i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) i.Traits = identity.Traits(`{"email":"not an email"}`) - require.Error(t, reg.IdentityManager().Create(context.Background(), i)) + require.Error(t, reg.IdentityManager().Create(ctx, i)) }) newTraits := func(email string, unprotected string) identity.Traits { @@ -62,7 +62,7 @@ func TestManager(t *testing.T) { } checkExtensionFieldsForIdentities := func(t *testing.T, expected string, original *identity.Identity) { - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) identities := []identity.Identity{*original, *fromStore} for k := range identities { @@ -75,7 +75,7 @@ func TestManager(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(email, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) checkExtensionFieldsForIdentities(t, email, original) got, ok := original.AvailableAAL.ToAAL() require.True(t, ok) @@ -87,7 +87,7 @@ func TestManager(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(email, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) got, ok := original.AvailableAAL.ToAAL() require.True(t, ok) assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, got) @@ -104,7 +104,7 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), }, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) got, ok := original.AvailableAAL.ToAAL() require.True(t, ok) assert.Equal(t, identity.AuthenticatorAssuranceLevel1, got) @@ -126,7 +126,7 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), }, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) got, ok := original.AvailableAAL.ToAAL() require.True(t, ok) assert.Equal(t, identity.AuthenticatorAssuranceLevel2, got) @@ -143,7 +143,7 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), }, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) got, ok := original.AvailableAAL.ToAAL() require.True(t, ok) assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, got) @@ -153,7 +153,7 @@ func TestManager(t *testing.T) { t.Run("case=should expose validation errors with option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = identity.Traits(`{"email":"not an email"}`) - err := reg.IdentityManager().Create(context.Background(), original, identity.ManagerExposeValidationErrorsForInternalTypeAssertion) + err := reg.IdentityManager().Create(ctx, original, identity.ManagerExposeValidationErrorsForInternalTypeAssertion) require.Error(t, err) assert.Contains(t, err.Error(), "\"not an email\" is not valid \"email\"") }) @@ -161,7 +161,7 @@ func TestManager(t *testing.T) { t.Run("case=should not expose validation errors without option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = identity.Traits(`{"email":"not an email"}`) - err := reg.IdentityManager().Create(context.Background(), original) + err := reg.IdentityManager().Create(ctx, original) require.Error(t, err) assert.NotContains(t, err.Error(), "\"not an email\" is not valid \"email\"") }) @@ -186,10 +186,10 @@ func TestManager(t *testing.T) { } first := createIdentity(email, "email_creds", creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, "email_creds", creds) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) var verr = new(identity.ErrDuplicateCredentials) @@ -210,10 +210,10 @@ func TestManager(t *testing.T) { } first := createIdentity(email, "email_webauthn", creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, "email_webauthn", nil) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) var verr = new(identity.ErrDuplicateCredentials) @@ -235,10 +235,10 @@ func TestManager(t *testing.T) { } first := createIdentity(email, "email_creds", creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, "email_creds", creds) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) var verr = new(identity.ErrDuplicateCredentials) @@ -270,10 +270,10 @@ func TestManager(t *testing.T) { } first := createIdentity(email, "email_creds", creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, "email_creds", creds) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) var verr = new(identity.ErrDuplicateCredentials) @@ -300,10 +300,10 @@ func TestManager(t *testing.T) { } first := createIdentity(email, field, creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, field, nil) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) var verr = new(identity.ErrDuplicateCredentials) @@ -329,10 +329,10 @@ func TestManager(t *testing.T) { } first := createIdentity(email, field, creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, field, nil) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) var verr = new(identity.ErrDuplicateCredentials) @@ -357,10 +357,10 @@ func TestManager(t *testing.T) { t.Run("case=should update identity and update extension fields", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits("baz@ory.sh", "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) original.Traits = newTraits("bar@ory.sh", "") - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) checkExtensionFieldsForIdentities(t, "bar@ory.sh", original) }) @@ -369,7 +369,7 @@ func TestManager(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(email, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) original.Credentials = map[identity.CredentialsType]identity.Credentials{ identity.CredentialsTypePassword: { Type: identity.CredentialsTypePassword, @@ -377,7 +377,7 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), }, } - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.AvailableAAL.String) }) @@ -392,16 +392,16 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), }, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.AvailableAAL.String) - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.AvailableAAL.String, "Updating without changes should not change AAL") original.Credentials[identity.CredentialsTypeTOTP] = identity.Credentials{ Type: identity.CredentialsTypeTOTP, Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), } - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, original.AvailableAAL.String) }) @@ -409,7 +409,7 @@ func TestManager(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(email, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) original.Credentials = map[identity.CredentialsType]identity.Credentials{ identity.CredentialsTypeTOTP: { Type: identity.CredentialsTypeTOTP, @@ -417,7 +417,7 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), }, } - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) assert.True(t, original.AvailableAAL.Valid) assert.EqualValues(t, identity.NoAuthenticatorAssuranceLevel, original.AvailableAAL.String) }) @@ -425,14 +425,14 @@ func TestManager(t *testing.T) { t.Run("case=should not update protected traits without option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits("email-update-1@ory.sh", "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) original.Traits = newTraits("email-update-2@ory.sh", "") - err := reg.IdentityManager().Update(context.Background(), original) + err := reg.IdentityManager().Update(ctx, original) require.Error(t, err) assert.Equal(t, identity.ErrProtectedFieldModified, errors.Cause(err)) - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) // As UpdateTraits takes only the ID as a parameter it cannot update the identity in place. // That is why we only check the identity in the store. @@ -482,21 +482,21 @@ func TestManager(t *testing.T) { originalEmail := x.NewUUID().String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(originalEmail, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) checkExtensionFields(fromStore, originalEmail)(t) newEmail := x.NewUUID().String() + "@ory.sh" original.Traits = newTraits(newEmail, "") - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) - fromStore, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) checkExtensionFields(fromStore, newEmail)(t) - recoveryAddresses, err := reg.PrivilegedIdentityPool().ListRecoveryAddresses(context.Background(), 0, 500) + recoveryAddresses, err := reg.PrivilegedIdentityPool().ListRecoveryAddresses(ctx, 0, 500) require.NoError(t, err) var foundRecoveryAddress bool @@ -508,7 +508,7 @@ func TestManager(t *testing.T) { } require.True(t, foundRecoveryAddress) - verifiableAddresses, err := reg.PrivilegedIdentityPool().ListVerifiableAddresses(context.Background(), 0, 500) + verifiableAddresses, err := reg.PrivilegedIdentityPool().ListVerifiableAddresses(ctx, 0, 500) require.NoError(t, err) var foundVerifiableAddress bool for _, a := range verifiableAddresses { @@ -569,13 +569,13 @@ func TestManager(t *testing.T) { t.Run("case=should update protected traits with option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits("email-updatetraits-1@ory.sh", "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) require.NoError(t, reg.IdentityManager().UpdateTraits( - context.Background(), original.ID, newTraits("email-updatetraits-2@ory.sh", ""), + ctx, original.ID, newTraits("email-updatetraits-2@ory.sh", ""), identity.ManagerAllowWriteProtectedTraits)) - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) // As UpdateTraits takes only the ID as a parameter it cannot update the identity in place. // That is why we only check the identity in the store. @@ -585,17 +585,17 @@ func TestManager(t *testing.T) { t.Run("case=should update identity and update extension fields", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`) - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) // These should all fail because they modify existing keys - require.Error(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"not-baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`))) - require.Error(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"not-baz@ory.sh","email_recovery":"not-baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`))) - require.Error(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"not-baz@ory.sh","unprotected": "foo"}`))) + require.Error(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"not-baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`))) + require.Error(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"not-baz@ory.sh","email_recovery":"not-baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`))) + require.Error(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"not-baz@ory.sh","unprotected": "foo"}`))) - require.NoError(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "bar"}`))) + require.NoError(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "bar"}`))) checkExtensionFieldsForIdentities(t, "baz@ory.sh", original) - actual, err := reg.IdentityPool().GetIdentity(context.Background(), original.ID, identity.ExpandNothing) + actual, err := reg.IdentityPool().GetIdentity(ctx, original.ID, identity.ExpandNothing) require.NoError(t, err) assert.JSONEq(t, `{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "bar"}`, string(actual.Traits)) }) @@ -603,14 +603,14 @@ func TestManager(t *testing.T) { t.Run("case=should not update protected traits without option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits("email-updatetraits-1@ory.sh", "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) err := reg.IdentityManager().UpdateTraits( - context.Background(), original.ID, newTraits("email-updatetraits-2@ory.sh", "")) + ctx, original.ID, newTraits("email-updatetraits-2@ory.sh", "")) require.Error(t, err) assert.Equal(t, identity.ErrProtectedFieldModified, errors.Cause(err)) - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) // As UpdateTraits takes only the ID as a parameter it cannot update the identity in place. // That is why we only check the identity in the store. @@ -619,7 +619,7 @@ func TestManager(t *testing.T) { }) t.Run("method=ConflictingIdentity", func(t *testing.T) { - ctx := context.Background() + ctx := ctx conflicOnIdentifier := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) conflicOnIdentifier.Traits = identity.Traits(`{"email":"conflict-on-identifier@example.com"}`) @@ -682,12 +682,13 @@ func TestManager(t *testing.T) { } func TestManagerNoDefaultNamedSchema(t *testing.T) { - conf, reg := internal.NewFastRegistryWithMocks(t) - conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "user_v0") - conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{ - {ID: "user_v0", URL: "file://./stub/manager.schema.json"}, - }) - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + _, reg := internal.NewFastRegistryWithMocks(t, configx.WithValues(map[string]interface{}{ + config.ViperKeyDefaultIdentitySchemaID: "user_v0", + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "user_v0", URL: "file://./stub/manager.schema.json"}, + }, + config.ViperKeyPublicBaseURL: "https://www.ory.sh/", + })) t.Run("case=should create identity with default schema", func(t *testing.T) { stateChangedAt := sqlxx.NullTime(time.Now().UTC()) @@ -697,6 +698,6 @@ func TestManagerNoDefaultNamedSchema(t *testing.T) { State: identity.StateActive, StateChangedAt: &stateChangedAt, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) }) } diff --git a/identity/test/pool.go b/identity/test/pool.go index 450b5c1ea881..bf6d114b8510 100644 --- a/identity/test/pool.go +++ b/identity/test/pool.go @@ -36,12 +36,11 @@ import ( "github.com/ory/x/urlx" ) -func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, m *identity.Manager, dbname string) func(t *testing.T) { +func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager, dbname string) func(t *testing.T) { return func(t *testing.T) { - exampleServerURL := urlx.ParseOrPanic("http://example.com") - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, exampleServerURL.String()) - nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) + + exampleServerURL := urlx.ParseOrPanic("http://example.com") expandSchema := schema.Schema{ ID: "expandSchema", URL: urlx.ParseOrPanic("file://./stub/expand.schema.json"), @@ -62,22 +61,25 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, URL: urlx.ParseOrPanic("file://./stub/handler/multiple_emails.schema.json"), RawURL: "file://./stub/identity-2.schema.json", } - conf.MustSet(ctx, config.ViperKeyIdentitySchemas, []config.Schema{ - { - ID: altSchema.ID, - URL: altSchema.RawURL, - }, - { - ID: defaultSchema.ID, - URL: defaultSchema.RawURL, - }, - { - ID: expandSchema.ID, - URL: expandSchema.RawURL, - }, - { - ID: multipleEmailsSchema.ID, - URL: multipleEmailsSchema.RawURL, + ctx := config.WithConfigValues(ctx, map[string]any{ + config.ViperKeyPublicBaseURL: exampleServerURL.String(), + config.ViperKeyIdentitySchemas: []config.Schema{ + { + ID: altSchema.ID, + URL: altSchema.RawURL, + }, + { + ID: defaultSchema.ID, + URL: defaultSchema.RawURL, + }, + { + ID: expandSchema.ID, + URL: expandSchema.RawURL, + }, + { + ID: multipleEmailsSchema.ID, + URL: multipleEmailsSchema.RawURL, + }, }, }) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/driver.go b/internal/driver.go index 3499a83b5b9b..5d31cfe5ceb2 100644 --- a/internal/driver.go +++ b/internal/driver.go @@ -84,7 +84,7 @@ func NewFastRegistryWithMocks(t *testing.T, opts ...configx.OptionModifier) (*co func NewRegistryDefaultWithDSN(t testing.TB, dsn string, opts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault) { ctx := context.Background() c := NewConfigurationWithDefaults(t, append(opts, configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: stringsx.Coalesce(dsn, dbal.NewSQLiteTestDatabase(t)), + config.ViperKeyDSN: stringsx.Coalesce(dsn, dbal.NewSQLiteTestDatabase(t)+"&lock=false&max_conns=1"), "dev": true, }))...) reg, err := driver.NewRegistryFromDSN(ctx, c, logrusx.New("", "", logrusx.ForceLevel(logrus.ErrorLevel))) diff --git a/persistence/sql/persister_cleanup_test.go b/persistence/sql/persister_cleanup_test.go index 65e95ea6ea00..efb14a05e6c9 100644 --- a/persistence/sql/persister_cleanup_test.go +++ b/persistence/sql/persister_cleanup_test.go @@ -14,6 +14,8 @@ import ( ) func TestPersister_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() ctx := context.Background() @@ -29,6 +31,8 @@ func TestPersister_Cleanup(t *testing.T) { } func TestPersister_Continuity_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -45,6 +49,8 @@ func TestPersister_Continuity_Cleanup(t *testing.T) { } func TestPersister_Login_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -61,6 +67,8 @@ func TestPersister_Login_Cleanup(t *testing.T) { } func TestPersister_Recovery_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -77,6 +85,8 @@ func TestPersister_Recovery_Cleanup(t *testing.T) { } func TestPersister_Registration_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -93,6 +103,8 @@ func TestPersister_Registration_Cleanup(t *testing.T) { } func TestPersister_Session_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -109,6 +121,8 @@ func TestPersister_Session_Cleanup(t *testing.T) { } func TestPersister_Settings_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -125,6 +139,8 @@ func TestPersister_Settings_Cleanup(t *testing.T) { } func TestPersister_Verification_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -141,6 +157,8 @@ func TestPersister_Verification_Cleanup(t *testing.T) { } func TestPersister_SessionTokenExchange_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() diff --git a/persistence/sql/persister_code.go b/persistence/sql/persister_code.go index 31e0b80dc2d2..ece7dea75ec3 100644 --- a/persistence/sql/persister_code.go +++ b/persistence/sql/persister_code.go @@ -94,7 +94,7 @@ func useOneTimeCode[P any, U interface { secrets: for _, secret := range p.r.Config().SecretsSession(ctx) { - suppliedCode := []byte(p.hmacValueWithSecret(ctx, userProvidedCode, secret)) + suppliedCode := []byte(hmacValueWithSecret(ctx, userProvidedCode, secret)) for i := range codes { c := codes[i] if subtle.ConstantTimeCompare([]byte(c.GetHMACCode()), suppliedCode) == 0 { diff --git a/persistence/sql/persister_errorx.go b/persistence/sql/persister_errorx.go index 15faf9fd163b..fc656074f0fc 100644 --- a/persistence/sql/persister_errorx.go +++ b/persistence/sql/persister_errorx.go @@ -4,18 +4,17 @@ package sql import ( - "bytes" "context" "encoding/json" "time" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/pkg/errors" "go.opentelemetry.io/otel/attribute" - "github.com/ory/jsonschema/v3" - "github.com/ory/herodot" + "github.com/ory/jsonschema/v3" "github.com/ory/x/otelx" "github.com/ory/x/sqlcon" @@ -28,7 +27,7 @@ func (p *Persister) CreateErrorContainer(ctx context.Context, csrfToken string, ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateErrorContainer") defer otelx.End(span, &err) - message, err := p.encodeSelfServiceErrors(ctx, errs) + message, err := encodeSelfServiceErrors(errs) if err != nil { return uuid.Nil, err } @@ -55,14 +54,19 @@ func (p *Persister) ReadErrorContainer(ctx context.Context, id uuid.UUID) (_ *er defer otelx.End(span, &err) var ec errorx.ErrorContainer - if err := p.GetConnection(ctx).Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&ec); err != nil { - return nil, sqlcon.HandleError(err) - } - - if err := p.GetConnection(ctx).RawQuery( - "UPDATE selfservice_errors SET was_seen = true, seen_at = ? WHERE id = ? AND nid = ?", - time.Now().UTC(), id, p.NetworkID(ctx)).Exec(); err != nil { - return nil, sqlcon.HandleError(err) + if err := p.Transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + if err := c.Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&ec); err != nil { + return sqlcon.HandleError(err) + } + + if err := c.RawQuery( + "UPDATE selfservice_errors SET was_seen = true, seen_at = ? WHERE id = ? AND nid = ?", + time.Now().UTC(), id, p.NetworkID(ctx)).Exec(); err != nil { + return sqlcon.HandleError(err) + } + return nil + }); err != nil { + return nil, err } return &ec, nil @@ -85,7 +89,7 @@ func (p *Persister) ClearErrorContainers(ctx context.Context, olderThan time.Dur return sqlcon.HandleError(err) } -func (p *Persister) encodeSelfServiceErrors(ctx context.Context, e error) ([]byte, error) { +func encodeSelfServiceErrors(e error) ([]byte, error) { if e == nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithDebug("A nil error was passed to the error manager which is most likely a code bug.")) } @@ -98,10 +102,10 @@ func (p *Persister) encodeSelfServiceErrors(ctx context.Context, e error) ([]byt e = herodot.ToDefaultError(e, "") } - var b bytes.Buffer - if err := json.NewEncoder(&b).Encode(e); err != nil { + enc, err := json.Marshal(e) + if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to encode error messages.").WithDebug(err.Error())) } - return b.Bytes(), nil + return enc, nil } diff --git a/persistence/sql/persister_hmac.go b/persistence/sql/persister_hmac.go index 9c4d6636f14c..8fdb04df3dd6 100644 --- a/persistence/sql/persister_hmac.go +++ b/persistence/sql/persister_hmac.go @@ -7,27 +7,19 @@ import ( "context" "crypto/hmac" "crypto/sha512" - "crypto/subtle" "fmt" + + "go.opentelemetry.io/otel/trace" ) func (p *Persister) hmacValue(ctx context.Context, value string) string { - return p.hmacValueWithSecret(ctx, value, p.r.Config().SecretsSession(ctx)[0]) + return hmacValueWithSecret(ctx, value, p.r.Config().SecretsSession(ctx)[0]) } -func (p *Persister) hmacValueWithSecret(ctx context.Context, value string, secret []byte) string { - _, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.hmacValueWithSecret") +func hmacValueWithSecret(ctx context.Context, value string, secret []byte) string { + _, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "persistence.sql.hmacValueWithSecret") defer span.End() h := hmac.New(sha512.New512_256, secret) _, _ = h.Write([]byte(value)) return fmt.Sprintf("%x", h.Sum(nil)) } - -func (p *Persister) hmacConstantCompare(ctx context.Context, value, hash string) bool { - for _, secret := range p.r.Config().SecretsSession(ctx) { - if subtle.ConstantTimeCompare([]byte(p.hmacValueWithSecret(ctx, value, secret)), []byte(hash)) == 1 { - return true - } - } - return false -} diff --git a/persistence/sql/persister_hmac_test.go b/persistence/sql/persister_hmac_test.go index 7b8cc8575368..c569affa0cd9 100644 --- a/persistence/sql/persister_hmac_test.go +++ b/persistence/sql/persister_hmac_test.go @@ -8,9 +8,10 @@ import ( "os" "testing" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" - "github.com/ory/x/configx" "github.com/ory/x/otelx" "github.com/gobuffalo/pop/v6" @@ -49,10 +50,10 @@ func (l *logRegistryOnly) Audit() *logrusx.Logger { panic("implement me") } -func (l *logRegistryOnly) Tracer(ctx context.Context) *otelx.Tracer { +func (l *logRegistryOnly) Tracer(context.Context) *otelx.Tracer { return otelx.NewNoop(l.l, new(otelx.Config)) } -func (l *logRegistryOnly) IdentityTraitsSchemas(ctx context.Context) (schema.IdentitySchemaList, error) { +func (l *logRegistryOnly) IdentityTraitsSchemas(context.Context) (schema.IdentitySchemaList, error) { panic("implement me") } @@ -63,25 +64,36 @@ func (l *logRegistryOnly) IdentityValidator() *identity.Validator { var _ persisterDependencies = &logRegistryOnly{} func TestPersisterHMAC(t *testing.T) { + t.Parallel() + ctx := context.Background() - conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"foobarbaz"}) + baseSecret := "foobarbaz" + baseSecretBytes := []byte(baseSecret) + opts := []configx.OptionModifier{configx.SkipValidation(), configx.WithValue(config.ViperKeySecretsDefault, []string{baseSecret})} + conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, &config.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: opts}, opts...) c, err := pop.NewConnection(&pop.ConnectionDetails{URL: "sqlite://foo?mode=memory"}) require.NoError(t, err) - p, err := NewPersister(context.Background(), &logRegistryOnly{c: conf}, c) + p, err := NewPersister(ctx, &logRegistryOnly{c: conf}, c) require.NoError(t, err) - assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "hashme"))) - assert.False(t, p.hmacConstantCompare(context.Background(), "notme", p.hmacValue(context.Background(), "hashme"))) - assert.False(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "notme"))) - - hash := p.hmacValue(context.Background(), "hashme") - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"notfoobarbaz"}) - assert.False(t, p.hmacConstantCompare(context.Background(), "hashme", hash)) - assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "hashme"))) - - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"notfoobarbaz", "foobarbaz"}) - assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", hash)) - assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "hashme"))) - assert.NotEqual(t, hash, p.hmacValue(context.Background(), "hashme")) + t.Run("case=behaves deterministically", func(t *testing.T) { + assert.Equal(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "hashme")) + assert.NotEqual(t, hmacValueWithSecret(ctx, "notme", baseSecretBytes), p.hmacValue(ctx, "hashme")) + assert.NotEqual(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "notme")) + }) + + hash := p.hmacValue(ctx, "hashme") + newSecret := "not" + baseSecret + + t.Run("case=with only new sectet", func(t *testing.T) { + ctx = config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret}) + assert.NotEqual(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "hashme")) + assert.Equal(t, hmacValueWithSecret(ctx, "hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme")) + }) + + t.Run("case=with new and old secret", func(t *testing.T) { + ctx = config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret, baseSecret}) + assert.Equal(t, hmacValueWithSecret(ctx, "hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme")) + assert.NotEqual(t, hash, p.hmacValue(ctx, "hashme")) + }) } diff --git a/persistence/sql/persister_recovery.go b/persistence/sql/persister_recovery.go index 468ba5a2b144..bb23d3fd319e 100644 --- a/persistence/sql/persister_recovery.go +++ b/persistence/sql/persister_recovery.go @@ -82,7 +82,7 @@ func (p *Persister) UseRecoveryToken(ctx context.Context, fID uuid.UUID, token s nid := p.NetworkID(ctx) if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { for _, secret := range p.r.Config().SecretsSession(ctx) { - if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_recovery_flow_id = ?", p.hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil { + if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_recovery_flow_id = ?", hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil { if !errors.Is(sqlcon.HandleError(err), sqlcon.ErrNoRows) { return err } diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go index f88d7380a5c7..6a48e763d0f3 100644 --- a/persistence/sql/persister_test.go +++ b/persistence/sql/persister_test.go @@ -94,7 +94,7 @@ func pl(t testing.TB) func(lvl logging.Level, s string, args ...interface{}) { func createCleanDatabases(t testing.TB) map[string]*driver.RegistryDefault { conns := map[string]string{ - "sqlite": "sqlite://file:" + t.TempDir() + "/db.sqlite?_fk=true", + "sqlite": "sqlite://file:" + t.TempDir() + "/db.sqlite?_fk=true&max_conns=1&lock=false", } var l sync.Mutex @@ -160,111 +160,104 @@ func createCleanDatabases(t testing.TB) map[string]*driver.RegistryDefault { } func TestPersister(t *testing.T) { + t.Parallel() + conns := createCleanDatabases(t) - ctx := context.Background() + ctx := testhelpers.WithDefaultIdentitySchema(context.Background(), "file://./stub/identity.schema.json") - for name := range conns { - name := name - reg := conns[name] + for name, reg := range conns { t.Run(fmt.Sprintf("database=%s", name), func(t *testing.T) { t.Parallel() _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) - conf := reg.Config() - t.Logf("DSN: %s", conf.DSN(ctx)) + t.Logf("DSN: %s", reg.Config().DSN(ctx)) - // This test must remain the first test in the test suite! t.Run("racy identity creation", func(t *testing.T) { - defaultSchema := schema.Schema{ - ID: config.DefaultIdentityTraitsSchemaID, - URL: urlx.ParseOrPanic("file://./stub/identity.schema.json"), - RawURL: "file://./stub/identity.schema.json", - } + t.Parallel() var wg sync.WaitGroup - testhelpers.SetDefaultIdentitySchema(reg.Config(), defaultSchema.RawURL) + _, ps := testhelpers.NewNetwork(t, ctx, reg.Persister()) - for i := 0; i < 10; i++ { + for i := range 10 { wg.Add(1) - // capture i - ii := i go func() { defer wg.Done() id := ri.NewIdentity("") id.SetCredentials(ri.CredentialsTypePassword, ri.Credentials{ Type: ri.CredentialsTypePassword, - Identifiers: []string{fmt.Sprintf("racy identity %d", ii)}, + Identifiers: []string{fmt.Sprintf("racy identity %d", i)}, Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), }) id.Traits = ri.Traits("{}") - require.NoError(t, ps.CreateIdentity(context.Background(), id)) + require.NoError(t, ps.CreateIdentity(ctx, id)) }() } wg.Wait() }) - t.Run("case=credentials types", func(t *testing.T) { + t.Run("case=credential types exist", func(t *testing.T) { + t.Parallel() for _, ct := range []ri.CredentialsType{ri.CredentialsTypeOIDC, ri.CredentialsTypePassword} { require.NoError(t, p.(*sql.Persister).Connection(context.Background()).Where("name = ?", ct).First(&ri.CredentialsTypeTable{})) } }) t.Run("contract=identity.TestPool", func(t *testing.T) { - pop.SetLogger(pl(t)) - identity.TestPool(ctx, conf, p, reg.IdentityManager(), name)(t) + t.Parallel() + identity.TestPool(ctx, p, reg.IdentityManager(), name)(t) }) t.Run("contract=registration.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() registration.TestFlowPersister(ctx, p)(t) }) t.Run("contract=errorx.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() errorx.TestPersister(ctx, p)(t) }) t.Run("contract=login.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() login.TestFlowPersister(ctx, p)(t) }) t.Run("contract=settings.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - settings.TestFlowPersister(ctx, conf, p)(t) + t.Parallel() + settings.TestFlowPersister(ctx, p)(t) }) t.Run("contract=session.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - session.TestPersister(ctx, conf, p)(t) + t.Parallel() + session.TestPersister(ctx, reg.Config(), p)(t) }) t.Run("contract=sessiontokenexchange.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - sessiontokenexchange.TestPersister(ctx, conf, p)(t) + t.Parallel() + sessiontokenexchange.TestPersister(ctx, p)(t) }) t.Run("contract=courier.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() upsert, insert := sqltesthelpers.DefaultNetworkWrapper(p) courier.TestPersister(ctx, upsert, insert)(t) }) t.Run("contract=verification.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - verification.TestFlowPersister(ctx, conf, p)(t) + t.Parallel() + verification.TestFlowPersister(ctx, p)(t) }) t.Run("contract=recovery.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - recovery.TestFlowPersister(ctx, conf, p)(t) + t.Parallel() + recovery.TestFlowPersister(ctx, p)(t) }) t.Run("contract=link.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - link.TestPersister(ctx, conf, p)(t) + t.Parallel() + link.TestPersister(ctx, p)(t) }) t.Run("contract=code.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - code.TestPersister(ctx, conf, p)(t) + t.Parallel() + code.TestPersister(ctx, p)(t) }) t.Run("contract=continuity.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() continuity.TestPersister(ctx, p)(t) }) }) @@ -283,6 +276,8 @@ func getErr(args ...interface{}) error { } func TestPersister_Transaction(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() diff --git a/persistence/sql/persister_verification.go b/persistence/sql/persister_verification.go index 8d983ed1635d..7feae0592ae7 100644 --- a/persistence/sql/persister_verification.go +++ b/persistence/sql/persister_verification.go @@ -82,7 +82,7 @@ func (p *Persister) UseVerificationToken(ctx context.Context, fID uuid.UUID, tok nid := p.NetworkID(ctx) if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { for _, secret := range p.r.Config().SecretsSession(ctx) { - if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_verification_flow_id = ?", p.hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil { + if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_verification_flow_id = ?", hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil { if !errors.Is(sqlcon.HandleError(err), sqlcon.ErrNoRows) { return err } diff --git a/selfservice/flow/recovery/test/persistence.go b/selfservice/flow/recovery/test/persistence.go index 8bc9efad88e0..75dd6b00c6b2 100644 --- a/selfservice/flow/recovery/test/persistence.go +++ b/selfservice/flow/recovery/test/persistence.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/flow" @@ -23,7 +22,7 @@ import ( "github.com/ory/x/sqlcon" ) -func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { +func TestFlowPersister(ctx context.Context, p interface { persistence.Persister }, ) func(t *testing.T) { @@ -33,7 +32,6 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") t.Run("case=should error when the recovery request does not exist", func(t *testing.T) { _, err := p.GetRecoveryFlow(ctx, x.NewUUID()) diff --git a/selfservice/flow/settings/test/persistence.go b/selfservice/flow/settings/test/persistence.go index 85c80e49d74e..498419a65c3a 100644 --- a/selfservice/flow/settings/test/persistence.go +++ b/selfservice/flow/settings/test/persistence.go @@ -27,7 +27,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/x" ) @@ -37,12 +36,10 @@ func clearids(r *settings.Flow) { r.IdentityID = uuid.Nil } -func TestFlowPersister(ctx context.Context, conf *config.Config, p persistence.Persister) func(t *testing.T) { +func TestFlowPersister(ctx context.Context, p persistence.Persister) func(t *testing.T) { return func(t *testing.T) { _, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") - t.Run("case=should error when the settings request does not exist", func(t *testing.T) { _, err := p.GetSettingsFlow(ctx, x.NewUUID()) require.Error(t, err) diff --git a/selfservice/flow/verification/test/persistence.go b/selfservice/flow/verification/test/persistence.go index 57c35cba8d2e..a021d06a152e 100644 --- a/selfservice/flow/verification/test/persistence.go +++ b/selfservice/flow/verification/test/persistence.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/flow" @@ -23,7 +22,7 @@ import ( "github.com/ory/x/sqlcon" ) -func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { +func TestFlowPersister(ctx context.Context, p interface { persistence.Persister }, ) func(t *testing.T) { @@ -34,8 +33,6 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") - t.Run("case=should error when the verification request does not exist", func(t *testing.T) { _, err := p.GetVerificationFlow(ctx, x.NewUUID()) require.Error(t, err) diff --git a/selfservice/sessiontokenexchange/test/persistence.go b/selfservice/sessiontokenexchange/test/persistence.go index 53db63db04f3..da19c3edc1a3 100644 --- a/selfservice/sessiontokenexchange/test/persistence.go +++ b/selfservice/sessiontokenexchange/test/persistence.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/sessiontokenexchange" @@ -36,11 +35,10 @@ func (t *testParams) setCodes(e *sessiontokenexchange.Exchanger) { t.returnToCode = e.ReturnToCode } -func TestPersister(ctx context.Context, _ *config.Config, p interface { +func TestPersister(ctx context.Context, p interface { persistence.Persister }) func(t *testing.T) { return func(t *testing.T) { - t.Parallel() nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) t.Run("suite=create-update-get", func(t *testing.T) { diff --git a/selfservice/strategy/code/test/persistence.go b/selfservice/strategy/code/test/persistence.go index f3c120402ddb..e7648cb055b3 100644 --- a/selfservice/strategy/code/test/persistence.go +++ b/selfservice/strategy/code/test/persistence.go @@ -24,15 +24,14 @@ import ( "github.com/ory/kratos/x" ) -func TestPersister(ctx context.Context, conf *config.Config, p interface { +func TestPersister(ctx context.Context, p interface { persistence.Persister }, ) func(t *testing.T) { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) + ctx := config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("code=recovery", func(t *testing.T) { newRecoveryCodeDTO := func(t *testing.T, email string) (*code.CreateRecoveryCodeParams, *recovery.Flow, *identity.RecoveryAddress) { diff --git a/selfservice/strategy/link/test/persistence.go b/selfservice/strategy/link/test/persistence.go index af5738eaae31..a77a1db1d9c8 100644 --- a/selfservice/strategy/link/test/persistence.go +++ b/selfservice/strategy/link/test/persistence.go @@ -28,15 +28,14 @@ import ( "github.com/ory/kratos/x" ) -func TestPersister(ctx context.Context, conf *config.Config, p interface { +func TestPersister(ctx context.Context, p interface { persistence.Persister }, ) func(t *testing.T) { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) + ctx := config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("token=recovery", func(t *testing.T) { newRecoveryToken := func(t *testing.T, email string) (*link.RecoveryToken, *recovery.Flow) { diff --git a/session/test/persistence.go b/session/test/persistence.go index 0db6964468d8..0b709a3866b8 100644 --- a/session/test/persistence.go +++ b/session/test/persistence.go @@ -42,8 +42,6 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { return func(t *testing.T) { _, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - ctx := testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/identity.schema.json") - t.Run("case=not found", func(t *testing.T) { _, err := p.GetSession(ctx, x.NewUUID(), session.ExpandNothing) require.Error(t, err) From bac030b3d5a4d145fb616d59d1603bb2c5f2a429 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:25:54 +0000 Subject: [PATCH 05/71] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From e0001b0db784457652581366bd7ead7cdf6b3898 Mon Sep 17 00:00:00 2001 From: Patrik Date: Tue, 18 Jun 2024 11:18:08 +0200 Subject: [PATCH 06/71] test: enable server-side config from context (#3954) --- cipher/cipher_test.go | 8 +- driver/config/handler_test.go | 40 +++-- driver/config/test_config.go | 64 -------- driver/config/testhelpers/config.go | 152 ++++++++++++++++++ driver/registry_default_test.go | 36 +++-- identity/test/pool.go | 4 +- internal/driver.go | 6 +- internal/testhelpers/config.go | 6 +- persistence/sql/persister_hmac_test.go | 8 +- selfservice/strategy/code/test/persistence.go | 6 +- selfservice/strategy/link/test/persistence.go | 4 +- session/test/persistence.go | 8 +- 12 files changed, 234 insertions(+), 108 deletions(-) delete mode 100644 driver/config/test_config.go create mode 100644 driver/config/testhelpers/config.go diff --git a/cipher/cipher_test.go b/cipher/cipher_test.go index eb8ba7e1ba7b..90e02ff0de45 100644 --- a/cipher/cipher_test.go +++ b/cipher/cipher_test.go @@ -9,6 +9,8 @@ import ( "fmt" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/x/configx" "github.com/stretchr/testify/assert" @@ -44,7 +46,7 @@ func TestCipher(t *testing.T) { t.Run("case=encryption_failed", func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}) + ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}) // secret have to be set _, err := c.Encrypt(ctx, []byte("not-empty")) @@ -53,7 +55,7 @@ func TestCipher(t *testing.T) { require.ErrorAs(t, err, &hErr) assert.Equal(t, "Unable to encrypt message because no cipher secrets were configured.", hErr.Reason()) - ctx = config.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{"bad-length"}) + ctx = confighelpers.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{"bad-length"}) // bad secret length _, err = c.Encrypt(ctx, []byte("not-empty")) @@ -70,7 +72,7 @@ func TestCipher(t *testing.T) { _, err = c.Decrypt(ctx, "not-empty") require.Error(t, err) - _, err = c.Decrypt(config.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}), "not-empty") + _, err = c.Decrypt(confighelpers.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}), "not-empty") require.Error(t, err) }) }) diff --git a/driver/config/handler_test.go b/driver/config/handler_test.go index da84a5bc08d4..8c73a9319621 100644 --- a/driver/config/handler_test.go +++ b/driver/config/handler_test.go @@ -6,9 +6,10 @@ package config_test import ( "context" "io" - "net/http/httptest" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,21 +18,32 @@ import ( "github.com/ory/kratos/internal" ) +type configProvider struct { + cfg *config.Config +} + +func (c *configProvider) Config() *config.Config { + return c.cfg +} + func TestNewConfigHashHandler(t *testing.T) { ctx := context.Background() - conf, reg := internal.NewFastRegistryWithMocks(t) + cfg := internal.NewConfigurationWithDefaults(t) router := httprouter.New() - config.NewConfigHashHandler(reg, router) - ts := httptest.NewServer(router) + config.NewConfigHashHandler(&configProvider{cfg: cfg}, router) + ts := confighelpers.NewConfigurableTestServer(router) t.Cleanup(ts.Close) - res, err := ts.Client().Get(ts.URL + "/health/config") + + // first request, get baseline hash + res, err := ts.Client(ctx).Get(ts.URL + "/health/config") require.NoError(t, err) defer res.Body.Close() require.Equal(t, 200, res.StatusCode) first, err := io.ReadAll(res.Body) require.NoError(t, err) - res, err = ts.Client().Get(ts.URL + "/health/config") + // second request, no config change + res, err = ts.Client(ctx).Get(ts.URL + "/health/config") require.NoError(t, err) defer res.Body.Close() require.Equal(t, 200, res.StatusCode) @@ -39,13 +51,21 @@ func TestNewConfigHashHandler(t *testing.T) { require.NoError(t, err) assert.Equal(t, first, second) - require.NoError(t, conf.Set(ctx, config.ViperKeySessionDomain, "foobar")) + // third request, with config change + res, err = ts.Client(confighelpers.WithConfigValue(ctx, config.ViperKeySessionDomain, "foobar")).Get(ts.URL + "/health/config") + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, 200, res.StatusCode) + third, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.NotEqual(t, first, third) - res, err = ts.Client().Get(ts.URL + "/health/config") + // fourth request, no config change + res, err = ts.Client(ctx).Get(ts.URL + "/health/config") require.NoError(t, err) defer res.Body.Close() require.Equal(t, 200, res.StatusCode) - second, err = io.ReadAll(res.Body) + fourth, err := io.ReadAll(res.Body) require.NoError(t, err) - assert.NotEqual(t, first, second) + assert.Equal(t, first, fourth) } diff --git a/driver/config/test_config.go b/driver/config/test_config.go deleted file mode 100644 index c95fba7b7876..000000000000 --- a/driver/config/test_config.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright © 2024 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package config - -import ( - "context" - - "github.com/ory/kratos/embedx" - "github.com/ory/x/configx" - "github.com/ory/x/contextx" -) - -type ( - TestConfigProvider struct { - contextx.Contextualizer - Options []configx.OptionModifier - } - contextKey int -) - -func (t *TestConfigProvider) NewProvider(ctx context.Context, opts ...configx.OptionModifier) (*configx.Provider, error) { - return configx.New(ctx, []byte(embedx.ConfigSchema), append(t.Options, opts...)...) -} - -func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider { - config = t.Contextualizer.Config(ctx, config) - values, ok := ctx.Value(contextConfigKey).([]map[string]any) - if !ok { - return config - } - opts := make([]configx.OptionModifier, 0, len(values)) - for _, v := range values { - opts = append(opts, configx.WithValues(v)) - } - config, err := t.NewProvider(ctx, opts...) - if err != nil { - // This is not production code. The provider is only used in tests. - panic(err) - } - return config -} - -const contextConfigKey contextKey = 1 - -var ( - _ contextx.Contextualizer = (*TestConfigProvider)(nil) -) - -func WithConfigValue(ctx context.Context, key string, value any) context.Context { - return WithConfigValues(ctx, map[string]any{key: value}) -} - -func WithConfigValues(ctx context.Context, setValues map[string]any) context.Context { - values, ok := ctx.Value(contextConfigKey).([]map[string]any) - if !ok { - values = make([]map[string]any, 0) - } - newValues := make([]map[string]any, len(values), len(values)+1) - copy(newValues, values) - newValues = append(newValues, setValues) - - return context.WithValue(ctx, contextConfigKey, newValues) -} diff --git a/driver/config/testhelpers/config.go b/driver/config/testhelpers/config.go new file mode 100644 index 000000000000..6d0e4ba0b910 --- /dev/null +++ b/driver/config/testhelpers/config.go @@ -0,0 +1,152 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "context" + "net/http" + "net/http/httptest" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/embedx" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" +) + +type ( + TestConfigProvider struct { + contextx.Contextualizer + Options []configx.OptionModifier + } + contextKey int +) + +func (t *TestConfigProvider) NewProvider(ctx context.Context, opts ...configx.OptionModifier) (*configx.Provider, error) { + return configx.New(ctx, []byte(embedx.ConfigSchema), append(t.Options, opts...)...) +} + +func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider { + config = t.Contextualizer.Config(ctx, config) + values, ok := ctx.Value(contextConfigKey).([]map[string]any) + if !ok { + return config + } + opts := make([]configx.OptionModifier, 0, len(values)) + for _, v := range values { + opts = append(opts, configx.WithValues(v)) + } + config, err := t.NewProvider(ctx, opts...) + if err != nil { + // This is not production code. The provider is only used in tests. + panic(err) + } + return config +} + +const contextConfigKey contextKey = 1 + +var ( + _ contextx.Contextualizer = (*TestConfigProvider)(nil) +) + +func WithConfigValue(ctx context.Context, key string, value any) context.Context { + return WithConfigValues(ctx, map[string]any{key: value}) +} + +func WithConfigValues(ctx context.Context, setValues ...map[string]any) context.Context { + values, ok := ctx.Value(contextConfigKey).([]map[string]any) + if !ok { + values = make([]map[string]any, 0) + } + newValues := make([]map[string]any, len(values), len(values)+len(setValues)) + copy(newValues, values) + newValues = append(newValues, setValues...) + + return context.WithValue(ctx, contextConfigKey, newValues) +} + +type ConfigurableTestHandler struct { + configs map[uuid.UUID][]map[string]any + handler http.Handler +} + +func NewConfigurableTestHandler(h http.Handler) *ConfigurableTestHandler { + return &ConfigurableTestHandler{ + configs: make(map[uuid.UUID][]map[string]any), + handler: h, + } +} + +func (t *ConfigurableTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + cID := r.Header.Get("Test-Config-Id") + if config, ok := t.configs[uuid.FromStringOrNil(cID)]; ok { + r = r.WithContext(WithConfigValues(r.Context(), config...)) + } + t.handler.ServeHTTP(w, r) +} + +func (t *ConfigurableTestHandler) RegisterConfig(config ...map[string]any) uuid.UUID { + id := uuid.Must(uuid.NewV4()) + t.configs[id] = config + return id +} + +func (t *ConfigurableTestHandler) UseConfig(r *http.Request, id uuid.UUID) *http.Request { + r.Header.Set("Test-Config-Id", id.String()) + return r +} + +func (t *ConfigurableTestHandler) UseConfigValues(r *http.Request, values ...map[string]any) *http.Request { + return t.UseConfig(r, t.RegisterConfig(values...)) +} + +type ConfigurableTestServer struct { + *httptest.Server + handler *ConfigurableTestHandler + transport http.RoundTripper +} + +func NewConfigurableTestServer(h http.Handler) *ConfigurableTestServer { + handler := NewConfigurableTestHandler(h) + server := httptest.NewServer(handler) + + t := server.Client().Transport + cts := &ConfigurableTestServer{ + handler: handler, + Server: server, + transport: t, + } + server.Client().Transport = cts + return cts +} + +func (t *ConfigurableTestServer) RoundTrip(r *http.Request) (*http.Response, error) { + config, ok := r.Context().Value(contextConfigKey).([]map[string]any) + if ok && config != nil { + r = t.handler.UseConfigValues(r, config...) + } + return t.transport.RoundTrip(r) +} + +type AutoContextClient struct { + *http.Client + transport http.RoundTripper + ctx context.Context +} + +func (t *ConfigurableTestServer) Client(ctx context.Context) *AutoContextClient { + baseClient := *t.Server.Client() + autoClient := &AutoContextClient{ + Client: &baseClient, + transport: t, + ctx: ctx, + } + baseClient.Transport = autoClient + return autoClient +} + +func (c *AutoContextClient) RoundTrip(r *http.Request) (*http.Response, error) { + return c.transport.RoundTrip(r.WithContext(c.ctx)) +} diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go index 020517a41159..fa3e7772c62a 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -10,6 +10,8 @@ import ( "os" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/x/contextx" "github.com/ory/kratos/selfservice/flow/recovery" @@ -69,7 +71,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreVerificationHooks(ctx) @@ -110,7 +112,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostVerificationHooks(ctx) @@ -152,7 +154,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreRecoveryHooks(ctx) @@ -191,7 +193,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostRecoveryHooks(ctx) @@ -238,7 +240,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreRegistrationHooks(ctx) @@ -342,7 +344,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostRegistrationPostPersistHooks(ctx, identity.CredentialsTypePassword) @@ -384,7 +386,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreLoginHooks(ctx) @@ -486,7 +488,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostLoginHooks(ctx, identity.CredentialsTypePassword) @@ -528,7 +530,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreSettingsHooks(ctx) @@ -614,7 +616,7 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostSettingsPostPersistHooks(ctx, "profile") @@ -685,7 +687,7 @@ func TestDriverDefault_Strategies(t *testing.T) { t.Run(fmt.Sprintf("subcase=%s", tc.name), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) s := reg.RegistrationStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { @@ -758,7 +760,7 @@ func TestDriverDefault_Strategies(t *testing.T) { t.Run(fmt.Sprintf("run=%s", tc.name), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) s := reg.LoginStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { @@ -790,7 +792,7 @@ func TestDriverDefault_Strategies(t *testing.T) { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { t.Parallel() - ctx := config.WithConfigValues(ctx, tc.config) + ctx := confighelpers.WithConfigValues(ctx, tc.config) s := reg.RecoveryStrategies(ctx) require.Len(t, s, len(tc.expect)) @@ -912,7 +914,7 @@ func TestGetActiveRecoveryStrategy(t *testing.T) { _, reg := internal.NewVeryFastRegistryWithoutDB(t) t.Run("returns error if active strategy is disabled", func(t *testing.T) { - ctx := config.WithConfigValues(ctx, map[string]any{ + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ "selfservice.methods.code.enabled": false, config.ViperKeySelfServiceRecoveryUse: "code", }) @@ -926,7 +928,7 @@ func TestGetActiveRecoveryStrategy(t *testing.T) { "code", "link", } { t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { - ctx := config.WithConfigValues(ctx, map[string]any{ + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, config.ViperKeySelfServiceRecoveryUse: sID, }) @@ -944,7 +946,7 @@ func TestGetActiveVerificationStrategy(t *testing.T) { ctx := context.Background() _, reg := internal.NewVeryFastRegistryWithoutDB(t) t.Run("returns error if active strategy is disabled", func(t *testing.T) { - ctx := config.WithConfigValues(ctx, map[string]any{ + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ "selfservice.methods.code.enabled": false, config.ViperKeySelfServiceVerificationUse: "code", }) @@ -957,7 +959,7 @@ func TestGetActiveVerificationStrategy(t *testing.T) { "code", "link", } { t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { - ctx := config.WithConfigValues(ctx, map[string]any{ + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, config.ViperKeySelfServiceVerificationUse: sID, }) diff --git a/identity/test/pool.go b/identity/test/pool.go index bf6d114b8510..458c057da916 100644 --- a/identity/test/pool.go +++ b/identity/test/pool.go @@ -13,6 +13,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/x/crdbx" "github.com/go-faker/faker/v4" @@ -61,7 +63,7 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager, URL: urlx.ParseOrPanic("file://./stub/handler/multiple_emails.schema.json"), RawURL: "file://./stub/identity-2.schema.json", } - ctx := config.WithConfigValues(ctx, map[string]any{ + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ config.ViperKeyPublicBaseURL: exampleServerURL.String(), config.ViperKeyIdentitySchemas: []config.Schema{ { diff --git a/internal/driver.go b/internal/driver.go index 5d31cfe5ceb2..b95b2dc7c0a9 100644 --- a/internal/driver.go +++ b/internal/driver.go @@ -9,6 +9,8 @@ import ( "runtime" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/x/contextx" "github.com/sirupsen/logrus" @@ -56,7 +58,7 @@ func NewConfigurationWithDefaults(t testing.TB, opts ...configx.OptionModifier) }, opts...) c := config.MustNew(t, logrusx.New("", ""), os.Stderr, - &config.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: configOpts}, + &confighelpers.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: configOpts}, configOpts..., ) return c @@ -91,7 +93,7 @@ func NewRegistryDefaultWithDSN(t testing.TB, dsn string, opts ...configx.OptionM require.NoError(t, err) pool := jsonnetsecure.NewProcessPool(runtime.GOMAXPROCS(0)) t.Cleanup(pool.Close) - require.NoError(t, reg.Init(context.Background(), &config.TestConfigProvider{Contextualizer: &contextx.Default{}}, driver.SkipNetworkInit, driver.WithDisabledMigrationLogging(), driver.WithJsonnetPool(pool))) + require.NoError(t, reg.Init(context.Background(), &confighelpers.TestConfigProvider{Contextualizer: &contextx.Default{}}, driver.SkipNetworkInit, driver.WithDisabledMigrationLogging(), driver.WithJsonnetPool(pool))) require.NoError(t, reg.Persister().MigrateUp(context.Background())) // always migrate up actual, err := reg.Persister().DetermineNetwork(context.Background()) diff --git a/internal/testhelpers/config.go b/internal/testhelpers/config.go index 8e17a6ab3a12..b3450bda72fd 100644 --- a/internal/testhelpers/config.go +++ b/internal/testhelpers/config.go @@ -8,6 +8,8 @@ import ( "encoding/base64" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/spf13/pflag" "github.com/stretchr/testify/require" @@ -33,7 +35,7 @@ func DefaultIdentitySchemaConfig(url string) map[string]any { } func WithDefaultIdentitySchema(ctx context.Context, url string) context.Context { - return config.WithConfigValues(ctx, DefaultIdentitySchemaConfig(url)) + return confighelpers.WithConfigValues(ctx, DefaultIdentitySchemaConfig(url)) } // Deprecated: Use context-based WithDefaultIdentitySchema instead @@ -58,7 +60,7 @@ func WithAddIdentitySchema(ctx context.Context, t *testing.T, conf *config.Confi schemas, err := conf.IdentityTraitsSchemas(ctx) require.NoError(t, err) - return config.WithConfigValue(ctx, config.ViperKeyIdentitySchemas, append(schemas, config.Schema{ + return confighelpers.WithConfigValue(ctx, config.ViperKeyIdentitySchemas, append(schemas, config.Schema{ ID: id, URL: url, })), id diff --git a/persistence/sql/persister_hmac_test.go b/persistence/sql/persister_hmac_test.go index c569affa0cd9..fa1d6e479308 100644 --- a/persistence/sql/persister_hmac_test.go +++ b/persistence/sql/persister_hmac_test.go @@ -8,6 +8,8 @@ import ( "os" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/x/configx" "github.com/ory/x/contextx" @@ -70,7 +72,7 @@ func TestPersisterHMAC(t *testing.T) { baseSecret := "foobarbaz" baseSecretBytes := []byte(baseSecret) opts := []configx.OptionModifier{configx.SkipValidation(), configx.WithValue(config.ViperKeySecretsDefault, []string{baseSecret})} - conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, &config.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: opts}, opts...) + conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, &confighelpers.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: opts}, opts...) c, err := pop.NewConnection(&pop.ConnectionDetails{URL: "sqlite://foo?mode=memory"}) require.NoError(t, err) p, err := NewPersister(ctx, &logRegistryOnly{c: conf}, c) @@ -86,13 +88,13 @@ func TestPersisterHMAC(t *testing.T) { newSecret := "not" + baseSecret t.Run("case=with only new sectet", func(t *testing.T) { - ctx = config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret}) + ctx = confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret}) assert.NotEqual(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "hashme")) assert.Equal(t, hmacValueWithSecret(ctx, "hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme")) }) t.Run("case=with new and old secret", func(t *testing.T) { - ctx = config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret, baseSecret}) + ctx = confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret, baseSecret}) assert.Equal(t, hmacValueWithSecret(ctx, "hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme")) assert.NotEqual(t, hash, p.hmacValue(ctx, "hashme")) }) diff --git a/selfservice/strategy/code/test/persistence.go b/selfservice/strategy/code/test/persistence.go index e7648cb055b3..d7600e9fbd9a 100644 --- a/selfservice/strategy/code/test/persistence.go +++ b/selfservice/strategy/code/test/persistence.go @@ -8,6 +8,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/flow" @@ -31,7 +33,7 @@ func TestPersister(ctx context.Context, p interface { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - ctx := config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) + ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("code=recovery", func(t *testing.T) { newRecoveryCodeDTO := func(t *testing.T, email string) (*code.CreateRecoveryCodeParams, *recovery.Flow, *identity.RecoveryAddress) { @@ -50,7 +52,7 @@ func TestPersister(ctx context.Context, p interface { require.NoError(t, p.CreateIdentity(ctx, &i)) return &code.CreateRecoveryCodeParams{ - RawCode: string(randx.MustString(8, randx.Numeric)), + RawCode: randx.MustString(8, randx.Numeric), FlowID: f.ID, RecoveryAddress: &i.RecoveryAddresses[0], ExpiresIn: time.Minute, diff --git a/selfservice/strategy/link/test/persistence.go b/selfservice/strategy/link/test/persistence.go index a77a1db1d9c8..c28250836775 100644 --- a/selfservice/strategy/link/test/persistence.go +++ b/selfservice/strategy/link/test/persistence.go @@ -8,6 +8,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/flow" @@ -35,7 +37,7 @@ func TestPersister(ctx context.Context, p interface { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - ctx := config.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) + ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("token=recovery", func(t *testing.T) { newRecoveryToken := func(t *testing.T, email string) (*link.RecoveryToken, *recovery.Flow) { diff --git a/session/test/persistence.go b/session/test/persistence.go index 0b709a3866b8..d124aa23eb4f 100644 --- a/session/test/persistence.go +++ b/session/test/persistence.go @@ -8,6 +8,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/pkg/errors" "golang.org/x/sync/errgroup" @@ -609,7 +611,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { }) t.Run("extend session lifespan but min time is not yet reached", func(t *testing.T) { - ctx := config.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) + ctx := confighelpers.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) var expected session.Session require.NoError(t, faker.FakeData(&expected)) @@ -624,7 +626,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { }) t.Run("extend session lifespan", func(t *testing.T) { - ctx := config.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) + ctx := confighelpers.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) var expected session.Session require.NoError(t, faker.FakeData(&expected)) @@ -644,7 +646,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { t.Skip("Skipping test because driver is not CockroachDB") } - ctx := config.WithConfigValue(ctx, config.ViperKeySessionRefreshMinTimeLeft, 2*time.Hour) + ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySessionRefreshMinTimeLeft, 2*time.Hour) var expected session.Session require.NoError(t, faker.FakeData(&expected)) From af5ea35759e74d7a1637823abcc21dc8e3e39a9d Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Thu, 20 Jun 2024 13:02:49 +0200 Subject: [PATCH 07/71] feat: clarify session extend behavior (#3962) --- internal/client-go/api_identity.go | 6 ++++++ internal/httpclient/api_identity.go | 6 ++++++ session/handler.go | 3 +++ spec/api.json | 2 +- spec/swagger.json | 2 +- 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index 62f5f80b36c2..b1201db57750 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -161,6 +161,9 @@ type IdentityApi interface { return a 200 OK response with the session in the body. Returning the session as part of the response will be deprecated in the future and should not be relied upon. + This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those + scenarios. This endpoint also returns 404 errors if the session does not exist. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. @@ -1494,6 +1497,9 @@ This endpoint returns per default a 204 No Content response on success. Older Or return a 200 OK response with the session in the body. Returning the session as part of the response will be deprecated in the future and should not be relied upon. +This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those +scenarios. This endpoint also returns 404 errors if the session does not exist. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the session's ID. diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index 62f5f80b36c2..b1201db57750 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -161,6 +161,9 @@ type IdentityApi interface { return a 200 OK response with the session in the body. Returning the session as part of the response will be deprecated in the future and should not be relied upon. + This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those + scenarios. This endpoint also returns 404 errors if the session does not exist. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. @@ -1494,6 +1497,9 @@ This endpoint returns per default a 204 No Content response on success. Older Or return a 200 OK response with the session in the body. Returning the session as part of the response will be deprecated in the future and should not be relied upon. +This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those +scenarios. This endpoint also returns 404 errors if the session does not exist. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the session's ID. diff --git a/session/handler.go b/session/handler.go index 9dab8860c773..84be9c25e1b8 100644 --- a/session/handler.go +++ b/session/handler.go @@ -877,6 +877,9 @@ type extendSession struct { // return a 200 OK response with the session in the body. Returning the session as part of the response // will be deprecated in the future and should not be relied upon. // +// This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those +// scenarios. This endpoint also returns 404 errors if the session does not exist. +// // Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. // // Schemes: http, https diff --git a/spec/api.json b/spec/api.json index 084bbfa5ea47..582e44a779f1 100644 --- a/spec/api.json +++ b/spec/api.json @@ -4969,7 +4969,7 @@ }, "/admin/sessions/{id}/extend": { "patch": { - "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", + "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nThis endpoint ignores consecutive requests to extend the same session and returns a 404 error in those\nscenarios. This endpoint also returns 404 errors if the session does not exist.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", "operationId": "extendSession", "parameters": [ { diff --git a/spec/swagger.json b/spec/swagger.json index 8ccd39801919..e77bfefa6307 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -1188,7 +1188,7 @@ "oryAccessToken": [] } ], - "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", + "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nThis endpoint ignores consecutive requests to extend the same session and returns a 404 error in those\nscenarios. This endpoint also returns 404 errors if the session does not exist.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", "schemes": [ "http", "https" From a43cef23c177acddbf8b03afef087feeaca51981 Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Tue, 25 Jun 2024 11:21:17 +0200 Subject: [PATCH 08/71] feat: allow deletion of an individual OIDC credential (#3968) This extends the existing `DELETE /admin/identities/{id}/credentials/{type}` API to accept an `?identifier=foobar` query parameter for `{type}==oidc` like such: `DELETE /admin/identities/{id}/credentials/oidc?identifier=github%3A012345` This will delete the GitHub OIDC credential with the identifier `github:012345` (`012345` is the subject as returned by GitHub). To find out which OIDC credentials exist, call `GET /admin/identities/{id}?include_credential=oidc` beforehand. This will allow you to delete individual OIDC credentials for users even if they have several set up. --- identity/handler.go | 67 ++++++---------------- identity/handler_test.go | 118 ++++++++++++++++++++++++++++---------- identity/identity.go | 74 ++++++++++++++++++++++++ identity/identity_test.go | 72 +++++++++++++++++++---- 4 files changed, 243 insertions(+), 88 deletions(-) diff --git a/identity/handler.go b/identity/handler.go index 3ac641e09377..cf4d3ff835e6 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -910,69 +910,36 @@ func (h *Handler) patch(w http.ResponseWriter, r *http.Request, ps httprouter.Pa h.r.Writer().Write(w, r, WithCredentialsMetadataAndAdminMetadataInJSON(updatedIdentity)) } -func deletCredentialWebAuthFromIdentity(identity *Identity) (*Identity, error) { - cred, ok := identity.GetCredentials(CredentialsTypeWebAuthn) - if !ok { - // This should never happend as it's checked earlier in the code; - // But we never know... - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("You tried to remove a CredentialsTypeWebAuthn but this user have no CredentialsTypeWebAuthn set up.")) - } - - var cc CredentialsWebAuthnConfig - if err := json.Unmarshal(cred.Config, &cc); err != nil { - // Database has been tampered or the json schema are incompatible (migration issue); - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) - } - - updated := make([]CredentialWebAuthn, 0) - for k, cred := range cc.Credentials { - if cred.IsPasswordless { - updated = append(updated, cc.Credentials[k]) - } - } - - if len(updated) == 0 { - identity.DeleteCredentialsType(CredentialsTypeWebAuthn) - return identity, nil - } - - cc.Credentials = updated - message, err := json.Marshal(cc) - if err != nil { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())) - } - - cred.Config = message - identity.SetCredentials(CredentialsTypeWebAuthn, *cred) - return identity, nil -} - // Delete Credential Parameters // // swagger:parameters deleteIdentityCredentials -// -//nolint:deadcode,unused -//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions -type deleteIdentityCredentials struct { +type _ struct { // ID is the identity's ID. // // required: true // in: path ID string `json:"id"` - // Type is the type of credentials to be deleted. + // Type is the type of credentials to delete. // // required: true // in: path Type CredentialsType `json:"type"` + + // Identifier is the identifier of the OIDC credential to delete. + // Find the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint. + // + // required: false + // in: query + Identifier string `json:"identifier"` } // swagger:route DELETE /admin/identities/{id}/credentials/{type} identity deleteIdentityCredentials // // # Delete a credential for a specific identity // -// Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type -// You can only delete second factor (aal2) credentials. +// Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. +// You cannot delete password or code auth credentials through this API. // // Consumes: // - application/json @@ -1006,14 +973,18 @@ func (h *Handler) deleteIdentityCredentials(w http.ResponseWriter, r *http.Reque case CredentialsTypeLookup, CredentialsTypeTOTP: identity.DeleteCredentialsType(cred.Type) case CredentialsTypeWebAuthn: - identity, err = deletCredentialWebAuthFromIdentity(identity) - if err != nil { + if err = identity.deleteCredentialWebAuthFromIdentity(); err != nil { h.r.Writer().WriteError(w, r, err) return } - case CredentialsTypeOIDC, CredentialsTypePassword, CredentialsTypeCodeAuth: - h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("You can't remove first factor credentials."))) + case CredentialsTypePassword, CredentialsTypeCodeAuth: + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("You cannot remove first factor credentials."))) return + case CredentialsTypeOIDC: + if err := identity.deleteCredentialOIDCFromIdentity(r.URL.Query().Get("identifier")); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } default: h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unknown credentials type %s.", cred.Type))) return diff --git a/identity/handler_test.go b/identity/handler_test.go index ab2ed0c0cbdc..53ca219b231b 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -32,6 +32,8 @@ import ( "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/schema" "github.com/ory/kratos/x" + "github.com/ory/x/ioutilx" + "github.com/ory/x/randx" "github.com/ory/x/snapshotx" "github.com/ory/x/sqlxx" "github.com/ory/x/urlx" @@ -81,8 +83,9 @@ func TestHandler(t *testing.T) { res, err := base.Client().Do(req) require.NoError(t, err) + defer res.Body.Close() - require.EqualValues(t, expectCode, res.StatusCode) + require.EqualValues(t, expectCode, res.StatusCode, "%s", ioutilx.MustReadAll(res.Body)) } send := func(t *testing.T, base *httptest.Server, method, href string, expectCode int, send interface{}) gjson.Result { @@ -1497,15 +1500,15 @@ func TestHandler(t *testing.T) { t.Run("case=should delete credential of a specific user and no longer be able to retrieve it", func(t *testing.T) { ignoreDefault := []string{"id", "schema_url", "state_changed_at", "created_at", "updated_at"} - createIdentity := func(identities map[identity.CredentialsType]string) func(t *testing.T) *identity.Identity { + type M = map[identity.CredentialsType]identity.Credentials + createIdentity := func(creds M) func(*testing.T) *identity.Identity { return func(t *testing.T) *identity.Identity { i := identity.NewIdentity("") - for ct, config := range identities { - i.SetCredentials(ct, identity.Credentials{ - Type: ct, - Config: sqlxx.JSONRawMessage(config), - }) + for k, v := range creds { + v.Type = k + creds[k] = v } + i.Credentials = creds i.Traits = identity.Traits("{}") require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i)) return i @@ -1516,26 +1519,83 @@ func TestHandler(t *testing.T) { remove(t, ts, "/identities/"+x.NewUUID().String()+"/credentials/azerty", http.StatusNotFound) }) t.Run("type=remove unknown type/"+name, func(t *testing.T) { - i := createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, + i := createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, })(t) remove(t, ts, "/identities/"+i.ID.String()+"/credentials/azerty", http.StatusNotFound) }) t.Run("type=remove password type/"+name, func(t *testing.T) { - i := createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, + i := createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, })(t) remove(t, ts, "/identities/"+i.ID.String()+"/credentials/password", http.StatusBadRequest) }) t.Run("type=remove oidc type/"+name, func(t *testing.T) { - i := createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypeOIDC: `{"id":"pst"}`, + // force ordering among github identifiers + githubSubject := "0" + randx.MustString(7, randx.Numeric) + githubSubject2 := "1" + randx.MustString(7, randx.Numeric) + googleSubject := randx.MustString(8, randx.Numeric) + initialConfig := []byte(fmt.Sprintf(`{ + "providers": [ + { + "subject": %q, + "provider": "github" + }, + { + "subject": %q, + "provider": "github" + }, + { + "subject": %q, + "provider": "google" + } + ] + }`, githubSubject, githubSubject2, googleSubject)) + identifiers := []string{ + identity.OIDCUniqueID("github", githubSubject), + identity.OIDCUniqueID("github", githubSubject2), + identity.OIDCUniqueID("google", googleSubject), + } + i := createIdentity(M{ + identity.CredentialsTypeOIDC: { + Identifiers: identifiers, + Config: initialConfig, + }, })(t) - remove(t, ts, "/identities/"+i.ID.String()+"/credentials/oidc", http.StatusBadRequest) + res := get(t, ts, "/identities/"+i.ID.String()+"?include_credential=oidc", http.StatusOK) + assert.EqualValues(t, i.ID.String(), res.Get("id").String(), "%s", res.Raw) + assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 3, "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.0").String(), identifiers[0], "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.1").String(), identifiers[1], "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.2").String(), identifiers[2], "%s", res.Raw) + + oidConfig := gjson.Parse(res.Get("credentials.oidc.config").String()) + assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 3, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.0.provider").String(), "github", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.0.subject").String(), githubSubject, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.1.provider").String(), "github", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.1.subject").String(), githubSubject2, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.2.provider").String(), "google", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.2.subject").String(), googleSubject, "%s", res.Raw) + + remove(t, ts, "/identities/"+i.ID.String()+"/credentials/oidc?identifier="+identifiers[1], http.StatusNoContent) + res = get(t, ts, "/identities/"+i.ID.String()+"?include_credential=oidc", http.StatusOK) + + assert.EqualValues(t, i.ID.String(), res.Get("id").String(), "%s", res.Raw) + assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 2, "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.0").String(), identifiers[0], "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.1").String(), identifiers[2], "%s", res.Raw) + + oidConfig = gjson.Parse(res.Get("credentials.oidc.config").String()) + assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 2, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.0.provider").String(), "github", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.0.subject").String(), githubSubject, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.1.provider").String(), "google", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.1.subject").String(), googleSubject, "%s", res.Raw) }) t.Run("type=remove webauthn passwordless type/"+name, func(t *testing.T) { expected := `{"credentials":[{"id":"THTndqZP5Mjvae1BFvJMaMfEMm7O7HE1ju+7PBaYA7Y=","added_at":"2022-12-16T14:11:55Z","public_key":"pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU=","display_name":"test","authenticator":{"aaguid":"rc4AAjW8xgpkiwsl8fBVAw==","sign_count":0,"clone_warning":false},"is_passwordless":true,"attestation_type":"none"}],"user_handle":"Ef5JiMpMRwuzauWs/9J0gQ=="}` - i := createIdentity(map[identity.CredentialsType]string{identity.CredentialsTypeWebAuthn: expected})(t) + i := createIdentity(M{identity.CredentialsTypeWebAuthn: {Config: []byte(expected)}})(t) remove(t, ts, "/identities/"+i.ID.String()+"/credentials/webauthn", http.StatusNoContent) // Check that webauthn has not been deleted res := get(t, ts, "/identities/"+i.ID.String(), http.StatusOK) @@ -1608,7 +1668,7 @@ func TestHandler(t *testing.T) { message, err := json.Marshal(config) require.NoError(t, err) - i := createIdentity(map[identity.CredentialsType]string{identity.CredentialsTypeWebAuthn: string(message)})(t) + i := createIdentity(M{identity.CredentialsTypeWebAuthn: {Config: message}})(t) remove(t, ts, "/identities/"+i.ID.String()+"/credentials/webauthn", http.StatusNoContent) // Check that webauthn has not been deleted res := get(t, ts, "/identities/"+i.ID.String(), http.StatusOK) @@ -1618,10 +1678,10 @@ func TestHandler(t *testing.T) { require.NoError(t, err) snapshotx.SnapshotT(t, identity.WithCredentialsAndAdminMetadataInJSON(*actual), snapshotx.ExceptNestedKeys(append(ignoreDefault, "hashed_password")...), snapshotx.ExceptPaths("credentials.oidc.identifiers")) }) - for ct, ctConf := range map[identity.CredentialsType]string{ - identity.CredentialsTypeLookup: `{"recovery_codes": [{"code": "aaa"}]}`, - identity.CredentialsTypeTOTP: `{"totp_url":"otpauth://totp/test"}`, - identity.CredentialsTypeWebAuthn: `{"credentials":[{"id":"THTndqZP5Mjvae1BFvJMaMfEMm7O7HE1ju+7PBaYA7Y=","added_at":"2022-12-16T14:11:55Z","public_key":"pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU=","display_name":"test","authenticator":{"aaguid":"rc4AAjW8xgpkiwsl8fBVAw==","sign_count":0,"clone_warning":false},"is_passwordless":false,"attestation_type":"none"}],"user_handle":"Ef5JiMpMRwuzauWs/9J0gQ=="}`, + for ct, ctConf := range map[identity.CredentialsType][]byte{ + identity.CredentialsTypeLookup: []byte(`{"recovery_codes": [{"code": "aaa"}]}`), + identity.CredentialsTypeTOTP: []byte(`{"totp_url":"otpauth://totp/test"}`), + identity.CredentialsTypeWebAuthn: []byte(`{"credentials":[{"id":"THTndqZP5Mjvae1BFvJMaMfEMm7O7HE1ju+7PBaYA7Y=","added_at":"2022-12-16T14:11:55Z","public_key":"pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU=","display_name":"test","authenticator":{"aaguid":"rc4AAjW8xgpkiwsl8fBVAw==","sign_count":0,"clone_warning":false},"is_passwordless":false,"attestation_type":"none"}],"user_handle":"Ef5JiMpMRwuzauWs/9J0gQ=="}`), } { t.Run("type=remove "+string(ct)+"/"+name, func(t *testing.T) { for _, tc := range []struct { @@ -1632,25 +1692,25 @@ func TestHandler(t *testing.T) { { desc: "with", exist: true, - setup: createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, - ct: ctConf, + setup: createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, + ct: {Config: ctConf}, }), }, { desc: "without", exist: false, - setup: createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, + setup: createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, }), }, { desc: "multiple", exist: true, - setup: createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, - identity.CredentialsTypeOIDC: `{"id":"pst"}`, - ct: ctConf, + setup: createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, + identity.CredentialsTypeOIDC: {Config: []byte(`{"id":"pst"}`)}, + ct: {Config: ctConf}, }), }, } { diff --git a/identity/identity.go b/identity/identity.go index 55dd66155ea9..3f692b831f2b 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -507,6 +507,80 @@ func (i *Identity) WithDeclassifiedCredentials(ctx context.Context, c cipher.Pro return &ii, nil } +func (i *Identity) deleteCredentialWebAuthFromIdentity() error { + cred, ok := i.GetCredentials(CredentialsTypeWebAuthn) + if !ok { + // This should never happend as it's checked earlier in the code; + // But we never know... + return errors.WithStack(herodot.ErrNotFound.WithReasonf("You tried to remove a WebAuthn credential but this user has no such credential set up.")) + } + + var cc CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &cc); err != nil { + // Database has been tampered or the json schema are incompatible (migration issue); + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) + } + + updated := make([]CredentialWebAuthn, 0) + for k, cred := range cc.Credentials { + if cred.IsPasswordless { + updated = append(updated, cc.Credentials[k]) + } + } + + if len(updated) == 0 { + i.DeleteCredentialsType(CredentialsTypeWebAuthn) + return nil + } + + cc.Credentials = updated + message, err := json.Marshal(cc) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())) + } + + cred.Config = message + i.SetCredentials(CredentialsTypeWebAuthn, *cred) + return nil +} + +func (i *Identity) deleteCredentialOIDCFromIdentity(identifierToDelete string) error { + if identifierToDelete == "" { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("You must provide an identifier to delete this credential.")) + } + _, hasOIDC := i.GetCredentials(CredentialsTypeOIDC) + if !hasOIDC { + return errors.WithStack(herodot.ErrNotFound.WithReasonf("You tried to remove an OIDC credential but this user has no such credential set up.")) + } + var oidcConfig CredentialsOIDC + creds, err := i.ParseCredentials(CredentialsTypeOIDC, &oidcConfig) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) + } + + var updatedIdentifiers []string + var updatedProviders []CredentialsOIDCProvider + var found bool + for _, cfg := range oidcConfig.Providers { + if identifierToDelete == OIDCUniqueID(cfg.Provider, cfg.Subject) { + found = true + continue + } + updatedIdentifiers = append(updatedIdentifiers, OIDCUniqueID(cfg.Provider, cfg.Subject)) + updatedProviders = append(updatedProviders, cfg) + } + if !found { + return errors.WithStack(herodot.ErrNotFound.WithReasonf("The identifier `%s` was not found among OIDC credentials.", identifierToDelete)) + } + creds.Identifiers = updatedIdentifiers + creds.Config, err = json.Marshal(&CredentialsOIDC{Providers: updatedProviders}) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())) + } + i.Credentials[CredentialsTypeOIDC] = *creds + return nil +} + // Patch Identities Parameters // // swagger:parameters batchPatchIdentities diff --git a/identity/identity_test.go b/identity/identity_test.go index 726011fd00eb..d14388feacd4 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -10,21 +10,16 @@ import ( "fmt" "testing" - "github.com/ory/x/snapshotx" - - "github.com/ory/kratos/cipher" - "github.com/ory/kratos/x" - + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" - "github.com/gofrs/uuid" - - "github.com/ory/x/sqlxx" - + "github.com/ory/kratos/cipher" "github.com/ory/kratos/driver/config" - - "github.com/stretchr/testify/assert" + "github.com/ory/kratos/x" + "github.com/ory/x/snapshotx" + "github.com/ory/x/sqlxx" ) func TestNewIdentity(t *testing.T) { @@ -387,3 +382,58 @@ func TestWithDeclassifiedCredentials(t *testing.T) { } }) } + +func TestDeleteCredentialOIDCFromIdentity(t *testing.T) { + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + + err := i.deleteCredentialOIDCFromIdentity("") + assert.Error(t, err) + err = i.deleteCredentialOIDCFromIdentity("does-not-exist") + assert.Error(t, err) + + credentials := map[CredentialsType]Credentials{ + CredentialsTypePassword: { + Identifiers: []string{"zab", "bar"}, + Type: CredentialsTypePassword, + Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}"), + }, + CredentialsTypeOIDC: { + Type: CredentialsTypeOIDC, + Identifiers: []string{"bar:1234", "baz:5678"}, + Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "bar", "subject": "1234"}, {"provider": "baz", "subject": "5678"}]}`), + }, + CredentialsTypeWebAuthn: { + Type: CredentialsTypeWebAuthn, + Identifiers: []string{"foo", "bar"}, + Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}"), + }, + } + i.Credentials = credentials + + err = i.deleteCredentialOIDCFromIdentity("zab") + assert.Error(t, err) + err = i.deleteCredentialOIDCFromIdentity("foo") + assert.Error(t, err) + err = i.deleteCredentialOIDCFromIdentity("bar") + assert.Error(t, err, "matches multiple OIDC credentials") + + require.NoError(t, i.deleteCredentialOIDCFromIdentity("bar:1234")) + + assert.Len(t, i.Credentials, 3) + + assert.Contains(t, i.Credentials, CredentialsTypePassword) + assert.EqualValues(t, i.Credentials[CredentialsTypePassword].Identifiers, []string{"zab", "bar"}) + + assert.Contains(t, i.Credentials, CredentialsTypeWebAuthn) + assert.EqualValues(t, i.Credentials[CredentialsTypeWebAuthn].Identifiers, []string{"foo", "bar"}) + + assert.Contains(t, i.Credentials, CredentialsTypeOIDC) + + oidc, ok := i.GetCredentials(CredentialsTypeOIDC) + require.True(t, ok) + assert.EqualValues(t, oidc.Identifiers, []string{"baz:5678"}) + var cfg CredentialsOIDC + _, err = i.ParseCredentials(CredentialsTypeOIDC, &cfg) + require.NoError(t, err) + assert.EqualValues(t, CredentialsOIDC{Providers: []CredentialsOIDCProvider{{Provider: "baz", Subject: "5678"}}}, cfg) +} From 7df3d561debb92de59b38a4e0f2c13d4f1e3f091 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Tue, 25 Jun 2024 09:22:48 +0000 Subject: [PATCH 09/71] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/api_identity.go | 21 +++++++++++++++------ internal/httpclient/api_identity.go | 21 +++++++++++++++------ spec/api.json | 12 ++++++++++-- spec/swagger.json | 10 ++++++++-- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index b1201db57750..b48819525a13 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -110,11 +110,11 @@ type IdentityApi interface { /* * DeleteIdentityCredentials Delete a credential for a specific identity - * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type - You can only delete second factor (aal2) credentials. + * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. + You cannot delete password or code auth credentials through this API. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + * @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode * @return IdentityApiApiDeleteIdentityCredentialsRequest */ DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest @@ -1071,6 +1071,12 @@ type IdentityApiApiDeleteIdentityCredentialsRequest struct { ApiService IdentityApi id string type_ string + identifier *string +} + +func (r IdentityApiApiDeleteIdentityCredentialsRequest) Identifier(identifier string) IdentityApiApiDeleteIdentityCredentialsRequest { + r.identifier = &identifier + return r } func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Response, error) { @@ -1079,12 +1085,12 @@ func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Respons /* - DeleteIdentityCredentials Delete a credential for a specific identity - - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type + - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. -You can only delete second factor (aal2) credentials. +You cannot delete password or code auth credentials through this API. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the identity's ID. - - @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + - @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode - @return IdentityApiApiDeleteIdentityCredentialsRequest */ func (a *IdentityApiService) DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest { @@ -1121,6 +1127,9 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe localVarQueryParams := url.Values{} localVarFormParams := url.Values{} + if r.identifier != nil { + localVarQueryParams.Add("identifier", parameterToString(*r.identifier, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index b1201db57750..b48819525a13 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -110,11 +110,11 @@ type IdentityApi interface { /* * DeleteIdentityCredentials Delete a credential for a specific identity - * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type - You can only delete second factor (aal2) credentials. + * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. + You cannot delete password or code auth credentials through this API. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + * @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode * @return IdentityApiApiDeleteIdentityCredentialsRequest */ DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest @@ -1071,6 +1071,12 @@ type IdentityApiApiDeleteIdentityCredentialsRequest struct { ApiService IdentityApi id string type_ string + identifier *string +} + +func (r IdentityApiApiDeleteIdentityCredentialsRequest) Identifier(identifier string) IdentityApiApiDeleteIdentityCredentialsRequest { + r.identifier = &identifier + return r } func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Response, error) { @@ -1079,12 +1085,12 @@ func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Respons /* - DeleteIdentityCredentials Delete a credential for a specific identity - - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type + - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. -You can only delete second factor (aal2) credentials. +You cannot delete password or code auth credentials through this API. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the identity's ID. - - @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + - @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode - @return IdentityApiApiDeleteIdentityCredentialsRequest */ func (a *IdentityApiService) DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest { @@ -1121,6 +1127,9 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe localVarQueryParams := url.Values{} localVarFormParams := url.Values{} + if r.identifier != nil { + localVarQueryParams.Add("identifier", parameterToString(*r.identifier, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/spec/api.json b/spec/api.json index 582e44a779f1..f8a84f6f7d97 100644 --- a/spec/api.json +++ b/spec/api.json @@ -4355,7 +4355,7 @@ }, "/admin/identities/{id}/credentials/{type}": { "delete": { - "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type\nYou can only delete second factor (aal2) credentials.", + "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.\nYou cannot delete password or code auth credentials through this API.", "operationId": "deleteIdentityCredentials", "parameters": [ { @@ -4368,7 +4368,7 @@ } }, { - "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Type is the type of credentials to delete.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "in": "path", "name": "type", "required": true, @@ -4388,6 +4388,14 @@ "type": "string" }, "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + }, + { + "description": "Identifier is the identifier of the OIDC credential to delete.\nFind the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint.", + "in": "query", + "name": "identifier", + "schema": { + "type": "string" + } } ], "responses": { diff --git a/spec/swagger.json b/spec/swagger.json index e77bfefa6307..570cb4003d62 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -662,7 +662,7 @@ "oryAccessToken": [] } ], - "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type\nYou can only delete second factor (aal2) credentials.", + "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.\nYou cannot delete password or code auth credentials through this API.", "consumes": [ "application/json" ], @@ -701,10 +701,16 @@ ], "type": "string", "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", - "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Type is the type of credentials to delete.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "name": "type", "in": "path", "required": true + }, + { + "type": "string", + "description": "Identifier is the identifier of the OIDC credential to delete.\nFind the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint.", + "name": "identifier", + "in": "query" } ], "responses": { From c5089801af2a656e9c1fc371a11aeb23918ba359 Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Mon, 1 Jul 2024 12:31:14 +0200 Subject: [PATCH 10/71] docs: typo in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a081e3be7f..58628071c945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -329,7 +329,7 @@ This feature enables two-step registration per default. Two-step registration is a significantly improved sign up flow and recommended when using more than one sign up methods. To disable two-step registration, set -`selfservice.flows.registration.enable_legacy_flow` to `true`. This value +`selfservice.flows.registration.enable_legacy_one_step` to `true`. This value defaults to `false`. ### Bug Fixes From 7c5299f1f832ebbe0622d0920b7a91253d26b06c Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Tue, 2 Jul 2024 10:23:22 +0200 Subject: [PATCH 11/71] fix: jsonnet timeouts (#3979) --- corpx/faker.go | 6 +- go.mod | 38 ++++++------- go.sum | 85 +++++++++++++++------------- persistence/sql/persister_session.go | 5 +- session/session.go | 8 +-- 5 files changed, 74 insertions(+), 68 deletions(-) diff --git a/corpx/faker.go b/corpx/faker.go index e8fc4b0e388f..ec54a252ab6b 100644 --- a/corpx/faker.go +++ b/corpx/faker.go @@ -17,8 +17,8 @@ import ( "github.com/ory/kratos/session" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" + "github.com/ory/x/pointerx" "github.com/ory/x/randx" - "github.com/ory/x/stringsx" ) var setup sync.Once @@ -31,13 +31,13 @@ func registerFakes() { _ = faker.SetRandomMapAndSliceSize(4) if err := faker.AddProvider("ptr_geo_location", func(v reflect.Value) (interface{}, error) { - return stringsx.GetPointer("Munich, Germany"), nil + return pointerx.Ptr("Munich, Germany"), nil }); err != nil { panic(err) } if err := faker.AddProvider("ptr_ipv4", func(v reflect.Value) (interface{}, error) { - return stringsx.GetPointer(faker.IPv4()), nil + return pointerx.Ptr(faker.IPv4()), nil }); err != nil { panic(err) } diff --git a/go.mod b/go.mod index 67e7a524c134..47238d202c79 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6 github.com/dgraph-io/ristretto v0.1.1 - github.com/fatih/color v1.13.0 + github.com/fatih/color v1.16.0 github.com/ghodss/yaml v1.0.0 github.com/go-crypt/crypt v0.2.9 github.com/go-faker/faker/v4 v4.2.0 @@ -50,7 +50,7 @@ require ( github.com/gorilla/sessions v1.2.1 github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69 github.com/hashicorp/consul/api v1.20.0 - github.com/hashicorp/go-retryablehttp v0.7.2 + github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/golang-lru v0.5.4 github.com/imdario/mergo v0.3.13 github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf @@ -69,7 +69,7 @@ require ( github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe github.com/ory/analytics-go/v5 v5.0.1 github.com/ory/client-go v0.2.0-alpha.60 - github.com/ory/dockertest/v3 v3.9.1 + github.com/ory/dockertest/v3 v3.10.1-0.20240619125955-3328cf9343b8 github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe github.com/ory/graceful v0.1.4-0.20230301144740-e222150c51d0 github.com/ory/herodot v0.10.3-0.20230626083119-d7e5192f0d88 @@ -77,7 +77,7 @@ require ( github.com/ory/jsonschema/v3 v3.0.8 github.com/ory/mail/v3 v3.0.0 github.com/ory/nosurf v1.2.7 - github.com/ory/x v0.0.623 + github.com/ory/x v0.0.639 github.com/peterhellberg/link v1.2.0 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 @@ -85,7 +85,7 @@ require ( github.com/rakutentech/jwk-go v1.1.3 github.com/rs/cors v1.8.2 github.com/samber/lo v1.37.0 - github.com/sirupsen/logrus v1.9.0 + github.com/sirupsen/logrus v1.9.3 github.com/slack-go/slack v0.7.4 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 @@ -110,11 +110,11 @@ require ( ) require ( - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect - github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/a8m/envsubst v1.3.0 // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect @@ -126,17 +126,17 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect github.com/boombuler/barcode v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cockroachdb/cockroach-go/v2 v2.3.5 - github.com/containerd/continuity v0.3.0 // indirect + github.com/containerd/continuity v0.4.3 // indirect github.com/cortesi/moddwatch v0.0.0-20210222043437-a6aaad86a36e // indirect github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/docker/cli v20.10.21+incompatible // indirect + github.com/docker/cli v24.0.9+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/docker v20.10.27+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/elliotchance/orderedmap v1.4.0 // indirect @@ -162,7 +162,7 @@ require ( github.com/go-openapi/validate v0.22.1 // indirect github.com/go-playground/locales v0.13.0 // indirect github.com/go-playground/universal-translator v0.17.0 // indirect - github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-webauthn/x v0.1.4 // indirect github.com/gobuffalo/envy v1.10.2 // indirect github.com/gobuffalo/flect v1.0.0 // indirect @@ -195,7 +195,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.2.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -233,7 +233,7 @@ require ( github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect - github.com/lib/pq v1.10.7 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailhog/MailHog-Server v1.0.1 // indirect github.com/mailhog/MailHog-UI v1.0.1 // indirect @@ -244,21 +244,21 @@ require ( github.com/mailhog/storage v1.0.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/microcosm-cc/bluemonday v1.0.21 // indirect + github.com/microcosm-cc/bluemonday v1.0.26 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect + github.com/moby/term v0.5.0 // indirect github.com/nyaruka/phonenumbers v1.3.6 // indirect github.com/ogier/pflag v0.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc2 // indirect - github.com/opencontainers/runc v1.1.12 // indirect + github.com/opencontainers/runc v1.1.13 // indirect github.com/openzipkin/zipkin-go v0.4.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect @@ -308,7 +308,7 @@ require ( go.opentelemetry.io/otel/metric v1.22.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/sys v0.21.0 // indirect golang.org/x/term v0.19.0 // indirect golang.org/x/tools v0.16.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect diff --git a/go.sum b/go.sum index 85e85e83626c..33830713d3b9 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f code.dny.dev/ssrf v0.2.0 h1:wCBP990rQQ1CYfRpW+YK1+8xhwUjv189AQ3WMo1jQaI= code.dny.dev/ssrf v0.2.0/go.mod h1:B+91l25OnyaLIeCx0WRJN5qfJ/4/ZTZxRXgm0lj/2w8= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -52,8 +52,8 @@ github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7Y github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= -github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= -github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -105,8 +105,8 @@ github.com/bwmarrin/discordgo v0.23.0 h1://ARp8qUrRZvDGMkfAjtcC20WOvsMtTgi+KrdKn github.com/bwmarrin/discordgo v0.23.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -125,8 +125,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go/v2 v2.3.5 h1:Khtm8K6fTTz/ZCWPzU9Ne3aOW9VyAnj4qIPCJgKtwK0= github.com/cockroachdb/cockroach-go/v2 v2.3.5/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8= -github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= -github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -140,8 +140,9 @@ github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec/go.mod h1:10Fm2kas github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -156,14 +157,14 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/docker/cli v20.10.21+incompatible h1:qVkgyYUnOLQ98LtXBrwd/duVqPT2X4SHndOuGsfwyhU= -github.com/docker/cli v20.10.21+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v24.0.9+incompatible h1:OxbimnP/z+qVjDLpq9wbeFU3Nc30XhSe+LkwYQisD50= +github.com/docker/cli v24.0.9+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v20.10.27+incompatible h1:Id/ZooynV4ZlD6xX20RCd3SR0Ikn7r4QZDa2ECK2TgA= github.com/docker/docker v20.10.27+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= @@ -181,8 +182,9 @@ github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2Vvl github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= @@ -480,9 +482,8 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= -github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -493,8 +494,8 @@ github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= -github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= @@ -689,8 +690,9 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/luna-duclos/instrumentedsql v1.1.3 h1:t7mvC0z1jUt5A0UQ6I/0H31ryymuQRnJcWCiqV3lSAA= github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -728,18 +730,19 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/goveralls v0.0.7 h1:vzy0i4a2iDzEFMdXIxcanRadkr0FBvSBKUmj0P8SPlQ= @@ -748,8 +751,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50= -github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= -github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= @@ -769,8 +772,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= -github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -802,14 +805,14 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= -github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= -github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= +github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= +github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= github.com/ory/analytics-go/v5 v5.0.1 h1:LX8T5B9FN8KZXOtxgN+R3I4THRRVB6+28IKgKBpXmAM= github.com/ory/analytics-go/v5 v5.0.1/go.mod h1:lWCiCjAaJkKfgR/BN5DCLMol8BjKS1x+4jxBxff/FF0= -github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY= -github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM= +github.com/ory/dockertest/v3 v3.10.1-0.20240619125955-3328cf9343b8 h1:pdmvNMAN5x5kPmntdHNmfl3TDszlGeXYri+JSA4JMNM= +github.com/ory/dockertest/v3 v3.10.1-0.20240619125955-3328cf9343b8/go.mod h1:Z3wDt3X5YzB70upzvwiBH2U3lj8q/SXHKT2dyMM7t3I= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe h1:rvu4obdvqR0fkSIJ8IfgzKOWwZ5kOT2UNfLq81Qk7rc= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe/go.mod h1:z4n3u6as84LbV4YmgjHhnwtccQqzf4cZlSk9f1FhygI= github.com/ory/graceful v0.1.4-0.20230301144740-e222150c51d0 h1:VMUeLRfQD14fOMvhpYZIIT4vtAqxYh+f3KnSqCeJ13o= @@ -827,8 +830,8 @@ github.com/ory/nosurf v1.2.7 h1:YrHrbSensQyU6r6HT/V5+HPdVEgrOTMJiLoJABSBOp4= github.com/ory/nosurf v1.2.7/go.mod h1:d4L3ZBa7Amv55bqxCBtCs63wSlyaiCkWVl4vKf3OUxA= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 h1:zm6sDvHy/U9XrGpixwHiuAwpp0Ock6khSVHkrv6lQQU= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/ory/x v0.0.623 h1:sFJiw2i/itTkBRJbhGXtrso9NcdscnjFlHBFitCzf8A= -github.com/ory/x v0.0.623/go.mod h1:CUw8/O3X8lUMheyV0iH+6LQ0tePrH+FBsW39MccCHgw= +github.com/ory/x v0.0.639 h1:6/9V6XlAwsPBFNpL/FMp83SbFD70n0Ql0dAaAlDbESA= +github.com/ory/x v0.0.639/go.mod h1:kjXXSK3a0lC9NNSkxG1sRlnrR9GoG52mvo8z4Nsicu0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -935,8 +938,9 @@ github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slack-go/slack v0.7.4 h1:Z+7CmUDV+ym4lYLA4NNLFIpr3+nDgViHrx8xsuXgrYs= github.com/slack-go/slack v0.7.4/go.mod h1:FGqNzJBmxIsZURAxh2a8D21AnOVvvXZvGligs4npPUM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -980,6 +984,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -1270,7 +1275,6 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1314,10 +1318,12 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1328,11 +1334,12 @@ golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1386,7 +1393,6 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1593,9 +1599,8 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= -gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/persistence/sql/persister_session.go b/persistence/sql/persister_session.go index 412ed2d8a825..37fccca0bd84 100644 --- a/persistence/sql/persister_session.go +++ b/persistence/sql/persister_session.go @@ -10,6 +10,7 @@ import ( "github.com/ory/herodot" "github.com/ory/x/dbal" + "github.com/ory/x/pointerx" "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" @@ -286,10 +287,10 @@ func (p *Persister) UpsertSession(ctx context.Context, s *session.Session) (err device.NID = s.NID if device.Location != nil { - device.Location = stringsx.GetPointer(stringsx.TruncateByteLen(*device.Location, SessionDeviceLocationMaxLength)) + device.Location = pointerx.Ptr(stringsx.TruncateByteLen(*device.Location, SessionDeviceLocationMaxLength)) } if device.UserAgent != nil { - device.UserAgent = stringsx.GetPointer(stringsx.TruncateByteLen(*device.UserAgent, SessionDeviceUserAgentMaxLength)) + device.UserAgent = pointerx.Ptr(stringsx.TruncateByteLen(*device.UserAgent, SessionDeviceUserAgentMaxLength)) } if err := p.DevicePersister.CreateDevice(ctx, device); err != nil { diff --git a/session/session.go b/session/session.go index 84f64ceec0d6..e5b826b88f2f 100644 --- a/session/session.go +++ b/session/session.go @@ -16,7 +16,7 @@ import ( "github.com/ory/x/httpx" "github.com/ory/x/pagination/keysetpagination" - "github.com/ory/x/stringsx" + "github.com/ory/x/pointerx" "github.com/pkg/errors" @@ -282,12 +282,12 @@ func (s *Session) Activate(r *http.Request, i *identity.Identity, c lifespanProv func (s *Session) SetSessionDeviceInformation(r *http.Request) { device := Device{ SessionID: s.ID, - IPAddress: stringsx.GetPointer(httpx.ClientIP(r)), + IPAddress: pointerx.Ptr(httpx.ClientIP(r)), } agent := r.Header["User-Agent"] if len(agent) > 0 { - device.UserAgent = stringsx.GetPointer(strings.Join(agent, " ")) + device.UserAgent = pointerx.Ptr(strings.Join(agent, " ")) } var clientGeoLocation []string @@ -297,7 +297,7 @@ func (s *Session) SetSessionDeviceInformation(r *http.Request) { if r.Header.Get("Cf-Ipcountry") != "" { clientGeoLocation = append(clientGeoLocation, r.Header.Get("Cf-Ipcountry")) } - device.Location = stringsx.GetPointer(strings.Join(clientGeoLocation, ", ")) + device.Location = pointerx.Ptr(strings.Join(clientGeoLocation, ", ")) s.Devices = append(s.Devices, device) } From c9d55730a10b71ac61bb5097f5f9c33f144f2a95 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Wed, 3 Jul 2024 09:27:58 +0200 Subject: [PATCH 12/71] feat: password migration hook (#3978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a password migration hook to easily migrate passwords for which we do not have the hash. For each user that needs to be migrated to Ory Network, a new identity is created with a credential of type password with a config of {"use_password_migration_hook": true} . When a user logs in, the credential identifier and password will be sent to the password_migration web hook if all of these are true: The user’s identity’s password credential is {"use_password_migration_hook": true} The password_migration hook is configured After calling the password_migration hook, the HTTP status code will be inspected: On 200, we parse the response as JSON and look for {"status": "password_match"}. The password credential config will be replaced with the hash of the actual password. On any other status code, we assume that the password is not valid. --------- Co-authored-by: zepatrik --- .github/workflows/ci.yaml | 5 +- .golangci.yml | 8 +- Makefile | 2 +- driver/config/config.go | 19 +- embedx/config.schema.json | 647 ++++++-------------- identity/credentials_password.go | 9 + identity/credentials_password_test.go | 46 ++ internal/client-go/go.sum | 1 + selfservice/hook/password_migration_hook.go | 111 ++++ selfservice/strategy/password/login.go | 32 +- selfservice/strategy/password/login_test.go | 229 ++++++- selfservice/strategy/password/strategy.go | 10 +- 12 files changed, 620 insertions(+), 499 deletions(-) create mode 100644 identity/credentials_password_test.go create mode 100644 selfservice/hook/password_migration_hook.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 26df4ffc97a3..9c2ee0555b42 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -88,13 +88,12 @@ jobs: - run: npm install name: Install node deps - name: Run golangci-lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v6 env: GOGC: 100 with: args: --timeout 10m0s - version: v1.56.2 - skip-pkg-cache: true + version: v1.59.1 - name: Build Kratos run: make install - name: Run go-acc (tests) diff --git a/.golangci.yml b/.golangci.yml index 374c9204ed1b..e83dd5a56a2e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,14 +19,12 @@ linters-settings: goimports: local-prefixes: github.com/ory -run: - skip-dirs: +issues: + exclude-dirs: - sdk/ - skip-files: + exclude-files: - ".+_test.go" - "corpx/faker.go" - -issues: exclude: - "Set is deprecated: use context-based WithConfigValue instead" - "SetDefaultIdentitySchemaFromRaw is deprecated: Use context-based WithDefaultIdentitySchemaFromRaw instead" diff --git a/Makefile b/Makefile index 61e4284d3994..7af282d469c3 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ docs/swagger: npx @redocly/openapi-cli preview-docs spec/swagger.json .bin/golangci-lint: Makefile - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -d -b .bin v1.56.2 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -d -b .bin v1.59.1 .bin/hydra: Makefile bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -d -b .bin hydra v2.2.0-rc.3 diff --git a/driver/config/config.go b/driver/config/config.go index 9f3c1b38938b..ac394d7c8518 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -203,6 +203,7 @@ const ( ViperKeyClientHTTPPrivateIPExceptionURLs = "clients.http.private_ip_exception_urls" ViperKeyPreviewDefaultReadConsistencyLevel = "preview.default_read_consistency_level" ViperKeyVersion = "version" + ViperKeyPasswordMigrationHook = "selfservice.flows.login.password_migration" ) const ( @@ -290,6 +291,10 @@ type ( Headers map[string]string `json:"headers" koanf:"headers"` LocalName string `json:"local_name" koanf:"local_name"` } + PasswordMigrationHook struct { + Enabled bool `json:"enabled"` + Config json.RawMessage `json:"config"` + } Config struct { l *logrusx.Logger p *configx.Provider @@ -518,13 +523,13 @@ func (p *Config) cors(ctx context.Context, prefix string) (cors.Options, bool) { }) } -// Deprecatd: use context-based WithConfigValue instead -func (p *Config) Set(ctx context.Context, key string, value interface{}) error { +// Deprecated: use context-based WithConfigValue instead +func (p *Config) Set(_ context.Context, key string, value interface{}) error { return p.p.Set(key, value) } // Deprecated: use context-based WithConfigValue instead -func (p *Config) MustSet(ctx context.Context, key string, value interface{}) { +func (p *Config) MustSet(_ context.Context, key string, value interface{}) { if err := p.p.Set(key, value); err != nil { p.l.WithError(err).Fatalf("Unable to set \"%s\" to \"%s\".", key, value) } @@ -1599,3 +1604,11 @@ func (p *Config) TokenizeTemplate(ctx context.Context, key string) (_ *SessionTo func (p *Config) DefaultConsistencyLevel(ctx context.Context) crdbx.ConsistencyLevel { return crdbx.ConsistencyLevelFromString(p.GetProvider(ctx).String(ViperKeyPreviewDefaultReadConsistencyLevel)) } + +func (p *Config) PasswordMigrationHook(ctx context.Context) (hook *PasswordMigrationHook) { + hook = new(PasswordMigrationHook) + // Error is ignored on purpose, as we then default to a hook with `enabled = false`. + _ = p.GetProvider(ctx).Unmarshal(ViperKeyPasswordMigrationHook, hook) + + return hook +} diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 5ebbdb1241ee..e763c402a91a 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -43,10 +43,7 @@ "description": "Ory Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/dashboard", - "/dashboard" - ] + "examples": ["https://my-app.com/dashboard", "/dashboard"] }, "selfServiceSessionRevokerHook": { "type": "object", @@ -56,9 +53,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "selfServiceSessionIssuerHook": { "type": "object", @@ -68,9 +63,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "selfServiceRequireVerifiedAddressHook": { "type": "object", @@ -80,9 +73,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "selfServiceVerificationHook": { "type": "object", @@ -92,9 +83,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "selfServiceShowVerificationUIHook": { "type": "object", @@ -104,9 +93,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "b2bSSOHook": { "type": "object", @@ -120,10 +107,7 @@ } }, "additionalProperties": false, - "required": [ - "hook", - "config" - ] + "required": ["hook", "config"] }, "webHookAuthBasicAuthProperties": { "properties": { @@ -143,17 +127,11 @@ } }, "additionalProperties": false, - "required": [ - "user", - "password" - ] + "required": ["user", "password"] } }, "additionalProperties": false, - "required": [ - "type", - "config" - ] + "required": ["type", "config"] }, "httpRequestConfig": { "type": "object", @@ -161,9 +139,7 @@ "url": { "title": "HTTP address of API endpoint", "description": "This URL will be used to send the emails to.", - "examples": [ - "https://example.com/api/v1/email" - ], + "examples": ["https://example.com/api/v1/email"], "type": "string", "pattern": "^https?://" }, @@ -228,25 +204,15 @@ "in": { "type": "string", "description": "How the api key should be transferred", - "enum": [ - "header", - "cookie" - ] + "enum": ["header", "cookie"] } }, "additionalProperties": false, - "required": [ - "name", - "value", - "in" - ] + "required": ["name", "value", "in"] } }, "additionalProperties": false, - "required": [ - "type", - "config" - ] + "required": ["type", "config"] }, "selfServiceWebHook": { "type": "object", @@ -285,10 +251,7 @@ "const": true } }, - "required": [ - "ignore", - "parse" - ] + "required": ["ignore", "parse"] } }, "url": { @@ -361,46 +324,30 @@ "response": { "properties": { "ignore": { - "enum": [ - true - ] + "enum": [true] } }, - "required": [ - "ignore" - ] + "required": ["ignore"] } }, - "required": [ - "response" - ] + "required": ["response"] } }, { "properties": { "can_interrupt": { - "enum": [ - false - ] + "enum": [false] } }, - "require": [ - "can_interrupt" - ] + "require": ["can_interrupt"] } ], "additionalProperties": false, - "required": [ - "url", - "method" - ] + "required": ["url", "method"] } }, "additionalProperties": false, - "required": [ - "hook", - "config" - ] + "required": ["hook", "config"] }, "OIDCClaims": { "title": "OpenID Connect claims", @@ -433,9 +380,7 @@ "essential": true }, "acr": { - "values": [ - "urn:mace:incommon:iap:silver" - ] + "values": ["urn:mace:incommon:iap:silver"] } } } @@ -483,9 +428,7 @@ "properties": { "id": { "type": "string", - "examples": [ - "google" - ] + "examples": ["google"] }, "provider": { "title": "Provider", @@ -514,9 +457,7 @@ "lark", "x" ], - "examples": [ - "google" - ] + "examples": ["google"] }, "label": { "title": "Optional string which will be used when generating labels for UI buttons.", @@ -531,23 +472,17 @@ "issuer_url": { "type": "string", "format": "uri", - "examples": [ - "https://accounts.google.com" - ] + "examples": ["https://accounts.google.com"] }, "auth_url": { "type": "string", "format": "uri", - "examples": [ - "https://accounts.google.com/o/oauth2/v2/auth" - ] + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] }, "token_url": { "type": "string", "format": "uri", - "examples": [ - "https://www.googleapis.com/oauth2/v4/token" - ] + "examples": ["https://www.googleapis.com/oauth2/v4/token"] }, "mapper_url": { "title": "Jsonnet Mapper URL", @@ -564,10 +499,7 @@ "type": "array", "items": { "type": "string", - "examples": [ - "offline_access", - "profile" - ] + "examples": ["offline_access", "profile"] } }, "microsoft_tenant": { @@ -586,30 +518,21 @@ "title": "Microsoft subject source", "description": "Controls which source the subject identifier is taken from by microsoft provider. If set to `userinfo` (the default) then the identifier is taken from the `sub` field of OIDC ID token or data received from `/userinfo` standard OIDC endpoint. If set to `me` then the `id` field of data structure received from `https://graph.microsoft.com/v1.0/me` is taken as an identifier.", "type": "string", - "enum": [ - "userinfo", - "me" - ], + "enum": ["userinfo", "me"], "default": "userinfo", - "examples": [ - "userinfo" - ] + "examples": ["userinfo"] }, "apple_team_id": { "title": "Apple Developer Team ID", "description": "Apple Developer Team ID needed for generating a JWT token for client secret", "type": "string", - "examples": [ - "KP76DQS54M" - ] + "examples": ["KP76DQS54M"] }, "apple_private_key_id": { "title": "Apple Private Key Identifier", "description": "Sign In with Apple Private Key Identifier needed for generating a JWT token for client secret", "type": "string", - "examples": [ - "UX56C66723" - ] + "examples": ["UX56C66723"] }, "apple_private_key": { "title": "Apple Private Key", @@ -626,42 +549,27 @@ "title": "Organization ID", "description": "The ID of the organization that this provider belongs to. Only effective in the Ory Network.", "type": "string", - "examples": [ - "12345678-1234-1234-1234-123456789012" - ] + "examples": ["12345678-1234-1234-1234-123456789012"] }, "additional_id_token_audiences": { "title": "Additional client ids allowed when using ID token submission", "type": "array", "items": { "type": "string", - "examples": [ - "12345678-1234-1234-1234-123456789012" - ] + "examples": ["12345678-1234-1234-1234-123456789012"] } }, "claims_source": { "title": "Claims source", "description": "Can be either `userinfo` (calls the userinfo endpoint to get the claims) or `id_token` (takes the claims from the id token). It defaults to `id_token`", "type": "string", - "enum": [ - "id_token", - "userinfo" - ], + "enum": ["id_token", "userinfo"], "default": "id_token", - "examples": [ - "id_token", - "userinfo" - ] + "examples": ["id_token", "userinfo"] } }, "additionalProperties": false, - "required": [ - "id", - "provider", - "client_id", - "mapper_url" - ], + "required": ["id", "provider", "client_id", "mapper_url"], "allOf": [ { "if": { @@ -670,23 +578,17 @@ "const": "microsoft" } }, - "required": [ - "provider" - ] + "required": ["provider"] }, "then": { - "required": [ - "microsoft_tenant" - ] + "required": ["microsoft_tenant"] }, "else": { "not": { "properties": { "microsoft_tenant": {} }, - "required": [ - "microsoft_tenant" - ] + "required": ["microsoft_tenant"] } } }, @@ -697,9 +599,7 @@ "const": "apple" } }, - "required": [ - "provider" - ] + "required": ["provider"] }, "then": { "not": { @@ -709,9 +609,7 @@ "minLength": 1 } }, - "required": [ - "client_secret" - ] + "required": ["client_secret"] }, "required": [ "apple_private_key_id", @@ -720,9 +618,7 @@ ] }, "else": { - "required": [ - "client_secret" - ], + "required": ["client_secret"], "allOf": [ { "not": { @@ -732,9 +628,7 @@ "minLength": 1 } }, - "required": [ - "apple_team_id" - ] + "required": ["apple_team_id"] } }, { @@ -745,9 +639,7 @@ "minLength": 1 } }, - "required": [ - "apple_private_key_id" - ] + "required": ["apple_private_key_id"] } }, { @@ -758,9 +650,7 @@ "minLength": 1 } }, - "required": [ - "apple_private_key" - ] + "required": ["apple_private_key"] } } ] @@ -940,10 +830,7 @@ "title": "Required Authenticator Assurance Level", "description": "Sets what Authenticator Assurance Level (used for 2FA) is required to access this feature. If set to `highest_available` then this endpoint requires the highest AAL the identity has set up. If set to `aal1` then the identity can access this feature without 2FA.", "type": "string", - "enum": [ - "aal1", - "highest_available" - ], + "enum": ["aal1", "highest_available"], "default": "highest_available" }, "selfServiceAfterSettings": { @@ -1139,9 +1026,7 @@ "path": { "title": "Path to PEM-encoded Fle", "type": "string", - "examples": [ - "path/to/file.pem" - ] + "examples": ["path/to/file.pem"] }, "base64": { "title": "Base64 Encoded Inline", @@ -1189,9 +1074,7 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": [ - "email" - ] + "required": ["email"] }, "valid": { "additionalProperties": false, @@ -1204,9 +1087,7 @@ "$ref": "#/definitions/smsCourierTemplate" } }, - "required": [ - "email" - ] + "required": ["email"] } } }, @@ -1277,9 +1158,7 @@ "selfservice": { "type": "object", "additionalProperties": false, - "required": [ - "default_browser_return_url" - ], + "required": ["default_browser_return_url"], "properties": { "default_browser_return_url": { "$ref": "#/definitions/defaultReturnTo" @@ -1314,30 +1193,20 @@ "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/user/settings" - ], + "examples": ["https://my-app.com/user/settings"], "default": "https://www.ory.sh/kratos/docs/fallback/settings" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "privileged_session_max_age": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "required_aal": { "$ref": "#/definitions/featureRequiredAal" @@ -1386,20 +1255,14 @@ "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/signup" - ], + "examples": ["https://my-app.com/signup"], "default": "https://www.ory.sh/kratos/docs/fallback/registration" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "before": { "$ref": "#/definitions/selfServiceBeforeRegistration" @@ -1424,31 +1287,77 @@ "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/login" - ], + "examples": ["https://my-app.com/login"], "default": "https://www.ory.sh/kratos/docs/fallback/login" }, "lifespan": { "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "style": { "title": "Login Flow Style", "description": "The style of the login flow. If set to `one_step` the login flow will be a one-step process. If set to `identifier_first` (experimental!) the login flow will first ask for the identifier and then the credentials.", "type": "string", - "enum": [ - "one_step", - "identifier_first" - ], + "enum": ["one_step", "identifier_first"], "default": "one_step" }, + "password_migration": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Password Migration", + "description": "If set to true will enable password migration.", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "description": "The URL the password migration hook should call", + "format": "uri" + }, + "method": { + "type": "string", + "description": "The HTTP method to use (GET, POST, etc).", + "const": "POST", + "default": "POST" + }, + "headers": { + "type": "object", + "description": "The HTTP headers that must be applied to the password migration hook.", + "additionalProperties": { + "type": "string" + } + }, + "emit_analytics_event": { + "type": "boolean", + "default": true, + "description": "Emit tracing events for this hook on delivery or error" + }, + "auth": { + "type": "object", + "title": "Auth mechanisms", + "description": "Define which auth mechanism the Web-Hook should use", + "oneOf": [ + { + "$ref": "#/definitions/webHookAuthApiKeyProperties" + }, + { + "$ref": "#/definitions/webHookAuthBasicAuthProperties" + } + ] + }, + "additionalProperties": false + } + } + } + }, "before": { "$ref": "#/definitions/selfServiceBeforeLogin" }, @@ -1473,9 +1382,7 @@ "description": "URL where the Ory Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/verify" - ], + "examples": ["https://my-app.com/verify"], "default": "https://www.ory.sh/kratos/docs/fallback/verification" }, "after": { @@ -1487,11 +1394,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "before": { "$ref": "#/definitions/selfServiceBeforeVerification" @@ -1500,10 +1403,7 @@ "title": "Verification Strategy", "description": "The strategy to use for verification requests", "type": "string", - "enum": [ - "link", - "code" - ], + "enum": ["link", "code"], "default": "code" }, "notify_unknown_recipients": { @@ -1530,9 +1430,7 @@ "description": "URL where the Ory Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/verify" - ], + "examples": ["https://my-app.com/verify"], "default": "https://www.ory.sh/kratos/docs/fallback/recovery" }, "after": { @@ -1544,11 +1442,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "before": { "$ref": "#/definitions/selfServiceBeforeRecovery" @@ -1557,10 +1451,7 @@ "title": "Recovery Strategy", "description": "The strategy to use for recovery requests", "type": "string", - "enum": [ - "link", - "code" - ], + "enum": ["link", "code"], "default": "code" }, "notify_unknown_recipients": { @@ -1580,9 +1471,7 @@ "description": "URL where the Ory Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", "format": "uri-reference", - "examples": [ - "https://my-app.com/kratos-error" - ], + "examples": ["https://my-app.com/kratos-error"], "default": "https://www.ory.sh/kratos/docs/fallback/error" } } @@ -1611,25 +1500,19 @@ "type": "string", "description": "The ID of the organization.", "format": "uuid", - "examples": [ - "00000000-0000-0000-0000-000000000000" - ] + "examples": ["00000000-0000-0000-0000-000000000000"] }, "label": { "type": "string", "description": "The label of the organization.", - "examples": [ - "ACME SSO" - ] + "examples": ["ACME SSO"] }, "domains": { "type": "array", "items": { "type": "string", "format": "hostname", - "examples": [ - "my-app.com" - ], + "examples": ["my-app.com"], "description": "If this domain matches the email's domain, this provider is shown." } } @@ -1669,20 +1552,14 @@ "base_url": { "title": "Override the base URL which should be used as the base for recovery and verification links.", "type": "string", - "examples": [ - "https://my-app.com" - ] + "examples": ["https://my-app.com"] }, "lifespan": { "title": "How long a link is valid for", "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] } } } @@ -1756,11 +1633,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] } } } @@ -1883,17 +1756,13 @@ "type": "string", "title": "Relying Party Display Name", "description": "An name to help the user identify this RP.", - "examples": [ - "Ory Foundation" - ] + "examples": ["Ory Foundation"] }, "id": { "type": "string", "title": "Relying Party Identifier", "description": "The id must be a subset of the domain currently in the browser.", - "examples": [ - "ory.sh" - ] + "examples": ["ory.sh"] }, "origin": { "type": "string", @@ -1901,9 +1770,7 @@ "description": "An explicit RP origin. If left empty, this defaults to `id`, prepended with the current protocol schema (HTTP or HTTPS).", "format": "uri", "deprecationMessage": "This field is deprecated. Use `origins` instead.", - "examples": [ - "https://www.ory.sh" - ] + "examples": ["https://www.ory.sh"] }, "origins": { "type": "array", @@ -1924,18 +1791,13 @@ "description": "An icon to help the user identify this RP.", "format": "uri", "deprecationMessage": "This field is deprecated and ignored due to security considerations.", - "examples": [ - "https://www.ory.sh/an-icon.png" - ] + "examples": ["https://www.ory.sh/an-icon.png"] } }, "type": "object", "oneOf": [ { - "required": [ - "id", - "display_name" - ], + "required": ["id", "display_name"], "properties": { "origin": { "not": {} @@ -1946,11 +1808,7 @@ } }, { - "required": [ - "id", - "display_name", - "origin" - ], + "required": ["id", "display_name", "origin"], "properties": { "origin": { "type": "string" @@ -1961,11 +1819,7 @@ } }, { - "required": [ - "id", - "display_name", - "origins" - ], + "required": ["id", "display_name", "origins"], "properties": { "origin": { "not": {} @@ -1990,14 +1844,10 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] }, "then": { - "required": [ - "config" - ] + "required": ["config"] } }, "passkey": { @@ -2020,17 +1870,13 @@ "type": "string", "title": "Relying Party Display Name", "description": "A name to help the user identify this RP.", - "examples": [ - "Ory Foundation" - ] + "examples": ["Ory Foundation"] }, "id": { "type": "string", "title": "Relying Party Identifier", "description": "The id must be a subset of the domain currently in the browser.", - "examples": [ - "ory.sh" - ] + "examples": ["ory.sh"] }, "origins": { "type": "array", @@ -2047,10 +1893,7 @@ } }, "type": "object", - "required": [ - "display_name", - "id" - ] + "required": ["display_name", "id"] } }, "additionalProperties": false @@ -2062,14 +1905,10 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] }, "then": { - "required": [ - "config" - ] + "required": ["config"] } }, "oidc": { @@ -2092,9 +1931,7 @@ "title": "Base URL for OAuth2 Redirect URIs", "description": "Can be used to modify the base URL for OAuth2 Redirect URLs. If unset, the Public Base URL will be used.", "format": "uri", - "examples": [ - "https://auth.myexample.org/" - ] + "examples": ["https://auth.myexample.org/"] }, "providers": { "title": "OpenID Connect and OAuth2 Providers", @@ -2199,9 +2036,7 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": [ - "email" - ] + "required": ["email"] } } }, @@ -2220,9 +2055,7 @@ "$ref": "#/definitions/smsCourierTemplate" } }, - "required": [ - "email" - ] + "required": ["email"] } } } @@ -2232,18 +2065,13 @@ "type": "string", "title": "Override message templates", "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.", - "examples": [ - "/conf/courier-templates" - ] + "examples": ["/conf/courier-templates"] }, "message_retries": { "description": "Defines the maximum number of times the sending of a message is retried after it failed before it is marked as abandoned", "type": "integer", "default": 5, - "examples": [ - 10, - 60 - ] + "examples": [10, 60] }, "worker": { "description": "Configures the dispatch worker.", @@ -2266,10 +2094,7 @@ "title": "Delivery Strategy", "description": "Defines how emails will be sent, either through SMTP (default) or HTTP.", "type": "string", - "enum": [ - "smtp", - "http" - ], + "enum": ["smtp", "http"], "default": "smtp" }, "http": { @@ -2326,9 +2151,7 @@ "title": "SMTP Sender Name", "description": "The recipient of an email will see this as the sender name.", "type": "string", - "examples": [ - "Bob" - ] + "examples": ["Bob"] }, "headers": { "title": "SMTP Headers", @@ -2376,9 +2199,7 @@ "url": { "title": "HTTP address of API endpoint", "description": "This URL will be used to connect to the SMS provider.", - "examples": [ - "https://api.twillio.com/sms/send" - ], + "examples": ["https://api.twillio.com/sms/send"], "type": "string", "pattern": "^https?:\\/\\/.*" }, @@ -2420,10 +2241,7 @@ }, "additionalProperties": false }, - "required": [ - "url", - "method" - ], + "required": ["url", "method"], "additionalProperties": false } }, @@ -2440,26 +2258,19 @@ "title": "Channel id", "description": "The channel id. Corresponds to the .via property of the identity schema for recovery, verification, etc. Currently only phone is supported.", "maxLength": 32, - "enum": [ - "sms" - ] + "enum": ["sms"] }, "type": { "type": "string", "title": "Channel type", "description": "The channel type. Currently only http is supported.", - "enum": [ - "http" - ] + "enum": ["http"] }, "request_config": { "$ref": "#/definitions/httpRequestConfig" } }, - "required": [ - "id", - "request_config" - ], + "required": ["id", "request_config"], "additionalProperties": false } } @@ -2510,10 +2321,7 @@ "type": "string", "title": "Default Read Consistency Level", "description": "The default consistency level to use when reading from the database. Defaults to `strong` to not break existing API contracts. Only set this to `eventual` if you can accept that other read APIs will suddenly return eventually consistent results. It is only effective in Ory Network.", - "enum": [ - "strong", - "eventual" - ], + "enum": ["strong", "eventual"], "default": "strong" } } @@ -2541,9 +2349,7 @@ "description": "The URL where the admin endpoint is exposed at.", "type": "string", "format": "uri", - "examples": [ - "https://kratos.private-network:4434/" - ] + "examples": ["https://kratos.private-network:4434/"] }, "host": { "title": "Admin Host", @@ -2557,9 +2363,7 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [ - 4434 - ], + "examples": [4434], "default": 4434 }, "socket": { @@ -2618,9 +2422,7 @@ ] }, "uniqueItems": true, - "default": [ - "*" - ], + "default": ["*"], "examples": [ [ "https://example.com", @@ -2632,13 +2434,7 @@ "allowed_methods": { "type": "array", "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", - "default": [ - "POST", - "GET", - "PUT", - "PATCH", - "DELETE" - ], + "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], "items": { "type": "string", "enum": [ @@ -2672,9 +2468,7 @@ "exposed_headers": { "type": "array", "description": "Sets which headers are safe to expose to the API of a CORS API specification.", - "default": [ - "Content-Type" - ], + "default": ["Content-Type"], "items": { "type": "string" } @@ -2717,9 +2511,7 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [ - 4433 - ], + "examples": [4433], "default": 4433 }, "socket": { @@ -2769,10 +2561,7 @@ "format": { "description": "The log format can either be text or JSON.", "type": "string", - "enum": [ - "json", - "text" - ] + "enum": ["json", "text"] } }, "additionalProperties": false @@ -2813,9 +2602,7 @@ "id": { "title": "The schema's ID.", "type": "string", - "examples": [ - "employee" - ] + "examples": ["employee"] }, "url": { "type": "string", @@ -2829,16 +2616,11 @@ ] } }, - "required": [ - "id", - "url" - ] + "required": ["id", "url"] } } }, - "required": [ - "schemas" - ], + "required": ["schemas"], "additionalProperties": false }, "secrets": { @@ -2887,10 +2669,7 @@ "description": "One of the values: argon2, bcrypt.\nAny other hashes will be migrated to the set algorithm once an identity authenticates using their password.", "type": "string", "default": "bcrypt", - "enum": [ - "argon2", - "bcrypt" - ] + "enum": ["argon2", "bcrypt"] }, "argon2": { "title": "Configuration for the Argon2id hasher.", @@ -2946,9 +2725,7 @@ "title": "Configuration for the Bcrypt hasher. Minimum is 4 when --dev flag is used and 12 otherwise.", "type": "object", "additionalProperties": false, - "required": [ - "cost" - ], + "required": ["cost"], "properties": { "cost": { "type": "integer", @@ -2970,11 +2747,7 @@ "description": "One of the values: noop, aes, xchacha20-poly1305", "type": "string", "default": "noop", - "enum": [ - "noop", - "aes", - "xchacha20-poly1305" - ] + "enum": ["noop", "aes", "xchacha20-poly1305"] } } }, @@ -2998,11 +2771,7 @@ "title": "HTTP Cookie Same Site Configuration", "description": "Sets the session and CSRF cookie SameSite.", "type": "string", - "enum": [ - "Strict", - "Lax", - "None" - ], + "enum": ["Strict", "Lax", "None"], "default": "Lax" } }, @@ -3032,9 +2801,7 @@ "patternProperties": { "[a-zA-Z0-9-_.]+": { "type": "object", - "required": [ - "jwks_url" - ], + "required": ["jwks_url"], "properties": { "ttl": { "type": "string", @@ -3067,11 +2834,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "24h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] }, "cookie": { "type": "object", @@ -3102,11 +2865,7 @@ "title": "Session Cookie SameSite Configuration", "description": "Sets the session cookie SameSite. Overrides `cookies.same_site`.", "type": "string", - "enum": [ - "Strict", - "Lax", - "None" - ] + "enum": ["Strict", "Lax", "None"] } }, "additionalProperties": false @@ -3116,11 +2875,7 @@ "description": "Sets when a session can be extended. Settings this value to `24h` will prevent the session from being extended before until 24 hours before it expires. This setting prevents excessive writes to the database. We highly recommend setting this value.", "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] } } }, @@ -3129,9 +2884,7 @@ "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", "type": "string", "pattern": "^(v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|$", - "examples": [ - "v0.5.0-alpha.1" - ] + "examples": ["v0.5.0-alpha.1"] }, "dev": { "type": "boolean" @@ -3155,9 +2908,7 @@ "type": "integer", "minimum": 0, "maximum": 65535, - "examples": [ - 4434 - ], + "examples": [4434], "default": 0 }, "config": { @@ -3263,14 +3014,10 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] } }, - "required": [ - "verification" - ] + "required": ["verification"] }, { "properties": { @@ -3280,31 +3027,21 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] } }, - "required": [ - "recovery" - ] + "required": ["recovery"] } ] } }, - "required": [ - "flows" - ] + "required": ["flows"] } }, - "required": [ - "selfservice" - ] + "required": ["selfservice"] }, "then": { - "required": [ - "courier" - ] + "required": ["courier"] } }, { @@ -3323,33 +3060,21 @@ ] } }, - "required": [ - "algorithm" - ] + "required": ["algorithm"] } }, - "required": [ - "ciphers" - ] + "required": ["ciphers"] }, "then": { - "required": [ - "secrets" - ], + "required": ["secrets"], "properties": { "secrets": { - "required": [ - "cipher" - ] + "required": ["cipher"] } } } } ], - "required": [ - "identity", - "dsn", - "selfservice" - ], + "required": ["identity", "dsn", "selfservice"], "additionalProperties": false } diff --git a/identity/credentials_password.go b/identity/credentials_password.go index 85f5ac1d7d0c..4a6e7ebc7144 100644 --- a/identity/credentials_password.go +++ b/identity/credentials_password.go @@ -9,4 +9,13 @@ package identity type CredentialsPassword struct { // HashedPassword is a hash-representation of the password. HashedPassword string `json:"hashed_password"` + + // UsePasswordMigrationHook is set to true if the password should be migrated + // using the password migration hook. If set, and the HashedPassword is empty, a + // webhook will be called during login to migrate the password. + UsePasswordMigrationHook bool `json:"use_password_migration_hook,omitempty"` +} + +func (cp *CredentialsPassword) ShouldUsePasswordMigrationHook() bool { + return cp != nil && cp.HashedPassword == "" && cp.UsePasswordMigrationHook } diff --git a/identity/credentials_password_test.go b/identity/credentials_password_test.go new file mode 100644 index 000000000000..6e62720779e6 --- /dev/null +++ b/identity/credentials_password_test.go @@ -0,0 +1,46 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCredentialsPassword_ShouldUsePasswordMigrationHook(t *testing.T) { + tests := []struct { + name string + cp *CredentialsPassword + want bool + }{{ + name: "pw set", + cp: &CredentialsPassword{ + HashedPassword: "pw", + UsePasswordMigrationHook: true, + }, + want: false, + }, { + name: "pw not set", + cp: &CredentialsPassword{ + HashedPassword: "", + UsePasswordMigrationHook: true, + }, + want: true, + }, { + name: "nil", + want: false, + }, { + name: "pw not set, hook not set", + cp: &CredentialsPassword{ + HashedPassword: "", + }, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, tt.cp.ShouldUsePasswordMigrationHook(), "ShouldUsePasswordMigrationHook()") + }) + } +} diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index c966c8ddfd0d..6cc3f5911d11 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/selfservice/hook/password_migration_hook.go b/selfservice/hook/password_migration_hook.go new file mode 100644 index 000000000000..065dc5dcddc6 --- /dev/null +++ b/selfservice/hook/password_migration_hook.go @@ -0,0 +1,111 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.11.0" + "go.opentelemetry.io/otel/trace" + grpccodes "google.golang.org/grpc/codes" + + "github.com/ory/herodot" + "github.com/ory/kratos/request" + "github.com/ory/kratos/schema" + "github.com/ory/x/otelx" +) + +type ( + PasswordMigration struct { + deps webHookDependencies + conf json.RawMessage + } + PasswordMigrationRequest struct { + Identifier string `json:"identifier"` + Password string `json:"password"` + } + PasswordMigrationResponse struct { + Status string `json:"status"` + } +) + +func NewPasswordMigrationHook(deps webHookDependencies, conf json.RawMessage) *PasswordMigration { + return &PasswordMigration{deps: deps, conf: conf} +} + +func (p *PasswordMigration) Execute(ctx context.Context, data *PasswordMigrationRequest) (err error) { + var ( + httpClient = p.deps.HTTPClient(ctx) + emitEvent = gjson.GetBytes(p.conf, "emit_analytics_event").Bool() || !gjson.GetBytes(p.conf, "emit_analytics_event").Exists() // default true + tracer = trace.SpanFromContext(ctx).TracerProvider().Tracer("kratos-webhooks") + ) + + ctx, span := tracer.Start(ctx, "selfservice.login.password_migration") + defer otelx.End(span, &err) + + if emitEvent { + instrumentHTTPClientForEvents(ctx, httpClient) + } + builder, err := request.NewBuilder(ctx, p.conf, p.deps, nil) + if err != nil { + return errors.WithStack(err) + } + req, err := builder.BuildRequest(ctx, nil) // passing a nil body here skips Jsonnet + if err != nil { + return errors.WithStack(err) + } + rawData, err := json.Marshal(data) + if err != nil { + return errors.WithStack(err) + } + if err = req.SetBody(rawData); err != nil { + return errors.WithStack(err) + } + + p.deps.Logger().WithRequest(req.Request).Info("Dispatching password migration hook") + req = req.WithContext(ctx) + + resp, err := httpClient.Do(req) + if err != nil { + return herodot.DefaultError{ + CodeField: http.StatusBadGateway, + StatusField: http.StatusText(http.StatusBadGateway), + GRPCCodeField: grpccodes.Aborted, + ReasonField: "A third-party upstream service could not be reached. Please try again later.", + ErrorField: "calling the password migration hook failed", + }.WithWrap(errors.WithStack(err)) + } + defer resp.Body.Close() + span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(resp.StatusCode)...) + + switch resp.StatusCode { + case http.StatusOK: + // We now check if the response matches `{"status": "password_match" }`. + dec := json.NewDecoder(io.LimitReader(resp.Body, 1024)) // limit the response body to 1KB + var response PasswordMigrationResponse + if err := dec.Decode(&response); err != nil || response.Status != "password_match" { + return errors.WithStack(schema.NewInvalidCredentialsError()) + } + return nil + + case http.StatusForbidden: + return errors.WithStack(schema.NewInvalidCredentialsError()) + default: + span.SetStatus(codes.Error, "Unexpected HTTP status code") + return herodot.DefaultError{ + CodeField: http.StatusBadGateway, + StatusField: http.StatusText(http.StatusBadGateway), + GRPCCodeField: grpccodes.Aborted, + ReasonField: "A third-party upstream service responded improperly. Please try again later.", + ErrorField: fmt.Sprintf("password migration hook failed with status code %v", resp.StatusCode), + } + } +} diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 8c91d7e6c4f9..3600e29b4e0e 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -10,7 +10,9 @@ import ( "net/http" "time" + "github.com/ory/kratos/hash" "github.com/ory/kratos/selfservice/flowhelpers" + "github.com/ory/kratos/selfservice/hook" "github.com/ory/kratos/session" "github.com/ory/x/stringsx" @@ -22,7 +24,6 @@ import ( "github.com/ory/herodot" "github.com/ory/x/decoderx" - "github.com/ory/kratos/hash" "github.com/ory/kratos/identity" "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/flow" @@ -69,7 +70,8 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.handleLoginError(w, r, f, &p, err) } - i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), stringsx.Coalesce(p.Identifier, p.LegacyIdentifier)) + identifier := stringsx.Coalesce(p.Identifier, p.LegacyIdentifier) + i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), identifier) if err != nil { time.Sleep(x.RandomDelay(s.d.Config().HasherArgon2(r.Context()).ExpectedDuration, s.d.Config().HasherArgon2(r.Context()).ExpectedDeviation)) return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError())) @@ -81,17 +83,33 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err) } - if err := hash.Compare(r.Context(), []byte(p.Password), []byte(o.HashedPassword)); err != nil { - return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError())) - } + if o.ShouldUsePasswordMigrationHook() { + pwHook := s.d.Config().PasswordMigrationHook(r.Context()) + if !pwHook.Enabled { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Password migration hook is not enabled but password migration is requested.")) + } + + migrationHook := hook.NewPasswordMigrationHook(s.d, pwHook.Config) + err = migrationHook.Execute(r.Context(), &hook.PasswordMigrationRequest{Identifier: identifier, Password: p.Password}) + if err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } - if !s.d.Hasher(r.Context()).Understands([]byte(o.HashedPassword)) { if err := s.migratePasswordHash(r.Context(), i.ID, []byte(p.Password)); err != nil { return nil, s.handleLoginError(w, r, f, &p, err) } + } else { + if err := hash.Compare(r.Context(), []byte(p.Password), []byte(o.HashedPassword)); err != nil { + return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError())) + } + + if !s.d.Hasher(r.Context()).Understands([]byte(o.HashedPassword)) { + if err := s.migratePasswordHash(r.Context(), i.ID, []byte(p.Password)); err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } + } } - f.Active = identity.CredentialsTypePassword f.Active = s.ID() if err = s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go index 8d8879cce91c..8c2f2cb73245 100644 --- a/selfservice/strategy/password/login_test.go +++ b/selfservice/strategy/password/login_test.go @@ -16,34 +16,30 @@ import ( "testing" "time" - "github.com/ory/kratos/driver" - "github.com/ory/kratos/internal/registrationhelpers" - - "github.com/ory/kratos/selfservice/flow" - + "github.com/gobuffalo/httptest" "github.com/gofrs/uuid" - - "github.com/ory/x/urlx" - - "github.com/ory/kratos/hash" - kratos "github.com/ory/kratos/internal/httpclient" - "github.com/ory/x/assertx" - "github.com/ory/x/errorsx" - "github.com/ory/x/ioutilx" - "github.com/ory/x/sqlxx" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" + "github.com/ory/kratos/driver" "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hash" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" + kratos "github.com/ory/kratos/internal/httpclient" + "github.com/ory/kratos/internal/registrationhelpers" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/text" "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/errorsx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" ) //go:embed stub/login.schema.json @@ -864,4 +860,207 @@ func TestCompleteLogin(t *testing.T) { false, true, http.StatusOK, redirTS.URL) assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) }) + + t.Run("suite=password migration hook", func(t *testing.T) { + ctx := context.Background() + + type ( + hookPayload = struct { + Identifier string `json:"identifier"` + Password string `json:"password"` + } + tsRequestHandler = func(hookPayload) (status int, body string) + ) + returnStatus := func(status int) func(string, string) tsRequestHandler { + return func(string, string) tsRequestHandler { + return func(hookPayload) (int, string) { return status, "" } + } + } + returnStatic := func(status int, body string) func(string, string) tsRequestHandler { + return func(string, string) tsRequestHandler { + return func(hookPayload) (int, string) { return status, body } + } + } + + // each test case sends (number of expected calls) handlers to the channel, at a max of 3 + tsChan := make(chan tsRequestHandler, 3) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + _ = r.Body.Close() + var payload hookPayload + require.NoError(t, json.Unmarshal(b, &payload)) + + select { + case handlerFn := <-tsChan: + status, body := handlerFn(payload) + w.WriteHeader(status) + _, _ = io.WriteString(w, body) + + default: + t.Fatal("unexpected call to the password migration hook") + } + })) + t.Cleanup(ts.Close) + + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyPasswordMigrationHook, &config.PasswordMigrationHook{ + Enabled: true, + Config: json.RawMessage(fmt.Sprintf(`{"URL":"%s"}`, ts.URL)), + })) + + for _, tc := range []struct { + name string + hookHandler func(identifier, password string) tsRequestHandler + expectHookCalls int + setupFn func() func() + credentialsConfig string + expectSuccess bool + }{{ + name: "should call migration hook", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: func(identifier, password string) tsRequestHandler { + return func(payload hookPayload) (status int, body string) { + if payload.Identifier == identifier && payload.Password == password { + return http.StatusOK, `{"status":"password_match"}` + } else { + return http.StatusOK, `{"status":"no_match"}` + } + } + }, + expectHookCalls: 1, + expectSuccess: true, + }, { + name: "should not update identity when the password is wrong", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatus(http.StatusForbidden), + expectHookCalls: 1, + expectSuccess: false, + }, { + name: "should inspect response", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatic(http.StatusOK, `{"status":"password_no_match"}`), + expectHookCalls: 1, + expectSuccess: false, + }, { + name: "should not update identity when the migration hook returns 200 without JSON", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatus(http.StatusOK), + expectHookCalls: 1, + expectSuccess: false, + }, { + name: "should not update identity when the migration hook returns 500", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatus(http.StatusInternalServerError), + expectHookCalls: 3, // expect retries on 500 + expectSuccess: false, + }, { + name: "should not update identity when the migration hook returns 201", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatic(http.StatusCreated, `{"status":"password_match"}`), + expectHookCalls: 1, + expectSuccess: false, + }, { + name: "should not update identity and not call hook when hash is set", + credentialsConfig: `{"use_password_migration_hook": true, "hashed_password":"hash"}`, + expectSuccess: false, + }, { + name: "should not update identity and not call hook when use_password_migration_hook is not set", + credentialsConfig: `{"hashed_password":"hash"}`, + expectSuccess: false, + }, { + name: "should not update identity and not call hook when credential is empty", + credentialsConfig: `{}`, + expectSuccess: false, + }, { + name: "should not call migration hook if disabled", + credentialsConfig: `{"use_password_migration_hook": true}`, + setupFn: func() func() { + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyPasswordMigrationHook+".enabled", false)) + return func() { + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyPasswordMigrationHook+".enabled", true)) + } + }, + expectSuccess: false, + }} { + + t.Run("case="+tc.name, func(t *testing.T) { + if tc.setupFn != nil { + cleanup := tc.setupFn() + t.Cleanup(cleanup) + } + + identifier := x.NewUUID().String() + password := x.NewUUID().String() + iId := x.NewUUID() + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, &identity.Identity{ + ID: iId, + Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)), + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{identifier}, + Config: sqlxx.JSONRawMessage(tc.credentialsConfig), + }, + }, + VerifiableAddresses: []identity.VerifiableAddress{ + { + ID: x.NewUUID(), + Value: identifier, + Verified: true, + CreatedAt: time.Now(), + IdentityID: iId, + }, + }, + })) + + values := func(v url.Values) { + v.Set("identifier", identifier) + v.Set("method", identity.CredentialsTypePassword.String()) + v.Set("password", password) + } + + for range tc.expectHookCalls { + tsChan <- tc.hookHandler(identifier, password) + } + + browserClient := testhelpers.NewClientWithCookies(t) + + if tc.expectSuccess { + body := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values, + false, false, http.StatusOK, redirTS.URL) + assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) + + // check if password hash algorithm is upgraded + _, c, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypePassword, identifier) + require.NoError(t, err) + var o identity.CredentialsPassword + require.NoError(t, json.NewDecoder(bytes.NewBuffer(c.Config)).Decode(&o)) + assert.True(t, reg.Hasher(ctx).Understands([]byte(o.HashedPassword)), "%s", o.HashedPassword) + assert.True(t, hash.IsBcryptHash([]byte(o.HashedPassword)), "%s", o.HashedPassword) + + // retry after upgraded + body = testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values, + false, true, http.StatusOK, redirTS.URL) + assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) + } else { + body := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values, + false, false, http.StatusOK, "") + assert.Empty(t, gjson.Get(body, "identity.traits.subject").String(), "%s", body) + // Check that the config did not change + _, c, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypePassword, identifier) + require.NoError(t, err) + assert.JSONEq(t, tc.credentialsConfig, string(c.Config)) + } + + // expect all hook calls to be done + select { + case <-tsChan: + t.Fatal("the test unexpectedly did too few calls to the password hook") + default: + // pass + } + }) + } + }) } diff --git a/selfservice/strategy/password/strategy.go b/selfservice/strategy/password/strategy.go index 911ad619cd15..ae57982dd89f 100644 --- a/selfservice/strategy/password/strategy.go +++ b/selfservice/strategy/password/strategy.go @@ -7,11 +7,12 @@ import ( "context" "encoding/json" - "github.com/ory/kratos/ui/node" - "github.com/go-playground/validator/v10" "github.com/pkg/errors" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/decoderx" "github.com/ory/kratos/continuity" @@ -37,9 +38,10 @@ type registrationStrategyDependencies interface { x.WriterProvider x.CSRFTokenGeneratorProvider x.CSRFProvider - + x.HTTPClientProvider + x.TracingProvider + jsonnetsecure.VMProvider config.Provider - continuity.ManagementProvider errorx.ManagementProvider From 020a9dea054fa6e5bad1ec253f2fe346a490131d Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Wed, 3 Jul 2024 07:29:21 +0000 Subject: [PATCH 13/71] autogen(openapi): regenerate swagger spec and internal client [skip ci] --- internal/client-go/go.sum | 1 - .../model_identity_credentials_password.go | 37 +++++++++++++++++++ .../model_identity_credentials_password.go | 37 +++++++++++++++++++ spec/api.json | 4 ++ spec/swagger.json | 4 ++ 5 files changed, 82 insertions(+), 1 deletion(-) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/client-go/model_identity_credentials_password.go b/internal/client-go/model_identity_credentials_password.go index 85f942fb6852..df1900568bb3 100644 --- a/internal/client-go/model_identity_credentials_password.go +++ b/internal/client-go/model_identity_credentials_password.go @@ -19,6 +19,8 @@ import ( type IdentityCredentialsPassword struct { // HashedPassword is a hash-representation of the password. HashedPassword *string `json:"hashed_password,omitempty"` + // UsePasswordMigrationHook is set to true if the password should be migrated using the password migration hook. If set, and the HashedPassword is empty, a webhook will be called during login to migrate the password. + UsePasswordMigrationHook *bool `json:"use_password_migration_hook,omitempty"` } // NewIdentityCredentialsPassword instantiates a new IdentityCredentialsPassword object @@ -70,11 +72,46 @@ func (o *IdentityCredentialsPassword) SetHashedPassword(v string) { o.HashedPassword = &v } +// GetUsePasswordMigrationHook returns the UsePasswordMigrationHook field value if set, zero value otherwise. +func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHook() bool { + if o == nil || o.UsePasswordMigrationHook == nil { + var ret bool + return ret + } + return *o.UsePasswordMigrationHook +} + +// GetUsePasswordMigrationHookOk returns a tuple with the UsePasswordMigrationHook field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHookOk() (*bool, bool) { + if o == nil || o.UsePasswordMigrationHook == nil { + return nil, false + } + return o.UsePasswordMigrationHook, true +} + +// HasUsePasswordMigrationHook returns a boolean if a field has been set. +func (o *IdentityCredentialsPassword) HasUsePasswordMigrationHook() bool { + if o != nil && o.UsePasswordMigrationHook != nil { + return true + } + + return false +} + +// SetUsePasswordMigrationHook gets a reference to the given bool and assigns it to the UsePasswordMigrationHook field. +func (o *IdentityCredentialsPassword) SetUsePasswordMigrationHook(v bool) { + o.UsePasswordMigrationHook = &v +} + func (o IdentityCredentialsPassword) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.HashedPassword != nil { toSerialize["hashed_password"] = o.HashedPassword } + if o.UsePasswordMigrationHook != nil { + toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_identity_credentials_password.go b/internal/httpclient/model_identity_credentials_password.go index 85f942fb6852..df1900568bb3 100644 --- a/internal/httpclient/model_identity_credentials_password.go +++ b/internal/httpclient/model_identity_credentials_password.go @@ -19,6 +19,8 @@ import ( type IdentityCredentialsPassword struct { // HashedPassword is a hash-representation of the password. HashedPassword *string `json:"hashed_password,omitempty"` + // UsePasswordMigrationHook is set to true if the password should be migrated using the password migration hook. If set, and the HashedPassword is empty, a webhook will be called during login to migrate the password. + UsePasswordMigrationHook *bool `json:"use_password_migration_hook,omitempty"` } // NewIdentityCredentialsPassword instantiates a new IdentityCredentialsPassword object @@ -70,11 +72,46 @@ func (o *IdentityCredentialsPassword) SetHashedPassword(v string) { o.HashedPassword = &v } +// GetUsePasswordMigrationHook returns the UsePasswordMigrationHook field value if set, zero value otherwise. +func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHook() bool { + if o == nil || o.UsePasswordMigrationHook == nil { + var ret bool + return ret + } + return *o.UsePasswordMigrationHook +} + +// GetUsePasswordMigrationHookOk returns a tuple with the UsePasswordMigrationHook field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHookOk() (*bool, bool) { + if o == nil || o.UsePasswordMigrationHook == nil { + return nil, false + } + return o.UsePasswordMigrationHook, true +} + +// HasUsePasswordMigrationHook returns a boolean if a field has been set. +func (o *IdentityCredentialsPassword) HasUsePasswordMigrationHook() bool { + if o != nil && o.UsePasswordMigrationHook != nil { + return true + } + + return false +} + +// SetUsePasswordMigrationHook gets a reference to the given bool and assigns it to the UsePasswordMigrationHook field. +func (o *IdentityCredentialsPassword) SetUsePasswordMigrationHook(v bool) { + o.UsePasswordMigrationHook = &v +} + func (o IdentityCredentialsPassword) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.HashedPassword != nil { toSerialize["hashed_password"] = o.HashedPassword } + if o.UsePasswordMigrationHook != nil { + toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook + } return json.Marshal(toSerialize) } diff --git a/spec/api.json b/spec/api.json index f8a84f6f7d97..1bb1345dc25c 100644 --- a/spec/api.json +++ b/spec/api.json @@ -1080,6 +1080,10 @@ "hashed_password": { "description": "HashedPassword is a hash-representation of the password.", "type": "string" + }, + "use_password_migration_hook": { + "description": "UsePasswordMigrationHook is set to true if the password should be migrated\nusing the password migration hook. If set, and the HashedPassword is empty, a\nwebhook will be called during login to migrate the password.", + "type": "boolean" } }, "title": "CredentialsPassword is contains the configuration for credentials of the type password.", diff --git a/spec/swagger.json b/spec/swagger.json index 570cb4003d62..e790c71fecb4 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -4211,6 +4211,10 @@ "hashed_password": { "description": "HashedPassword is a hash-representation of the password.", "type": "string" + }, + "use_password_migration_hook": { + "description": "UsePasswordMigrationHook is set to true if the password should be migrated\nusing the password migration hook. If set, and the HashedPassword is empty, a\nwebhook will be called during login to migrate the password.", + "type": "boolean" } } }, From 180287a3153042f586319208b314386074be7554 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Thu, 4 Jul 2024 11:48:18 +0200 Subject: [PATCH 14/71] chore: use label in link/unlink settings nodes (#3977) --- ..._on_linked_credentials-agent=githuber.json | 4 +- ...on_linked_credentials-agent=multiuser.json | 4 +- ..._on_linked_credentials-agent=password.json | 4 +- ...e=should_link_a_connection-flow=fetch.json | 4 +- ...hould_link_a_connection-flow=original.json | 4 +- ...hould_link_a_connection-flow=response.json | 4 +- ...er_does_not_have_oidc_credentials_yet.json | 4 +- ...ink_a_connection_which_already_exists.json | 4 +- ..._connection_not_yet_linked-flow=fetch.json | 4 +- ...a_connection_not_yet_linked-flow=json.json | 4 +- ...an_non-existing_connection-flow=fetch.json | 4 +- ..._an_non-existing_connection-flow=json.json | 4 +- selfservice/strategy/oidc/nodes.go | 8 +- .../strategy/oidc/strategy_settings.go | 5 +- .../strategy/oidc/strategy_settings_test.go | 123 ++++++++++++------ .../profiles/oidc/login/success.spec.ts | 2 +- .../profiles/oidc/settings/success.spec.ts | 2 +- 17 files changed, 115 insertions(+), 73 deletions(-) diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json index 19da7fb7f971..48d0280aff04 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json index dd0dc9e5f179..3b534e240899 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json index 55909b7380a6..94db74f27534 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050002, - "text": "Link ory", + "text": "Link Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json index b775cb07f8b3..37108bfe985a 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json index 19da7fb7f971..48d0280aff04 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json index cda03ca13acb..fc364efa9d90 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json @@ -154,10 +154,10 @@ "meta": { "label": { "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info", "context": { - "provider": "ory" + "provider": "Ory" } } } diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json index 1763aae80238..cc010fb2c206 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050002, - "text": "Link ory", + "text": "Link Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json index a8b9407aab8a..ddaaf6905c12 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json @@ -154,10 +154,10 @@ "meta": { "label": { "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info", "context": { - "provider": "ory" + "provider": "Ory" } } } diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json index 19da7fb7f971..48d0280aff04 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json index a8b9407aab8a..ddaaf6905c12 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json @@ -154,10 +154,10 @@ "meta": { "label": { "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info", "context": { - "provider": "ory" + "provider": "Ory" } } } diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json index 19da7fb7f971..48d0280aff04 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json @@ -153,10 +153,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json index a8b9407aab8a..ddaaf6905c12 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json @@ -154,10 +154,10 @@ "meta": { "label": { "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info", "context": { - "provider": "ory" + "provider": "Ory" } } } diff --git a/selfservice/strategy/oidc/nodes.go b/selfservice/strategy/oidc/nodes.go index e60dd6324d1a..3dc725e0d967 100644 --- a/selfservice/strategy/oidc/nodes.go +++ b/selfservice/strategy/oidc/nodes.go @@ -8,10 +8,10 @@ import ( "github.com/ory/kratos/ui/node" ) -func NewLinkNode(provider string) *node.Node { - return node.NewInputField("link", provider, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateLinkOIDC(provider)) +func NewLinkNode(providerID, providerLabel string) *node.Node { + return node.NewInputField("link", providerID, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateLinkOIDC(providerLabel)) } -func NewUnlinkNode(provider string) *node.Node { - return node.NewInputField("unlink", provider, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC(provider)) +func NewUnlinkNode(providerID, providerLabel string) *node.Node { + return node.NewInputField("unlink", providerID, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC(providerLabel)) } diff --git a/selfservice/strategy/oidc/strategy_settings.go b/selfservice/strategy/oidc/strategy_settings.go index 4fde3a457548..d4a92056a20b 100644 --- a/selfservice/strategy/oidc/strategy_settings.go +++ b/selfservice/strategy/oidc/strategy_settings.go @@ -12,6 +12,7 @@ import ( "time" "github.com/ory/x/sqlxx" + "github.com/ory/x/stringsx" "github.com/tidwall/sjson" @@ -173,7 +174,7 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity if l.Config().OrganizationID != "" { continue } - sr.UI.GetNodes().Append(NewLinkNode(l.Config().ID)) + sr.UI.GetNodes().Append(NewLinkNode(l.Config().ID, stringsx.Coalesce(l.Config().Label, l.Config().ID))) } count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(r.Context(), confidential) @@ -185,7 +186,7 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity // This means that we're able to remove a connection because it is the last configured credential. If it is // removed, the identity is no longer able to sign in. for _, l := range linked { - sr.UI.GetNodes().Append(NewUnlinkNode(l.Config().ID)) + sr.UI.GetNodes().Append(NewUnlinkNode(l.Config().ID, stringsx.Coalesce(l.Config().Label, l.Config().ID))) } } diff --git a/selfservice/strategy/oidc/strategy_settings_test.go b/selfservice/strategy/oidc/strategy_settings_test.go index 753bae5321b8..65e5ab30600c 100644 --- a/selfservice/strategy/oidc/strategy_settings_test.go +++ b/selfservice/strategy/oidc/strategy_settings_test.go @@ -7,6 +7,7 @@ import ( "context" _ "embed" "encoding/json" + "fmt" "net/http" "net/url" "strconv" @@ -15,6 +16,7 @@ import ( "github.com/ory/x/snapshotx" + "github.com/ory/kratos/driver" kratos "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/ui/container" "github.com/ory/kratos/ui/node" @@ -28,7 +30,6 @@ import ( "github.com/ory/x/sqlxx" - "github.com/ory/kratos/driver" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" @@ -36,6 +37,7 @@ import ( "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/settings" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" "github.com/ory/kratos/selfservice/strategy/oidc" "github.com/ory/kratos/x" ) @@ -67,7 +69,9 @@ func TestSettingsStrategy(t *testing.T) { viperSetProviderConfig( t, conf, - newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "ory"), + newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "ory", func(c *oidc.Configuration) { + c.Label = "Ory" + }), newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "google"), newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "github"), orgSSO, @@ -609,21 +613,27 @@ func TestSettingsStrategy(t *testing.T) { } func TestPopulateSettingsMethod(t *testing.T) { - ctx := context.Background() - nreg := func(t *testing.T, conf *oidc.ConfigurationCollection) *driver.RegistryDefault { - c, reg := internal.NewFastRegistryWithMocks(t) - - testhelpers.SetDefaultIdentitySchema(c, "file://stub/registration.schema.json") - c.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + t.Parallel() + nCtx := func(t *testing.T, conf *oidc.ConfigurationCollection) (*driver.RegistryDefault, context.Context) { + _, reg := internal.NewFastRegistryWithMocks(t) + ctx := context.Background() + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/registration.schema.json") + ctx = confighelpers.WithConfigValue(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + baseKey := fmt.Sprintf("%s.%s", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeOIDC) + + ctx = confighelpers.WithConfigValues(ctx, map[string]interface{}{ + baseKey + ".enabled": true, + baseKey + ".config": conf, + }) // Enabled per default: // conf.Set(ctx, configuration.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true}) - viperSetProviderConfig(t, c, conf.Providers...) - return reg + // viperSetProviderConfig(t, c, conf.Providers...) + return reg, ctx } - ns := func(t *testing.T, reg *driver.RegistryDefault) *oidc.Strategy { - ss, err := reg.SettingsStrategies(context.Background()).Strategy(identity.CredentialsTypeOIDC.String()) + ns := func(t *testing.T, reg *driver.RegistryDefault, ctx context.Context) *oidc.Strategy { + ss, err := reg.SettingsStrategies(ctx).Strategy(identity.CredentialsTypeOIDC.String()) require.NoError(t, err) return ss.(*oidc.Strategy) } @@ -632,13 +642,14 @@ func TestPopulateSettingsMethod(t *testing.T) { return &settings.Flow{Type: flow.TypeBrowser, ID: x.NewUUID(), UI: container.New("")} } - populate := func(t *testing.T, reg *driver.RegistryDefault, i *identity.Identity, req *settings.Flow) *container.Container { - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) - require.NoError(t, ns(t, reg).PopulateSettingsMethod(new(http.Request), i, req)) - require.NotNil(t, req.UI) - require.NotNil(t, req.UI.Nodes) - assert.Equal(t, "POST", req.UI.Method) - return req.UI + populate := func(t *testing.T, reg *driver.RegistryDefault, ctx context.Context, i *identity.Identity, f *settings.Flow) *container.Container { + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + req := new(http.Request) + require.NoError(t, ns(t, reg, ctx).PopulateSettingsMethod(req.WithContext(ctx), i, f)) + require.NotNil(t, f.UI) + require.NotNil(t, f.UI.Nodes) + assert.Equal(t, "POST", f.UI.Method) + return f.UI } defaultConfig := []oidc.Configuration{ @@ -648,12 +659,14 @@ func TestPopulateSettingsMethod(t *testing.T) { } t.Run("case=should not populate non-browser flow", func(t *testing.T) { - reg := nreg(t, &oidc.ConfigurationCollection{Providers: []oidc.Configuration{{Provider: "generic", ID: "github"}}}) + t.Parallel() + reg, ctx := nCtx(t, &oidc.ConfigurationCollection{Providers: []oidc.Configuration{{Provider: "generic", ID: "github"}}}) i := &identity.Identity{Traits: []byte(`{"subject":"foo@bar.com"}`)} - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) - req := &settings.Flow{Type: flow.TypeAPI, ID: x.NewUUID(), UI: container.New("")} - require.NoError(t, ns(t, reg).PopulateSettingsMethod(new(http.Request), i, req)) - require.Empty(t, req.UI.Nodes) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + f := &settings.Flow{Type: flow.TypeAPI, ID: x.NewUUID(), UI: container.New("")} + req := new(http.Request) + require.NoError(t, ns(t, reg, ctx).PopulateSettingsMethod(req.WithContext(ctx), i, f)) + require.Empty(t, f.UI.Nodes) }) for k, tc := range []struct { @@ -674,25 +687,25 @@ func TestPopulateSettingsMethod(t *testing.T) { }, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("github"), + oidc.NewLinkNode("github", "github"), }, }, { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("facebook"), - oidc.NewLinkNode("google"), - oidc.NewLinkNode("github"), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("google", "google"), + oidc.NewLinkNode("github", "github"), }, }, { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("facebook"), - oidc.NewLinkNode("google"), - oidc.NewLinkNode("github"), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("google", "google"), + oidc.NewLinkNode("github", "github"), }, i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{}, Config: []byte(`{}`)}, }, @@ -700,8 +713,8 @@ func TestPopulateSettingsMethod(t *testing.T) { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("facebook"), - oidc.NewLinkNode("github"), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("github", "github"), }, i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{ "google:1234", @@ -711,9 +724,9 @@ func TestPopulateSettingsMethod(t *testing.T) { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("facebook"), - oidc.NewLinkNode("github"), - oidc.NewUnlinkNode("google"), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("github", "github"), + oidc.NewUnlinkNode("google", "google"), }, withpw: true, i: &identity.Credentials{ @@ -727,9 +740,9 @@ func TestPopulateSettingsMethod(t *testing.T) { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("github"), - oidc.NewUnlinkNode("google"), - oidc.NewUnlinkNode("facebook"), + oidc.NewLinkNode("github", "github"), + oidc.NewUnlinkNode("google", "google"), + oidc.NewUnlinkNode("facebook", "facebook"), }, i: &identity.Credentials{ Type: identity.CredentialsTypeOIDC, Identifiers: []string{ @@ -739,9 +752,37 @@ func TestPopulateSettingsMethod(t *testing.T) { Config: []byte(`{"providers":[{"provider":"google","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`), }, }, + { + c: []oidc.Configuration{ + {Provider: "generic", ID: "labeled", Label: "Labeled"}, + }, + e: node.Nodes{ + node.NewCSRFNode(x.FakeCSRFToken), + oidc.NewLinkNode("labeled", "Labeled"), + }, + }, + { + c: []oidc.Configuration{ + {Provider: "generic", ID: "labeled", Label: "Labeled"}, + {Provider: "generic", ID: "facebook"}, + }, + e: node.Nodes{ + node.NewCSRFNode(x.FakeCSRFToken), + oidc.NewUnlinkNode("labeled", "Labeled"), + oidc.NewUnlinkNode("facebook", "facebook"), + }, + i: &identity.Credentials{ + Type: identity.CredentialsTypeOIDC, Identifiers: []string{ + "labeled:1234", + "facebook:1234", + }, + Config: []byte(`{"providers":[{"provider":"labeled","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`), + }, + }, } { t.Run("iteration="+strconv.Itoa(k), func(t *testing.T) { - reg := nreg(t, &oidc.ConfigurationCollection{Providers: tc.c}) + t.Parallel() + reg, ctx := nCtx(t, &oidc.ConfigurationCollection{Providers: tc.c}) i := &identity.Identity{ Traits: []byte(`{"subject":"foo@bar.com"}`), Credentials: make(map[identity.CredentialsType]identity.Credentials, 2), @@ -756,7 +797,7 @@ func TestPopulateSettingsMethod(t *testing.T) { Config: []byte(`{"hashed_password":"$argon2id$..."}`), } } - actual := populate(t, reg, i, nr()) + actual := populate(t, reg, ctx, i, nr()) assert.EqualValues(t, tc.e, actual.Nodes) }) } diff --git a/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts index 8e381e7acf5d..153b3332dd81 100644 --- a/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts @@ -70,7 +70,7 @@ context("Social Sign In Successes", () => { cy.visit(settings) cy.get('[value="hydra"]') .should("have.attr", "name", "unlink") - .should("contain.text", "Unlink hydra") + .should("contain.text", "Unlink Ory") }) it("should be able to sign up with redirects", () => { diff --git a/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts index 674e24e0d668..5f22ae886338 100644 --- a/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts @@ -94,7 +94,7 @@ context("Social Sign In Settings Success", () => { cy.get('[value="hydra"]') .should("have.attr", "name", "unlink") - .should("contain.text", "Unlink hydra") + .should("contain.text", "Unlink Ory") }) it("should link google", () => { From 7674f46c86fb900f1e2150ccfb721b0bf3f23d88 Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:38:23 +0000 Subject: [PATCH 15/71] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 531 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 365 insertions(+), 166 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58628071c945..41491e3bebf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,286 +5,294 @@ **Table of Contents** -- [ (2024-04-26)](#2024-04-26) +- [ (2024-07-04)](#2024-07-04) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) + - [Documentation](#documentation) - [Features](#features) - [Tests](#tests) - - [Unclassified](#unclassified) -- [1.1.0 (2024-02-20)](#110-2024-02-20) +- [1.2.0 (2024-06-05)](#120-2024-06-05) - [Breaking Changes](#breaking-changes-1) - [Bug Fixes](#bug-fixes-1) - [Code Generation](#code-generation) - - [Documentation](#documentation) + - [Documentation](#documentation-1) - [Features](#features-1) - - [Reverts](#reverts) - [Tests](#tests-1) + - [Unclassified](#unclassified) +- [1.1.0 (2024-02-20)](#110-2024-02-20) + - [Breaking Changes](#breaking-changes-2) + - [Bug Fixes](#bug-fixes-2) + - [Code Generation](#code-generation-1) + - [Documentation](#documentation-2) + - [Features](#features-2) + - [Reverts](#reverts) + - [Tests](#tests-2) - [Unclassified](#unclassified-1) - [1.0.0 (2023-07-12)](#100-2023-07-12) - - [Bug Fixes](#bug-fixes-2) - - [Code Generation](#code-generation-1) - - [Documentation](#documentation-1) - - [Features](#features-2) - - [Tests](#tests-2) + - [Bug Fixes](#bug-fixes-3) + - [Code Generation](#code-generation-2) + - [Documentation](#documentation-3) + - [Features](#features-3) + - [Tests](#tests-3) - [Unclassified](#unclassified-2) - [0.13.0 (2023-04-18)](#0130-2023-04-18) - - [Breaking Changes](#breaking-changes-2) - - [Bug Fixes](#bug-fixes-3) - - [Code Generation](#code-generation-2) - - [Code Refactoring](#code-refactoring) - - [Documentation](#documentation-2) - - [Features](#features-3) - - [Tests](#tests-3) - - [Unclassified](#unclassified-3) -- [0.11.1 (2023-01-14)](#0111-2023-01-14) - [Breaking Changes](#breaking-changes-3) - [Bug Fixes](#bug-fixes-4) - [Code Generation](#code-generation-3) - - [Documentation](#documentation-3) + - [Code Refactoring](#code-refactoring) + - [Documentation](#documentation-4) - [Features](#features-4) - [Tests](#tests-4) -- [0.11.0 (2022-12-02)](#0110-2022-12-02) - - [Code Generation](#code-generation-4) - - [Features](#features-5) -- [0.11.0-alpha.0.pre.2 (2022-11-28)](#0110-alpha0pre2-2022-11-28) + - [Unclassified](#unclassified-3) +- [0.11.1 (2023-01-14)](#0111-2023-01-14) - [Breaking Changes](#breaking-changes-4) - [Bug Fixes](#bug-fixes-5) - - [Code Generation](#code-generation-5) + - [Code Generation](#code-generation-4) + - [Documentation](#documentation-5) + - [Features](#features-5) + - [Tests](#tests-5) +- [0.11.0 (2022-12-02)](#0110-2022-12-02) + - [Code Generation](#code-generation-5) + - [Features](#features-6) +- [0.11.0-alpha.0.pre.2 (2022-11-28)](#0110-alpha0pre2-2022-11-28) + - [Breaking Changes](#breaking-changes-5) + - [Bug Fixes](#bug-fixes-6) + - [Code Generation](#code-generation-6) - [Code Refactoring](#code-refactoring-1) - - [Documentation](#documentation-4) - - [Features](#features-6) + - [Documentation](#documentation-6) + - [Features](#features-7) - [Reverts](#reverts-1) - - [Tests](#tests-5) + - [Tests](#tests-6) - [Unclassified](#unclassified-4) - [0.10.1 (2022-06-01)](#0101-2022-06-01) - - [Bug Fixes](#bug-fixes-6) - - [Code Generation](#code-generation-6) + - [Bug Fixes](#bug-fixes-7) + - [Code Generation](#code-generation-7) - [0.10.0 (2022-05-30)](#0100-2022-05-30) - - [Breaking Changes](#breaking-changes-5) - - [Bug Fixes](#bug-fixes-7) - - [Code Generation](#code-generation-7) - - [Code Refactoring](#code-refactoring-2) - - [Documentation](#documentation-5) - - [Features](#features-7) - - [Tests](#tests-6) - - [Unclassified](#unclassified-5) -- [0.9.0-alpha.3 (2022-03-25)](#090-alpha3-2022-03-25) - [Breaking Changes](#breaking-changes-6) - [Bug Fixes](#bug-fixes-8) - [Code Generation](#code-generation-8) - - [Documentation](#documentation-6) -- [0.9.0-alpha.2 (2022-03-22)](#090-alpha2-2022-03-22) - - [Bug Fixes](#bug-fixes-9) - - [Code Generation](#code-generation-9) -- [0.9.0-alpha.1 (2022-03-21)](#090-alpha1-2022-03-21) - - [Breaking Changes](#breaking-changes-7) - - [Bug Fixes](#bug-fixes-10) - - [Code Generation](#code-generation-10) - - [Code Refactoring](#code-refactoring-3) + - [Code Refactoring](#code-refactoring-2) - [Documentation](#documentation-7) - [Features](#features-8) - [Tests](#tests-7) - - [Unclassified](#unclassified-6) -- [0.8.3-alpha.1.pre.0 (2022-01-21)](#083-alpha1pre0-2022-01-21) + - [Unclassified](#unclassified-5) +- [0.9.0-alpha.3 (2022-03-25)](#090-alpha3-2022-03-25) + - [Breaking Changes](#breaking-changes-7) + - [Bug Fixes](#bug-fixes-9) + - [Code Generation](#code-generation-9) + - [Documentation](#documentation-8) +- [0.9.0-alpha.2 (2022-03-22)](#090-alpha2-2022-03-22) + - [Bug Fixes](#bug-fixes-10) + - [Code Generation](#code-generation-10) +- [0.9.0-alpha.1 (2022-03-21)](#090-alpha1-2022-03-21) - [Breaking Changes](#breaking-changes-8) - [Bug Fixes](#bug-fixes-11) - [Code Generation](#code-generation-11) - - [Code Refactoring](#code-refactoring-4) - - [Documentation](#documentation-8) + - [Code Refactoring](#code-refactoring-3) + - [Documentation](#documentation-9) - [Features](#features-9) - [Tests](#tests-8) + - [Unclassified](#unclassified-6) +- [0.8.3-alpha.1.pre.0 (2022-01-21)](#083-alpha1pre0-2022-01-21) + - [Breaking Changes](#breaking-changes-9) + - [Bug Fixes](#bug-fixes-12) + - [Code Generation](#code-generation-12) + - [Code Refactoring](#code-refactoring-4) + - [Documentation](#documentation-10) + - [Features](#features-10) + - [Tests](#tests-9) - [0.8.2-alpha.1 (2021-12-17)](#082-alpha1-2021-12-17) - - [Bug Fixes](#bug-fixes-12) - - [Code Generation](#code-generation-12) - - [Documentation](#documentation-9) -- [0.8.1-alpha.1 (2021-12-13)](#081-alpha1-2021-12-13) - [Bug Fixes](#bug-fixes-13) - [Code Generation](#code-generation-13) - - [Documentation](#documentation-10) - - [Features](#features-10) - - [Tests](#tests-9) + - [Documentation](#documentation-11) +- [0.8.1-alpha.1 (2021-12-13)](#081-alpha1-2021-12-13) + - [Bug Fixes](#bug-fixes-14) + - [Code Generation](#code-generation-14) + - [Documentation](#documentation-12) + - [Features](#features-11) + - [Tests](#tests-10) - [0.8.0-alpha.4.pre.0 (2021-11-09)](#080-alpha4pre0-2021-11-09) - - [Breaking Changes](#breaking-changes-9) - - [Bug Fixes](#bug-fixes-14) - - [Code Generation](#code-generation-14) - - [Documentation](#documentation-11) - - [Features](#features-11) - - [Tests](#tests-10) + - [Breaking Changes](#breaking-changes-10) + - [Bug Fixes](#bug-fixes-15) + - [Code Generation](#code-generation-15) + - [Documentation](#documentation-13) + - [Features](#features-12) + - [Tests](#tests-11) - [0.8.0-alpha.3 (2021-10-28)](#080-alpha3-2021-10-28) - - [Bug Fixes](#bug-fixes-15) - - [Code Generation](#code-generation-15) -- [0.8.0-alpha.2 (2021-10-28)](#080-alpha2-2021-10-28) + - [Bug Fixes](#bug-fixes-16) - [Code Generation](#code-generation-16) +- [0.8.0-alpha.2 (2021-10-28)](#080-alpha2-2021-10-28) + - [Code Generation](#code-generation-17) - [0.8.0-alpha.1 (2021-10-27)](#080-alpha1-2021-10-27) - - [Breaking Changes](#breaking-changes-10) - - [Bug Fixes](#bug-fixes-16) - - [Code Generation](#code-generation-17) + - [Breaking Changes](#breaking-changes-11) + - [Bug Fixes](#bug-fixes-17) + - [Code Generation](#code-generation-18) - [Code Refactoring](#code-refactoring-5) - - [Documentation](#documentation-12) - - [Features](#features-12) + - [Documentation](#documentation-14) + - [Features](#features-13) - [Reverts](#reverts-2) - - [Tests](#tests-11) + - [Tests](#tests-12) - [Unclassified](#unclassified-7) - [0.7.6-alpha.1 (2021-09-12)](#076-alpha1-2021-09-12) - - [Code Generation](#code-generation-18) -- [0.7.5-alpha.1 (2021-09-11)](#075-alpha1-2021-09-11) - [Code Generation](#code-generation-19) -- [0.7.4-alpha.1 (2021-09-09)](#074-alpha1-2021-09-09) - - [Bug Fixes](#bug-fixes-17) +- [0.7.5-alpha.1 (2021-09-11)](#075-alpha1-2021-09-11) - [Code Generation](#code-generation-20) - - [Documentation](#documentation-13) - - [Features](#features-13) - - [Tests](#tests-12) -- [0.7.3-alpha.1 (2021-08-28)](#073-alpha1-2021-08-28) +- [0.7.4-alpha.1 (2021-09-09)](#074-alpha1-2021-09-09) - [Bug Fixes](#bug-fixes-18) - [Code Generation](#code-generation-21) - - [Documentation](#documentation-14) + - [Documentation](#documentation-15) - [Features](#features-14) -- [0.7.1-alpha.1 (2021-07-22)](#071-alpha1-2021-07-22) + - [Tests](#tests-13) +- [0.7.3-alpha.1 (2021-08-28)](#073-alpha1-2021-08-28) - [Bug Fixes](#bug-fixes-19) - [Code Generation](#code-generation-22) - - [Documentation](#documentation-15) - - [Tests](#tests-13) + - [Documentation](#documentation-16) + - [Features](#features-15) +- [0.7.1-alpha.1 (2021-07-22)](#071-alpha1-2021-07-22) + - [Bug Fixes](#bug-fixes-20) + - [Code Generation](#code-generation-23) + - [Documentation](#documentation-17) + - [Tests](#tests-14) - [0.7.0-alpha.1 (2021-07-13)](#070-alpha1-2021-07-13) - - [Breaking Changes](#breaking-changes-11) - - [Bug Fixes](#bug-fixes-20) - - [Code Generation](#code-generation-23) - - [Code Refactoring](#code-refactoring-6) - - [Documentation](#documentation-16) - - [Features](#features-15) - - [Tests](#tests-14) - - [Unclassified](#unclassified-8) -- [0.6.3-alpha.1 (2021-05-17)](#063-alpha1-2021-05-17) - [Breaking Changes](#breaking-changes-12) - [Bug Fixes](#bug-fixes-21) - [Code Generation](#code-generation-24) + - [Code Refactoring](#code-refactoring-6) + - [Documentation](#documentation-18) + - [Features](#features-16) + - [Tests](#tests-15) + - [Unclassified](#unclassified-8) +- [0.6.3-alpha.1 (2021-05-17)](#063-alpha1-2021-05-17) + - [Breaking Changes](#breaking-changes-13) + - [Bug Fixes](#bug-fixes-22) + - [Code Generation](#code-generation-25) - [Code Refactoring](#code-refactoring-7) - [0.6.2-alpha.1 (2021-05-14)](#062-alpha1-2021-05-14) - - [Code Generation](#code-generation-25) - - [Documentation](#documentation-17) -- [0.6.1-alpha.1 (2021-05-11)](#061-alpha1-2021-05-11) - [Code Generation](#code-generation-26) - - [Features](#features-16) -- [0.6.0-alpha.2 (2021-05-07)](#060-alpha2-2021-05-07) - - [Bug Fixes](#bug-fixes-22) + - [Documentation](#documentation-19) +- [0.6.1-alpha.1 (2021-05-11)](#061-alpha1-2021-05-11) - [Code Generation](#code-generation-27) - [Features](#features-17) +- [0.6.0-alpha.2 (2021-05-07)](#060-alpha2-2021-05-07) + - [Bug Fixes](#bug-fixes-23) + - [Code Generation](#code-generation-28) + - [Features](#features-18) - [0.6.0-alpha.1 (2021-05-05)](#060-alpha1-2021-05-05) - - [Breaking Changes](#breaking-changes-13) - - [Bug Fixes](#bug-fixes-23) - - [Code Generation](#code-generation-28) + - [Breaking Changes](#breaking-changes-14) + - [Bug Fixes](#bug-fixes-24) + - [Code Generation](#code-generation-29) - [Code Refactoring](#code-refactoring-8) - - [Documentation](#documentation-18) - - [Features](#features-18) - - [Tests](#tests-15) + - [Documentation](#documentation-20) + - [Features](#features-19) + - [Tests](#tests-16) - [Unclassified](#unclassified-9) - [0.5.5-alpha.1 (2020-12-09)](#055-alpha1-2020-12-09) - - [Bug Fixes](#bug-fixes-24) - - [Code Generation](#code-generation-29) - - [Documentation](#documentation-19) - - [Features](#features-19) - - [Tests](#tests-16) - - [Unclassified](#unclassified-10) -- [0.5.4-alpha.1 (2020-11-11)](#054-alpha1-2020-11-11) - [Bug Fixes](#bug-fixes-25) - [Code Generation](#code-generation-30) - - [Code Refactoring](#code-refactoring-9) - - [Documentation](#documentation-20) + - [Documentation](#documentation-21) - [Features](#features-20) -- [0.5.3-alpha.1 (2020-10-27)](#053-alpha1-2020-10-27) + - [Tests](#tests-17) + - [Unclassified](#unclassified-10) +- [0.5.4-alpha.1 (2020-11-11)](#054-alpha1-2020-11-11) - [Bug Fixes](#bug-fixes-26) - [Code Generation](#code-generation-31) - - [Documentation](#documentation-21) + - [Code Refactoring](#code-refactoring-9) + - [Documentation](#documentation-22) - [Features](#features-21) - - [Tests](#tests-17) -- [0.5.2-alpha.1 (2020-10-22)](#052-alpha1-2020-10-22) +- [0.5.3-alpha.1 (2020-10-27)](#053-alpha1-2020-10-27) - [Bug Fixes](#bug-fixes-27) - [Code Generation](#code-generation-32) - - [Documentation](#documentation-22) + - [Documentation](#documentation-23) + - [Features](#features-22) - [Tests](#tests-18) -- [0.5.1-alpha.1 (2020-10-20)](#051-alpha1-2020-10-20) +- [0.5.2-alpha.1 (2020-10-22)](#052-alpha1-2020-10-22) - [Bug Fixes](#bug-fixes-28) - [Code Generation](#code-generation-33) - - [Documentation](#documentation-23) - - [Features](#features-22) + - [Documentation](#documentation-24) - [Tests](#tests-19) +- [0.5.1-alpha.1 (2020-10-20)](#051-alpha1-2020-10-20) + - [Bug Fixes](#bug-fixes-29) + - [Code Generation](#code-generation-34) + - [Documentation](#documentation-25) + - [Features](#features-23) + - [Tests](#tests-20) - [Unclassified](#unclassified-11) - [0.5.0-alpha.1 (2020-10-15)](#050-alpha1-2020-10-15) - - [Breaking Changes](#breaking-changes-14) - - [Bug Fixes](#bug-fixes-29) - - [Code Generation](#code-generation-34) + - [Breaking Changes](#breaking-changes-15) + - [Bug Fixes](#bug-fixes-30) + - [Code Generation](#code-generation-35) - [Code Refactoring](#code-refactoring-10) - - [Documentation](#documentation-24) - - [Features](#features-23) - - [Tests](#tests-20) + - [Documentation](#documentation-26) + - [Features](#features-24) + - [Tests](#tests-21) - [Unclassified](#unclassified-12) - [0.4.6-alpha.1 (2020-07-13)](#046-alpha1-2020-07-13) - - [Bug Fixes](#bug-fixes-30) - - [Code Generation](#code-generation-35) -- [0.4.5-alpha.1 (2020-07-13)](#045-alpha1-2020-07-13) - [Bug Fixes](#bug-fixes-31) - [Code Generation](#code-generation-36) -- [0.4.4-alpha.1 (2020-07-10)](#044-alpha1-2020-07-10) +- [0.4.5-alpha.1 (2020-07-13)](#045-alpha1-2020-07-13) - [Bug Fixes](#bug-fixes-32) - [Code Generation](#code-generation-37) - - [Documentation](#documentation-25) -- [0.4.3-alpha.1 (2020-07-08)](#043-alpha1-2020-07-08) +- [0.4.4-alpha.1 (2020-07-10)](#044-alpha1-2020-07-10) - [Bug Fixes](#bug-fixes-33) - [Code Generation](#code-generation-38) -- [0.4.2-alpha.1 (2020-07-08)](#042-alpha1-2020-07-08) + - [Documentation](#documentation-27) +- [0.4.3-alpha.1 (2020-07-08)](#043-alpha1-2020-07-08) - [Bug Fixes](#bug-fixes-34) - [Code Generation](#code-generation-39) +- [0.4.2-alpha.1 (2020-07-08)](#042-alpha1-2020-07-08) + - [Bug Fixes](#bug-fixes-35) + - [Code Generation](#code-generation-40) - [0.4.0-alpha.1 (2020-07-08)](#040-alpha1-2020-07-08) - - [Breaking Changes](#breaking-changes-15) - - [Bug Fixes](#bug-fixes-35) - - [Code Generation](#code-generation-40) + - [Breaking Changes](#breaking-changes-16) + - [Bug Fixes](#bug-fixes-36) + - [Code Generation](#code-generation-41) - [Code Refactoring](#code-refactoring-11) - - [Documentation](#documentation-26) - - [Features](#features-24) + - [Documentation](#documentation-28) + - [Features](#features-25) - [Unclassified](#unclassified-13) - [0.3.0-alpha.1 (2020-05-15)](#030-alpha1-2020-05-15) - - [Breaking Changes](#breaking-changes-16) - - [Bug Fixes](#bug-fixes-36) + - [Breaking Changes](#breaking-changes-17) + - [Bug Fixes](#bug-fixes-37) - [Chores](#chores) - [Code Refactoring](#code-refactoring-12) - - [Documentation](#documentation-27) - - [Features](#features-25) + - [Documentation](#documentation-29) + - [Features](#features-26) - [Unclassified](#unclassified-14) - [0.2.1-alpha.1 (2020-05-05)](#021-alpha1-2020-05-05) - [Chores](#chores-1) - - [Documentation](#documentation-28) + - [Documentation](#documentation-30) - [0.2.0-alpha.2 (2020-05-04)](#020-alpha2-2020-05-04) - - [Breaking Changes](#breaking-changes-17) - - [Bug Fixes](#bug-fixes-37) + - [Breaking Changes](#breaking-changes-18) + - [Bug Fixes](#bug-fixes-38) - [Chores](#chores-2) - [Code Refactoring](#code-refactoring-13) - - [Documentation](#documentation-29) - - [Features](#features-26) + - [Documentation](#documentation-31) + - [Features](#features-27) - [Unclassified](#unclassified-15) - [0.1.1-alpha.1 (2020-02-18)](#011-alpha1-2020-02-18) - - [Bug Fixes](#bug-fixes-38) + - [Bug Fixes](#bug-fixes-39) - [Code Refactoring](#code-refactoring-14) - - [Documentation](#documentation-30) + - [Documentation](#documentation-32) - [0.1.0-alpha.6 (2020-02-16)](#010-alpha6-2020-02-16) - - [Bug Fixes](#bug-fixes-39) + - [Bug Fixes](#bug-fixes-40) - [Code Refactoring](#code-refactoring-15) - - [Documentation](#documentation-31) - - [Features](#features-27) -- [0.1.0-alpha.5 (2020-02-06)](#010-alpha5-2020-02-06) - - [Documentation](#documentation-32) + - [Documentation](#documentation-33) - [Features](#features-28) +- [0.1.0-alpha.5 (2020-02-06)](#010-alpha5-2020-02-06) + - [Documentation](#documentation-34) + - [Features](#features-29) - [0.1.0-alpha.4 (2020-02-06)](#010-alpha4-2020-02-06) - [Continuous Integration](#continuous-integration) - - [Documentation](#documentation-33) + - [Documentation](#documentation-35) - [0.1.0-alpha.3 (2020-02-06)](#010-alpha3-2020-02-06) - [Continuous Integration](#continuous-integration-1) - [0.1.0-alpha.2 (2020-02-03)](#010-alpha2-2020-02-03) - - [Bug Fixes](#bug-fixes-40) - - [Documentation](#documentation-34) - - [Features](#features-29) + - [Bug Fixes](#bug-fixes-41) + - [Documentation](#documentation-36) + - [Features](#features-30) - [Unclassified](#unclassified-16) - [0.1.0-alpha.1 (2020-01-31)](#010-alpha1-2020-01-31) - - [Documentation](#documentation-35) + - [Documentation](#documentation-37) - [0.0.3-alpha.15 (2020-01-31)](#003-alpha15-2020-01-31) - [Unclassified](#unclassified-17) - [0.0.3-alpha.14 (2020-01-31)](#003-alpha14-2020-01-31) @@ -317,19 +325,172 @@ - [Unclassified](#unclassified-28) - [0.0.1-alpha.3 (2020-01-28)](#001-alpha3-2020-01-28) - [Continuous Integration](#continuous-integration-6) - - [Documentation](#documentation-36) + - [Documentation](#documentation-38) - [Unclassified](#unclassified-29) -# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-26) +# [](https://github.com/ory/kratos/compare/v1.2.0...v) (2024-07-04) + +## Breaking Changes + +Going forward, the `/admin/session/.../extend` endpoint will return 204 no +content for new Ory Network projects. We will deprecate returning 200 + session +body in the future. + +### Bug Fixes + +- Jsonnet timeouts ([#3979](https://github.com/ory/kratos/issues/3979)) + ([7c5299f](https://github.com/ory/kratos/commit/7c5299f1f832ebbe0622d0920b7a91253d26b06c)) + +### Documentation + +- Typo in changelog + ([c508980](https://github.com/ory/kratos/commit/c5089801af2a656e9c1fc371a11aeb23918ba359)) + +### Features + +- Allow deletion of an individual OIDC credential + ([#3968](https://github.com/ory/kratos/issues/3968)) + ([a43cef2](https://github.com/ory/kratos/commit/a43cef23c177acddbf8b03afef087feeaca51981)): + + This extends the existing `DELETE /admin/identities/{id}/credentials/{type}` + API to accept an `?identifier=foobar` query parameter for `{type}==oidc` like + such: + + `DELETE /admin/identities/{id}/credentials/oidc?identifier=github%3A012345` + + This will delete the GitHub OIDC credential with the identifier + `github:012345` (`012345` is the subject as returned by GitHub). + + To find out which OIDC credentials exist, call + `GET /admin/identities/{id}?include_credential=oidc` beforehand. + + This will allow you to delete individual OIDC credentials for users even if + they have several set up. + +- Clarify session extend behavior + ([#3962](https://github.com/ory/kratos/issues/3962)) + ([af5ea35](https://github.com/ory/kratos/commit/af5ea35759e74d7a1637823abcc21dc8e3e39a9d)) +- Improve session extend performance + ([#3948](https://github.com/ory/kratos/issues/3948)) + ([4e3fad4](https://github.com/ory/kratos/commit/4e3fad4b4739b5cf00d658155350cb599f2cd06a)): + + This patch improves the performance for extending session lifespans. Lifespan + extension is tricky as it is often part of the middleware of Ory Kratos + consumers. As such, it is prone to transaction contention when we read and + write to the same session row at the same time (and potentially multiple + times). + + To address this, we: + + 1. Introduce a locking mechanism on the row to reduce transaction contention; + 2. Add a new feature flag that toggles returning 204 no content instead of + 200 + session. + + Be aware that all reads on the session table will have to wait for the + transaction to commit before they return a value. This may cause long(er) + response times on `/session/whoami` for sessions that are being extended at + the same time. + +- Password migration hook ([#3978](https://github.com/ory/kratos/issues/3978)) + ([c9d5573](https://github.com/ory/kratos/commit/c9d55730a10b71ac61bb5097f5f9c33f144f2a95)): + + This adds a password migration hook to easily migrate passwords for which we + do not have the hash. + + For each user that needs to be migrated to Ory Network, a new identity is + created with a credential of type password with a config of + {"use_password_migration_hook": true} . When a user logs in, the credential + identifier and password will be sent to the password_migration web hook if all + of these are true: The user’s identity’s password credential is + {"use_password_migration_hook": true} The password_migration hook is + configured After calling the password_migration hook, the HTTP status code + will be inspected: On 200, we parse the response as JSON and look for + {"status": "password_match"}. The password credential config will be replaced + with the hash of the actual password. On any other status code, we assume that + the password is not valid. + +### Tests + +- Deflake and parallelize persister tests + ([#3953](https://github.com/ory/kratos/issues/3953)) + ([61f87d9](https://github.com/ory/kratos/commit/61f87d90bd67e5bb1f00ee110d986e4f72fc4c91)) +- Deflake session extend config side-effect + ([#3950](https://github.com/ory/kratos/issues/3950)) + ([b192c92](https://github.com/ory/kratos/commit/b192c92d6c969d470d6479bc33dbc351d327c1f9)) +- Enable server-side config from context + ([#3954](https://github.com/ory/kratos/issues/3954)) + ([e0001b0](https://github.com/ory/kratos/commit/e0001b0db784457652581366bd7ead7cdf6b3898)) + +# [1.2.0](https://github.com/ory/kratos/compare/v1.1.0...v1.2.0) (2024-06-05) + +Ory Kratos v1.2 is the most complete, scalable, and secure open-source identity +server available. We are thrilled to announce its release! + +![Ory Kratos 1.2 released](https://www.ory.sh/images/newsletter/kratos-1.2.0/banner.png) + +This release introduces two major features: two-step registration and full +PassKey with resident key support. + +Passkeys provide a secure and convenient authentication method, eliminating the +need for passwords while ensuring strong security. With this release, we have +added support for resident keys, enabling offline authentication. Credential +discovery allows users to link existing passkeys to their Ory account +seamlessly. + +[Watch the PassKey demo video](https://github.com/aeneasr/web-next-deprecated/assets/3372410/e676c518-c82a-42a6-821e-28aecadb270c) + +Two-step registration improves the user experience by dividing the registration +process into two steps. Users first enter their identity traits, and then choose +a credential method for authentication, resulting in a streamlined process. This +feature is especially useful when enabling multiple authentication strategies, +as it eliminates the need to repeat identity traits for each strategy. + +![Two-Step Registration](https://ik.imagekit.io/launchnotes/production/tr:w-1640,c-at_max,f-auto/ngul9dzfjdt3pe8benegjjeeagi1) + +The 107 commits since v1.1 include several improvements: + +- **Webhooks** now carry session information if available. +- **Transient Payloads** are now available across all self-service flows. +- **Sign in with Twitter** is now available. +- **Sign in with LinkedIn** now includes an additional v2 provider compatible + with LinkedIn's new SSO API. +- **Two-Step Registration**: An improved registration experience that separates + entering profile information from choosing authentication methods. +- **User Credentials Meta-Information** can now be included on the list + endpoint. +- **Social Sign-In** is now resilient to double-submit issues common with + Facebook and Apple mobile login. + +**Two-Step Registration Enabled by Default**: This is now the default setting. +To disable, set `selfservice.flows.registration.enable_legacy_flow` to `true`. + +- Improved account linking and credential discovery during sign-up. +- The `return_to` parameter is now respected in OIDC API flows. +- Adjustments to database indices. +- Enhanced error messages for security violations. +- Improved SDK types. +- The `verification` and `verification_ui` hooks are now available in the login + flow. +- Webhooks now contain the correct identity state in the after-verification hook + chain. + +We are doing this survey to find out how we can support self-hosted Ory users +better. We strive to provide you with the best product and service possible and +your feedback will help us understand what we're doing well and where we can +improve to better meet your needs. We truly value your opinion and thank you in +advance for taking the time to share your thoughts with us! + +Fill out the +[survey now](https://share-eu1.hsforms.com/15DiCnJpcRuijnpAdnDhxxwextgn)! ## Breaking Changes This feature enables two-step registration per default. Two-step registration is a significantly improved sign up flow and recommended when using more than one sign up methods. To disable two-step registration, set -`selfservice.flows.registration.enable_legacy_one_step` to `true`. This value +`selfservice.flows.registration.enable_legacy_flow` to `true`. This value defaults to `false`. ### Bug Fixes @@ -362,8 +523,13 @@ defaults to `false`. - Audit issues ([#3797](https://github.com/ory/kratos/issues/3797)) ([7017490](https://github.com/ory/kratos/commit/7017490caa9c70e22d5c626773c0266521813ff5)) +- Change return urls in quickstarts + ([#3928](https://github.com/ory/kratos/issues/3928)) + ([9730e09](https://github.com/ory/kratos/commit/9730e099a656d211389d8e993c64d8082784c929)) - Close res body ([#3870](https://github.com/ory/kratos/issues/3870)) ([cc39f8d](https://github.com/ory/kratos/commit/cc39f8df7c235af0df616432bc4f88681896ad85)) +- CVEs in dependencies ([#3902](https://github.com/ory/kratos/issues/3902)) + ([e5d3b0a](https://github.com/ory/kratos/commit/e5d3b0afde3c80c6c9cf8815c56d82e291ede663)) - Db index and duplicate credentials error ([#3896](https://github.com/ory/kratos/issues/3896)) ([9f34a21](https://github.com/ory/kratos/commit/9f34a21ea2035a5d33edd96753023a3c8c6c054c)): @@ -411,6 +577,9 @@ defaults to `false`. - Missing indices and foreign keys ([#3800](https://github.com/ory/kratos/issues/3800)) ([0b32ce1](https://github.com/ory/kratos/commit/0b32ce113be47aa724d3468062ced09f8f60c52a)) +- **oidc:** Grace period for continuity container on oidc callbacks + ([#3915](https://github.com/ory/kratos/issues/3915)) + ([1a9a096](https://github.com/ory/kratos/commit/1a9a096d619925dd3718ad9dd9daf77387572ece)) - Passing transient payloads ([#3838](https://github.com/ory/kratos/issues/3838)) ([d01b670](https://github.com/ory/kratos/commit/d01b6705bf36efb6e0f3d71ed22d0574ab8a98a4)) @@ -473,6 +642,17 @@ defaults to `false`. - fix: transient payload with OIDC login +### Code Generation + +- Pin v1.2.0 release commit + ([1a70648](https://github.com/ory/kratos/commit/1a70648c4d5b9b8d135dd7bea3842057e67b574e)) + +### Documentation + +- Remove delete reference from batch patch identity + ([#3906](https://github.com/ory/kratos/issues/3906)) + ([cd01cb9](https://github.com/ory/kratos/commit/cd01cb9fb23a24e52d46538a9ea63c2144c3b145)) + ### Features - Add `include_credential` query param to `/admin/identities` list call @@ -491,6 +671,9 @@ defaults to `false`. - Add verification hook to login flow ([#3829](https://github.com/ory/kratos/issues/3829)) ([43e4ead](https://github.com/ory/kratos/commit/43e4eadce7fa6e66bf1f9c03136d141bffd3094f)) +- Allow admin to create API code recovery flows + ([#3939](https://github.com/ory/kratos/issues/3939)) + ([25d1ecd](https://github.com/ory/kratos/commit/25d1ecd90317193095e01b97ff21d92920035b02)) - Control edge cache ttl ([#3808](https://github.com/ory/kratos/issues/3808)) ([c9dcce5](https://github.com/ory/kratos/commit/c9dcce5a41137937df1aad7ac81170b443740f88)) - Linkedin v2 provider ([#3804](https://github.com/ory/kratos/issues/3804)) @@ -520,6 +703,22 @@ defaults to `false`. - Resolve failing test for empty tokens ([#3775](https://github.com/ory/kratos/issues/3775)) ([7277368](https://github.com/ory/kratos/commit/7277368bc28df8f0badffc7e739cef20f05e9a02)) +- Resolve flaky e2e tests ([#3935](https://github.com/ory/kratos/issues/3935)) + ([a14927d](https://github.com/ory/kratos/commit/a14927dfa5f8d0fbda7e5a831f0a09a42369e06c)): + + - test: resolve flaky code registration tests + + - chore: don't fail logout if cookie is not found + + - chore: remove .only + + - chore: reduce wait + + - chore: u + + - chore: u + + - chore: u ### Unclassified From a84fb3feab627a99d75f10a20230b87842463ed0 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Fri, 5 Jul 2024 11:28:55 +0200 Subject: [PATCH 16/71] chore: improve courier logging (#3985) --- courier/smtp_channel.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/courier/smtp_channel.go b/courier/smtp_channel.go index 9ed9335f8e7f..15a685bcd7ad 100644 --- a/courier/smtp_channel.go +++ b/courier/smtp_channel.go @@ -107,8 +107,8 @@ func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) error { gm.AddAlternative("text/html", htmlBody) } - if err := c.smtpClient.DialAndSend(ctx, gm); err != nil { - c.d.Logger(). + if err := errors.WithStack(c.smtpClient.DialAndSend(ctx, gm)); err != nil { + logger. WithError(err). Error("Unable to send email using SMTP connection.") From 7e7fdc26467e456674f5eca011187fc213bb833f Mon Sep 17 00:00:00 2001 From: ory-bot <60093411+ory-bot@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:18:50 +0000 Subject: [PATCH 17/71] autogen(docs): regenerate and update changelog [skip ci] --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41491e3bebf5..10b384631715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2024-07-04)](#2024-07-04) +- [ (2024-07-05)](#2024-07-05) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Documentation](#documentation) @@ -330,7 +330,7 @@ -# [](https://github.com/ory/kratos/compare/v1.2.0...v) (2024-07-04) +# [](https://github.com/ory/kratos/compare/v1.2.0...v) (2024-07-05) ## Breaking Changes From b5a66e0dde3a8fa6fdeb727482481b6302589631 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Fri, 5 Jul 2024 12:50:07 +0200 Subject: [PATCH 18/71] fix: move password migration hook config (#3986) This moves the password migration hook to ```yaml selfservice: methods: password: config: migrate_hook: ... ``` --- driver/config/config.go | 2 +- driver/config/config_test.go | 2 +- embedx/config.schema.json | 110 +++++++++++++++++------------------ 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/driver/config/config.go b/driver/config/config.go index ac394d7c8518..81be527a2632 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -203,7 +203,7 @@ const ( ViperKeyClientHTTPPrivateIPExceptionURLs = "clients.http.private_ip_exception_urls" ViperKeyPreviewDefaultReadConsistencyLevel = "preview.default_read_consistency_level" ViperKeyVersion = "version" - ViperKeyPasswordMigrationHook = "selfservice.flows.login.password_migration" + ViperKeyPasswordMigrationHook = "selfservice.methods.password.config.migrate_hook" ) const ( diff --git a/driver/config/config_test.go b/driver/config/config_test.go index dc276eb3a171..8f9dfaaf20ec 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -218,7 +218,7 @@ func TestViperProvider(t *testing.T) { config string enabled bool }{ - {id: "password", enabled: true, config: `{"haveibeenpwned_host":"api.pwnedpasswords.com","haveibeenpwned_enabled":true,"ignore_network_errors":true,"max_breaches":0,"min_password_length":8,"identifier_similarity_check_enabled":true}`}, + {id: "password", enabled: true, config: `{"haveibeenpwned_host":"api.pwnedpasswords.com","haveibeenpwned_enabled":true,"ignore_network_errors":true,"max_breaches":0,"migrate_hook":{"config":{"emit_analytics_event":true,"method":"POST"},"enabled":false},"min_password_length":8,"identifier_similarity_check_enabled":true}`}, {id: "oidc", enabled: true, config: `{"providers":[{"client_id":"a","client_secret":"b","id":"github","provider":"github","mapper_url":"http://test.kratos.ory.sh/default-identity.schema.json"}]}`}, {id: "totp", enabled: true, config: `{"issuer":"issuer.ory.sh"}`}, } { diff --git a/embedx/config.schema.json b/embedx/config.schema.json index e763c402a91a..c62b3c39f00c 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -1303,61 +1303,6 @@ "enum": ["one_step", "identifier_first"], "default": "one_step" }, - "password_migration": { - "type": "object", - "additionalProperties": false, - "properties": { - "enabled": { - "type": "boolean", - "title": "Enable Password Migration", - "description": "If set to true will enable password migration.", - "default": false - }, - "config": { - "type": "object", - "additionalProperties": false, - "properties": { - "url": { - "type": "string", - "description": "The URL the password migration hook should call", - "format": "uri" - }, - "method": { - "type": "string", - "description": "The HTTP method to use (GET, POST, etc).", - "const": "POST", - "default": "POST" - }, - "headers": { - "type": "object", - "description": "The HTTP headers that must be applied to the password migration hook.", - "additionalProperties": { - "type": "string" - } - }, - "emit_analytics_event": { - "type": "boolean", - "default": true, - "description": "Emit tracing events for this hook on delivery or error" - }, - "auth": { - "type": "object", - "title": "Auth mechanisms", - "description": "Define which auth mechanism the Web-Hook should use", - "oneOf": [ - { - "$ref": "#/definitions/webHookAuthApiKeyProperties" - }, - { - "$ref": "#/definitions/webHookAuthBasicAuthProperties" - } - ] - }, - "additionalProperties": false - } - } - } - }, "before": { "$ref": "#/definitions/selfServiceBeforeLogin" }, @@ -1691,6 +1636,61 @@ "description": "If set to false the password validation does not check for similarity between the password and the user identifier.", "type": "boolean", "default": true + }, + "migrate_hook": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Password Migration", + "description": "If set to true will enable password migration.", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "description": "The URL the password migration hook should call", + "format": "uri" + }, + "method": { + "type": "string", + "description": "The HTTP method to use (GET, POST, etc).", + "const": "POST", + "default": "POST" + }, + "headers": { + "type": "object", + "description": "The HTTP headers that must be applied to the password migration hook.", + "additionalProperties": { + "type": "string" + } + }, + "emit_analytics_event": { + "type": "boolean", + "default": true, + "description": "Emit tracing events for this hook on delivery or error" + }, + "auth": { + "type": "object", + "title": "Auth mechanisms", + "description": "Define which auth mechanism the Web-Hook should use", + "oneOf": [ + { + "$ref": "#/definitions/webHookAuthApiKeyProperties" + }, + { + "$ref": "#/definitions/webHookAuthBasicAuthProperties" + } + ] + }, + "additionalProperties": false + } + } + } } }, "additionalProperties": false From 1bdc19ae3e1a3df38234cb892f65de4a2c95f041 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 7 May 2024 10:31:14 +0200 Subject: [PATCH 19/71] feat: identifier first auth --- courier/sms_test.go | 3 +- driver/config/config.go | 14 + driver/registry_default.go | 2 + embedx/config.schema.json | 21 +- identity/credentials.go | 2 + identity/credentials_oidc.go | 2 +- internal/driver.go | 23 +- schema/errors.go | 11 +- selfservice/flow/login/error_test.go | 10 +- selfservice/flow/login/flow.go | 4 +- selfservice/flow/login/handler.go | 32 +- selfservice/flow/login/strategy.go | 1 - .../flow/login/strategy_form_hydrator.go | 38 +++ .../flow/login/strategy_form_hydrator_test.go | 36 +++ selfservice/flow/state.go | 9 +- selfservice/flowhelpers/login.go | 4 +- selfservice/strategy/code/strategy.go | 14 +- selfservice/strategy/code/strategy_login.go | 25 ++ .../multistep/.schema/login.schema.json | 24 ++ selfservice/strategy/multistep/schema.go | 11 + selfservice/strategy/multistep/strategy.go | 63 ++++ .../strategy/multistep/strategy_login.go | 161 ++++++++++ selfservice/strategy/multistep/types.go | 26 ++ selfservice/strategy/oidc/strategy.go | 35 +- selfservice/strategy/oidc/strategy_login.go | 53 +++ selfservice/strategy/passkey/passkey_login.go | 302 ++++++++++-------- .../strategy/passkey/passkey_login_test.go | 4 +- .../strategy/passkey/passkey_strategy.go | 1 - selfservice/strategy/password/login.go | 86 +++-- selfservice/strategy/webauthn/login.go | 184 ++++++----- selfservice/strategy/webauthn/strategy.go | 1 - test/e2e/cypress/support/commands.ts | 2 +- test/e2e/profiles/code/.kratos.yml | 3 +- test/e2e/profiles/email/.kratos.yml | 2 + test/e2e/profiles/mfa/.kratos.yml | 2 + test/e2e/profiles/mobile/.kratos.yml | 3 + test/e2e/profiles/network/.kratos.yml | 2 + .../profiles/oidc-provider-mfa/.kratos.yml | 2 + test/e2e/profiles/oidc-provider/.kratos.yml | 2 + test/e2e/profiles/oidc/.kratos.yml | 2 + test/e2e/profiles/passkey/.kratos.yml | 2 + test/e2e/profiles/passwordless/.kratos.yml | 2 + test/e2e/profiles/recovery-mfa/.kratos.yml | 2 + test/e2e/profiles/recovery/.kratos.yml | 2 + test/e2e/profiles/spa/.kratos.yml | 2 + test/e2e/profiles/two-steps/.kratos.yml | 3 +- test/e2e/profiles/verification/.kratos.yml | 2 + test/e2e/profiles/webhooks/.kratos.yml | 2 + text/id.go | 32 +- text/message_login.go | 14 +- text/message_validation.go | 8 + ui/node/attributes.go | 101 +++++- ui/node/attributes_test.go | 64 ++++ ui/node/node.go | 33 ++ ui/node/node_test.go | 62 ++++ 55 files changed, 1229 insertions(+), 324 deletions(-) create mode 100644 selfservice/flow/login/strategy_form_hydrator.go create mode 100644 selfservice/flow/login/strategy_form_hydrator_test.go create mode 100644 selfservice/strategy/multistep/.schema/login.schema.json create mode 100644 selfservice/strategy/multistep/schema.go create mode 100644 selfservice/strategy/multistep/strategy.go create mode 100644 selfservice/strategy/multistep/strategy_login.go create mode 100644 selfservice/strategy/multistep/types.go diff --git a/courier/sms_test.go b/courier/sms_test.go index a93a7974bf71..5dc727048be6 100644 --- a/courier/sms_test.go +++ b/courier/sms_test.go @@ -63,6 +63,7 @@ func TestQueueSMS(t *testing.T) { Body: body.Body, }) })) + t.Cleanup(srv.Close) requestConfig := fmt.Sprintf(`{ "url": "%s", @@ -112,8 +113,6 @@ func TestQueueSMS(t *testing.T) { assert.Equal(t, expected.To, message.To) assert.Equal(t, fmt.Sprintf("stub sms body %s\n", expected.Body), message.Body) } - - srv.Close() } func TestDisallowedInternalNetwork(t *testing.T) { diff --git a/driver/config/config.go b/driver/config/config.go index 81be527a2632..d9615451777e 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -134,6 +134,8 @@ const ( ViperKeySelfServiceRegistrationAfter = "selfservice.flows.registration.after" ViperKeySelfServiceRegistrationBeforeHooks = "selfservice.flows.registration.before.hooks" ViperKeySelfServiceLoginUI = "selfservice.flows.login.ui_url" + ViperKeySelfServiceLoginFlowTwoStepEnabled = "selfservice.flows.login.two_step.enabled" + ViperKeySecurityAccountEnumerationMitigate = "security.account_enumeration.mitigate" ViperKeySelfServiceLoginRequestLifespan = "selfservice.flows.login.lifespan" ViperKeySelfServiceLoginAfter = "selfservice.flows.login.after" ViperKeySelfServiceLoginBeforeHooks = "selfservice.flows.login.before.hooks" @@ -782,8 +784,12 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self // we need to forcibly set these values here: defaultEnabled := false switch strategy { + case "identity_discovery": + defaultEnabled = p.SelfServiceLoginFlowTwoStepEnabled(ctx) + break case "code", "password", "profile": defaultEnabled = true + break } // Backwards compatibility for the old "passwordless_enabled" key @@ -1612,3 +1618,11 @@ func (p *Config) PasswordMigrationHook(ctx context.Context) (hook *PasswordMigra return hook } + +func (p *Config) SelfServiceLoginFlowTwoStepEnabled(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeySelfServiceLoginFlowTwoStepEnabled) +} + +func (p *Config) SecurityAccountEnumerationMitigate(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeySecurityAccountEnumerationMitigate) +} diff --git a/driver/registry_default.go b/driver/registry_default.go index eab63a120981..0c5f59238d28 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -6,6 +6,7 @@ package driver import ( "context" "crypto/sha256" + "github.com/ory/kratos/selfservice/strategy/multistep" "net/http" "strings" "sync" @@ -324,6 +325,7 @@ func (m *RegistryDefault) selfServiceStrategies() []any { passkey.NewStrategy(m), webauthn.NewStrategy(m), lookup.NewStrategy(m), + multistep.NewStrategy(m), } } } diff --git a/embedx/config.schema.json b/embedx/config.schema.json index c62b3c39f00c..5016926ab036 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -1557,12 +1557,6 @@ "title": "Enables login flows code method to fulfil MFA requests", "default": false }, - "passwordless_login_fallback_enabled": { - "type": "boolean", - "title": "Passwordless Login Fallback Enabled", - "description": "This setting allows the code method to always login a user with code if they have registered with another authentication method such as password or social sign in.", - "default": false - }, "enabled": { "type": "boolean", "title": "Enables Code Method", @@ -2879,6 +2873,21 @@ } } }, + "security": { + "type": "object", + "properties": { + "account_enumeration": { + "type": "object", + "properties": { + "mitigate": { + "type": "boolean", + "default": false, + "description": "Mitigate account enumeration by making it harder to figure out if an identifier (email, phone number) exists or not. Enabling this setting degrades user experience. This setting does not mitigate all possible attack vectors yet." + } + } + } + } + }, "version": { "title": "The kratos version this config is written for.", "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", diff --git a/identity/credentials.go b/identity/credentials.go index 3cc910c5a74e..3c7fa0f31834 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -88,6 +88,8 @@ const ( CredentialsTypeCodeAuth CredentialsType = "code" CredentialsTypePasskey CredentialsType = "passkey" CredentialsTypeProfile CredentialsType = "profile" + + TwoStep CredentialsType = "identity_discovery" // TODO move this somewhere else ) func (c CredentialsType) String() string { diff --git a/identity/credentials_oidc.go b/identity/credentials_oidc.go index 27462f927024..bcd03673c616 100644 --- a/identity/credentials_oidc.go +++ b/identity/credentials_oidc.go @@ -88,7 +88,7 @@ func NewCredentialsOIDC(tokens *CredentialsOIDCEncryptedTokens, provider, subjec return &Credentials{ Type: CredentialsTypeOIDC, - Identifiers: []string{OIDCUniqueID(provider, subject)}, + Identifiers: []string{OIDCUniqueID(provider, subject) /* getEmailFromTraits (needs to be verified) */}, Config: b.Bytes(), }, nil } diff --git a/internal/driver.go b/internal/driver.go index b95b2dc7c0a9..c10134347bd3 100644 --- a/internal/driver.go +++ b/internal/driver.go @@ -42,17 +42,18 @@ func init() { func NewConfigurationWithDefaults(t testing.TB, opts ...configx.OptionModifier) *config.Config { configOpts := append([]configx.OptionModifier{ configx.WithValues(map[string]interface{}{ - "log.level": "error", - config.ViperKeyDSN: dbal.NewSQLiteTestDatabase(t), - config.ViperKeyHasherArgon2ConfigMemory: 16384, - config.ViperKeyHasherArgon2ConfigIterations: 1, - config.ViperKeyHasherArgon2ConfigParallelism: 1, - config.ViperKeyHasherArgon2ConfigSaltLength: 16, - config.ViperKeyHasherBcryptCost: 4, - config.ViperKeyHasherArgon2ConfigKeyLength: 16, - config.ViperKeyCourierSMTPURL: "smtp://foo:bar@baz.com/", - config.ViperKeySelfServiceBrowserDefaultReturnTo: "https://www.ory.sh/redirect-not-set", - config.ViperKeySecretsCipher: []string{"secret-thirty-two-character-long"}, + "log.level": "error", + config.ViperKeyDSN: dbal.NewSQLiteTestDatabase(t), + config.ViperKeyHasherArgon2ConfigMemory: 16384, + config.ViperKeyHasherArgon2ConfigIterations: 1, + config.ViperKeyHasherArgon2ConfigParallelism: 1, + config.ViperKeyHasherArgon2ConfigSaltLength: 16, + config.ViperKeyHasherBcryptCost: 4, + config.ViperKeyHasherArgon2ConfigKeyLength: 16, + config.ViperKeyCourierSMTPURL: "smtp://foo:bar@baz.com/", + config.ViperKeySelfServiceBrowserDefaultReturnTo: "https://www.ory.sh/redirect-not-set", + config.ViperKeySecretsCipher: []string{"secret-thirty-two-character-long"}, + config.ViperKeySelfServiceLoginFlowTwoStepEnabled: false, }), configx.SkipValidation(), }, opts...) diff --git a/schema/errors.go b/schema/errors.go index 30c1f72976f3..6ff52a5047c2 100644 --- a/schema/errors.go +++ b/schema/errors.go @@ -117,12 +117,21 @@ func NewInvalidCredentialsError() error { ValidationError: &jsonschema.ValidationError{ Message: `the provided credentials are invalid, check for spelling mistakes in your password or username, email address, or phone number`, InstancePtr: "#/", - Context: &ValidationErrorContextPasswordPolicyViolation{}, }, Messages: new(text.Messages).Add(text.NewErrorValidationInvalidCredentials()), }) } +func NewAccountNotFoundError() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: "this account does not exist or has no login method configured", + InstancePtr: "#/identifier", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationAccountNotFound()), + }) +} + type ValidationErrorContextDuplicateCredentialsError struct { AvailableCredentials []string `json:"available_credential_types"` AvailableOIDCProviders []string `json:"available_oidc_providers"` diff --git a/selfservice/flow/login/error_test.go b/selfservice/flow/login/error_test.go index 5cc78c35bda1..e6f27f452225 100644 --- a/selfservice/flow/login/error_test.go +++ b/selfservice/flow/login/error_test.go @@ -6,13 +6,12 @@ package login_test import ( "context" "encoding/json" + "github.com/ory/kratos/identity" "io" "net/http" "testing" "time" - "github.com/ory/kratos/identity" - "github.com/gofrs/uuid" "github.com/ory/kratos/ui/node" @@ -74,7 +73,12 @@ func TestHandleError(t *testing.T) { require.NoError(t, err) for _, s := range reg.LoginStrategies(context.Background()) { - require.NoError(t, s.PopulateLoginMethod(req, identity.AuthenticatorAssuranceLevel1, f)) + switch s.(type) { + case login.LegacyFormHydrator: + require.NoError(t, s.(login.LegacyFormHydrator).PopulateLoginMethod(req, identity.AuthenticatorAssuranceLevel1, f)) + case login.FormHydrator: + require.NoError(t, s.(login.FormHydrator).PopulateLoginMethodFirstFactor(req, f)) + } } require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), f)) diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index a01d449a2751..06b189d6c351 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -230,9 +230,9 @@ func (f Flow) GetID() uuid.UUID { return f.ID } -// IsForced returns true if the login flow was triggered to re-authenticate the user. +// IsRefresh returns true if the login flow was triggered to re-authenticate the user. // This is the case if the refresh query parameter is set to true. -func (f *Flow) IsForced() bool { +func (f *Flow) IsRefresh() bool { return f.Refresh } diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go index 88b3712602a0..bf53f7591115 100644 --- a/selfservice/flow/login/handler.go +++ b/selfservice/flow/login/handler.go @@ -212,8 +212,36 @@ preLoginHook: } for _, s := range h.d.LoginStrategies(r.Context(), strategyFilters...) { - if err := s.PopulateLoginMethod(r, f.RequestedAAL, f); err != nil { - return nil, nil, err + var populateErr error + + switch strat := s.(type) { + case FormHydrator: + switch { + case f.IsRefresh(): + populateErr = strat.PopulateLoginMethodRefresh(r, f) + break + case f.RequestedAAL == identity.AuthenticatorAssuranceLevel1: + if h.d.Config().SelfServiceLoginFlowTwoStepEnabled(r.Context()) { + populateErr = strat.PopulateLoginMethodMultiStepIdentification(r, f) + } else { + populateErr = strat.PopulateLoginMethodFirstFactor(r, f) + } + break + case f.RequestedAAL == identity.AuthenticatorAssuranceLevel2: + populateErr = strat.PopulateLoginMethodSecondFactor(r, f) + break + } + break + case LegacyFormHydrator: + populateErr = strat.PopulateLoginMethod(r, f.RequestedAAL, f) + break + default: + populateErr = errors.WithStack(x.PseudoPanic.WithReasonf("A login strategy was expected to implement one of the interfaces LegacyFormHydrator or FormHydrator but did not.")) + break + } + + if populateErr != nil { + return nil, nil, populateErr } } diff --git a/selfservice/flow/login/strategy.go b/selfservice/flow/login/strategy.go index c70ad9cc8684..fec71d3beb1d 100644 --- a/selfservice/flow/login/strategy.go +++ b/selfservice/flow/login/strategy.go @@ -20,7 +20,6 @@ type Strategy interface { ID() identity.CredentialsType NodeGroup() node.UiNodeGroup RegisterLoginRoutes(*x.RouterPublic) - PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *Flow) error Login(w http.ResponseWriter, r *http.Request, f *Flow, sess *session.Session) (i *identity.Identity, err error) CompletedAuthenticationMethod(ctx context.Context, methods session.AuthenticationMethods) session.AuthenticationMethod } diff --git a/selfservice/flow/login/strategy_form_hydrator.go b/selfservice/flow/login/strategy_form_hydrator.go new file mode 100644 index 000000000000..1f87aeabf1b8 --- /dev/null +++ b/selfservice/flow/login/strategy_form_hydrator.go @@ -0,0 +1,38 @@ +package login + +import ( + "github.com/ory/kratos/identity" + "net/http" +) + +type LegacyFormHydrator interface { + PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *Flow) error +} + +type FormHydrator interface { + PopulateLoginMethodRefresh(r *http.Request, sr *Flow) error + PopulateLoginMethodFirstFactor(r *http.Request, sr *Flow) error + PopulateLoginMethodSecondFactor(r *http.Request, sr *Flow) error + PopulateLoginMethodMultiStepSelection(r *http.Request, sr *Flow, options ...FormHydratorModifier) error + PopulateLoginMethodMultiStepIdentification(r *http.Request, sr *Flow) error +} + +type FormHydratorOptions struct { + IdentityHint *identity.Identity +} + +type FormHydratorModifier func(o *FormHydratorOptions) + +func WithIdentityHint(i *identity.Identity) FormHydratorModifier { + return func(o *FormHydratorOptions) { + o.IdentityHint = i + } +} + +func NewFormHydratorOptions(modifiers []FormHydratorModifier) *FormHydratorOptions { + o := new(FormHydratorOptions) + for _, m := range modifiers { + m(o) + } + return o +} diff --git a/selfservice/flow/login/strategy_form_hydrator_test.go b/selfservice/flow/login/strategy_form_hydrator_test.go new file mode 100644 index 000000000000..db63de83bf35 --- /dev/null +++ b/selfservice/flow/login/strategy_form_hydrator_test.go @@ -0,0 +1,36 @@ +package login + +import ( + "github.com/ory/kratos/identity" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestWithIdentityHint(t *testing.T) { + expected := new(identity.Identity) + opts := NewFormHydratorOptions([]FormHydratorModifier{WithIdentityHint(expected)}) + assert.Equal(t, expected, opts.IdentityHint) +} + +func TestWithAccountEnumerationBucket(t *testing.T) { + opts := NewFormHydratorOptions([]FormHydratorModifier{}) + for _, c := range identity.AllCredentialTypes { + assert.Falsef(t, opts.BucketShowsCredential(c), "expected false for %s", c) + } + + opts = NewFormHydratorOptions([]FormHydratorModifier{WithAccountEnumerationBucket("hello@ory.sh")}) + found := 0 + var foundType identity.CredentialsType + for _, c := range identity.AllCredentialTypes { + c := c + if opts.BucketShowsCredential(c) { + foundType = c + found++ + } + } + + assert.Equal(t, 1, found, "expected exactly one to be true") + + opts = NewFormHydratorOptions([]FormHydratorModifier{WithAccountEnumerationBucket("hello@ory.sh")}) + assert.Truef(t, opts.BucketShowsCredential(foundType), "expected true for %s because bucket should be stable", foundType) +} diff --git a/selfservice/flow/state.go b/selfservice/flow/state.go index 76a0683fc19d..5ff346931f46 100644 --- a/selfservice/flow/state.go +++ b/selfservice/flow/state.go @@ -31,9 +31,16 @@ const ( StatePassedChallenge State = "passed_challenge" StateShowForm State = "show_form" StateSuccess State = "success" + + StateLoginIdentifierFirstForm State = "identifier_first_form" ) -var states = []State{StateChooseMethod, StateEmailSent, StatePassedChallenge} +var states = []State{ + StateLoginIdentifierFirstForm, + StateChooseMethod, + StateEmailSent, + StatePassedChallenge, +} func indexOf(current State) int { for k, s := range states { diff --git a/selfservice/flowhelpers/login.go b/selfservice/flowhelpers/login.go index 2e97f85ebe30..60c17176a740 100644 --- a/selfservice/flowhelpers/login.go +++ b/selfservice/flowhelpers/login.go @@ -15,11 +15,11 @@ func GuessForcedLoginIdentifier(r *http.Request, d interface { session.ManagementProvider identity.PrivilegedPoolProvider }, f interface { - IsForced() bool + IsRefresh() bool }, ct identity.CredentialsType) (identifier string, id *identity.Identity, creds *identity.Credentials) { var ok bool // This block adds the identifier to the method when the request is forced - as a hint for the user. - if !f.IsForced() { + if !f.IsRefresh() { // do nothing } else if sess, err := d.SessionManager().FetchFromRequest(r.Context(), r); err != nil { // do nothing diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index ee3ce353e4ae..80147786477a 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -180,6 +180,7 @@ func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error { if f.GetType() == flow.TypeBrowser { f.GetUI().SetCSRF(s.deps.GenerateCSRFToken(r)) } + return nil } @@ -299,15 +300,10 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error // preserve the login identifier that was submitted // so we can retry the code flow with the same data for _, n := range f.GetUI().Nodes { - if n.Group == node.DefaultGroup { - // we don't need the user to change the values here - // for better UX let's make them disabled - // when there are errors we won't hide the fields - if len(n.Messages) == 0 { - if input, ok := n.Attributes.(*node.InputAttributes); ok { - input.Type = "hidden" - n.Attributes = input - } + if n.ID() == "identifier" { + if input, ok := n.Attributes.(*node.InputAttributes); ok { + input.Type = "hidden" + n.Attributes = input } freshNodes = append(freshNodes, n) } diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go index a9d7459f5c56..6054580cfd19 100644 --- a/selfservice/strategy/code/strategy_login.go +++ b/selfservice/strategy/code/strategy_login.go @@ -7,6 +7,7 @@ import ( "context" "database/sql" "encoding/json" + "github.com/ory/kratos/text" "net/http" "strings" @@ -29,6 +30,7 @@ import ( "github.com/ory/x/decoderx" ) +var _ login.FormHydrator = new(Strategy) var _ login.Strategy = new(Strategy) // Update Login flow using the code method @@ -372,3 +374,26 @@ func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *logi return i, nil } + +func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, f *login.Flow) error { + return s.PopulateMethod(r, f) +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error { + return s.PopulateMethod(r, f) +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, f *login.Flow) error { + return s.PopulateMethod(r, f) +} + +func (s *Strategy) PopulateLoginMethodMultiStepSelection(_ *http.Request, f *login.Flow, _ ...login.FormHydratorModifier) error { + f.GetUI().Nodes.Append( + node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginCode()), + ) + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepIdentification(r *http.Request, f *login.Flow) error { + return nil +} diff --git a/selfservice/strategy/multistep/.schema/login.schema.json b/selfservice/strategy/multistep/.schema/login.schema.json new file mode 100644 index 000000000000..c19b425f41ea --- /dev/null +++ b/selfservice/strategy/multistep/.schema/login.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/identity_disovery/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "identifier": { + "type": "string", + "minLength": 1 + }, + "method": { + "type": "string", + "enum": [ + "identity_discovery" + ] + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/selfservice/strategy/multistep/schema.go b/selfservice/strategy/multistep/schema.go new file mode 100644 index 000000000000..53704e7cce72 --- /dev/null +++ b/selfservice/strategy/multistep/schema.go @@ -0,0 +1,11 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package multistep + +import ( + _ "embed" +) + +//go:embed .schema/login.schema.json +var loginSchema []byte diff --git a/selfservice/strategy/multistep/strategy.go b/selfservice/strategy/multistep/strategy.go new file mode 100644 index 000000000000..9d75de3a2e5b --- /dev/null +++ b/selfservice/strategy/multistep/strategy.go @@ -0,0 +1,63 @@ +package multistep + +import ( + "context" + "github.com/go-playground/validator/v10" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" +) + +type dependencies interface { + x.LoggingProvider + x.WriterProvider + x.CSRFTokenGeneratorProvider + x.CSRFProvider + + config.Provider + + identity.PrivilegedPoolProvider + login.StrategyProvider + login.FlowPersistenceProvider +} + +type Strategy struct { + d dependencies + v *validator.Validate + hd *decoderx.HTTP +} + +func NewStrategy(d any) *Strategy { + return &Strategy{ + d: d.(dependencies), + v: validator.New(), + hd: decoderx.NewHTTP(), + } +} + +func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return 0, nil +} + +func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return 0, nil +} + +func (s *Strategy) ID() identity.CredentialsType { + return identity.TwoStep +} + +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context, _ session.AuthenticationMethods) session.AuthenticationMethod { + return session.AuthenticationMethod{ + Method: s.ID(), + AAL: identity.AuthenticatorAssuranceLevel1, + } +} + +func (s *Strategy) NodeGroup() node.UiNodeGroup { + return node.TwoStepGroup +} diff --git a/selfservice/strategy/multistep/strategy_login.go b/selfservice/strategy/multistep/strategy_login.go new file mode 100644 index 000000000000..3552c5f11095 --- /dev/null +++ b/selfservice/strategy/multistep/strategy_login.go @@ -0,0 +1,161 @@ +package multistep + +import ( + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" + "github.com/ory/x/sqlcon" + "github.com/pkg/errors" + "net/http" +) + +var _ login.FormHydrator = new(Strategy) +var _ login.Strategy = new(Strategy) + +func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, f *login.Flow, payload *updateLoginFlowWithMultiStepMethod, err error) error { + if f != nil { + f.UI.Nodes.SetValueAttribute("identifier", payload.Identifier) + if f.Type == flow.TypeBrowser { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + } + + return err +} + +func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ *session.Session) (_ *identity.Identity, err error) { + if !s.d.Config().SelfServiceLoginFlowTwoStepEnabled(r.Context()) { + return nil, errors.WithStack(flow.ErrStrategyNotResponsible) + } + + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + return nil, err + } + + var p updateLoginFlowWithMultiStepMethod + if err := s.hd.Decode(r, &p, + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema), + decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } + f.TransientPayload = p.TransientPayload + + if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } + + var opts []login.FormHydratorModifier + + // Look up the user by the identifier. + identityHint, err := s.d.PrivilegedIdentityPool().FindIdentityByCredentialIdentifier(r.Context(), p.Identifier, + // We are dealing with user input -> lookup should be case-insensitive. + false, + ) + if errors.Is(err, sqlcon.ErrNoRows) { + // User not found + if !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) { + // We don't have to mitigate account enumeration and show the user that the account doesn't exist + return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewAccountNotFoundError())) + } + + // We have to mitigate account enumeration. So we continue without setting the identity hint. + } else if err != nil { + // An error happened during lookup + return nil, s.handleLoginError(w, r, f, &p, err) + } else if !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) { + // Hydrate credentials + if err := s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(r.Context(), identityHint, identity.ExpandCredentials); err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } + } + + f.UI.ResetMessages() + f.UI.Nodes.SetValueAttribute("identifier", p.Identifier) + + // Add identity hint + opts = append(opts, login.WithIdentityHint(identityHint)) + + for _, ls := range s.d.LoginStrategies(r.Context()) { + populator, ok := ls.(login.FormHydrator) + if !ok { + continue + } + + if err := populator.PopulateLoginMethodMultiStepSelection(r, f, opts...); err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } + } + + f.Active = identity.TwoStep + if err = s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { + return nil, s.handleLoginError(w, r, f, &p, err) + } + + if x.IsJSONRequest(r) { + s.d.Writer().WriteCode(w, r, http.StatusBadRequest, f) + } else { + http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())).String(), http.StatusSeeOther) + } + + return nil, flow.ErrCompletedByStrategy +} + +func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepIdentification(r *http.Request, f *login.Flow) error { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + + f.UI.SetNode(node.NewInputField("identifier", "", s.NodeGroup(), node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) + f.UI.GetNodes().Append(node.NewInputField("method", s.ID(), s.NodeGroup(), node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue())) + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepSelection(_ *http.Request, f *login.Flow, _ ...login.FormHydratorModifier) error { + f.UI.GetNodes().RemoveMatching(node.NewInputField("method", s.ID(), s.NodeGroup(), node.InputAttributeTypeSubmit)) + + // We set the identifier to hidden, so it's still available in the form but not visible to the user. + for k, n := range f.UI.Nodes { + if n.ID() != "identifier" { + continue + } + + attrs, ok := f.UI.Nodes[k].Attributes.(*node.InputAttributes) + if !ok { + continue + } + + attrs.Type = node.InputAttributeTypeHidden + f.UI.Nodes[k].Attributes = attrs + } + + return nil +} + +func (s *Strategy) RegisterLoginRoutes(_ *x.RouterPublic) {} diff --git a/selfservice/strategy/multistep/types.go b/selfservice/strategy/multistep/types.go new file mode 100644 index 000000000000..5268f9c49195 --- /dev/null +++ b/selfservice/strategy/multistep/types.go @@ -0,0 +1,26 @@ +package multistep + +import "encoding/json" + +// Update Login Flow with Multi-Step Method +// +// swagger:model updateLoginFlowWithMultiStepMethod +type updateLoginFlowWithMultiStepMethod struct { + // Method should be set to "password" when logging in using the identifier and password strategy. + // + // required: true + Method string `json:"method"` + + // Sending the anti-csrf token is only required for browser login flows. + CSRFToken string `json:"csrf_token"` + + // Identifier is the email or username of the user trying to log in. + // + // required: true + Identifier string `json:"identifier"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` +} diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 6515d06367ee..2fe7b23265e3 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -24,7 +24,6 @@ import ( "golang.org/x/oauth2" "github.com/ory/kratos/cipher" - "github.com/ory/kratos/selfservice/flowhelpers" "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/x/jsonnetsecure" "github.com/ory/x/otelx" @@ -537,38 +536,8 @@ func (s *Strategy) populateMethod(r *http.Request, f flow.Flow, message func(pro return err } - providers := conf.Providers - - if lf, ok := f.(*login.Flow); ok && lf.IsForced() { - if _, id, c := flowhelpers.GuessForcedLoginIdentifier(r, s.d, lf, s.ID()); id != nil { - if c == nil { - // no OIDC credentials, don't add any providers - providers = nil - } else { - var credentials identity.CredentialsOIDC - if err := json.Unmarshal(c.Config, &credentials); err != nil { - // failed to read OIDC credentials, don't add any providers - providers = nil - } else { - // add only providers that can actually be used to log in as this identity - providers = make([]Configuration, 0, len(conf.Providers)) - for i := range conf.Providers { - for j := range credentials.Providers { - if conf.Providers[i].ID == credentials.Providers[j].Provider { - providers = append(providers, conf.Providers[i]) - break - } - } - } - } - } - } - } - - // does not need sorting because there is only one field - c := f.GetUI() - c.SetCSRF(s.d.GenerateCSRFToken(r)) - AddProviders(c, providers, message) + f.GetUI().SetCSRF(s.d.GenerateCSRFToken(r)) + AddProviders(f.GetUI(), conf.Providers, message) return nil } diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index 42b948ec7c11..43a697b961f2 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -6,6 +6,7 @@ package oidc import ( "bytes" "encoding/json" + "github.com/ory/kratos/selfservice/flowhelpers" "net/http" "strings" "time" @@ -34,6 +35,7 @@ import ( "github.com/ory/kratos/x" ) +var _ login.FormHydrator = new(Strategy) var _ login.Strategy = new(Strategy) func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { @@ -290,3 +292,54 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, errors.WithStack(flow.ErrCompletedByStrategy) } + +func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, lf *login.Flow) error { + conf, err := s.Config(r.Context()) + if err != nil { + return err + } + + providers := conf.Providers + _, id, c := flowhelpers.GuessForcedLoginIdentifier(r, s.d, lf, s.ID()) + if id == nil || c == nil { + providers = nil + } else { + var credentials identity.CredentialsOIDC + if err := json.Unmarshal(c.Config, &credentials); err != nil { + // failed to read OIDC credentials, don't add any providers + providers = nil + } else { + // add only providers that can actually be used to log in as this identity + providers = make([]Configuration, 0, len(conf.Providers)) + for i := range conf.Providers { + for j := range credentials.Providers { + if conf.Providers[i].ID == credentials.Providers[j].Provider { + providers = append(providers, conf.Providers[i]) + break + } + } + } + } + } + + lf.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + AddProviders(lf.UI, providers, text.NewInfoLoginWith) + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error { + return s.populateMethod(r, f, text.NewInfoLoginWith) +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepSelection(_ *http.Request, sr *login.Flow, _ ...login.FormHydratorModifier) error { + sr.GetUI().UnsetNode("provider") + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepIdentification(r *http.Request, f *login.Flow) error { + return s.populateMethod(r, f, text.NewInfoLoginWith) +} diff --git a/selfservice/strategy/passkey/passkey_login.go b/selfservice/strategy/passkey/passkey_login.go index 63d5ec66f2f0..54b3f475ed38 100644 --- a/selfservice/strategy/passkey/passkey_login.go +++ b/selfservice/strategy/passkey/passkey_login.go @@ -29,23 +29,13 @@ import ( "github.com/ory/x/decoderx" ) +var _ login.FormHydrator = new(Strategy) + func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { webauthnx.RegisterWebauthnRoute(r) } -func (s *Strategy) PopulateLoginMethod(r *http.Request, aal identity.AuthenticatorAssuranceLevel, sr *login.Flow) error { - if sr.Type != flow.TypeBrowser || aal != identity.AuthenticatorAssuranceLevel1 { - return nil - } - - return s.populateLoginMethodForPasskeys(r, sr) -} - func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *login.Flow) error { - if loginFlow.IsForced() { - return s.populateLoginMethodForRefresh(r, loginFlow) - } - ctx := r.Context() loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) @@ -113,119 +103,6 @@ func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *lo Type: node.InputAttributeTypeHidden, }}) - loginFlow.UI.Nodes.Append(node.NewInputField( - node.PasskeyLoginTrigger, - "", - node.PasskeyGroup, - node.InputAttributeTypeButton, - node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js - attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here - }), - ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) - - return nil -} - -func (s *Strategy) populateLoginMethodForRefresh(r *http.Request, loginFlow *login.Flow) error { - ctx := r.Context() - - identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, loginFlow, s.ID()) - if identifier == "" { - return nil - } - - id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID) - if err != nil { - return err - } - - cred, ok := id.GetCredentials(s.ID()) - if !ok { - // Identity has no passkey - return nil - } - - var conf identity.CredentialsWebAuthnConfig - if err := json.Unmarshal(cred.Config, &conf); err != nil { - return errors.WithStack(err) - } - - webAuthCreds := conf.Credentials.ToWebAuthn() - if len(webAuthCreds) == 0 { - // Identity has no webauthn - return nil - } - - passkeyIdentifier := s.PasskeyDisplayNameFromIdentity(ctx, id) - - webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) - if err != nil { - return errors.WithStack(err) - } - option, sessionData, err := webAuthn.BeginLogin(&webauthnx.User{ - Name: passkeyIdentifier, - ID: conf.UserHandle, - Credentials: webAuthCreds, - Config: webAuthn.Config, - }) - if err != nil { - return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate passkey login.").WithDebug(err.Error())) - } - - loginFlow.InternalContext, err = sjson.SetBytes( - loginFlow.InternalContext, - flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), - sessionData, - ) - if err != nil { - return errors.WithStack(err) - } - - injectWebAuthnOptions, err := json.Marshal(option) - if err != nil { - return errors.WithStack(err) - } - - loginFlow.UI.Nodes.Upsert(&node.Node{ - Type: node.Input, - Group: node.PasskeyGroup, - Meta: &node.Meta{}, - Attributes: &node.InputAttributes{ - Name: node.PasskeyChallenge, - Type: node.InputAttributeTypeHidden, - FieldValue: string(injectWebAuthnOptions), - }}) - - loginFlow.UI.Nodes.Append(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) - - loginFlow.UI.Nodes.Upsert(&node.Node{ - Type: node.Input, - Group: node.PasskeyGroup, - Meta: &node.Meta{}, - Attributes: &node.InputAttributes{ - Name: node.PasskeyLogin, - Type: node.InputAttributeTypeHidden, - }}) - - loginFlow.UI.Nodes.Append(node.NewInputField( - node.PasskeyLoginTrigger, - "", - node.PasskeyGroup, - node.InputAttributeTypeButton, - node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js - }), - ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) - - loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - loginFlow.UI.SetNode(node.NewInputField( - "identifier", - passkeyIdentifier, - node.DefaultGroup, - node.InputAttributeTypeHidden, - )) - return nil } @@ -393,3 +270,178 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f * return i, nil } + +func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, f *login.Flow) error { + if f.Type != flow.TypeBrowser { + return nil + } + + ctx := r.Context() + + identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, f, s.ID()) + if identifier == "" { + return nil + } + + id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID) + if err != nil { + return err + } + + cred, ok := id.GetCredentials(s.ID()) + if !ok { + // Identity has no passkey + return nil + } + + var conf identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &conf); err != nil { + return errors.WithStack(err) + } + + webAuthCreds := conf.Credentials.ToWebAuthn() + if len(webAuthCreds) == 0 { + // Identity has no webauthn + return nil + } + + passkeyIdentifier := s.PasskeyDisplayNameFromIdentity(ctx, id) + + webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return errors.WithStack(err) + } + option, sessionData, err := webAuthn.BeginLogin(&webauthnx.User{ + Name: passkeyIdentifier, + ID: conf.UserHandle, + Credentials: webAuthCreds, + Config: webAuthn.Config, + }) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate passkey login.").WithDebug(err.Error())) + } + + f.InternalContext, err = sjson.SetBytes( + f.InternalContext, + flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), + sessionData, + ) + if err != nil { + return errors.WithStack(err) + } + + injectWebAuthnOptions, err := json.Marshal(option) + if err != nil { + return errors.WithStack(err) + } + + f.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyChallenge, + Type: node.InputAttributeTypeHidden, + FieldValue: string(injectWebAuthnOptions), + }}) + + f.UI.Nodes.Append(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + + f.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyLogin, + Type: node.InputAttributeTypeHidden, + }}) + + f.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.SetNode(node.NewInputField( + "identifier", + passkeyIdentifier, + node.DefaultGroup, + node.InputAttributeTypeHidden, + )) + + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser { + return nil + } + + if err := s.populateLoginMethodForPasskeys(r, sr); err != nil { + return err + } + + sr.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepSelection(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error { + if sr.Type != flow.TypeBrowser { + return nil + } + + o := login.NewFormHydratorOptions(opts) + + if o.IdentityHint == nil { + // Identity was not found so add fields + } else { + // If we have an identity hint we can perform identity credentials discovery and + // hide this credential if it should not be included. + count, err := s.CountActiveFirstFactorCredentials(o.IdentityHint.Credentials) + if err != nil { + return err + } else if count == 0 && !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) { + return nil + } + } + + sr.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepIdentification(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser { + return nil + } + + return s.populateLoginMethodForPasskeys(r, sr) +} diff --git a/selfservice/strategy/passkey/passkey_login_test.go b/selfservice/strategy/passkey/passkey_login_test.go index cae6aa0ee4dd..2a6c2075557e 100644 --- a/selfservice/strategy/passkey/passkey_login_test.go +++ b/selfservice/strategy/passkey/passkey_login_test.go @@ -45,12 +45,12 @@ func TestPopulateLoginMethod(t *testing.T) { t.Run("case=should not handle AAL2", func(t *testing.T) { loginFlow := &login.Flow{Type: flow.TypeBrowser} - assert.Nil(t, s.PopulateLoginMethod(nil, identity.AuthenticatorAssuranceLevel2, loginFlow)) + assert.Nil(t, s.PopulateLoginMethodSecondFactor(nil, loginFlow)) }) t.Run("case=should not handle API flows", func(t *testing.T) { loginFlow := &login.Flow{Type: flow.TypeAPI} - assert.Nil(t, s.PopulateLoginMethod(nil, identity.AuthenticatorAssuranceLevel1, loginFlow)) + assert.Nil(t, s.PopulateLoginMethodFirstFactor(nil, loginFlow)) }) } diff --git a/selfservice/strategy/passkey/passkey_strategy.go b/selfservice/strategy/passkey/passkey_strategy.go index b590a7e93b6d..25329f5781b1 100644 --- a/selfservice/strategy/passkey/passkey_strategy.go +++ b/selfservice/strategy/passkey/passkey_strategy.go @@ -6,7 +6,6 @@ package passkey import ( "context" "encoding/json" - "github.com/pkg/errors" "github.com/ory/kratos/continuity" diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 3600e29b4e0e..9381e50a9bb5 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -33,6 +33,8 @@ import ( "github.com/ory/kratos/x" ) +var _ login.FormHydrator = new(Strategy) + func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { } @@ -144,43 +146,79 @@ func (s *Strategy) migratePasswordHash(ctx context.Context, identifier uuid.UUID return s.d.PrivilegedIdentityPool().UpdateIdentity(ctx, i) } -func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *login.Flow) error { - // This strategy can only solve AAL1 - if requestedAAL > identity.AuthenticatorAssuranceLevel1 { +func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, sr *login.Flow) error { + identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID()) + if identifier == "" { return nil } - if sr.IsForced() { - // We only show this method on a refresh request if the user has indeed a password set. - identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID()) - if identifier == "" { - return nil - } + // If we don't have a password set, do not show the password field. + count, err := s.CountActiveFirstFactorCredentials(id.Credentials) + if err != nil { + return err + } else if count == 0 { + return nil + } - count, err := s.CountActiveFirstFactorCredentials(id.Credentials) - if err != nil { - return err - } else if count == 0 { - return nil - } + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden)) + sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword)) + sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLogin())) + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) addIdentifierNode(r *http.Request, sr *login.Flow) error { + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } - sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden)) + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + + sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error { + if err := s.addIdentifierNode(r, sr); err != nil { + return err + } + + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword)) + sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginPassword())) + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepSelection(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error { + o := login.NewFormHydratorOptions(opts) + + if o.IdentityHint == nil { + // Identity was not found so add fields } else { - ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) - if err != nil { - return err - } - identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + // If we have an identity hint we can perform identity credentials discovery and + // hide this credential if it should not be included. + count, err := s.CountActiveFirstFactorCredentials(o.IdentityHint.Credentials) if err != nil { return err + } else if count == 0 && !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) { + return nil } - sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) } sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword)) - sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLogin())) + sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginPassword())) + return nil +} +func (s *Strategy) PopulateLoginMethodMultiStepIdentification(r *http.Request, sr *login.Flow) error { return nil } diff --git a/selfservice/strategy/webauthn/login.go b/selfservice/strategy/webauthn/login.go index 4c7dd23f09ea..93cbe38fadd6 100644 --- a/selfservice/strategy/webauthn/login.go +++ b/selfservice/strategy/webauthn/login.go @@ -34,84 +34,14 @@ import ( "github.com/ory/x/decoderx" ) +var _ login.FormHydrator = new(Strategy) + func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { webauthnx.RegisterWebauthnRoute(r) } -func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *login.Flow) error { - if sr.Type != flow.TypeBrowser { - return nil - } - - if s.d.Config().WebAuthnForPasswordless(r.Context()) && (requestedAAL == identity.AuthenticatorAssuranceLevel1) { - if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) { - return nil - } else if err != nil { - return err - } - return nil - } else if sr.IsForced() { - if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) { - return nil - } else if err != nil { - return err - } - return nil - } else if !s.d.Config().WebAuthnForPasswordless(r.Context()) && (requestedAAL == identity.AuthenticatorAssuranceLevel2) { - // We have done proper validation before so this should never error - sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r) - if err != nil { - return err - } - - if err := s.populateLoginMethod(r, sr, sess.Identity, text.NewInfoSelfServiceLoginWebAuthn(), identity.AuthenticatorAssuranceLevel2); errors.Is(err, webauthnx.ErrNoCredentials) { - return nil - } else if err != nil { - return err - } - - return nil - } - - return nil -} - func (s *Strategy) populateLoginMethodForPasswordless(r *http.Request, sr *login.Flow) error { - if sr.IsForced() { - identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID()) - if identifier == "" { - return nil - } - - if err := s.populateLoginMethod(r, sr, id, text.NewInfoSelfServiceLoginWebAuthn(), ""); errors.Is(err, webauthnx.ErrNoCredentials) { - return nil - } else if err != nil { - return err - } - - sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden)) - return nil - } - - ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) - if err != nil { - return err - } - identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) - if err != nil { - return err - } - sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(node.NewInputField( - "identifier", - "", - node.DefaultGroup, - node.InputAttributeTypeText, - node.WithRequiredInputAttribute, - func(attributes *node.InputAttributes) { attributes.Autocomplete = "username webauthn" }, - ).WithMetaLabel(identifierLabel)) sr.UI.GetNodes().Append(node.NewInputField("method", "webauthn", node.WebAuthnGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginWebAuthn())) return nil } @@ -134,7 +64,7 @@ func (s *Strategy) populateLoginMethod(r *http.Request, sr *login.Flow, i *ident } webAuthCreds := conf.Credentials.ToWebAuthn() - if !sr.IsForced() { + if !sr.IsRefresh() { webAuthCreds = conf.Credentials.ToWebAuthnFiltered(aal) } @@ -245,7 +175,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.handleLoginError(r, f, err) } - if s.d.Config().WebAuthnForPasswordless(r.Context()) || f.IsForced() && f.RequestedAAL == identity.AuthenticatorAssuranceLevel1 { + if s.d.Config().WebAuthnForPasswordless(r.Context()) || f.IsRefresh() && f.RequestedAAL == identity.AuthenticatorAssuranceLevel1 { return s.loginPasswordless(w, r, f, &p) } @@ -337,7 +267,7 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f * } webAuthCreds := o.Credentials.ToWebAuthnFiltered(aal) - if f.IsForced() { + if f.IsRefresh() { webAuthCreds = o.Credentials.ToWebAuthn() } @@ -365,3 +295,107 @@ func (s *Strategy) loginMultiFactor(w http.ResponseWriter, r *http.Request, f *l } return s.loginAuthenticate(w, r, f, identityID, p, identity.AuthenticatorAssuranceLevel2) } + +func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser { + return nil + } + + identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID()) + if identifier == "" { + return nil + } + + if err := s.populateLoginMethod(r, sr, id, text.NewInfoSelfServiceLoginWebAuthn(), ""); errors.Is(err, webauthnx.ErrNoCredentials) { + return nil + } else if err != nil { + return err + } + + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden)) + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) { + return nil + } + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + + sr.UI.SetNode(node.NewInputField( + "identifier", + "", + node.DefaultGroup, + node.InputAttributeTypeText, + node.WithRequiredInputAttribute, + func(attributes *node.InputAttributes) { attributes.Autocomplete = "username webauthn" }, + ).WithMetaLabel(identifierLabel)) + + if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) { + return nil + } else if err != nil { + return err + } + + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser || s.d.Config().WebAuthnForPasswordless(r.Context()) { + return nil + } + + // We have done proper validation before so this should never error + sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r) + if err != nil { + return err + } + + if err := s.populateLoginMethod(r, sr, sess.Identity, text.NewInfoSelfServiceLoginWebAuthn(), identity.AuthenticatorAssuranceLevel2); errors.Is(err, webauthnx.ErrNoCredentials) { + return nil + } else if err != nil { + return err + } + + return nil +} + +func (s *Strategy) PopulateLoginMethodMultiStepSelection(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error { + if sr.Type != flow.TypeBrowser && !s.d.Config().WebAuthnForPasswordless(r.Context()) { + return nil + } + + o := login.NewFormHydratorOptions(opts) + if o.IdentityHint == nil { + // Identity was not found so add fields + } else { + // If we have an identity hint we can perform identity credentials discovery and + // hide this credential if it should not be included. + count, err := s.CountActiveFirstFactorCredentials(o.IdentityHint.Credentials) + if err != nil { + return err + } else if count == 0 && !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) { + return nil + } + } + + if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) { + return nil + } else if err != nil { + return err + } + return nil + +} + +func (s *Strategy) PopulateLoginMethodMultiStepIdentification(r *http.Request, sr *login.Flow) error { + return nil +} diff --git a/selfservice/strategy/webauthn/strategy.go b/selfservice/strategy/webauthn/strategy.go index 998490055996..ba2ced37b3e5 100644 --- a/selfservice/strategy/webauthn/strategy.go +++ b/selfservice/strategy/webauthn/strategy.go @@ -6,7 +6,6 @@ package webauthn import ( "context" "encoding/json" - "github.com/pkg/errors" "github.com/ory/kratos/continuity" diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index 0b4584646abc..199dfa81a79a 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -429,7 +429,7 @@ Cypress.Commands.add( f.group === "default" && "name" in f.attributes && f.attributes.name === "traits.email", - ).attributes.value, + )?.attributes.value, ).to.eq(email) return cy diff --git a/test/e2e/profiles/code/.kratos.yml b/test/e2e/profiles/code/.kratos.yml index 3e98857e1628..680fb7255457 100644 --- a/test/e2e/profiles/code/.kratos.yml +++ b/test/e2e/profiles/code/.kratos.yml @@ -19,6 +19,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false after: code: hooks: @@ -38,7 +40,6 @@ selfservice: enabled: true code: passwordless_enabled: true - passwordless_login_fallback_enabled: false enabled: true config: lifespan: 1h diff --git a/test/e2e/profiles/email/.kratos.yml b/test/e2e/profiles/email/.kratos.yml index b1d62a3e25c4..dbc47fa538b7 100644 --- a/test/e2e/profiles/email/.kratos.yml +++ b/test/e2e/profiles/email/.kratos.yml @@ -18,6 +18,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/mfa/.kratos.yml b/test/e2e/profiles/mfa/.kratos.yml index 99becd59a868..d2fd33e1a13b 100644 --- a/test/e2e/profiles/mfa/.kratos.yml +++ b/test/e2e/profiles/mfa/.kratos.yml @@ -19,6 +19,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/mobile/.kratos.yml b/test/e2e/profiles/mobile/.kratos.yml index c0a46e57c197..32b01485c91b 100644 --- a/test/e2e/profiles/mobile/.kratos.yml +++ b/test/e2e/profiles/mobile/.kratos.yml @@ -20,6 +20,9 @@ selfservice: verification: enabled: false + login: + two_step: + enabled: false methods: totp: enabled: true diff --git a/test/e2e/profiles/network/.kratos.yml b/test/e2e/profiles/network/.kratos.yml index c3c8b3daedd7..41ba01f50303 100644 --- a/test/e2e/profiles/network/.kratos.yml +++ b/test/e2e/profiles/network/.kratos.yml @@ -21,6 +21,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false before: hooks: - hook: web_hook diff --git a/test/e2e/profiles/oidc-provider-mfa/.kratos.yml b/test/e2e/profiles/oidc-provider-mfa/.kratos.yml index ac577ce45724..690da6b70b3f 100644 --- a/test/e2e/profiles/oidc-provider-mfa/.kratos.yml +++ b/test/e2e/profiles/oidc-provider-mfa/.kratos.yml @@ -21,6 +21,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/oidc-provider/.kratos.yml b/test/e2e/profiles/oidc-provider/.kratos.yml index 09b2c9978700..900ebf1fb0e4 100644 --- a/test/e2e/profiles/oidc-provider/.kratos.yml +++ b/test/e2e/profiles/oidc-provider/.kratos.yml @@ -42,6 +42,8 @@ selfservice: - hook: session login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/oidc/.kratos.yml b/test/e2e/profiles/oidc/.kratos.yml index b0a327bb5096..f174237751cd 100644 --- a/test/e2e/profiles/oidc/.kratos.yml +++ b/test/e2e/profiles/oidc/.kratos.yml @@ -51,6 +51,8 @@ selfservice: - hook: session login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/passkey/.kratos.yml b/test/e2e/profiles/passkey/.kratos.yml index 85441f599e1b..0f08d87434d8 100644 --- a/test/e2e/profiles/passkey/.kratos.yml +++ b/test/e2e/profiles/passkey/.kratos.yml @@ -22,6 +22,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/passwordless/.kratos.yml b/test/e2e/profiles/passwordless/.kratos.yml index b3582a61216c..4fc40604a148 100644 --- a/test/e2e/profiles/passwordless/.kratos.yml +++ b/test/e2e/profiles/passwordless/.kratos.yml @@ -25,6 +25,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/recovery-mfa/.kratos.yml b/test/e2e/profiles/recovery-mfa/.kratos.yml index 03b0337cef2f..8e215ef0d162 100644 --- a/test/e2e/profiles/recovery-mfa/.kratos.yml +++ b/test/e2e/profiles/recovery-mfa/.kratos.yml @@ -22,6 +22,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false registration: ui_url: http://localhost:4455/registration error: diff --git a/test/e2e/profiles/recovery/.kratos.yml b/test/e2e/profiles/recovery/.kratos.yml index 3d3ca8f3aca7..00077bb140c3 100644 --- a/test/e2e/profiles/recovery/.kratos.yml +++ b/test/e2e/profiles/recovery/.kratos.yml @@ -21,6 +21,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false registration: ui_url: http://localhost:4455/registration error: diff --git a/test/e2e/profiles/spa/.kratos.yml b/test/e2e/profiles/spa/.kratos.yml index 6d5eb44a67de..69c169f7ccf0 100644 --- a/test/e2e/profiles/spa/.kratos.yml +++ b/test/e2e/profiles/spa/.kratos.yml @@ -23,6 +23,8 @@ selfservice: hook: session login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: diff --git a/test/e2e/profiles/two-steps/.kratos.yml b/test/e2e/profiles/two-steps/.kratos.yml index d23dd0bce07c..01b35e53d2cc 100644 --- a/test/e2e/profiles/two-steps/.kratos.yml +++ b/test/e2e/profiles/two-steps/.kratos.yml @@ -28,6 +28,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false error: ui_url: http://localhost:4455/error verification: @@ -66,7 +68,6 @@ selfservice: code: enabled: true passwordless_enabled: true - passwordless_login_fallback_enabled: false config: lifespan: 1h diff --git a/test/e2e/profiles/verification/.kratos.yml b/test/e2e/profiles/verification/.kratos.yml index ca4932f18c08..881f8d43a58e 100644 --- a/test/e2e/profiles/verification/.kratos.yml +++ b/test/e2e/profiles/verification/.kratos.yml @@ -26,6 +26,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false registration: ui_url: http://localhost:4455/registration error: diff --git a/test/e2e/profiles/webhooks/.kratos.yml b/test/e2e/profiles/webhooks/.kratos.yml index 00eb10537adb..f4f6698a3f29 100644 --- a/test/e2e/profiles/webhooks/.kratos.yml +++ b/test/e2e/profiles/webhooks/.kratos.yml @@ -30,6 +30,8 @@ selfservice: login: ui_url: http://localhost:4455/login + two_step: + enabled: false after: password: hooks: diff --git a/text/id.go b/text/id.go index a466caec0f8c..edff417a2738 100644 --- a/text/id.go +++ b/text/id.go @@ -31,6 +31,7 @@ const ( InfoSelfServiceLoginCodeMFA // 1010019 InfoSelfServiceLoginCodeMFAHint // 1010020 InfoSelfServiceLoginPasskey // 1010021 + InfoSelfServiceLoginPassword // 1010022 ) const ( @@ -86,21 +87,21 @@ const ( ) const ( - InfoNodeLabel ID = 1070000 + iota // 1070000 - InfoNodeLabelInputPassword // 1070001 - InfoNodeLabelGenerated // 1070002 - InfoNodeLabelSave // 1070003 - InfoNodeLabelID // 1070004 - InfoNodeLabelSubmit // 1070005 - InfoNodeLabelVerifyOTP // 1070006 - InfoNodeLabelEmail // 1070007 - InfoNodeLabelResendOTP // 1070008 - InfoNodeLabelContinue // 1070009 - InfoNodeLabelRecoveryCode // 1070010 - InfoNodeLabelVerificationCode // 1070011 - InfoNodeLabelRegistrationCode // 1070012 - InfoNodeLabelLoginCode // 1070013 - InfoNodeLabelLoginAndLinkCredential + InfoNodeLabel ID = 1070000 + iota // 1070000 + InfoNodeLabelInputPassword // 1070001 + InfoNodeLabelGenerated // 1070002 + InfoNodeLabelSave // 1070003 + InfoNodeLabelID // 1070004 + InfoNodeLabelSubmit // 1070005 + InfoNodeLabelVerifyOTP // 1070006 + InfoNodeLabelEmail // 1070007 + InfoNodeLabelResendOTP // 1070008 + InfoNodeLabelContinue // 1070009 + InfoNodeLabelRecoveryCode // 1070010 + InfoNodeLabelVerificationCode // 1070011 + InfoNodeLabelRegistrationCode // 1070012 + InfoNodeLabelLoginCode // 1070013 + InfoNodeLabelLoginAndLinkCredential // 1070014 ) const ( @@ -148,6 +149,7 @@ const ( ErrorValidationPasswordTooManyBreaches ErrorValidationNoCodeUser ErrorValidationTraitsMismatch + ErrorValidationAccountNotFound ) const ( diff --git a/text/message_login.go b/text/message_login.go index ec627458a028..9312a21e97a2 100644 --- a/text/message_login.go +++ b/text/message_login.go @@ -89,6 +89,14 @@ func NewInfoLoginTOTP() *Message { } } +func NewInfoLoginPassword() *Message { + return &Message{ + ID: InfoSelfServiceLoginPassword, + Text: "Sign in with password", + Type: Info, + } +} + func NewInfoLoginLookup() *Message { return &Message{ ID: InfoLoginLookup, @@ -182,7 +190,7 @@ func NewErrorValidationVerificationNoStrategyFound() *Message { func NewInfoSelfServiceLoginWebAuthn() *Message { return &Message{ ID: InfoSelfServiceLoginWebAuthn, - Text: "Use security key", + Text: "Sign in with hardware key", Type: Info, } } @@ -198,7 +206,7 @@ func NewInfoSelfServiceLoginPasskey() *Message { func NewInfoSelfServiceContinueLoginWebAuthn() *Message { return &Message{ ID: InfoSelfServiceLoginContinueWebAuthn, - Text: "Continue with security key", + Text: "Sign in with hardware key", Type: Info, } } @@ -239,7 +247,7 @@ func NewInfoSelfServiceLoginCode() *Message { return &Message{ ID: InfoSelfServiceLoginCode, Type: Info, - Text: "Sign in with code", + Text: "Send sign in code", } } diff --git a/text/message_validation.go b/text/message_validation.go index c10fddead805..2fd1e4c2d28d 100644 --- a/text/message_validation.go +++ b/text/message_validation.go @@ -257,6 +257,14 @@ func NewErrorValidationInvalidCredentials() *Message { } } +func NewErrorValidationAccountNotFound() *Message { + return &Message{ + ID: ErrorValidationAccountNotFound, + Text: "This account does not exist or has no login method configured.", + Type: Error, + } +} + func NewErrorValidationDuplicateCredentials() *Message { return &Message{ ID: ErrorValidationDuplicateCredentials, diff --git a/ui/node/attributes.go b/ui/node/attributes.go index 9611b5828dff..762df9fd46c7 100644 --- a/ui/node/attributes.go +++ b/ui/node/attributes.go @@ -3,7 +3,10 @@ package node -import "github.com/ory/kratos/text" +import ( + "fmt" + "github.com/ory/kratos/text" +) const ( InputAttributeTypeText UiNodeInputAttributeType = "text" @@ -53,6 +56,9 @@ type Attributes interface { // swagger:ignore GetNodeType() UiNodeType + + // swagger:ignore + Matches(other Attributes) bool } // InputAttributes represents the attributes of an input node @@ -267,6 +273,99 @@ func (a *ScriptAttributes) ID() string { return a.Identifier } +func (a *InputAttributes) Matches(other Attributes) bool { + ot, ok := other.(*InputAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + if len(ot.Type) > 0 && a.Type != ot.Type { + return false + } + + if ot.FieldValue != nil && fmt.Sprintf("%v", a.FieldValue) != fmt.Sprintf("%v", ot.FieldValue) { + return false + } + + if len(ot.Name) > 0 && a.Name != ot.Name { + return false + } + + return true +} + +func (a *ImageAttributes) Matches(other Attributes) bool { + ot, ok := other.(*ImageAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + if len(ot.Source) > 0 && a.Source != ot.Source { + return false + } + + return true +} + +func (a *AnchorAttributes) Matches(other Attributes) bool { + ot, ok := other.(*AnchorAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + if len(ot.HREF) > 0 && a.HREF != ot.HREF { + return false + } + + return true +} + +func (a *TextAttributes) Matches(other Attributes) bool { + ot, ok := other.(*TextAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + return true +} + +func (a *ScriptAttributes) Matches(other Attributes) bool { + ot, ok := other.(*ScriptAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + if ot.Type != "" && a.Type != ot.Type { + return false + } + + if ot.Source != "" && a.Source != ot.Source { + return false + } + + return true +} + func (a *InputAttributes) SetValue(value interface{}) { a.FieldValue = value } diff --git a/ui/node/attributes_test.go b/ui/node/attributes_test.go index 218919e1145e..62c2316d3c9f 100644 --- a/ui/node/attributes_test.go +++ b/ui/node/attributes_test.go @@ -21,6 +21,70 @@ func TestIDs(t *testing.T) { assert.EqualValues(t, "foo", (&ScriptAttributes{Identifier: "foo"}).ID()) } +func TestMatchesAnchorAttributes(t *testing.T) { + assert.True(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&AnchorAttributes{Identifier: "foo"})) + assert.True(t, (&AnchorAttributes{HREF: "bar"}).Matches(&AnchorAttributes{HREF: "bar"})) + assert.False(t, (&AnchorAttributes{HREF: "foo"}).Matches(&AnchorAttributes{HREF: "bar"})) + assert.False(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&AnchorAttributes{HREF: "bar"})) + + assert.True(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "foo", HREF: "bar"})) + assert.False(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "foo", HREF: "baz"})) + assert.False(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "bar", HREF: "bar"})) + + assert.False(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"})) +} + +func TestMatchesImageAttributes(t *testing.T) { + assert.True(t, (&ImageAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Identifier: "foo"})) + assert.True(t, (&ImageAttributes{Source: "bar"}).Matches(&ImageAttributes{Source: "bar"})) + assert.False(t, (&ImageAttributes{Source: "foo"}).Matches(&ImageAttributes{Source: "bar"})) + assert.False(t, (&ImageAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Source: "bar"})) + + assert.True(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "foo", Source: "bar"})) + assert.False(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "foo", Source: "baz"})) + assert.False(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "bar", Source: "bar"})) + + assert.False(t, (&ImageAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"})) +} + +func TestMatchesInputAttributes(t *testing.T) { + // Test when other is not of type *InputAttributes + var attr Attributes = &ImageAttributes{} + inputAttr := &InputAttributes{Name: "foo"} + assert.False(t, inputAttr.Matches(attr)) + + // Test when ID is different + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText} + inputAttr = &InputAttributes{Name: "bar", Type: InputAttributeTypeText} + assert.False(t, inputAttr.Matches(attr)) + + // Test when Type is different + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText} + inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeNumber} + assert.False(t, inputAttr.Matches(attr)) + + // Test when FieldValue is different + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"} + inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "baz"} + assert.False(t, inputAttr.Matches(attr)) + + // Test when Name is different + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText} + inputAttr = &InputAttributes{Name: "bar", Type: InputAttributeTypeText} + assert.False(t, inputAttr.Matches(attr)) + + // Test when all fields are the same + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"} + inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"} + assert.True(t, inputAttr.Matches(attr)) +} + +func TestMatchesTextAttributes(t *testing.T) { + assert.True(t, (&TextAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"})) + assert.True(t, (&TextAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"})) + assert.False(t, (&TextAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Identifier: "foo"})) +} + func TestNodeEncode(t *testing.T) { script := jsonx.TestMarshalJSONString(t, &Node{Attributes: &ScriptAttributes{}}) assert.EqualValues(t, Script, gjson.Get(script, "attributes.node_type").String()) diff --git a/ui/node/node.go b/ui/node/node.go index e08295b827f4..cf0cedb79f94 100644 --- a/ui/node/node.go +++ b/ui/node/node.go @@ -49,6 +49,7 @@ const ( LookupGroup UiNodeGroup = "lookup_secret" WebAuthnGroup UiNodeGroup = "webauthn" PasskeyGroup UiNodeGroup = "passkey" + TwoStepGroup UiNodeGroup = "two_step" ) func (g UiNodeGroup) String() string { @@ -218,6 +219,7 @@ func SortUseOrder(keysInOrder []string) func(*sortOptions) { options.keysInOrder = keysInOrder } } + func SortUseOrderAppend(keysInOrder []string) func(*sortOptions) { return func(options *sortOptions) { options.keysInOrderAppend = keysInOrder @@ -353,6 +355,37 @@ func (n *Nodes) Append(node *Node) { *n = append(*n, node) } +func (n *Nodes) RemoveMatching(node *Node) { + if n == nil { + return + } + + var r Nodes + for k, v := range *n { + if !(*n)[k].Matches(node) { + r = append(r, v) + } + } + + *n = r +} + +func (n *Node) Matches(needle *Node) bool { + if len(needle.ID()) > 0 && n.ID() != needle.ID() { + return false + } + + if needle.Type != "" && n.Type != needle.Type { + return false + } + + if needle.Group != "" && n.Group != needle.Group { + return false + } + + return n.Attributes.Matches(needle.Attributes) +} + func (n *Node) UnmarshalJSON(data []byte) error { var attr Attributes switch t := gjson.GetBytes(data, "type").String(); UiNodeType(t) { diff --git a/ui/node/node_test.go b/ui/node/node_test.go index f8867b98c2a3..c9f1dca1838f 100644 --- a/ui/node/node_test.go +++ b/ui/node/node_test.go @@ -8,6 +8,7 @@ import ( "context" "embed" "encoding/json" + "github.com/ory/kratos/text" "path/filepath" "testing" @@ -193,3 +194,64 @@ func TestNodeJSON(t *testing.T) { require.EqualError(t, json.NewDecoder(bytes.NewReader(json.RawMessage(`{"type": "foo"}`))).Decode(&n), "unexpected node type: foo") }) } + +func TestMatchesNode(t *testing.T) { + // Test when ID is different + node1 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + node2 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}} + assert.False(t, node1.Matches(node2)) + + // Test when Type is different + node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + node2 = &node.Node{Type: node.Text, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + assert.False(t, node1.Matches(node2)) + + // Test when Group is different + node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + node2 = &node.Node{Type: node.Input, Group: node.OpenIDConnectGroup, Attributes: &node.InputAttributes{Name: "foo"}} + assert.False(t, node1.Matches(node2)) + + // Test when all fields are the same + node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + node2 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + assert.True(t, node1.Matches(node2)) +} + +func TestRemoveMatchingNodes(t *testing.T) { + nodes := node.Nodes{ + &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}, + &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}}, + &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}}, + } + + // Test when node to remove is present + nodeToRemove := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}} + nodes.RemoveMatching(nodeToRemove) + assert.Len(t, nodes, 2) + for _, n := range nodes { + assert.NotEqual(t, nodeToRemove.ID(), n.ID()) + } + + // Test when node to remove is not present + nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "qux"}} + nodes.RemoveMatching(nodeToRemove) + assert.Len(t, nodes, 2) // length should remain the same + + // Test when node to remove is present + nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}} + ui := &container.Container{ + Nodes: nodes, + } + + ui.GetNodes().RemoveMatching(nodeToRemove) + assert.Len(t, *ui.GetNodes(), 1) + for _, n := range *ui.GetNodes() { + assert.NotEqual(t, "bar", n.ID()) + assert.NotEqual(t, "baz", n.ID()) + } + + ui.Nodes.Append(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue())) + assert.NotNil(t, ui.Nodes.Find("method")) + ui.GetNodes().RemoveMatching(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit)) + assert.Nil(t, ui.Nodes.Find("method")) +} From 735fc5b2c5a99746d3012cc38ee2e1b7cc3a67f2 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 26 Mar 2024 16:46:34 +0100 Subject: [PATCH 20/71] feat: add additional messages --- text/id.go | 1 + text/message_system.go | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/text/id.go b/text/id.go index edff417a2738..995d037e9606 100644 --- a/text/id.go +++ b/text/id.go @@ -201,4 +201,5 @@ const ( const ( ErrorSystem ID = 5000000 + iota ErrorSystemGeneric + ErrorSelfServiceNoMethodsAvailable ) diff --git a/text/message_system.go b/text/message_system.go index d94ce73f9872..b4b0f20659e7 100644 --- a/text/message_system.go +++ b/text/message_system.go @@ -13,3 +13,11 @@ func NewErrorSystemGeneric(reason string) *Message { }), } } + +func NewErrorSelfServiceNoMethodsAvailable() *Message { + return &Message{ + ID: ErrorSelfServiceNoMethodsAvailable, + Text: "No authentication methods are available for this request. Please contact the site or app owner.", + Type: Error, + } +} From 99c945c92d0c2745dc8df4402d755afd53e1b9aa Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:04:57 +0200 Subject: [PATCH 21/71] feat: add redirect to continue_with for SPA flows This patch adds the new `continue_with` action `redirect_browser_to`, which contains the redirect URL the app should redirect to. It is only supported for SPA (not server-side browser apps, not native apps) flows at this point in time. --- .schema/openapi/patches/schema.yaml | 2 + internal/client-go/.openapi-generator/FILES | 2 + internal/client-go/README.md | 1 + internal/client-go/model_continue_with.go | 40 +++++ ...model_continue_with_redirect_browser_to.go | 147 +++++++++++++++++ internal/httpclient/.openapi-generator/FILES | 2 + internal/httpclient/README.md | 1 + internal/httpclient/model_continue_with.go | 40 +++++ ...model_continue_with_redirect_browser_to.go | 147 +++++++++++++++++ selfservice/flow/continue_with.go | 28 ++++ selfservice/flow/login/hook.go | 4 + selfservice/flow/registration/hook.go | 7 +- selfservice/flow/settings/hook.go | 1 + .../strategy/code/strategy_login_test.go | 10 +- .../code/strategy_registration_test.go | 16 +- selfservice/strategy/lookup/login_test.go | 7 + selfservice/strategy/lookup/settings_test.go | 6 + .../strategy/passkey/passkey_login_test.go | 7 + .../passkey/passkey_registration_test.go | 9 ++ .../strategy/passkey/passkey_settings_test.go | 7 + selfservice/strategy/password/login_test.go | 26 +++ .../strategy/password/registration_test.go | 51 +++--- .../strategy/password/settings_test.go | 11 +- ...on=hydrate_the_proper_fields-type=spa.json | 153 ++++++++++++++++++ selfservice/strategy/profile/strategy_test.go | 9 +- selfservice/strategy/totp/login_test.go | 17 +- selfservice/strategy/totp/settings_test.go | 12 ++ selfservice/strategy/webauthn/login_test.go | 7 + .../strategy/webauthn/registration_test.go | 7 + .../strategy/webauthn/settings_test.go | 8 + spec/api.json | 20 +++ spec/swagger.json | 16 ++ 32 files changed, 790 insertions(+), 31 deletions(-) create mode 100644 internal/client-go/model_continue_with_redirect_browser_to.go create mode 100644 internal/httpclient/model_continue_with_redirect_browser_to.go create mode 100644 selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json diff --git a/.schema/openapi/patches/schema.yaml b/.schema/openapi/patches/schema.yaml index 206aceb2708e..ff661ce4079d 100644 --- a/.schema/openapi/patches/schema.yaml +++ b/.schema/openapi/patches/schema.yaml @@ -43,6 +43,7 @@ set_ory_session_token: "#/components/schemas/continueWithSetOrySessionToken" show_settings_ui: "#/components/schemas/continueWithSettingsUi" show_recovery_ui: "#/components/schemas/continueWithRecoveryUi" + redirect_browser_to: "#/components/schemas/continueWithRedirectBrowserTo" - op: add path: /components/schemas/continueWith/oneOf @@ -51,3 +52,4 @@ - "$ref": "#/components/schemas/continueWithSetOrySessionToken" - "$ref": "#/components/schemas/continueWithSettingsUi" - "$ref": "#/components/schemas/continueWithRecoveryUi" + - "$ref": "#/components/schemas/continueWithRedirectBrowserTo" diff --git a/internal/client-go/.openapi-generator/FILES b/internal/client-go/.openapi-generator/FILES index fdf34c5e1507..8f05b235508f 100644 --- a/internal/client-go/.openapi-generator/FILES +++ b/internal/client-go/.openapi-generator/FILES @@ -15,6 +15,7 @@ docs/ConsistencyRequestParameters.md docs/ContinueWith.md docs/ContinueWithRecoveryUi.md docs/ContinueWithRecoveryUiFlow.md +docs/ContinueWithRedirectBrowserTo.md docs/ContinueWithSetOrySessionToken.md docs/ContinueWithSettingsUi.md docs/ContinueWithSettingsUiFlow.md @@ -139,6 +140,7 @@ model_consistency_request_parameters.go model_continue_with.go model_continue_with_recovery_ui.go model_continue_with_recovery_ui_flow.go +model_continue_with_redirect_browser_to.go model_continue_with_set_ory_session_token.go model_continue_with_settings_ui.go model_continue_with_settings_ui_flow.go diff --git a/internal/client-go/README.md b/internal/client-go/README.md index 04dd61ab7d1e..01f9831e7520 100644 --- a/internal/client-go/README.md +++ b/internal/client-go/README.md @@ -142,6 +142,7 @@ Class | Method | HTTP request | Description - [ContinueWith](docs/ContinueWith.md) - [ContinueWithRecoveryUi](docs/ContinueWithRecoveryUi.md) - [ContinueWithRecoveryUiFlow](docs/ContinueWithRecoveryUiFlow.md) + - [ContinueWithRedirectBrowserTo](docs/ContinueWithRedirectBrowserTo.md) - [ContinueWithSetOrySessionToken](docs/ContinueWithSetOrySessionToken.md) - [ContinueWithSettingsUi](docs/ContinueWithSettingsUi.md) - [ContinueWithSettingsUiFlow](docs/ContinueWithSettingsUiFlow.md) diff --git a/internal/client-go/model_continue_with.go b/internal/client-go/model_continue_with.go index 9e97dbf479e7..6fb1056836e6 100644 --- a/internal/client-go/model_continue_with.go +++ b/internal/client-go/model_continue_with.go @@ -19,6 +19,7 @@ import ( // ContinueWith - struct for ContinueWith type ContinueWith struct { ContinueWithRecoveryUi *ContinueWithRecoveryUi + ContinueWithRedirectBrowserTo *ContinueWithRedirectBrowserTo ContinueWithSetOrySessionToken *ContinueWithSetOrySessionToken ContinueWithSettingsUi *ContinueWithSettingsUi ContinueWithVerificationUi *ContinueWithVerificationUi @@ -31,6 +32,13 @@ func ContinueWithRecoveryUiAsContinueWith(v *ContinueWithRecoveryUi) ContinueWit } } +// ContinueWithRedirectBrowserToAsContinueWith is a convenience function that returns ContinueWithRedirectBrowserTo wrapped in ContinueWith +func ContinueWithRedirectBrowserToAsContinueWith(v *ContinueWithRedirectBrowserTo) ContinueWith { + return ContinueWith{ + ContinueWithRedirectBrowserTo: v, + } +} + // ContinueWithSetOrySessionTokenAsContinueWith is a convenience function that returns ContinueWithSetOrySessionToken wrapped in ContinueWith func ContinueWithSetOrySessionTokenAsContinueWith(v *ContinueWithSetOrySessionToken) ContinueWith { return ContinueWith{ @@ -62,6 +70,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error { return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") } + // check if the discriminator value is 'redirect_browser_to' + if jsonDict["action"] == "redirect_browser_to" { + // try to unmarshal JSON data into ContinueWithRedirectBrowserTo + err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo) + if err == nil { + return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match + } else { + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) + } + } + // check if the discriminator value is 'set_ory_session_token' if jsonDict["action"] == "set_ory_session_token" { // try to unmarshal JSON data into ContinueWithSetOrySessionToken @@ -122,6 +142,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'continueWithRedirectBrowserTo' + if jsonDict["action"] == "continueWithRedirectBrowserTo" { + // try to unmarshal JSON data into ContinueWithRedirectBrowserTo + err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo) + if err == nil { + return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match + } else { + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) + } + } + // check if the discriminator value is 'continueWithSetOrySessionToken' if jsonDict["action"] == "continueWithSetOrySessionToken" { // try to unmarshal JSON data into ContinueWithSetOrySessionToken @@ -167,6 +199,10 @@ func (src ContinueWith) MarshalJSON() ([]byte, error) { return json.Marshal(&src.ContinueWithRecoveryUi) } + if src.ContinueWithRedirectBrowserTo != nil { + return json.Marshal(&src.ContinueWithRedirectBrowserTo) + } + if src.ContinueWithSetOrySessionToken != nil { return json.Marshal(&src.ContinueWithSetOrySessionToken) } @@ -191,6 +227,10 @@ func (obj *ContinueWith) GetActualInstance() interface{} { return obj.ContinueWithRecoveryUi } + if obj.ContinueWithRedirectBrowserTo != nil { + return obj.ContinueWithRedirectBrowserTo + } + if obj.ContinueWithSetOrySessionToken != nil { return obj.ContinueWithSetOrySessionToken } diff --git a/internal/client-go/model_continue_with_redirect_browser_to.go b/internal/client-go/model_continue_with_redirect_browser_to.go new file mode 100644 index 000000000000..46344016b779 --- /dev/null +++ b/internal/client-go/model_continue_with_redirect_browser_to.go @@ -0,0 +1,147 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui +type ContinueWithRedirectBrowserTo struct { + // Action will always be `redirect_browser_to` + Action interface{} `json:"action"` + // The URL to redirect the browser to + RedirectBrowserTo *string `json:"redirect_browser_to,omitempty"` +} + +// NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewContinueWithRedirectBrowserTo(action interface{}) *ContinueWithRedirectBrowserTo { + this := ContinueWithRedirectBrowserTo{} + this.Action = action + return &this +} + +// NewContinueWithRedirectBrowserToWithDefaults instantiates a new ContinueWithRedirectBrowserTo object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowserTo { + this := ContinueWithRedirectBrowserTo{} + return &this +} + +// GetAction returns the Action field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *ContinueWithRedirectBrowserTo) GetAction() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.Action +} + +// GetActionOk returns a tuple with the Action field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*interface{}, bool) { + if o == nil || o.Action == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithRedirectBrowserTo) SetAction(v interface{}) { + o.Action = v +} + +// GetRedirectBrowserTo returns the RedirectBrowserTo field value if set, zero value otherwise. +func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string { + if o == nil || o.RedirectBrowserTo == nil { + var ret string + return ret + } + return *o.RedirectBrowserTo +} + +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) { + if o == nil || o.RedirectBrowserTo == nil { + return nil, false + } + return o.RedirectBrowserTo, true +} + +// HasRedirectBrowserTo returns a boolean if a field has been set. +func (o *ContinueWithRedirectBrowserTo) HasRedirectBrowserTo() bool { + if o != nil && o.RedirectBrowserTo != nil { + return true + } + + return false +} + +// SetRedirectBrowserTo gets a reference to the given string and assigns it to the RedirectBrowserTo field. +func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) { + o.RedirectBrowserTo = &v +} + +func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Action != nil { + toSerialize["action"] = o.Action + } + if o.RedirectBrowserTo != nil { + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + } + return json.Marshal(toSerialize) +} + +type NullableContinueWithRedirectBrowserTo struct { + value *ContinueWithRedirectBrowserTo + isSet bool +} + +func (v NullableContinueWithRedirectBrowserTo) Get() *ContinueWithRedirectBrowserTo { + return v.value +} + +func (v *NullableContinueWithRedirectBrowserTo) Set(val *ContinueWithRedirectBrowserTo) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithRedirectBrowserTo) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithRedirectBrowserTo) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithRedirectBrowserTo(val *ContinueWithRedirectBrowserTo) *NullableContinueWithRedirectBrowserTo { + return &NullableContinueWithRedirectBrowserTo{value: val, isSet: true} +} + +func (v NullableContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithRedirectBrowserTo) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index fdf34c5e1507..8f05b235508f 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -15,6 +15,7 @@ docs/ConsistencyRequestParameters.md docs/ContinueWith.md docs/ContinueWithRecoveryUi.md docs/ContinueWithRecoveryUiFlow.md +docs/ContinueWithRedirectBrowserTo.md docs/ContinueWithSetOrySessionToken.md docs/ContinueWithSettingsUi.md docs/ContinueWithSettingsUiFlow.md @@ -139,6 +140,7 @@ model_consistency_request_parameters.go model_continue_with.go model_continue_with_recovery_ui.go model_continue_with_recovery_ui_flow.go +model_continue_with_redirect_browser_to.go model_continue_with_set_ory_session_token.go model_continue_with_settings_ui.go model_continue_with_settings_ui_flow.go diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index 04dd61ab7d1e..01f9831e7520 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -142,6 +142,7 @@ Class | Method | HTTP request | Description - [ContinueWith](docs/ContinueWith.md) - [ContinueWithRecoveryUi](docs/ContinueWithRecoveryUi.md) - [ContinueWithRecoveryUiFlow](docs/ContinueWithRecoveryUiFlow.md) + - [ContinueWithRedirectBrowserTo](docs/ContinueWithRedirectBrowserTo.md) - [ContinueWithSetOrySessionToken](docs/ContinueWithSetOrySessionToken.md) - [ContinueWithSettingsUi](docs/ContinueWithSettingsUi.md) - [ContinueWithSettingsUiFlow](docs/ContinueWithSettingsUiFlow.md) diff --git a/internal/httpclient/model_continue_with.go b/internal/httpclient/model_continue_with.go index 9e97dbf479e7..6fb1056836e6 100644 --- a/internal/httpclient/model_continue_with.go +++ b/internal/httpclient/model_continue_with.go @@ -19,6 +19,7 @@ import ( // ContinueWith - struct for ContinueWith type ContinueWith struct { ContinueWithRecoveryUi *ContinueWithRecoveryUi + ContinueWithRedirectBrowserTo *ContinueWithRedirectBrowserTo ContinueWithSetOrySessionToken *ContinueWithSetOrySessionToken ContinueWithSettingsUi *ContinueWithSettingsUi ContinueWithVerificationUi *ContinueWithVerificationUi @@ -31,6 +32,13 @@ func ContinueWithRecoveryUiAsContinueWith(v *ContinueWithRecoveryUi) ContinueWit } } +// ContinueWithRedirectBrowserToAsContinueWith is a convenience function that returns ContinueWithRedirectBrowserTo wrapped in ContinueWith +func ContinueWithRedirectBrowserToAsContinueWith(v *ContinueWithRedirectBrowserTo) ContinueWith { + return ContinueWith{ + ContinueWithRedirectBrowserTo: v, + } +} + // ContinueWithSetOrySessionTokenAsContinueWith is a convenience function that returns ContinueWithSetOrySessionToken wrapped in ContinueWith func ContinueWithSetOrySessionTokenAsContinueWith(v *ContinueWithSetOrySessionToken) ContinueWith { return ContinueWith{ @@ -62,6 +70,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error { return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") } + // check if the discriminator value is 'redirect_browser_to' + if jsonDict["action"] == "redirect_browser_to" { + // try to unmarshal JSON data into ContinueWithRedirectBrowserTo + err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo) + if err == nil { + return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match + } else { + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) + } + } + // check if the discriminator value is 'set_ory_session_token' if jsonDict["action"] == "set_ory_session_token" { // try to unmarshal JSON data into ContinueWithSetOrySessionToken @@ -122,6 +142,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'continueWithRedirectBrowserTo' + if jsonDict["action"] == "continueWithRedirectBrowserTo" { + // try to unmarshal JSON data into ContinueWithRedirectBrowserTo + err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo) + if err == nil { + return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match + } else { + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) + } + } + // check if the discriminator value is 'continueWithSetOrySessionToken' if jsonDict["action"] == "continueWithSetOrySessionToken" { // try to unmarshal JSON data into ContinueWithSetOrySessionToken @@ -167,6 +199,10 @@ func (src ContinueWith) MarshalJSON() ([]byte, error) { return json.Marshal(&src.ContinueWithRecoveryUi) } + if src.ContinueWithRedirectBrowserTo != nil { + return json.Marshal(&src.ContinueWithRedirectBrowserTo) + } + if src.ContinueWithSetOrySessionToken != nil { return json.Marshal(&src.ContinueWithSetOrySessionToken) } @@ -191,6 +227,10 @@ func (obj *ContinueWith) GetActualInstance() interface{} { return obj.ContinueWithRecoveryUi } + if obj.ContinueWithRedirectBrowserTo != nil { + return obj.ContinueWithRedirectBrowserTo + } + if obj.ContinueWithSetOrySessionToken != nil { return obj.ContinueWithSetOrySessionToken } diff --git a/internal/httpclient/model_continue_with_redirect_browser_to.go b/internal/httpclient/model_continue_with_redirect_browser_to.go new file mode 100644 index 000000000000..46344016b779 --- /dev/null +++ b/internal/httpclient/model_continue_with_redirect_browser_to.go @@ -0,0 +1,147 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui +type ContinueWithRedirectBrowserTo struct { + // Action will always be `redirect_browser_to` + Action interface{} `json:"action"` + // The URL to redirect the browser to + RedirectBrowserTo *string `json:"redirect_browser_to,omitempty"` +} + +// NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewContinueWithRedirectBrowserTo(action interface{}) *ContinueWithRedirectBrowserTo { + this := ContinueWithRedirectBrowserTo{} + this.Action = action + return &this +} + +// NewContinueWithRedirectBrowserToWithDefaults instantiates a new ContinueWithRedirectBrowserTo object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowserTo { + this := ContinueWithRedirectBrowserTo{} + return &this +} + +// GetAction returns the Action field value +// If the value is explicit nil, the zero value for interface{} will be returned +func (o *ContinueWithRedirectBrowserTo) GetAction() interface{} { + if o == nil { + var ret interface{} + return ret + } + + return o.Action +} + +// GetActionOk returns a tuple with the Action field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*interface{}, bool) { + if o == nil || o.Action == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithRedirectBrowserTo) SetAction(v interface{}) { + o.Action = v +} + +// GetRedirectBrowserTo returns the RedirectBrowserTo field value if set, zero value otherwise. +func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string { + if o == nil || o.RedirectBrowserTo == nil { + var ret string + return ret + } + return *o.RedirectBrowserTo +} + +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) { + if o == nil || o.RedirectBrowserTo == nil { + return nil, false + } + return o.RedirectBrowserTo, true +} + +// HasRedirectBrowserTo returns a boolean if a field has been set. +func (o *ContinueWithRedirectBrowserTo) HasRedirectBrowserTo() bool { + if o != nil && o.RedirectBrowserTo != nil { + return true + } + + return false +} + +// SetRedirectBrowserTo gets a reference to the given string and assigns it to the RedirectBrowserTo field. +func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) { + o.RedirectBrowserTo = &v +} + +func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Action != nil { + toSerialize["action"] = o.Action + } + if o.RedirectBrowserTo != nil { + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + } + return json.Marshal(toSerialize) +} + +type NullableContinueWithRedirectBrowserTo struct { + value *ContinueWithRedirectBrowserTo + isSet bool +} + +func (v NullableContinueWithRedirectBrowserTo) Get() *ContinueWithRedirectBrowserTo { + return v.value +} + +func (v *NullableContinueWithRedirectBrowserTo) Set(val *ContinueWithRedirectBrowserTo) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithRedirectBrowserTo) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithRedirectBrowserTo) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithRedirectBrowserTo(val *ContinueWithRedirectBrowserTo) *NullableContinueWithRedirectBrowserTo { + return &NullableContinueWithRedirectBrowserTo{value: val, isSet: true} +} + +func (v NullableContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithRedirectBrowserTo) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/selfservice/flow/continue_with.go b/selfservice/flow/continue_with.go index 7a5f9ce22410..5b56bbf9aab6 100644 --- a/selfservice/flow/continue_with.go +++ b/selfservice/flow/continue_with.go @@ -201,6 +201,34 @@ func NewContinueWithRecoveryUI(f Flow) *ContinueWithRecoveryUI { } } +// swagger:enum ContinueWithActionRedirectTo +type ContinueWithActionRedirectBrowserTo string + +// #nosec G101 -- only a key constant +const ( + ContinueWithActionRedirectBrowserToString ContinueWithActionRedirectBrowserTo = "redirect_browser_to" +) + +// Indicates, that the UI flow could be continued by showing a recovery ui +// +// swagger:model continueWithRedirectBrowserTo +type ContinueWithRedirectBrowserTo struct { + // Action will always be `redirect_browser_to` + // + // required: true + Action ContinueWithActionRedirectBrowserTo `json:"action"` + + // The URL to redirect the browser to + RedirectTo string `json:"redirect_browser_to"` +} + +func NewContinueWithRedirectBrowserTo(redirectTo string) *ContinueWithRedirectBrowserTo { + return &ContinueWithRedirectBrowserTo{ + Action: ContinueWithActionRedirectBrowserToString, + RedirectTo: redirectTo, + } +} + func ErrorWithContinueWith(err *herodot.DefaultError, continueWith ...ContinueWith) *herodot.DefaultError { if err.DetailsField == nil { err.DetailsField = map[string]interface{}{} diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index f0e06ccfc934..4d3deddf2a0a 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -159,6 +159,10 @@ func (e *HookExecutor) PostLoginHook( "redirect_reason": "login successful", })...) + if f.Type != flow.TypeAPI { + f.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String())) + } + classified := s s = s.Declassified() diff --git a/selfservice/flow/registration/hook.go b/selfservice/flow/registration/hook.go index 6a997009c1c5..e44be2487bbb 100644 --- a/selfservice/flow/registration/hook.go +++ b/selfservice/flow/registration/hook.go @@ -192,12 +192,17 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque if err != nil { return err } + span.SetAttributes(otelx.StringAttrs(map[string]string{ "return_to": returnTo.String(), - "flow_type": string(flow.TypeBrowser), + "flow_type": string(registrationFlow.Type), "redirect_reason": "registration successful", })...) + if registrationFlow.Type == flow.TypeBrowser && x.IsJSONRequest(r) { + registrationFlow.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String())) + } + e.d.Audit(). WithRequest(r). WithField("identity_id", i.ID). diff --git a/selfservice/flow/settings/hook.go b/selfservice/flow/settings/hook.go index b688fd0fc431..88741e766736 100644 --- a/selfservice/flow/settings/hook.go +++ b/selfservice/flow/settings/hook.go @@ -308,6 +308,7 @@ func (e *HookExecutor) PostSettingsHook(w http.ResponseWriter, r *http.Request, } // ContinueWith items are transient items, not stored in the database, and need to be carried over here, so // they can be returned to the client. + ctxUpdate.Flow.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String())) updatedFlow.ContinueWithItems = ctxUpdate.Flow.ContinueWithItems e.d.Writer().Write(w, r, updatedFlow) diff --git a/selfservice/strategy/code/strategy_login_test.go b/selfservice/strategy/code/strategy_login_test.go index 19cac6d38375..55f090c19dd5 100644 --- a/selfservice/strategy/code/strategy_login_test.go +++ b/selfservice/strategy/code/strategy_login_test.go @@ -12,6 +12,8 @@ import ( "net/url" "testing" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/x/ioutilx" "github.com/ory/x/snapshotx" "github.com/ory/x/sqlcon" @@ -247,9 +249,15 @@ func TestLoginCodeStrategy(t *testing.T) { assert.NotEmpty(t, loginCode) // 3. Submit OTP - submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + state := submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { v.Set("code", loginCode) }, true, nil) + if tc.apiType == ApiTypeSPA { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(state.body, "continue_with.0.action").String(), "%s", state.body) + assert.Contains(t, gjson.Get(state.body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", state.body) + } else { + assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body) + } }) t.Run("case=new identities automatically have login with code", func(t *testing.T) { diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go index 27c645a94190..0b6caaa15da1 100644 --- a/selfservice/strategy/code/strategy_registration_test.go +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -15,6 +15,8 @@ import ( "strings" "testing" + "github.com/ory/kratos/selfservice/flow" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" @@ -37,6 +39,7 @@ type state struct { email string testServer *httptest.Server resultIdentity *identity.Identity + body string } func TestRegistrationCodeStrategyDisabled(t *testing.T) { @@ -172,6 +175,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { values.Set("method", "code") body, resp := testhelpers.RegistrationMakeRequest(t, apiType == ApiTypeNative, apiType == ApiTypeSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, apiType == ApiTypeNative, values)) + s.body = body if submitAssertion != nil { submitAssertion(ctx, t, s, body, resp) @@ -213,6 +217,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { vals(&values) body, resp := testhelpers.RegistrationMakeRequest(t, apiType == ApiTypeNative, apiType == ApiTypeSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, apiType == ApiTypeNative, values)) + s.body = body if submitAssertion != nil { submitAssertion(ctx, t, s, body, resp) @@ -240,7 +245,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { t.Parallel() ctx := context.Background() - _, reg, public := setup(ctx, t) + conf, reg, public := setup(ctx, t) for _, tc := range []struct { d string @@ -279,6 +284,15 @@ func TestRegistrationCodeStrategy(t *testing.T) { state = submitOTP(ctx, t, reg, state, func(v *url.Values) { v.Set("code", registrationCode) }, tc.apiType, nil) + + if tc.apiType == ApiTypeSPA { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(state.body, "continue_with.0.action").String(), "%s", state.body) + assert.Contains(t, gjson.Get(state.body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", state.body) + } else if tc.apiType == ApiTypeSPA { + assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body) + } else if tc.apiType == ApiTypeNative { + assert.NotContains(t, gjson.Get(state.body, "continue_with").Raw, string(flow.ContinueWithActionRedirectBrowserToString), "%s", state.body) + } }) t.Run("case=should normalize email address on sign up", func(t *testing.T) { diff --git a/selfservice/strategy/lookup/login_test.go b/selfservice/strategy/lookup/login_test.go index c4896962c660..b3bc454dac70 100644 --- a/selfservice/strategy/lookup/login_test.go +++ b/selfservice/strategy/lookup/login_test.go @@ -14,6 +14,8 @@ import ( "testing" "time" + "github.com/ory/kratos/selfservice/flow" + "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -241,6 +243,7 @@ func TestCompleteLogin(t *testing.T) { // We can still use another key body, res = doAPIFlowWithClient(t, payload("key-2"), id, apiClient, true) check(t, false, body, res, "key-2", 3) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=browser", func(t *testing.T) { @@ -250,6 +253,7 @@ func TestCompleteLogin(t *testing.T) { // We can still use another key body, res = doBrowserFlowWithClient(t, false, payload("key-5"), id, browserClient, true) check(t, true, body, res, "key-5", 3) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=spa", func(t *testing.T) { @@ -259,6 +263,9 @@ func TestCompleteLogin(t *testing.T) { // We can still use another key body, res = doBrowserFlowWithClient(t, true, payload("key-8"), id, browserClient, true) check(t, false, body, res, "key-8", 3) + + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body) }) }) diff --git a/selfservice/strategy/lookup/settings_test.go b/selfservice/strategy/lookup/settings_test.go index fce2be4c0974..48a7faf19705 100644 --- a/selfservice/strategy/lookup/settings_test.go +++ b/selfservice/strategy/lookup/settings_test.go @@ -423,8 +423,11 @@ func TestCompleteSettings(t *testing.T) { if spa { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + 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(), uiTS.URL, "%s", actual) } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) } assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) @@ -508,8 +511,11 @@ func TestCompleteSettings(t *testing.T) { if spa { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + 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(), uiTS.URL, "%s", actual) } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) } assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) diff --git a/selfservice/strategy/passkey/passkey_login_test.go b/selfservice/strategy/passkey/passkey_login_test.go index 2a6c2075557e..67ec6737f3ba 100644 --- a/selfservice/strategy/passkey/passkey_login_test.go +++ b/selfservice/strategy/passkey/passkey_login_test.go @@ -209,7 +209,14 @@ func TestCompleteLogin(t *testing.T) { actualFlow, err := fix.reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id)) require.NoError(t, err) + assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypePasskey, passkey.InternalContextKeySessionData))) + if spa { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), fix.conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body) + } else { + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + } } // We test here that login works even if the identity schema contains diff --git a/selfservice/strategy/passkey/passkey_registration_test.go b/selfservice/strategy/passkey/passkey_registration_test.go index d495e8c4dfe4..d7191207cedb 100644 --- a/selfservice/strategy/passkey/passkey_registration_test.go +++ b/selfservice/strategy/passkey/passkey_registration_test.go @@ -8,6 +8,8 @@ import ( "net/url" "testing" + "github.com/ory/kratos/selfservice/flow" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -327,6 +329,13 @@ func TestRegistration(t *testing.T) { i, _, err := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID) require.NoError(t, err) assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + + if f == "spa" { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), fix.redirNoSessionTS.URL+"/registration-return-ts", "%s", actual) + } else { + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) + } }) } }) diff --git a/selfservice/strategy/passkey/passkey_settings_test.go b/selfservice/strategy/passkey/passkey_settings_test.go index ced111071711..842f4d22c10e 100644 --- a/selfservice/strategy/passkey/passkey_settings_test.go +++ b/selfservice/strategy/passkey/passkey_settings_test.go @@ -271,6 +271,13 @@ func TestCompleteSettings(t *testing.T) { flow.PrefixInternalContextKey(identity.CredentialsTypePasskey, passkey.InternalContextKeySessionData))) testhelpers.EnsureAAL(t, browserClient, fix.publicTS, "aal1", string(identity.CredentialsTypePasskey)) + + if spa { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), fix.uiTS.URL, "%s", body) + } else { + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + } } t.Run("type=browser", func(t *testing.T) { diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go index 8c2f2cb73245..df2fe5e29cae 100644 --- a/selfservice/strategy/password/login_test.go +++ b/selfservice/strategy/password/login_test.go @@ -737,6 +737,32 @@ func TestCompleteLogin(t *testing.T) { assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) }) + t.Run("should succeed and include redirect continue_with in SPA flow", func(t *testing.T) { + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false) + values := url.Values{"method": {"password"}, "identifier": {strings.ToUpper(identifier)}, "password": {pwd}, "csrf_token": {x.FakeCSRFToken}}.Encode() + body, res := testhelpers.LoginMakeRequest(t, false, true, f, browserClient, values) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.EqualValues(t, conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body) + }) + + t.Run("should succeed and not have redirect continue_with in api flow", func(t *testing.T) { + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false) + + body, res := testhelpers.LoginMakeRequest(t, true, true, f, browserClient, fmt.Sprintf(`{"method":"password","identifier":"%s","password":"%s"}`, strings.ToUpper(identifier), pwd)) + + assert.EqualValues(t, http.StatusOK, res.StatusCode, body) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + }) + t.Run("should login even if old form field name is used", func(t *testing.T) { identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go index 14bf2382b212..d52ca2d77707 100644 --- a/selfservice/strategy/password/registration_test.go +++ b/selfservice/strategy/password/registration_test.go @@ -14,6 +14,8 @@ import ( "testing" "time" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/driver" "github.com/ory/kratos/internal/registrationhelpers" @@ -106,7 +108,7 @@ func TestRegistration(t *testing.T) { }) }) - var expectLoginBody = func(t *testing.T, browserRedirTS *httptest.Server, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + var expectRegistrationBody = func(t *testing.T, browserRedirTS *httptest.Server, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { if isAPI { return testhelpers.SubmitRegistrationForm(t, isAPI, hc, publicTS, values, isSPA, http.StatusOK, @@ -126,17 +128,17 @@ func TestRegistration(t *testing.T) { isSPA, http.StatusOK, expectReturnTo) } - var expectSuccessfulLogin = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + var expectSuccessfulRegistration = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { useReturnToFromTS(redirTS) - return expectLoginBody(t, redirTS, isAPI, isSPA, hc, values) + return expectRegistrationBody(t, redirTS, isAPI, isSPA, hc, values) } - var expectNoLogin = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + var expectNoRegistration = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { useReturnToFromTS(redirNoSessionTS) t.Cleanup(func() { useReturnToFromTS(redirTS) }) - return expectLoginBody(t, redirNoSessionTS, isAPI, isSPA, hc, values) + return expectRegistrationBody(t, redirNoSessionTS, isAPI, isSPA, hc, values) } t.Run("case=should reject invalid transient payload", func(t *testing.T) { @@ -178,7 +180,7 @@ func TestRegistration(t *testing.T) { t.Run("type=api", func(t *testing.T) { username := x.NewUUID().String() - body := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) { setValues(username, v) }) assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body) @@ -188,7 +190,7 @@ func TestRegistration(t *testing.T) { t.Run("type=spa", func(t *testing.T) { username := x.NewUUID().String() - body := expectSuccessfulLogin(t, false, true, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, false, true, nil, func(v url.Values) { setValues(username, v) }) assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body) @@ -198,7 +200,7 @@ func TestRegistration(t *testing.T) { t.Run("type=browser", func(t *testing.T) { username := x.NewUUID().String() - body := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { setValues(username, v) }) assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body) @@ -213,7 +215,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=api", func(t *testing.T) { - body := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-api") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -221,10 +223,11 @@ func TestRegistration(t *testing.T) { assert.Equal(t, `registration-identifier-8-api`, gjson.Get(body, "identity.traits.username").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session_token").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body) + assert.NotContains(t, gjson.Get(body, "continue_with").Raw, string(flow.ContinueWithActionRedirectBrowserToString), "%s", body) }) t.Run("type=spa", func(t *testing.T) { - body := expectSuccessfulLogin(t, false, true, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, false, true, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-spa") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -232,15 +235,17 @@ func TestRegistration(t *testing.T) { assert.Equal(t, `registration-identifier-8-spa`, gjson.Get(body, "identity.traits.username").String(), "%s", body) assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) }) t.Run("type=browser", func(t *testing.T) { - body := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-browser") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") }) assert.Equal(t, `registration-identifier-8-browser`, gjson.Get(body, "identity.traits.username").String(), "%s", body) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) }) @@ -249,7 +254,7 @@ func TestRegistration(t *testing.T) { conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) t.Run("type=api", func(t *testing.T) { - body := expectNoLogin(t, true, false, nil, func(v url.Values) { + body := expectNoRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-api-nosession") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -260,7 +265,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=spa", func(t *testing.T) { - expectNoLogin(t, false, true, nil, func(v url.Values) { + expectNoRegistration(t, false, true, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-spa-nosession") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -268,7 +273,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=browser", func(t *testing.T) { - expectNoLogin(t, false, false, nil, func(v url.Values) { + expectNoRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-browser-nosession") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -300,7 +305,7 @@ func TestRegistration(t *testing.T) { v.Set("traits.foobar", "bar") } - _ = expectSuccessfulLogin(t, true, false, apiClient, values) + _ = expectSuccessfulRegistration(t, true, false, apiClient, values) body := testhelpers.SubmitRegistrationForm(t, true, apiClient, publicTS, applyTransform(values, transform), false, http.StatusBadRequest, publicTS.URL+registration.RouteSubmitFlow) @@ -314,7 +319,7 @@ func TestRegistration(t *testing.T) { v.Set("traits.foobar", "bar") } - _ = expectSuccessfulLogin(t, false, true, nil, values) + _ = expectSuccessfulRegistration(t, false, true, nil, values) body := registrationhelpers.ExpectValidationError(t, publicTS, conf, "spa", applyTransform(values, transform)) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-spa-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body) }) @@ -326,7 +331,7 @@ func TestRegistration(t *testing.T) { v.Set("traits.foobar", "bar") } - _ = expectSuccessfulLogin(t, false, false, nil, values) + _ = expectSuccessfulRegistration(t, false, false, nil, values) body := registrationhelpers.ExpectValidationError(t, publicTS, conf, "browser", applyTransform(values, transform)) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-browser-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body) }) @@ -541,7 +546,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=api", func(t *testing.T) { - actual := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) { + actual := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-10-api") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -550,7 +555,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=spa", func(t *testing.T) { - actual := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) { + actual := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-10-spa") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -559,7 +564,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=browser", func(t *testing.T) { - actual := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) { + actual := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-10-browser") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -620,7 +625,7 @@ func TestRegistration(t *testing.T) { username := "registration-custom-schema" t.Run("type=api", func(t *testing.T) { - body := expectNoLogin(t, true, false, nil, func(v url.Values) { + body := expectNoRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", username+"-api") v.Set("password", x.NewUUID().String()) v.Set("traits.baz", "bar") @@ -631,7 +636,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=spa", func(t *testing.T) { - expectNoLogin(t, false, true, nil, func(v url.Values) { + expectNoRegistration(t, false, true, nil, func(v url.Values) { v.Set("traits.username", username+"-spa") v.Set("password", x.NewUUID().String()) v.Set("traits.baz", "bar") @@ -639,7 +644,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=browser", func(t *testing.T) { - expectNoLogin(t, false, false, nil, func(v url.Values) { + expectNoRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", username+"-browser") v.Set("password", x.NewUUID().String()) v.Set("traits.baz", "bar") diff --git a/selfservice/strategy/password/settings_test.go b/selfservice/strategy/password/settings_test.go index a4ee7e6c7fa0..49912ee95418 100644 --- a/selfservice/strategy/password/settings_test.go +++ b/selfservice/strategy/password/settings_test.go @@ -13,6 +13,8 @@ import ( "strings" "testing" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/internal/settingshelpers" "github.com/ory/kratos/text" @@ -82,7 +84,7 @@ func TestSettings(t *testing.T) { testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true) testhelpers.StrategyEnable(t, conf, settings.StrategyProfile, true) - _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) + settingsUI := testhelpers.NewSettingsUIFlowEchoServer(t, reg) _ = testhelpers.NewErrorTestServer(t, reg) _ = testhelpers.NewLoginUIWith401Response(t, conf) conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1m") @@ -242,15 +244,20 @@ func TestSettings(t *testing.T) { t.Run("type=api", func(t *testing.T) { actual := testhelpers.SubmitSettingsForm(t, true, false, apiUser1, publicTS, payload, http.StatusOK, publicTS.URL+settings.RouteSubmitFlow) check(t, actual) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) t.Run("type=spa", func(t *testing.T) { actual := testhelpers.SubmitSettingsForm(t, false, true, browserUser1, publicTS, payload, http.StatusOK, publicTS.URL+settings.RouteSubmitFlow) 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(), settingsUI.URL, "%s", actual) }) t.Run("type=browser", func(t *testing.T) { - check(t, testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, payload, http.StatusOK, conf.SelfServiceFlowSettingsUI(ctx).String())) + actual := testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, payload, http.StatusOK, conf.SelfServiceFlowSettingsUI(ctx).String()) + check(t, actual) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) }) diff --git a/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json b/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json new file mode 100644 index 000000000000..d6665e756663 --- /dev/null +++ b/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json @@ -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" + } + ] +} diff --git a/selfservice/strategy/profile/strategy_test.go b/selfservice/strategy/profile/strategy_test.go index 7d0c831711c3..92351d5b8d72 100644 --- a/selfservice/strategy/profile/strategy_test.go +++ b/selfservice/strategy/profile/strategy_test.go @@ -210,7 +210,7 @@ func TestStrategyTraits(t *testing.T) { run(t, apiIdentity1, pr, settings.RouteInitAPIFlow) }) - t.Run("type=api", func(t *testing.T) { + 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) @@ -449,15 +449,20 @@ func TestStrategyTraits(t *testing.T) { 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) { - check(t, expectSuccess(t, false, false, browserUser1, payload("not-john-doe-browser@mail.com"))) + 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) }) }) diff --git a/selfservice/strategy/totp/login_test.go b/selfservice/strategy/totp/login_test.go index 6456ea7cc599..2eb434256ed8 100644 --- a/selfservice/strategy/totp/login_test.go +++ b/selfservice/strategy/totp/login_test.go @@ -13,6 +13,8 @@ import ( "testing" "time" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/x/assertx" "github.com/gofrs/uuid" @@ -333,23 +335,36 @@ func TestCompleteLogin(t *testing.T) { t.Run("type=api", func(t *testing.T) { body, res := doAPIFlow(t, payload, id) check(t, false, body, res) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=browser", func(t *testing.T) { body, res := doBrowserFlow(t, false, payload, id, "") check(t, true, body, res) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=browser set return_to", func(t *testing.T) { returnTo := "https://www.ory.sh" - _, res := doBrowserFlow(t, false, payload, id, returnTo) + body, res := doBrowserFlow(t, false, payload, id, returnTo) t.Log(res.Request.URL.String()) assert.Contains(t, res.Request.URL.String(), returnTo) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=spa", func(t *testing.T) { body, res := doBrowserFlow(t, true, payload, id, "") check(t, false, body, res) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.EqualValues(t, conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body) + }) + + t.Run("type=spa set return_to", func(t *testing.T) { + returnTo := "https://www.ory.sh" + body, res := doBrowserFlow(t, true, payload, id, returnTo) + check(t, false, body, res) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.EqualValues(t, returnTo, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body) }) }) diff --git a/selfservice/strategy/totp/settings_test.go b/selfservice/strategy/totp/settings_test.go index 0fd479f1b220..43d41b2f66aa 100644 --- a/selfservice/strategy/totp/settings_test.go +++ b/selfservice/strategy/totp/settings_test.go @@ -241,6 +241,7 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) t.Run("type=spa", func(t *testing.T) { @@ -250,6 +251,9 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) + + 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(), uiTS.URL, "%s", actual) }) t.Run("type=browser", func(t *testing.T) { @@ -259,6 +263,7 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), uiTS.URL) assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) }) @@ -344,6 +349,13 @@ func TestCompleteSettings(t *testing.T) { checkIdentity(t, id, key) testhelpers.EnsureAAL(t, hc, publicTS, "aal2", string(identity.CredentialsTypeTOTP)) + + if isSPA { + 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(), uiTS.URL, "%s", actual) + } else { + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) + } } t.Run("type=api", func(t *testing.T) { diff --git a/selfservice/strategy/webauthn/login_test.go b/selfservice/strategy/webauthn/login_test.go index f5d332182163..46db972c4cc7 100644 --- a/selfservice/strategy/webauthn/login_test.go +++ b/selfservice/strategy/webauthn/login_test.go @@ -446,6 +446,13 @@ func TestCompleteLogin(t *testing.T) { actualFlow, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id)) require.NoError(t, err) assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeWebAuthn, webauthn.InternalContextKeySessionData))) + + if spa { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body) + } else { + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + } } t.Run("type=browser", func(t *testing.T) { diff --git a/selfservice/strategy/webauthn/registration_test.go b/selfservice/strategy/webauthn/registration_test.go index c0503b151ed8..973e1ae0ec81 100644 --- a/selfservice/strategy/webauthn/registration_test.go +++ b/selfservice/strategy/webauthn/registration_test.go @@ -367,6 +367,13 @@ func TestRegistration(t *testing.T) { i, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email) require.NoError(t, err) assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + + if f == "spa" { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), redirNoSessionTS.URL+"/registration-return-ts", "%s", actual) + } else { + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) + } }) } }) diff --git a/selfservice/strategy/webauthn/settings_test.go b/selfservice/strategy/webauthn/settings_test.go index acf4fd357b1d..3b46cc8de752 100644 --- a/selfservice/strategy/webauthn/settings_test.go +++ b/selfservice/strategy/webauthn/settings_test.go @@ -465,6 +465,13 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), uiTS.URL) } assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) + + if spa { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", body) + } else { + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + } } actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) @@ -474,6 +481,7 @@ func TestCompleteSettings(t *testing.T) { // Check not to remove other credentials with webauthn _, ok = actual.GetCredentials(identity.CredentialsTypePassword) assert.True(t, ok) + } t.Run("type=browser", func(t *testing.T) { diff --git a/spec/api.json b/spec/api.json index 1bb1345dc25c..3552ad49eb12 100644 --- a/spec/api.json +++ b/spec/api.json @@ -465,6 +465,7 @@ "continueWith": { "discriminator": { "mapping": { + "redirect_browser_to": "#/components/schemas/continueWithRedirectBrowserTo", "set_ory_session_token": "#/components/schemas/continueWithSetOrySessionToken", "show_recovery_ui": "#/components/schemas/continueWithRecoveryUi", "show_settings_ui": "#/components/schemas/continueWithSettingsUi", @@ -484,6 +485,9 @@ }, { "$ref": "#/components/schemas/continueWithRecoveryUi" + }, + { + "$ref": "#/components/schemas/continueWithRedirectBrowserTo" } ] }, @@ -525,6 +529,22 @@ ], "type": "object" }, + "continueWithRedirectBrowserTo": { + "description": "Indicates, that the UI flow could be continued by showing a recovery ui", + "properties": { + "action": { + "description": "Action will always be `redirect_browser_to`" + }, + "redirect_browser_to": { + "description": "The URL to redirect the browser to", + "type": "string" + } + }, + "required": [ + "action" + ], + "type": "object" + }, "continueWithSetOrySessionToken": { "description": "Indicates that a session was issued, and the application should use this token for authenticated requests", "properties": { diff --git a/spec/swagger.json b/spec/swagger.json index e790c71fecb4..50cb2858b4a1 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3659,6 +3659,22 @@ } } }, + "continueWithRedirectBrowserTo": { + "description": "Indicates, that the UI flow could be continued by showing a recovery ui", + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "description": "Action will always be `redirect_browser_to`" + }, + "redirect_browser_to": { + "description": "The URL to redirect the browser to", + "type": "string" + } + } + }, "continueWithSetOrySessionToken": { "description": "Indicates that a session was issued, and the application should use this token for authenticated requests", "type": "object", From 7b636d860c6917cb1133d6d1d7401808adb890c7 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:00:49 +0200 Subject: [PATCH 22/71] feat: add browser return_to continue_with action --- .../model_continue_with_recovery_ui_flow.go | 2 +- ...model_continue_with_redirect_browser_to.go | 51 ++++++++----------- .../model_continue_with_settings_ui_flow.go | 37 ++++++++++++++ ...odel_continue_with_verification_ui_flow.go | 2 +- .../model_continue_with_recovery_ui_flow.go | 2 +- ...model_continue_with_redirect_browser_to.go | 51 ++++++++----------- .../model_continue_with_settings_ui_flow.go | 37 ++++++++++++++ ...odel_continue_with_verification_ui_flow.go | 2 +- selfservice/flow/continue_with.go | 23 +++++++-- .../strategy/code/strategy_recovery.go | 5 +- spec/api.json | 18 +++++-- spec/swagger.json | 18 +++++-- 12 files changed, 171 insertions(+), 77 deletions(-) diff --git a/internal/client-go/model_continue_with_recovery_ui_flow.go b/internal/client-go/model_continue_with_recovery_ui_flow.go index 3fde7e717ef2..251725a73c3b 100644 --- a/internal/client-go/model_continue_with_recovery_ui_flow.go +++ b/internal/client-go/model_continue_with_recovery_ui_flow.go @@ -19,7 +19,7 @@ import ( type ContinueWithRecoveryUiFlow struct { // The ID of the recovery flow Id string `json:"id"` - // The URL of the recovery flow + // The URL of the recovery flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. Url *string `json:"url,omitempty"` } diff --git a/internal/client-go/model_continue_with_redirect_browser_to.go b/internal/client-go/model_continue_with_redirect_browser_to.go index 46344016b779..20c3e4f3c562 100644 --- a/internal/client-go/model_continue_with_redirect_browser_to.go +++ b/internal/client-go/model_continue_with_redirect_browser_to.go @@ -17,19 +17,20 @@ import ( // ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui type ContinueWithRedirectBrowserTo struct { - // Action will always be `redirect_browser_to` - Action interface{} `json:"action"` + // Action will always be `redirect_browser_to` redirect_browser_to ContinueWithActionRedirectBrowserToString + Action string `json:"action"` // The URL to redirect the browser to - RedirectBrowserTo *string `json:"redirect_browser_to,omitempty"` + RedirectBrowserTo string `json:"redirect_browser_to"` } // NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo object // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewContinueWithRedirectBrowserTo(action interface{}) *ContinueWithRedirectBrowserTo { +func NewContinueWithRedirectBrowserTo(action string, redirectBrowserTo string) *ContinueWithRedirectBrowserTo { this := ContinueWithRedirectBrowserTo{} this.Action = action + this.RedirectBrowserTo = redirectBrowserTo return &this } @@ -42,10 +43,9 @@ func NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowser } // GetAction returns the Action field value -// If the value is explicit nil, the zero value for interface{} will be returned -func (o *ContinueWithRedirectBrowserTo) GetAction() interface{} { +func (o *ContinueWithRedirectBrowserTo) GetAction() string { if o == nil { - var ret interface{} + var ret string return ret } @@ -54,57 +54,48 @@ func (o *ContinueWithRedirectBrowserTo) GetAction() interface{} { // GetActionOk returns a tuple with the Action field value // and a boolean to check if the value has been set. -// NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*interface{}, bool) { - if o == nil || o.Action == nil { +func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*string, bool) { + if o == nil { return nil, false } return &o.Action, true } // SetAction sets field value -func (o *ContinueWithRedirectBrowserTo) SetAction(v interface{}) { +func (o *ContinueWithRedirectBrowserTo) SetAction(v string) { o.Action = v } -// GetRedirectBrowserTo returns the RedirectBrowserTo field value if set, zero value otherwise. +// GetRedirectBrowserTo returns the RedirectBrowserTo field value func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string { - if o == nil || o.RedirectBrowserTo == nil { + if o == nil { var ret string return ret } - return *o.RedirectBrowserTo + + return o.RedirectBrowserTo } -// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value if set, nil otherwise +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value // and a boolean to check if the value has been set. func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) { - if o == nil || o.RedirectBrowserTo == nil { + if o == nil { return nil, false } - return o.RedirectBrowserTo, true -} - -// HasRedirectBrowserTo returns a boolean if a field has been set. -func (o *ContinueWithRedirectBrowserTo) HasRedirectBrowserTo() bool { - if o != nil && o.RedirectBrowserTo != nil { - return true - } - - return false + return &o.RedirectBrowserTo, true } -// SetRedirectBrowserTo gets a reference to the given string and assigns it to the RedirectBrowserTo field. +// SetRedirectBrowserTo sets field value func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) { - o.RedirectBrowserTo = &v + o.RedirectBrowserTo = v } func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} - if o.Action != nil { + if true { toSerialize["action"] = o.Action } - if o.RedirectBrowserTo != nil { + if true { toSerialize["redirect_browser_to"] = o.RedirectBrowserTo } return json.Marshal(toSerialize) diff --git a/internal/client-go/model_continue_with_settings_ui_flow.go b/internal/client-go/model_continue_with_settings_ui_flow.go index 4ccaf74ef1b8..d6e9b9441f99 100644 --- a/internal/client-go/model_continue_with_settings_ui_flow.go +++ b/internal/client-go/model_continue_with_settings_ui_flow.go @@ -19,6 +19,8 @@ import ( type ContinueWithSettingsUiFlow struct { // The ID of the settings flow Id string `json:"id"` + // The URL of the settings flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + Url *string `json:"url,omitempty"` } // NewContinueWithSettingsUiFlow instantiates a new ContinueWithSettingsUiFlow object @@ -63,11 +65,46 @@ func (o *ContinueWithSettingsUiFlow) SetId(v string) { o.Id = v } +// GetUrl returns the Url field value if set, zero value otherwise. +func (o *ContinueWithSettingsUiFlow) GetUrl() string { + if o == nil || o.Url == nil { + var ret string + return ret + } + return *o.Url +} + +// GetUrlOk returns a tuple with the Url field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ContinueWithSettingsUiFlow) GetUrlOk() (*string, bool) { + if o == nil || o.Url == nil { + return nil, false + } + return o.Url, true +} + +// HasUrl returns a boolean if a field has been set. +func (o *ContinueWithSettingsUiFlow) HasUrl() bool { + if o != nil && o.Url != nil { + return true + } + + return false +} + +// SetUrl gets a reference to the given string and assigns it to the Url field. +func (o *ContinueWithSettingsUiFlow) SetUrl(v string) { + o.Url = &v +} + func (o ContinueWithSettingsUiFlow) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if true { toSerialize["id"] = o.Id } + if o.Url != nil { + toSerialize["url"] = o.Url + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_continue_with_verification_ui_flow.go b/internal/client-go/model_continue_with_verification_ui_flow.go index 8fdd4609cf93..3c73a0761339 100644 --- a/internal/client-go/model_continue_with_verification_ui_flow.go +++ b/internal/client-go/model_continue_with_verification_ui_flow.go @@ -19,7 +19,7 @@ import ( type ContinueWithVerificationUiFlow struct { // The ID of the verification flow Id string `json:"id"` - // The URL of the verification flow + // The URL of the verification flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. Url *string `json:"url,omitempty"` // The address that should be verified in this flow VerifiableAddress string `json:"verifiable_address"` diff --git a/internal/httpclient/model_continue_with_recovery_ui_flow.go b/internal/httpclient/model_continue_with_recovery_ui_flow.go index 3fde7e717ef2..251725a73c3b 100644 --- a/internal/httpclient/model_continue_with_recovery_ui_flow.go +++ b/internal/httpclient/model_continue_with_recovery_ui_flow.go @@ -19,7 +19,7 @@ import ( type ContinueWithRecoveryUiFlow struct { // The ID of the recovery flow Id string `json:"id"` - // The URL of the recovery flow + // The URL of the recovery flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. Url *string `json:"url,omitempty"` } diff --git a/internal/httpclient/model_continue_with_redirect_browser_to.go b/internal/httpclient/model_continue_with_redirect_browser_to.go index 46344016b779..20c3e4f3c562 100644 --- a/internal/httpclient/model_continue_with_redirect_browser_to.go +++ b/internal/httpclient/model_continue_with_redirect_browser_to.go @@ -17,19 +17,20 @@ import ( // ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui type ContinueWithRedirectBrowserTo struct { - // Action will always be `redirect_browser_to` - Action interface{} `json:"action"` + // Action will always be `redirect_browser_to` redirect_browser_to ContinueWithActionRedirectBrowserToString + Action string `json:"action"` // The URL to redirect the browser to - RedirectBrowserTo *string `json:"redirect_browser_to,omitempty"` + RedirectBrowserTo string `json:"redirect_browser_to"` } // NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo object // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewContinueWithRedirectBrowserTo(action interface{}) *ContinueWithRedirectBrowserTo { +func NewContinueWithRedirectBrowserTo(action string, redirectBrowserTo string) *ContinueWithRedirectBrowserTo { this := ContinueWithRedirectBrowserTo{} this.Action = action + this.RedirectBrowserTo = redirectBrowserTo return &this } @@ -42,10 +43,9 @@ func NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowser } // GetAction returns the Action field value -// If the value is explicit nil, the zero value for interface{} will be returned -func (o *ContinueWithRedirectBrowserTo) GetAction() interface{} { +func (o *ContinueWithRedirectBrowserTo) GetAction() string { if o == nil { - var ret interface{} + var ret string return ret } @@ -54,57 +54,48 @@ func (o *ContinueWithRedirectBrowserTo) GetAction() interface{} { // GetActionOk returns a tuple with the Action field value // and a boolean to check if the value has been set. -// NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*interface{}, bool) { - if o == nil || o.Action == nil { +func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*string, bool) { + if o == nil { return nil, false } return &o.Action, true } // SetAction sets field value -func (o *ContinueWithRedirectBrowserTo) SetAction(v interface{}) { +func (o *ContinueWithRedirectBrowserTo) SetAction(v string) { o.Action = v } -// GetRedirectBrowserTo returns the RedirectBrowserTo field value if set, zero value otherwise. +// GetRedirectBrowserTo returns the RedirectBrowserTo field value func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string { - if o == nil || o.RedirectBrowserTo == nil { + if o == nil { var ret string return ret } - return *o.RedirectBrowserTo + + return o.RedirectBrowserTo } -// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value if set, nil otherwise +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value // and a boolean to check if the value has been set. func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) { - if o == nil || o.RedirectBrowserTo == nil { + if o == nil { return nil, false } - return o.RedirectBrowserTo, true -} - -// HasRedirectBrowserTo returns a boolean if a field has been set. -func (o *ContinueWithRedirectBrowserTo) HasRedirectBrowserTo() bool { - if o != nil && o.RedirectBrowserTo != nil { - return true - } - - return false + return &o.RedirectBrowserTo, true } -// SetRedirectBrowserTo gets a reference to the given string and assigns it to the RedirectBrowserTo field. +// SetRedirectBrowserTo sets field value func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) { - o.RedirectBrowserTo = &v + o.RedirectBrowserTo = v } func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} - if o.Action != nil { + if true { toSerialize["action"] = o.Action } - if o.RedirectBrowserTo != nil { + if true { toSerialize["redirect_browser_to"] = o.RedirectBrowserTo } return json.Marshal(toSerialize) diff --git a/internal/httpclient/model_continue_with_settings_ui_flow.go b/internal/httpclient/model_continue_with_settings_ui_flow.go index 4ccaf74ef1b8..d6e9b9441f99 100644 --- a/internal/httpclient/model_continue_with_settings_ui_flow.go +++ b/internal/httpclient/model_continue_with_settings_ui_flow.go @@ -19,6 +19,8 @@ import ( type ContinueWithSettingsUiFlow struct { // The ID of the settings flow Id string `json:"id"` + // The URL of the settings flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + Url *string `json:"url,omitempty"` } // NewContinueWithSettingsUiFlow instantiates a new ContinueWithSettingsUiFlow object @@ -63,11 +65,46 @@ func (o *ContinueWithSettingsUiFlow) SetId(v string) { o.Id = v } +// GetUrl returns the Url field value if set, zero value otherwise. +func (o *ContinueWithSettingsUiFlow) GetUrl() string { + if o == nil || o.Url == nil { + var ret string + return ret + } + return *o.Url +} + +// GetUrlOk returns a tuple with the Url field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ContinueWithSettingsUiFlow) GetUrlOk() (*string, bool) { + if o == nil || o.Url == nil { + return nil, false + } + return o.Url, true +} + +// HasUrl returns a boolean if a field has been set. +func (o *ContinueWithSettingsUiFlow) HasUrl() bool { + if o != nil && o.Url != nil { + return true + } + + return false +} + +// SetUrl gets a reference to the given string and assigns it to the Url field. +func (o *ContinueWithSettingsUiFlow) SetUrl(v string) { + o.Url = &v +} + func (o ContinueWithSettingsUiFlow) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if true { toSerialize["id"] = o.Id } + if o.Url != nil { + toSerialize["url"] = o.Url + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_continue_with_verification_ui_flow.go b/internal/httpclient/model_continue_with_verification_ui_flow.go index 8fdd4609cf93..3c73a0761339 100644 --- a/internal/httpclient/model_continue_with_verification_ui_flow.go +++ b/internal/httpclient/model_continue_with_verification_ui_flow.go @@ -19,7 +19,7 @@ import ( type ContinueWithVerificationUiFlow struct { // The ID of the verification flow Id string `json:"id"` - // The URL of the verification flow + // The URL of the verification flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. Url *string `json:"url,omitempty"` // The address that should be verified in this flow VerifiableAddress string `json:"verifiable_address"` diff --git a/selfservice/flow/continue_with.go b/selfservice/flow/continue_with.go index 5b56bbf9aab6..9bc9e6152d78 100644 --- a/selfservice/flow/continue_with.go +++ b/selfservice/flow/continue_with.go @@ -89,6 +89,8 @@ type ContinueWithVerificationUIFlow struct { // The URL of the verification flow // + // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + // // required: false URL string `json:"url,omitempty"` } @@ -134,8 +136,11 @@ type ContinueWithSettingsUI struct { // // required: true Action ContinueWithActionShowSettingsUI `json:"action"` + // Flow contains the ID of the verification flow // + // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + // // required: true Flow ContinueWithSettingsUIFlow `json:"flow"` } @@ -146,13 +151,21 @@ type ContinueWithSettingsUIFlow struct { // // required: true ID uuid.UUID `json:"id"` + + // The URL of the settings flow + // + // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + // + // required: false + URL string `json:"url,omitempty"` } -func NewContinueWithSettingsUI(f Flow) *ContinueWithSettingsUI { +func NewContinueWithSettingsUI(f Flow, redirectTo string) *ContinueWithSettingsUI { return &ContinueWithSettingsUI{ Action: ContinueWithActionShowSettingsUIString, Flow: ContinueWithSettingsUIFlow{ - ID: f.GetID(), + ID: f.GetID(), + URL: redirectTo, }, } } @@ -188,6 +201,8 @@ type ContinueWithRecoveryUIFlow struct { // The URL of the recovery flow // + // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + // // required: false URL string `json:"url,omitempty"` } @@ -201,7 +216,7 @@ func NewContinueWithRecoveryUI(f Flow) *ContinueWithRecoveryUI { } } -// swagger:enum ContinueWithActionRedirectTo +// swagger:enum ContinueWithActionRedirectBrowserTo type ContinueWithActionRedirectBrowserTo string // #nosec G101 -- only a key constant @@ -219,6 +234,8 @@ type ContinueWithRedirectBrowserTo struct { Action ContinueWithActionRedirectBrowserTo `json:"action"` // The URL to redirect the browser to + // + // required: true RedirectTo string `json:"redirect_browser_to"` } diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index 758e81d04fd9..9376a8e1ef4d 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -235,12 +235,13 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request, } if s.deps.Config().UseContinueWithTransitions(ctx) { + redirectTo := sf.AppendTo(s.deps.Config().SelfServiceFlowSettingsUI(r.Context())).String() switch { case f.Type.IsAPI(), x.IsJSONRequest(r): - f.ContinueWith = append(f.ContinueWith, flow.NewContinueWithSettingsUI(sf)) + f.ContinueWith = append(f.ContinueWith, flow.NewContinueWithSettingsUI(sf, redirectTo)) s.deps.Writer().Write(w, r, f) default: - http.Redirect(w, r, sf.AppendTo(s.deps.Config().SelfServiceFlowSettingsUI(r.Context())).String(), http.StatusSeeOther) + http.Redirect(w, r, redirectTo, http.StatusSeeOther) } } else { if x.IsJSONRequest(r) { diff --git a/spec/api.json b/spec/api.json index 3552ad49eb12..a78fe5793d00 100644 --- a/spec/api.json +++ b/spec/api.json @@ -520,7 +520,7 @@ "type": "string" }, "url": { - "description": "The URL of the recovery flow", + "description": "The URL of the recovery flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", "type": "string" } }, @@ -533,7 +533,12 @@ "description": "Indicates, that the UI flow could be continued by showing a recovery ui", "properties": { "action": { - "description": "Action will always be `redirect_browser_to`" + "description": "Action will always be `redirect_browser_to`\nredirect_browser_to ContinueWithActionRedirectBrowserToString", + "enum": [ + "redirect_browser_to" + ], + "type": "string", + "x-go-enum-desc": "redirect_browser_to ContinueWithActionRedirectBrowserToString" }, "redirect_browser_to": { "description": "The URL to redirect the browser to", @@ -541,7 +546,8 @@ } }, "required": [ - "action" + "action", + "redirect_browser_to" ], "type": "object" }, @@ -594,6 +600,10 @@ "description": "The ID of the settings flow", "format": "uuid", "type": "string" + }, + "url": { + "description": "The URL of the settings flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", + "type": "string" } }, "required": [ @@ -630,7 +640,7 @@ "type": "string" }, "url": { - "description": "The URL of the verification flow", + "description": "The URL of the verification flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", "type": "string" }, "verifiable_address": { diff --git a/spec/swagger.json b/spec/swagger.json index 50cb2858b4a1..fe16afa03c25 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3654,7 +3654,7 @@ "format": "uuid" }, "url": { - "description": "The URL of the recovery flow", + "description": "The URL of the recovery flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", "type": "string" } } @@ -3663,11 +3663,17 @@ "description": "Indicates, that the UI flow could be continued by showing a recovery ui", "type": "object", "required": [ - "action" + "action", + "redirect_browser_to" ], "properties": { "action": { - "description": "Action will always be `redirect_browser_to`" + "description": "Action will always be `redirect_browser_to`\nredirect_browser_to ContinueWithActionRedirectBrowserToString", + "type": "string", + "enum": [ + "redirect_browser_to" + ], + "x-go-enum-desc": "redirect_browser_to ContinueWithActionRedirectBrowserToString" }, "redirect_browser_to": { "description": "The URL to redirect the browser to", @@ -3728,6 +3734,10 @@ "description": "The ID of the settings flow", "type": "string", "format": "uuid" + }, + "url": { + "description": "The URL of the settings flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", + "type": "string" } } }, @@ -3765,7 +3775,7 @@ "format": "uuid" }, "url": { - "description": "The URL of the verification flow", + "description": "The URL of the verification flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", "type": "string" }, "verifiable_address": { From 0150795d902dcc7cfb2298c3b5a98da1c2541e46 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:11:37 +0200 Subject: [PATCH 23/71] feat(sdk): add missing profile discriminator to update registration --- .schema/openapi/patches/selfservice.yaml | 4 +- .../model_update_registration_flow_body.go | 44 ++++++++++++++++++- .../model_update_registration_flow_body.go | 44 ++++++++++++++++++- spec/api.json | 6 ++- 4 files changed, 92 insertions(+), 6 deletions(-) diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml index 81d82247586b..db9d7d3e6720 100644 --- a/.schema/openapi/patches/selfservice.yaml +++ b/.schema/openapi/patches/selfservice.yaml @@ -19,6 +19,7 @@ - "$ref": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" - "$ref": "#/components/schemas/updateRegistrationFlowWithCodeMethod" - "$ref": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" + - "$ref": "#/components/schemas/updateRegistrationFlowWithProfileMethod" - op: add path: /components/schemas/updateRegistrationFlowBody/discriminator value: @@ -28,7 +29,8 @@ oidc: "#/components/schemas/updateRegistrationFlowWithOidcMethod" webauthn: "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" code: "#/components/schemas/updateRegistrationFlowWithCodeMethod" - passKey: "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" + passkey: "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" + profile: "#/components/schemas/updateRegistrationFlowWithProfileMethod" - op: add path: /components/schemas/registrationFlowState/enum value: diff --git a/internal/client-go/model_update_registration_flow_body.go b/internal/client-go/model_update_registration_flow_body.go index 64374c620f8f..82a578cfc4d3 100644 --- a/internal/client-go/model_update_registration_flow_body.go +++ b/internal/client-go/model_update_registration_flow_body.go @@ -22,6 +22,7 @@ type UpdateRegistrationFlowBody struct { UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod + UpdateRegistrationFlowWithProfileMethod *UpdateRegistrationFlowWithProfileMethod UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod } @@ -53,6 +54,13 @@ func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *Upd } } +// UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithProfileMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithProfileMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithProfileMethod: v, + } +} + // UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithWebAuthnMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithWebAuthnMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -94,8 +102,8 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } - // check if the discriminator value is 'passKey' - if jsonDict["method"] == "passKey" { + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { // try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod) if err == nil { @@ -118,6 +126,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'profile' + if jsonDict["method"] == "profile" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'webauthn' if jsonDict["method"] == "webauthn" { // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod @@ -178,6 +198,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'updateRegistrationFlowWithProfileMethod' + if jsonDict["method"] == "updateRegistrationFlowWithProfileMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'updateRegistrationFlowWithWebAuthnMethod' if jsonDict["method"] == "updateRegistrationFlowWithWebAuthnMethod" { // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod @@ -211,6 +243,10 @@ func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateRegistrationFlowWithPasswordMethod) } + if src.UpdateRegistrationFlowWithProfileMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithProfileMethod) + } + if src.UpdateRegistrationFlowWithWebAuthnMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithWebAuthnMethod) } @@ -239,6 +275,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { return obj.UpdateRegistrationFlowWithPasswordMethod } + if obj.UpdateRegistrationFlowWithProfileMethod != nil { + return obj.UpdateRegistrationFlowWithProfileMethod + } + if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil { return obj.UpdateRegistrationFlowWithWebAuthnMethod } diff --git a/internal/httpclient/model_update_registration_flow_body.go b/internal/httpclient/model_update_registration_flow_body.go index 64374c620f8f..82a578cfc4d3 100644 --- a/internal/httpclient/model_update_registration_flow_body.go +++ b/internal/httpclient/model_update_registration_flow_body.go @@ -22,6 +22,7 @@ type UpdateRegistrationFlowBody struct { UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod + UpdateRegistrationFlowWithProfileMethod *UpdateRegistrationFlowWithProfileMethod UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod } @@ -53,6 +54,13 @@ func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *Upd } } +// UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithProfileMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithProfileMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithProfileMethod: v, + } +} + // UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithWebAuthnMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithWebAuthnMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -94,8 +102,8 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } - // check if the discriminator value is 'passKey' - if jsonDict["method"] == "passKey" { + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { // try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod) if err == nil { @@ -118,6 +126,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'profile' + if jsonDict["method"] == "profile" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'webauthn' if jsonDict["method"] == "webauthn" { // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod @@ -178,6 +198,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { } } + // check if the discriminator value is 'updateRegistrationFlowWithProfileMethod' + if jsonDict["method"] == "updateRegistrationFlowWithProfileMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) + } + } + // check if the discriminator value is 'updateRegistrationFlowWithWebAuthnMethod' if jsonDict["method"] == "updateRegistrationFlowWithWebAuthnMethod" { // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod @@ -211,6 +243,10 @@ func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateRegistrationFlowWithPasswordMethod) } + if src.UpdateRegistrationFlowWithProfileMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithProfileMethod) + } + if src.UpdateRegistrationFlowWithWebAuthnMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithWebAuthnMethod) } @@ -239,6 +275,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { return obj.UpdateRegistrationFlowWithPasswordMethod } + if obj.UpdateRegistrationFlowWithProfileMethod != nil { + return obj.UpdateRegistrationFlowWithProfileMethod + } + if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil { return obj.UpdateRegistrationFlowWithWebAuthnMethod } diff --git a/spec/api.json b/spec/api.json index a78fe5793d00..afbd72885470 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2963,8 +2963,9 @@ "mapping": { "code": "#/components/schemas/updateRegistrationFlowWithCodeMethod", "oidc": "#/components/schemas/updateRegistrationFlowWithOidcMethod", - "passKey": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod", + "passkey": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod", "password": "#/components/schemas/updateRegistrationFlowWithPasswordMethod", + "profile": "#/components/schemas/updateRegistrationFlowWithProfileMethod", "webauthn": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" }, "propertyName": "method" @@ -2984,6 +2985,9 @@ }, { "$ref": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" + }, + { + "$ref": "#/components/schemas/updateRegistrationFlowWithProfileMethod" } ] }, From dd6e53d62f343a317edf403218b20599539218c6 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:37:28 +0200 Subject: [PATCH 24/71] feat(sdk): avoid eval with javascript triggers Using `OnLoadTrigger` and `OnClickTrigger` one can now map the trigger to the corresponding JavaScript function. For example, trigger `{"on_click_trigger":"oryWebAuthnRegistration"}` should be translated to `window.oryWebAuthnRegistration()`: ``` if (attrs.onClickTrigger) { window[attrs.onClickTrigger]() } ``` --- .../model_ui_node_input_attributes.go | 78 ++++++++++- .../model_ui_node_input_attributes.go | 78 ++++++++++- ...sswordless-case=passkey_button_exists.json | 4 +- ...resh_passwordless_credentials-browser.json | 3 +- ...=refresh_passwordless_credentials-spa.json | 3 +- ...device_is_shown_which_can_be_unlinked.json | 3 +- ...-case=one_activation_element_is_shown.json | 3 +- ...on-case=passkey_button_exists-browser.json | 3 +- ...ration-case=passkey_button_exists-spa.json | 3 +- selfservice/strategy/passkey/passkey_login.go | 125 +++++++++++++++++- .../strategy/passkey/passkey_registration.go | 9 +- .../strategy/passkey/passkey_settings.go | 5 +- ...oad_is_set_when_identity_has_webauthn.json | 40 +++--- ...ebauthn_login_is_invalid-type=browser.json | 3 +- ...if_webauthn_login_is_invalid-type=spa.json | 3 +- ...passwordless_enabled=false#01-browser.json | 40 +++--- ...als-passwordless_enabled=false#01-spa.json | 40 +++--- ...passwordless_enabled=false#02-browser.json | 40 +++--- ...als-passwordless_enabled=false#02-spa.json | 40 +++--- ...ls-passwordless_enabled=false-browser.json | 40 +++--- ...ntials-passwordless_enabled=false-spa.json | 40 +++--- ...-passwordless_enabled=true#01-browser.json | 40 +++--- ...ials-passwordless_enabled=true#01-spa.json | 40 +++--- ...-passwordless_enabled=true#02-browser.json | 40 +++--- ...ials-passwordless_enabled=true#02-spa.json | 40 +++--- ...als-passwordless_enabled=true-browser.json | 40 +++--- ...entials-passwordless_enabled=true-spa.json | 40 +++--- ...device_is_shown_which_can_be_unlinked.json | 28 ++-- ...ast_credential_available-type=browser.json | 3 - ...he_last_credential_available-type=spa.json | 3 - ...-case=one_activation_element_is_shown.json | 28 ++-- ...f_it_is_MFA_at_all_times-type=browser.json | 3 - ...al_if_it_is_MFA_at_all_times-type=spa.json | 3 - ...n-case=webauthn_button_exists-browser.json | 6 +- ...ation-case=webauthn_button_exists-spa.json | 6 +- selfservice/strategy/webauthn/login_test.go | 18 +-- .../strategy/webauthn/registration_test.go | 1 + .../strategy/webauthn/settings_test.go | 17 +-- spec/api.json | 30 ++++- spec/swagger.json | 30 ++++- ui/node/attributes.go | 15 +++ x/webauthnx/js/trigger.go | 22 +++ x/webauthnx/js/trigger_test.go | 14 ++ x/webauthnx/js/webauthn.js | 39 +++++- x/webauthnx/nodes.go | 10 +- 45 files changed, 764 insertions(+), 355 deletions(-) create mode 100644 x/webauthnx/js/trigger.go create mode 100644 x/webauthnx/js/trigger_test.go diff --git a/internal/client-go/model_ui_node_input_attributes.go b/internal/client-go/model_ui_node_input_attributes.go index b373dda7ccfd..7056a308d651 100644 --- a/internal/client-go/model_ui_node_input_attributes.go +++ b/internal/client-go/model_ui_node_input_attributes.go @@ -26,10 +26,14 @@ type UiNodeInputAttributes struct { Name string `json:"name"` // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` - // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. + // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead. Onclick *string `json:"onclick,omitempty"` - // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. + // OnClickTrigger may contain a WebAuthn trigger which should be executed on click. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration + OnclickTrigger *string `json:"onclickTrigger,omitempty"` + // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead. Onload *string `json:"onload,omitempty"` + // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration + OnloadTrigger *string `json:"onloadTrigger,omitempty"` // The input's pattern. Pattern *string `json:"pattern,omitempty"` // Mark this input field as required. @@ -229,6 +233,38 @@ func (o *UiNodeInputAttributes) SetOnclick(v string) { o.Onclick = &v } +// GetOnclickTrigger returns the OnclickTrigger field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnclickTrigger() string { + if o == nil || o.OnclickTrigger == nil { + var ret string + return ret + } + return *o.OnclickTrigger +} + +// GetOnclickTriggerOk returns a tuple with the OnclickTrigger field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnclickTriggerOk() (*string, bool) { + if o == nil || o.OnclickTrigger == nil { + return nil, false + } + return o.OnclickTrigger, true +} + +// HasOnclickTrigger returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnclickTrigger() bool { + if o != nil && o.OnclickTrigger != nil { + return true + } + + return false +} + +// SetOnclickTrigger gets a reference to the given string and assigns it to the OnclickTrigger field. +func (o *UiNodeInputAttributes) SetOnclickTrigger(v string) { + o.OnclickTrigger = &v +} + // GetOnload returns the Onload field value if set, zero value otherwise. func (o *UiNodeInputAttributes) GetOnload() string { if o == nil || o.Onload == nil { @@ -261,6 +297,38 @@ func (o *UiNodeInputAttributes) SetOnload(v string) { o.Onload = &v } +// GetOnloadTrigger returns the OnloadTrigger field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnloadTrigger() string { + if o == nil || o.OnloadTrigger == nil { + var ret string + return ret + } + return *o.OnloadTrigger +} + +// GetOnloadTriggerOk returns a tuple with the OnloadTrigger field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnloadTriggerOk() (*string, bool) { + if o == nil || o.OnloadTrigger == nil { + return nil, false + } + return o.OnloadTrigger, true +} + +// HasOnloadTrigger returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnloadTrigger() bool { + if o != nil && o.OnloadTrigger != nil { + return true + } + + return false +} + +// SetOnloadTrigger gets a reference to the given string and assigns it to the OnloadTrigger field. +func (o *UiNodeInputAttributes) SetOnloadTrigger(v string) { + o.OnloadTrigger = &v +} + // GetPattern returns the Pattern field value if set, zero value otherwise. func (o *UiNodeInputAttributes) GetPattern() string { if o == nil || o.Pattern == nil { @@ -402,9 +470,15 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { if o.Onclick != nil { toSerialize["onclick"] = o.Onclick } + if o.OnclickTrigger != nil { + toSerialize["onclickTrigger"] = o.OnclickTrigger + } if o.Onload != nil { toSerialize["onload"] = o.Onload } + if o.OnloadTrigger != nil { + toSerialize["onloadTrigger"] = o.OnloadTrigger + } if o.Pattern != nil { toSerialize["pattern"] = o.Pattern } diff --git a/internal/httpclient/model_ui_node_input_attributes.go b/internal/httpclient/model_ui_node_input_attributes.go index b373dda7ccfd..7056a308d651 100644 --- a/internal/httpclient/model_ui_node_input_attributes.go +++ b/internal/httpclient/model_ui_node_input_attributes.go @@ -26,10 +26,14 @@ type UiNodeInputAttributes struct { Name string `json:"name"` // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` - // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. + // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead. Onclick *string `json:"onclick,omitempty"` - // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. + // OnClickTrigger may contain a WebAuthn trigger which should be executed on click. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration + OnclickTrigger *string `json:"onclickTrigger,omitempty"` + // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead. Onload *string `json:"onload,omitempty"` + // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration + OnloadTrigger *string `json:"onloadTrigger,omitempty"` // The input's pattern. Pattern *string `json:"pattern,omitempty"` // Mark this input field as required. @@ -229,6 +233,38 @@ func (o *UiNodeInputAttributes) SetOnclick(v string) { o.Onclick = &v } +// GetOnclickTrigger returns the OnclickTrigger field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnclickTrigger() string { + if o == nil || o.OnclickTrigger == nil { + var ret string + return ret + } + return *o.OnclickTrigger +} + +// GetOnclickTriggerOk returns a tuple with the OnclickTrigger field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnclickTriggerOk() (*string, bool) { + if o == nil || o.OnclickTrigger == nil { + return nil, false + } + return o.OnclickTrigger, true +} + +// HasOnclickTrigger returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnclickTrigger() bool { + if o != nil && o.OnclickTrigger != nil { + return true + } + + return false +} + +// SetOnclickTrigger gets a reference to the given string and assigns it to the OnclickTrigger field. +func (o *UiNodeInputAttributes) SetOnclickTrigger(v string) { + o.OnclickTrigger = &v +} + // GetOnload returns the Onload field value if set, zero value otherwise. func (o *UiNodeInputAttributes) GetOnload() string { if o == nil || o.Onload == nil { @@ -261,6 +297,38 @@ func (o *UiNodeInputAttributes) SetOnload(v string) { o.Onload = &v } +// GetOnloadTrigger returns the OnloadTrigger field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnloadTrigger() string { + if o == nil || o.OnloadTrigger == nil { + var ret string + return ret + } + return *o.OnloadTrigger +} + +// GetOnloadTriggerOk returns a tuple with the OnloadTrigger field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnloadTriggerOk() (*string, bool) { + if o == nil || o.OnloadTrigger == nil { + return nil, false + } + return o.OnloadTrigger, true +} + +// HasOnloadTrigger returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnloadTrigger() bool { + if o != nil && o.OnloadTrigger != nil { + return true + } + + return false +} + +// SetOnloadTrigger gets a reference to the given string and assigns it to the OnloadTrigger field. +func (o *UiNodeInputAttributes) SetOnloadTrigger(v string) { + o.OnloadTrigger = &v +} + // GetPattern returns the Pattern field value if set, zero value otherwise. func (o *UiNodeInputAttributes) GetPattern() string { if o == nil || o.Pattern == nil { @@ -402,9 +470,15 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { if o.Onclick != nil { toSerialize["onclick"] = o.Onclick } + if o.OnclickTrigger != nil { + toSerialize["onclickTrigger"] = o.OnclickTrigger + } if o.Onload != nil { toSerialize["onload"] = o.Onload } + if o.OnloadTrigger != nil { + toSerialize["onloadTrigger"] = o.OnloadTrigger + } if o.Pattern != nil { toSerialize["pattern"] = o.Pattern } diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json index d2dd6567d240..33c3cd4a7c34 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json @@ -38,7 +38,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -54,7 +54,9 @@ "name": "passkey_login_trigger", "node_type": "input", "onclick": "window.__oryPasskeyLogin()", + "onclick_trigger": "oryPasskeyLogin", "onload": "window.__oryPasskeyLoginAutocompleteInit()", + "onload_trigger": "oryPasskeyLoginAutocompleteInit", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json index c331d4f4280f..9b6602d9064f 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json @@ -30,7 +30,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -46,6 +46,7 @@ "name": "passkey_login_trigger", "node_type": "input", "onclick": "window.__oryPasskeyLogin()", + "onclick_trigger": "oryPasskeyLogin", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json index c331d4f4280f..9b6602d9064f 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json @@ -30,7 +30,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -46,6 +46,7 @@ "name": "passkey_login_trigger", "node_type": "input", "onclick": "window.__oryPasskeyLogin()", + "onclick_trigger": "oryPasskeyLogin", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index f9032e39049d..10cdc52924d6 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -5,6 +5,7 @@ "name": "passkey_register_trigger", "node_type": "input", "onclick": "window.__oryPasskeySettingsRegistration()", + "onclick_trigger": "oryPasskeySettingsRegistration", "type": "button", "value": "" }, @@ -109,7 +110,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index 7e5c5b3d082b..ce4393561cfd 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -5,6 +5,7 @@ "name": "passkey_register_trigger", "node_type": "input", "onclick": "window.__oryPasskeySettingsRegistration()", + "onclick_trigger": "oryPasskeySettingsRegistration", "type": "button", "value": "" }, @@ -61,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json index 18e0cda77811..4eb8c3d30eb7 100644 --- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,6 +71,7 @@ "name": "passkey_register_trigger", "node_type": "input", "onclick": "window.__oryPasskeyRegistration()", + "onclick_trigger": "oryPasskeyRegistration", "type": "button" }, "group": "passkey", diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json index 18e0cda77811..4eb8c3d30eb7 100644 --- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,6 +71,7 @@ "name": "passkey_register_trigger", "node_type": "input", "onclick": "window.__oryPasskeyRegistration()", + "onclick_trigger": "oryPasskeyRegistration", "type": "button" }, "group": "passkey", diff --git a/selfservice/strategy/passkey/passkey_login.go b/selfservice/strategy/passkey/passkey_login.go index 54b3f475ed38..927944261007 100644 --- a/selfservice/strategy/passkey/passkey_login.go +++ b/selfservice/strategy/passkey/passkey_login.go @@ -9,6 +9,8 @@ import ( "net/http" "strings" + "github.com/ory/kratos/x/webauthnx/js" + "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/pkg/errors" @@ -103,6 +105,119 @@ func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *lo Type: node.InputAttributeTypeHidden, }}) + loginFlow.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + return nil +} + +func (s *Strategy) populateLoginMethodForRefresh(r *http.Request, loginFlow *login.Flow) error { + ctx := r.Context() + + identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, loginFlow, s.ID()) + if identifier == "" { + return nil + } + + id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID) + if err != nil { + return err + } + + cred, ok := id.GetCredentials(s.ID()) + if !ok { + // Identity has no passkey + return nil + } + + var conf identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &conf); err != nil { + return errors.WithStack(err) + } + + webAuthCreds := conf.Credentials.ToWebAuthn() + if len(webAuthCreds) == 0 { + // Identity has no webauthn + return nil + } + + passkeyIdentifier := s.PasskeyDisplayNameFromIdentity(ctx, id) + + webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return errors.WithStack(err) + } + option, sessionData, err := webAuthn.BeginLogin(&webauthnx.User{ + Name: passkeyIdentifier, + ID: conf.UserHandle, + Credentials: webAuthCreds, + Config: webAuthn.Config, + }) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate passkey login.").WithDebug(err.Error())) + } + + loginFlow.InternalContext, err = sjson.SetBytes( + loginFlow.InternalContext, + flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), + sessionData, + ) + if err != nil { + return errors.WithStack(err) + } + + injectWebAuthnOptions, err := json.Marshal(option) + if err != nil { + return errors.WithStack(err) + } + + loginFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyChallenge, + Type: node.InputAttributeTypeHidden, + FieldValue: string(injectWebAuthnOptions), + }}) + + loginFlow.UI.Nodes.Append(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + + loginFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyLogin, + Type: node.InputAttributeTypeHidden, + }}) + + loginFlow.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + loginFlow.UI.SetNode(node.NewInputField( + "identifier", + passkeyIdentifier, + node.DefaultGroup, + node.InputAttributeTypeHidden, + )) + return nil } @@ -362,7 +477,8 @@ func (s *Strategy) PopulateLoginMethodRefresh(r *http.Request, f *login.Flow) er node.PasskeyGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin }), ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) @@ -392,8 +508,11 @@ func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flo node.PasskeyGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js - attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin + + attr.OnLoad = js.WebAuthnTriggersPasskeyLoginAutocompleteInit.String() + "()" // same here + attr.OnLoadTrigger = js.WebAuthnTriggersPasskeyLoginAutocompleteInit }), ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) diff --git a/selfservice/strategy/passkey/passkey_registration.go b/selfservice/strategy/passkey/passkey_registration.go index 88efd420d725..9be753f70c40 100644 --- a/selfservice/strategy/passkey/passkey_registration.go +++ b/selfservice/strategy/passkey/passkey_registration.go @@ -11,6 +11,8 @@ import ( "net/url" "strings" + "github.com/ory/kratos/x/webauthnx/js" + "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/pkg/errors" @@ -280,9 +282,10 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, regFlow *registra Group: node.PasskeyGroup, Meta: &node.Meta{Label: text.NewInfoSelfServiceRegistrationRegisterPasskey()}, Attributes: &node.InputAttributes{ - Name: node.PasskeyRegisterTrigger, - Type: node.InputAttributeTypeButton, - OnClick: "window.__oryPasskeyRegistration()", // defined in webauthn.js + Name: node.PasskeyRegisterTrigger, + Type: node.InputAttributeTypeButton, + OnClick: js.WebAuthnTriggersPasskeyRegistration.String() + "()", // defined in webauthn.js + OnClickTrigger: js.WebAuthnTriggersPasskeyRegistration, }}) // Passkey nodes end diff --git a/selfservice/strategy/passkey/passkey_settings.go b/selfservice/strategy/passkey/passkey_settings.go index 548a261e442b..04beff02ab09 100644 --- a/selfservice/strategy/passkey/passkey_settings.go +++ b/selfservice/strategy/passkey/passkey_settings.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "github.com/ory/kratos/x/webauthnx/js" + "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/gofrs/uuid" @@ -114,7 +116,8 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity node.PasskeyGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) { - a.OnClick = "window.__oryPasskeySettingsRegistration()" + a.OnClick = js.WebAuthnTriggersPasskeySettingsRegistration.String() + "()" + a.OnClickTrigger = js.WebAuthnTriggersPasskeySettingsRegistration }), ).WithMetaLabel(text.NewInfoSelfServiceSettingsRegisterPasskey())) diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json index ca960c98d683..472dc71f4672 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json @@ -24,25 +24,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -61,7 +42,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -70,5 +51,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json index f4be195cdecf..03a66e9c5616 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "type": "text/javascript", "node_type": "script" }, @@ -51,6 +51,7 @@ "name": "webauthn_login_trigger", "type": "button", "disabled": false, + "onclick_trigger": "oryWebAuthnLogin", "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json index f4be195cdecf..03a66e9c5616 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "type": "text/javascript", "node_type": "script" }, @@ -51,6 +51,7 @@ "name": "webauthn_login_trigger", "type": "button", "disabled": false, + "onclick_trigger": "oryWebAuthnLogin", "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json index 581bff275b17..c359007414d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclick_trigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Use security key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index 0b1702c09413..02acdfb345d5 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -82,33 +82,33 @@ { "attributes": { "disabled": false, - "name": "webauthn_register_trigger", + "name": "webauthn_register", "node_type": "input", - "type": "button", + "type": "hidden", "value": "" }, "group": "webauthn", "messages": [], - "meta": { - "label": { - "id": 1050012, - "text": "Add security key", - "type": "info" - } - }, + "meta": {}, "type": "input" }, { "attributes": { "disabled": false, - "name": "webauthn_register", + "name": "webauthn_register_trigger", "node_type": "input", - "type": "hidden", - "value": "" + "onclick_trigger": "oryWebAuthnRegistration", + "type": "button" }, "group": "webauthn", "messages": [], - "meta": {}, + "meta": { + "label": { + "id": 1050012, + "text": "Add security key", + "type": "info" + } + }, "type": "input" }, { @@ -116,7 +116,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json index 515658a3d64f..9bd36e752fd0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json @@ -5,9 +5,6 @@ "webauthn_register_displayname": [ "" ], - "webauthn_register_trigger": [ - "" - ], "webauthn_remove": [ "666f6f666f6f" ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json index 515658a3d64f..9bd36e752fd0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json @@ -5,9 +5,6 @@ "webauthn_register_displayname": [ "" ], - "webauthn_register_trigger": [ - "" - ], "webauthn_remove": [ "666f6f666f6f" ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index b21fa4833028..8fddd27469f3 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -34,33 +34,33 @@ { "attributes": { "disabled": false, - "name": "webauthn_register_trigger", + "name": "webauthn_register", "node_type": "input", - "type": "button", + "type": "hidden", "value": "" }, "group": "webauthn", "messages": [], - "meta": { - "label": { - "id": 1050012, - "text": "Add security key", - "type": "info" - } - }, + "meta": {}, "type": "input" }, { "attributes": { "disabled": false, - "name": "webauthn_register", + "name": "webauthn_register_trigger", "node_type": "input", - "type": "hidden", - "value": "" + "onclick_trigger": "oryWebAuthnRegistration", + "type": "button" }, "group": "webauthn", "messages": [], - "meta": {}, + "meta": { + "label": { + "id": 1050012, + "text": "Add security key", + "type": "info" + } + }, "type": "input" }, { @@ -68,7 +68,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json index 515658a3d64f..9bd36e752fd0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json @@ -5,9 +5,6 @@ "webauthn_register_displayname": [ "" ], - "webauthn_register_trigger": [ - "" - ], "webauthn_remove": [ "666f6f666f6f" ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json index 515658a3d64f..9bd36e752fd0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json @@ -5,9 +5,6 @@ "webauthn_register_displayname": [ "" ], - "webauthn_register_trigger": [ - "" - ], "webauthn_remove": [ "666f6f666f6f" ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json index 14a920d0a18d..edcf3a92b509 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json @@ -75,8 +75,8 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "type": "button", - "value": "" + "onclick_trigger": "oryWebAuthnRegistration", + "type": "button" }, "group": "webauthn", "messages": [], @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json index 14a920d0a18d..edcf3a92b509 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json @@ -75,8 +75,8 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "type": "button", - "value": "" + "onclick_trigger": "oryWebAuthnRegistration", + "type": "button" }, "group": "webauthn", "messages": [], @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==", + "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/login_test.go b/selfservice/strategy/webauthn/login_test.go index 46db972c4cc7..cfb2b18455ac 100644 --- a/selfservice/strategy/webauthn/login_test.go +++ b/selfservice/strategy/webauthn/login_test.go @@ -170,9 +170,10 @@ func TestCompleteLogin(t *testing.T) { }, testhelpers.InitFlowWithRefresh()) snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", - "2.attributes.onclick", - "4.attributes.nonce", - "4.attributes.src", + "3.attributes.nonce", + "3.attributes.src", + "4.attributes.value", + "4.attributes.onclick", }) nodes, err := json.Marshal(f.Ui.Nodes) require.NoError(t, err) @@ -475,12 +476,13 @@ func TestCompleteLogin(t *testing.T) { testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", "1.attributes.value", - "2.attributes.onclick", - "2.attributes.onload", - "4.attributes.src", - "4.attributes.nonce", + "3.attributes.src", + "3.attributes.nonce", + "4.attributes.onclick", + "4.attributes.onload", + "4.attributes.value", }) - ensureReplacement(t, "2", f.Ui, "allowCredentials") + ensureReplacement(t, "4", f.Ui, "allowCredentials") }) t.Run("case=webauthn payload is not set when identity has no webauthn", func(t *testing.T) { diff --git a/selfservice/strategy/webauthn/registration_test.go b/selfservice/strategy/webauthn/registration_test.go index 973e1ae0ec81..8dd3e38bd036 100644 --- a/selfservice/strategy/webauthn/registration_test.go +++ b/selfservice/strategy/webauthn/registration_test.go @@ -145,6 +145,7 @@ func TestRegistration(t *testing.T) { testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "2.attributes.value", "5.attributes.onclick", + "5.attributes.value", "6.attributes.nonce", "6.attributes.src", }) diff --git a/selfservice/strategy/webauthn/settings_test.go b/selfservice/strategy/webauthn/settings_test.go index 3b46cc8de752..9a34159c9a3d 100644 --- a/selfservice/strategy/webauthn/settings_test.go +++ b/selfservice/strategy/webauthn/settings_test.go @@ -143,11 +143,12 @@ func TestCompleteSettings(t *testing.T) { testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", - "4.attributes.onclick", + "5.attributes.onclick", + "5.attributes.value", "6.attributes.src", "6.attributes.nonce", }) - ensureReplacement(t, "4", f.Ui, "Ory Corp") + ensureReplacement(t, "5", f.Ui, "Ory Corp") }) t.Run("case=one activation element is shown", func(t *testing.T) { @@ -159,12 +160,13 @@ func TestCompleteSettings(t *testing.T) { testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", - "2.attributes.onload", - "2.attributes.onclick", + "3.attributes.onload", + "3.attributes.onclick", + "3.attributes.value", "4.attributes.src", "4.attributes.nonce", }) - ensureReplacement(t, "2", f.Ui, "Ory Corp") + ensureReplacement(t, "3", f.Ui, "Ory Corp") }) t.Run("case=webauthn only works for browsers", func(t *testing.T) { @@ -375,7 +377,7 @@ func TestCompleteSettings(t *testing.T) { body, res := doBrowserFlow(t, spa, func(v url.Values) { // The remove key should be empty - snapshotx.SnapshotTExcept(t, v, []string{"csrf_token"}) + snapshotx.SnapshotTExcept(t, v, []string{"csrf_token", "webauthn_register_trigger"}) v.Set(node.WebAuthnRemove, "666f6f666f6f") }, id) @@ -416,7 +418,7 @@ func TestCompleteSettings(t *testing.T) { body, res := doBrowserFlow(t, spa, func(v url.Values) { // The remove key should be set - snapshotx.SnapshotTExcept(t, v, []string{"csrf_token"}) + snapshotx.SnapshotTExcept(t, v, []string{"csrf_token", "webauthn_register_trigger"}) v.Set(node.WebAuthnRemove, "666f6f666f6f") }, id) @@ -481,7 +483,6 @@ func TestCompleteSettings(t *testing.T) { // Check not to remove other credentials with webauthn _, ok = actual.GetCredentials(identity.CredentialsTypePassword) assert.True(t, ok) - } t.Run("type=browser", func(t *testing.T) { diff --git a/spec/api.json b/spec/api.json index afbd72885470..60a4c8b539c8 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2396,13 +2396,39 @@ "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "onclick": { - "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.", + "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.", "type": "string" }, + "onclickTrigger": { + "description": "OnClickTrigger may contain a WebAuthn trigger which should be executed on click.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration", + "enum": [ + "oryWebAuthnRegistration", + "oryWebAuthnLogin", + "oryPasskeyLogin", + "oryPasskeyLoginAutocompleteInit", + "oryPasskeyRegistration", + "oryPasskeySettingsRegistration" + ], + "type": "string", + "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration" + }, "onload": { - "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.", + "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.", "type": "string" }, + "onloadTrigger": { + "description": "OnLoadTrigger may contain a WebAuthn trigger which should be executed on load.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration", + "enum": [ + "oryWebAuthnRegistration", + "oryWebAuthnLogin", + "oryPasskeyLogin", + "oryPasskeyLoginAutocompleteInit", + "oryPasskeyRegistration", + "oryPasskeySettingsRegistration" + ], + "type": "string", + "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration" + }, "pattern": { "description": "The input's pattern.", "type": "string" diff --git a/spec/swagger.json b/spec/swagger.json index fe16afa03c25..9f4636b75977 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -5477,13 +5477,39 @@ "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "onclick": { - "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.", + "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.", "type": "string" }, + "onclickTrigger": { + "description": "OnClickTrigger may contain a WebAuthn trigger which should be executed on click.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration", + "type": "string", + "enum": [ + "oryWebAuthnRegistration", + "oryWebAuthnLogin", + "oryPasskeyLogin", + "oryPasskeyLoginAutocompleteInit", + "oryPasskeyRegistration", + "oryPasskeySettingsRegistration" + ], + "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration" + }, "onload": { - "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.", + "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.", "type": "string" }, + "onloadTrigger": { + "description": "OnLoadTrigger may contain a WebAuthn trigger which should be executed on load.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration", + "type": "string", + "enum": [ + "oryWebAuthnRegistration", + "oryWebAuthnLogin", + "oryPasskeyLogin", + "oryPasskeyLoginAutocompleteInit", + "oryPasskeyRegistration", + "oryPasskeySettingsRegistration" + ], + "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration" + }, "pattern": { "description": "The input's pattern.", "type": "string" diff --git a/ui/node/attributes.go b/ui/node/attributes.go index 762df9fd46c7..2c8045ecb743 100644 --- a/ui/node/attributes.go +++ b/ui/node/attributes.go @@ -6,6 +6,7 @@ package node import ( "fmt" "github.com/ory/kratos/text" + "github.com/ory/kratos/x/webauthnx/js" ) const ( @@ -97,12 +98,26 @@ type InputAttributes struct { // OnClick may contain javascript which should be executed on click. This is primarily // used for WebAuthn. + // + // Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead. OnClick string `json:"onclick,omitempty"` + // OnClickTrigger may contain a WebAuthn trigger which should be executed on click. + // + // The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. + OnClickTrigger js.WebAuthnTriggers `json:"onclickTrigger,omitempty"` + // OnLoad may contain javascript which should be executed on load. This is primarily // used for WebAuthn. + // + // Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead. OnLoad string `json:"onload,omitempty"` + // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load. + // + // The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. + OnLoadTrigger js.WebAuthnTriggers `json:"onloadTrigger,omitempty"` + // NodeType represents this node's types. It is a mirror of `node.type` and // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "input". // diff --git a/x/webauthnx/js/trigger.go b/x/webauthnx/js/trigger.go new file mode 100644 index 000000000000..7b236191ce8e --- /dev/null +++ b/x/webauthnx/js/trigger.go @@ -0,0 +1,22 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package js + +import "fmt" + +// swagger:enum WebAuthnTriggers +type WebAuthnTriggers string + +const ( + WebAuthnTriggersWebAuthnRegistration WebAuthnTriggers = "oryWebAuthnRegistration" + WebAuthnTriggersWebAuthnLogin WebAuthnTriggers = "oryWebAuthnLogin" + WebAuthnTriggersPasskeyLogin WebAuthnTriggers = "oryPasskeyLogin" + WebAuthnTriggersPasskeyLoginAutocompleteInit WebAuthnTriggers = "oryPasskeyLoginAutocompleteInit" + WebAuthnTriggersPasskeyRegistration WebAuthnTriggers = "oryPasskeyRegistration" + WebAuthnTriggersPasskeySettingsRegistration WebAuthnTriggers = "oryPasskeySettingsRegistration" +) + +func (r WebAuthnTriggers) String() string { + return fmt.Sprintf("window.%s", string(r)) +} diff --git a/x/webauthnx/js/trigger_test.go b/x/webauthnx/js/trigger_test.go new file mode 100644 index 000000000000..97f9dc00ee77 --- /dev/null +++ b/x/webauthnx/js/trigger_test.go @@ -0,0 +1,14 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package js + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToString(t *testing.T) { + assert.Equal(t, "window.oryWebAuthnRegistration", WebAuthnTriggersWebAuthnRegistration.String()) +} diff --git a/x/webauthnx/js/webauthn.js b/x/webauthnx/js/webauthn.js index 61a7cb8f976d..638bd4ece082 100644 --- a/x/webauthnx/js/webauthn.js +++ b/x/webauthnx/js/webauthn.js @@ -32,7 +32,7 @@ } function __oryWebAuthnLogin( - opt, + options, resultQuerySelector = '*[name="webauthn_login"]', triggerQuerySelector = '*[name="webauthn_login_trigger"]', ) { @@ -40,6 +40,12 @@ alert("This browser does not support WebAuthn!") } + const triggerEl = document.querySelector(triggerQuerySelector) + let opt = options + if (!opt) { + opt = JSON.parse(triggerEl.value) + } + opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) opt.publicKey.allowCredentials = opt.publicKey.allowCredentials.map( function (value) { @@ -71,7 +77,7 @@ }, }) - document.querySelector(triggerQuerySelector).closest("form").submit() + triggerEl.closest("form").submit() }) .catch((err) => { alert(err) @@ -79,7 +85,7 @@ } function __oryWebAuthnRegistration( - opt, + options, resultQuerySelector = '*[name="webauthn_register"]', triggerQuerySelector = '*[name="webauthn_register_trigger"]', ) { @@ -87,6 +93,12 @@ alert("This browser does not support WebAuthn!") } + const triggerEl = document.querySelector(triggerQuerySelector) + let opt = options + if (!opt) { + opt = JSON.parse(triggerEl.value) + } + opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id) opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) @@ -118,14 +130,14 @@ }, }) - document.querySelector(triggerQuerySelector).closest("form").submit() + triggerEl.closest("form").submit() }) .catch((err) => { alert(err) }) } - window.__oryPasskeyLoginAutocompleteInit = async function () { + async function __oryPasskeyLoginAutocompleteInit () { const dataEl = document.getElementsByName("passkey_challenge")[0] const resultEl = document.getElementsByName("passkey_login")[0] const identifierEl = document.getElementsByName("identifier")[0] @@ -195,7 +207,7 @@ }) } - window.__oryPasskeyLogin = function () { + function __oryPasskeyLogin () { const dataEl = document.getElementsByName("passkey_challenge")[0] const resultEl = document.getElementsByName("passkey_login")[0] @@ -262,7 +274,7 @@ }) } - window.__oryPasskeyRegistration = function () { + function __oryPasskeyRegistration () { const dataEl = document.getElementsByName("passkey_create_data")[0] const resultEl = document.getElementsByName("passkey_register")[0] @@ -373,8 +385,21 @@ }) } + // Deprecated naming with underscores - kept for support with Ory Elements v0 window.__oryWebAuthnLogin = __oryWebAuthnLogin window.__oryWebAuthnRegistration = __oryWebAuthnRegistration window.__oryPasskeySettingsRegistration = __oryPasskeySettingsRegistration + window.__oryPasskeyLogin = __oryPasskeyLogin + window.__oryPasskeyRegistration = __oryPasskeyRegistration + window.__oryPasskeyLoginAutocompleteInit = __oryPasskeyLoginAutocompleteInit + + // Current naming - use with Ory Elements v1 + window.oryWebAuthnLogin = __oryWebAuthnLogin + window.oryWebAuthnRegistration = __oryWebAuthnRegistration + window.oryPasskeySettingsRegistration = __oryPasskeySettingsRegistration + window.oryPasskeyLogin = __oryPasskeyLogin + window.oryPasskeyRegistration = __oryPasskeyRegistration + window.oryPasskeyLoginAutocompleteInit = __oryPasskeyLoginAutocompleteInit + window.__oryWebAuthnInitialized = true })() diff --git a/x/webauthnx/nodes.go b/x/webauthnx/nodes.go index 76fac1c397cd..85ecf621cfda 100644 --- a/x/webauthnx/nodes.go +++ b/x/webauthnx/nodes.go @@ -10,6 +10,8 @@ import ( "fmt" "net/url" + "github.com/ory/kratos/x/webauthnx/js" + "github.com/ory/x/stringsx" "github.com/ory/x/urlx" @@ -21,7 +23,9 @@ import ( func NewWebAuthnConnectionTrigger(options string) *node.Node { return node.NewInputField(node.WebAuthnRegisterTrigger, "", node.WebAuthnGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) { - a.OnClick = "window.__oryWebAuthnRegistration(" + options + ")" + a.OnClick = fmt.Sprintf("%s(%s)", js.WebAuthnTriggersWebAuthnRegistration, options) + a.OnClickTrigger = js.WebAuthnTriggersWebAuthnRegistration + a.FieldValue = options })) } @@ -44,7 +48,9 @@ func NewWebAuthnConnectionInput() *node.Node { func NewWebAuthnLoginTrigger(options string) *node.Node { return node.NewInputField(node.WebAuthnLoginTrigger, "", node.WebAuthnGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) { - a.OnClick = "window.__oryWebAuthnLogin(" + options + ")" + a.OnClick = fmt.Sprintf("%s(%s)", js.WebAuthnTriggersWebAuthnLogin, options) + a.FieldValue = options + a.OnClickTrigger = js.WebAuthnTriggersWebAuthnLogin })) } From 04850f45cfbdc89223366ffa3b540d579a3b44be Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:49:23 +0200 Subject: [PATCH 25/71] fix: replace submit with continue button for recovery and verification and add maxlength --- .../model_ui_node_input_attributes.go | 37 +++++++++++++++++++ .../model_ui_node_input_attributes.go | 37 +++++++++++++++++++ ...=fails_if_active_strategy_is_disabled.json | 4 +- ...=fails_if_active_strategy_is_disabled.json | 4 +- ...=fails_if_active_strategy_is_disabled.json | 4 +- ...=fails_if_active_strategy_is_disabled.json | 4 +- ...ail_field_when_creating_recovery_code.json | 4 +- ...set_all_the_correct_recovery_payloads.json | 4 +- ...ct_recovery_payloads_after_submission.json | 4 +- ...he_correct_recovery_payloads-type=api.json | 4 +- ...orrect_recovery_payloads-type=browser.json | 4 +- ...he_correct_recovery_payloads-type=spa.json | 4 +- ...ry_payloads_after_submission-type=api.json | 4 +- ...ayloads_after_submission-type=browser.json | 4 +- ...ry_payloads_after_submission-type=spa.json | 4 +- ...all_the_correct_verification_payloads.json | 4 +- ...erification_payloads_after_submission.json | 4 +- selfservice/strategy/code/strategy.go | 9 +++-- .../strategy/code/strategy_recovery.go | 5 ++- .../strategy/code/strategy_recovery_admin.go | 7 +++- ...set_all_the_correct_recovery_payloads.json | 4 +- ...ct_recovery_payloads_after_submission.json | 4 +- ...all_the_correct_verification_payloads.json | 4 +- ...erification_payloads_after_submission.json | 4 +- .../strategy/link/strategy_recovery.go | 2 +- .../strategy/link/strategy_verification.go | 2 +- ...sswordless-case=passkey_button_exists.json | 8 ++-- ...resh_passwordless_credentials-browser.json | 4 +- ...=refresh_passwordless_credentials-spa.json | 4 +- ...device_is_shown_which_can_be_unlinked.json | 4 +- ...-case=one_activation_element_is_shown.json | 4 +- ...on-case=passkey_button_exists-browser.json | 4 +- ...ration-case=passkey_button_exists-spa.json | 4 +- ...oad_is_set_when_identity_has_webauthn.json | 2 +- ...ebauthn_login_is_invalid-type=browser.json | 2 +- ...if_webauthn_login_is_invalid-type=spa.json | 2 +- ...passwordless_enabled=false#01-browser.json | 2 +- ...als-passwordless_enabled=false#01-spa.json | 2 +- ...passwordless_enabled=false#02-browser.json | 2 +- ...als-passwordless_enabled=false#02-spa.json | 2 +- ...ls-passwordless_enabled=false-browser.json | 2 +- ...ntials-passwordless_enabled=false-spa.json | 2 +- ...-passwordless_enabled=true#01-browser.json | 2 +- ...ials-passwordless_enabled=true#01-spa.json | 2 +- ...-passwordless_enabled=true#02-browser.json | 2 +- ...ials-passwordless_enabled=true#02-spa.json | 2 +- ...als-passwordless_enabled=true-browser.json | 2 +- ...entials-passwordless_enabled=true-spa.json | 2 +- ...device_is_shown_which_can_be_unlinked.json | 2 +- ...-case=one_activation_element_is_shown.json | 2 +- ...n-case=webauthn_button_exists-browser.json | 2 +- ...ation-case=webauthn_button_exists-spa.json | 2 +- spec/api.json | 5 +++ spec/swagger.json | 5 +++ ui/node/attributes.go | 3 ++ 55 files changed, 176 insertions(+), 82 deletions(-) diff --git a/internal/client-go/model_ui_node_input_attributes.go b/internal/client-go/model_ui_node_input_attributes.go index 7056a308d651..f8deff5d5417 100644 --- a/internal/client-go/model_ui_node_input_attributes.go +++ b/internal/client-go/model_ui_node_input_attributes.go @@ -22,6 +22,8 @@ type UiNodeInputAttributes struct { // Sets the input's disabled field to true or false. Disabled bool `json:"disabled"` Label *UiText `json:"label,omitempty"` + // MaxLength may contain the input's maximum length. + Maxlength *int64 `json:"maxlength,omitempty"` // The input's element name. Name string `json:"name"` // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script @@ -153,6 +155,38 @@ func (o *UiNodeInputAttributes) SetLabel(v UiText) { o.Label = &v } +// GetMaxlength returns the Maxlength field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetMaxlength() int64 { + if o == nil || o.Maxlength == nil { + var ret int64 + return ret + } + return *o.Maxlength +} + +// GetMaxlengthOk returns a tuple with the Maxlength field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetMaxlengthOk() (*int64, bool) { + if o == nil || o.Maxlength == nil { + return nil, false + } + return o.Maxlength, true +} + +// HasMaxlength returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasMaxlength() bool { + if o != nil && o.Maxlength != nil { + return true + } + + return false +} + +// SetMaxlength gets a reference to the given int64 and assigns it to the Maxlength field. +func (o *UiNodeInputAttributes) SetMaxlength(v int64) { + o.Maxlength = &v +} + // GetName returns the Name field value func (o *UiNodeInputAttributes) GetName() string { if o == nil { @@ -461,6 +495,9 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { if o.Label != nil { toSerialize["label"] = o.Label } + if o.Maxlength != nil { + toSerialize["maxlength"] = o.Maxlength + } if true { toSerialize["name"] = o.Name } diff --git a/internal/httpclient/model_ui_node_input_attributes.go b/internal/httpclient/model_ui_node_input_attributes.go index 7056a308d651..f8deff5d5417 100644 --- a/internal/httpclient/model_ui_node_input_attributes.go +++ b/internal/httpclient/model_ui_node_input_attributes.go @@ -22,6 +22,8 @@ type UiNodeInputAttributes struct { // Sets the input's disabled field to true or false. Disabled bool `json:"disabled"` Label *UiText `json:"label,omitempty"` + // MaxLength may contain the input's maximum length. + Maxlength *int64 `json:"maxlength,omitempty"` // The input's element name. Name string `json:"name"` // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script @@ -153,6 +155,38 @@ func (o *UiNodeInputAttributes) SetLabel(v UiText) { o.Label = &v } +// GetMaxlength returns the Maxlength field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetMaxlength() int64 { + if o == nil || o.Maxlength == nil { + var ret int64 + return ret + } + return *o.Maxlength +} + +// GetMaxlengthOk returns a tuple with the Maxlength field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetMaxlengthOk() (*int64, bool) { + if o == nil || o.Maxlength == nil { + return nil, false + } + return o.Maxlength, true +} + +// HasMaxlength returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasMaxlength() bool { + if o != nil && o.Maxlength != nil { + return true + } + + return false +} + +// SetMaxlength gets a reference to the given int64 and assigns it to the Maxlength field. +func (o *UiNodeInputAttributes) SetMaxlength(v int64) { + o.Maxlength = &v +} + // GetName returns the Name field value func (o *UiNodeInputAttributes) GetName() string { if o == nil { @@ -461,6 +495,9 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { if o.Label != nil { toSerialize["label"] = o.Label } + if o.Maxlength != nil { + toSerialize["maxlength"] = o.Maxlength + } if true { toSerialize["name"] = o.Name } diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json index f4c0270da2dc..17eb6e965bcb 100644 --- a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json +++ b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json @@ -50,8 +50,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json index 56782eed4571..a9ad1e527fb4 100644 --- a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json +++ b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json @@ -50,8 +50,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json index f4c0270da2dc..17eb6e965bcb 100644 --- a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json +++ b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json @@ -50,8 +50,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json index 56782eed4571..a9ad1e527fb4 100644 --- a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json +++ b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json @@ -50,8 +50,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json index a9f46bedb5c4..7030380e7fc2 100644 --- a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json +++ b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json @@ -31,8 +31,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json index ec1092ad77a6..195ca691e981 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json index dbf1dcd2cbb7..8d24938c9ae3 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json @@ -58,8 +58,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json index ec1092ad77a6..195ca691e981 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json index ec1092ad77a6..195ca691e981 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json index ec1092ad77a6..195ca691e981 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json index dbf1dcd2cbb7..8d24938c9ae3 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json @@ -58,8 +58,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json index dbf1dcd2cbb7..8d24938c9ae3 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json @@ -58,8 +58,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json index dbf1dcd2cbb7..8d24938c9ae3 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json @@ -58,8 +58,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json index 37f61ac9e827..01def57fd58f 100644 --- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json +++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json @@ -30,8 +30,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json index 42456da54dc5..7e7096cd7358 100644 --- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json +++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json @@ -44,8 +44,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index 80147786477a..351949f7cd95 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -193,7 +193,7 @@ func (s *Strategy) populateChooseMethodFlow(r *http.Request, f flow.Flow) error node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeInputEmail()), ) - codeMetaLabel = text.NewInfoNodeLabelSubmit() + codeMetaLabel = text.NewInfoNodeLabelContinue() case *login.Flow: ds, err := s.deps.Config().DefaultIdentityTraitsSchemaURL(ctx) if err != nil { @@ -363,13 +363,16 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error ) // code input field - freshNodes.Upsert(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + freshNodes.Upsert(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "[0-9]+" + a.MaxLength = CodeLength + })). WithMetaLabel(codeMetaLabel)) // code submit button freshNodes. Append(node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit())) + WithMetaLabel(text.NewInfoNodeLabelContinue())) if resendNode != nil { freshNodes.Append(resendNode) diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index 9376a8e1ef4d..f33356f2df31 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -43,7 +43,7 @@ func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) err f.UI. GetNodes(). Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit())) + WithMetaLabel(text.NewInfoNodeLabelContinue())) return nil } @@ -406,6 +406,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R f.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithInputAttributes(func(a *node.InputAttributes) { a.Required = true a.Pattern = "[0-9]+" + a.MaxLength = CodeLength })). WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), ) @@ -414,7 +415,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R f.UI. GetNodes(). Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit())) + WithMetaLabel(text.NewInfoNodeLabelContinue())) f.UI.Nodes.Append(node.NewInputField("email", body.Email, node.CodeGroup, node.InputAttributeTypeSubmit). WithMetaLabel(text.NewInfoNodeResendOTP()), diff --git a/selfservice/strategy/code/strategy_recovery_admin.go b/selfservice/strategy/code/strategy_recovery_admin.go index 028bb811bcaa..63aa36a90edd 100644 --- a/selfservice/strategy/code/strategy_recovery_admin.go +++ b/selfservice/strategy/code/strategy_recovery_admin.go @@ -178,13 +178,16 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. recoveryFlow.DangerousSkipCSRFCheck = true recoveryFlow.State = flow.StateEmailSent recoveryFlow.UI.Nodes = node.Nodes{} - recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "[0-9]+" + a.MaxLength = CodeLength + })). WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), ) recoveryFlow.UI.Nodes. Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit())) + WithMetaLabel(text.NewInfoNodeLabelContinue())) if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, recoveryFlow); err != nil { s.deps.Writer().WriteError(w, r, err) diff --git a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json index 3bb3cbbf3ef6..5ac9946936c8 100644 --- a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json +++ b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json index 498575cfee1b..1a8d048fe37d 100644 --- a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json +++ b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json @@ -45,8 +45,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json index 3bb3cbbf3ef6..5ac9946936c8 100644 --- a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json +++ b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json index 498575cfee1b..1a8d048fe37d 100644 --- a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json +++ b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json @@ -45,8 +45,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go index 184399ca1002..e6d91051c2c4 100644 --- a/selfservice/strategy/link/strategy_recovery.go +++ b/selfservice/strategy/link/strategy_recovery.go @@ -56,7 +56,7 @@ func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) err // v0.5: form.Field{Name: "email", Type: "email", Required: true}, node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) - f.UI.GetNodes().Append(node.NewInputField("method", s.RecoveryStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit())) + f.UI.GetNodes().Append(node.NewInputField("method", s.RecoveryStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue())) return nil } diff --git a/selfservice/strategy/link/strategy_verification.go b/selfservice/strategy/link/strategy_verification.go index a2a72ea9a277..61f95da52fef 100644 --- a/selfservice/strategy/link/strategy_verification.go +++ b/selfservice/strategy/link/strategy_verification.go @@ -44,7 +44,7 @@ func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.F // v0.5: form.Field{Name: "email", Type: "email", Required: true} node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) - f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit())) + f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue())) return nil } diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json index 33c3cd4a7c34..0635ea89a614 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json @@ -53,10 +53,10 @@ "disabled": false, "name": "passkey_login_trigger", "node_type": "input", - "onclick": "window.__oryPasskeyLogin()", - "onclick_trigger": "oryPasskeyLogin", - "onload": "window.__oryPasskeyLoginAutocompleteInit()", - "onload_trigger": "oryPasskeyLoginAutocompleteInit", + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", + "onload": "window.oryPasskeyLoginAutocompleteInit()", + "onloadTrigger": "oryPasskeyLoginAutocompleteInit", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json index 9b6602d9064f..c9ece0d3c08e 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json @@ -45,8 +45,8 @@ "disabled": false, "name": "passkey_login_trigger", "node_type": "input", - "onclick": "window.__oryPasskeyLogin()", - "onclick_trigger": "oryPasskeyLogin", + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json index 9b6602d9064f..c9ece0d3c08e 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json @@ -45,8 +45,8 @@ "disabled": false, "name": "passkey_login_trigger", "node_type": "input", - "onclick": "window.__oryPasskeyLogin()", - "onclick_trigger": "oryPasskeyLogin", + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index 10cdc52924d6..b7e2168a1591 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -4,8 +4,8 @@ "disabled": false, "name": "passkey_register_trigger", "node_type": "input", - "onclick": "window.__oryPasskeySettingsRegistration()", - "onclick_trigger": "oryPasskeySettingsRegistration", + "onclick": "window.oryPasskeySettingsRegistration()", + "onclickTrigger": "oryPasskeySettingsRegistration", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index ce4393561cfd..88861c80cdcb 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -4,8 +4,8 @@ "disabled": false, "name": "passkey_register_trigger", "node_type": "input", - "onclick": "window.__oryPasskeySettingsRegistration()", - "onclick_trigger": "oryPasskeySettingsRegistration", + "onclick": "window.oryPasskeySettingsRegistration()", + "onclickTrigger": "oryPasskeySettingsRegistration", "type": "button", "value": "" }, diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json index 4eb8c3d30eb7..e232b6edde24 100644 --- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json @@ -70,8 +70,8 @@ "disabled": false, "name": "passkey_register_trigger", "node_type": "input", - "onclick": "window.__oryPasskeyRegistration()", - "onclick_trigger": "oryPasskeyRegistration", + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", "type": "button" }, "group": "passkey", diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json index 4eb8c3d30eb7..e232b6edde24 100644 --- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json @@ -70,8 +70,8 @@ "disabled": false, "name": "passkey_register_trigger", "node_type": "input", - "onclick": "window.__oryPasskeyRegistration()", - "onclick_trigger": "oryPasskeyRegistration", + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", "type": "button" }, "group": "passkey", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json index 472dc71f4672..71ffeaabc0d0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json @@ -57,7 +57,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json index 03a66e9c5616..77f06d2f6c1e 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json @@ -51,7 +51,7 @@ "name": "webauthn_login_trigger", "type": "button", "disabled": false, - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json index 03a66e9c5616..77f06d2f6c1e 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json @@ -51,7 +51,7 @@ "name": "webauthn_login_trigger", "type": "button", "disabled": false, - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json index c359007414d0..c87a991f8f6d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json @@ -58,7 +58,7 @@ "disabled": false, "name": "webauthn_login_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnLogin", + "onclickTrigger": "oryWebAuthnLogin", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index 02acdfb345d5..7fab410d6716 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -97,7 +97,7 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnRegistration", + "onclickTrigger": "oryWebAuthnRegistration", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index 8fddd27469f3..68bfeefda8cc 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -49,7 +49,7 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnRegistration", + "onclickTrigger": "oryWebAuthnRegistration", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json index edcf3a92b509..91b3ff4cfcbc 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json @@ -75,7 +75,7 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnRegistration", + "onclickTrigger": "oryWebAuthnRegistration", "type": "button" }, "group": "webauthn", diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json index edcf3a92b509..91b3ff4cfcbc 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json @@ -75,7 +75,7 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "onclick_trigger": "oryWebAuthnRegistration", + "onclickTrigger": "oryWebAuthnRegistration", "type": "button" }, "group": "webauthn", diff --git a/spec/api.json b/spec/api.json index 60a4c8b539c8..e76a0b1737f1 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2379,6 +2379,11 @@ "label": { "$ref": "#/components/schemas/uiText" }, + "maxlength": { + "description": "MaxLength may contain the input's maximum length.", + "format": "int64", + "type": "integer" + }, "name": { "description": "The input's element name.", "type": "string" diff --git a/spec/swagger.json b/spec/swagger.json index 9f4636b75977..5cab5b7aea54 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -5460,6 +5460,11 @@ "label": { "$ref": "#/definitions/uiText" }, + "maxlength": { + "description": "MaxLength may contain the input's maximum length.", + "type": "integer", + "format": "int64" + }, "name": { "description": "The input's element name.", "type": "string" diff --git a/ui/node/attributes.go b/ui/node/attributes.go index 2c8045ecb743..7db1927c3499 100644 --- a/ui/node/attributes.go +++ b/ui/node/attributes.go @@ -118,6 +118,9 @@ type InputAttributes struct { // The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. OnLoadTrigger js.WebAuthnTriggers `json:"onloadTrigger,omitempty"` + // MaxLength may contain the input's maximum length. + MaxLength int `json:"maxlength,omitempty"` + // NodeType represents this node's types. It is a mirror of `node.type` and // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "input". // From 51042d99fab301f0bb44665e56c5a2364e7d8866 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:43:41 +0200 Subject: [PATCH 26/71] feat: set maxlength for totp input --- ...not_contain_email_field_when_creating_recovery_code.json | 2 ++ ..._all_the_correct_recovery_payloads_after_submission.json | 1 + ...correct_recovery_payloads_after_submission-type=api.json | 1 + ...ect_recovery_payloads_after_submission-type=browser.json | 1 + ...correct_recovery_payloads_after_submission-type=spa.json | 1 + ..._the_correct_verification_payloads_after_submission.json | 2 ++ ...eLogin-flow=passwordless-case=passkey_button_exists.json | 2 +- ...fresh-case=refresh_passwordless_credentials-browser.json | 2 +- ...w=refresh-case=refresh_passwordless_credentials-spa.json | 2 +- ...ttings-case=a_device_is_shown_which_can_be_unlinked.json | 2 +- ...mpleteSettings-case=one_activation_element_is_shown.json | 2 +- ...TestRegistration-case=passkey_button_exists-browser.json | 2 +- .../TestRegistration-case=passkey_button_exists-spa.json | 2 +- ...gin-case=totp_payload_is_set_when_identity_has_totp.json | 1 + selfservice/strategy/totp/generator.go | 3 ++- selfservice/strategy/totp/login.go | 2 +- ...=webauthn_payload_is_set_when_identity_has_webauthn.json | 2 +- ...ould_fail_if_webauthn_login_is_invalid-type=browser.json | 2 +- ...e=should_fail_if_webauthn_login_is_invalid-type=spa.json | 2 +- ...0_credentials-passwordless_enabled=false#01-browser.json | 2 +- ...fa_v0_credentials-passwordless_enabled=false#01-spa.json | 2 +- ...0_credentials-passwordless_enabled=false#02-browser.json | 2 +- ...fa_v0_credentials-passwordless_enabled=false#02-spa.json | 2 +- ...a_v0_credentials-passwordless_enabled=false-browser.json | 2 +- ...e=mfa_v0_credentials-passwordless_enabled=false-spa.json | 2 +- ...v0_credentials-passwordless_enabled=true#01-browser.json | 2 +- ...mfa_v0_credentials-passwordless_enabled=true#01-spa.json | 2 +- ...v0_credentials-passwordless_enabled=true#02-browser.json | 2 +- ...mfa_v0_credentials-passwordless_enabled=true#02-spa.json | 2 +- ...fa_v0_credentials-passwordless_enabled=true-browser.json | 2 +- ...se=mfa_v0_credentials-passwordless_enabled=true-spa.json | 2 +- ...ttings-case=a_device_is_shown_which_can_be_unlinked.json | 2 +- ...mpleteSettings-case=one_activation_element_is_shown.json | 2 +- ...estRegistration-case=webauthn_button_exists-browser.json | 2 +- .../TestRegistration-case=webauthn_button_exists-spa.json | 2 +- ui/node/attributes_input.go | 6 ++++++ 36 files changed, 44 insertions(+), 28 deletions(-) diff --git a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json index 7030380e7fc2..736578d0e543 100644 --- a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json +++ b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json @@ -6,7 +6,9 @@ "name": "code", "type": "text", "required": true, + "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json index 8d24938c9ae3..a5ab6784616a 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json @@ -21,6 +21,7 @@ "required": true, "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json index 8d24938c9ae3..a5ab6784616a 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json @@ -21,6 +21,7 @@ "required": true, "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json index 8d24938c9ae3..a5ab6784616a 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json @@ -21,6 +21,7 @@ "required": true, "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json index 8d24938c9ae3..a5ab6784616a 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json @@ -21,6 +21,7 @@ "required": true, "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json index 7e7096cd7358..fde2aae2986f 100644 --- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json +++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json @@ -19,7 +19,9 @@ "name": "code", "type": "text", "required": true, + "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json index 0635ea89a614..e99dd52e418e 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json @@ -38,7 +38,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json index c9ece0d3c08e..1e026fb9979a 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json @@ -30,7 +30,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json index c9ece0d3c08e..1e026fb9979a 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json @@ -30,7 +30,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index b7e2168a1591..a0383567eda4 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -110,7 +110,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index 88861c80cdcb..8d91edf04ce5 100644 --- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -62,7 +62,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json index e232b6edde24..e4c5160c9697 100644 --- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json index e232b6edde24..e4c5160c9697 100644 --- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json b/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json index afae3de49f05..23611d1c2255 100644 --- a/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json +++ b/selfservice/strategy/totp/.snapshots/TestCompleteLogin-case=totp_payload_is_set_when_identity_has_totp.json @@ -15,6 +15,7 @@ { "attributes": { "disabled": false, + "maxlength": 6, "name": "totp_code", "node_type": "input", "required": true, diff --git a/selfservice/strategy/totp/generator.go b/selfservice/strategy/totp/generator.go index fe79d8991d0d..9846506f1671 100644 --- a/selfservice/strategy/totp/generator.go +++ b/selfservice/strategy/totp/generator.go @@ -25,6 +25,7 @@ import ( // So we need 160/8 = 20 key length. stdtotp.Generate uses the key // length for reading from crypto.Rand. const secretSize = 160 / 8 +const digits = otp.DigitsSix func NewKey(ctx context.Context, accountName string, d interface { config.Provider @@ -33,7 +34,7 @@ func NewKey(ctx context.Context, accountName string, d interface { Issuer: d.Config().TOTPIssuer(ctx), AccountName: accountName, SecretSize: secretSize, - Digits: otp.DigitsSix, + Digits: digits, Period: 30, }) if err != nil { diff --git a/selfservice/strategy/totp/login.go b/selfservice/strategy/totp/login.go index 2aaface8dc5c..7b4564cb165d 100644 --- a/selfservice/strategy/totp/login.go +++ b/selfservice/strategy/totp/login.go @@ -50,7 +50,7 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.Au } sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(node.NewInputField("totp_code", "", node.TOTPGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoLoginTOTPLabel())) + sr.UI.SetNode(node.NewInputField("totp_code", "", node.TOTPGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute, node.WithMaxLengthInputAttribute(int(digits))).WithMetaLabel(text.NewInfoLoginTOTPLabel())) sr.UI.GetNodes().Append(node.NewInputField("method", s.ID(), node.TOTPGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginTOTP())) return nil diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json index 71ffeaabc0d0..71fbb382f0de 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json @@ -42,7 +42,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json index 77f06d2f6c1e..399562e7015d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "type": "text/javascript", "node_type": "script" }, diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json index 77f06d2f6c1e..399562e7015d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "type": "text/javascript", "node_type": "script" }, diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json index c87a991f8f6d..6b4d9b33d63d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json @@ -43,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index 7fab410d6716..f0edfe3c5966 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -116,7 +116,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index 68bfeefda8cc..c15a847d4703 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -68,7 +68,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json index 91b3ff4cfcbc..20e3d3566fb0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json index 91b3ff4cfcbc..20e3d3566fb0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-pRsQlAeFkuxTY2n9F69ZNQ7ah9WGFsvR63UGhDE2kt1X2n7vKsa4Xgl7293HRlZ33AD/+KR8eRNFMvjaau6GwQ==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/ui/node/attributes_input.go b/ui/node/attributes_input.go index b63ac9365a51..176c1a25b42e 100644 --- a/ui/node/attributes_input.go +++ b/ui/node/attributes_input.go @@ -38,6 +38,12 @@ func WithRequiredInputAttribute(a *InputAttributes) { a.Required = true } +func WithMaxLengthInputAttribute(maxLength int) func(a *InputAttributes) { + return func(a *InputAttributes) { + a.MaxLength = maxLength + } +} + func WithInputAttributes(f func(a *InputAttributes)) func(a *InputAttributes) { return func(a *InputAttributes) { f(a) From 7597bc6345848b66161d5a9b7a42307bbc85c978 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 24 May 2024 10:38:20 +0200 Subject: [PATCH 27/71] fix: add missing JS triggers --- selfservice/strategy/passkey/passkey_login.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/selfservice/strategy/passkey/passkey_login.go b/selfservice/strategy/passkey/passkey_login.go index 927944261007..f7d2d4580ed4 100644 --- a/selfservice/strategy/passkey/passkey_login.go +++ b/selfservice/strategy/passkey/passkey_login.go @@ -111,8 +111,11 @@ func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *lo node.PasskeyGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js - attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin + + attr.OnLoad = js.WebAuthnTriggersPasskeyLoginAutocompleteInit.String() + "()" // same here + attr.OnLoadTrigger = js.WebAuthnTriggersPasskeyLoginAutocompleteInit }), ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) @@ -206,7 +209,8 @@ func (s *Strategy) populateLoginMethodForRefresh(r *http.Request, loginFlow *log node.PasskeyGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin }), ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) @@ -549,8 +553,11 @@ func (s *Strategy) PopulateLoginMethodMultiStepSelection(r *http.Request, sr *lo node.PasskeyGroup, node.InputAttributeTypeButton, node.WithInputAttributes(func(attr *node.InputAttributes) { - attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js - attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin + + attr.OnLoad = js.WebAuthnTriggersPasskeyLoginAutocompleteInit.String() + "()" // same here + attr.OnLoadTrigger = js.WebAuthnTriggersPasskeyLoginAutocompleteInit }), ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) From 612e3bf09dbffd3feba08d5100bffbc39cbd240a Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 24 May 2024 10:41:48 +0200 Subject: [PATCH 28/71] feat: add if method to sdk --- .schema/openapi/patches/selfservice.yaml | 2 ++ selfservice/strategy/multistep/strategy_login.go | 4 ++-- selfservice/strategy/multistep/types.go | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml index db9d7d3e6720..88aca38afef4 100644 --- a/.schema/openapi/patches/selfservice.yaml +++ b/.schema/openapi/patches/selfservice.yaml @@ -52,6 +52,7 @@ - "$ref": "#/components/schemas/updateLoginFlowWithLookupSecretMethod" - "$ref": "#/components/schemas/updateLoginFlowWithCodeMethod" - "$ref": "#/components/schemas/updateLoginFlowWithPasskeyMethod" + - "$ref": "#/components/schemas/updateLoginFlowWithTwoStepMethod" - op: add path: /components/schemas/updateLoginFlowBody/discriminator value: @@ -64,6 +65,7 @@ lookup_secret: "#/components/schemas/updateLoginFlowWithLookupSecretMethod" code: "#/components/schemas/updateLoginFlowWithCodeMethod" passkey: "#/components/schemas/updateLoginFlowWithPasskeyMethod" + two_step: "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod" - op: add path: /components/schemas/loginFlowState/enum value: diff --git a/selfservice/strategy/multistep/strategy_login.go b/selfservice/strategy/multistep/strategy_login.go index 3552c5f11095..c4b821163ad4 100644 --- a/selfservice/strategy/multistep/strategy_login.go +++ b/selfservice/strategy/multistep/strategy_login.go @@ -18,7 +18,7 @@ import ( var _ login.FormHydrator = new(Strategy) var _ login.Strategy = new(Strategy) -func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, f *login.Flow, payload *updateLoginFlowWithMultiStepMethod, err error) error { +func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, f *login.Flow, payload *updateLoginFlowWithIdentifierFirstMethod, err error) error { if f != nil { f.UI.Nodes.SetValueAttribute("identifier", payload.Identifier) if f.Type == flow.TypeBrowser { @@ -38,7 +38,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, err } - var p updateLoginFlowWithMultiStepMethod + var p updateLoginFlowWithIdentifierFirstMethod if err := s.hd.Decode(r, &p, decoderx.HTTPDecoderSetValidatePayloads(true), decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema), diff --git a/selfservice/strategy/multistep/types.go b/selfservice/strategy/multistep/types.go index 5268f9c49195..1ac62c5c0667 100644 --- a/selfservice/strategy/multistep/types.go +++ b/selfservice/strategy/multistep/types.go @@ -4,8 +4,8 @@ import "encoding/json" // Update Login Flow with Multi-Step Method // -// swagger:model updateLoginFlowWithMultiStepMethod -type updateLoginFlowWithMultiStepMethod struct { +// swagger:model updateLoginFlowWithIdentifierFirstMethod +type updateLoginFlowWithIdentifierFirstMethod struct { // Method should be set to "password" when logging in using the identifier and password strategy. // // required: true From 5d8e3276036841fb01fafe8f3c9dd44bc21ee51f Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Fri, 24 May 2024 11:12:57 +0200 Subject: [PATCH 29/71] chore: regenerate SDK --- cmd/cliclient/client.go | 2 +- cmd/identities/definitions.go | 2 +- cmd/identities/get.go | 2 +- cmd/identities/import.go | 2 +- cmd/identities/import_test.go | 2 +- examples/go/selfservice/recovery/main_test.go | 2 +- examples/go/selfservice/settings/main_test.go | 2 +- .../go/selfservice/verification/main_test.go | 2 +- internal/httpclient/.gitignore | 24 - internal/httpclient/.openapi-generator-ignore | 23 - internal/httpclient/.openapi-generator/FILES | 260 - .../httpclient/.openapi-generator/VERSION | 1 - internal/httpclient/.travis.yml | 8 - internal/httpclient/README.md | 291 - internal/httpclient/api_courier.go | 362 - internal/httpclient/api_frontend.go | 5863 ----------------- internal/httpclient/api_metadata.go | 442 -- internal/httpclient/client.go | 550 -- internal/httpclient/configuration.go | 230 - internal/httpclient/git_push.sh | 58 - .../model_authenticator_assurance_level.go | 86 - .../model_batch_patch_identities_response.go | 115 - .../model_consistency_request_parameters.go | 115 - internal/httpclient/model_continue_with.go | 284 - .../model_continue_with_recovery_ui.go | 137 - .../model_continue_with_recovery_ui_flow.go | 145 - ...model_continue_with_redirect_browser_to.go | 138 - ...del_continue_with_set_ory_session_token.go | 138 - .../model_continue_with_settings_ui.go | 137 - .../model_continue_with_settings_ui_flow.go | 145 - .../model_continue_with_verification_ui.go | 137 - ...odel_continue_with_verification_ui_flow.go | 175 - .../model_courier_message_status.go | 86 - .../httpclient/model_courier_message_type.go | 84 - .../httpclient/model_create_identity_body.go | 361 - ..._create_recovery_link_for_identity_body.go | 145 - .../model_delete_my_sessions_count.go | 115 - ...enticator_assurance_level_not_satisfied.go | 151 - ..._error_browser_location_change_required.go | 151 - .../httpclient/model_error_flow_replaced.go | 151 - internal/httpclient/model_error_generic.go | 107 - internal/httpclient/model_flow_error.go | 219 - internal/httpclient/model_generic_error.go | 367 -- .../model_get_version_200_response.go | 108 - .../model_health_not_ready_status.go | 115 - internal/httpclient/model_health_status.go | 115 - internal/httpclient/model_identity.go | 582 -- .../httpclient/model_identity_credentials.go | 300 - .../model_identity_credentials_code.go | 163 - .../model_identity_credentials_oidc.go | 114 - ...odel_identity_credentials_oidc_provider.go | 294 - internal/httpclient/model_identity_patch.go | 151 - .../model_identity_patch_response.go | 189 - .../model_identity_schema_container.go | 152 - .../model_identity_with_credentials.go | 150 - .../model_identity_with_credentials_oidc.go | 114 - ...l_identity_with_credentials_oidc_config.go | 151 - ...y_with_credentials_oidc_config_provider.go | 138 - ...odel_identity_with_credentials_password.go | 114 - ...entity_with_credentials_password_config.go | 152 - .../httpclient/model_is_alive_200_response.go | 108 - .../httpclient/model_is_ready_503_response.go | 108 - internal/httpclient/model_json_patch.go | 213 - internal/httpclient/model_login_flow.go | 705 -- internal/httpclient/model_login_flow_state.go | 85 - internal/httpclient/model_logout_flow.go | 138 - internal/httpclient/model_message.go | 445 -- internal/httpclient/model_message_dispatch.go | 265 - .../model_needs_privileged_session_error.go | 144 - internal/httpclient/model_o_auth2_client.go | 1848 ------ ...consent_request_open_id_connect_context.go | 263 - .../httpclient/model_o_auth2_login_request.go | 407 -- .../httpclient/model_patch_identities_body.go | 115 - .../model_perform_native_logout_body.go | 108 - .../model_recovery_code_for_identity.go | 176 - internal/httpclient/model_recovery_flow.go | 438 -- .../httpclient/model_recovery_flow_state.go | 85 - .../model_recovery_identity_address.go | 240 - .../model_recovery_link_for_identity.go | 146 - .../httpclient/model_registration_flow.go | 558 -- .../model_registration_flow_state.go | 85 - .../model_self_service_flow_expired_error.go | 226 - internal/httpclient/model_session.go | 440 -- .../model_session_authentication_method.go | 262 - internal/httpclient/model_session_device.go | 219 - internal/httpclient/model_settings_flow.go | 467 -- .../httpclient/model_settings_flow_state.go | 84 - ...model_successful_code_exchange_response.go | 144 - .../model_successful_native_login.go | 181 - .../model_successful_native_registration.go | 217 - internal/httpclient/model_token_pagination.go | 160 - .../model_token_pagination_headers.go | 152 - internal/httpclient/model_ui_container.go | 203 - internal/httpclient/model_ui_node.go | 225 - .../model_ui_node_anchor_attributes.go | 197 - .../httpclient/model_ui_node_attributes.go | 284 - .../model_ui_node_image_attributes.go | 228 - .../model_ui_node_input_attributes.go | 568 -- internal/httpclient/model_ui_node_meta.go | 114 - .../model_ui_node_script_attributes.go | 348 - .../model_ui_node_text_attributes.go | 167 - internal/httpclient/model_ui_text.go | 204 - .../httpclient/model_update_identity_body.go | 280 - .../model_update_login_flow_body.go | 364 - ...odel_update_login_flow_with_code_method.go | 286 - ...te_login_flow_with_lookup_secret_method.go | 175 - ...odel_update_login_flow_with_oidc_method.go | 360 - ...l_update_login_flow_with_passkey_method.go | 182 - ..._update_login_flow_with_password_method.go | 279 - ...odel_update_login_flow_with_totp_method.go | 212 - ...update_login_flow_with_web_authn_method.go | 249 - .../model_update_recovery_flow_body.go | 164 - ...l_update_recovery_flow_with_code_method.go | 256 - ...l_update_recovery_flow_with_link_method.go | 212 - .../model_update_registration_flow_body.go | 324 - ...date_registration_flow_with_code_method.go | 286 - ...date_registration_flow_with_oidc_method.go | 360 - ...e_registration_flow_with_passkey_method.go | 249 - ..._registration_flow_with_password_method.go | 242 - ...e_registration_flow_with_profile_method.go | 249 - ...registration_flow_with_web_authn_method.go | 286 - .../model_update_settings_flow_body.go | 364 - ...update_settings_flow_with_lookup_method.go | 330 - ...l_update_settings_flow_with_oidc_method.go | 330 - ...pdate_settings_flow_with_passkey_method.go | 219 - ...date_settings_flow_with_password_method.go | 212 - ...pdate_settings_flow_with_profile_method.go | 212 - ...l_update_settings_flow_with_totp_method.go | 256 - ...ate_settings_flow_with_web_authn_method.go | 293 - .../model_update_verification_flow_body.go | 164 - ...date_verification_flow_with_code_method.go | 256 - ...date_verification_flow_with_link_method.go | 212 - .../model_verifiable_identity_address.go | 346 - .../httpclient/model_verification_flow.go | 422 -- .../model_verification_flow_state.go | 85 - internal/httpclient/model_version.go | 115 - internal/httpclient/response.go | 48 - internal/httpclient/utils.go | 329 - internal/registrationhelpers/helpers.go | 2 +- internal/testhelpers/sdk.go | 2 +- internal/testhelpers/selfservice_login.go | 2 +- internal/testhelpers/selfservice_recovery.go | 2 +- .../testhelpers/selfservice_registration.go | 2 +- internal/testhelpers/selfservice_settings.go | 2 +- .../testhelpers/selfservice_verification.go | 2 +- selfservice/flow/settings/handler_test.go | 2 +- .../strategy/code/strategy_login_test.go | 2 +- .../code/strategy_recovery_admin_test.go | 2 +- .../strategy/code/strategy_recovery_test.go | 2 +- .../code/strategy_registration_test.go | 2 +- .../strategy/link/strategy_recovery_test.go | 12 + selfservice/strategy/lookup/settings_test.go | 2 +- .../strategy/oidc/strategy_settings_test.go | 2 +- .../strategy/passkey/testfixture_test.go | 2 +- selfservice/strategy/password/login_test.go | 15 +- .../strategy/password/settings_test.go | 2 +- selfservice/strategy/profile/strategy_test.go | 2 +- selfservice/strategy/totp/settings_test.go | 2 +- selfservice/strategy/webauthn/login_test.go | 2 +- .../strategy/webauthn/registration_test.go | 2 +- .../strategy/webauthn/settings_test.go | 2 +- spec/api.json | 74 +- spec/swagger.json | 73 +- 163 files changed, 147 insertions(+), 35966 deletions(-) delete mode 100644 internal/httpclient/.gitignore delete mode 100644 internal/httpclient/.openapi-generator-ignore delete mode 100644 internal/httpclient/.openapi-generator/FILES delete mode 100644 internal/httpclient/.openapi-generator/VERSION delete mode 100644 internal/httpclient/.travis.yml delete mode 100644 internal/httpclient/README.md delete mode 100644 internal/httpclient/api_courier.go delete mode 100644 internal/httpclient/api_frontend.go delete mode 100644 internal/httpclient/api_metadata.go delete mode 100644 internal/httpclient/client.go delete mode 100644 internal/httpclient/configuration.go delete mode 100644 internal/httpclient/git_push.sh delete mode 100644 internal/httpclient/model_authenticator_assurance_level.go delete mode 100644 internal/httpclient/model_batch_patch_identities_response.go delete mode 100644 internal/httpclient/model_consistency_request_parameters.go delete mode 100644 internal/httpclient/model_continue_with.go delete mode 100644 internal/httpclient/model_continue_with_recovery_ui.go delete mode 100644 internal/httpclient/model_continue_with_recovery_ui_flow.go delete mode 100644 internal/httpclient/model_continue_with_redirect_browser_to.go delete mode 100644 internal/httpclient/model_continue_with_set_ory_session_token.go delete mode 100644 internal/httpclient/model_continue_with_settings_ui.go delete mode 100644 internal/httpclient/model_continue_with_settings_ui_flow.go delete mode 100644 internal/httpclient/model_continue_with_verification_ui.go delete mode 100644 internal/httpclient/model_continue_with_verification_ui_flow.go delete mode 100644 internal/httpclient/model_courier_message_status.go delete mode 100644 internal/httpclient/model_courier_message_type.go delete mode 100644 internal/httpclient/model_create_identity_body.go delete mode 100644 internal/httpclient/model_create_recovery_link_for_identity_body.go delete mode 100644 internal/httpclient/model_delete_my_sessions_count.go delete mode 100644 internal/httpclient/model_error_authenticator_assurance_level_not_satisfied.go delete mode 100644 internal/httpclient/model_error_browser_location_change_required.go delete mode 100644 internal/httpclient/model_error_flow_replaced.go delete mode 100644 internal/httpclient/model_error_generic.go delete mode 100644 internal/httpclient/model_flow_error.go delete mode 100644 internal/httpclient/model_generic_error.go delete mode 100644 internal/httpclient/model_get_version_200_response.go delete mode 100644 internal/httpclient/model_health_not_ready_status.go delete mode 100644 internal/httpclient/model_health_status.go delete mode 100644 internal/httpclient/model_identity.go delete mode 100644 internal/httpclient/model_identity_credentials.go delete mode 100644 internal/httpclient/model_identity_credentials_code.go delete mode 100644 internal/httpclient/model_identity_credentials_oidc.go delete mode 100644 internal/httpclient/model_identity_credentials_oidc_provider.go delete mode 100644 internal/httpclient/model_identity_patch.go delete mode 100644 internal/httpclient/model_identity_patch_response.go delete mode 100644 internal/httpclient/model_identity_schema_container.go delete mode 100644 internal/httpclient/model_identity_with_credentials.go delete mode 100644 internal/httpclient/model_identity_with_credentials_oidc.go delete mode 100644 internal/httpclient/model_identity_with_credentials_oidc_config.go delete mode 100644 internal/httpclient/model_identity_with_credentials_oidc_config_provider.go delete mode 100644 internal/httpclient/model_identity_with_credentials_password.go delete mode 100644 internal/httpclient/model_identity_with_credentials_password_config.go delete mode 100644 internal/httpclient/model_is_alive_200_response.go delete mode 100644 internal/httpclient/model_is_ready_503_response.go delete mode 100644 internal/httpclient/model_json_patch.go delete mode 100644 internal/httpclient/model_login_flow.go delete mode 100644 internal/httpclient/model_login_flow_state.go delete mode 100644 internal/httpclient/model_logout_flow.go delete mode 100644 internal/httpclient/model_message.go delete mode 100644 internal/httpclient/model_message_dispatch.go delete mode 100644 internal/httpclient/model_needs_privileged_session_error.go delete mode 100644 internal/httpclient/model_o_auth2_client.go delete mode 100644 internal/httpclient/model_o_auth2_consent_request_open_id_connect_context.go delete mode 100644 internal/httpclient/model_o_auth2_login_request.go delete mode 100644 internal/httpclient/model_patch_identities_body.go delete mode 100644 internal/httpclient/model_perform_native_logout_body.go delete mode 100644 internal/httpclient/model_recovery_code_for_identity.go delete mode 100644 internal/httpclient/model_recovery_flow.go delete mode 100644 internal/httpclient/model_recovery_flow_state.go delete mode 100644 internal/httpclient/model_recovery_identity_address.go delete mode 100644 internal/httpclient/model_recovery_link_for_identity.go delete mode 100644 internal/httpclient/model_registration_flow.go delete mode 100644 internal/httpclient/model_registration_flow_state.go delete mode 100644 internal/httpclient/model_self_service_flow_expired_error.go delete mode 100644 internal/httpclient/model_session.go delete mode 100644 internal/httpclient/model_session_authentication_method.go delete mode 100644 internal/httpclient/model_session_device.go delete mode 100644 internal/httpclient/model_settings_flow.go delete mode 100644 internal/httpclient/model_settings_flow_state.go delete mode 100644 internal/httpclient/model_successful_code_exchange_response.go delete mode 100644 internal/httpclient/model_successful_native_login.go delete mode 100644 internal/httpclient/model_successful_native_registration.go delete mode 100644 internal/httpclient/model_token_pagination.go delete mode 100644 internal/httpclient/model_token_pagination_headers.go delete mode 100644 internal/httpclient/model_ui_container.go delete mode 100644 internal/httpclient/model_ui_node.go delete mode 100644 internal/httpclient/model_ui_node_anchor_attributes.go delete mode 100644 internal/httpclient/model_ui_node_attributes.go delete mode 100644 internal/httpclient/model_ui_node_image_attributes.go delete mode 100644 internal/httpclient/model_ui_node_input_attributes.go delete mode 100644 internal/httpclient/model_ui_node_meta.go delete mode 100644 internal/httpclient/model_ui_node_script_attributes.go delete mode 100644 internal/httpclient/model_ui_node_text_attributes.go delete mode 100644 internal/httpclient/model_ui_text.go delete mode 100644 internal/httpclient/model_update_identity_body.go delete mode 100644 internal/httpclient/model_update_login_flow_body.go delete mode 100644 internal/httpclient/model_update_login_flow_with_code_method.go delete mode 100644 internal/httpclient/model_update_login_flow_with_lookup_secret_method.go delete mode 100644 internal/httpclient/model_update_login_flow_with_oidc_method.go delete mode 100644 internal/httpclient/model_update_login_flow_with_passkey_method.go delete mode 100644 internal/httpclient/model_update_login_flow_with_password_method.go delete mode 100644 internal/httpclient/model_update_login_flow_with_totp_method.go delete mode 100644 internal/httpclient/model_update_login_flow_with_web_authn_method.go delete mode 100644 internal/httpclient/model_update_recovery_flow_body.go delete mode 100644 internal/httpclient/model_update_recovery_flow_with_code_method.go delete mode 100644 internal/httpclient/model_update_recovery_flow_with_link_method.go delete mode 100644 internal/httpclient/model_update_registration_flow_body.go delete mode 100644 internal/httpclient/model_update_registration_flow_with_code_method.go delete mode 100644 internal/httpclient/model_update_registration_flow_with_oidc_method.go delete mode 100644 internal/httpclient/model_update_registration_flow_with_passkey_method.go delete mode 100644 internal/httpclient/model_update_registration_flow_with_password_method.go delete mode 100644 internal/httpclient/model_update_registration_flow_with_profile_method.go delete mode 100644 internal/httpclient/model_update_registration_flow_with_web_authn_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_body.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_lookup_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_oidc_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_passkey_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_password_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_profile_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_totp_method.go delete mode 100644 internal/httpclient/model_update_settings_flow_with_web_authn_method.go delete mode 100644 internal/httpclient/model_update_verification_flow_body.go delete mode 100644 internal/httpclient/model_update_verification_flow_with_code_method.go delete mode 100644 internal/httpclient/model_update_verification_flow_with_link_method.go delete mode 100644 internal/httpclient/model_verifiable_identity_address.go delete mode 100644 internal/httpclient/model_verification_flow.go delete mode 100644 internal/httpclient/model_verification_flow_state.go delete mode 100644 internal/httpclient/model_version.go delete mode 100644 internal/httpclient/response.go delete mode 100644 internal/httpclient/utils.go diff --git a/cmd/cliclient/client.go b/cmd/cliclient/client.go index 82a41f2aacb1..fc7a4bed451c 100644 --- a/cmd/cliclient/client.go +++ b/cmd/cliclient/client.go @@ -18,7 +18,7 @@ import ( "github.com/spf13/pflag" - kratos "github.com/ory/kratos/internal/httpclient" + kratos "github.com/ory/client-go" ) const ( diff --git a/cmd/identities/definitions.go b/cmd/identities/definitions.go index 956266e70aa3..876eeba9f4a9 100644 --- a/cmd/identities/definitions.go +++ b/cmd/identities/definitions.go @@ -6,7 +6,7 @@ package identities import ( "strings" - kratos "github.com/ory/kratos/internal/httpclient" + kratos "github.com/ory/client-go" "github.com/ory/x/cmdx" ) diff --git a/cmd/identities/get.go b/cmd/identities/get.go index 677ac3bc9121..a575af2920e9 100644 --- a/cmd/identities/get.go +++ b/cmd/identities/get.go @@ -6,7 +6,7 @@ package identities import ( "fmt" - kratos "github.com/ory/kratos/internal/httpclient" + kratos "github.com/ory/client-go" "github.com/ory/kratos/x" "github.com/ory/x/cmdx" "github.com/ory/x/stringsx" diff --git a/cmd/identities/import.go b/cmd/identities/import.go index 1de8a22de385..16641f919477 100644 --- a/cmd/identities/import.go +++ b/cmd/identities/import.go @@ -7,7 +7,7 @@ import ( "encoding/json" "fmt" - kratos "github.com/ory/kratos/internal/httpclient" + kratos "github.com/ory/client-go" "github.com/ory/x/cmdx" diff --git a/cmd/identities/import_test.go b/cmd/identities/import_test.go index 8db159de9cff..1e8031b906ac 100644 --- a/cmd/identities/import_test.go +++ b/cmd/identities/import_test.go @@ -19,8 +19,8 @@ import ( "github.com/stretchr/testify/require" "github.com/tidwall/gjson" + kratos "github.com/ory/client-go" "github.com/ory/kratos/driver/config" - kratos "github.com/ory/kratos/internal/httpclient" ) func TestImportCmd(t *testing.T) { diff --git a/examples/go/selfservice/recovery/main_test.go b/examples/go/selfservice/recovery/main_test.go index b4ca43c511d6..d37a561227eb 100644 --- a/examples/go/selfservice/recovery/main_test.go +++ b/examples/go/selfservice/recovery/main_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ory "github.com/ory/client-go" "github.com/ory/kratos/examples/go/pkg" - ory "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/internal/testhelpers" ) diff --git a/examples/go/selfservice/settings/main_test.go b/examples/go/selfservice/settings/main_test.go index 1dd5fa8cf77c..12518930724a 100644 --- a/examples/go/selfservice/settings/main_test.go +++ b/examples/go/selfservice/settings/main_test.go @@ -6,7 +6,7 @@ package main import ( "testing" - ory "github.com/ory/kratos/internal/httpclient" + ory "github.com/ory/client-go" "github.com/stretchr/testify/assert" diff --git a/examples/go/selfservice/verification/main_test.go b/examples/go/selfservice/verification/main_test.go index ca9ba687fb12..6a6621a15657 100644 --- a/examples/go/selfservice/verification/main_test.go +++ b/examples/go/selfservice/verification/main_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ory "github.com/ory/client-go" "github.com/ory/kratos/examples/go/pkg" - ory "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/internal/testhelpers" ) diff --git a/internal/httpclient/.gitignore b/internal/httpclient/.gitignore deleted file mode 100644 index daf913b1b347..000000000000 --- a/internal/httpclient/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof diff --git a/internal/httpclient/.openapi-generator-ignore b/internal/httpclient/.openapi-generator-ignore deleted file mode 100644 index 7484ee590a38..000000000000 --- a/internal/httpclient/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES deleted file mode 100644 index 8f05b235508f..000000000000 --- a/internal/httpclient/.openapi-generator/FILES +++ /dev/null @@ -1,260 +0,0 @@ -.gitignore -.openapi-generator-ignore -.travis.yml -README.md -api/openapi.yaml -api_courier.go -api_frontend.go -api_identity.go -api_metadata.go -client.go -configuration.go -docs/AuthenticatorAssuranceLevel.md -docs/BatchPatchIdentitiesResponse.md -docs/ConsistencyRequestParameters.md -docs/ContinueWith.md -docs/ContinueWithRecoveryUi.md -docs/ContinueWithRecoveryUiFlow.md -docs/ContinueWithRedirectBrowserTo.md -docs/ContinueWithSetOrySessionToken.md -docs/ContinueWithSettingsUi.md -docs/ContinueWithSettingsUiFlow.md -docs/ContinueWithVerificationUi.md -docs/ContinueWithVerificationUiFlow.md -docs/CourierApi.md -docs/CourierMessageStatus.md -docs/CourierMessageType.md -docs/CreateIdentityBody.md -docs/CreateRecoveryCodeForIdentityBody.md -docs/CreateRecoveryLinkForIdentityBody.md -docs/DeleteMySessionsCount.md -docs/ErrorAuthenticatorAssuranceLevelNotSatisfied.md -docs/ErrorBrowserLocationChangeRequired.md -docs/ErrorFlowReplaced.md -docs/ErrorGeneric.md -docs/FlowError.md -docs/FrontendApi.md -docs/GenericError.md -docs/GetVersion200Response.md -docs/HealthNotReadyStatus.md -docs/HealthStatus.md -docs/Identity.md -docs/IdentityApi.md -docs/IdentityCredentials.md -docs/IdentityCredentialsCode.md -docs/IdentityCredentialsOidc.md -docs/IdentityCredentialsOidcProvider.md -docs/IdentityCredentialsPassword.md -docs/IdentityPatch.md -docs/IdentityPatchResponse.md -docs/IdentitySchemaContainer.md -docs/IdentityWithCredentials.md -docs/IdentityWithCredentialsOidc.md -docs/IdentityWithCredentialsOidcConfig.md -docs/IdentityWithCredentialsOidcConfigProvider.md -docs/IdentityWithCredentialsPassword.md -docs/IdentityWithCredentialsPasswordConfig.md -docs/IsAlive200Response.md -docs/IsReady503Response.md -docs/JsonPatch.md -docs/LoginFlow.md -docs/LoginFlowState.md -docs/LogoutFlow.md -docs/Message.md -docs/MessageDispatch.md -docs/MetadataApi.md -docs/NeedsPrivilegedSessionError.md -docs/OAuth2Client.md -docs/OAuth2ConsentRequestOpenIDConnectContext.md -docs/OAuth2LoginRequest.md -docs/PatchIdentitiesBody.md -docs/PerformNativeLogoutBody.md -docs/RecoveryCodeForIdentity.md -docs/RecoveryFlow.md -docs/RecoveryFlowState.md -docs/RecoveryIdentityAddress.md -docs/RecoveryLinkForIdentity.md -docs/RegistrationFlow.md -docs/RegistrationFlowState.md -docs/SelfServiceFlowExpiredError.md -docs/Session.md -docs/SessionAuthenticationMethod.md -docs/SessionDevice.md -docs/SettingsFlow.md -docs/SettingsFlowState.md -docs/SuccessfulCodeExchangeResponse.md -docs/SuccessfulNativeLogin.md -docs/SuccessfulNativeRegistration.md -docs/TokenPagination.md -docs/TokenPaginationHeaders.md -docs/UiContainer.md -docs/UiNode.md -docs/UiNodeAnchorAttributes.md -docs/UiNodeAttributes.md -docs/UiNodeImageAttributes.md -docs/UiNodeInputAttributes.md -docs/UiNodeMeta.md -docs/UiNodeScriptAttributes.md -docs/UiNodeTextAttributes.md -docs/UiText.md -docs/UpdateIdentityBody.md -docs/UpdateLoginFlowBody.md -docs/UpdateLoginFlowWithCodeMethod.md -docs/UpdateLoginFlowWithLookupSecretMethod.md -docs/UpdateLoginFlowWithOidcMethod.md -docs/UpdateLoginFlowWithPasskeyMethod.md -docs/UpdateLoginFlowWithPasswordMethod.md -docs/UpdateLoginFlowWithTotpMethod.md -docs/UpdateLoginFlowWithWebAuthnMethod.md -docs/UpdateRecoveryFlowBody.md -docs/UpdateRecoveryFlowWithCodeMethod.md -docs/UpdateRecoveryFlowWithLinkMethod.md -docs/UpdateRegistrationFlowBody.md -docs/UpdateRegistrationFlowWithCodeMethod.md -docs/UpdateRegistrationFlowWithOidcMethod.md -docs/UpdateRegistrationFlowWithPasskeyMethod.md -docs/UpdateRegistrationFlowWithPasswordMethod.md -docs/UpdateRegistrationFlowWithProfileMethod.md -docs/UpdateRegistrationFlowWithWebAuthnMethod.md -docs/UpdateSettingsFlowBody.md -docs/UpdateSettingsFlowWithLookupMethod.md -docs/UpdateSettingsFlowWithOidcMethod.md -docs/UpdateSettingsFlowWithPasskeyMethod.md -docs/UpdateSettingsFlowWithPasswordMethod.md -docs/UpdateSettingsFlowWithProfileMethod.md -docs/UpdateSettingsFlowWithTotpMethod.md -docs/UpdateSettingsFlowWithWebAuthnMethod.md -docs/UpdateVerificationFlowBody.md -docs/UpdateVerificationFlowWithCodeMethod.md -docs/UpdateVerificationFlowWithLinkMethod.md -docs/VerifiableIdentityAddress.md -docs/VerificationFlow.md -docs/VerificationFlowState.md -docs/Version.md -git_push.sh -go.mod -go.sum -model_authenticator_assurance_level.go -model_batch_patch_identities_response.go -model_consistency_request_parameters.go -model_continue_with.go -model_continue_with_recovery_ui.go -model_continue_with_recovery_ui_flow.go -model_continue_with_redirect_browser_to.go -model_continue_with_set_ory_session_token.go -model_continue_with_settings_ui.go -model_continue_with_settings_ui_flow.go -model_continue_with_verification_ui.go -model_continue_with_verification_ui_flow.go -model_courier_message_status.go -model_courier_message_type.go -model_create_identity_body.go -model_create_recovery_code_for_identity_body.go -model_create_recovery_link_for_identity_body.go -model_delete_my_sessions_count.go -model_error_authenticator_assurance_level_not_satisfied.go -model_error_browser_location_change_required.go -model_error_flow_replaced.go -model_error_generic.go -model_flow_error.go -model_generic_error.go -model_get_version_200_response.go -model_health_not_ready_status.go -model_health_status.go -model_identity.go -model_identity_credentials.go -model_identity_credentials_code.go -model_identity_credentials_oidc.go -model_identity_credentials_oidc_provider.go -model_identity_credentials_password.go -model_identity_patch.go -model_identity_patch_response.go -model_identity_schema_container.go -model_identity_with_credentials.go -model_identity_with_credentials_oidc.go -model_identity_with_credentials_oidc_config.go -model_identity_with_credentials_oidc_config_provider.go -model_identity_with_credentials_password.go -model_identity_with_credentials_password_config.go -model_is_alive_200_response.go -model_is_ready_503_response.go -model_json_patch.go -model_login_flow.go -model_login_flow_state.go -model_logout_flow.go -model_message.go -model_message_dispatch.go -model_needs_privileged_session_error.go -model_o_auth2_client.go -model_o_auth2_consent_request_open_id_connect_context.go -model_o_auth2_login_request.go -model_patch_identities_body.go -model_perform_native_logout_body.go -model_recovery_code_for_identity.go -model_recovery_flow.go -model_recovery_flow_state.go -model_recovery_identity_address.go -model_recovery_link_for_identity.go -model_registration_flow.go -model_registration_flow_state.go -model_self_service_flow_expired_error.go -model_session.go -model_session_authentication_method.go -model_session_device.go -model_settings_flow.go -model_settings_flow_state.go -model_successful_code_exchange_response.go -model_successful_native_login.go -model_successful_native_registration.go -model_token_pagination.go -model_token_pagination_headers.go -model_ui_container.go -model_ui_node.go -model_ui_node_anchor_attributes.go -model_ui_node_attributes.go -model_ui_node_image_attributes.go -model_ui_node_input_attributes.go -model_ui_node_meta.go -model_ui_node_script_attributes.go -model_ui_node_text_attributes.go -model_ui_text.go -model_update_identity_body.go -model_update_login_flow_body.go -model_update_login_flow_with_code_method.go -model_update_login_flow_with_lookup_secret_method.go -model_update_login_flow_with_oidc_method.go -model_update_login_flow_with_passkey_method.go -model_update_login_flow_with_password_method.go -model_update_login_flow_with_totp_method.go -model_update_login_flow_with_web_authn_method.go -model_update_recovery_flow_body.go -model_update_recovery_flow_with_code_method.go -model_update_recovery_flow_with_link_method.go -model_update_registration_flow_body.go -model_update_registration_flow_with_code_method.go -model_update_registration_flow_with_oidc_method.go -model_update_registration_flow_with_passkey_method.go -model_update_registration_flow_with_password_method.go -model_update_registration_flow_with_profile_method.go -model_update_registration_flow_with_web_authn_method.go -model_update_settings_flow_body.go -model_update_settings_flow_with_lookup_method.go -model_update_settings_flow_with_oidc_method.go -model_update_settings_flow_with_passkey_method.go -model_update_settings_flow_with_password_method.go -model_update_settings_flow_with_profile_method.go -model_update_settings_flow_with_totp_method.go -model_update_settings_flow_with_web_authn_method.go -model_update_verification_flow_body.go -model_update_verification_flow_with_code_method.go -model_update_verification_flow_with_link_method.go -model_verifiable_identity_address.go -model_verification_flow.go -model_verification_flow_state.go -model_version.go -response.go -test/api_courier_test.go -test/api_frontend_test.go -test/api_identity_test.go -test/api_metadata_test.go -utils.go diff --git a/internal/httpclient/.openapi-generator/VERSION b/internal/httpclient/.openapi-generator/VERSION deleted file mode 100644 index 4b49d9bb63ee..000000000000 --- a/internal/httpclient/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.2.0 \ No newline at end of file diff --git a/internal/httpclient/.travis.yml b/internal/httpclient/.travis.yml deleted file mode 100644 index f5cb2ce9a5aa..000000000000 --- a/internal/httpclient/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: go - -install: - - go get -d -v . - -script: - - go build -v ./ - diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md deleted file mode 100644 index 01f9831e7520..000000000000 --- a/internal/httpclient/README.md +++ /dev/null @@ -1,291 +0,0 @@ -# Go API client for client - -This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. - - -## Overview -This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client. - -- API version: -- Package version: 1.0.0 -- Build package: org.openapitools.codegen.languages.GoClientCodegen - -## Installation - -Install the following dependencies: - -```shell -go get github.com/stretchr/testify/assert -go get golang.org/x/oauth2 -go get golang.org/x/net/context -``` - -Put the package under your project folder and add the following in import: - -```golang -import client "github.com/ory/client-go" -``` - -To use a proxy, set the environment variable `HTTP_PROXY`: - -```golang -os.Setenv("HTTP_PROXY", "http://proxy_name:proxy_port") -``` - -## Configuration of Server URL - -Default configuration comes with `Servers` field that contains server objects as defined in the OpenAPI specification. - -### Select Server Configuration - -For using other server than the one defined on index 0 set context value `sw.ContextServerIndex` of type `int`. - -```golang -ctx := context.WithValue(context.Background(), client.ContextServerIndex, 1) -``` - -### Templated Server URL - -Templated server URL is formatted using default variables from configuration or from context value `sw.ContextServerVariables` of type `map[string]string`. - -```golang -ctx := context.WithValue(context.Background(), client.ContextServerVariables, map[string]string{ - "basePath": "v2", -}) -``` - -Note, enum values are always validated and all unused variables are silently ignored. - -### URLs Configuration per Operation - -Each operation can use different server URL defined using `OperationServers` map in the `Configuration`. -An operation is uniquely identifield by `"{classname}Service.{nickname}"` string. -Similar rules for overriding default operation server index and variables applies by using `sw.ContextOperationServerIndices` and `sw.ContextOperationServerVariables` context maps. - -``` -ctx := context.WithValue(context.Background(), client.ContextOperationServerIndices, map[string]int{ - "{classname}Service.{nickname}": 2, -}) -ctx = context.WithValue(context.Background(), client.ContextOperationServerVariables, map[string]map[string]string{ - "{classname}Service.{nickname}": { - "port": "8443", - }, -}) -``` - -## Documentation for API Endpoints - -All URIs are relative to *http://localhost* - -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -*CourierApi* | [**GetCourierMessage**](docs/CourierApi.md#getcouriermessage) | **Get** /admin/courier/messages/{id} | Get a Message -*CourierApi* | [**ListCourierMessages**](docs/CourierApi.md#listcouriermessages) | **Get** /admin/courier/messages | List Messages -*FrontendApi* | [**CreateBrowserLoginFlow**](docs/FrontendApi.md#createbrowserloginflow) | **Get** /self-service/login/browser | Create Login Flow for Browsers -*FrontendApi* | [**CreateBrowserLogoutFlow**](docs/FrontendApi.md#createbrowserlogoutflow) | **Get** /self-service/logout/browser | Create a Logout URL for Browsers -*FrontendApi* | [**CreateBrowserRecoveryFlow**](docs/FrontendApi.md#createbrowserrecoveryflow) | **Get** /self-service/recovery/browser | Create Recovery Flow for Browsers -*FrontendApi* | [**CreateBrowserRegistrationFlow**](docs/FrontendApi.md#createbrowserregistrationflow) | **Get** /self-service/registration/browser | Create Registration Flow for Browsers -*FrontendApi* | [**CreateBrowserSettingsFlow**](docs/FrontendApi.md#createbrowsersettingsflow) | **Get** /self-service/settings/browser | Create Settings Flow for Browsers -*FrontendApi* | [**CreateBrowserVerificationFlow**](docs/FrontendApi.md#createbrowserverificationflow) | **Get** /self-service/verification/browser | Create Verification Flow for Browser Clients -*FrontendApi* | [**CreateNativeLoginFlow**](docs/FrontendApi.md#createnativeloginflow) | **Get** /self-service/login/api | Create Login Flow for Native Apps -*FrontendApi* | [**CreateNativeRecoveryFlow**](docs/FrontendApi.md#createnativerecoveryflow) | **Get** /self-service/recovery/api | Create Recovery Flow for Native Apps -*FrontendApi* | [**CreateNativeRegistrationFlow**](docs/FrontendApi.md#createnativeregistrationflow) | **Get** /self-service/registration/api | Create Registration Flow for Native Apps -*FrontendApi* | [**CreateNativeSettingsFlow**](docs/FrontendApi.md#createnativesettingsflow) | **Get** /self-service/settings/api | Create Settings Flow for Native Apps -*FrontendApi* | [**CreateNativeVerificationFlow**](docs/FrontendApi.md#createnativeverificationflow) | **Get** /self-service/verification/api | Create Verification Flow for Native Apps -*FrontendApi* | [**DisableMyOtherSessions**](docs/FrontendApi.md#disablemyothersessions) | **Delete** /sessions | Disable my other sessions -*FrontendApi* | [**DisableMySession**](docs/FrontendApi.md#disablemysession) | **Delete** /sessions/{id} | Disable one of my sessions -*FrontendApi* | [**ExchangeSessionToken**](docs/FrontendApi.md#exchangesessiontoken) | **Get** /sessions/token-exchange | Exchange Session Token -*FrontendApi* | [**GetFlowError**](docs/FrontendApi.md#getflowerror) | **Get** /self-service/errors | Get User-Flow Errors -*FrontendApi* | [**GetLoginFlow**](docs/FrontendApi.md#getloginflow) | **Get** /self-service/login/flows | Get Login Flow -*FrontendApi* | [**GetRecoveryFlow**](docs/FrontendApi.md#getrecoveryflow) | **Get** /self-service/recovery/flows | Get Recovery Flow -*FrontendApi* | [**GetRegistrationFlow**](docs/FrontendApi.md#getregistrationflow) | **Get** /self-service/registration/flows | Get Registration Flow -*FrontendApi* | [**GetSettingsFlow**](docs/FrontendApi.md#getsettingsflow) | **Get** /self-service/settings/flows | Get Settings Flow -*FrontendApi* | [**GetVerificationFlow**](docs/FrontendApi.md#getverificationflow) | **Get** /self-service/verification/flows | Get Verification Flow -*FrontendApi* | [**GetWebAuthnJavaScript**](docs/FrontendApi.md#getwebauthnjavascript) | **Get** /.well-known/ory/webauthn.js | Get WebAuthn JavaScript -*FrontendApi* | [**ListMySessions**](docs/FrontendApi.md#listmysessions) | **Get** /sessions | Get My Active Sessions -*FrontendApi* | [**PerformNativeLogout**](docs/FrontendApi.md#performnativelogout) | **Delete** /self-service/logout/api | Perform Logout for Native Apps -*FrontendApi* | [**ToSession**](docs/FrontendApi.md#tosession) | **Get** /sessions/whoami | Check Who the Current HTTP Session Belongs To -*FrontendApi* | [**UpdateLoginFlow**](docs/FrontendApi.md#updateloginflow) | **Post** /self-service/login | Submit a Login Flow -*FrontendApi* | [**UpdateLogoutFlow**](docs/FrontendApi.md#updatelogoutflow) | **Get** /self-service/logout | Update Logout Flow -*FrontendApi* | [**UpdateRecoveryFlow**](docs/FrontendApi.md#updaterecoveryflow) | **Post** /self-service/recovery | Update Recovery Flow -*FrontendApi* | [**UpdateRegistrationFlow**](docs/FrontendApi.md#updateregistrationflow) | **Post** /self-service/registration | Update Registration Flow -*FrontendApi* | [**UpdateSettingsFlow**](docs/FrontendApi.md#updatesettingsflow) | **Post** /self-service/settings | Complete Settings Flow -*FrontendApi* | [**UpdateVerificationFlow**](docs/FrontendApi.md#updateverificationflow) | **Post** /self-service/verification | Complete Verification Flow -*IdentityApi* | [**BatchPatchIdentities**](docs/IdentityApi.md#batchpatchidentities) | **Patch** /admin/identities | Create multiple identities -*IdentityApi* | [**CreateIdentity**](docs/IdentityApi.md#createidentity) | **Post** /admin/identities | Create an Identity -*IdentityApi* | [**CreateRecoveryCodeForIdentity**](docs/IdentityApi.md#createrecoverycodeforidentity) | **Post** /admin/recovery/code | Create a Recovery Code -*IdentityApi* | [**CreateRecoveryLinkForIdentity**](docs/IdentityApi.md#createrecoverylinkforidentity) | **Post** /admin/recovery/link | Create a Recovery Link -*IdentityApi* | [**DeleteIdentity**](docs/IdentityApi.md#deleteidentity) | **Delete** /admin/identities/{id} | Delete an Identity -*IdentityApi* | [**DeleteIdentityCredentials**](docs/IdentityApi.md#deleteidentitycredentials) | **Delete** /admin/identities/{id}/credentials/{type} | Delete a credential for a specific identity -*IdentityApi* | [**DeleteIdentitySessions**](docs/IdentityApi.md#deleteidentitysessions) | **Delete** /admin/identities/{id}/sessions | Delete & Invalidate an Identity's Sessions -*IdentityApi* | [**DisableSession**](docs/IdentityApi.md#disablesession) | **Delete** /admin/sessions/{id} | Deactivate a Session -*IdentityApi* | [**ExtendSession**](docs/IdentityApi.md#extendsession) | **Patch** /admin/sessions/{id}/extend | Extend a Session -*IdentityApi* | [**GetIdentity**](docs/IdentityApi.md#getidentity) | **Get** /admin/identities/{id} | Get an Identity -*IdentityApi* | [**GetIdentitySchema**](docs/IdentityApi.md#getidentityschema) | **Get** /schemas/{id} | Get Identity JSON Schema -*IdentityApi* | [**GetSession**](docs/IdentityApi.md#getsession) | **Get** /admin/sessions/{id} | Get Session -*IdentityApi* | [**ListIdentities**](docs/IdentityApi.md#listidentities) | **Get** /admin/identities | List Identities -*IdentityApi* | [**ListIdentitySchemas**](docs/IdentityApi.md#listidentityschemas) | **Get** /schemas | Get all Identity Schemas -*IdentityApi* | [**ListIdentitySessions**](docs/IdentityApi.md#listidentitysessions) | **Get** /admin/identities/{id}/sessions | List an Identity's Sessions -*IdentityApi* | [**ListSessions**](docs/IdentityApi.md#listsessions) | **Get** /admin/sessions | List All Sessions -*IdentityApi* | [**PatchIdentity**](docs/IdentityApi.md#patchidentity) | **Patch** /admin/identities/{id} | Patch an Identity -*IdentityApi* | [**UpdateIdentity**](docs/IdentityApi.md#updateidentity) | **Put** /admin/identities/{id} | Update an Identity -*MetadataApi* | [**GetVersion**](docs/MetadataApi.md#getversion) | **Get** /version | Return Running Software Version. -*MetadataApi* | [**IsAlive**](docs/MetadataApi.md#isalive) | **Get** /health/alive | Check HTTP Server Status -*MetadataApi* | [**IsReady**](docs/MetadataApi.md#isready) | **Get** /health/ready | Check HTTP Server and Database Status - - -## Documentation For Models - - - [AuthenticatorAssuranceLevel](docs/AuthenticatorAssuranceLevel.md) - - [BatchPatchIdentitiesResponse](docs/BatchPatchIdentitiesResponse.md) - - [ConsistencyRequestParameters](docs/ConsistencyRequestParameters.md) - - [ContinueWith](docs/ContinueWith.md) - - [ContinueWithRecoveryUi](docs/ContinueWithRecoveryUi.md) - - [ContinueWithRecoveryUiFlow](docs/ContinueWithRecoveryUiFlow.md) - - [ContinueWithRedirectBrowserTo](docs/ContinueWithRedirectBrowserTo.md) - - [ContinueWithSetOrySessionToken](docs/ContinueWithSetOrySessionToken.md) - - [ContinueWithSettingsUi](docs/ContinueWithSettingsUi.md) - - [ContinueWithSettingsUiFlow](docs/ContinueWithSettingsUiFlow.md) - - [ContinueWithVerificationUi](docs/ContinueWithVerificationUi.md) - - [ContinueWithVerificationUiFlow](docs/ContinueWithVerificationUiFlow.md) - - [CourierMessageStatus](docs/CourierMessageStatus.md) - - [CourierMessageType](docs/CourierMessageType.md) - - [CreateIdentityBody](docs/CreateIdentityBody.md) - - [CreateRecoveryCodeForIdentityBody](docs/CreateRecoveryCodeForIdentityBody.md) - - [CreateRecoveryLinkForIdentityBody](docs/CreateRecoveryLinkForIdentityBody.md) - - [DeleteMySessionsCount](docs/DeleteMySessionsCount.md) - - [ErrorAuthenticatorAssuranceLevelNotSatisfied](docs/ErrorAuthenticatorAssuranceLevelNotSatisfied.md) - - [ErrorBrowserLocationChangeRequired](docs/ErrorBrowserLocationChangeRequired.md) - - [ErrorFlowReplaced](docs/ErrorFlowReplaced.md) - - [ErrorGeneric](docs/ErrorGeneric.md) - - [FlowError](docs/FlowError.md) - - [GenericError](docs/GenericError.md) - - [GetVersion200Response](docs/GetVersion200Response.md) - - [HealthNotReadyStatus](docs/HealthNotReadyStatus.md) - - [HealthStatus](docs/HealthStatus.md) - - [Identity](docs/Identity.md) - - [IdentityCredentials](docs/IdentityCredentials.md) - - [IdentityCredentialsCode](docs/IdentityCredentialsCode.md) - - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) - - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) - - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) - - [IdentityPatch](docs/IdentityPatch.md) - - [IdentityPatchResponse](docs/IdentityPatchResponse.md) - - [IdentitySchemaContainer](docs/IdentitySchemaContainer.md) - - [IdentityWithCredentials](docs/IdentityWithCredentials.md) - - [IdentityWithCredentialsOidc](docs/IdentityWithCredentialsOidc.md) - - [IdentityWithCredentialsOidcConfig](docs/IdentityWithCredentialsOidcConfig.md) - - [IdentityWithCredentialsOidcConfigProvider](docs/IdentityWithCredentialsOidcConfigProvider.md) - - [IdentityWithCredentialsPassword](docs/IdentityWithCredentialsPassword.md) - - [IdentityWithCredentialsPasswordConfig](docs/IdentityWithCredentialsPasswordConfig.md) - - [IsAlive200Response](docs/IsAlive200Response.md) - - [IsReady503Response](docs/IsReady503Response.md) - - [JsonPatch](docs/JsonPatch.md) - - [LoginFlow](docs/LoginFlow.md) - - [LoginFlowState](docs/LoginFlowState.md) - - [LogoutFlow](docs/LogoutFlow.md) - - [Message](docs/Message.md) - - [MessageDispatch](docs/MessageDispatch.md) - - [NeedsPrivilegedSessionError](docs/NeedsPrivilegedSessionError.md) - - [OAuth2Client](docs/OAuth2Client.md) - - [OAuth2ConsentRequestOpenIDConnectContext](docs/OAuth2ConsentRequestOpenIDConnectContext.md) - - [OAuth2LoginRequest](docs/OAuth2LoginRequest.md) - - [PatchIdentitiesBody](docs/PatchIdentitiesBody.md) - - [PerformNativeLogoutBody](docs/PerformNativeLogoutBody.md) - - [RecoveryCodeForIdentity](docs/RecoveryCodeForIdentity.md) - - [RecoveryFlow](docs/RecoveryFlow.md) - - [RecoveryFlowState](docs/RecoveryFlowState.md) - - [RecoveryIdentityAddress](docs/RecoveryIdentityAddress.md) - - [RecoveryLinkForIdentity](docs/RecoveryLinkForIdentity.md) - - [RegistrationFlow](docs/RegistrationFlow.md) - - [RegistrationFlowState](docs/RegistrationFlowState.md) - - [SelfServiceFlowExpiredError](docs/SelfServiceFlowExpiredError.md) - - [Session](docs/Session.md) - - [SessionAuthenticationMethod](docs/SessionAuthenticationMethod.md) - - [SessionDevice](docs/SessionDevice.md) - - [SettingsFlow](docs/SettingsFlow.md) - - [SettingsFlowState](docs/SettingsFlowState.md) - - [SuccessfulCodeExchangeResponse](docs/SuccessfulCodeExchangeResponse.md) - - [SuccessfulNativeLogin](docs/SuccessfulNativeLogin.md) - - [SuccessfulNativeRegistration](docs/SuccessfulNativeRegistration.md) - - [TokenPagination](docs/TokenPagination.md) - - [TokenPaginationHeaders](docs/TokenPaginationHeaders.md) - - [UiContainer](docs/UiContainer.md) - - [UiNode](docs/UiNode.md) - - [UiNodeAnchorAttributes](docs/UiNodeAnchorAttributes.md) - - [UiNodeAttributes](docs/UiNodeAttributes.md) - - [UiNodeImageAttributes](docs/UiNodeImageAttributes.md) - - [UiNodeInputAttributes](docs/UiNodeInputAttributes.md) - - [UiNodeMeta](docs/UiNodeMeta.md) - - [UiNodeScriptAttributes](docs/UiNodeScriptAttributes.md) - - [UiNodeTextAttributes](docs/UiNodeTextAttributes.md) - - [UiText](docs/UiText.md) - - [UpdateIdentityBody](docs/UpdateIdentityBody.md) - - [UpdateLoginFlowBody](docs/UpdateLoginFlowBody.md) - - [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md) - - [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md) - - [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md) - - [UpdateLoginFlowWithPasskeyMethod](docs/UpdateLoginFlowWithPasskeyMethod.md) - - [UpdateLoginFlowWithPasswordMethod](docs/UpdateLoginFlowWithPasswordMethod.md) - - [UpdateLoginFlowWithTotpMethod](docs/UpdateLoginFlowWithTotpMethod.md) - - [UpdateLoginFlowWithWebAuthnMethod](docs/UpdateLoginFlowWithWebAuthnMethod.md) - - [UpdateRecoveryFlowBody](docs/UpdateRecoveryFlowBody.md) - - [UpdateRecoveryFlowWithCodeMethod](docs/UpdateRecoveryFlowWithCodeMethod.md) - - [UpdateRecoveryFlowWithLinkMethod](docs/UpdateRecoveryFlowWithLinkMethod.md) - - [UpdateRegistrationFlowBody](docs/UpdateRegistrationFlowBody.md) - - [UpdateRegistrationFlowWithCodeMethod](docs/UpdateRegistrationFlowWithCodeMethod.md) - - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) - - [UpdateRegistrationFlowWithPasskeyMethod](docs/UpdateRegistrationFlowWithPasskeyMethod.md) - - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) - - [UpdateRegistrationFlowWithProfileMethod](docs/UpdateRegistrationFlowWithProfileMethod.md) - - [UpdateRegistrationFlowWithWebAuthnMethod](docs/UpdateRegistrationFlowWithWebAuthnMethod.md) - - [UpdateSettingsFlowBody](docs/UpdateSettingsFlowBody.md) - - [UpdateSettingsFlowWithLookupMethod](docs/UpdateSettingsFlowWithLookupMethod.md) - - [UpdateSettingsFlowWithOidcMethod](docs/UpdateSettingsFlowWithOidcMethod.md) - - [UpdateSettingsFlowWithPasskeyMethod](docs/UpdateSettingsFlowWithPasskeyMethod.md) - - [UpdateSettingsFlowWithPasswordMethod](docs/UpdateSettingsFlowWithPasswordMethod.md) - - [UpdateSettingsFlowWithProfileMethod](docs/UpdateSettingsFlowWithProfileMethod.md) - - [UpdateSettingsFlowWithTotpMethod](docs/UpdateSettingsFlowWithTotpMethod.md) - - [UpdateSettingsFlowWithWebAuthnMethod](docs/UpdateSettingsFlowWithWebAuthnMethod.md) - - [UpdateVerificationFlowBody](docs/UpdateVerificationFlowBody.md) - - [UpdateVerificationFlowWithCodeMethod](docs/UpdateVerificationFlowWithCodeMethod.md) - - [UpdateVerificationFlowWithLinkMethod](docs/UpdateVerificationFlowWithLinkMethod.md) - - [VerifiableIdentityAddress](docs/VerifiableIdentityAddress.md) - - [VerificationFlow](docs/VerificationFlow.md) - - [VerificationFlowState](docs/VerificationFlowState.md) - - [Version](docs/Version.md) - - -## Documentation For Authorization - - - -### oryAccessToken - -- **Type**: API key -- **API key parameter name**: Authorization -- **Location**: HTTP header - -Note, each API key must be added to a map of `map[string]APIKey` where the key is: Authorization and passed in as the auth context for each request. - - -## Documentation for Utility Methods - -Due to the fact that model structure members are all pointers, this package contains -a number of utility functions to easily obtain pointers to values of basic types. -Each of these functions takes a value of the given basic type and returns a pointer to it: - -* `PtrBool` -* `PtrInt` -* `PtrInt32` -* `PtrInt64` -* `PtrFloat` -* `PtrFloat32` -* `PtrFloat64` -* `PtrString` -* `PtrTime` - -## Author - -office@ory.sh - diff --git a/internal/httpclient/api_courier.go b/internal/httpclient/api_courier.go deleted file mode 100644 index 91bcc08025eb..000000000000 --- a/internal/httpclient/api_courier.go +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Ory Identities API - * - * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. - * - * API version: - * Contact: office@ory.sh - */ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package client - -import ( - "bytes" - "context" - "io" - "net/http" - "net/url" - "strings" -) - -// Linger please -var ( - _ context.Context -) - -type CourierApi interface { - - /* - * GetCourierMessage Get a Message - * Gets a specific messages by the given ID. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param id MessageID is the ID of the message. - * @return CourierApiApiGetCourierMessageRequest - */ - GetCourierMessage(ctx context.Context, id string) CourierApiApiGetCourierMessageRequest - - /* - * GetCourierMessageExecute executes the request - * @return Message - */ - GetCourierMessageExecute(r CourierApiApiGetCourierMessageRequest) (*Message, *http.Response, error) - - /* - * ListCourierMessages List Messages - * Lists all messages by given status and recipient. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return CourierApiApiListCourierMessagesRequest - */ - ListCourierMessages(ctx context.Context) CourierApiApiListCourierMessagesRequest - - /* - * ListCourierMessagesExecute executes the request - * @return []Message - */ - ListCourierMessagesExecute(r CourierApiApiListCourierMessagesRequest) ([]Message, *http.Response, error) -} - -// CourierApiService CourierApi service -type CourierApiService service - -type CourierApiApiGetCourierMessageRequest struct { - ctx context.Context - ApiService CourierApi - id string -} - -func (r CourierApiApiGetCourierMessageRequest) Execute() (*Message, *http.Response, error) { - return r.ApiService.GetCourierMessageExecute(r) -} - -/* - * GetCourierMessage Get a Message - * Gets a specific messages by the given ID. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param id MessageID is the ID of the message. - * @return CourierApiApiGetCourierMessageRequest - */ -func (a *CourierApiService) GetCourierMessage(ctx context.Context, id string) CourierApiApiGetCourierMessageRequest { - return CourierApiApiGetCourierMessageRequest{ - ApiService: a, - ctx: ctx, - id: id, - } -} - -/* - * Execute executes the request - * @return Message - */ -func (a *CourierApiService) GetCourierMessageExecute(r CourierApiApiGetCourierMessageRequest) (*Message, *http.Response, error) { - var ( - localVarHTTPMethod = http.MethodGet - localVarPostBody interface{} - localVarFormFileName string - localVarFileName string - localVarFileBytes []byte - localVarReturnValue *Message - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierApiService.GetCourierMessage") - if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/admin/courier/messages/{id}" - localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterToString(r.id, "")), -1) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - if r.ctx != nil { - // API Key Authentication - if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { - if apiKey, ok := auth["oryAccessToken"]; ok { - var key string - if apiKey.Prefix != "" { - key = apiKey.Prefix + " " + apiKey.Key - } else { - key = apiKey.Key - } - localVarHeaderParams["Authorization"] = key - } - } - } - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) - if err != nil { - return localVarReturnValue, nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - if localVarHTTPResponse.StatusCode == 400 { - var v ErrorGeneric - err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr - } - newErr.model = v - return localVarReturnValue, localVarHTTPResponse, newErr - } - var v ErrorGeneric - err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr - } - newErr.model = v - return localVarReturnValue, localVarHTTPResponse, newErr - } - - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - return localVarReturnValue, localVarHTTPResponse, nil -} - -type CourierApiApiListCourierMessagesRequest struct { - ctx context.Context - ApiService CourierApi - pageSize *int64 - pageToken *string - status *CourierMessageStatus - recipient *string -} - -func (r CourierApiApiListCourierMessagesRequest) PageSize(pageSize int64) CourierApiApiListCourierMessagesRequest { - r.pageSize = &pageSize - return r -} -func (r CourierApiApiListCourierMessagesRequest) PageToken(pageToken string) CourierApiApiListCourierMessagesRequest { - r.pageToken = &pageToken - return r -} -func (r CourierApiApiListCourierMessagesRequest) Status(status CourierMessageStatus) CourierApiApiListCourierMessagesRequest { - r.status = &status - return r -} -func (r CourierApiApiListCourierMessagesRequest) Recipient(recipient string) CourierApiApiListCourierMessagesRequest { - r.recipient = &recipient - return r -} - -func (r CourierApiApiListCourierMessagesRequest) Execute() ([]Message, *http.Response, error) { - return r.ApiService.ListCourierMessagesExecute(r) -} - -/* - * ListCourierMessages List Messages - * Lists all messages by given status and recipient. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return CourierApiApiListCourierMessagesRequest - */ -func (a *CourierApiService) ListCourierMessages(ctx context.Context) CourierApiApiListCourierMessagesRequest { - return CourierApiApiListCourierMessagesRequest{ - ApiService: a, - ctx: ctx, - } -} - -/* - * Execute executes the request - * @return []Message - */ -func (a *CourierApiService) ListCourierMessagesExecute(r CourierApiApiListCourierMessagesRequest) ([]Message, *http.Response, error) { - var ( - localVarHTTPMethod = http.MethodGet - localVarPostBody interface{} - localVarFormFileName string - localVarFileName string - localVarFileBytes []byte - localVarReturnValue []Message - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierApiService.ListCourierMessages") - if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/admin/courier/messages" - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - if r.pageSize != nil { - localVarQueryParams.Add("page_size", parameterToString(*r.pageSize, "")) - } - if r.pageToken != nil { - localVarQueryParams.Add("page_token", parameterToString(*r.pageToken, "")) - } - if r.status != nil { - localVarQueryParams.Add("status", parameterToString(*r.status, "")) - } - if r.recipient != nil { - localVarQueryParams.Add("recipient", parameterToString(*r.recipient, "")) - } - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - if r.ctx != nil { - // API Key Authentication - if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { - if apiKey, ok := auth["oryAccessToken"]; ok { - var key string - if apiKey.Prefix != "" { - key = apiKey.Prefix + " " + apiKey.Key - } else { - key = apiKey.Key - } - localVarHeaderParams["Authorization"] = key - } - } - } - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) - if err != nil { - return localVarReturnValue, nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - if localVarHTTPResponse.StatusCode == 400 { - var v ErrorGeneric - err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr - } - newErr.model = v - return localVarReturnValue, localVarHTTPResponse, newErr - } - var v ErrorGeneric - err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr - } - newErr.model = v - return localVarReturnValue, localVarHTTPResponse, newErr - } - - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - return localVarReturnValue, localVarHTTPResponse, nil -} diff --git a/internal/httpclient/api_frontend.go b/internal/httpclient/api_frontend.go deleted file mode 100644 index cfb87b55902a..000000000000 --- a/internal/httpclient/api_frontend.go +++ /dev/null @@ -1,5863 +0,0 @@ -/* - * Ory Identities API - * - * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. - * - * API version: - * Contact: office@ory.sh - */ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package client - -import ( - "bytes" - "context" - "io" - "net/http" - "net/url" - "strings" -) - -// Linger please -var ( - _ context.Context -) - -type FrontendApi interface { - - /* - * CreateBrowserLoginFlow Create Login Flow for Browsers - * This endpoint initializes a browser-based user login flow. This endpoint will set the appropriate - cookies and anti-CSRF measures required for browser-based flows. - - If this endpoint is opened as a link in the browser, it will be redirected to - `selfservice.flows.login.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session - exists already, the browser will be redirected to `urls.default_redirect_url` unless the query parameter - `?refresh=true` was set. - - If this endpoint is called via an AJAX request, the response contains the flow without a redirect. In the - case of an error, the `error.id` of the JSON response body can be one of: - - `session_already_available`: The user is already signed in. - `session_aal1_required`: Multi-factor auth (e.g. 2fa) was requested but the user has no session yet. - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - `security_identity_mismatch`: The requested `?return_to` address is not allowed to be used. Adjust this in the configuration! - - The optional query parameter login_challenge is set when using Kratos with - Hydra in an OAuth2 flow. See the oauth2_provider.url configuration - option. - - This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Firefox, ...) as cookies are needed. - - More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserLoginFlowRequest - */ - CreateBrowserLoginFlow(ctx context.Context) FrontendApiApiCreateBrowserLoginFlowRequest - - /* - * CreateBrowserLoginFlowExecute executes the request - * @return LoginFlow - */ - CreateBrowserLoginFlowExecute(r FrontendApiApiCreateBrowserLoginFlowRequest) (*LoginFlow, *http.Response, error) - - /* - * CreateBrowserLogoutFlow Create a Logout URL for Browsers - * This endpoint initializes a browser-based user logout flow and a URL which can be used to log out the user. - - This endpoint is NOT INTENDED for API clients and only works - with browsers (Chrome, Firefox, ...). For API clients you can - call the `/self-service/logout/api` URL directly with the Ory Session Token. - - The URL is only valid for the currently signed in user. If no user is signed in, this endpoint returns - a 401 error. - - When calling this endpoint from a backend, please ensure to properly forward the HTTP cookies. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserLogoutFlowRequest - */ - CreateBrowserLogoutFlow(ctx context.Context) FrontendApiApiCreateBrowserLogoutFlowRequest - - /* - * CreateBrowserLogoutFlowExecute executes the request - * @return LogoutFlow - */ - CreateBrowserLogoutFlowExecute(r FrontendApiApiCreateBrowserLogoutFlowRequest) (*LogoutFlow, *http.Response, error) - - /* - * CreateBrowserRecoveryFlow Create Recovery Flow for Browsers - * This endpoint initializes a browser-based account recovery flow. Once initialized, the browser will be redirected to - `selfservice.flows.recovery.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session - exists, the browser is returned to the configured return URL. - - If this endpoint is called via an AJAX request, the response contains the recovery flow without any redirects - or a 400 bad request error if the user is already authenticated. - - This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Firefox, ...) as cookies are needed. - - More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserRecoveryFlowRequest - */ - CreateBrowserRecoveryFlow(ctx context.Context) FrontendApiApiCreateBrowserRecoveryFlowRequest - - /* - * CreateBrowserRecoveryFlowExecute executes the request - * @return RecoveryFlow - */ - CreateBrowserRecoveryFlowExecute(r FrontendApiApiCreateBrowserRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) - - /* - * CreateBrowserRegistrationFlow Create Registration Flow for Browsers - * This endpoint initializes a browser-based user registration flow. This endpoint will set the appropriate - cookies and anti-CSRF measures required for browser-based flows. - - If this endpoint is opened as a link in the browser, it will be redirected to - `selfservice.flows.registration.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session - exists already, the browser will be redirected to `urls.default_redirect_url`. - - If this endpoint is called via an AJAX request, the response contains the flow without a redirect. In the - case of an error, the `error.id` of the JSON response body can be one of: - - `session_already_available`: The user is already signed in. - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - `security_identity_mismatch`: The requested `?return_to` address is not allowed to be used. Adjust this in the configuration! - - If this endpoint is called via an AJAX request, the response contains the registration flow without a redirect. - - This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Firefox, ...) as cookies are needed. - - More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserRegistrationFlowRequest - */ - CreateBrowserRegistrationFlow(ctx context.Context) FrontendApiApiCreateBrowserRegistrationFlowRequest - - /* - * CreateBrowserRegistrationFlowExecute executes the request - * @return RegistrationFlow - */ - CreateBrowserRegistrationFlowExecute(r FrontendApiApiCreateBrowserRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) - - /* - * CreateBrowserSettingsFlow Create Settings Flow for Browsers - * This endpoint initializes a browser-based user settings flow. Once initialized, the browser will be redirected to - `selfservice.flows.settings.ui_url` with the flow ID set as the query parameter `?flow=`. If no valid - Ory Kratos Session Cookie is included in the request, a login flow will be initialized. - - If this endpoint is opened as a link in the browser, it will be redirected to - `selfservice.flows.settings.ui_url` with the flow ID set as the query parameter `?flow=`. If no valid user session - was set, the browser will be redirected to the login endpoint. - - If this endpoint is called via an AJAX request, the response contains the settings flow without any redirects - or a 401 forbidden error if no valid session was set. - - Depending on your configuration this endpoint might return a 403 error if the session has a lower Authenticator - Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn - credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user - to sign in with the second factor (happens automatically for server-side browser flows) or change the configuration. - - If this endpoint is called via an AJAX request, the response contains the flow without a redirect. In the - case of an error, the `error.id` of the JSON response body can be one of: - - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - `session_inactive`: No Ory Session was found - sign in a user first. - `security_identity_mismatch`: The requested `?return_to` address is not allowed to be used. Adjust this in the configuration! - - This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Firefox, ...) as cookies are needed. - - More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserSettingsFlowRequest - */ - CreateBrowserSettingsFlow(ctx context.Context) FrontendApiApiCreateBrowserSettingsFlowRequest - - /* - * CreateBrowserSettingsFlowExecute executes the request - * @return SettingsFlow - */ - CreateBrowserSettingsFlowExecute(r FrontendApiApiCreateBrowserSettingsFlowRequest) (*SettingsFlow, *http.Response, error) - - /* - * CreateBrowserVerificationFlow Create Verification Flow for Browser Clients - * This endpoint initializes a browser-based account verification flow. Once initialized, the browser will be redirected to - `selfservice.flows.verification.ui_url` with the flow ID set as the query parameter `?flow=`. - - If this endpoint is called via an AJAX request, the response contains the recovery flow without any redirects. - - This endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...). - - More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserVerificationFlowRequest - */ - CreateBrowserVerificationFlow(ctx context.Context) FrontendApiApiCreateBrowserVerificationFlowRequest - - /* - * CreateBrowserVerificationFlowExecute executes the request - * @return VerificationFlow - */ - CreateBrowserVerificationFlowExecute(r FrontendApiApiCreateBrowserVerificationFlowRequest) (*VerificationFlow, *http.Response, error) - - /* - * CreateNativeLoginFlow Create Login Flow for Native Apps - * This endpoint initiates a login flow for native apps that do not use a browser, such as mobile devices, smart TVs, and so on. - - If a valid provided session cookie or session token is provided, a 400 Bad Request error - will be returned unless the URL query parameter `?refresh=true` is set. - - To fetch an existing login flow call `/self-service/login/flows?flow=`. - - You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server - Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make - you vulnerable to a variety of CSRF attacks, including CSRF login attacks. - - In the case of an error, the `error.id` of the JSON response body can be one of: - - `session_already_available`: The user is already signed in. - `session_aal1_required`: Multi-factor auth (e.g. 2fa) was requested but the user has no session yet. - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - - This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). - - More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeLoginFlowRequest - */ - CreateNativeLoginFlow(ctx context.Context) FrontendApiApiCreateNativeLoginFlowRequest - - /* - * CreateNativeLoginFlowExecute executes the request - * @return LoginFlow - */ - CreateNativeLoginFlowExecute(r FrontendApiApiCreateNativeLoginFlowRequest) (*LoginFlow, *http.Response, error) - - /* - * CreateNativeRecoveryFlow Create Recovery Flow for Native Apps - * This endpoint initiates a recovery flow for API clients such as mobile devices, smart TVs, and so on. - - If a valid provided session cookie or session token is provided, a 400 Bad Request error. - - On an existing recovery flow, use the `getRecoveryFlow` API endpoint. - - You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server - Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make - you vulnerable to a variety of CSRF attacks. - - This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). - - More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeRecoveryFlowRequest - */ - CreateNativeRecoveryFlow(ctx context.Context) FrontendApiApiCreateNativeRecoveryFlowRequest - - /* - * CreateNativeRecoveryFlowExecute executes the request - * @return RecoveryFlow - */ - CreateNativeRecoveryFlowExecute(r FrontendApiApiCreateNativeRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) - - /* - * CreateNativeRegistrationFlow Create Registration Flow for Native Apps - * This endpoint initiates a registration flow for API clients such as mobile devices, smart TVs, and so on. - - If a valid provided session cookie or session token is provided, a 400 Bad Request error - will be returned unless the URL query parameter `?refresh=true` is set. - - To fetch an existing registration flow call `/self-service/registration/flows?flow=`. - - You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server - Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make - you vulnerable to a variety of CSRF attacks. - - In the case of an error, the `error.id` of the JSON response body can be one of: - - `session_already_available`: The user is already signed in. - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - - This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). - - More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeRegistrationFlowRequest - */ - CreateNativeRegistrationFlow(ctx context.Context) FrontendApiApiCreateNativeRegistrationFlowRequest - - /* - * CreateNativeRegistrationFlowExecute executes the request - * @return RegistrationFlow - */ - CreateNativeRegistrationFlowExecute(r FrontendApiApiCreateNativeRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) - - /* - * CreateNativeSettingsFlow Create Settings Flow for Native Apps - * This endpoint initiates a settings flow for API clients such as mobile devices, smart TVs, and so on. - You must provide a valid Ory Kratos Session Token for this endpoint to respond with HTTP 200 OK. - - To fetch an existing settings flow call `/self-service/settings/flows?flow=`. - - You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server - Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make - you vulnerable to a variety of CSRF attacks. - - Depending on your configuration this endpoint might return a 403 error if the session has a lower Authenticator - Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn - credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user - to sign in with the second factor or change the configuration. - - In the case of an error, the `error.id` of the JSON response body can be one of: - - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - `session_inactive`: No Ory Session was found - sign in a user first. - - This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). - - More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeSettingsFlowRequest - */ - CreateNativeSettingsFlow(ctx context.Context) FrontendApiApiCreateNativeSettingsFlowRequest - - /* - * CreateNativeSettingsFlowExecute executes the request - * @return SettingsFlow - */ - CreateNativeSettingsFlowExecute(r FrontendApiApiCreateNativeSettingsFlowRequest) (*SettingsFlow, *http.Response, error) - - /* - * CreateNativeVerificationFlow Create Verification Flow for Native Apps - * This endpoint initiates a verification flow for API clients such as mobile devices, smart TVs, and so on. - - To fetch an existing verification flow call `/self-service/verification/flows?flow=`. - - You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server - Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make - you vulnerable to a variety of CSRF attacks. - - This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). - - More information can be found at [Ory Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeVerificationFlowRequest - */ - CreateNativeVerificationFlow(ctx context.Context) FrontendApiApiCreateNativeVerificationFlowRequest - - /* - * CreateNativeVerificationFlowExecute executes the request - * @return VerificationFlow - */ - CreateNativeVerificationFlowExecute(r FrontendApiApiCreateNativeVerificationFlowRequest) (*VerificationFlow, *http.Response, error) - - /* - * DisableMyOtherSessions Disable my other sessions - * Calling this endpoint invalidates all except the current session that belong to the logged-in user. - Session data are not deleted. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiDisableMyOtherSessionsRequest - */ - DisableMyOtherSessions(ctx context.Context) FrontendApiApiDisableMyOtherSessionsRequest - - /* - * DisableMyOtherSessionsExecute executes the request - * @return DeleteMySessionsCount - */ - DisableMyOtherSessionsExecute(r FrontendApiApiDisableMyOtherSessionsRequest) (*DeleteMySessionsCount, *http.Response, error) - - /* - * DisableMySession Disable one of my sessions - * Calling this endpoint invalidates the specified session. The current session cannot be revoked. - Session data are not deleted. - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @param id ID is the session's ID. - * @return FrontendApiApiDisableMySessionRequest - */ - DisableMySession(ctx context.Context, id string) FrontendApiApiDisableMySessionRequest - - /* - * DisableMySessionExecute executes the request - */ - DisableMySessionExecute(r FrontendApiApiDisableMySessionRequest) (*http.Response, error) - - /* - * ExchangeSessionToken Exchange Session Token - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiExchangeSessionTokenRequest - */ - ExchangeSessionToken(ctx context.Context) FrontendApiApiExchangeSessionTokenRequest - - /* - * ExchangeSessionTokenExecute executes the request - * @return SuccessfulNativeLogin - */ - ExchangeSessionTokenExecute(r FrontendApiApiExchangeSessionTokenRequest) (*SuccessfulNativeLogin, *http.Response, error) - - /* - * GetFlowError Get User-Flow Errors - * This endpoint returns the error associated with a user-facing self service errors. - - This endpoint supports stub values to help you implement the error UI: - - `?id=stub:500` - returns a stub 500 (Internal Server Error) error. - - More information can be found at [Ory Kratos User User Facing Error Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetFlowErrorRequest - */ - GetFlowError(ctx context.Context) FrontendApiApiGetFlowErrorRequest - - /* - * GetFlowErrorExecute executes the request - * @return FlowError - */ - GetFlowErrorExecute(r FrontendApiApiGetFlowErrorRequest) (*FlowError, *http.Response, error) - - /* - * GetLoginFlow Get Login Flow - * This endpoint returns a login flow's context with, for example, error details and other information. - - Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header. - For AJAX requests you must ensure that cookies are included in the request or requests will fail. - - If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain - and you need to forward the incoming HTTP Cookie header to this endpoint: - - ```js - pseudo-code example - router.get('/login', async function (req, res) { - const flow = await client.getLoginFlow(req.header('cookie'), req.query['flow']) - - res.render('login', flow) - }) - ``` - - This request may fail due to several reasons. The `error.id` can be one of: - - `session_already_available`: The user is already signed in. - `self_service_flow_expired`: The flow is expired and you should request a new one. - - More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetLoginFlowRequest - */ - GetLoginFlow(ctx context.Context) FrontendApiApiGetLoginFlowRequest - - /* - * GetLoginFlowExecute executes the request - * @return LoginFlow - */ - GetLoginFlowExecute(r FrontendApiApiGetLoginFlowRequest) (*LoginFlow, *http.Response, error) - - /* - * GetRecoveryFlow Get Recovery Flow - * This endpoint returns a recovery flow's context with, for example, error details and other information. - - Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header. - For AJAX requests you must ensure that cookies are included in the request or requests will fail. - - If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain - and you need to forward the incoming HTTP Cookie header to this endpoint: - - ```js - pseudo-code example - router.get('/recovery', async function (req, res) { - const flow = await client.getRecoveryFlow(req.header('Cookie'), req.query['flow']) - - res.render('recovery', flow) - }) - ``` - - More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetRecoveryFlowRequest - */ - GetRecoveryFlow(ctx context.Context) FrontendApiApiGetRecoveryFlowRequest - - /* - * GetRecoveryFlowExecute executes the request - * @return RecoveryFlow - */ - GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) - - /* - * GetRegistrationFlow Get Registration Flow - * This endpoint returns a registration flow's context with, for example, error details and other information. - - Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header. - For AJAX requests you must ensure that cookies are included in the request or requests will fail. - - If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain - and you need to forward the incoming HTTP Cookie header to this endpoint: - - ```js - pseudo-code example - router.get('/registration', async function (req, res) { - const flow = await client.getRegistrationFlow(req.header('cookie'), req.query['flow']) - - res.render('registration', flow) - }) - ``` - - This request may fail due to several reasons. The `error.id` can be one of: - - `session_already_available`: The user is already signed in. - `self_service_flow_expired`: The flow is expired and you should request a new one. - - More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetRegistrationFlowRequest - */ - GetRegistrationFlow(ctx context.Context) FrontendApiApiGetRegistrationFlowRequest - - /* - * GetRegistrationFlowExecute executes the request - * @return RegistrationFlow - */ - GetRegistrationFlowExecute(r FrontendApiApiGetRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) - - /* - * GetSettingsFlow Get Settings Flow - * When accessing this endpoint through Ory Kratos' Public API you must ensure that either the Ory Kratos Session Cookie - or the Ory Kratos Session Token are set. - - Depending on your configuration this endpoint might return a 403 error if the session has a lower Authenticator - Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn - credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user - to sign in with the second factor or change the configuration. - - You can access this endpoint without credentials when using Ory Kratos' Admin API. - - If this endpoint is called via an AJAX request, the response contains the flow without a redirect. In the - case of an error, the `error.id` of the JSON response body can be one of: - - `security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred. - `session_inactive`: No Ory Session was found - sign in a user first. - `security_identity_mismatch`: The flow was interrupted with `session_refresh_required` but apparently some other - identity logged in instead. - - More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetSettingsFlowRequest - */ - GetSettingsFlow(ctx context.Context) FrontendApiApiGetSettingsFlowRequest - - /* - * GetSettingsFlowExecute executes the request - * @return SettingsFlow - */ - GetSettingsFlowExecute(r FrontendApiApiGetSettingsFlowRequest) (*SettingsFlow, *http.Response, error) - - /* - * GetVerificationFlow Get Verification Flow - * This endpoint returns a verification flow's context with, for example, error details and other information. - - Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header. - For AJAX requests you must ensure that cookies are included in the request or requests will fail. - - If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain - and you need to forward the incoming HTTP Cookie header to this endpoint: - - ```js - pseudo-code example - router.get('/recovery', async function (req, res) { - const flow = await client.getVerificationFlow(req.header('cookie'), req.query['flow']) - - res.render('verification', flow) - }) - ``` - - More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetVerificationFlowRequest - */ - GetVerificationFlow(ctx context.Context) FrontendApiApiGetVerificationFlowRequest - - /* - * GetVerificationFlowExecute executes the request - * @return VerificationFlow - */ - GetVerificationFlowExecute(r FrontendApiApiGetVerificationFlowRequest) (*VerificationFlow, *http.Response, error) - - /* - * GetWebAuthnJavaScript Get WebAuthn JavaScript - * This endpoint provides JavaScript which is needed in order to perform WebAuthn login and registration. - - If you are building a JavaScript Browser App (e.g. in ReactJS or AngularJS) you will need to load this file: - - ```html -