diff --git a/driver/config/config.go b/driver/config/config.go index 6ec50eb761da..a2928858dcb2 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -224,8 +224,9 @@ type ( Config json.RawMessage `json:"config"` } SelfServiceStrategy struct { - Enabled bool `json:"enabled"` - Config json.RawMessage `json:"config"` + Enabled bool `json:"enabled"` + Config json.RawMessage `json:"config"` + AllowedFlows []string `json:"allowed_flows"` } Schema struct { ID string `json:"id" koanf:"id"` @@ -727,10 +728,13 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self config = c } - enabledKey := fmt.Sprintf("%s.%s.enabled", ViperKeySelfServiceStrategyConfig, strategy) + basePath := fmt.Sprintf("%s.%s", ViperKeySelfServiceStrategyConfig, strategy) + + enabledKey := fmt.Sprintf("%s.enabled", basePath) s := &SelfServiceStrategy{ - Enabled: pp.Bool(enabledKey), - Config: json.RawMessage(config), + Enabled: pp.Bool(enabledKey), + Config: json.RawMessage(config), + AllowedFlows: pp.Strings(fmt.Sprintf("%s.allowed_flows", basePath)), } // The default value can easily be overwritten by setting e.g. `{"selfservice": "null"}` which means that diff --git a/driver/registry_default.go b/driver/registry_default.go index 1d553e5df922..3b059c7e29fc 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -96,11 +96,12 @@ type RegistryDefault struct { persister persistence.Persister migrationStatus popx.MigrationStatuses - hookVerifier *hook.Verifier - hookSessionIssuer *hook.SessionIssuer - hookSessionDestroyer *hook.SessionDestroyer - hookAddressVerifier *hook.AddressVerifier - hookShowVerificationUI *hook.ShowVerificationUIHook + hookVerifier *hook.Verifier + hookSessionIssuer *hook.SessionIssuer + hookSessionDestroyer *hook.SessionDestroyer + hookAddressVerifier *hook.AddressVerifier + hookShowVerificationUI *hook.ShowVerificationUIHook + hookCodeAddressVerifier *hook.CodeAddressVerifier identityHandler *identity.Handler identityValidator *identity.Validator @@ -329,6 +330,12 @@ func (m *RegistryDefault) selfServiceStrategies() []interface{} { func (m *RegistryDefault) RegistrationStrategies(ctx context.Context) (registrationStrategies registration.Strategies) { for _, strategy := range m.selfServiceStrategies() { if s, ok := strategy.(registration.Strategy); ok { + // the code method needs to be checked explicitly for registration + // TODO: we need to somehow check if the `code` strategy is enabled specifically for registration + // if s.ID() == identity.CredentialsTypeCodeAuth && m.Config().SelfServiceStrategy(ctx, string(s.ID())).RegistrationEnabled { + // registrationStrategies = append(registrationStrategies, s) + // continue + // } if m.Config().SelfServiceStrategy(ctx, string(s.ID())).Enabled { registrationStrategies = append(registrationStrategies, s) } @@ -351,6 +358,12 @@ func (m *RegistryDefault) AllRegistrationStrategies() registration.Strategies { func (m *RegistryDefault) LoginStrategies(ctx context.Context) (loginStrategies login.Strategies) { for _, strategy := range m.selfServiceStrategies() { if s, ok := strategy.(login.Strategy); ok { + // the code method needs to be checked explicity for login + // TODO: we need to somwhow check if the `code` strategy is enabled specifically for login + // if s.ID() == identity.CredentialsTypeCodeAuth && m.Config().SelfServiceStrategy(ctx, string(s.ID())).LoginEnabled { + // loginStrategies = append(loginStrategies, s) + // continue + // } if m.Config().SelfServiceStrategy(ctx, string(s.ID())).Enabled { loginStrategies = append(loginStrategies, s) } diff --git a/driver/registry_default_hooks.go b/driver/registry_default_hooks.go index 6efffc05a777..c3f809d2144e 100644 --- a/driver/registry_default_hooks.go +++ b/driver/registry_default_hooks.go @@ -15,6 +15,13 @@ func (m *RegistryDefault) HookVerifier() *hook.Verifier { return m.hookVerifier } +func (m *RegistryDefault) HookCodeAddressVerifier() *hook.CodeAddressVerifier { + if m.hookCodeAddressVerifier == nil { + m.hookCodeAddressVerifier = hook.NewCodeAddressVerifier(m) + } + return m.hookCodeAddressVerifier +} + func (m *RegistryDefault) HookSessionIssuer() *hook.SessionIssuer { if m.hookSessionIssuer == nil { m.hookSessionIssuer = hook.NewSessionIssuer(m) diff --git a/driver/registry_default_registration.go b/driver/registry_default_registration.go index 7f78517891f0..306076795824 100644 --- a/driver/registry_default_registration.go +++ b/driver/registry_default_registration.go @@ -28,6 +28,12 @@ func (m *RegistryDefault) PostRegistrationPostPersistHooks(ctx context.Context, initialHookCount = 1 } + // TODO: this needs to be specific to the flow and not just the `code` general strategy + if m.Config().SelfServiceStrategy(ctx, identity.CredentialsTypeCodeAuth.String()).Enabled { + b = append(b, m.HookCodeAddressVerifier()) + initialHookCount += 1 + } + for _, v := range m.getHooks(string(credentialsType), m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, string(credentialsType))) { if hook, ok := v.(registration.PostHookPostPersistExecutor); ok { b = append(b, hook) diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 4476c39d2d18..bded3ad9c74d 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"] }, "selfServiceShowVerificationUIHook": { "type": "object", @@ -92,9 +83,7 @@ } }, "additionalProperties": false, - "required": [ - "hook" - ] + "required": ["hook"] }, "webHookAuthBasicAuthProperties": { "properties": { @@ -114,17 +103,11 @@ } }, "additionalProperties": false, - "required": [ - "user", - "password" - ] + "required": ["user", "password"] } }, "additionalProperties": false, - "required": [ - "type", - "config" - ] + "required": ["type", "config"] }, "httpRequestConfig": { "type": "object", @@ -132,9 +115,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?://" }, @@ -199,25 +180,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", @@ -256,10 +227,7 @@ "const": true } }, - "required": [ - "ignore", - "parse" - ] + "required": ["ignore", "parse"] } }, "url": { @@ -320,46 +288,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", @@ -392,9 +344,7 @@ "essential": true }, "acr": { - "values": [ - "urn:mace:incommon:iap:silver" - ] + "values": ["urn:mace:incommon:iap:silver"] } } } @@ -442,9 +392,7 @@ "properties": { "id": { "type": "string", - "examples": [ - "google" - ] + "examples": ["google"] }, "provider": { "title": "Provider", @@ -471,9 +419,7 @@ "linkedin", "lark" ], - "examples": [ - "google" - ] + "examples": ["google"] }, "label": { "title": "Optional string which will be used when generating labels for UI buttons.", @@ -488,23 +434,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", @@ -521,10 +461,7 @@ "type": "array", "items": { "type": "string", - "examples": [ - "offline_access", - "profile" - ] + "examples": ["offline_access", "profile"] } }, "microsoft_tenant": { @@ -543,30 +480,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", @@ -581,12 +509,7 @@ } }, "additionalProperties": false, - "required": [ - "id", - "provider", - "client_id", - "mapper_url" - ], + "required": ["id", "provider", "client_id", "mapper_url"], "allOf": [ { "if": { @@ -595,23 +518,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"] } } }, @@ -622,9 +539,7 @@ "const": "apple" } }, - "required": [ - "provider" - ] + "required": ["provider"] }, "then": { "not": { @@ -634,9 +549,7 @@ "minLength": 1 } }, - "required": [ - "client_secret" - ] + "required": ["client_secret"] }, "required": [ "apple_private_key_id", @@ -645,9 +558,7 @@ ] }, "else": { - "required": [ - "client_secret" - ], + "required": ["client_secret"], "allOf": [ { "not": { @@ -657,9 +568,7 @@ "minLength": 1 } }, - "required": [ - "apple_team_id" - ] + "required": ["apple_team_id"] } }, { @@ -670,9 +579,7 @@ "minLength": 1 } }, - "required": [ - "apple_private_key_id" - ] + "required": ["apple_private_key_id"] } }, { @@ -683,9 +590,7 @@ "minLength": 1 } }, - "required": [ - "apple_private_key" - ] + "required": ["apple_private_key"] } } ] @@ -826,10 +731,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": { @@ -986,9 +888,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", @@ -1036,9 +936,7 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": [ - "email" - ] + "required": ["email"] }, "valid": { "additionalProperties": false, @@ -1048,9 +946,7 @@ "$ref": "#/definitions/emailCourierTemplate" } }, - "required": [ - "email" - ] + "required": ["email"] } } }, @@ -1100,9 +996,7 @@ "selfservice": { "type": "object", "additionalProperties": false, - "required": [ - "default_browser_return_url" - ], + "required": ["default_browser_return_url"], "properties": { "default_browser_return_url": { "$ref": "#/definitions/defaultReturnTo" @@ -1136,30 +1030,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" @@ -1202,20 +1086,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" @@ -1234,20 +1112,14 @@ "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"] }, "before": { "$ref": "#/definitions/selfServiceBeforeLogin" @@ -1273,9 +1145,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": { @@ -1287,11 +1157,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" @@ -1300,10 +1166,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": { @@ -1330,9 +1193,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": { @@ -1344,11 +1205,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" @@ -1357,10 +1214,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": { @@ -1380,9 +1234,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" } } @@ -1421,20 +1273,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"] } } } @@ -1459,11 +1305,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] } } } @@ -1581,44 +1423,33 @@ }, "rp": { "title": "Relying Party (RP) Config", - "required": [ - "id", - "display_name" - ], + "required": ["id", "display_name"], "properties": { "display_name": { "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", "title": "Relying Party Origin", "description": "An explicit RP origin. If left empty, this defaults to `id`.", "format": "uri", - "examples": [ - "https://www.ory.sh/login" - ] + "examples": ["https://www.ory.sh/login"] }, "icon": { "type": "string", "title": "Relying Party Icon", "description": "An icon to help the user identify this RP.", "format": "uri", - "examples": [ - "https://www.ory.sh/an-icon.png" - ] + "examples": ["https://www.ory.sh/an-icon.png"] } }, "type": "object" @@ -1633,14 +1464,10 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] }, "then": { - "required": [ - "config" - ] + "required": ["config"] } }, "oidc": { @@ -1663,9 +1490,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", @@ -1764,18 +1589,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] }, "delivery_strategy": { "title": "Delivery Strategy", @@ -1838,9 +1658,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", @@ -1864,9 +1682,7 @@ "default": "localhost" } }, - "required": [ - "connection_uri" - ], + "required": ["connection_uri"], "additionalProperties": false }, "sms": { @@ -1891,9 +1707,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?:\\/\\/.*" }, @@ -1935,19 +1749,14 @@ }, "additionalProperties": false }, - "required": [ - "url", - "method" - ], + "required": ["url", "method"], "additionalProperties": false } }, "additionalProperties": false } }, - "required": [ - "smtp" - ], + "required": ["smtp"], "additionalProperties": false }, "oauth2_provider": { @@ -1978,10 +1787,10 @@ ] }, "override_return_to": { - "title":"Persist OAuth2 request between flows", - "type":"boolean", - "default":false, - "description":"Override the return_to query parameter with the OAuth2 provider request URL when perfoming an OAuth2 login flow." + "title": "Persist OAuth2 request between flows", + "type": "boolean", + "default": false, + "description": "Override the return_to query parameter with the OAuth2 provider request URL when perfoming an OAuth2 login flow." } }, "additionalProperties": false @@ -2009,9 +1818,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", @@ -2025,9 +1832,7 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [ - 4434 - ], + "examples": [4434], "default": 4434 }, "socket": { @@ -2086,9 +1891,7 @@ ] }, "uniqueItems": true, - "default": [ - "*" - ], + "default": ["*"], "examples": [ [ "https://example.com", @@ -2100,13 +1903,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": [ @@ -2137,9 +1934,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" } @@ -2182,9 +1977,7 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [ - 4433 - ], + "examples": [4433], "default": 4433 }, "socket": { @@ -2234,10 +2027,7 @@ "format": { "description": "The log format can either be text or JSON.", "type": "string", - "enum": [ - "json", - "text" - ] + "enum": ["json", "text"] } }, "additionalProperties": false @@ -2278,9 +2068,7 @@ "id": { "title": "The schema's ID.", "type": "string", - "examples": [ - "employee" - ] + "examples": ["employee"] }, "url": { "type": "string", @@ -2294,16 +2082,11 @@ ] } }, - "required": [ - "id", - "url" - ] + "required": ["id", "url"] } } }, - "required": [ - "schemas" - ], + "required": ["schemas"], "additionalProperties": false }, "secrets": { @@ -2352,10 +2135,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.", @@ -2411,9 +2191,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", @@ -2435,11 +2213,7 @@ "description": "One of the values: noop, aes, xchacha20-poly1305", "type": "string", "default": "noop", - "enum": [ - "noop", - "aes", - "xchacha20-poly1305" - ] + "enum": ["noop", "aes", "xchacha20-poly1305"] } } }, @@ -2463,11 +2237,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" } }, @@ -2494,11 +2264,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", @@ -2529,11 +2295,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 @@ -2544,11 +2306,7 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "24h", - "examples": [ - "1h", - "1m", - "1s" - ] + "examples": ["1h", "1m", "1s"] } } }, @@ -2557,9 +2315,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" @@ -2583,9 +2339,7 @@ "type": "integer", "minimum": 0, "maximum": 65535, - "examples": [ - 4434 - ], + "examples": [4434], "default": 0 }, "config": { @@ -2654,14 +2408,10 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] } }, - "required": [ - "verification" - ] + "required": ["verification"] }, { "properties": { @@ -2671,31 +2421,21 @@ "const": true } }, - "required": [ - "enabled" - ] + "required": ["enabled"] } }, - "required": [ - "recovery" - ] + "required": ["recovery"] } ] } }, - "required": [ - "flows" - ] + "required": ["flows"] } }, - "required": [ - "selfservice" - ] + "required": ["selfservice"] }, "then": { - "required": [ - "courier" - ] + "required": ["courier"] } }, { @@ -2714,33 +2454,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/persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.mysql.up.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.mysql.up.sql index 7179870fe63a..93792bdf4f2d 100644 --- a/persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.mysql.up.sql +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.mysql.up.sql @@ -2,6 +2,7 @@ CREATE TABLE identity_registration_codes ( id CHAR(36) NOT NULL PRIMARY KEY, code VARCHAR (64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, used_at timestamp NULL DEFAULT NULL, expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', diff --git a/persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.up.sql b/persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.up.sql index 88802070d4c5..b036f71f7a24 100644 --- a/persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.up.sql +++ b/persistence/sql/migrations/sql/20230707133700000000_identity_registration_code.up.sql @@ -2,6 +2,7 @@ CREATE TABLE identity_registration_codes ( id UUID NOT NULL PRIMARY KEY, code VARCHAR (64) NOT NULL, -- HMACed value of the actual code + address VARCHAR(255) NOT NULL, used_at timestamp NULL DEFAULT NULL, expires_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', issued_at timestamp NOT NULL DEFAULT '2000-01-01 00:00:00', diff --git a/persistence/sql/persister_registration.go b/persistence/sql/persister_registration.go index 68a03562f6a4..94aaabbdfbd8 100644 --- a/persistence/sql/persister_registration.go +++ b/persistence/sql/persister_registration.go @@ -181,3 +181,14 @@ func (p *Persister) DeleteRegistrationCodesOfFlow(ctx context.Context, flowID uu //#nosec G201 -- TableName is static return p.GetConnection(ctx).RawQuery(fmt.Sprintf("DELETE FROM %s WHERE selfservice_registration_flow_id = ? AND nid = ?", new(code.RegistrationCode).TableName(ctx)), flowID, p.NetworkID(ctx)).Exec() } + +func (p *Persister) GetUsedRegistrationCode(ctx context.Context, flowID uuid.UUID) (*code.RegistrationCode, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUsedRegistrationCode") + defer span.End() + + var registrationCode code.RegistrationCode + if err := p.Connection(ctx).RawQuery(fmt.Sprintf("SELECT * FROM %s WHERE selfservice_registration_flow_id = ? AND used_at IS NOT NULL AND nid = ?", new(code.RegistrationCode).TableName(ctx)), flowID, p.NetworkID(ctx)).First(®istrationCode); err != nil { + return nil, sqlcon.HandleError(err) + } + return ®istrationCode, nil +} diff --git a/selfservice/flow/request.go b/selfservice/flow/request.go index af1b31968caa..beca893e7534 100644 --- a/selfservice/flow/request.go +++ b/selfservice/flow/request.go @@ -25,6 +25,7 @@ var methodSchema []byte var ErrOriginHeaderNeedsBrowserFlow = herodot.ErrBadRequest. WithReasonf(`The HTTP Request Header included the "Origin" key, indicating that this request was made as part of an AJAX request in a Browser. The flow however was initiated as an API request. To prevent potential misuse and mitigate several attack vectors including CSRF, the request has been blocked. Please consult the documentation.`) + var ErrCookieHeaderNeedsBrowserFlow = herodot.ErrBadRequest. WithReasonf(`The HTTP Request Header included the "Cookie" key, indicating that this request was made by a Browser. The flow however was initiated as an API request. To prevent potential misuse and mitigate several attack vectors including CSRF, the request has been blocked. Please consult the documentation.`) @@ -78,7 +79,8 @@ var dec = decoderx.NewHTTP() func MethodEnabledAndAllowedFromRequest(r *http.Request, expected string, d interface { config.Provider -}) error { +}, +) error { var method struct { Method string `json:"method" form:"method"` } @@ -99,16 +101,38 @@ func MethodEnabledAndAllowedFromRequest(r *http.Request, expected string, d inte return MethodEnabledAndAllowed(r.Context(), expected, method.Method, d) } +// TODO: to disable specific flows we need to pass down the flow somehow to this method +// we could do this by adding an additional parameter, but not all methods have access to the flow +// this adds a lot of refactoring work, so we should think about a better way to do this func MethodEnabledAndAllowed(ctx context.Context, expected, actual string, d interface { config.Provider -}) error { +}, +) error { if actual != expected { return errors.WithStack(ErrStrategyNotResponsible) } - if !d.Config().SelfServiceStrategy(ctx, expected).Enabled { - return errors.WithStack(herodot.ErrNotFound.WithReason(strategy.EndpointDisabledMessage)) + stratConf := d.Config().SelfServiceStrategy(ctx, expected) + + err := herodot.ErrNotFound.WithReason(strategy.EndpointDisabledMessage) + + if stratConf.Enabled { + return nil } - return nil + // TODO: Implement a way to disable specific flows for this strategy + // For example, with Code strategy we might only want to allow login flows or recovery flows. + + // if len(stratConf.AllowedFlows) == 0 { + // return nil + // } + // + // // must match one of the allowed flows since this method is enabled + // for _, s := range stratConf.AllowedFlows { + // if strings.EqualFold(s, string(flow.GetFlowName())) { + // return nil + // } + // } + + return errors.WithStack(err) } diff --git a/selfservice/hook/code_address_verifier.go b/selfservice/hook/code_address_verifier.go new file mode 100644 index 000000000000..ea377d8f389f --- /dev/null +++ b/selfservice/hook/code_address_verifier.go @@ -0,0 +1,60 @@ +package hook + +import ( + "net/http" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/code" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/pkg/errors" +) + +type ( + codeAddressDependencies interface { + config.Provider + x.CSRFTokenGeneratorProvider + x.CSRFProvider + verification.StrategyProvider + verification.FlowPersistenceProvider + code.RegistrationCodePersistenceProvider + identity.PrivilegedPoolProvider + x.WriterProvider + } + CodeAddressVerifier struct { + r codeAddressDependencies + } +) + +var _ registration.PostHookPostPersistExecutor = new(Verifier) + +func NewCodeAddressVerifier(r codeAddressDependencies) *CodeAddressVerifier { + return &CodeAddressVerifier{r: r} +} + +func (cv *CodeAddressVerifier) ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *registration.Flow, s *session.Session) error { + if a.Active != identity.CredentialsTypeCodeAuth { + return nil + } + + recoveryCode, err := cv.r.RegistrationCodePersister().GetUsedRegistrationCode(r.Context(), a.GetID()) + if err != nil { + return errors.WithStack(err) + } + + for idx := range s.Identity.VerifiableAddresses { + va := s.Identity.VerifiableAddresses[idx] + if !va.Verified && recoveryCode.Address == va.Value { + va.Verified = true + if err := cv.r.PrivilegedIdentityPool().UpdateVerifiableAddress(r.Context(), &va); err != nil { + return errors.WithStack(err) + } + break + } + } + + return nil +} diff --git a/selfservice/strategy/code/code_sender.go b/selfservice/strategy/code/code_sender.go index d7c7e3497b0f..800b143a12c8 100644 --- a/selfservice/strategy/code/code_sender.go +++ b/selfservice/strategy/code/code_sender.go @@ -144,20 +144,21 @@ func (s *Sender) SendRegistrationCode(ctx context.Context, f *registration.Flow, WithSensitiveField("address", to). Debug("Preparing registration code.") - rawCode := GenerateCode() - - code, err := s.deps. - RegistrationCodePersister(). - CreateRegistrationCode(ctx, &CreateRegistrationCodeParams{ - RawCode: rawCode, - ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx), - FlowID: f.ID, - }) - if err != nil { - return err - } - for _, address := range to { + rawCode := GenerateCode() + + code, err := s.deps. + RegistrationCodePersister(). + CreateRegistrationCode(ctx, &CreateRegistrationCodeParams{ + RawCode: rawCode, + ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx), + FlowID: f.ID, + Address: address, + }) + if err != nil { + return err + } + if err := s.SendRegistrationCodeTo(ctx, address, identity, rawCode, code); err != nil { return err } diff --git a/selfservice/strategy/code/persistence.go b/selfservice/strategy/code/persistence.go index 52f5f560064e..84e36f617f4d 100644 --- a/selfservice/strategy/code/persistence.go +++ b/selfservice/strategy/code/persistence.go @@ -38,5 +38,6 @@ type ( CreateRegistrationCode(context.Context, *CreateRegistrationCodeParams) (*RegistrationCode, error) UseRegistrationCode(ctx context.Context, flowID uuid.UUID, code string) (*RegistrationCode, error) DeleteRegistrationCodesOfFlow(ctx context.Context, flowID uuid.UUID) error + GetUsedRegistrationCode(ctx context.Context, flowID uuid.UUID) (*RegistrationCode, error) } ) diff --git a/selfservice/strategy/code/registration_code.go b/selfservice/strategy/code/registration_code.go index 8fb53b82c725..a7126094eed0 100644 --- a/selfservice/strategy/code/registration_code.go +++ b/selfservice/strategy/code/registration_code.go @@ -9,8 +9,6 @@ import ( "time" "github.com/gofrs/uuid" - - "github.com/ory/kratos/identity" ) type RegistrationCode struct { @@ -21,6 +19,10 @@ type RegistrationCode struct { // format: uuid ID uuid.UUID `json:"id" db:"id" faker:"-"` + // Address represents the address that the code was sent to. + // this can be an email address or a phone number. + Address string `json:"-" db:"address"` + // CodeHMAC represents the HMACed value of the verification code CodeHMAC string `json:"-" db:"code"` @@ -64,8 +66,8 @@ func (f RegistrationCode) IsValid() bool { } type CreateRegistrationCodeParams struct { - RawCode string - ExpiresIn time.Duration - FlowID uuid.UUID - VerifiableAddress *identity.VerifiableAddress + RawCode string + ExpiresIn time.Duration + FlowID uuid.UUID + Address string } diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go index e656b430ae31..e942c1840c8f 100644 --- a/selfservice/strategy/code/strategy_registration_test.go +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -5,6 +5,7 @@ package code_test import ( "context" + "fmt" "io" "net/http" "net/url" @@ -31,7 +32,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") - testhelpers.StrategyEnable(t, conf, string(identity.CredentialsTypeCodeAuth), true) + conf.MustSet(ctx, fmt.Sprintf("%s.%s.registration_enabled", config.ViperKeySelfServiceStrategyConfig, string(identity.CredentialsTypeCodeAuth)), false) conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh") conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.sh"}) conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{ @@ -45,7 +46,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) - t.Run("case=should be able to log in with otp without any other identity credentials", func(t *testing.T) { + t.Run("case=should be able to register with otp without any other identity credentials", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) // 1. Initiate flow @@ -84,6 +85,9 @@ func TestRegistrationCodeStrategy(t *testing.T) { resp, err = client.Do(req) require.NoError(t, err) + // ory_kratos_continuity cookie is set to keep the state between the initial and the follow-up request + // since we cannot persist the identity until the code has been entered and verified, we keep the state + // within the cookie var continuityCookie *http.Cookie for _, c := range resp.Cookies() { if strings.EqualFold(c.Name, "ory_kratos_continuity") { @@ -125,6 +129,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { body, err = io.ReadAll(resp.Body) require.NoError(t, err) + // we should now end up with a session cookie var sessionCookie *http.Cookie for _, c := range resp.Cookies() { if c.Name == "ory_kratos_session" { @@ -134,5 +139,10 @@ func TestRegistrationCodeStrategy(t *testing.T) { } require.NotNil(t, sessionCookie) require.NotEmpty(t, sessionCookie.Value) + + verifiableAddress, err := reg.IdentityPool().FindVerifiableAddressByValue(ctx, identity.VerifiableAddressTypeEmail, email) + require.NoError(t, err) + require.True(t, verifiableAddress.Verified) + assert.EqualValues(t, identity.VerifiableAddressStatusCompleted, verifiableAddress.Status) }) } diff --git a/selfservice/strategy/code/stub/registration.schema.json b/selfservice/strategy/code/stub/registration.schema.json index 4a343f21e42b..3a3c6ff98172 100644 --- a/selfservice/strategy/code/stub/registration.schema.json +++ b/selfservice/strategy/code/stub/registration.schema.json @@ -14,6 +14,9 @@ "code": { "identifier": true } + }, + "verification": { + "via": "email" } } }