diff --git a/.golangci.yml b/.golangci.yml index b91920e1bbbf..06c3e769f85b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,13 +8,13 @@ linters: - structcheck - typecheck - unused - - bodyclose - dupl - gosec - varcheck disable: - ineffassign - godox + - bodyclose # too many false negatives run: skip-dirs: diff --git a/.schema/config.schema.json b/.schema/config.schema.json index 81f49be036c1..01cd04b35dd5 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -4,10 +4,20 @@ "title": "ORY Kratos Configuration", "type": "object", "definitions": { + "defaultReturnTo": { + "title": "Default Return To URL", + "description": "ORY Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. This value may be overridden by a `default_return_to` in a lower configuration level (`foo.bar.default_return_to` overrides `foo.default_return_to` overrides `default_return_to`) and by the `?return_to` query in certain cases.", + "type": "string", + "format": "uri", + "minLength": 6, + "examples": [ + "https://my-app.com/dashboard" + ] + }, "selfServiceRedirectHook": { "type": "object", "properties": { - "job": { + "hook": { "const": "redirect" }, "config": { @@ -29,44 +39,44 @@ }, "additionalItems": false, "required": [ - "job", + "hook", "config" ] }, "selfServiceSessionRevokerHook": { "type": "object", "properties": { - "job": { + "hook": { "const": "revoke_active_sessions" } }, "additionalItems": false, "required": [ - "job" + "hook" ] }, "selfServiceVerifyHook": { "type": "object", "properties": { - "job": { + "hook": { "const": "verify" } }, "additionalItems": false, "required": [ - "job" + "hook" ] }, "selfServiceSessionIssuerHook": { "type": "object", "properties": { - "job": { + "hook": { "const": "session" } }, "additionalItems": false, "required": [ - "job" + "hook" ] }, "selfServiceOIDCProvider": { @@ -121,96 +131,132 @@ "schema_url" ] }, - "selfServiceAfterSettingsHooks": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/definitions/selfServiceRedirectHook" + "selfServiceAfterSettingsStrategy": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_return_to": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceVerifyHook" + } + ] }, - { - "$ref": "#/definitions/selfServiceVerifyHook" - } - ] - }, - "uniqueItems": true + "uniqueItems": true, + "additionalItems": false + } + } }, - "selfServiceAfterLoginHooks": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/definitions/selfServiceRedirectHook" - }, - { - "$ref": "#/definitions/selfServiceSessionIssuerHook" + "selfServiceAfterLoginStrategy": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_return_to": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionRevokerHook" + } + ] }, - { - "$ref": "#/definitions/selfServiceSessionRevokerHook" - } - ] - }, - "uniqueItems": true + "uniqueItems": true, + "additionalItems": false + } + } }, - "selfServiceAfterRegistrationHooks": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/definitions/selfServiceRedirectHook" - }, - { - "$ref": "#/definitions/selfServiceSessionIssuerHook" + "selfServiceAfterRegistrationStrategy": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_return_to": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceRedirectHook" + }, + { + "$ref": "#/definitions/selfServiceSessionIssuerHook" + }, + { + "$ref": "#/definitions/selfServiceVerifyHook" + } + ] }, - { - "$ref": "#/definitions/selfServiceVerifyHook" - } - ] - }, - "uniqueItems": true + "uniqueItems": true, + "additionalItems": false + } + } }, "selfServiceAfterSettings": { "type": "object", + "additionalProperties": false, "properties": { + "default_return_to": { + "$ref": "#/definitions/defaultReturnTo" + }, "password": { - "$ref": "#/definitions/selfServiceAfterSettingsHooks" + "$ref": "#/definitions/selfServiceAfterSettingsStrategy" }, "profile": { - "$ref": "#/definitions/selfServiceAfterSettingsHooks" + "$ref": "#/definitions/selfServiceAfterSettingsStrategy" } - }, - "additionalItems": false + } }, "selfServiceAfterLogin": { "type": "object", + "additionalProperties": false, "properties": { + "default_return_to": { + "$ref": "#/definitions/defaultReturnTo" + }, "password": { - "$ref": "#/definitions/selfServiceAfterLoginHooks" + "$ref": "#/definitions/selfServiceAfterLoginStrategy" }, "oidc": { - "$ref": "#/definitions/selfServiceAfterLoginHooks" + "$ref": "#/definitions/selfServiceAfterLoginStrategy" } - }, - "additionalItems": false + } }, "selfServiceAfterRegistration": { "type": "object", + "additionalProperties": false, "properties": { + "default_return_to": { + "$ref": "#/definitions/defaultReturnTo" + }, "password": { - "$ref": "#/definitions/selfServiceAfterRegistrationHooks" + "$ref": "#/definitions/selfServiceAfterRegistrationStrategy" }, "oidc": { - "$ref": "#/definitions/selfServiceAfterRegistrationHooks" + "$ref": "#/definitions/selfServiceAfterRegistrationStrategy" } - }, - "additionalItems": false + } }, "selfServiceBefore": { - "type": "array", - "items": { - "$ref": "#/definitions/selfServiceRedirectHook" - }, - "uniqueItems": true + "type": "object", + "additionalItems": false, + "properties": { + "hooks": { + "type": "array", + "items": { + "$ref": "#/definitions/selfServiceRedirectHook" + }, + "uniqueItems": true + } + } }, "cookiesSameSite": { "type": "string", @@ -344,11 +390,7 @@ "default": "1h" }, "before": { - "type": "array", - "items": { - "$ref": "#/definitions/selfServiceRedirectHook" - }, - "uniqueItems": true + "$ref": "#/definitions/selfServiceBefore" }, "after": { "$ref": "#/definitions/selfServiceAfterRegistration" @@ -376,7 +418,10 @@ "template_override_path": { "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." + "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" + ] }, "smtp": { "title": "SMTP Configuration", @@ -385,9 +430,9 @@ "properties": { "connection_uri": { "title": "SMTP connection string", - "description": "This URI will be used to connect to the SMTP server.", + "description": "This URI will be used to connect to the SMTP server. Use the query parameter to allow (`?skip_ssl_verify=true`) or disallow (`?skip_ssl_verify=false`) self-signed TLS certificates. Please keep in mind that any host other than localhost / 127.0.0.1 must use smtp over TLS (smtps) or the connection will not be possible.", "examples": [ - "smtps://foo:bar@my-mailserver:1234/" + "smtps://foo:bar@my-mailserver:1234/?skip_ssl_verify=false" ], "type": "string", "format": "uri" @@ -425,7 +470,9 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [4434], + "examples": [ + 4434 + ], "default": 4434 } }, @@ -442,7 +489,9 @@ "type": "integer", "minimum": 1, "maximum": 65535, - "examples": [4433], + "examples": [ + 4433 + ], "default": 4433 } }, @@ -456,60 +505,98 @@ "properties": { "self": { "type": "object", + "additionalProperties": false, "properties": { "public": { "type": "string", - "format": "uri" + "format": "uri", + "examples": [ + "https://my-app.com/.ory/kratos/public" + ] }, "admin": { "type": "string", - "format": "uri" + "format": "uri", + "examples": [ + "https://kratos.private-network:4434/" + ] } - }, - "additionalProperties": false + } }, "mfa_ui": { + "title": "Multi-Factor UI URL", + "description": "URL where the Multi-Factor UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", - "format": "uri" + "format": "uri", + "examples": [ + "https://my-app.com/login/mfa" + ] }, "login_ui": { + "title": "Login UI URL", + "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", - "format": "uri" + "format": "uri", + "examples": [ + "https://my-app.com/login" + ] }, "settings_ui": { + "title": "Settings UI URL", + "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", - "format": "uri" + "format": "uri", + "examples": [ + "https://my-app.com/user/settings" + ] }, "default_return_to": { - "type": "string", - "format": "uri" + "$ref": "#/definitions/defaultReturnTo" }, "registration_ui": { + "title": "Registration UI URL", + "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", "type": "string", - "format": "uri" + "format": "uri", + "examples": [ + "https://my-app.com/signup" + ] }, "error_ui": { + "title": "ORY Kratos Error UI URL", + "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" + "format": "uri", + "examples": [ + "https://my-app.com/kratos-error" + ] }, "verify_ui": { - "title": "Verify User Interface URL", - "description": "The URL of the Verify User Interface, the page where users can request activate and / or verify their email or telephone number.", + "title": "Verify UI URL", + "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" + "format": "uri", + "examples": [ + "https://my-app.com/verify" + ] }, - "whitelisted_return_to_domains": { + "whitelisted_return_to_urls": { + "title": "Whitelisted Return To URLs", + "description": "List of URLs that are allowed to be redirected to. A redirection request is made by appending `?return_to=...` to Login, Registration, and other self-service flows.", "type": "array", "items": { "type": "string", "format": "uri" }, + "examples": [ + "https://app.my-app.com/dashboard", + "https://www.my-app.com/" + ], "uniqueItems": true } }, "required": [ "settings_ui", - "mfa_ui", "login_ui", "registration_ui", "error_ui", diff --git a/Makefile b/Makefile index 2eb98c4e2abd..8225e71b6514 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ export GO111MODULE := on export PATH := .bin:${PATH} deps: -ifneq ("v0",$(shell cat .bin/.lock)) +ifneq ("v0", $(shell cat .bin/.lock)) curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b .bin/ v1.24.0 go build -o .bin/go-acc github.com/ory/go-acc go build -o .bin/goreturns github.com/sqs/goreturns diff --git a/continuity/manager_test.go b/continuity/manager_test.go index ae08be5c4fea..d18427e0f159 100644 --- a/continuity/manager_test.go +++ b/continuity/manager_test.go @@ -37,7 +37,7 @@ type persisterTestPayload struct { } func TestManager(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/identity.schema.json") viper.Set(configuration.ViperKeyURLsSelfPublic, "https://www.ory.sh") diff --git a/contrib/quickstart/kratos/email-password/.kratos.yml b/contrib/quickstart/kratos/email-password/.kratos.yml index 4fe21945f827..aaed8f043693 100644 --- a/contrib/quickstart/kratos/email-password/.kratos.yml +++ b/contrib/quickstart/kratos/email-password/.kratos.yml @@ -7,17 +7,9 @@ selfservice: privileged_session_max_age: 1m after: profile: - - - job: verify - - - job: redirect - config: - default_redirect_url: http://127.0.0.1:4455/dashboard - password: - - - job: redirect - config: - default_redirect_url: http://127.0.0.1:4455/dashboard + hooks: + - + hook: verify verify: return_to: http://127.0.0.1:4455/ @@ -27,29 +19,16 @@ selfservice: login: request_lifespan: 10m - after: - password: - - - job: session - - - job: redirect - config: - default_redirect_url: http://127.0.0.1:4455/ - allow_user_defined_redirect: true registration: request_lifespan: 10m after: password: - - - job: verify - - - job: session - - - job: redirect - config: - default_redirect_url: http://127.0.0.1:4455/ - allow_user_defined_redirect: true + hooks: + - + hook: session + - + hook: verify log: level: debug @@ -72,7 +51,7 @@ urls: public: http://127.0.0.1:4455/.ory/kratos/public/ admin: http://kratos:4434/ default_return_to: http://127.0.0.1:4455/ - whitelisted_return_to_domains: + whitelisted_return_to_urls: - http://127.0.0.1:4455 hashers: diff --git a/courier/courier_test.go b/courier/courier_test.go index dfd809208006..7fecd87874b8 100644 --- a/courier/courier_test.go +++ b/courier/courier_test.go @@ -98,7 +98,7 @@ func TestSMTP(t *testing.T) { t.Logf("SMTP URL: %s", smtp) t.Logf("API URL: %s", api) - conf, reg := internal.NewRegistryDefault(t) + conf, reg := internal.NewFastRegistryWithMocks(t) viper.Set(configuration.ViperKeyCourierSMTPURL, smtp) viper.Set(configuration.ViperKeyCourierSMTPFrom, "test-stub@ory.sh") c := reg.Courier() diff --git a/courier/template/verify_invalid_test.go b/courier/template/verify_invalid_test.go index fcd08723b1db..01a9491a9390 100644 --- a/courier/template/verify_invalid_test.go +++ b/courier/template/verify_invalid_test.go @@ -11,7 +11,7 @@ import ( ) func TestVerifyInvalid(t *testing.T) { - conf, _ := internal.NewRegistryDefault(t) + conf, _ := internal.NewFastRegistryWithMocks(t) tpl := template.NewVerifyInvalid(conf, &template.VerifyInvalidModel{}) rendered, err := tpl.EmailBody() diff --git a/courier/template/verify_valid_test.go b/courier/template/verify_valid_test.go index c442d239de62..705768474d87 100644 --- a/courier/template/verify_valid_test.go +++ b/courier/template/verify_valid_test.go @@ -11,7 +11,7 @@ import ( ) func TestVerifyValid(t *testing.T) { - conf, _ := internal.NewRegistryDefault(t) + conf, _ := internal.NewFastRegistryWithMocks(t) tpl := template.NewVerifyValid(conf, &template.VerifyValidModel{}) rendered, err := tpl.EmailBody() diff --git a/docs/docs/concepts/selfservice-flow-completion.md b/docs/docs/concepts/selfservice-flow-completion.md new file mode 100644 index 000000000000..42b0d2e2e026 --- /dev/null +++ b/docs/docs/concepts/selfservice-flow-completion.md @@ -0,0 +1,61 @@ +--- +id: selfservice-flow-completion +title: Self-Service Flow Completion +--- + +Self-Service flows such as Login, Registration, Updating Settings support two successful response modes: + +- For browsers, the response will be a [redirection](#redirection). +- For API clients (this includes AJAX) the response will be in [JSON](#json). + +## Redirection + +Browser requests, identified by the `Accept: text/html` header, complete with a redirection flow. +If no redirection URL is set for the flow, the Default Redirect URL will be used: + +```yaml file="path/to/my/kratos.config.yml" +urls: + default_redirect_to: https://always-end-up-here-per-default/ +``` + +It is possible to specify a redirect URL per Self-Service Flow: + +```yaml file="path/to/my/kratos.config.yml" +selfservice: + login: + after: + default_redirect_to: https://end-up-here-after-login/ + registration: + after: + default_redirect_to: https://end-up-here-after-registration/ + # ... +``` + +You may also set redirect URLs per strategy (overrides `selfservice..default_return_to`): + +```yaml file="path/to/my/kratos.config.yml" +selfservice: + login: + after: + default_redirect_to: https://this-is-overridden-by-password/ + password: + default_redirect_to: https://end-up-here-after-login-with-password/ + # ... +``` + +It is also possible to redirect someone back to the original URL. For example, if a user +requests `https://www.myapp.com/blog/write` but is not logged in, we want the user +to end up at that page after login. To achieve that, you append `?return_to=https://www.myapp.com/blog/write` +when initializing the Login / Registration /Settings flow. + +Because ORY Kratos prevents Open Redirect Attacks, you need to whitelist the domain in your ORY Kratos config: + +```yaml file="path/to/my/kratos.config.yml" +urls: + whitelisted_return_to_urls: + - https://www.myapp.com/ +``` + +## JSON + +This feature is currently in prototype phase and will be documented at a later stage. diff --git a/docs/docs/reference/configuration.md b/docs/docs/reference/configuration.md index 9afa4210ff4a..c29dac0022e5 100644 --- a/docs/docs/reference/configuration.md +++ b/docs/docs/reference/configuration.md @@ -600,15 +600,15 @@ urls: # admin: http://PfnGJZXKZpWcmAW.wcmhK-Z - ## whitelisted_return_to_domains ## + ## whitelisted_return_to_urls ## # # Set this value using environment variables on # - Linux/macOS: - # $ export URLS_WHITELISTED_RETURN_TO_DOMAINS= + # $ export URLS_whitelisted_return_to_urls= # - Windows Command Line (CMD): - # > set URLS_WHITELISTED_RETURN_TO_DOMAINS= + # > set URLS_whitelisted_return_to_urls= # - whitelisted_return_to_domains: + whitelisted_return_to_urls: - https://mLXbuCzlJbAaYVJaVQ.namM9tRqlZAoKO+uRYpH - http://WhIslWEQlfsYCGFmZrbPBCAX.qqlJ4L - http://JDOLAPSVNPPoVJlrCKjfXcNRPwlhn.qbcYGB2ssMW5wJ6gALSyIC9Xcb diff --git a/docs/docs/self-service/flows/user-login-user-registration.md b/docs/docs/self-service/flows/user-login-user-registration.md index 8c555546268e..ad2b997235cd 100644 --- a/docs/docs/self-service/flows/user-login-user-registration.md +++ b/docs/docs/self-service/flows/user-login-user-registration.md @@ -98,14 +98,16 @@ for rendering all the Login, Registration, ... screens, and another app (think "Service Oriented Architecture", "Micro-Services" or "Service Mesh") is responsible for rendering your Dashboards, Management Screens, and so on. -> It is highly RECOMMENDED to all the applications (or "services"), including -> ORY Kratos, behind a common API Gateway or Reverse Proxy. This greatly reduces -> the amount of work you have to do to get all the Cookies working properly. We -> RECOMMEND using [ORY Oathkeeper](http://github.com/ory/oathkeeper) for this as -> it integrates best with the ORY Ecosystem and because all of our examples use -> ORY Oathkeeper. You MAY of course use any other reverse proxy (Envoy, AWS API -> Gateway, Ambassador, Nginx, Kong, ...), but we do not have examples or guides -> for those at this time. +::: Note +It is RECOMMENDED to put all applications (or "services"), including +ORY Kratos, behind a common API Gateway or Reverse Proxy. This greatly reduces +the amount of work you have to do to get all the Cookies working properly. We +RECOMMEND using [ORY Oathkeeper](http://github.com/ory/oathkeeper) for this as +it integrates best with the ORY Ecosystem and because all of our examples use +ORY Oathkeeper. You MAY of course use any other reverse proxy (Envoy, AWS API +Gateway, Ambassador, Nginx, Kong, ...), but we do not have examples or guides +for those at this time. +::: ### Code @@ -194,49 +196,34 @@ JQuery, ReactJS, AngularJS, ...) that uses AJAX requests to fetch data. For these type of applications, read this section first and go to section [Client-Side Browser Applications](#client-side-browser-applications) next. -#### Network Topology - -Your Server-Side Application and ORY Kratos are deployed in a Virtual Private -Cluster that can not be accessed from the public internet directly. Instead, -only ORY Oathkeeper can be accessed from the public internet and proxies -incoming requests to the appropriate service: - -- Public internet traffic to domain `example.org` is sent to ORY Oathkeeper - which in turn: - - proxies URLs matching `https://example.org/auth/login` to your Server-Side - Application available at - `https://your-service-side-application.example-org.vpc/auth/login` - - proxies URLs matching `https://example.org/auth/registration` to your - Server-Side Application available at - `https://your-service-side-application.example-org.vpc/auth/registration` - - `https://example.org/.ory/kratos/public/*` is proxied to - `https://ory-kratos-public.example-org.vpc/` -- `https://ory-kratos-admin.example-org.vpc/` exposes ORY Kratos' Admin API and - is not accessible by the open internet and ideally only by Your Server-Side - Application. -- `https://ory-kratos-public.example-org.vpc` exposes ORY Kratos' Public API and - is ideally only accessible by ORY Oathkeeper. -- `https://your-service-side-application.example-org.vpc` exposes your - Server-Side Application and is ideally only accessible by ORY Oathkeeper. - -The ORY Kratos Admin API is exposed only in the intranet and only the -Server-Side Application should be able to talk to it. - -[![User Login and Registration Network Topology for Server-Side Applications](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggVERcblxuc3ViZ3JhcGggcGlbUHVibGljIEludGVybmV0XVxuICAgIEJbQnJvd3Nlcl1cbmVuZFxuXG5zdWJncmFwaCB2cGNbVlBDIC8gQ2xvdWQgLyBEb2NrZXIgTmV0d29ya11cbnN1YmdyYXBoIFwiRGVtaWxpdGFyaXplZCBab25lIC8gRE1aXCJcbiAgICBPS1tPUlkgT2F0aGtlZXBlciA6NDQ1NV1cbiAgICBCIC0tPiBPS1xuZW5kXG5cbiAgICBPSyAtLT58XCJGb3J3YXJkcyAvYXV0aC9sb2dpbiB0b1wifCBTQUxJXG4gICAgT0sgLS0-fFwiRm9yd2FyZHMgL2F1dGgvcmVnaXN0cmF0aW9uIHRvXCJ8IFNBTFJcbiAgICBPSyAtLT58XCJGb3J3YXJkcyAvLm9yeS9rcmF0b3MvcHVibGljLyogdG9cInwgS1BcblxuICAgIHN1YmdyYXBoIFwiUHJpdmF0ZSBTdWJuZXQgLyBJbnRyYW5ldFwiXG4gICAgS1sgT1JZIEtyYXRvcyBdXG5cbiAgICBLUChbIE9SWSBLcmF0b3MgUHVibGljIEFQSSBdKVxuICAgIEtBKFsgT1JZIEtyYXRvcyBBZG1pbiBBUEkgXSlcbiAgICBTQSAtLT58XCJ0YWxrcyB0b1wifCBLQVxuICAgIEtBIC0uYmVsb25ncyB0by4tPiBLXG4gICAgS1AgLS5iZWxvbmdzIHRvLi0-IEtcblxuICAgIHN1YmdyYXBoIHNhW1wiWW91ciBBcHBsaWNhdGlvblwiXVxuXG4gICAgICAgIFNBW1wiWW91ciBTZXJ2ZXItU2lkZSBBcHBsaWNhdGlvblwiXVxuICAgICAgICBTQUxJIC0uYmVsb25ncyB0by4tPiBTQVxuICAgICAgICBTQUxSIC0uYmVsb25ncyB0by4tPiBTQVxuICAgICAgICBTQUxJKFtSb3V0ZSAvYXV0aC9sb2dpbl0pIFxuICAgICAgICBTQUxSKFtSb3V0ZSAvYXV0aC9yZWdpc3RyYXRpb25dKSBcbiAgICBlbmRcbiAgICBlbmRcblxuZW5kXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoibmV1dHJhbCIsImZsb3djaGFydCI6eyJyYW5rU3BhY2luZyI6NzAsIm5vZGVTcGFjaW5nIjozMCwiY3VydmUiOiJiYXNpcyJ9fSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggVERcblxuc3ViZ3JhcGggcGlbUHVibGljIEludGVybmV0XVxuICAgIEJbQnJvd3Nlcl1cbmVuZFxuXG5zdWJncmFwaCB2cGNbVlBDIC8gQ2xvdWQgLyBEb2NrZXIgTmV0d29ya11cbnN1YmdyYXBoIFwiRGVtaWxpdGFyaXplZCBab25lIC8gRE1aXCJcbiAgICBPS1tPUlkgT2F0aGtlZXBlciA6NDQ1NV1cbiAgICBCIC0tPiBPS1xuZW5kXG5cbiAgICBPSyAtLT58XCJGb3J3YXJkcyAvYXV0aC9sb2dpbiB0b1wifCBTQUxJXG4gICAgT0sgLS0-fFwiRm9yd2FyZHMgL2F1dGgvcmVnaXN0cmF0aW9uIHRvXCJ8IFNBTFJcbiAgICBPSyAtLT58XCJGb3J3YXJkcyAvLm9yeS9rcmF0b3MvcHVibGljLyogdG9cInwgS1BcblxuICAgIHN1YmdyYXBoIFwiUHJpdmF0ZSBTdWJuZXQgLyBJbnRyYW5ldFwiXG4gICAgS1sgT1JZIEtyYXRvcyBdXG5cbiAgICBLUChbIE9SWSBLcmF0b3MgUHVibGljIEFQSSBdKVxuICAgIEtBKFsgT1JZIEtyYXRvcyBBZG1pbiBBUEkgXSlcbiAgICBTQSAtLT58XCJ0YWxrcyB0b1wifCBLQVxuICAgIEtBIC0uYmVsb25ncyB0by4tPiBLXG4gICAgS1AgLS5iZWxvbmdzIHRvLi0-IEtcblxuICAgIHN1YmdyYXBoIHNhW1wiWW91ciBBcHBsaWNhdGlvblwiXVxuXG4gICAgICAgIFNBW1wiWW91ciBTZXJ2ZXItU2lkZSBBcHBsaWNhdGlvblwiXVxuICAgICAgICBTQUxJIC0uYmVsb25ncyB0by4tPiBTQVxuICAgICAgICBTQUxSIC0uYmVsb25ncyB0by4tPiBTQVxuICAgICAgICBTQUxJKFtSb3V0ZSAvYXV0aC9sb2dpbl0pIFxuICAgICAgICBTQUxSKFtSb3V0ZSAvYXV0aC9yZWdpc3RyYXRpb25dKSBcbiAgICBlbmRcbiAgICBlbmRcblxuZW5kXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoibmV1dHJhbCIsImZsb3djaGFydCI6eyJyYW5rU3BhY2luZyI6NzAsIm5vZGVTcGFjaW5nIjozMCwiY3VydmUiOiJiYXNpcyJ9fSwidXBkYXRlRWRpdG9yIjpmYWxzZX0) +#### Network Architecture + +We recommend checking out the [Quickstart Network Architecture](../../quickstart.mdx#network-architecture) +for a high-level, exemplary, overview of the network. In summary: + +1. The SecureApp (your application) is exposed at http://127.0.0.1:4455 and proxies requests matching path `./ory/kratos/public/*` +to ORY Krato's Public API Port. +1. ORY Kratos exposes (for debugging only!!) the Public API at http://127.0.0.1:4433 and Admin API at http://127.0.0.1:4434. +1. Within the "intranet" or "private network", ORY Kratos is exposed at http://kratos:4433 and http://kratos:4434. These +URLs are be used by the SecureApp to communicate with ORY Kratos. + +Keep in mind that his architecture is just one of many possible network architectures. +It is however one of the simplest as well and it works locally. For production deployments +you would probably use an Reverse Proxy such as Nginx, Kong, Envoy, ORY Oathkeeper, or others. #### User Login and User Registration Process Sequence The Login and Registration User Flow is composed of several high-level steps summarized in this state diagram: -[![User Login and Registration State Machine](https://mermaid.ink/img/eyJjb2RlIjoic3RhdGVEaWFncmFtXG4gIHMxOiBVc2VyIGJyb3dzZXMgYXBwXG4gIHMyOiBFeGVjdXRlIFwiQmVmb3JlIExvZ2luL1JlZ2lzdHJhdGlvbiBKb2IocylcIlxuICBzMzogVXNlciBJbnRlcmZhY2UgQXBwbGljYXRpb24gcmVuZGVycyBcIkxvZ2luL1JlZ2lzdHJhdGlvbiBSZXF1ZXN0XCJcbiAgczQ6IEV4ZWN1dGUgXCJBZnRlciBMb2dpbi9SZWdpc3RyYXRpb24gSm9iKHMpXCJcbiAgczU6IFVwZGF0ZSBcIkxvZ2luL1JlZ2lzdHJhdGlvbiBSZXF1ZXN0XCIgd2l0aCBFcnJvciBDb250ZXh0KHMpXG4gIHM2OiBMb2dpbi9SZWdpc3RyYXRpb24gc3VjY2Vzc2Z1bFxuXG5cblxuXHRbKl0gLS0-IHMxXG4gIHMxIC0tPiBzMiA6IFVzZXIgY2xpY2tzIFwiTG9nIGluIC8gU2lnbiB1cFwiXG4gIHMyIC0tPiBFcnJvciA6IEEgam9iIGZhaWxzXG4gIHMyIC0tPiBzMyA6IFVzZXIgaXMgcmVkaXJlY3RlZCB0byBMb2dpbi9SZWdpc3RyYXRpb24gVUkgVVJMXG4gIHMzIC0tPiBzNCA6IFVzZXIgcHJvdmlkZXMgdmFsaWQgY3JlZGVudGlhbHMvcmVnaXN0cmF0aW9uIGRhdGFcbiAgczMgLS0-IHM1IDogVXNlciBwcm92aWRlcyBpbnZhbGlkIGNyZWRlbnRpYWxzL3JlZ2lzdHJhdGlvbiBkYXRhXG4gIHM1IC0tPiBzMyA6IFVzZXIgaXMgcmVkaXJlY3RlZCB0byBMb2dpbi9SZWdpc3RyYXRpb24gVUkgVVJMXG4gIHM0IC0tPiBFcnJvciA6IEEgam9iIGZhaWxzXG4gIHM0IC0tPiBzNlxuICBzNiAtLT4gWypdXG5cbiAgRXJyb3IgLS0-IFsqXVxuXG5cbiIsIm1lcm1haWQiOnsidGhlbWUiOiJkZWZhdWx0In0sInVwZGF0ZUVkaXRvciI6ZmFsc2V9)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic3RhdGVEaWFncmFtXG4gIHMxOiBVc2VyIGJyb3dzZXMgYXBwXG4gIHMyOiBFeGVjdXRlIFwiQmVmb3JlIExvZ2luL1JlZ2lzdHJhdGlvbiBKb2IocylcIlxuICBzMzogVXNlciBJbnRlcmZhY2UgQXBwbGljYXRpb24gcmVuZGVycyBcIkxvZ2luL1JlZ2lzdHJhdGlvbiBSZXF1ZXN0XCJcbiAgczQ6IEV4ZWN1dGUgXCJBZnRlciBMb2dpbi9SZWdpc3RyYXRpb24gSm9iKHMpXCJcbiAgczU6IFVwZGF0ZSBcIkxvZ2luL1JlZ2lzdHJhdGlvbiBSZXF1ZXN0XCIgd2l0aCBFcnJvciBDb250ZXh0KHMpXG4gIHM2OiBMb2dpbi9SZWdpc3RyYXRpb24gc3VjY2Vzc2Z1bFxuXG5cblxuXHRbKl0gLS0-IHMxXG4gIHMxIC0tPiBzMiA6IFVzZXIgY2xpY2tzIFwiTG9nIGluIC8gU2lnbiB1cFwiXG4gIHMyIC0tPiBFcnJvciA6IEEgam9iIGZhaWxzXG4gIHMyIC0tPiBzMyA6IFVzZXIgaXMgcmVkaXJlY3RlZCB0byBMb2dpbi9SZWdpc3RyYXRpb24gVUkgVVJMXG4gIHMzIC0tPiBzNCA6IFVzZXIgcHJvdmlkZXMgdmFsaWQgY3JlZGVudGlhbHMvcmVnaXN0cmF0aW9uIGRhdGFcbiAgczMgLS0-IHM1IDogVXNlciBwcm92aWRlcyBpbnZhbGlkIGNyZWRlbnRpYWxzL3JlZ2lzdHJhdGlvbiBkYXRhXG4gIHM1IC0tPiBzMyA6IFVzZXIgaXMgcmVkaXJlY3RlZCB0byBMb2dpbi9SZWdpc3RyYXRpb24gVUkgVVJMXG4gIHM0IC0tPiBFcnJvciA6IEEgam9iIGZhaWxzXG4gIHM0IC0tPiBzNlxuICBzNiAtLT4gWypdXG5cbiAgRXJyb3IgLS0-IFsqXVxuXG5cbiIsIm1lcm1haWQiOnsidGhlbWUiOiJkZWZhdWx0In0sInVwZGF0ZUVkaXRvciI6ZmFsc2V9) +[![User Login and Registration State Machine](https://mermaid.ink/img/eyJjb2RlIjoic3RhdGVEaWFncmFtXG4gIHMxOiBVc2VyIGJyb3dzZXMgYXBwXG4gIHMyOiBFeGVjdXRlIFwiQmVmb3JlIExvZ2luL1JlZ2lzdHJhdGlvbiBIb29rKHMpXCJcbiAgczM6IFVzZXIgSW50ZXJmYWNlIEFwcGxpY2F0aW9uIHJlbmRlcnMgXCJMb2dpbi9SZWdpc3RyYXRpb24gUmVxdWVzdFwiXG4gIHM0OiBFeGVjdXRlIFwiQWZ0ZXIgTG9naW4vUmVnaXN0cmF0aW9uIEhvb2socylcIlxuICBzNTogVXBkYXRlIFwiTG9naW4vUmVnaXN0cmF0aW9uIFJlcXVlc3RcIiB3aXRoIEVycm9yIENvbnRleHQocylcbiAgczY6IExvZ2luL1JlZ2lzdHJhdGlvbiBzdWNjZXNzZnVsXG5cblxuXG5cdFsqXSAtLT4gczFcbiAgczEgLS0-IHMyIDogVXNlciBjbGlja3MgXCJMb2cgaW4gLyBTaWduIHVwXCJcbiAgczIgLS0-IEVycm9yIDogQSBob29rIGZhaWxzXG4gIHMyIC0tPiBzMyA6IFVzZXIgaXMgcmVkaXJlY3RlZCB0byBMb2dpbi9SZWdpc3RyYXRpb24gVUkgVVJMXG4gIHMzIC0tPiBzNCA6IFVzZXIgcHJvdmlkZXMgdmFsaWQgY3JlZGVudGlhbHMvcmVnaXN0cmF0aW9uIGRhdGFcbiAgczMgLS0-IHM1IDogVXNlciBwcm92aWRlcyBpbnZhbGlkIGNyZWRlbnRpYWxzL3JlZ2lzdHJhdGlvbiBkYXRhXG4gIHM1IC0tPiBzMyA6IFVzZXIgaXMgcmVkaXJlY3RlZCB0byBMb2dpbi9SZWdpc3RyYXRpb24gVUkgVVJMXG4gIHM0IC0tPiBFcnJvciA6IEEgSG9vayBmYWlsc1xuICBzNCAtLT4gczZcbiAgczYgLS0-IFsqXVxuXG4gIEVycm9yIC0tPiBbKl1cblxuXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic3RhdGVEaWFncmFtXG4gIHMxOiBVc2VyIGJyb3dzZXMgYXBwXG4gIHMyOiBFeGVjdXRlIFwiQmVmb3JlIExvZ2luL1JlZ2lzdHJhdGlvbiBIb29rKHMpXCJcbiAgczM6IFVzZXIgSW50ZXJmYWNlIEFwcGxpY2F0aW9uIHJlbmRlcnMgXCJMb2dpbi9SZWdpc3RyYXRpb24gUmVxdWVzdFwiXG4gIHM0OiBFeGVjdXRlIFwiQWZ0ZXIgTG9naW4vUmVnaXN0cmF0aW9uIEhvb2socylcIlxuICBzNTogVXBkYXRlIFwiTG9naW4vUmVnaXN0cmF0aW9uIFJlcXVlc3RcIiB3aXRoIEVycm9yIENvbnRleHQocylcbiAgczY6IExvZ2luL1JlZ2lzdHJhdGlvbiBzdWNjZXNzZnVsXG5cblxuXG5cdFsqXSAtLT4gczFcbiAgczEgLS0-IHMyIDogVXNlciBjbGlja3MgXCJMb2cgaW4gLyBTaWduIHVwXCJcbiAgczIgLS0-IEVycm9yIDogQSBob29rIGZhaWxzXG4gIHMyIC0tPiBzMyA6IFVzZXIgaXMgcmVkaXJlY3RlZCB0byBMb2dpbi9SZWdpc3RyYXRpb24gVUkgVVJMXG4gIHMzIC0tPiBzNCA6IFVzZXIgcHJvdmlkZXMgdmFsaWQgY3JlZGVudGlhbHMvcmVnaXN0cmF0aW9uIGRhdGFcbiAgczMgLS0-IHM1IDogVXNlciBwcm92aWRlcyBpbnZhbGlkIGNyZWRlbnRpYWxzL3JlZ2lzdHJhdGlvbiBkYXRhXG4gIHM1IC0tPiBzMyA6IFVzZXIgaXMgcmVkaXJlY3RlZCB0byBMb2dpbi9SZWdpc3RyYXRpb24gVUkgVVJMXG4gIHM0IC0tPiBFcnJvciA6IEEgSG9vayBmYWlsc1xuICBzNCAtLT4gczZcbiAgczYgLS0-IFsqXVxuXG4gIEVycm9yIC0tPiBbKl1cblxuXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ) 1. The **Login/Registration User Flow** is initiated because a link was clicked or an action was performed that requires an active user session. 1. ORY Kratos executes Jobs defined in the **Before Login/Registration Workflow**. If a failure occurs, the whole flow is aborted. 1. The user's browser is redirected to - `https://example.org/.ory/kratos/public/auth/browser/(login|registration)` + `http://127.0.0.1:4455/.ory/kratos/public/auth/browser/(login|registration)` (the notation `(login|registration)` expresses the two possibilities of `../auth/browser/login` or `../auth/browser/registration`). 1. ORY Kratos does some internal processing (e.g. checks if a session cookie is @@ -244,13 +231,13 @@ summarized in this state diagram: the user's browser to the Login UI URL which is defined using the `urls.login_ui` (or `urls.registration_ui`) config or `URLS_LOGIN_UI` (or `URLS_REGISTRATION_UI`) environment variable, which is set to the ui - endpoints - for example `https://example.org/auth/login` and - `https://example.org/auth/registration`). The user's browser is thus - redirected to `https://example.org/auth/(login|registration)?request=abcde`. + endpoints - for example `https://127.0.0.1:4455/auth/login` and + `https://127.0.0.1:4455/auth/registration`). The user's browser is thus + redirected to `https://127.0.0.1:4455/auth/(login|registration)?request=abcde`. The `request` query parameter includes a unique ID which will be used to fetch contextual data for this login request. 1. Your Server-Side Application makes a `GET` request to - `https://ory-kratos-admin.example-org.vpc/auth/browser/requests/(login|registration)?request=abcde`. + `http://kratos:4434/auth/browser/requests/(login|registration)?request=abcde`. ORY Kratos responds with a JSON Payload that contains data (form fields, error messages, ...) for all enabled User Login Strategies: `json5 { "id": "abcde", "expires_at": "2020-01-27T09:34:39.3249566Z", "issued_at": "2020-01-27T09:24:39.3249689Z", "request_url": "https://example.org/.ory/kratos/public/auth/browser/(login|registration)", "methods": { "oidc": { "method": "oidc", "config": { /* ... */ } }, "password": { "method": "password", "config": { /* ... */ } } // ... } }` @@ -258,9 +245,9 @@ summarized in this state diagram: interacts with it an completes the Login by clicking, for example, the "Login", the "Login with Google", ... button. 1. The User's browser makes a request to one of ORY Kratos' Strategy URLs (e.g. - `https://example.org/.ory/kratos/public/auth/browser/methods/password/(login|registration)` + `http://127.0.0.1:4455/.ory/kratos/public/auth/browser/methods/password/(login|registration)` or - `https://example.org/.ory/kratos/public/auth/browser/methods/oidc/auth/abcde`). + `https://127.0.0.1:4455/.ory/kratos/public/auth/browser/methods/oidc/auth/abcde`). ORY Kratos validates the User's credentials (when logging in - e.g. Username and Password, by performing an OpenID Connect flow, ...) or the registration form data (when signing up - e.g. is the E-Mail address valid, is the person @@ -295,15 +282,17 @@ summarized in this state diagram: } ``` and the user's Browser is redirected back to the Login UI: - `https://example.org/auth/(login|registration)?request=abcde`. + `https://127.0.0.1:4455/auth/(login|registration)?request=abcde`. - If credentials / data is valid, ORY Kratos proceeds with the next step. - If the flow is a registration request and the registration data is valid, the identity is created. -1. ORY Kratos executes Jobs (e.g. redirect somewhere) defined in the **After +1. ORY Kratos executes hooks defined in the **After Login/Registration Workflow**. If a failure occurs, the whole flow is aborted. +1. The client receives the expected response. For browsers, this is a HTTP Redirection, for API clients it will +be a JSON response containing the session and/or identity. For more information on this topic check [Self-Service Flow Completion](../../concepts/selfservice-flow-completion.md). -[![User Login Sequence Diagram for Server-Side Applications](https://mermaid.ink/img/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBPIGFzIE9SWSBPYXRoa2VlcGVyXG4gIHBhcnRpY2lwYW50IEtQIGFzIE9SWSBLcmF0b3MgUHVibGljIEFQSVxuICBwYXJ0aWNpcGFudCBBIGFzIFlvdXIgU2VydmVyLVNpZGUgQXBwbGljYXRpb25cbiAgcGFydGljaXBhbnQgS0EgYXMgT1JZIEtyYXRvcyBBZG1pbiBBUElcblxuICBCLT4-K086IEdFVCAvLm9yeS9rcmF0b3MvcHVibGljL2F1dGgvYnJvd3Nlci8obG9naW58cmVnaXN0cmF0aW9uKVxuICBPLT4-K0tQOiBHRVQgL2F1dGgvYnJvd3Nlci8obG9naW58cmVnaXN0cmF0aW9uKVxuICBLUC0tPj5LUDogRXhlY3V0ZSBKb2JzIGRlZmluZWQgaW4gXCJCZWZvcmUgTG9naW4vUmVnaXN0cmF0aW9uIFdvcmtmbG93KHMpXCJcbiAgS1AtLT4-LU86IEhUVFAgMzAyIEZvdW5kIC9hcHAvYXV0aC8obG9naW58cmVnaXN0cmF0aW9uKT9yZXF1ZXN0PWFiY2RlXG4gIE8tLT4-LUI6IEhUVFAgMzAyIEZvdW5kIC9hcHAvYXV0aC8obG9naW58cmVnaXN0cmF0aW9uKT9yZXF1ZXN0PWFiY2RlXG5cbiAgQi0-PitPOiBHRVQgL2FwcC9hdXRoLyhsb2dpbnxyZWdpc3RyYXRpb24pP3JlcXVlc3Q9YWJjZGVcbiAgTy0-PitBOiBHRVQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBBLT4-K0tBOiBHRVQgL2F1dGgvYnJvd3Nlci9yZXF1ZXN0cy8obG9naW58cmVnaXN0cmF0aW9uKT9yZXF1ZXN0PWFiY2RlXG4gIEtBLT4-LUE6IFNlbmRzIExvZ2luL1JlZ2lzdHJhdGlvbiBSZXF1ZXN0IEpTT04gUGF5bG9hZFxuICBOb3RlIG92ZXIgQSxLQTogIHtcIm1ldGhvZHNcIjp7XCJwYXNzd29yZFwiOi4uLixcIm9pZGNcIjouLn19XG4gIEEtLT4-QTogR2VuZXJhdGUgYW5kIHJlbmRlciBIVE1MXG4gIEEtLT4-LU86IFJldHVybiBIVE1MIChGb3JtLCAuLi4pXG4gIE8tLT4-LUI6IFJldHVybiBIVE1MIChGb3JtLCAuLi4pXG5cbiAgQi0tPj5COiBGaWxsIG91dCBIVE1MXG5cbiAgQi0-PitPOiBQT1NUIEhUTUwgRm9ybVxuICBPLT4-K0tQOiBQT1NUIEhUTUwgRm9ybVxuICBLUC0tPj5LUDogQ2hlY2tzIGxvZ2luIC8gcmVnaXN0cmF0aW9uIGRhdGFcblxuXG4gIGFsdCBMb2dpbiBkYXRhIGlzIHZhbGlkXG4gICAgS1AtLT4-LUtQOiBFeGVjdXRlIEpvYnMgZGVmaW5lZCBpbiBcIkFmdGVyIExvZ2luIFdvcmtmbG93KHMpXCJcbiAgICBLUC0tPj5POiBIVFRQIDMwMiBGb3VuZCAvYXBwL2Rhc2hib2FyZFxuICAgIE5vdGUgb3ZlciBLUCxCOiBTZXQtQ29va2llOiBhdXRoX3Nlc3Npb249Li4uXG4gICAgTy0tPj4tQjogSFRUUCAzMDIgRm91bmQgL2FwcC9kYXNoYm9hcmRcbiAgICBCLT4-TzogR0VUIC9hcHAvZGFzaGJvYXJkXG4gICAgTy0tPktBOiBWYWxpZGF0ZXMgU2Vzc2lvbiBDb29raWVcbiAgICBPLT4-QTogR0VUIC9kYXNoYm9hcmRcbiAgZWxzZSBMb2dpbiBkYXRhIGlzIGludmFsaWRcbiAgICBOb3RlIG92ZXIgS1AsQjogVXNlciByZXRyaWVzIGxvZ2luIC8gcmVnaXN0cmF0aW9uXG4gICAgS1AtLT4-TzogSFRUUCAzMDIgRm91bmQgL2FwcC9hdXRoLyhsb2dpbnxyZWdpc3RyYXRpb24pP3JlcXVlc3Q9YWJjZGVcbiAgICBPLS0-PkI6IEhUVFAgMzAyIEZvdW5kIC9hcHAvYXV0aC8obG9naW58cmVnaXN0cmF0aW9uKT9yZXF1ZXN0PWFiY2RlXG4gIGVuZFxuICAiLCJtZXJtYWlkIjp7InRoZW1lIjoibmV1dHJhbCIsInNlcXVlbmNlRGlhZ3JhbSI6eyJkaWFncmFtTWFyZ2luWCI6MTUsImRpYWdyYW1NYXJnaW5ZIjoxNSwiYm94VGV4dE1hcmdpbiI6MSwibm90ZU1hcmdpbiI6MTAsIm1lc3NhZ2VNYXJnaW4iOjU1LCJtaXJyb3JBY3RvcnMiOnRydWV9fSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBPIGFzIE9SWSBPYXRoa2VlcGVyXG4gIHBhcnRpY2lwYW50IEtQIGFzIE9SWSBLcmF0b3MgUHVibGljIEFQSVxuICBwYXJ0aWNpcGFudCBBIGFzIFlvdXIgU2VydmVyLVNpZGUgQXBwbGljYXRpb25cbiAgcGFydGljaXBhbnQgS0EgYXMgT1JZIEtyYXRvcyBBZG1pbiBBUElcblxuICBCLT4-K086IEdFVCAvLm9yeS9rcmF0b3MvcHVibGljL2F1dGgvYnJvd3Nlci8obG9naW58cmVnaXN0cmF0aW9uKVxuICBPLT4-K0tQOiBHRVQgL2F1dGgvYnJvd3Nlci8obG9naW58cmVnaXN0cmF0aW9uKVxuICBLUC0tPj5LUDogRXhlY3V0ZSBKb2JzIGRlZmluZWQgaW4gXCJCZWZvcmUgTG9naW4vUmVnaXN0cmF0aW9uIFdvcmtmbG93KHMpXCJcbiAgS1AtLT4-LU86IEhUVFAgMzAyIEZvdW5kIC9hcHAvYXV0aC8obG9naW58cmVnaXN0cmF0aW9uKT9yZXF1ZXN0PWFiY2RlXG4gIE8tLT4-LUI6IEhUVFAgMzAyIEZvdW5kIC9hcHAvYXV0aC8obG9naW58cmVnaXN0cmF0aW9uKT9yZXF1ZXN0PWFiY2RlXG5cbiAgQi0-PitPOiBHRVQgL2FwcC9hdXRoLyhsb2dpbnxyZWdpc3RyYXRpb24pP3JlcXVlc3Q9YWJjZGVcbiAgTy0-PitBOiBHRVQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBBLT4-K0tBOiBHRVQgL2F1dGgvYnJvd3Nlci9yZXF1ZXN0cy8obG9naW58cmVnaXN0cmF0aW9uKT9yZXF1ZXN0PWFiY2RlXG4gIEtBLT4-LUE6IFNlbmRzIExvZ2luL1JlZ2lzdHJhdGlvbiBSZXF1ZXN0IEpTT04gUGF5bG9hZFxuICBOb3RlIG92ZXIgQSxLQTogIHtcIm1ldGhvZHNcIjp7XCJwYXNzd29yZFwiOi4uLixcIm9pZGNcIjouLn19XG4gIEEtLT4-QTogR2VuZXJhdGUgYW5kIHJlbmRlciBIVE1MXG4gIEEtLT4-LU86IFJldHVybiBIVE1MIChGb3JtLCAuLi4pXG4gIE8tLT4-LUI6IFJldHVybiBIVE1MIChGb3JtLCAuLi4pXG5cbiAgQi0tPj5COiBGaWxsIG91dCBIVE1MXG5cbiAgQi0-PitPOiBQT1NUIEhUTUwgRm9ybVxuICBPLT4-K0tQOiBQT1NUIEhUTUwgRm9ybVxuICBLUC0tPj5LUDogQ2hlY2tzIGxvZ2luIC8gcmVnaXN0cmF0aW9uIGRhdGFcblxuXG4gIGFsdCBMb2dpbiBkYXRhIGlzIHZhbGlkXG4gICAgS1AtLT4-LUtQOiBFeGVjdXRlIEpvYnMgZGVmaW5lZCBpbiBcIkFmdGVyIExvZ2luIFdvcmtmbG93KHMpXCJcbiAgICBLUC0tPj5POiBIVFRQIDMwMiBGb3VuZCAvYXBwL2Rhc2hib2FyZFxuICAgIE5vdGUgb3ZlciBLUCxCOiBTZXQtQ29va2llOiBhdXRoX3Nlc3Npb249Li4uXG4gICAgTy0tPj4tQjogSFRUUCAzMDIgRm91bmQgL2FwcC9kYXNoYm9hcmRcbiAgICBCLT4-TzogR0VUIC9hcHAvZGFzaGJvYXJkXG4gICAgTy0tPktBOiBWYWxpZGF0ZXMgU2Vzc2lvbiBDb29raWVcbiAgICBPLT4-QTogR0VUIC9kYXNoYm9hcmRcbiAgZWxzZSBMb2dpbiBkYXRhIGlzIGludmFsaWRcbiAgICBOb3RlIG92ZXIgS1AsQjogVXNlciByZXRyaWVzIGxvZ2luIC8gcmVnaXN0cmF0aW9uXG4gICAgS1AtLT4-TzogSFRUUCAzMDIgRm91bmQgL2FwcC9hdXRoLyhsb2dpbnxyZWdpc3RyYXRpb24pP3JlcXVlc3Q9YWJjZGVcbiAgICBPLS0-PkI6IEhUVFAgMzAyIEZvdW5kIC9hcHAvYXV0aC8obG9naW58cmVnaXN0cmF0aW9uKT9yZXF1ZXN0PWFiY2RlXG4gIGVuZFxuICAiLCJtZXJtYWlkIjp7InRoZW1lIjoibmV1dHJhbCIsInNlcXVlbmNlRGlhZ3JhbSI6eyJkaWFncmFtTWFyZ2luWCI6MTUsImRpYWdyYW1NYXJnaW5ZIjoxNSwiYm94VGV4dE1hcmdpbiI6MSwibm90ZU1hcmdpbiI6MTAsIm1lc3NhZ2VNYXJnaW4iOjU1LCJtaXJyb3JBY3RvcnMiOnRydWV9fSwidXBkYXRlRWRpdG9yIjpmYWxzZX0) +[![User Login Sequence Diagram for Server-Side Applications](https://mermaid.ink/img/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBBIGFzIFlvdXIgU2VydmVyLVNpZGUgQXBwbGljYXRpb25cbiAgcGFydGljaXBhbnQgS1AgYXMgT1JZIEtyYXRvcyBQdWJsaWMgQVBJXG4gIHBhcnRpY2lwYW50IEtBIGFzIE9SWSBLcmF0b3MgQWRtaW4gQVBJXG5cbiAgQi0-PitBOiBHRVQgLy5vcnkva3JhdG9zL3B1YmxpYy9zZWxmLXNlcnZpY2UvYnJvd3Nlci9mbG93cy8obG9naW58cmVnaXN0cmF0aW9uKVxuICBBLT4-K0tQOiBHRVQgL3NlbGYtc2VydmljZS9icm93c2VyL2Zsb3dzL2xvZ2luKGxvZ2lufHJlZ2lzdHJhdGlvbilcbiAgS1AtLT4-S1A6IEV4ZWN1dGUgSG9va3MgZGVmaW5lZCBpbiBcIkJlZm9yZSBMb2dpbi9SZWdpc3RyYXRpb25cIlxuICBLUC0tPj4tQTogSFRUUCAzMDIgRm91bmQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBBLS0-Pi1COiBIVFRQIDMwMiBGb3VuZCAvYXV0aC8obG9naW58cmVnaXN0cmF0aW9uKT9yZXF1ZXN0PWFiY2RlXG5cbiAgQi0-PitBOiBHRVQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBBLT4-K0tBOiBHRVQvc2VsZi1zZXJ2aWNlL2Jyb3dzZXIvZmxvd3MvcmVxdWVzdHMvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBLQS0-Pi1BOiBTZW5kcyBMb2dpbi9SZWdpc3RyYXRpb24gUmVxdWVzdCBKU09OIFBheWxvYWRcbiAgTm90ZSBvdmVyIEEsS0E6ICB7XCJtZXRob2RzXCI6e1wicGFzc3dvcmRcIjouLi4sXCJvaWRjXCI6Li59fVxuICBBLS0-PkE6IEdlbmVyYXRlIGFuZCByZW5kZXIgSFRNTFxuICBBLS0-Pi1COiBSZXR1cm4gSFRNTCAoRm9ybSwgLi4uKVxuXG4gIEItLT4-QjogRmlsbCBvdXQgSFRNTFxuXG4gIEItPj4rS1A6IFBPU1QgSFRNTCBGb3JtXG4gIEtQLS0-PktQOiBDaGVja3MgbG9naW4gLyByZWdpc3RyYXRpb24gZGF0YVxuXG5cbiAgYWx0IExvZ2luIGRhdGEgaXMgdmFsaWRcbiAgICBLUC0tPj4tS1A6IEV4ZWN1dGUgSm9icyBkZWZpbmVkIGluIFwiQWZ0ZXIgTG9naW4gV29ya2Zsb3cocylcIlxuICAgIEtQLS0-PkE6IEhUVFAgMzAyIEZvdW5kIC9kYXNoYm9hcmRcbiAgICBOb3RlIG92ZXIgS1AsQjogU2V0LUNvb2tpZTogYXV0aF9zZXNzaW9uPS4uLlxuICAgIEItPj4rQTogR0VUIC9kYXNoYm9hcmRcbiAgICBBLS0-S0E6IFZhbGlkYXRlcyBTZXNzaW9uIENvb2tpZVxuICAgIEEtPj4tQjogU2VuZCBEYXNoYm9hcmQgUmVzcG9uc2VcbiAgZWxzZSBMb2dpbiBkYXRhIGlzIGludmFsaWRcbiAgICBOb3RlIG92ZXIgS1AsQjogVXNlciByZXRyaWVzIGxvZ2luIC8gcmVnaXN0cmF0aW9uXG4gICAgS1AtLT4-QjogSFRUUCAzMDIgRm91bmQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBlbmRcbiAgIiwibWVybWFpZCI6eyJ0aGVtZSI6Im5ldXRyYWwiLCJzZXF1ZW5jZURpYWdyYW0iOnsiZGlhZ3JhbU1hcmdpblgiOjE1LCJkaWFncmFtTWFyZ2luWSI6MTUsImJveFRleHRNYXJnaW4iOjEsIm5vdGVNYXJnaW4iOjEwLCJtZXNzYWdlTWFyZ2luIjo1NSwibWlycm9yQWN0b3JzIjp0cnVlfX0sInVwZGF0ZUVkaXRvciI6ZmFsc2V9)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBBIGFzIFlvdXIgU2VydmVyLVNpZGUgQXBwbGljYXRpb25cbiAgcGFydGljaXBhbnQgS1AgYXMgT1JZIEtyYXRvcyBQdWJsaWMgQVBJXG4gIHBhcnRpY2lwYW50IEtBIGFzIE9SWSBLcmF0b3MgQWRtaW4gQVBJXG5cbiAgQi0-PitBOiBHRVQgLy5vcnkva3JhdG9zL3B1YmxpYy9zZWxmLXNlcnZpY2UvYnJvd3Nlci9mbG93cy8obG9naW58cmVnaXN0cmF0aW9uKVxuICBBLT4-K0tQOiBHRVQgL3NlbGYtc2VydmljZS9icm93c2VyL2Zsb3dzL2xvZ2luKGxvZ2lufHJlZ2lzdHJhdGlvbilcbiAgS1AtLT4-S1A6IEV4ZWN1dGUgSG9va3MgZGVmaW5lZCBpbiBcIkJlZm9yZSBMb2dpbi9SZWdpc3RyYXRpb25cIlxuICBLUC0tPj4tQTogSFRUUCAzMDIgRm91bmQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBBLS0-Pi1COiBIVFRQIDMwMiBGb3VuZCAvYXV0aC8obG9naW58cmVnaXN0cmF0aW9uKT9yZXF1ZXN0PWFiY2RlXG5cbiAgQi0-PitBOiBHRVQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBBLT4-K0tBOiBHRVQvc2VsZi1zZXJ2aWNlL2Jyb3dzZXIvZmxvd3MvcmVxdWVzdHMvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBLQS0-Pi1BOiBTZW5kcyBMb2dpbi9SZWdpc3RyYXRpb24gUmVxdWVzdCBKU09OIFBheWxvYWRcbiAgTm90ZSBvdmVyIEEsS0E6ICB7XCJtZXRob2RzXCI6e1wicGFzc3dvcmRcIjouLi4sXCJvaWRjXCI6Li59fVxuICBBLS0-PkE6IEdlbmVyYXRlIGFuZCByZW5kZXIgSFRNTFxuICBBLS0-Pi1COiBSZXR1cm4gSFRNTCAoRm9ybSwgLi4uKVxuXG4gIEItLT4-QjogRmlsbCBvdXQgSFRNTFxuXG4gIEItPj4rS1A6IFBPU1QgSFRNTCBGb3JtXG4gIEtQLS0-PktQOiBDaGVja3MgbG9naW4gLyByZWdpc3RyYXRpb24gZGF0YVxuXG5cbiAgYWx0IExvZ2luIGRhdGEgaXMgdmFsaWRcbiAgICBLUC0tPj4tS1A6IEV4ZWN1dGUgSm9icyBkZWZpbmVkIGluIFwiQWZ0ZXIgTG9naW4gV29ya2Zsb3cocylcIlxuICAgIEtQLS0-PkE6IEhUVFAgMzAyIEZvdW5kIC9kYXNoYm9hcmRcbiAgICBOb3RlIG92ZXIgS1AsQjogU2V0LUNvb2tpZTogYXV0aF9zZXNzaW9uPS4uLlxuICAgIEItPj4rQTogR0VUIC9kYXNoYm9hcmRcbiAgICBBLS0-S0E6IFZhbGlkYXRlcyBTZXNzaW9uIENvb2tpZVxuICAgIEEtPj4tQjogU2VuZCBEYXNoYm9hcmQgUmVzcG9uc2VcbiAgZWxzZSBMb2dpbiBkYXRhIGlzIGludmFsaWRcbiAgICBOb3RlIG92ZXIgS1AsQjogVXNlciByZXRyaWVzIGxvZ2luIC8gcmVnaXN0cmF0aW9uXG4gICAgS1AtLT4-QjogSFRUUCAzMDIgRm91bmQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBlbmRcbiAgIiwibWVybWFpZCI6eyJ0aGVtZSI6Im5ldXRyYWwiLCJzZXF1ZW5jZURpYWdyYW0iOnsiZGlhZ3JhbU1hcmdpblgiOjE1LCJkaWFncmFtTWFyZ2luWSI6MTUsImJveFRleHRNYXJnaW4iOjEsIm5vdGVNYXJnaW4iOjEwLCJtZXNzYWdlTWFyZ2luIjo1NSwibWlycm9yQWN0b3JzIjp0cnVlfX0sInVwZGF0ZUVkaXRvciI6ZmFsc2V9) ### Client-Side Browser Applications @@ -311,98 +300,27 @@ Because Client-Side Browser Applications do not have access to ORY Kratos' Admin API, they must use the ORY Kratos Public API instead. The flow for a Client-Side Browser Application is almost the exact same as the one for Server-Side Applications, with the small difference that -`https://example.org/.ory/kratos/public/auth/browser/requests/login?request=abcde` +`https://127.0.0.1:4455/.ory/kratos/public/auth/browser/requests/login?request=abcde` would be called via AJAX instead of making a request to -`https://ory-kratos-admin.example-org.vpc/auth/browser/requests/login?request=abcde`. +`https://kratos:4434/auth/browser/requests/login?request=abcde`. -> To prevent brute force, guessing, session injection, and other attacks, it is -> required that cookies are working for this endpoint. The cookie set in the -> initial HTTP request made to -> `https://example.org/.ory/kratos/public/auth/browser/login` MUST be set and -> available when calling this endpoint! +::: Note +To prevent brute force, guessing, session injection, and other attacks, it is +required that cookies are working for this endpoint. The cookie set in the +initial HTTP request made to +`https://127.0.0.1:4455/.ory/kratos/public/auth/browser/login` MUST be set and +available when calling this endpoint! +::: ## Self-Service User Login and User Registration for API Clients Will be addressed in a future release. -## Executing Jobs before User Login +## Hooks -ORY Kratos allows you to configure jobs that run before the Login Request is -generated. This may be helpful if you'd like to restrict logins to IPs coming +ORY Kratos allows you to configure hooks that run before and after a Login or +Registration Request is generated. This may be helpful if you'd +like to restrict logins to IPs coming from your internal network or other logic. -You can find available `before` jobs in -[Self-Service Before Login Jobs](../workflows/jobs/before.md#user-login) and -configure them using the ORY Kratos configuration file: - -```yaml -selfservice: - login: - before: - - run: - config: - # -``` - -## Executing Jobs after User Login - -ORY Kratos allows you to configure jobs that run before the Login Request is -generated. This may be helpful if you'd like to restrict logins to IPs coming -from your internal network or other logic. - -You can find available `after` jobs in -[Self-Service After Login Jobs](../workflows/jobs/after.md#user-login) and -configure them using the ORY Kratos configuration file: - -```yaml -selfservice: - after: - : - - run: - config: - # -``` - -It's possible to define jobs running after login for each individual User Login -Flow Strategy (e.g. `password`, `oidc`). - -## Executing Jobs before User Registration - -ORY Kratos allows you to configure jobs that run before the Registration Request -is generated. This may be helpful if you'd like to restrict registrations to IPs -coming from your internal network or other logic. - -You can find available `before` jobs in -[Self-Service Before Registration Jobs](../workflows/jobs/before.md#user-registration) -and configure them using the ORY Kratos configuration file: - -```yaml -selfservice: - registration: - before: - - run: - config: - # -``` - -## Executing Jobs after User Registration - -ORY Kratos allows you to configure jobs that run before the Registration Request -is generated. This may be helpful if you'd like to restrict registrations to IPs -coming from your internal network or other logic. - -You can find available `after` jobs in -[Self-Service After Registration Jobs](../workflows/jobs/after.md#user-registration) -and configure them using the ORY Kratos configuration file: - -```yaml -selfservice: - after: - : - - run: - config: - # -``` - -It's possible to define jobs running after registration for each individual User -Registration Flow Strategy (e.g. `password`, `oidc`). +For more information about hooks please read the [Hook Documentation](../hooks/index.md). diff --git a/docs/docs/self-service/flows/verify-email-account-activation.mdx b/docs/docs/self-service/flows/verify-email-account-activation.mdx index c9f7331ffd5c..d6eeeccea6bc 100644 --- a/docs/docs/self-service/flows/verify-email-account-activation.mdx +++ b/docs/docs/self-service/flows/verify-email-account-activation.mdx @@ -17,7 +17,7 @@ already known. Depending on the result, one of the two flows will be executed: - Unknown email address: An email is sent to the address informing the recipient that someone tried to verify this email address but that it is not known by - the system: + the system: Verification email for unknown address; - Known email address: An email which includes a verification link is sent to the address: @@ -31,7 +31,7 @@ The emails are using templates that can be customised as explained in template IDs are: - Unknown email address: `verify_invalid` -- Known email address: `verify_valids` +- Known email address: `verify_valid` ## Account Activation @@ -118,7 +118,7 @@ selfservice: after: password: # .... - - job: verify # This sends the verification email after successful registration + - hook: verify # This sends the verification email after successful registration # .... ``` diff --git a/docs/docs/self-service/hooks/index.md b/docs/docs/self-service/hooks/index.md new file mode 100644 index 000000000000..91c97a863a2d --- /dev/null +++ b/docs/docs/self-service/hooks/index.md @@ -0,0 +1,246 @@ +--- +id: index +title: Hooks +--- + +Hooks execute logic before or after a flow (login, registration, settings, ...): + +- *Before login:* is executed when a login is initiated. +- *After login:* is executed after a login was successful. +- *Before registration:* is executed when a registration is initiated. +- *After registration:* is executed when a registration was successful: + - *Before persisting:* runs before the identity is saved in the database. + - *After persisting:* runs after the identity was saved in the database. +- *Before settings:* is executed when a settings is initiated. +- *After settings:* is executed when a settings was successful: + - *Before persisting:* runs before the identity is saved in the database. + - *After persisting:* runs after the identity was saved in the database. + +## Login + +Hooks running before & after successful user authentication are defined per +Self-Service Login Strategy in ORY Kratos' configuration file. + +### Before + +```yaml title="path/to/my/kratos.config.yml" +selfservice: + login: + before: + - hook: redirect + config: + to: https://www.ory.sh/maintenance +``` + +#### `redirect` + +The `redirect` job sends HTTP 302 Found and redirects the client +to the specified endpoint. This is useful +when you want to disable any settings functionality (e.g. due to maintenance). + +```yaml title="path/to/my/kratos.config.yml" +selfservice: + login: + before: + - hook: redirect + config: + to: https://www.ory.sh/maintenance +``` + + +### After + +```yaml title="path/to/my/kratos.config.yml" +selfservice: + login: + after: + oidc: + - hook: redirect + config: + to: https://www.ory.sh/ + password: + - hook: revoke_active_sessions +``` + +#### `redirect` + +The `redirect` job sends HTTP 302 Found and redirects the client +to the specified endpoint. This hook overrides the default redirection +behaviour and enforces the specified redirect URL. + +Using this hook should be an exception. + +```yaml title="path/to/my/kratos.config.yml" +selfservice: + login: + after: + : + - hook: redirect + config: + to: https://url-to-redirect/to +``` + +#### `revoke_active_sessions` + +The `revoke_active_sessions` will delete all active sessions for that user on +successful login: + +```yaml title="path/to/my/kratos.config.yml" +selfservice: + login: + after: + : + - hook: revoke_active_sessions + # can not be configured +``` + +## Registration + +Hooks running before & after successful user registration are defined per +Self-Service Registration Strategy in ORY Kratos' configuration file. + +### Before + +```yaml title="path/to/my/kratos.config.yml" +selfservice: + registration: + before: + - hook: redirect + config: + to: https://www.ory.sh/maintenance +``` + +#### `redirect` + +The `redirect` job sends HTTP 302 Found and redirects the client +to the specified endpoint. This is useful +when you want to disable any settings functionality (e.g. due to maintenance). + +```yaml title="path/to/my/kratos.config.yml" +selfservice: + registration: + before: + - hook: redirect + config: + to: https://www.ory.sh/maintenance +``` + +### After + +```yaml title="path/to/my/kratos.config.yml" +selfservice: + registration: + after: + oidc: + - hook: session + password: + - hook: session +``` + +#### `session` + +Adding the `session` hook signs the user immediately in once the account has been created. +It runs after the identity has been saved to the database. + +::: warn +Using this job as part of your post-registration workflow makes your system +vulnerable to +[Account Enumeration Attacks](../../concepts/security.md#account-enumeration-attacks) +because a threat agent can distinguish between existing and non-existing +accounts by checking if `Set-Cookie` was sent as part of the registration +response. +::: + +It sends a `Set-Cookie` header which contains the session +cookie. To use it, you must first define one or more (for secret rotation) +session secrets and then use it in one of the `after` work flows: + +```yaml title="path/to/my/kratos.config.yml" +secrets: + session: + - something-super-secret # The first entry will be used to sign and verify session cookies + + # All other entries will be used to verify session cookies that were signed before "something-super-secret" became + # the current signing secret. + - old-session-secret + - older-session-secret + - ancient-session-secret + +selfservice: + registration: + after: + : + - hook: session + # can not be configured +``` + +#### `redirect` + +The `redirect` hook sends HTTP 302 Found and redirects the client +to the specified endpoint. + +::: Note +Using this hook for registration disables user registration because it runs +before the identity is saved to the database. It may +be useful in cases where you temporary suspend user registration. +::: + +Using this hook should be an exception. + +```yaml title="path/to/my/kratos.config.yml" +selfservice: + registration: + after: + : + - hook: redirect + config: + to: https://url-to-redirect/to +``` + +### `verify` + +The `verify` hook checks for verifiable email addresses and sends a verification / activation +email. For more information, +please read [User Verification and Account Activation](../flows/verify-email-account-activation.mdx). + +## Settings + +Hooks running before & after successfully updating user settings and are defined per +Self-Service Settings Strategy in ORY Kratos' configuration file. + +### Before + +Settings flows do not have before hooks. + +### After + +```yaml title="path/to/my/kratos.config.yml" +selfservice: + settings: + after: + - hook: redirect + config: + to: https://www.ory.sh/ +``` + +#### `redirect` + +The `redirect` job sends HTTP 302 Found and redirects the client +to the specified endpoint. + +Per default, the settings endpoint returns to the settings page +with the original settings request ID. This is useful +when you want to show e.g. a success message indicating +that the data has successfully been saved. + +To override this behaviour, use this redirect hook. + +```yaml title="path/to/my/kratos.config.yml" +selfservice: + settings: + after: + : + - hook: redirect + config: + to: https://www.ory.sh/settings-updated +``` diff --git a/docs/docs/self-service/strategies/username-email-password.md b/docs/docs/self-service/strategies/username-email-password.md index 2b518902f948..e5e34b49f732 100644 --- a/docs/docs/self-service/strategies/username-email-password.md +++ b/docs/docs/self-service/strategies/username-email-password.md @@ -570,18 +570,11 @@ selfservice: after: password: # !! DO NOT enable `session` or all registration processes will fail!! - # - run: session + # - hook: session # You **must** enable identifier verification or no email will be sent and the registration is thus just a blank # entry in the database with no way of logging in. - - run: verify - - # You **must** enable redirection. The page should show a message such as: "Registration complete, please - # check your email for further steps". The user will be redirected to this page regardless of the registration - # status (success, invalid). - - run: redirect - config: - default_redirect_url: http://test.kratos.ory.sh:4000/registration_next_steps + - hook: verify ``` #### Disable Account Enumeration Defenses @@ -629,13 +622,8 @@ selfservice: registration: after: password: - - run: session + - hook: session # You can optionally enable verification of the provided email address(es) or phone number(s) - # - run: verify - - # You may now directly redirect to e.g. the dashboard: - - run: redirect - config: - default_redirect_url: http://test.kratos.ory.sh:4000/dashboard + # - hook: verify ``` diff --git a/docs/docs/self-service/workflows/jobs/after.md b/docs/docs/self-service/workflows/jobs/after.md deleted file mode 100644 index 46d7914d981a..000000000000 --- a/docs/docs/self-service/workflows/jobs/after.md +++ /dev/null @@ -1,172 +0,0 @@ ---- -id: after -title: After Jobs ---- - -## User Login - -Jobs running after successful user authentication can be defined per -Self-Service Login Strategy in ORY Kratos' configuration file, for example: - -```yaml -selfservice: - login: - after: - passwordless: - - job: redirect - config: - redirect_to: https://url-to-redirect/to - oidc: - - job: redirect - config: - redirect_to: https://url-to-redirect/to - password: - - job: redirect - config: - redirect_to: https://url-to-redirect/to -``` - -### `session` - -The `session` job will send a `Set-Cookie` header which contains the session -cookie. To use it, you must first define one or more (for secret rotation) -session secrets and then use it in one of the `after` work flows: - -```yaml -secrets: - session: - - something-super-secret # The first entry will be used to sign and verify session cookies - - # All other entries will be used to verify session cookies that were signed before "something-super-secret" became - # the current signing secret. - - old-session-secret - - older-session-secret - - ancient-session-secret - -selfservice: - registration: - after: - : - - job: session - # can not be configured -``` - -> This job is required for login to work, otherwise no session will be created. - -### `redirect` - -The `redirect` job will send a HTTP 302 Found response and redirect the client -to the specified endpoint. There are two configuration options available: - -```yaml -selfservice: - login: - after: - : - - job: redirect - config: - # redirect_to sets the URL the client will be redirected to. - redirect_to: https://url-to-redirect/to - - # allow_user_defined, if enabled, will check for a `?return_to` query parameter in the original request URL. - # If the parameter is set and the URL is whitelisted in `urls.whitelisted_return_to_domains` - allow_user_defined: true -``` - -> It is highly recommended to set up a redirect job after login, otherwise the -> user might get stuck on an empty, white screen. - -### `revoke_active_sessions` - -The `revoke_active_sessions` will delete all active sessions for that user on -successful login: - -```yaml -selfservice: - login: - after: - : - - job: revoke_active_sessions - # can not be configured -``` - -## User Registration - -Jobs running after successful user registration can be defined per Self-Service -Registration Strategy in ORY Kratos' configuration file, for example: - -```yaml -selfservice: - registration: - after: - passwordless: - - job: redirect - config: - redirect_to: https://url-to-redirect/to - oidc: - - job: redirect - config: - redirect_to: https://url-to-redirect/to - password: - - job: redirect - config: - redirect_to: https://url-to-redirect/to -``` - -### `session` - -The `session` job will send a `Set-Cookie` header which contains the session -cookie. To use it, you must first define one or more (for secret rotation) -session secrets and then use it in one of the `after` work flows: - -```yaml -secrets: - session: - - something-super-secret # The first entry will be used to sign and verify session cookies - - # All other entries will be used to verify session cookies that were signed before "something-super-secret" became - # the current signing secret. - - old-session-secret - - older-session-secret - - ancient-session-secret - -selfservice: - registration: - after: - : - - job: session - # can not be configured -``` - -The `session` job is useful if you want users to be signed in immediately after -registration, without further account activation or an additional login flow. - -> Using this job as part of your post-registration workflow makes your system -> vulnerable to -> [Account Enumeration Attacks](../../../concepts/security.md#account-enumeration-attacks) -> because a threat agent can distinguish between existing and non-existing -> accounts by checking if `Set-Cookie` was sent as part of the registration -> response. - -### `redirect` - -The `redirect` job will send a HTTP 302 Found response and redirect the client -to the specified endpoint. There are two configuration options available: - -```yaml -selfservice: - registration: - after: - : - - job: redirect - config: - # redirect_to sets the URL the client will be redirected to. - redirect_to: https://url-to-redirect/to - - # allow_user_defined, if enabled, will check for a `?return_to` query parameter in the original request URL. - # If the parameter is set and the URL is whitelisted in `urls.whitelisted_return_to_domains` - allow_user_defined: true -``` - -> It is highly recommended to set up a redirect job after registration, -> otherwise the user might get stuck on an empty, white screen. diff --git a/docs/docs/self-service/workflows/jobs/before.md b/docs/docs/self-service/workflows/jobs/before.md deleted file mode 100644 index a047751a165e..000000000000 --- a/docs/docs/self-service/workflows/jobs/before.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: before -title: Before Jobs ---- - -There are currently no jobs available for before workflows. Jobs will be added -in the future. Please -[open an issue on ORY Kratos' GitHub](https://github.com/ory/kratos/issues/new/choose) -if you have use cases. diff --git a/docs/sidebar.js b/docs/sidebar.js index 1acc9ecb4ba9..72b090c4ba85 100644 --- a/docs/sidebar.js +++ b/docs/sidebar.js @@ -35,9 +35,8 @@ module.exports = { }, { type: "category", - label: "Hooks / Jobs", items: [ - "self-service/workflows/jobs/before", - "self-service/workflows/jobs/after" + label: "Hooks", items: [ + "self-service/hooks/index" ] } ], diff --git a/driver/configuration/provider.go b/driver/configuration/provider.go index af6bbe70243a..781d8097e128 100644 --- a/driver/configuration/provider.go +++ b/driver/configuration/provider.go @@ -20,7 +20,7 @@ type HasherArgon2Config struct { } type SelfServiceHook struct { - Job string `json:"job"` + Name string `json:"hook"` Config json.RawMessage `json:"config"` } @@ -76,8 +76,11 @@ type Provider interface { SelfServiceLoginBeforeHooks() []SelfServiceHook SelfServiceRegistrationBeforeHooks() []SelfServiceHook SelfServiceLoginAfterHooks(strategy string) []SelfServiceHook + SelfServiceLoginReturnTo(strategy string) *url.URL SelfServiceRegistrationAfterHooks(strategy string) []SelfServiceHook + SelfServiceRegistrationReturnTo(strategy string) *url.URL SelfServiceSettingsAfterHooks(strategy string) []SelfServiceHook + SelfServiceSettingsReturnTo(strategy string, defaultReturnTo *url.URL) *url.URL SelfServiceLogoutRedirectURL() *url.URL SelfServiceVerificationLinkLifespan() time.Duration SelfServicePrivilegedSessionMaxAge() time.Duration diff --git a/driver/configuration/provider_viper.go b/driver/configuration/provider_viper.go index 0852a30f76a3..9ccf7af514e1 100644 --- a/driver/configuration/provider_viper.go +++ b/driver/configuration/provider_viper.go @@ -41,35 +41,41 @@ const ( ViperKeySecretsSession = "secrets.session" - ViperKeyURLsDefaultReturnTo = "urls.default_return_to" + ViperKeyDefaultReturnTo = "default_return_to" + ViperKeyURLsDefaultReturnTo = "urls." + ViperKeyDefaultReturnTo ViperKeyURLsSelfPublic = "urls.self.public" ViperKeyURLsSelfAdmin = "urls.self.admin" ViperKeyURLsLogin = "urls.login_ui" ViperKeyURLsError = "urls.error_ui" ViperKeyURLsVerification = "urls.verify_ui" - ViperKeyURLsProfile = "urls.settings_ui" + ViperKeyURLsSettings = "urls.settings_ui" ViperKeyURLsMFA = "urls.mfa_ui" ViperKeyURLsRegistration = "urls.registration_ui" - ViperKeyURLsWhitelistedReturnToDomains = "urls.whitelisted_return_to_domains" + ViperKeyURLsWhitelistedReturnToDomains = "urls.whitelisted_return_to_urls" ViperKeyLifespanSession = "ttl.session" ViperKeySessionSameSite = "security.session.cookie.same_site" - ViperKeySelfServiceStrategyConfig = "selfservice.strategies" - ViperKeySelfServiceRegistrationBeforeConfig = "selfservice.registration.before" - ViperKeySelfServiceRegistrationAfterConfig = "selfservice.registration.after" - ViperKeySelfServiceLifespanRegistrationRequest = "selfservice.registration.request_lifespan" - ViperKeySelfServiceLoginBeforeConfig = "selfservice.login.before" - ViperKeySelfServiceLoginAfterConfig = "selfservice.login.after" - ViperKeySelfServiceLifespanLoginRequest = "selfservice.login.request_lifespan" - ViperKeySelfServiceLogoutRedirectURL = "selfservice.logout.redirect_to" - ViperKeySelfServiceSettingsAfterConfig = "selfservice.settings.after" + ViperKeySelfServiceStrategyConfig = "selfservice.strategies" + + ViperKeySelfServiceRegistrationAfter = "selfservice.registration.after" + ViperKeySelfServiceRegistrationBeforeHooks = "selfservice.registration.before.hooks" + ViperKeySelfServiceLifespanRegistrationRequest = "selfservice.registration.request_lifespan" + + ViperKeySelfServiceLoginAfter = "selfservice.login.after" + ViperKeySelfServiceLoginBeforeHooks = "selfservice.login.before.hooks" + ViperKeySelfServiceLifespanLoginRequest = "selfservice.login.request_lifespan" + + ViperKeySelfServiceLogoutRedirectURL = "selfservice.logout.redirect_to" + + ViperKeySelfServiceSettingsAfter = "selfservice.settings.after" ViperKeySelfServiceSettingsRequestLifespan = "selfservice.settings.request_lifespan" ViperKeySelfServicePrivilegedAuthenticationAfter = "selfservice.settings.privileged_session_max_age" - ViperKeySelfServiceLifespanLink = "selfservice.verify.link_lifespan" - ViperKeySelfServiceLifespanVerificationRequest = "selfservice.verify.request_lifespan" - ViperKeySelfServiceVerifyReturnTo = "selfservice.verify.return_to" + + ViperKeySelfServiceLifespanLink = "selfservice.verify.link_lifespan" + ViperKeySelfServiceLifespanVerificationRequest = "selfservice.verify.request_lifespan" + ViperKeySelfServiceVerifyReturnTo = "selfservice.verify.return_to" ViperKeyDefaultIdentityTraitsSchemaURL = "identity.traits.default_schema_url" ViperKeyIdentityTraitsSchemas = "identity.traits.schemas" @@ -81,6 +87,10 @@ const ( ViperKeyHasherArgon2ConfigKeyLength = "hashers.argon2.key_length" ) +func HookStrategyKey(key, strategy string) string { + return fmt.Sprintf("%s.%s.hooks", key, strategy) +} + func NewViperProvider(l logrus.FieldLogger, dev bool) *ViperProvider { return &ViperProvider{ l: l, @@ -164,11 +174,11 @@ func (p *ViperProvider) DSN() string { } func (p *ViperProvider) SelfServiceLoginBeforeHooks() []SelfServiceHook { - return p.selfServiceHooks(ViperKeySelfServiceLoginBeforeConfig) + return p.selfServiceHooks(ViperKeySelfServiceLoginBeforeHooks) } func (p *ViperProvider) SelfServiceRegistrationBeforeHooks() []SelfServiceHook { - return p.selfServiceHooks(ViperKeySelfServiceRegistrationBeforeConfig) + return p.selfServiceHooks(ViperKeySelfServiceRegistrationBeforeHooks) } func (p *ViperProvider) selfServiceHooks(key string) []SelfServiceHook { @@ -198,15 +208,15 @@ func (p *ViperProvider) selfServiceHooks(key string) []SelfServiceHook { } func (p *ViperProvider) SelfServiceLoginAfterHooks(strategy string) []SelfServiceHook { - return p.selfServiceHooks(ViperKeySelfServiceLoginAfterConfig + "." + strategy) + return p.selfServiceHooks(HookStrategyKey(ViperKeySelfServiceLoginAfter, strategy)) } func (p *ViperProvider) SelfServiceSettingsAfterHooks(strategy string) []SelfServiceHook { - return p.selfServiceHooks(ViperKeySelfServiceSettingsAfterConfig + "." + strategy) + return p.selfServiceHooks(HookStrategyKey(ViperKeySelfServiceSettingsAfter, strategy)) } func (p *ViperProvider) SelfServiceRegistrationAfterHooks(strategy string) []SelfServiceHook { - return p.selfServiceHooks(ViperKeySelfServiceRegistrationAfterConfig + "." + strategy) + return p.selfServiceHooks(HookStrategyKey(ViperKeySelfServiceRegistrationAfter, strategy)) } func (p *ViperProvider) SelfServiceStrategy(strategy string) *SelfServiceStrategy { @@ -270,7 +280,7 @@ func (p *ViperProvider) LoginURL() *url.URL { } func (p *ViperProvider) SettingsURL() *url.URL { - return mustParseURLFromViper(p.l, ViperKeyURLsProfile) + return mustParseURLFromViper(p.l, ViperKeyURLsSettings) } func (p *ViperProvider) ErrorURL() *url.URL { @@ -390,3 +400,30 @@ func (p *ViperProvider) SessionSameSiteMode() http.SameSite { } return http.SameSiteDefaultMode } + +func (p *ViperProvider) SelfServiceLoginReturnTo(strategy string) *url.URL { + return p.selfServiceReturnTo(ViperKeySelfServiceLoginAfter, strategy) +} + +func (p *ViperProvider) SelfServiceRegistrationReturnTo(strategy string) *url.URL { + return p.selfServiceReturnTo(ViperKeySelfServiceRegistrationAfter, strategy) +} + +func (p *ViperProvider) SelfServiceSettingsReturnTo(strategy string, defaultReturnTo *url.URL) *url.URL { + redir, err := url.ParseRequestURI( + viperx.GetString(p.l, ViperKeySelfServiceSettingsAfter+"."+strategy+"."+ViperKeyDefaultReturnTo, + viperx.GetString(p.l, ViperKeySelfServiceSettingsAfter+"."+ViperKeyDefaultReturnTo, ""))) + if err != nil { + return defaultReturnTo + } + return redir +} + +func (p *ViperProvider) selfServiceReturnTo(key string, strategy string) *url.URL { + redir, err := url.ParseRequestURI( + viperx.GetString(p.l, key+"."+strategy+"."+ViperKeyDefaultReturnTo, viperx.GetString(p.l, key+"."+ViperKeyDefaultReturnTo, ""))) + if err != nil { + return p.DefaultReturnToURL() + } + return redir +} diff --git a/driver/configuration/provider_viper_test.go b/driver/configuration/provider_viper_test.go index 918f40c2bc8f..20a9ec1fc2c6 100644 --- a/driver/configuration/provider_viper_test.go +++ b/driver/configuration/provider_viper_test.go @@ -1,6 +1,7 @@ package configuration_test import ( + "encoding/json" "testing" "time" @@ -49,6 +50,17 @@ func TestViperProvider(t *testing.T) { }, ds) }) + t.Run("group=default_return_to", func(t *testing.T) { + assert.Equal(t, "https://self-service/login/password/return_to", p.SelfServiceLoginReturnTo("password").String()) + assert.Equal(t, "https://self-service/login/return_to", p.SelfServiceLoginReturnTo("oidc").String()) + + assert.Equal(t, "https://self-service/registration/return_to", p.SelfServiceRegistrationReturnTo("password").String()) + assert.Equal(t, "https://self-service/registration/oidc/return_to", p.SelfServiceRegistrationReturnTo("oidc").String()) + + assert.Equal(t, "https://self-service/settings/password/return_to", p.SelfServiceSettingsReturnTo("password", p.DefaultReturnToURL()).String()) + assert.Equal(t, "https://self-service/settings/return_to", p.SelfServiceSettingsReturnTo("profile", p.DefaultReturnToURL()).String()) + }) + t.Run("group=identity", func(t *testing.T) { assert.Equal(t, "http://test.kratos.ory.sh/default-identity.schema.json", p.DefaultIdentityTraitsSchemaURL().String()) @@ -101,35 +113,36 @@ func TestViperProvider(t *testing.T) { t.Run("hook=before", func(t *testing.T) { hook := p.SelfServiceRegistrationBeforeHooks()[0] - assert.EqualValues(t, "redirect", hook.Job) + assert.EqualValues(t, "redirect", hook.Name) assert.JSONEq(t, `{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`, string(hook.Config)) }) for _, tc := range []struct { - strategy string - redirectConfig string + strategy string + hooks []configuration.SelfServiceHook }{ { - strategy: "password", - redirectConfig: `{"allow_user_defined_redirect":true,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`, + strategy: "password", + hooks: []configuration.SelfServiceHook{ + {Name: "session", Config: json.RawMessage(`{}`)}, + {Name: "verify", Config: json.RawMessage(`{}`)}, + {Name: "redirect", Config: json.RawMessage(`{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`)}, + }, }, { - strategy: "oidc", - redirectConfig: `{"allow_user_defined_redirect":true,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`, + strategy: "oidc", + hooks: []configuration.SelfServiceHook{ + {Name: "verify", Config: json.RawMessage(`{}`)}, + {Name: "session", Config: json.RawMessage(`{}`)}, + {Name: "redirect", Config: json.RawMessage(`{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`)}, + }, }, } { t.Run("hook=after/strategy="+tc.strategy, func(t *testing.T) { hooks := p.SelfServiceRegistrationAfterHooks(tc.strategy) - - hook := hooks[0] - assert.EqualValues(t, "session", hook.Job) - - hook = hooks[1] - assert.EqualValues(t, "redirect", hook.Job) - assert.JSONEq(t, tc.redirectConfig, string(hook.Config)) + assert.Equal(t, tc.hooks, hooks) }) } - }) t.Run("method=login", func(t *testing.T) { @@ -137,35 +150,62 @@ func TestViperProvider(t *testing.T) { t.Run("hook=before", func(t *testing.T) { hook := p.SelfServiceLoginBeforeHooks()[0] - assert.EqualValues(t, "redirect", hook.Job) + assert.EqualValues(t, "redirect", hook.Name) assert.JSONEq(t, `{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`, string(hook.Config)) }) for _, tc := range []struct { - strategy string - redirectConfig string + strategy string + hooks []configuration.SelfServiceHook }{ { - strategy: "password", - redirectConfig: `{"allow_user_defined_redirect":true,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`, + strategy: "password", + hooks: []configuration.SelfServiceHook{ + {Name: "revoke_active_sessions", Config: json.RawMessage(`{}`)}, + }, }, { - strategy: "oidc", - redirectConfig: `{"allow_user_defined_redirect":true,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`, + strategy: "oidc", + hooks: []configuration.SelfServiceHook{ + {Name: "revoke_active_sessions", Config: json.RawMessage(`{}`)}, + }, }, } { t.Run("hook=after/strategy="+tc.strategy, func(t *testing.T) { hooks := p.SelfServiceLoginAfterHooks(tc.strategy) + assert.Equal(t, tc.hooks, hooks) + }) + } + }) - hook := hooks[0] - assert.EqualValues(t, "revoke_active_sessions", hook.Job) + t.Run("method=settings", func(t *testing.T) { + assert.Equal(t, time.Minute*99, p.SelfServiceSettingsRequestLifespan()) + assert.Equal(t, time.Minute*5, p.SelfServicePrivilegedSessionMaxAge()) - hook = hooks[1] - assert.EqualValues(t, "session", hook.Job) + t.Run("hook=before", func(t *testing.T) { + hook := p.SelfServiceLoginBeforeHooks()[0] + assert.EqualValues(t, "redirect", hook.Name) + assert.JSONEq(t, `{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`, string(hook.Config)) + }) - hook = hooks[2] - assert.EqualValues(t, "redirect", hook.Job) - assert.JSONEq(t, tc.redirectConfig, string(hook.Config)) + for _, tc := range []struct { + strategy string + hooks []configuration.SelfServiceHook + }{ + { + strategy: "password", + hooks: []configuration.SelfServiceHook{}, + }, + { + strategy: "profile", + hooks: []configuration.SelfServiceHook{ + {Name: "verify", Config: json.RawMessage(`{}`)}, + }, + }, + } { + t.Run("hook=after/strategy="+tc.strategy, func(t *testing.T) { + hooks := p.SelfServiceSettingsAfterHooks(tc.strategy) + assert.Equal(t, tc.hooks, hooks) }) } }) diff --git a/driver/registry_default.go b/driver/registry_default.go index ccbbcc9f0724..67d94b376048 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -10,6 +10,7 @@ import ( "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/flow/verify" + "github.com/ory/kratos/selfservice/hook" "github.com/ory/kratos/x" "github.com/cenkalti/backoff" @@ -55,6 +56,8 @@ type RegistryDefault struct { l logrus.FieldLogger c configuration.Provider + injectedSelfserviceHooks map[string]func(configuration.SelfServiceHook) interface{} + nosurf x.CSRFHandler trc *tracing.Tracer writer herodot.Writer @@ -63,6 +66,10 @@ type RegistryDefault struct { courier *courier.Courier persister persistence.Persister + hookVerifier *hook.Verifier + hookSessionIssuer *hook.SessionIssuer + hookSessionDestroyer *hook.SessionDestroyer + identityHandler *identity.Handler identityValidator *identity.Validator identityManager *identity.Manager diff --git a/driver/registry_default_hooks.go b/driver/registry_default_hooks.go index ea85557b9f9f..a8a5b0590554 100644 --- a/driver/registry_default_hooks.go +++ b/driver/registry_default_hooks.go @@ -1,77 +1,67 @@ package driver import ( - "bytes" "encoding/json" - "fmt" - "net/url" "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/selfservice/hook" ) -func (m *RegistryDefault) getHooks(credentialsType string, configs []configuration.SelfServiceHook) []interface{} { - var i []interface{} +func (m *RegistryDefault) HookVerifier() *hook.Verifier { + if m.hookVerifier == nil { + m.hookVerifier = hook.NewVerifier(m) + } + return m.hookVerifier +} + +func (m *RegistryDefault) HookSessionIssuer() *hook.SessionIssuer { + if m.hookSessionIssuer == nil { + m.hookSessionIssuer = hook.NewSessionIssuer(m) + } + return m.hookSessionIssuer +} + +func (m *RegistryDefault) HookSessionDestroyer() *hook.SessionDestroyer { + if m.hookSessionDestroyer == nil { + m.hookSessionDestroyer = hook.NewSessionDestroyer(m) + } + return m.hookSessionDestroyer +} + +func (m *RegistryDefault) HookRedirector(config json.RawMessage) *hook.Redirector { + return hook.NewRedirector(config) +} +func (m *RegistryDefault) WithHooks(hooks map[string]func(configuration.SelfServiceHook) interface{}) { + m.injectedSelfserviceHooks = hooks +} + +func (m *RegistryDefault) getHooks(credentialsType string, configs []configuration.SelfServiceHook) (i []interface{}) { for _, h := range configs { - switch h.Job { + switch h.Name { case hook.KeyVerify: - i = append( - i, - hook.NewVerifier(m), - ) + i = append(i, m.HookVerifier()) case hook.KeySessionIssuer: - i = append( - i, - hook.NewSessionIssuer(m), - ) + i = append(i, m.HookSessionIssuer()) case hook.KeySessionDestroyer: - i = append( - i, - hook.NewSessionDestroyer(m), - ) + i = append(i, m.HookSessionDestroyer()) case hook.KeyRedirector: - var rc struct { - R string `json:"default_redirect_url"` - A bool `json:"allow_user_defined_redirect"` - } - - if err := json.NewDecoder(bytes.NewBuffer(h.Config)).Decode(&rc); err != nil { - m.l.WithError(err). - WithField("type", credentialsType). - WithField("hook", h.Job). - WithField("config", fmt.Sprintf("%s", h.Config)). - Errorf("The after hook is misconfigured.") - continue + i = append(i, m.HookRedirector(h.Config)) + default: + var found bool + for name, m := range m.injectedSelfserviceHooks { + if name == h.Name { + i = append(i, m(h)) + found = true + break + } } - - rcr, err := url.ParseRequestURI(rc.R) - if err != nil { - m.l.WithError(err). - WithField("type", credentialsType). - WithField("hook", h.Job). - WithField("config", fmt.Sprintf("%s", h.Config)). - Errorf("The after hook is misconfigured.") + if found { continue } - - i = append( - i, - hook.NewRedirector( - func() *url.URL { - return rcr - }, - m.c.WhitelistedReturnToDomains, - func() bool { - return rc.A - }, - m.c.SelfPublicURL, - ), - ) - default: m.l. - WithField("type", credentialsType). - WithField("hook", h.Job). + WithField("for", credentialsType). + WithField("hook", h.Name). Errorf("A unknown hook was requested and can therefore not be used") } } diff --git a/driver/registry_default_login.go b/driver/registry_default_login.go index eda9353fb04a..e4a41e80ad69 100644 --- a/driver/registry_default_login.go +++ b/driver/registry_default_login.go @@ -12,22 +12,22 @@ func (m *RegistryDefault) LoginHookExecutor() *login.HookExecutor { return m.selfserviceLoginExecutor } -func (m *RegistryDefault) PreLoginHooks() []login.PreHookExecutor { - return []login.PreHookExecutor{} +func (m *RegistryDefault) PreLoginHooks() (b []login.PreHookExecutor) { + for _, v := range m.getHooks("", m.c.SelfServiceLoginBeforeHooks()) { + if hook, ok := v.(login.PreHookExecutor); ok { + b = append(b, hook) + } + } + return } -func (m *RegistryDefault) PostLoginHooks(credentialsType identity.CredentialsType) []login.PostHookExecutor { - a := m.getHooks(string(credentialsType), m.c.SelfServiceLoginAfterHooks(string(credentialsType))) - - var b []login.PostHookExecutor - - for _, v := range a { +func (m *RegistryDefault) PostLoginHooks(credentialsType identity.CredentialsType) (b []login.PostHookExecutor) { + for _, v := range m.getHooks(string(credentialsType), m.c.SelfServiceLoginAfterHooks(string(credentialsType))) { if hook, ok := v.(login.PostHookExecutor); ok { b = append(b, hook) } } - - return b + return } func (m *RegistryDefault) LoginHandler() *login.Handler { diff --git a/driver/registry_default_profile.go b/driver/registry_default_profile.go index 9c97b9b527b9..e83f30d20691 100644 --- a/driver/registry_default_profile.go +++ b/driver/registry_default_profile.go @@ -2,20 +2,24 @@ package driver import "github.com/ory/kratos/selfservice/flow/settings" -func (m *RegistryDefault) PostSettingsHooks(credentialsType string) []settings.PostHookExecutor { - a := m.getHooks(credentialsType, m.c.SelfServiceSettingsAfterHooks(credentialsType)) - - var b []settings.PostHookExecutor - for _, v := range a { - if hook, ok := v.(settings.PostHookExecutor); ok { +func (m *RegistryDefault) PostSettingsPrePersistHooks(settingsType string) (b []settings.PostHookPrePersistExecutor) { + for _, v := range m.getHooks(settingsType, m.c.SelfServiceSettingsAfterHooks(settingsType)) { + if hook, ok := v.(settings.PostHookPrePersistExecutor); ok { b = append(b, hook) } } - - return b + return +} +func (m *RegistryDefault) PostSettingsPostPersistHooks(settingsType string) (b []settings.PostHookPostPersistExecutor) { + for _, v := range m.getHooks(settingsType, m.c.SelfServiceSettingsAfterHooks(settingsType)) { + if hook, ok := v.(settings.PostHookPostPersistExecutor); ok { + b = append(b, hook) + } + } + return } -func (m *RegistryDefault) SettingsExecutor() *settings.HookExecutor { +func (m *RegistryDefault) SettingsHookExecutor() *settings.HookExecutor { if m.selfserviceSettingsExecutor == nil { m.selfserviceSettingsExecutor = settings.NewHookExecutor(m, m.c) } diff --git a/driver/registry_default_registration.go b/driver/registry_default_registration.go index f53a24d0d745..552d9fe1f2a7 100644 --- a/driver/registry_default_registration.go +++ b/driver/registry_default_registration.go @@ -5,23 +5,33 @@ import ( "github.com/ory/kratos/selfservice/flow/registration" ) -func (m *RegistryDefault) PostRegistrationHooks(credentialsType identity.CredentialsType) []registration.PostHookExecutor { - a := m.getHooks(string(credentialsType), m.c.SelfServiceRegistrationAfterHooks(string(credentialsType))) - - var b []registration.PostHookExecutor - - for _, v := range a { - if hook, ok := v.(registration.PostHookExecutor); ok { +func (m *RegistryDefault) PostRegistrationPrePersistHooks(credentialsType identity.CredentialsType) (b []registration.PostHookPrePersistExecutor) { + for _, v := range m.getHooks(string(credentialsType), m.c.SelfServiceRegistrationAfterHooks(string(credentialsType))) { + if hook, ok := v.(registration.PostHookPrePersistExecutor); ok { b = append(b, hook) } } + return +} - return b +func (m *RegistryDefault) PostRegistrationPostPersistHooks(credentialsType identity.CredentialsType) (b []registration.PostHookPostPersistExecutor) { + for _, v := range m.getHooks(string(credentialsType), m.c.SelfServiceRegistrationAfterHooks(string(credentialsType))) { + if hook, ok := v.(registration.PostHookPostPersistExecutor); ok { + b = append(b, hook) + } + } + return } -func (m *RegistryDefault) PreRegistrationHooks() []registration.PreHookExecutor { - return []registration.PreHookExecutor{} +func (m *RegistryDefault) PreRegistrationHooks() (b []registration.PreHookExecutor) { + for _, v := range m.getHooks("", m.c.SelfServiceRegistrationBeforeHooks()) { + if hook, ok := v.(registration.PreHookExecutor); ok { + b = append(b, hook) + } + } + return } + func (m *RegistryDefault) RegistrationExecutor() *registration.HookExecutor { if m.selfserviceRegistrationExecutor == nil { m.selfserviceRegistrationExecutor = registration.NewHookExecutor(m, m.c) diff --git a/driver/registry_default_schemas_test.go b/driver/registry_default_schemas_test.go index 15835b0c9a52..7a99e38434e4 100644 --- a/driver/registry_default_schemas_test.go +++ b/driver/registry_default_schemas_test.go @@ -13,7 +13,7 @@ import ( ) func TestRegistryDefault_IdentityTraitsSchemas(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) defaultSchema := schema.Schema{ ID: "default", URL: urlx.ParseOrPanic("file://default.schema.json"), diff --git a/identity/handler_test.go b/identity/handler_test.go index 23f21844fe04..2eb949a08383 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -25,7 +25,7 @@ import ( ) func TestHandler(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) router := x.NewRouterAdmin() reg.IdentityHandler().RegisterAdminRoutes(router) ts := httptest.NewServer(router) diff --git a/identity/manager_test.go b/identity/manager_test.go index 916a196d8566..85c5d16e2ce4 100644 --- a/identity/manager_test.go +++ b/identity/manager_test.go @@ -17,7 +17,7 @@ import ( ) func TestManager(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/manager.schema.json") viper.Set(configuration.ViperKeyURLsSelfPublic, "https://www.ory.sh/") viper.Set(configuration.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") diff --git a/identity/validator_test.go b/identity/validator_test.go index 1c0003bb00f4..6d174593488a 100644 --- a/identity/validator_test.go +++ b/identity/validator_test.go @@ -50,7 +50,7 @@ func TestSchemaValidator(t *testing.T) { ts := httptest.NewServer(router) defer ts.Close() - conf, reg := internal.NewRegistryDefault(t) + conf, reg := internal.NewFastRegistryWithMocks(t) viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, ts.URL+"/schema/firstName") viper.Set(configuration.ViperKeyIdentityTraitsSchemas, []configuration.SchemaConfig{ {ID: "whatever", URL: ts.URL + "/schema/whatever"}, diff --git a/internal/.kratos.yaml b/internal/.kratos.yaml index f9830be2896a..84892639dff8 100644 --- a/internal/.kratos.yaml +++ b/internal/.kratos.yaml @@ -27,7 +27,7 @@ urls: public: http://public.kratos.ory.sh admin: http://admin.kratos.ory.sh error_ui: http://test.kratos.ory.sh/error - whitelisted_return_to_domains: + whitelisted_return_to_urls: - http://return-to-1-test.ory.sh/ - http://return-to-2-test.ory.sh/ @@ -66,58 +66,65 @@ selfservice: schema_url: http://test.kratos.ory.sh/default-identity.schema.json logout: redirect_to: http://test.kratos.ory.sh:4000/ - login: + + settings: request_lifespan: 99m - before: - - job: redirect - config: - default_redirect_url: http://test.kratos.ory.sh:4000/ - allow_user_defined_redirect: false + privileged_session_max_age: 5m after: + default_return_to: https://self-service/settings/return_to password: - - - job: revoke_active_sessions - - job: session - - job: redirect + default_return_to: https://self-service/settings/password/return_to + profile: + hooks: + - + hook: verify + login: + request_lifespan: 99m + before: + hooks: + - hook: redirect config: default_redirect_url: http://test.kratos.ory.sh:4000/ - allow_user_defined_redirect: true + allow_user_defined_redirect: false + after: + default_return_to: https://self-service/login/return_to + password: + default_return_to: https://self-service/login/password/return_to + hooks: + - + hook: revoke_active_sessions oidc: - - - job: revoke_active_sessions - - job: session - - job: redirect - config: - default_redirect_url: http://test.kratos.ory.sh:4000/ - allow_user_defined_redirect: true + hooks: + - + hook: revoke_active_sessions registration: request_lifespan: 98m before: - - job: redirect - config: - default_redirect_url: http://test.kratos.ory.sh:4000/ - allow_user_defined_redirect: false - after: - password: - - - job: session - - - job: redirect + hooks: + - hook: redirect config: default_redirect_url: http://test.kratos.ory.sh:4000/ - allow_user_defined_redirect: true + allow_user_defined_redirect: false + after: + default_return_to: https://self-service/registration/return_to + password: + hooks: + - + hook: session + - + hook: verify + - hook: redirect + config: + default_redirect_url: http://test.kratos.ory.sh:4000/ + allow_user_defined_redirect: false oidc: - - - job: session - - - job: redirect - config: - default_redirect_url: http://test.kratos.ory.sh:4000/ - allow_user_defined_redirect: true - -# - job: account_activation -# config: -# redirect: -# pending: -# success: -# invalidate_after: 10h \ No newline at end of file + default_return_to: https://self-service/registration/oidc/return_to + hooks: + - + hook: verify + - + hook: session + - hook: redirect + config: + default_redirect_url: http://test.kratos.ory.sh:4000/ + allow_user_defined_redirect: false diff --git a/internal/driver.go b/internal/driver.go index 807dcefcf4f5..746fe3454ab3 100644 --- a/internal/driver.go +++ b/internal/driver.go @@ -14,6 +14,7 @@ import ( "github.com/ory/kratos/driver" "github.com/ory/kratos/driver/configuration" + "github.com/ory/kratos/selfservice/hook" "github.com/ory/kratos/x" ) @@ -34,14 +35,23 @@ func NewConfigurationWithDefaults() *configuration.ViperProvider { return configuration.NewViperProvider(logrusx.New(), true) } -func NewRegistryDefault(t *testing.T) (*configuration.ViperProvider, *driver.RegistryDefault) { +// 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) (*configuration.ViperProvider, *driver.RegistryDefault) { conf, reg := NewRegistryDefaultWithDSN(t, "") reg.WithCSRFTokenGenerator(x.FakeCSRFTokenGenerator) reg.WithCSRFHandler(x.NewFakeCSRFHandler("")) + reg.WithHooks(map[string]func(configuration.SelfServiceHook) interface{}{ + "err": func(c configuration.SelfServiceHook) interface{} { + return &hook.Error{Config: c.Config} + }, + }) + require.NoError(t, reg.Persister().MigrateUp(context.Background())) return conf, reg } +// NewRegistryDefaultWithDSN returns a more standard registry without mocks. Good for e2e and advanced integration testing! func NewRegistryDefaultWithDSN(t *testing.T, dsn string) (*configuration.ViperProvider, *driver.RegistryDefault) { viper.Reset() resetConfig() diff --git a/internal/faker.go b/internal/faker.go index b2ac8a4478d2..b43e482186d2 100644 --- a/internal/faker.go +++ b/internal/faker.go @@ -93,7 +93,7 @@ func RegisterFakes() { if err := faker.AddProvider("settings_request_methods", func(v reflect.Value) (interface{}, error) { var methods = make(map[string]*settings.RequestMethod) - for _, ct := range []string{settings.StrategyTraitsID, string(identity.CredentialsTypePassword), string(identity.CredentialsTypeOIDC)} { + for _, ct := range []string{settings.StrategyProfile, string(identity.CredentialsTypePassword), string(identity.CredentialsTypeOIDC)} { var f form.HTMLForm if err := faker.FakeData(&f); err != nil { return nil, err diff --git a/internal/httpclient/client/admin/create_identity_parameters.go b/internal/httpclient/client/admin/create_identity_parameters.go index 518e607d5f39..535dd83d420f 100644 --- a/internal/httpclient/client/admin/create_identity_parameters.go +++ b/internal/httpclient/client/admin/create_identity_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/admin/create_identity_responses.go b/internal/httpclient/client/admin/create_identity_responses.go index 5e2173f5c4c9..05f6ca1042cd 100644 --- a/internal/httpclient/client/admin/create_identity_responses.go +++ b/internal/httpclient/client/admin/create_identity_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/admin/delete_identity_parameters.go b/internal/httpclient/client/admin/delete_identity_parameters.go index 0c5d40832174..77e6c1f84f25 100644 --- a/internal/httpclient/client/admin/delete_identity_parameters.go +++ b/internal/httpclient/client/admin/delete_identity_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewDeleteIdentityParams creates a new DeleteIdentityParams object diff --git a/internal/httpclient/client/admin/delete_identity_responses.go b/internal/httpclient/client/admin/delete_identity_responses.go index 5b1140d26aef..bb286ae9f204 100644 --- a/internal/httpclient/client/admin/delete_identity_responses.go +++ b/internal/httpclient/client/admin/delete_identity_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/admin/get_identity_parameters.go b/internal/httpclient/client/admin/get_identity_parameters.go index b1a377d82aef..060833223b1e 100644 --- a/internal/httpclient/client/admin/get_identity_parameters.go +++ b/internal/httpclient/client/admin/get_identity_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewGetIdentityParams creates a new GetIdentityParams object diff --git a/internal/httpclient/client/admin/get_identity_responses.go b/internal/httpclient/client/admin/get_identity_responses.go index 9a8941f9f074..9bd6ed7088b7 100644 --- a/internal/httpclient/client/admin/get_identity_responses.go +++ b/internal/httpclient/client/admin/get_identity_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/admin/list_identities_parameters.go b/internal/httpclient/client/admin/list_identities_parameters.go index b88e99230734..69d3f8622bfe 100644 --- a/internal/httpclient/client/admin/list_identities_parameters.go +++ b/internal/httpclient/client/admin/list_identities_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewListIdentitiesParams creates a new ListIdentitiesParams object diff --git a/internal/httpclient/client/admin/list_identities_responses.go b/internal/httpclient/client/admin/list_identities_responses.go index c5f85b4f5691..4e2a23c2db7a 100644 --- a/internal/httpclient/client/admin/list_identities_responses.go +++ b/internal/httpclient/client/admin/list_identities_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/admin/update_identity_parameters.go b/internal/httpclient/client/admin/update_identity_parameters.go index 7dfd612b8a75..5797e6f05b5d 100644 --- a/internal/httpclient/client/admin/update_identity_parameters.go +++ b/internal/httpclient/client/admin/update_identity_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/admin/update_identity_responses.go b/internal/httpclient/client/admin/update_identity_responses.go index c3521abb3587..e4f954f401de 100644 --- a/internal/httpclient/client/admin/update_identity_responses.go +++ b/internal/httpclient/client/admin/update_identity_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/common/get_schema_parameters.go b/internal/httpclient/client/common/get_schema_parameters.go index fdf8a692485d..f230212d06e2 100644 --- a/internal/httpclient/client/common/get_schema_parameters.go +++ b/internal/httpclient/client/common/get_schema_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewGetSchemaParams creates a new GetSchemaParams object diff --git a/internal/httpclient/client/common/get_schema_responses.go b/internal/httpclient/client/common/get_schema_responses.go index 7b35f518d7b7..d0ed4b6edfb5 100644 --- a/internal/httpclient/client/common/get_schema_responses.go +++ b/internal/httpclient/client/common/get_schema_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/common/get_self_service_browser_login_request_parameters.go b/internal/httpclient/client/common/get_self_service_browser_login_request_parameters.go index f859332108eb..34509bbaca3d 100644 --- a/internal/httpclient/client/common/get_self_service_browser_login_request_parameters.go +++ b/internal/httpclient/client/common/get_self_service_browser_login_request_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewGetSelfServiceBrowserLoginRequestParams creates a new GetSelfServiceBrowserLoginRequestParams object diff --git a/internal/httpclient/client/common/get_self_service_browser_login_request_responses.go b/internal/httpclient/client/common/get_self_service_browser_login_request_responses.go index 32b07cd5f693..d1b42921c660 100644 --- a/internal/httpclient/client/common/get_self_service_browser_login_request_responses.go +++ b/internal/httpclient/client/common/get_self_service_browser_login_request_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/common/get_self_service_browser_registration_request_parameters.go b/internal/httpclient/client/common/get_self_service_browser_registration_request_parameters.go index 6af504255231..821288865362 100644 --- a/internal/httpclient/client/common/get_self_service_browser_registration_request_parameters.go +++ b/internal/httpclient/client/common/get_self_service_browser_registration_request_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewGetSelfServiceBrowserRegistrationRequestParams creates a new GetSelfServiceBrowserRegistrationRequestParams object diff --git a/internal/httpclient/client/common/get_self_service_browser_registration_request_responses.go b/internal/httpclient/client/common/get_self_service_browser_registration_request_responses.go index 8651879d9413..cdc512145293 100644 --- a/internal/httpclient/client/common/get_self_service_browser_registration_request_responses.go +++ b/internal/httpclient/client/common/get_self_service_browser_registration_request_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/common/get_self_service_browser_settings_request_parameters.go b/internal/httpclient/client/common/get_self_service_browser_settings_request_parameters.go index f78142315e2f..0fdf8bf4f431 100644 --- a/internal/httpclient/client/common/get_self_service_browser_settings_request_parameters.go +++ b/internal/httpclient/client/common/get_self_service_browser_settings_request_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewGetSelfServiceBrowserSettingsRequestParams creates a new GetSelfServiceBrowserSettingsRequestParams object diff --git a/internal/httpclient/client/common/get_self_service_browser_settings_request_responses.go b/internal/httpclient/client/common/get_self_service_browser_settings_request_responses.go index a2b6d47374f2..76afbc7fe2ed 100644 --- a/internal/httpclient/client/common/get_self_service_browser_settings_request_responses.go +++ b/internal/httpclient/client/common/get_self_service_browser_settings_request_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/common/get_self_service_error_parameters.go b/internal/httpclient/client/common/get_self_service_error_parameters.go index 70758db564b0..286395306da5 100644 --- a/internal/httpclient/client/common/get_self_service_error_parameters.go +++ b/internal/httpclient/client/common/get_self_service_error_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewGetSelfServiceErrorParams creates a new GetSelfServiceErrorParams object diff --git a/internal/httpclient/client/common/get_self_service_error_responses.go b/internal/httpclient/client/common/get_self_service_error_responses.go index 101855f2f99b..ed41a6de4470 100644 --- a/internal/httpclient/client/common/get_self_service_error_responses.go +++ b/internal/httpclient/client/common/get_self_service_error_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/common/get_self_service_verification_request_parameters.go b/internal/httpclient/client/common/get_self_service_verification_request_parameters.go index f2d4a7e79d35..79688820ba59 100644 --- a/internal/httpclient/client/common/get_self_service_verification_request_parameters.go +++ b/internal/httpclient/client/common/get_self_service_verification_request_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewGetSelfServiceVerificationRequestParams creates a new GetSelfServiceVerificationRequestParams object diff --git a/internal/httpclient/client/common/get_self_service_verification_request_responses.go b/internal/httpclient/client/common/get_self_service_verification_request_responses.go index 629e2177c130..988c1c548a7a 100644 --- a/internal/httpclient/client/common/get_self_service_verification_request_responses.go +++ b/internal/httpclient/client/common/get_self_service_verification_request_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/health/is_instance_alive_parameters.go b/internal/httpclient/client/health/is_instance_alive_parameters.go index 7f882b77ed31..359c3d7334c5 100644 --- a/internal/httpclient/client/health/is_instance_alive_parameters.go +++ b/internal/httpclient/client/health/is_instance_alive_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewIsInstanceAliveParams creates a new IsInstanceAliveParams object diff --git a/internal/httpclient/client/health/is_instance_alive_responses.go b/internal/httpclient/client/health/is_instance_alive_responses.go index e2a749c5fd6b..a20e9da35f70 100644 --- a/internal/httpclient/client/health/is_instance_alive_responses.go +++ b/internal/httpclient/client/health/is_instance_alive_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/health/is_instance_ready_parameters.go b/internal/httpclient/client/health/is_instance_ready_parameters.go index f03a934a8619..247b03483d88 100644 --- a/internal/httpclient/client/health/is_instance_ready_parameters.go +++ b/internal/httpclient/client/health/is_instance_ready_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewIsInstanceReadyParams creates a new IsInstanceReadyParams object diff --git a/internal/httpclient/client/health/is_instance_ready_responses.go b/internal/httpclient/client/health/is_instance_ready_responses.go index d64fb185adef..3b3d8c69cfae 100644 --- a/internal/httpclient/client/health/is_instance_ready_responses.go +++ b/internal/httpclient/client/health/is_instance_ready_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/ory_kratos_client.go b/internal/httpclient/client/ory_kratos_client.go index e83baae4618f..8bbc309ceac0 100644 --- a/internal/httpclient/client/ory_kratos_client.go +++ b/internal/httpclient/client/ory_kratos_client.go @@ -6,8 +6,9 @@ package client // Editing this file might prove futile when you re-run the swagger generate command import ( - "github.com/go-openapi/runtime" httptransport "github.com/go-openapi/runtime/client" + + "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/client/admin" diff --git a/internal/httpclient/client/public/complete_self_service_browser_settings_password_strategy_flow_parameters.go b/internal/httpclient/client/public/complete_self_service_browser_settings_password_strategy_flow_parameters.go index c1ae371feb07..c18b5fb00897 100644 --- a/internal/httpclient/client/public/complete_self_service_browser_settings_password_strategy_flow_parameters.go +++ b/internal/httpclient/client/public/complete_self_service_browser_settings_password_strategy_flow_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewCompleteSelfServiceBrowserSettingsPasswordStrategyFlowParams creates a new CompleteSelfServiceBrowserSettingsPasswordStrategyFlowParams object diff --git a/internal/httpclient/client/public/complete_self_service_browser_settings_password_strategy_flow_responses.go b/internal/httpclient/client/public/complete_self_service_browser_settings_password_strategy_flow_responses.go index e0fee737b011..22a4174fde17 100644 --- a/internal/httpclient/client/public/complete_self_service_browser_settings_password_strategy_flow_responses.go +++ b/internal/httpclient/client/public/complete_self_service_browser_settings_password_strategy_flow_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/public/complete_self_service_browser_settings_profile_strategy_flow_parameters.go b/internal/httpclient/client/public/complete_self_service_browser_settings_profile_strategy_flow_parameters.go index 3002b0f9b61a..f5d28851b72b 100644 --- a/internal/httpclient/client/public/complete_self_service_browser_settings_profile_strategy_flow_parameters.go +++ b/internal/httpclient/client/public/complete_self_service_browser_settings_profile_strategy_flow_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/public/complete_self_service_browser_settings_profile_strategy_flow_responses.go b/internal/httpclient/client/public/complete_self_service_browser_settings_profile_strategy_flow_responses.go index fe112eaa427e..7cce3bbdbf76 100644 --- a/internal/httpclient/client/public/complete_self_service_browser_settings_profile_strategy_flow_responses.go +++ b/internal/httpclient/client/public/complete_self_service_browser_settings_profile_strategy_flow_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/public/complete_self_service_browser_verification_flow_parameters.go b/internal/httpclient/client/public/complete_self_service_browser_verification_flow_parameters.go index 92918cff12d4..c0783b3d6318 100644 --- a/internal/httpclient/client/public/complete_self_service_browser_verification_flow_parameters.go +++ b/internal/httpclient/client/public/complete_self_service_browser_verification_flow_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewCompleteSelfServiceBrowserVerificationFlowParams creates a new CompleteSelfServiceBrowserVerificationFlowParams object diff --git a/internal/httpclient/client/public/complete_self_service_browser_verification_flow_responses.go b/internal/httpclient/client/public/complete_self_service_browser_verification_flow_responses.go index ea3d53ff79f3..c9e48fe53fdd 100644 --- a/internal/httpclient/client/public/complete_self_service_browser_verification_flow_responses.go +++ b/internal/httpclient/client/public/complete_self_service_browser_verification_flow_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/public/initialize_self_service_browser_login_flow_parameters.go b/internal/httpclient/client/public/initialize_self_service_browser_login_flow_parameters.go index f7c5eb9ab114..531543e0a403 100644 --- a/internal/httpclient/client/public/initialize_self_service_browser_login_flow_parameters.go +++ b/internal/httpclient/client/public/initialize_self_service_browser_login_flow_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewInitializeSelfServiceBrowserLoginFlowParams creates a new InitializeSelfServiceBrowserLoginFlowParams object diff --git a/internal/httpclient/client/public/initialize_self_service_browser_login_flow_responses.go b/internal/httpclient/client/public/initialize_self_service_browser_login_flow_responses.go index 071d74715124..665b49a63589 100644 --- a/internal/httpclient/client/public/initialize_self_service_browser_login_flow_responses.go +++ b/internal/httpclient/client/public/initialize_self_service_browser_login_flow_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/public/initialize_self_service_browser_logout_flow_parameters.go b/internal/httpclient/client/public/initialize_self_service_browser_logout_flow_parameters.go index fb53aaafaead..97476353c3ea 100644 --- a/internal/httpclient/client/public/initialize_self_service_browser_logout_flow_parameters.go +++ b/internal/httpclient/client/public/initialize_self_service_browser_logout_flow_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewInitializeSelfServiceBrowserLogoutFlowParams creates a new InitializeSelfServiceBrowserLogoutFlowParams object diff --git a/internal/httpclient/client/public/initialize_self_service_browser_logout_flow_responses.go b/internal/httpclient/client/public/initialize_self_service_browser_logout_flow_responses.go index 30e222afa883..8c46c2fb8291 100644 --- a/internal/httpclient/client/public/initialize_self_service_browser_logout_flow_responses.go +++ b/internal/httpclient/client/public/initialize_self_service_browser_logout_flow_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/public/initialize_self_service_browser_registration_flow_parameters.go b/internal/httpclient/client/public/initialize_self_service_browser_registration_flow_parameters.go index 8e11dc9018d9..698328ed2a82 100644 --- a/internal/httpclient/client/public/initialize_self_service_browser_registration_flow_parameters.go +++ b/internal/httpclient/client/public/initialize_self_service_browser_registration_flow_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewInitializeSelfServiceBrowserRegistrationFlowParams creates a new InitializeSelfServiceBrowserRegistrationFlowParams object diff --git a/internal/httpclient/client/public/initialize_self_service_browser_registration_flow_responses.go b/internal/httpclient/client/public/initialize_self_service_browser_registration_flow_responses.go index e3328029e7a1..5d76505b50c4 100644 --- a/internal/httpclient/client/public/initialize_self_service_browser_registration_flow_responses.go +++ b/internal/httpclient/client/public/initialize_self_service_browser_registration_flow_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/public/initialize_self_service_browser_verification_flow_parameters.go b/internal/httpclient/client/public/initialize_self_service_browser_verification_flow_parameters.go index ebc11fb98f11..395209ef8e42 100644 --- a/internal/httpclient/client/public/initialize_self_service_browser_verification_flow_parameters.go +++ b/internal/httpclient/client/public/initialize_self_service_browser_verification_flow_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewInitializeSelfServiceBrowserVerificationFlowParams creates a new InitializeSelfServiceBrowserVerificationFlowParams object diff --git a/internal/httpclient/client/public/initialize_self_service_browser_verification_flow_responses.go b/internal/httpclient/client/public/initialize_self_service_browser_verification_flow_responses.go index 78bf0eb6d379..d881d5a15fc3 100644 --- a/internal/httpclient/client/public/initialize_self_service_browser_verification_flow_responses.go +++ b/internal/httpclient/client/public/initialize_self_service_browser_verification_flow_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/public/initialize_self_service_settings_flow_parameters.go b/internal/httpclient/client/public/initialize_self_service_settings_flow_parameters.go index 0036a3b9c58c..3e6af86a792b 100644 --- a/internal/httpclient/client/public/initialize_self_service_settings_flow_parameters.go +++ b/internal/httpclient/client/public/initialize_self_service_settings_flow_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewInitializeSelfServiceSettingsFlowParams creates a new InitializeSelfServiceSettingsFlowParams object diff --git a/internal/httpclient/client/public/initialize_self_service_settings_flow_responses.go b/internal/httpclient/client/public/initialize_self_service_settings_flow_responses.go index 0e4a6a69246d..0044d113be8d 100644 --- a/internal/httpclient/client/public/initialize_self_service_settings_flow_responses.go +++ b/internal/httpclient/client/public/initialize_self_service_settings_flow_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/public/self_service_browser_verify_parameters.go b/internal/httpclient/client/public/self_service_browser_verify_parameters.go index f62a2a4ecb89..751741d7ccf4 100644 --- a/internal/httpclient/client/public/self_service_browser_verify_parameters.go +++ b/internal/httpclient/client/public/self_service_browser_verify_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewSelfServiceBrowserVerifyParams creates a new SelfServiceBrowserVerifyParams object diff --git a/internal/httpclient/client/public/self_service_browser_verify_responses.go b/internal/httpclient/client/public/self_service_browser_verify_responses.go index 980071a46eb9..3b14ee9e2454 100644 --- a/internal/httpclient/client/public/self_service_browser_verify_responses.go +++ b/internal/httpclient/client/public/self_service_browser_verify_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/public/whoami_parameters.go b/internal/httpclient/client/public/whoami_parameters.go index c3a182c714bb..69861624d632 100644 --- a/internal/httpclient/client/public/whoami_parameters.go +++ b/internal/httpclient/client/public/whoami_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewWhoamiParams creates a new WhoamiParams object diff --git a/internal/httpclient/client/public/whoami_responses.go b/internal/httpclient/client/public/whoami_responses.go index 17df465a26bb..4d9179ab127e 100644 --- a/internal/httpclient/client/public/whoami_responses.go +++ b/internal/httpclient/client/public/whoami_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/client/version/get_version_parameters.go b/internal/httpclient/client/version/get_version_parameters.go index 29bf00d13b82..f937b57ccc91 100644 --- a/internal/httpclient/client/version/get_version_parameters.go +++ b/internal/httpclient/client/version/get_version_parameters.go @@ -13,7 +13,8 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" ) // NewGetVersionParams creates a new GetVersionParams object diff --git a/internal/httpclient/client/version/get_version_responses.go b/internal/httpclient/client/version/get_version_responses.go index cc0995099222..60491e6c4fa2 100644 --- a/internal/httpclient/client/version/get_version_responses.go +++ b/internal/httpclient/client/version/get_version_responses.go @@ -10,7 +10,8 @@ import ( "io" "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" + + strfmt "github.com/go-openapi/strfmt" "github.com/ory/kratos/internal/httpclient/models" ) diff --git a/internal/httpclient/models/complete_self_service_browser_settings_strategy_profile_flow_payload.go b/internal/httpclient/models/complete_self_service_browser_settings_strategy_profile_flow_payload.go index de5dfe297ab6..f437dc4346b4 100644 --- a/internal/httpclient/models/complete_self_service_browser_settings_strategy_profile_flow_payload.go +++ b/internal/httpclient/models/complete_self_service_browser_settings_strategy_profile_flow_payload.go @@ -7,13 +7,12 @@ package models import ( "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // CompleteSelfServiceBrowserSettingsStrategyProfileFlowPayload complete self service browser settings strategy profile flow payload -// // swagger:model completeSelfServiceBrowserSettingsStrategyProfileFlowPayload type CompleteSelfServiceBrowserSettingsStrategyProfileFlowPayload struct { diff --git a/internal/httpclient/models/credentials_type.go b/internal/httpclient/models/credentials_type.go index 81188627891b..0d3419209bc9 100644 --- a/internal/httpclient/models/credentials_type.go +++ b/internal/httpclient/models/credentials_type.go @@ -6,13 +6,12 @@ package models // Editing this file might prove futile when you re-run the swagger generate command import ( - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" ) // CredentialsType CredentialsType represents several different credential types, like password credentials, passwordless credentials, // // and so on. -// // swagger:model CredentialsType type CredentialsType string diff --git a/internal/httpclient/models/error.go b/internal/httpclient/models/error.go index e27e3fb05264..75b27b4359b5 100644 --- a/internal/httpclient/models/error.go +++ b/internal/httpclient/models/error.go @@ -6,12 +6,11 @@ package models // Editing this file might prove futile when you re-run the swagger generate command import ( - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" ) // Error error -// // swagger:model Error type Error struct { diff --git a/internal/httpclient/models/error_container.go b/internal/httpclient/models/error_container.go index 8978b5bb67f0..8957af09e278 100644 --- a/internal/httpclient/models/error_container.go +++ b/internal/httpclient/models/error_container.go @@ -7,12 +7,11 @@ package models import ( "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" ) // ErrorContainer error container -// // swagger:model errorContainer type ErrorContainer struct { diff --git a/internal/httpclient/models/form.go b/internal/httpclient/models/form.go index 82c7e4cac86c..3e00917e211c 100644 --- a/internal/httpclient/models/form.go +++ b/internal/httpclient/models/form.go @@ -9,13 +9,12 @@ import ( "strconv" "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // Form HTMLForm represents a HTML Form. The container can work with both HTTP Form and JSON requests -// // swagger:model form type Form struct { diff --git a/internal/httpclient/models/form_field.go b/internal/httpclient/models/form_field.go index 38eae4c32552..38148da27d5c 100644 --- a/internal/httpclient/models/form_field.go +++ b/internal/httpclient/models/form_field.go @@ -9,13 +9,12 @@ import ( "strconv" "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // FormField Field represents a HTML Form Field -// // swagger:model formField type FormField struct { diff --git a/internal/httpclient/models/form_fields.go b/internal/httpclient/models/form_fields.go index 9a28d1c13efc..f3fb25eab4a9 100644 --- a/internal/httpclient/models/form_fields.go +++ b/internal/httpclient/models/form_fields.go @@ -9,12 +9,11 @@ import ( "strconv" "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" ) // FormFields Fields contains multiple fields -// // swagger:model formFields type FormFields []*FormField diff --git a/internal/httpclient/models/generic_error.go b/internal/httpclient/models/generic_error.go index 0582f957ae2e..1d3d8213a9cb 100644 --- a/internal/httpclient/models/generic_error.go +++ b/internal/httpclient/models/generic_error.go @@ -7,14 +7,13 @@ package models import ( "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" ) // GenericError Error response // // Error responses are sent when an error (e.g. unauthorized, bad request, ...) occurred. -// // swagger:model genericError type GenericError struct { diff --git a/internal/httpclient/models/generic_error_payload.go b/internal/httpclient/models/generic_error_payload.go index 5fe7451bff05..727f41c1eef2 100644 --- a/internal/httpclient/models/generic_error_payload.go +++ b/internal/httpclient/models/generic_error_payload.go @@ -6,12 +6,11 @@ package models // Editing this file might prove futile when you re-run the swagger generate command import ( - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" ) // GenericErrorPayload generic error payload -// // swagger:model genericErrorPayload type GenericErrorPayload struct { diff --git a/internal/httpclient/models/health_not_ready_status.go b/internal/httpclient/models/health_not_ready_status.go index 64626783ed40..d10e6d25c60b 100644 --- a/internal/httpclient/models/health_not_ready_status.go +++ b/internal/httpclient/models/health_not_ready_status.go @@ -6,12 +6,11 @@ package models // Editing this file might prove futile when you re-run the swagger generate command import ( - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" ) // HealthNotReadyStatus health not ready status -// // swagger:model healthNotReadyStatus type HealthNotReadyStatus struct { diff --git a/internal/httpclient/models/health_status.go b/internal/httpclient/models/health_status.go index 60ba32416b0d..517e8500ffde 100644 --- a/internal/httpclient/models/health_status.go +++ b/internal/httpclient/models/health_status.go @@ -6,12 +6,11 @@ package models // Editing this file might prove futile when you re-run the swagger generate command import ( - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" ) // HealthStatus health status -// // swagger:model healthStatus type HealthStatus struct { diff --git a/internal/httpclient/models/identity.go b/internal/httpclient/models/identity.go index fbc0a84aee73..59ffde65e2e0 100644 --- a/internal/httpclient/models/identity.go +++ b/internal/httpclient/models/identity.go @@ -9,13 +9,12 @@ import ( "strconv" "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // Identity identity -// // swagger:model Identity type Identity struct { diff --git a/internal/httpclient/models/login_request.go b/internal/httpclient/models/login_request.go index 0e1c113787f8..6ac167cd2d8b 100644 --- a/internal/httpclient/models/login_request.go +++ b/internal/httpclient/models/login_request.go @@ -7,13 +7,12 @@ package models import ( "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // LoginRequest login request -// // swagger:model loginRequest type LoginRequest struct { diff --git a/internal/httpclient/models/login_request_method.go b/internal/httpclient/models/login_request_method.go index 1fec0c40e46d..571408bc47dd 100644 --- a/internal/httpclient/models/login_request_method.go +++ b/internal/httpclient/models/login_request_method.go @@ -7,13 +7,12 @@ package models import ( "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // LoginRequestMethod login request method -// // swagger:model loginRequestMethod type LoginRequestMethod struct { diff --git a/internal/httpclient/models/login_request_method_config.go b/internal/httpclient/models/login_request_method_config.go index d16d99ad98cc..d53d89369c6c 100644 --- a/internal/httpclient/models/login_request_method_config.go +++ b/internal/httpclient/models/login_request_method_config.go @@ -9,13 +9,12 @@ import ( "strconv" "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // LoginRequestMethodConfig login request method config -// // swagger:model loginRequestMethodConfig type LoginRequestMethodConfig struct { diff --git a/internal/httpclient/models/registration_request.go b/internal/httpclient/models/registration_request.go index 93aadabd35e1..476d8ca158ba 100644 --- a/internal/httpclient/models/registration_request.go +++ b/internal/httpclient/models/registration_request.go @@ -7,13 +7,12 @@ package models import ( "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // RegistrationRequest registration request -// // swagger:model registrationRequest type RegistrationRequest struct { diff --git a/internal/httpclient/models/registration_request_method.go b/internal/httpclient/models/registration_request_method.go index 70f012f06fc3..4e6dbe10ff97 100644 --- a/internal/httpclient/models/registration_request_method.go +++ b/internal/httpclient/models/registration_request_method.go @@ -7,12 +7,11 @@ package models import ( "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" ) // RegistrationRequestMethod registration request method -// // swagger:model registrationRequestMethod type RegistrationRequestMethod struct { diff --git a/internal/httpclient/models/registration_request_method_config.go b/internal/httpclient/models/registration_request_method_config.go index 9f4156603bd0..b11585c1dfde 100644 --- a/internal/httpclient/models/registration_request_method_config.go +++ b/internal/httpclient/models/registration_request_method_config.go @@ -9,13 +9,12 @@ import ( "strconv" "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // RegistrationRequestMethodConfig registration request method config -// // swagger:model registrationRequestMethodConfig type RegistrationRequestMethodConfig struct { diff --git a/internal/httpclient/models/request_method_config.go b/internal/httpclient/models/request_method_config.go index cd96304265f0..4b1fa1e4eea2 100644 --- a/internal/httpclient/models/request_method_config.go +++ b/internal/httpclient/models/request_method_config.go @@ -9,13 +9,12 @@ import ( "strconv" "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // RequestMethodConfig request method config -// // swagger:model RequestMethodConfig type RequestMethodConfig struct { diff --git a/internal/httpclient/models/session.go b/internal/httpclient/models/session.go index ae09bba19410..6f56dbd83001 100644 --- a/internal/httpclient/models/session.go +++ b/internal/httpclient/models/session.go @@ -7,13 +7,12 @@ package models import ( "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // Session session -// // swagger:model session type Session struct { diff --git a/internal/httpclient/models/settings_request.go b/internal/httpclient/models/settings_request.go index ea689f0798f2..c73d29b75cc6 100644 --- a/internal/httpclient/models/settings_request.go +++ b/internal/httpclient/models/settings_request.go @@ -7,7 +7,7 @@ package models import ( "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) @@ -18,7 +18,6 @@ import ( // (e.g. profile data, passwords, ...) in a selfservice manner. // // For more information head over to: https://www.ory.sh/docs/kratos/selfservice/flows/user-settings-profile-management -// // swagger:model settingsRequest type SettingsRequest struct { diff --git a/internal/httpclient/models/settings_request_method.go b/internal/httpclient/models/settings_request_method.go index 996d3e98546b..a64fae59ce3c 100644 --- a/internal/httpclient/models/settings_request_method.go +++ b/internal/httpclient/models/settings_request_method.go @@ -7,12 +7,11 @@ package models import ( "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" ) // SettingsRequestMethod settings request method -// // swagger:model settingsRequestMethod type SettingsRequestMethod struct { diff --git a/internal/httpclient/models/traits.go b/internal/httpclient/models/traits.go index 6aefa298503d..e39059ab688c 100644 --- a/internal/httpclient/models/traits.go +++ b/internal/httpclient/models/traits.go @@ -6,6 +6,5 @@ package models // Editing this file might prove futile when you re-run the swagger generate command // Traits traits -// // swagger:model Traits type Traits interface{} diff --git a/internal/httpclient/models/uuid.go b/internal/httpclient/models/uuid.go index f28209a7a402..f5b247624d0d 100644 --- a/internal/httpclient/models/uuid.go +++ b/internal/httpclient/models/uuid.go @@ -7,12 +7,11 @@ package models import ( "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/validate" ) // UUID UUID -// // swagger:model UUID type UUID strfmt.UUID4 diff --git a/internal/httpclient/models/verifiable_address.go b/internal/httpclient/models/verifiable_address.go index 7f6d5f9fbfeb..7d6de66bae2e 100644 --- a/internal/httpclient/models/verifiable_address.go +++ b/internal/httpclient/models/verifiable_address.go @@ -7,13 +7,12 @@ package models import ( "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // VerifiableAddress verifiable address -// // swagger:model VerifiableAddress type VerifiableAddress struct { diff --git a/internal/httpclient/models/verifiable_address_type.go b/internal/httpclient/models/verifiable_address_type.go index ec7b8b5ca309..6a34a17742e2 100644 --- a/internal/httpclient/models/verifiable_address_type.go +++ b/internal/httpclient/models/verifiable_address_type.go @@ -6,11 +6,10 @@ package models // Editing this file might prove futile when you re-run the swagger generate command import ( - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" ) // VerifiableAddressType verifiable address type -// // swagger:model VerifiableAddressType type VerifiableAddressType string diff --git a/internal/httpclient/models/verification_request.go b/internal/httpclient/models/verification_request.go index bf5183e5c4c7..3f090b6fb2fa 100644 --- a/internal/httpclient/models/verification_request.go +++ b/internal/httpclient/models/verification_request.go @@ -7,7 +7,7 @@ package models import ( "github.com/go-openapi/errors" - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) @@ -18,7 +18,6 @@ import ( // channel such as an email address or a phone number. // // For more information head over to: https://www.ory.sh/docs/kratos/selfservice/flows/verify-email-account-activation -// // swagger:model verificationRequest type VerificationRequest struct { diff --git a/internal/httpclient/models/version.go b/internal/httpclient/models/version.go index 8e687bcb20df..748ab7974b14 100644 --- a/internal/httpclient/models/version.go +++ b/internal/httpclient/models/version.go @@ -6,12 +6,11 @@ package models // Editing this file might prove futile when you re-run the swagger generate command import ( - "github.com/go-openapi/strfmt" + strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" ) // Version version -// // swagger:model version type Version struct { diff --git a/session/handler_mock.go b/internal/testhelpers/handler_mock.go similarity index 80% rename from session/handler_mock.go rename to internal/testhelpers/handler_mock.go index c8dd8c358cef..ef53cb2e6fca 100644 --- a/session/handler_mock.go +++ b/internal/testhelpers/handler_mock.go @@ -1,4 +1,4 @@ -package session +package testhelpers import ( "context" @@ -20,22 +20,22 @@ import ( "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/identity" + "github.com/ory/kratos/session" "github.com/ory/kratos/x" ) type mockDeps interface { identity.PrivilegedPoolProvider - ManagementProvider - PersistenceProvider + session.ManagementProvider + session.PersistenceProvider } -func MockSetSession(t *testing.T, reg mockDeps) httprouter.Handle { +func MockSetSession(t *testing.T, reg mockDeps, conf configuration.Provider) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { i := identity.NewIdentity(configuration.DefaultIdentityTraitsSchemaID) require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) - _, err := reg.SessionManager().CreateToRequest(context.Background(), i, w, r) - require.NoError(t, err) + require.NoError(t, reg.SessionManager().CreateToRequest(context.Background(), w, r, session.NewSession(i, conf, time.Now().UTC())))) w.WriteHeader(http.StatusOK) } @@ -53,9 +53,9 @@ func MockGetSession(t *testing.T, reg mockDeps) httprouter.Handle { } } -func MockMakeAuthenticatedRequest(t *testing.T, reg mockDeps, router *httprouter.Router, req *http.Request) ([]byte, *http.Response) { +func MockMakeAuthenticatedRequest(t *testing.T, reg mockDeps, conf configuration.Provider, router *httprouter.Router, req *http.Request) ([]byte, *http.Response) { set := "/" + uuid.New().String() + "/set" - router.GET(set, MockSetSession(t, reg)) + router.GET(set, MockSetSession(t, reg, conf)) client := MockCookieClient(t) MockHydrateCookieClient(t, client, "http://"+req.URL.Host+set) @@ -85,15 +85,15 @@ func MockHydrateCookieClient(t *testing.T, c *http.Client, u string) { var found bool for _, c := range res.Cookies() { - if c.Name == DefaultSessionCookieName { + if c.Name == session.DefaultSessionCookieName { found = true } } require.True(t, found) } -func MockSessionCreateHandlerWithIdentity(t *testing.T, reg mockDeps, i *identity.Identity) (httprouter.Handle, *Session) { - var sess Session +func MockSessionCreateHandlerWithIdentity(t *testing.T, reg mockDeps, i *identity.Identity) (httprouter.Handle, *session.Session) { + var sess session.Session require.NoError(t, faker.FakeData(&sess)) // require AuthenticatedAt to be time.Now() as we always compare it to the current time sess.AuthenticatedAt = time.Now() @@ -112,12 +112,11 @@ func MockSessionCreateHandlerWithIdentity(t *testing.T, reg mockDeps, i *identit require.Len(t, inserted.Credentials, len(i.Credentials)) return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - require.NoError(t, reg.SessionManager().SaveToRequest(context.Background(), &sess, w, r)) + require.NoError(t, reg.SessionManager().SaveToRequest(context.Background(), w, r, &sess)) }, &sess - } -func MockSessionCreateHandler(t *testing.T, reg mockDeps) (httprouter.Handle, *Session) { +func MockSessionCreateHandler(t *testing.T, reg mockDeps) (httprouter.Handle, *session.Session) { return MockSessionCreateHandlerWithIdentity(t, reg, &identity.Identity{ ID: x.NewUUID(), Traits: identity.Traits(json.RawMessage(`{"baz":"bar","foo":true,"bar":2.5}`)), diff --git a/internal/testhelpers/selfservice.go b/internal/testhelpers/selfservice.go new file mode 100644 index 000000000000..98ac1a644f16 --- /dev/null +++ b/internal/testhelpers/selfservice.go @@ -0,0 +1,188 @@ +package testhelpers + +import ( + "context" + "errors" + "io/ioutil" + "net/http" + "net/url" + "testing" + + "github.com/bxcodec/faker" + "github.com/gobuffalo/httptest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/viper" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/configuration" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flow/settings" +) + +func TestSelfServicePreHook( + configKey string, + makeRequestPre func(t *testing.T, ts *httptest.Server) (*http.Response, string), + newServer func(t *testing.T) *httptest.Server, +) func(t *testing.T) { + return func(t *testing.T) { + t.Run("case=pass without hooks", func(t *testing.T) { + t.Cleanup(SelfServiceHookConfigReset) + + res, _ := makeRequestPre(t, newServer(t)) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("case=pass if hooks pass", func(t *testing.T) { + t.Cleanup(SelfServiceHookConfigReset) + viper.Set(configKey, []configuration.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) + + res, _ := makeRequestPre(t, newServer(t)) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("case=err if hooks err", func(t *testing.T) { + t.Cleanup(SelfServiceHookConfigReset) + viper.Set(configKey, []configuration.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecuteLoginPreHook": "err","ExecuteRegistrationPreHook": "err"}`)}}) + + res, body := makeRequestPre(t, newServer(t)) + assert.EqualValues(t, http.StatusInternalServerError, res.StatusCode, "%s", body) + assert.EqualValues(t, "err", body) + }) + + t.Run("case=abort if hooks aborts", func(t *testing.T) { + t.Cleanup(SelfServiceHookConfigReset) + viper.Set(configKey, []configuration.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecuteLoginPreHook": "abort","ExecuteRegistrationPreHook": "abort"}`)}}) + + res, body := makeRequestPre(t, newServer(t)) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Empty(t, body) + }) + + t.Run("case=redirect", func(t *testing.T) { + t.Cleanup(SelfServiceHookConfigReset) + viper.Set(configKey, []configuration.SelfServiceHook{{Name: "redirect", Config: []byte(`{"to": "https://www.ory.sh/"}`)}}) + + res, _ := makeRequestPre(t, newServer(t)) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/", res.Request.URL.String()) + }) + } +} + +func SelfServiceHookCreateFakeIdentity(t *testing.T, reg driver.Registry) *identity.Identity { + i := SelfServiceHookFakeIdentity(t) + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + return i +} + +func SelfServiceHookFakeIdentity(t *testing.T) *identity.Identity { + var i identity.Identity + require.NoError(t, faker.FakeData(&i)) + i.Traits = identity.Traits(`{}`) + return &i +} + +func SelfServiceHookConfigReset() { + viper.Set(configuration.ViperKeySelfServiceLoginAfter, nil) + viper.Set(configuration.ViperKeySelfServiceLoginBeforeHooks, nil) + viper.Set(configuration.ViperKeySelfServiceRegistrationAfter, nil) + viper.Set(configuration.ViperKeySelfServiceRegistrationBeforeHooks, nil) + viper.Set(configuration.ViperKeySelfServiceSettingsAfter, nil) +} + +func SelfServiceHookSettingsSetDefaultRedirectTo(value string) { + viper.Set(configuration.ViperKeySelfServiceSettingsAfter+"."+configuration.ViperKeyDefaultReturnTo, value) +} + +func SelfServiceHookSettingsSetDefaultRedirectToStrategy(strategy, value string) { + viper.Set(configuration.ViperKeySelfServiceSettingsAfter+"."+strategy+"."+configuration.ViperKeyDefaultReturnTo, value) +} +func SelfServiceHookLoginSetDefaultRedirectTo(value string) { + viper.Set(configuration.ViperKeySelfServiceLoginAfter+"."+configuration.ViperKeyDefaultReturnTo, value) +} + +func SelfServiceHookLoginSetDefaultRedirectToStrategy(strategy, value string) { + viper.Set(configuration.ViperKeySelfServiceLoginAfter+"."+strategy+"."+configuration.ViperKeyDefaultReturnTo, value) +} + +func SelfServiceHookRegistrationSetDefaultRedirectTo(value string) { + viper.Set(configuration.ViperKeySelfServiceRegistrationAfter+"."+configuration.ViperKeyDefaultReturnTo, value) +} + +func SelfServiceHookRegistrationSetDefaultRedirectToStrategy(strategy, value string) { + viper.Set(configuration.ViperKeySelfServiceRegistrationAfter+"."+strategy+"."+configuration.ViperKeyDefaultReturnTo, value) +} + +func SelfServiceHookLoginViperSetPost(strategy string, c []configuration.SelfServiceHook) { + viper.Set(configuration.HookStrategyKey(configuration.ViperKeySelfServiceLoginAfter, strategy), c) +} + +func SelfServiceHookRegistrationViperSetPost(strategy string, c []configuration.SelfServiceHook) { + viper.Set(configuration.HookStrategyKey(configuration.ViperKeySelfServiceRegistrationAfter, strategy), c) +} + +func SelfServiceHookLoginErrorHandler(t *testing.T, w http.ResponseWriter, r *http.Request, err error) bool { + return selfServiceHookErrorHandler(t, w, r, login.ErrHookAbortRequest, err) +} + +func SelfServiceHookRegistrationErrorHandler(t *testing.T, w http.ResponseWriter, r *http.Request, err error) bool { + return selfServiceHookErrorHandler(t, w, r, registration.ErrHookAbortRequest, err) +} + +func SelfServiceHookSettingsErrorHandler(t *testing.T, w http.ResponseWriter, r *http.Request, err error) bool { + return selfServiceHookErrorHandler(t, w, r, settings.ErrHookAbortRequest, err) +} + +func selfServiceHookErrorHandler(t *testing.T, w http.ResponseWriter, r *http.Request, abortErr error, actualErr error) bool { + if actualErr != nil { + t.Logf("received error: %+v", actualErr) + if errors.Is(actualErr, abortErr) { + return false + } + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(actualErr.Error())) + return false + } + return true +} + +func SelfServiceMakeLoginPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) { + return selfServiceMakeHookRequest(t, ts, "/login/pre", false, url.Values{}) +} + +func SelfServiceMakeLoginPostHookRequest(t *testing.T, ts *httptest.Server, asAPI bool, query url.Values) (*http.Response, string) { + return selfServiceMakeHookRequest(t, ts, "/login/post", asAPI, query) +} + +func SelfServiceMakeRegistrationPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) { + return selfServiceMakeHookRequest(t, ts, "/registration/pre", false, url.Values{}) +} + +func SelfServiceMakeRegistrationPostHookRequest(t *testing.T, ts *httptest.Server, asAPI bool, query url.Values) (*http.Response, string) { + return selfServiceMakeHookRequest(t, ts, "/registration/post", asAPI, query) +} + +func SelfServiceMakeSettingsPostHookRequest(t *testing.T, ts *httptest.Server, asAPI bool, query url.Values) (*http.Response, string) { + return selfServiceMakeHookRequest(t, ts, "/settings/post", asAPI, query) +} + +func selfServiceMakeHookRequest(t *testing.T, ts *httptest.Server, suffix string, asAPI bool, query url.Values) (*http.Response, string) { + if len(query) > 0 { + suffix += "?" + query.Encode() + } + req, err := http.NewRequest("GET", ts.URL+suffix, nil) + require.NoError(t, err) + if asAPI { + req.Header.Set("Accept", "application/json") + } + res, err := ts.Client().Do(req) + require.NoError(t, err) + defer res.Body.Close() + body, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + return res, string(body) +} diff --git a/internal/testhelpers/selfservice_settings.go b/internal/testhelpers/selfservice_settings.go index 74dab35442f7..c3144be5ed08 100644 --- a/internal/testhelpers/selfservice_settings.go +++ b/internal/testhelpers/selfservice_settings.go @@ -28,21 +28,14 @@ import ( "github.com/ory/kratos/internal/httpclient/client/common" "github.com/ory/kratos/internal/httpclient/models" "github.com/ory/kratos/selfservice/flow/settings" - "github.com/ory/kratos/session" "github.com/ory/kratos/x" ) -func SetSettingsStrategyAfterHooks(t *testing.T, strategy string, u string) { - viper.Set( - configuration.ViperKeySelfServiceSettingsAfterConfig+"."+strategy, - HookConfigRedirectTo(t, u)) -} - func HookConfigRedirectTo(t *testing.T, u string) (m []map[string]interface{}) { var b bytes.Buffer _, err := fmt.Fprintf(&b, `[ { - "job": "redirect", + "hook": "redirect", "config": { "default_redirect_url": "%s", "allow_user_defined_redirect": true @@ -97,7 +90,7 @@ func NewSettingsUITestServer(t *testing.T) *httptest.Server { ts := httptest.NewServer(router) t.Cleanup(ts.Close) - viper.Set(configuration.ViperKeyURLsProfile, ts.URL+"/settings") + viper.Set(configuration.ViperKeyURLsSettings, ts.URL+"/settings") viper.Set(configuration.ViperKeyURLsLogin, ts.URL+"/login") return ts @@ -149,7 +142,7 @@ func NewSettingsAPIServer(t *testing.T, reg *driver.RegistryDefault, ids []ident viper.Set(configuration.ViperKeyURLsSelfAdmin, tsa.URL) for k := range ids { - route, _ := session.MockSessionCreateHandlerWithIdentity(t, reg, &ids[k]) + route, _ := MockSessionCreateHandlerWithIdentity(t, reg, &ids[k]) public.GET("/sessions/set/"+strconv.Itoa(k), route) } @@ -172,7 +165,7 @@ func SettingsSubmitForm( require.NoError(t, err) assert.EqualValues(t, http.StatusNoContent, res.StatusCode, "%s", b) - assert.Equal(t, viper.GetString(configuration.ViperKeyURLsProfile), res.Request.URL.Scheme+"://"+res.Request.URL.Host+res.Request.URL.Path, "should end up at the settings URL, used: %s", pointerx.StringR(f.Action)) + assert.Equal(t, viper.GetString(configuration.ViperKeyURLsSettings), res.Request.URL.Scheme+"://"+res.Request.URL.Host+res.Request.URL.Path, "should end up at the settings URL, used: %s", pointerx.StringR(f.Action)) rs, err := NewSDKClientFromURL(viper.GetString(configuration.ViperKeyURLsSelfPublic)).Common.GetSelfServiceBrowserSettingsRequest( common.NewGetSelfServiceBrowserSettingsRequestParams().WithHTTPClient(hc). diff --git a/internal/testhelpers/session.go b/internal/testhelpers/session.go index 8aecf8be723b..bd44d248ffd6 100644 --- a/internal/testhelpers/session.go +++ b/internal/testhelpers/session.go @@ -3,12 +3,10 @@ package testhelpers import ( "net/http" "testing" - - "github.com/ory/kratos/session" ) func NewSessionClient(t *testing.T, u string) *http.Client { - c := session.MockCookieClient(t) - session.MockHydrateCookieClient(t, c, u) + c := MockCookieClient(t) + MockHydrateCookieClient(t, c, u) return c } diff --git a/internal/testhelpers/stub/fake-session.schema.json b/internal/testhelpers/stub/fake-session.schema.json new file mode 100644 index 000000000000..558106d35900 --- /dev/null +++ b/internal/testhelpers/stub/fake-session.schema.json @@ -0,0 +1,7 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": {} +} diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go index 689775c0b72c..73b180ac62e4 100644 --- a/persistence/sql/persister_test.go +++ b/persistence/sql/persister_test.go @@ -177,7 +177,7 @@ func getErr(args ...interface{}) error { } func TestPersister_Transaction(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() t.Run("case=should not create identity because callback returned error", func(t *testing.T) { diff --git a/schema/handler_test.go b/schema/handler_test.go index f2e8570d2eb2..f171d7f6fe8b 100644 --- a/schema/handler_test.go +++ b/schema/handler_test.go @@ -22,7 +22,7 @@ import ( ) func TestHandler(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) router := x.NewRouterPublic() reg.SchemaHandler().RegisterPublicRoutes(router) ts := httptest.NewServer(router) diff --git a/selfservice/errorx/handler_test.go b/selfservice/errorx/handler_test.go index 377256da71cc..d970369cc5f2 100644 --- a/selfservice/errorx/handler_test.go +++ b/selfservice/errorx/handler_test.go @@ -27,7 +27,7 @@ import ( ) func TestHandler(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) h := errorx.NewHandler(reg) t.Run("case=public authorization", func(t *testing.T) { diff --git a/selfservice/flow/login/error.go b/selfservice/flow/login/error.go index f08e06551eae..3093c75389b2 100644 --- a/selfservice/flow/login/error.go +++ b/selfservice/flow/login/error.go @@ -21,7 +21,7 @@ import ( ) var ( - ErrHookAbortRequest = errors.New("abort hook") + ErrHookAbortRequest = errors.New("aborted login hook execution") ) type ( diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go index fb2fb4453413..ebf7ac32bfcc 100644 --- a/selfservice/flow/login/handler.go +++ b/selfservice/flow/login/handler.go @@ -9,7 +9,6 @@ import ( "github.com/justinas/nosurf" "github.com/pkg/errors" - "github.com/ory/x/errorsx" "github.com/ory/x/urlx" "github.com/ory/kratos/driver/configuration" @@ -57,7 +56,7 @@ func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) { } func (h *Handler) NewLoginRequest(w http.ResponseWriter, r *http.Request, redir func(request *Request) (string, error)) error { - a := NewLoginRequest(h.c.SelfServiceLoginRequestLifespan(), h.d.GenerateCSRFToken(r), r) + a := NewRequest(h.c.SelfServiceLoginRequestLifespan(), h.d.GenerateCSRFToken(r), r) for _, s := range h.d.LoginStrategies() { if err := s.PopulateLoginMethod(r, a); err != nil { return err @@ -65,7 +64,7 @@ func (h *Handler) NewLoginRequest(w http.ResponseWriter, r *http.Request, redir } if err := h.d.LoginHookExecutor().PreLoginHook(w, r, a); err != nil { - if errorsx.Cause(err) == ErrHookAbortRequest { + if errors.Is(err, ErrHookAbortRequest) { return nil } return err @@ -120,12 +119,15 @@ func (h *Handler) initLoginRequest(w http.ResponseWriter, r *http.Request, ps ht return urlx.CopyWithQuery(h.c.LoginURL(), url.Values{"request": {a.ID.String()}}).String(), nil } - returnTo, err := x.DetermineReturnToURL(r.URL, h.c.DefaultReturnToURL(), []url.URL{*h.c.SelfPublicURL()}) + returnTo, err := x.SecureRedirectTo(r, h.c.DefaultReturnToURL(), + x.SecureRedirectAllowSelfServiceURLs(h.c.SelfPublicURL()), + x.SecureRedirectAllowURLs(h.c.WhitelistedReturnToDomains()), + ) if err != nil { h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) } - return returnTo, nil + return returnTo.String(), nil }); err != nil { h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) return diff --git a/selfservice/flow/login/handler_test.go b/selfservice/flow/login/handler_test.go index 507c14875d7d..8cc1276c7eb8 100644 --- a/selfservice/flow/login/handler_test.go +++ b/selfservice/flow/login/handler_test.go @@ -21,7 +21,6 @@ import ( "github.com/ory/kratos/internal" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow/login" - "github.com/ory/kratos/session" "github.com/ory/kratos/x" ) @@ -30,7 +29,7 @@ func init() { } func TestHandlerSettingForced(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + conf, reg := internal.NewFastRegistryWithMocks(t) reg.WithCSRFTokenGenerator(x.FakeCSRFTokenGenerator) router := x.NewRouterPublic() @@ -59,7 +58,7 @@ func TestHandlerSettingForced(t *testing.T) { mar := func(t *testing.T, extQuery url.Values) (*http.Response, []byte) { rid := x.NewUUID() req := x.NewTestHTTPRequest(t, "GET", ts.URL+login.BrowserLoginPath, nil) - loginReq := login.NewLoginRequest(time.Minute, x.FakeCSRFToken, req) + loginReq := login.NewRequest(time.Minute, x.FakeCSRFToken, req) loginReq.ID = rid for _, s := range reg.LoginStrategies() { require.NoError(t, s.PopulateLoginMethod(req, loginReq)) @@ -72,7 +71,7 @@ func TestHandlerSettingForced(t *testing.T) { } req.URL.RawQuery = q.Encode() - body, res := session.MockMakeAuthenticatedRequest(t, reg, router.Router, req) + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, req) return res, body } @@ -123,7 +122,7 @@ func TestHandlerSettingForced(t *testing.T) { } func TestLoginHandler(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) public, admin := func() (*httptest.Server, *httptest.Server) { public := x.NewRouterPublic() diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index ac50738d2a3e..46ed5a2e102a 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -2,19 +2,25 @@ package login import ( "net/http" + "time" + + "github.com/pkg/errors" "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/identity" "github.com/ory/kratos/session" + "github.com/ory/kratos/x" ) type ( PreHookExecutor interface { ExecuteLoginPreHook(w http.ResponseWriter, r *http.Request, a *Request) error } + PostHookExecutor interface { ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, a *Request, s *session.Session) error } + HooksProvider interface { PreLoginHooks() []PreHookExecutor PostLoginHooks(credentialsType identity.CredentialsType) []PostHookExecutor @@ -23,8 +29,10 @@ type ( type ( executorDependencies interface { - identity.ManagementProvider HooksProvider + session.ManagementProvider + x.WriterProvider + x.LoggingProvider } HookExecutor struct { d executorDependencies @@ -36,26 +44,31 @@ type ( ) func NewHookExecutor(d executorDependencies, c configuration.Provider) *HookExecutor { - return &HookExecutor{d: d, c: c} + return &HookExecutor{ + d: d, + c: c, + } } -func (e *HookExecutor) PostLoginHook(w http.ResponseWriter, r *http.Request, hooks []PostHookExecutor, a *Request, i *identity.Identity) error { - s := session.NewSession(i, r, e.c) +func (e *HookExecutor) PostLoginHook(w http.ResponseWriter, r *http.Request, ct identity.CredentialsType, a *Request, i *identity.Identity) error { + s := session.NewSession(i, e.c, time.Now().UTC()).Declassify() - for _, executor := range hooks { + for _, executor := range e.d.PostLoginHooks(ct) { if err := executor.ExecuteLoginPostHook(w, r, a, s); err != nil { + if errors.Is(err, ErrHookAbortRequest) { + e.d.Logger().Warn("A successful login attempt was aborted because a hook returned ErrHookAbortRequest.") + return nil + } return err } } - if s.WasIdentityModified() { - if err := e.d.IdentityManager().Update(r.Context(), s.Identity); err != nil { - return err - } + if err := e.d.SessionManager().CreateToRequest(r.Context(), w, r, s); err != nil { + return errors.WithStack(err) } - s.ResetModifiedIdentityFlag() - return nil + return x.SecureContentNegotiationRedirection(w, r, s.Declassify(), a.RequestURL, + e.d.Writer(), e.c, x.SecureRedirectOverrideDefaultReturnTo(e.c.SelfServiceLoginReturnTo(ct.String()))) } func (e *HookExecutor) PreLoginHook(w http.ResponseWriter, r *http.Request, a *Request) error { diff --git a/selfservice/flow/login/hook_test.go b/selfservice/flow/login/hook_test.go index 86fa07a63528..6ca83d3fb592 100644 --- a/selfservice/flow/login/hook_test.go +++ b/selfservice/flow/login/hook_test.go @@ -1,143 +1,146 @@ package login_test import ( - "context" - "errors" - "fmt" "net/http" + "net/url" "testing" + "time" - "github.com/bxcodec/faker" + "github.com/gobuffalo/httptest" + "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" "github.com/ory/viper" "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow/login" - "github.com/ory/kratos/session" + "github.com/ory/kratos/x" ) -type loginPreHookMock struct { - err error -} - -func (m *loginPreHookMock) ExecuteLoginPreHook(w http.ResponseWriter, r *http.Request, a *login.Request) error { - return m.err -} - -type mockPostHook struct { - err error - modifyIdentity bool -} - -var updatedSchema = configuration.SchemaConfig{ - ID: "updatedSchema", - URL: "file://./stub/updated.schema.json", -} - -func (m *mockPostHook) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, a *login.Request, s *session.Session) error { - if m.modifyIdentity { - i := s.Identity - i.TraitsSchemaID = updatedSchema.ID - s.UpdateIdentity(i) - } - return m.err -} - -type loginExecutorDependenciesMock struct { - preErr []error -} - -func (m *loginExecutorDependenciesMock) PostLoginHooks(credentialsType identity.CredentialsType) []login.PostHookExecutor { - return []login.PostHookExecutor{} -} - -func (m *loginExecutorDependenciesMock) IdentityManager() *identity.Manager { - return nil -} - -func (m *loginExecutorDependenciesMock) PreLoginHooks() []login.PreHookExecutor { - hooks := make([]login.PreHookExecutor, len(m.preErr)) - for k := range hooks { - hooks[k] = &loginPreHookMock{m.preErr[k]} - } - return hooks -} - func TestLoginExecutor(t *testing.T) { - t.Run("method=PostLoginHook", func(t *testing.T) { - for k, tc := range []struct { - hooks []login.PostHookExecutor - expectSchemaID string - expectErr error - }{ - {hooks: nil}, - {hooks: []login.PostHookExecutor{}}, - {hooks: []login.PostHookExecutor{&mockPostHook{err: errors.New("err")}}, expectErr: errors.New("err")}, - {hooks: []login.PostHookExecutor{ - new(mockPostHook), - &mockPostHook{err: errors.New("err")}}, expectErr: errors.New("err"), - }, - { - hooks: []login.PostHookExecutor{ - new(mockPostHook), - &mockPostHook{modifyIdentity: true}, - }, - expectSchemaID: updatedSchema.ID, - }, - } { - t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - conf, reg := internal.NewRegistryDefault(t) - - var i identity.Identity - require.NoError(t, faker.FakeData(&i)) - i.TraitsSchemaID = "" - i.Traits = identity.Traits(`{}`) - viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/login.schema.json") - viper.Set(configuration.ViperKeyIdentityTraitsSchemas, []configuration.SchemaConfig{updatedSchema}) - viper.Set(configuration.ViperKeyURLsSelfPublic, "http://example.com") - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.TODO(), &i)) - - e := login.NewHookExecutor(reg, conf) - err := e.PostLoginHook(nil, &http.Request{}, tc.hooks, nil, &i) - if tc.expectErr != nil { - require.EqualError(t, err, tc.expectErr.Error()) - return - } - - require.NoError(t, err) - if tc.expectSchemaID != "" { - got, err := reg.IdentityPool().GetIdentity(context.TODO(), i.ID) - require.NoError(t, err) - assert.EqualValues(t, tc.expectSchemaID, got.TraitsSchemaID) - } - }) - } - }) - - t.Run("method=PreLoginHook", func(t *testing.T) { - for k, tc := range []struct { - expectErr error - reg *loginExecutorDependenciesMock - }{ - { - reg: &loginExecutorDependenciesMock{preErr: []error{nil, nil, errors.New("err")}}, - expectErr: errors.New("err"), - }, - {reg: &loginExecutorDependenciesMock{preErr: []error{nil}}}, - } { - t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - conf, _ := internal.NewRegistryDefault(t) - e := login.NewHookExecutor(tc.reg, conf) - if tc.expectErr == nil { - require.NoError(t, e.PreLoginHook(nil, nil, nil)) - } else { - require.EqualError(t, e.PreLoginHook(nil, nil, nil), tc.expectErr.Error()) - } + for _, strategy := range []string{ + identity.CredentialsTypePassword.String(), + identity.CredentialsTypeOIDC.String(), + } { + t.Run("strategy="+strategy, func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/login.schema.json") + viper.Set(configuration.ViperKeyURLsDefaultReturnTo, "https://www.ory.sh/") + + newServer := func(t *testing.T) *httptest.Server { + router := httprouter.New() + + router.GET("/login/pre", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + if testhelpers.SelfServiceHookLoginErrorHandler(t, w, r, reg.LoginHookExecutor().PreLoginHook(w, r, login.NewRequest(time.Minute, "", r))) { + _, _ = w.Write([]byte("ok")) + } + }) + + router.GET("/login/post", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + a := login.NewRequest(time.Minute, "", r) + a.RequestURL = x.RequestURL(r).String() + testhelpers.SelfServiceHookLoginErrorHandler(t, w, r, + reg.LoginHookExecutor().PostLoginHook(w, r, identity.CredentialsType(strategy), a, testhelpers.SelfServiceHookCreateFakeIdentity(t, reg))) + }) + + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL) + return ts + } + + makeRequestPost := testhelpers.SelfServiceMakeLoginPostHookRequest + viperSetPost := testhelpers.SelfServiceHookLoginViperSetPost + + t.Run("method=PostLoginHook", func(t *testing.T) { + t.Run("case=pass without hooks", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + + res, _ := makeRequestPost(t, newServer(t), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/", res.Request.URL.String()) + }) + + t.Run("case=pass if hooks pass", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + viperSetPost(strategy, []configuration.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) + + res, _ := makeRequestPost(t, newServer(t), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/", res.Request.URL.String()) + }) + + t.Run("case=fail if hooks fail", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + viperSetPost(strategy, []configuration.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecuteLoginPostHook": "abort"}`)}}) + + res, body := makeRequestPost(t, newServer(t), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "", body) + }) + + t.Run("case=prevent return_to value because domain not whitelisted", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + + res, _ := makeRequestPost(t, newServer(t), false, url.Values{"return_to": {"https://www.ory.sh/kratos/"}}) + assert.EqualValues(t, http.StatusInternalServerError, res.StatusCode) + }) + + t.Run("case=use return_to value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + viper.Set(configuration.ViperKeyURLsWhitelistedReturnToDomains, []string{"https://www.ory.sh/"}) + + res, _ := makeRequestPost(t, newServer(t), false, url.Values{"return_to": {"https://www.ory.sh/kratos/"}}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/kratos/", res.Request.URL.String()) + }) + + t.Run("case=use nested config value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + viper.Set(configuration.ViperKeySelfServiceLoginAfter+"."+configuration.ViperKeyDefaultReturnTo, "https://www.ory.sh/kratos") + + res, _ := makeRequestPost(t, newServer(t), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/kratos/", res.Request.URL.String()) + }) + + t.Run("case=use nested config value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + testhelpers.SelfServiceHookLoginSetDefaultRedirectTo("https://www.ory.sh/not-kratos") + testhelpers.SelfServiceHookLoginSetDefaultRedirectToStrategy(strategy, "https://www.ory.sh/kratos") + + res, _ := makeRequestPost(t, newServer(t), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/kratos/", res.Request.URL.String()) + }) + + t.Run("case=pass if hooks pass", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + viperSetPost(strategy, []configuration.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) + + res, _ := makeRequestPost(t, newServer(t), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/", res.Request.URL.String()) + }) + + t.Run("case=send a json response for API clients", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + + res, body := makeRequestPost(t, newServer(t), true, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.NotEmpty(t, gjson.Get(body, "identity.id")) + }) }) - } - }) + + t.Run("method=PreLoginHook", testhelpers.TestSelfServicePreHook( + configuration.ViperKeySelfServiceLoginBeforeHooks, + testhelpers.SelfServiceMakeLoginPreHookRequest, + newServer, + )) + }) + } } diff --git a/selfservice/flow/login/request.go b/selfservice/flow/login/request.go index 3fd7ac66f5f8..4817de010129 100644 --- a/selfservice/flow/login/request.go +++ b/selfservice/flow/login/request.go @@ -70,7 +70,7 @@ type Request struct { Forced bool `json:"forced" db:"forced"` } -func NewLoginRequest(exp time.Duration, csrf string, r *http.Request) *Request { +func NewRequest(exp time.Duration, csrf string, r *http.Request) *Request { source := urlx.Copy(r.URL) source.Host = r.Host diff --git a/selfservice/flow/logout/handler_test.go b/selfservice/flow/logout/handler_test.go index 09eb78b8a551..3ed7c30ab56a 100644 --- a/selfservice/flow/logout/handler_test.go +++ b/selfservice/flow/logout/handler_test.go @@ -18,13 +18,14 @@ import ( "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow/logout" "github.com/ory/kratos/session" "github.com/ory/kratos/x" ) func TestLogoutHandler(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + conf, reg := internal.NewFastRegistryWithMocks(t) handler := reg.LogoutHandler() viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/registration.schema.json") @@ -42,7 +43,7 @@ func TestLogoutHandler(t *testing.T) { require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), sess.Identity)) require.NoError(t, reg.SessionPersister().CreateSession(context.Background(), &sess)) - router.GET("/set", session.MockSetSession(t, reg)) + router.GET("/set", testhelpers.MockSetSession(t, reg, conf)) router.GET("/csrf", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { _, _ = w.Write([]byte(nosurf.Token(r))) @@ -56,10 +57,10 @@ func TestLogoutHandler(t *testing.T) { viper.Set(configuration.ViperKeySelfServiceLogoutRedirectURL, redirTS.URL) viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL) - client := session.MockCookieClient(t) + client := testhelpers.MockCookieClient(t) t.Run("case=set initial session", func(t *testing.T) { - session.MockHydrateCookieClient(t, client, ts.URL+"/set") + testhelpers.MockHydrateCookieClient(t, client, ts.URL+"/set") }) var token string diff --git a/selfservice/flow/registration/error.go b/selfservice/flow/registration/error.go index e572110838bc..26a241d5f6ca 100644 --- a/selfservice/flow/registration/error.go +++ b/selfservice/flow/registration/error.go @@ -22,7 +22,7 @@ import ( ) var ( - ErrHookAbortRequest = errors.New("abort hook") + ErrHookAbortRequest = errors.New("aborted registration hook execution") ) type ( diff --git a/selfservice/flow/registration/handler.go b/selfservice/flow/registration/handler.go index c9cf7a5d8e5b..7290a9d95901 100644 --- a/selfservice/flow/registration/handler.go +++ b/selfservice/flow/registration/handler.go @@ -9,7 +9,6 @@ import ( "github.com/justinas/nosurf" "github.com/pkg/errors" - "github.com/ory/x/errorsx" "github.com/ory/x/urlx" "github.com/ory/kratos/driver/configuration" @@ -65,7 +64,7 @@ func (h *Handler) NewRegistrationRequest(w http.ResponseWriter, r *http.Request, } if err := h.d.RegistrationExecutor().PreRegistrationHook(w, r, a); err != nil { - if errorsx.Cause(err) == ErrHookAbortRequest { + if errors.Is(err, ErrHookAbortRequest) { return nil } return err diff --git a/selfservice/flow/registration/handler_test.go b/selfservice/flow/registration/handler_test.go index b47a362e930f..5c9ed39628b3 100644 --- a/selfservice/flow/registration/handler_test.go +++ b/selfservice/flow/registration/handler_test.go @@ -19,7 +19,6 @@ import ( "github.com/ory/kratos/internal" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow/registration" - "github.com/ory/kratos/session" "github.com/ory/kratos/x" ) @@ -28,7 +27,7 @@ func init() { } func TestHandlerRedirectOnAuthenticated(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + conf, reg := internal.NewFastRegistryWithMocks(t) router := x.NewRouterPublic() reg.RegistrationHandler().RegisterPublicRoutes(router) @@ -46,13 +45,13 @@ func TestHandlerRedirectOnAuthenticated(t *testing.T) { viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/registration.schema.json") t.Run("does redirect to default on authenticated request", func(t *testing.T) { - body, _ := session.MockMakeAuthenticatedRequest(t, reg, router.Router, x.NewTestHTTPRequest(t, "GET", ts.URL+registration.BrowserRegistrationPath, nil)) + body, _ := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, x.NewTestHTTPRequest(t, "GET", ts.URL+registration.BrowserRegistrationPath, nil)) assert.EqualValues(t, "already authenticated", string(body)) }) } func TestRegistrationHandler(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) public, admin := func() (*httptest.Server, *httptest.Server) { public := x.NewRouterPublic() diff --git a/selfservice/flow/registration/hook.go b/selfservice/flow/registration/hook.go index ca8a35a65b7a..3aebd1334812 100644 --- a/selfservice/flow/registration/hook.go +++ b/selfservice/flow/registration/hook.go @@ -1,7 +1,9 @@ package registration import ( + "errors" "net/http" + "time" "github.com/ory/x/errorsx" "github.com/ory/x/sqlcon" @@ -17,21 +19,42 @@ type ( PreHookExecutor interface { ExecuteRegistrationPreHook(w http.ResponseWriter, r *http.Request, a *Request) error } - PostHookExecutor interface { - ExecuteRegistrationPostHook(w http.ResponseWriter, r *http.Request, a *Request, s *session.Session) error + PreHookExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Request) error + + PostHookPostPersistExecutor interface { + ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *Request, s *session.Session) error + } + PostHookPostPersistExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Request, s *session.Session) error + + PostHookPrePersistExecutor interface { + ExecutePostRegistrationPrePersistHook(w http.ResponseWriter, r *http.Request, a *Request, i *identity.Identity) error } + PostHookPrePersistExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Request, i *identity.Identity) error + HooksProvider interface { PreRegistrationHooks() []PreHookExecutor - PostRegistrationHooks(credentialsType identity.CredentialsType) []PostHookExecutor + PostRegistrationPrePersistHooks(credentialsType identity.CredentialsType) []PostHookPrePersistExecutor + PostRegistrationPostPersistHooks(credentialsType identity.CredentialsType) []PostHookPostPersistExecutor } ) +func (f PreHookExecutorFunc) ExecuteRegistrationPreHook(w http.ResponseWriter, r *http.Request, a *Request) error { + return f(w, r, a) +} +func (f PostHookPostPersistExecutorFunc) ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *Request, s *session.Session) error { + return f(w, r, a, s) +} +func (f PostHookPrePersistExecutorFunc) ExecutePostRegistrationPrePersistHook(w http.ResponseWriter, r *http.Request, a *Request, i *identity.Identity) error { + return f(w, r, a, i) +} + type ( executorDependencies interface { identity.ManagementProvider identity.ValidationProvider HooksProvider x.LoggingProvider + x.WriterProvider } HookExecutor struct { d executorDependencies @@ -42,25 +65,29 @@ type ( } ) -func NewHookExecutor( - d executorDependencies, - c configuration.Provider, -) *HookExecutor { +func NewHookExecutor(d executorDependencies, c configuration.Provider) *HookExecutor { return &HookExecutor{ d: d, c: c, } } -func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Request, hooks []PostHookExecutor, a *Request, i *identity.Identity) error { - s := session.NewSession(i, r, e.c) +func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Request, ct identity.CredentialsType, a *Request, i *identity.Identity) error { + for _, executor := range e.d.PostRegistrationPrePersistHooks(ct) { + if err := executor.ExecutePostRegistrationPrePersistHook(w, r, a, i); err != nil { + if errors.Is(err, ErrHookAbortRequest) { + return nil + } + return err + } + } // We need to make sure that the identity has a valid schema before passing it down to the identity pool. - if err := e.d.IdentityValidator().Validate(s.Identity); err != nil { + if err := e.d.IdentityValidator().Validate(i); err != nil { return err // We're now creating the identity because any of the hooks could trigger a "redirect" or a "session" which // would imply that the identity has to exist already. - } else if err := e.d.IdentityManager().Create(r.Context(), s.Identity); err != nil { + } else if err := e.d.IdentityManager().Create(r.Context(), i); err != nil { if errorsx.Cause(err) == sqlcon.ErrUniqueViolation { return schema.NewDuplicateCredentialsError() } @@ -71,28 +98,22 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque WithField("identity_id", i.ID). Debug("A new identity has registered using self-service registration. Running post execution hooks.") - // Now we execute the post-registration hooks! - for _, executor := range hooks { - if err := executor.ExecuteRegistrationPostHook(w, r, a, s); err != nil { - // TODO https://github.com/ory/kratos/issues/51 #51 + s := session.NewSession(i, e.c, time.Now().UTC()) + for _, executor := range e.d.PostRegistrationPostPersistHooks(ct) { + if err := executor.ExecutePostRegistrationPostPersistHook(w, r, a, s); err != nil { + if errors.Is(err, ErrHookAbortRequest) { + return nil + } return err } } - // We need to make sure that the identity has a valid schema before passing it down to the identity pool. - if err := e.d.IdentityValidator().Validate(s.Identity); err != nil { - return err - // We're now creating the identity because any of the hooks could trigger a "redirect" or a "session" which - // would imply that the identity has to exist already. - } else if err := e.d.IdentityManager().Update(r.Context(), s.Identity); err != nil { - return err - } - e.d.Logger(). WithField("identity_id", i.ID). Debug("Post registration execution hooks completed successfully.") - return nil + return x.SecureContentNegotiationRedirection(w, r, s.Declassify(), a.RequestURL, + e.d.Writer(), e.c, x.SecureRedirectOverrideDefaultReturnTo(e.c.SelfServiceRegistrationReturnTo(ct.String()))) } func (e *HookExecutor) PreRegistrationHook(w http.ResponseWriter, r *http.Request, a *Request) error { diff --git a/selfservice/flow/registration/hook_test.go b/selfservice/flow/registration/hook_test.go index 02cd19406115..1eea109a3546 100644 --- a/selfservice/flow/registration/hook_test.go +++ b/selfservice/flow/registration/hook_test.go @@ -2,150 +2,166 @@ package registration_test import ( "context" - "errors" - "fmt" "net/http" + "net/url" "testing" + "time" - "github.com/bxcodec/faker" - "github.com/sirupsen/logrus" + "github.com/gobuffalo/httptest" + "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" "github.com/ory/viper" "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow/registration" - "github.com/ory/kratos/session" + "github.com/ory/kratos/x" ) -type registrationPostHookMock struct { - err error - modifyIdentity bool -} - -func (m *registrationPostHookMock) ExecuteRegistrationPostHook(w http.ResponseWriter, r *http.Request, a *registration.Request, s *session.Session) error { - if m.modifyIdentity { - i := s.Identity - i.Traits = identity.Traits(`{"foo":"bar"}"`) - s.UpdateIdentity(i) - } - return m.err -} - -type registrationPreHookMock struct { - err error -} - -func (m *registrationPreHookMock) ExecuteRegistrationPreHook(w http.ResponseWriter, r *http.Request, a *registration.Request) error { - return m.err -} +func TestRegistrationExecutor(t *testing.T) { + for _, strategy := range []string{ + identity.CredentialsTypePassword.String(), + identity.CredentialsTypeOIDC.String(), + } { + t.Run("strategy="+strategy, func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/registration.schema.json") + viper.Set(configuration.ViperKeyURLsDefaultReturnTo, "https://www.ory.sh/") + + newServer := func(t *testing.T, i *identity.Identity) *httptest.Server { + router := httprouter.New() + handleErr := testhelpers.SelfServiceHookRegistrationErrorHandler + router.GET("/registration/pre", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + if handleErr(t, w, r, reg.RegistrationHookExecutor().PreRegistrationHook(w, r, registration.NewRequest(time.Minute, x.FakeCSRFToken, r))) { + w.Write([]byte("ok")) + } + }) + + router.GET("/registration/post", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + if i == nil { + i = testhelpers.SelfServiceHookFakeIdentity(t) + } + a := registration.NewRequest(time.Minute, x.FakeCSRFToken, r) + a.RequestURL = x.RequestURL(r).String() + _ = handleErr(t, w, r, reg.RegistrationHookExecutor().PostRegistrationHook(w, r, identity.CredentialsType(strategy), a, i)) + }) + + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL) + return ts + } + + makeRequestPost := testhelpers.SelfServiceMakeRegistrationPostHookRequest + viperSetPost := testhelpers.SelfServiceHookRegistrationViperSetPost + t.Run("method=PostRegistrationHook", func(t *testing.T) { + t.Run("case=pass without hooks", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + i := testhelpers.SelfServiceHookFakeIdentity(t) + + ts := newServer(t, i) + res, _ := makeRequestPost(t, ts, false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/", res.Request.URL.String()) + + actual, err := reg.IdentityPool().GetIdentity(context.Background(), i.ID) + require.NoError(t, err) + assert.Equal(t, actual.Traits, i.Traits) + }) -type registrationExecutorDependenciesMock struct { - preErr []error -} + t.Run("case=pass if hooks pass", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + viperSetPost(strategy, []configuration.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) -func (m *registrationExecutorDependenciesMock) PostRegistrationHooks(credentialsType identity.CredentialsType) []registration.PostHookExecutor { - return nil -} + res, _ := makeRequestPost(t, newServer(t, nil), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/", res.Request.URL.String()) + }) -func (m *registrationExecutorDependenciesMock) IdentityManager() *identity.Manager { - return nil -} + t.Run("case=fail if hooks fail", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + viperSetPost(strategy, []configuration.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecutePostRegistrationPrePersistHook": "abort"}`)}}) + i := testhelpers.SelfServiceHookFakeIdentity(t) -func (m *registrationExecutorDependenciesMock) PrivilegedIdentityPool() identity.PrivilegedPool { - return nil -} + res, body := makeRequestPost(t, newServer(t, i), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "", body) -func (m *registrationExecutorDependenciesMock) IdentityValidator() *identity.Validator { - return nil -} + _, err := reg.IdentityPool().GetIdentity(context.Background(), i.ID) + require.Error(t, err) + }) -func (m *registrationExecutorDependenciesMock) Logger() logrus.FieldLogger { - return logrus.New() -} + t.Run("case=prevent return_to value because domain not whitelisted", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + i := testhelpers.SelfServiceHookFakeIdentity(t) -func (m *registrationExecutorDependenciesMock) PreRegistrationHooks() []registration.PreHookExecutor { - hooks := make([]registration.PreHookExecutor, len(m.preErr)) - for k := range hooks { - hooks[k] = ®istrationPreHookMock{m.preErr[k]} - } - return hooks -} + res, body := makeRequestPost(t, newServer(t, i), false, url.Values{"return_to": {"https://www.ory.sh/kratos/"}}) + assert.EqualValues(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, body, "malformed or contained invalid") -func TestRegistrationExecutor(t *testing.T) { - t.Run("method=PostRegistrationHook", func(t *testing.T) { - for k, tc := range []struct { - hooks []registration.PostHookExecutor - expectTraits string - expectErr error - }{ - {hooks: nil}, - {hooks: []registration.PostHookExecutor{}}, - {hooks: []registration.PostHookExecutor{®istrationPostHookMock{err: errors.New("err")}}, expectErr: errors.New("err")}, - {hooks: []registration.PostHookExecutor{ - new(registrationPostHookMock), - ®istrationPostHookMock{err: errors.New("err")}}, expectErr: errors.New("err"), - }, - { - hooks: []registration.PostHookExecutor{ - new(registrationPostHookMock), - ®istrationPostHookMock{modifyIdentity: true}, - }, - expectTraits: `{"foo":"bar"}`, - }, - } { - t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - conf, reg := internal.NewRegistryDefault(t) - viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://stub/registration.schema.json") - viper.Set(configuration.ViperKeyURLsSelfPublic, "http://example.com") - - var i identity.Identity - require.NoError(t, faker.FakeData(&i)) - i.TraitsSchemaID = "" - i.Traits = identity.Traits("{}") - - e := registration.NewHookExecutor(reg, conf) - err := e.PostRegistrationHook(nil, &http.Request{}, tc.hooks, nil, &i) - if tc.expectErr != nil { - require.EqualError(t, err, tc.expectErr.Error()) - return - } - - require.NoError(t, err) - if tc.expectTraits != "" { - got, err := reg.IdentityPool().GetIdentity(context.TODO(), i.ID) + actual, err := reg.IdentityPool().GetIdentity(context.Background(), i.ID) require.NoError(t, err) - assert.EqualValues(t, tc.expectTraits, string(got.Traits)) - } - }) - } - }) - - t.Run("method=PreRegistrationHook", func(t *testing.T) { - for k, tc := range []struct { - expectErr error - reg *registrationExecutorDependenciesMock - }{ - { - reg: ®istrationExecutorDependenciesMock{preErr: []error{nil, nil, errors.New("err")}}, - expectErr: errors.New("err"), - }, - { - reg: ®istrationExecutorDependenciesMock{preErr: []error{nil}}, - }, - } { - t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - conf, _ := internal.NewRegistryDefault(t) - e := registration.NewHookExecutor(tc.reg, conf) - if tc.expectErr == nil { - require.NoError(t, e.PreRegistrationHook(nil, nil, nil)) - } else { - require.EqualError(t, e.PreRegistrationHook(nil, nil, nil), tc.expectErr.Error()) - } + assert.Equal(t, actual.Traits, i.Traits) + }) + + t.Run("case=use return_to value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + viper.Set(configuration.ViperKeyURLsWhitelistedReturnToDomains, []string{"https://www.ory.sh/"}) + + res, _ := makeRequestPost(t, newServer(t, nil), false, url.Values{"return_to": {"https://www.ory.sh/kratos/"}}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/kratos/", res.Request.URL.String()) + }) + + t.Run("case=use nested config value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + testhelpers.SelfServiceHookRegistrationSetDefaultRedirectToStrategy(strategy, "https://www.ory.sh/kratos") + + res, _ := makeRequestPost(t, newServer(t, nil), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/kratos/", res.Request.URL.String()) + }) + + t.Run("case=use nested config value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + testhelpers.SelfServiceHookRegistrationSetDefaultRedirectTo("https://www.ory.sh/not-kratos") + testhelpers.SelfServiceHookRegistrationSetDefaultRedirectToStrategy(strategy, "https://www.ory.sh/kratos") + + res, _ := makeRequestPost(t, newServer(t, nil), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/kratos/", res.Request.URL.String()) + }) + + t.Run("case=pass if hooks pass", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + viperSetPost(strategy, []configuration.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) + + res, _ := makeRequestPost(t, newServer(t, nil), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/", res.Request.URL.String()) + }) + + t.Run("case=send a json response for API clients", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + + res, body := makeRequestPost(t, newServer(t, nil), true, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.NotEmpty(t, gjson.Get(body, "identity.id")) + }) }) - } - }) + + t.Run("method=PreRegistrationHook", testhelpers.TestSelfServicePreHook( + configuration.ViperKeySelfServiceRegistrationBeforeHooks, + testhelpers.SelfServiceMakeRegistrationPreHookRequest, + func(t *testing.T) *httptest.Server { + return newServer(t, nil) + }, + )) + }) + } } diff --git a/selfservice/flow/settings/error.go b/selfservice/flow/settings/error.go index 6d91fca1def0..64cd0eb3aef7 100644 --- a/selfservice/flow/settings/error.go +++ b/selfservice/flow/settings/error.go @@ -22,7 +22,7 @@ var ( ErrRequestExpired = herodot.ErrBadRequest. WithError("settings request expired"). WithReasonf(`The settings request has expired. Please restart the flow.`) - ErrHookAbortRequest = errors.New("abort hook") + ErrHookAbortRequest = errors.New("aborted settings hook execution") ErrRequestNeedsReAuthentication = herodot.ErrForbidden.WithReasonf("The login session is too old and thus not allowed to update these fields. Please re-authenticate.") ) diff --git a/selfservice/flow/settings/handler_test.go b/selfservice/flow/settings/handler_test.go index 8e132d068a22..abf0079699bf 100644 --- a/selfservice/flow/settings/handler_test.go +++ b/selfservice/flow/settings/handler_test.go @@ -31,7 +31,7 @@ func init() { } func TestHandler(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/identity.schema.json") _ = testhelpers.NewSettingsUITestServer(t) @@ -116,7 +116,7 @@ func TestHandler(t *testing.T) { }) t.Run("description=should fail to post data if CSRF is missing", func(t *testing.T) { - f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyTraitsID) + f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyProfile) res, err := primaryUser.PostForm(pointerx.StringR(f.Action), url.Values{}) require.NoError(t, err) assert.EqualValues(t, 400, res.StatusCode, "should return a 400 error because CSRF token is not set") diff --git a/selfservice/flow/settings/hook.go b/selfservice/flow/settings/hook.go index 24e909a43c88..6d29a916162f 100644 --- a/selfservice/flow/settings/hook.go +++ b/selfservice/flow/settings/hook.go @@ -2,10 +2,13 @@ package settings import ( "net/http" + "net/url" "time" "github.com/pkg/errors" + "github.com/ory/x/urlx" + "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/identity" "github.com/ory/kratos/session" @@ -13,14 +16,30 @@ import ( ) type ( - PostHookExecutor interface { - ExecuteSettingsPostHook(w http.ResponseWriter, r *http.Request, a *Request, s *session.Session) error + PostHookPrePersistExecutor interface { + ExecuteSettingsPrePersistHook(w http.ResponseWriter, r *http.Request, a *Request, s *identity.Identity) error + } + PostHookPrePersistExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Request, s *identity.Identity) error + + PostHookPostPersistExecutor interface { + ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http.Request, a *Request, s *identity.Identity) error } + PostHookPostPersistExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Request, s *identity.Identity) error + HooksProvider interface { - PostSettingsHooks(credentialsType string) []PostHookExecutor + PostSettingsPrePersistHooks(settingsType string) []PostHookPrePersistExecutor + PostSettingsPostPersistHooks(settingsType string) []PostHookPostPersistExecutor } ) +func (f PostHookPrePersistExecutorFunc) ExecuteSettingsPrePersistHook(w http.ResponseWriter, r *http.Request, a *Request, s *identity.Identity) error { + return f(w, r, a, s) +} + +func (f PostHookPostPersistExecutorFunc) ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http.Request, a *Request, s *identity.Identity) error { + return f(w, r, a, s) +} + type ( executorDependencies interface { identity.ManagementProvider @@ -28,13 +47,14 @@ type ( HooksProvider x.LoggingProvider RequestPersistenceProvider + x.WriterProvider } HookExecutor struct { d executorDependencies c configuration.Provider } HookExecutorProvider interface { - SettingsExecutor() *HookExecutor + SettingsHookExecutor() *HookExecutor } ) @@ -48,7 +68,20 @@ func NewHookExecutor( } } -func (e *HookExecutor) PostSettingsHook(w http.ResponseWriter, r *http.Request, hooks []PostHookExecutor, a *Request, ss *session.Session, i *identity.Identity) error { +func (e *HookExecutor) PostSettingsHook(w http.ResponseWriter, r *http.Request, settingsType string, a *Request, ss *session.Session, i *identity.Identity) error { + e.d.Logger(). + WithField("identity_id", i.ID). + Debug("An identity's settings have been updated, running post hooks.") + + for _, executor := range e.d.PostSettingsPrePersistHooks(settingsType) { + if err := executor.ExecuteSettingsPrePersistHook(w, r, a, i); err != nil { + if errors.Is(err, ErrHookAbortRequest) { + return nil + } + return err + } + } + options := []identity.ManagerOption{identity.ManagerExposeValidationErrors} if ss.AuthenticatedAt.Add(e.c.SelfServicePrivilegedSessionMaxAge()).After(time.Now()) { options = append(options, identity.ManagerAllowWriteProtectedTraits) @@ -63,18 +96,15 @@ func (e *HookExecutor) PostSettingsHook(w http.ResponseWriter, r *http.Request, ss.Identity = i a.UpdateSuccessful = true - if err := e.d.SettingsRequestPersister().UpdateSettingsRequest(r.Context(), a); err != nil { return err } - e.d.Logger(). - WithField("identity_id", i.ID). - Debug("An identity's settings have been updated, running post hooks.") - - // Now we execute the post-Settings hooks! - for _, executor := range hooks { - if err := executor.ExecuteSettingsPostHook(w, r, a, ss); err != nil { + for _, executor := range e.d.PostSettingsPostPersistHooks(settingsType) { + if err := executor.ExecuteSettingsPostPersistHook(w, r, a, i); err != nil { + if errors.Is(err, ErrHookAbortRequest) { + return nil + } return err } } @@ -83,5 +113,10 @@ func (e *HookExecutor) PostSettingsHook(w http.ResponseWriter, r *http.Request, WithField("identity_id", i.ID). Debug("Post settings execution hooks completed successfully.") - return nil + return x.SecureContentNegotiationRedirection(w, r, ss.Declassify(), a.RequestURL, e.d.Writer(), e.c, + x.SecureRedirectOverrideDefaultReturnTo( + e.c.SelfServiceSettingsReturnTo(settingsType, + urlx.CopyWithQuery( + e.c.SettingsURL(), + url.Values{"request": {a.ID.String()}})))) } diff --git a/selfservice/flow/settings/hook_test.go b/selfservice/flow/settings/hook_test.go index 090671f47dd7..1c5d6e240739 100644 --- a/selfservice/flow/settings/hook_test.go +++ b/selfservice/flow/settings/hook_test.go @@ -1,91 +1,153 @@ package settings_test import ( - "context" - "errors" - "fmt" "net/http" "net/url" "testing" "time" - "github.com/bxcodec/faker" + "github.com/gobuffalo/httptest" + "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" "github.com/ory/viper" "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/hook" "github.com/ory/kratos/session" + "github.com/ory/kratos/x" ) -type mockPostHook struct { - err error -} +func TestSettingsExecutor(t *testing.T) { + for _, strategy := range []string{ + identity.CredentialsTypePassword.String(), + settings.StrategyProfile, + } { + t.Run("strategy="+strategy, func(t *testing.T) { + conf, reg := internal.NewFastRegistryWithMocks(t) + viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/identity.schema.json") + viper.Set(configuration.ViperKeyURLsDefaultReturnTo, "https://www.ory.sh/") -func (m *mockPostHook) ExecuteSettingsPostHook(w http.ResponseWriter, r *http.Request, a *settings.Request, s *session.Session) error { - return m.err -} + reg.WithHooks(map[string]func(configuration.SelfServiceHook) interface{}{ + "err": func(c configuration.SelfServiceHook) interface{} { + return &hook.Error{Config: c.Config} + }, + }) -type loginExecutorDependenciesMock struct { - preErr []error -} + newServer := func(t *testing.T) *httptest.Server { + router := httprouter.New() + handleErr := testhelpers.SelfServiceHookSettingsErrorHandler + router.GET("/settings/post", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + i := testhelpers.SelfServiceHookCreateFakeIdentity(t, reg) + sess := session.NewSession(i, conf, time.Now().UTC()) -func (m *loginExecutorDependenciesMock) PostLoginHooks(credentialsType identity.CredentialsType) []settings.PostHookExecutor { - hooks := make([]settings.PostHookExecutor, len(m.preErr)) - for k := range hooks { - hooks[k] = &mockPostHook{m.preErr[k]} - } - return hooks -} + a := settings.NewRequest(time.Minute, r, sess) + a.RequestURL = x.RequestURL(r).String() + require.NoError(t, reg.SettingsRequestPersister().CreateSettingsRequest(r.Context(), a)) + _ = handleErr(t, w, r, reg.SettingsHookExecutor(). + PostSettingsHook(w, r, strategy, a, sess, i)) + }) + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL) + return ts + } -func TestSettingsExecutor(t *testing.T) { - t.Run("method=PostSettingsHook", func(t *testing.T) { - for k, tc := range []struct { - hooks []settings.PostHookExecutor - expectSchemaID string - expectErr error - }{ - {hooks: nil}, - {hooks: []settings.PostHookExecutor{}}, - {hooks: []settings.PostHookExecutor{&mockPostHook{err: errors.New("err")}}, expectErr: errors.New("err")}, - {hooks: []settings.PostHookExecutor{ - new(mockPostHook), - &mockPostHook{err: errors.New("err")}}, expectErr: errors.New("err"), - }, - } { - t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - conf, reg := internal.NewRegistryDefault(t) - - var i identity.Identity - require.NoError(t, faker.FakeData(&i)) - i.TraitsSchemaID = "" - i.Traits = identity.Traits(`{}`) - viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/identity.schema.json") - viper.Set(configuration.ViperKeyURLsSelfPublic, "http://example.com") - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.TODO(), &i)) - - sess := &session.Session{Identity: &i, IdentityID: i.ID} - pr := settings.NewRequest(time.Hour, &http.Request{URL: new(url.URL)}, sess) - require.NoError(t, reg.SettingsRequestPersister().CreateSettingsRequest(context.Background(), pr)) - - e := settings.NewHookExecutor(reg, conf) - err := e.PostSettingsHook(nil, &http.Request{}, tc.hooks, pr, sess, &i) - if tc.expectErr != nil { - require.EqualError(t, err, tc.expectErr.Error()) - return - } - - require.NoError(t, err) - if tc.expectSchemaID != "" { - got, err := reg.IdentityPool().GetIdentity(context.TODO(), i.ID) - require.NoError(t, err) - assert.EqualValues(t, tc.expectSchemaID, got.TraitsSchemaID) - } + makeRequestPost := testhelpers.SelfServiceMakeSettingsPostHookRequest + viperSetPost := func(strategy string, c []configuration.SelfServiceHook) { + viper.Set(configuration.HookStrategyKey(configuration.ViperKeySelfServiceSettingsAfter, strategy), c) + } + + uiTS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + t.Cleanup(uiTS.Close) + uiURL := uiTS.URL + "/user/settings" + viper.Set(configuration.ViperKeyURLsSettings, uiURL) + + t.Run("method=PostSettingsHook", func(t *testing.T) { + t.Run("case=pass without hooks", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + + res, _ := makeRequestPost(t, newServer(t), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), uiURL) + }) + + t.Run("case=pass if hooks pass", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + + viperSetPost(strategy, []configuration.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) + res, _ := makeRequestPost(t, newServer(t), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), uiURL) + }) + + t.Run("case=fail if hooks fail", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + + viperSetPost(strategy, []configuration.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecuteSettingsPrePersistHook": "abort"}`)}}) + res, body := makeRequestPost(t, newServer(t), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "", body) + }) + + t.Run("case=prevent return_to value because domain not whitelisted", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + + res, _ := makeRequestPost(t, newServer(t), false, url.Values{"return_to": {"https://www.ory.sh/kratos/"}}) + assert.EqualValues(t, http.StatusInternalServerError, res.StatusCode) + }) + + t.Run("case=use return_to value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + viper.Set(configuration.ViperKeyURLsWhitelistedReturnToDomains, []string{"https://www.ory.sh/"}) + testhelpers.SelfServiceHookSettingsSetDefaultRedirectTo("https://www.ory.sh") + + res, _ := makeRequestPost(t, newServer(t), false, url.Values{"return_to": {"https://www.ory.sh/kratos/"}}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/kratos/", res.Request.URL.String()) + }) + + t.Run("case=use nested config value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + testhelpers.SelfServiceHookSettingsSetDefaultRedirectTo("https://www.ory.sh/kratos") + + res, _ := makeRequestPost(t, newServer(t), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/kratos/", res.Request.URL.String()) + }) + + t.Run("case=use nested config value", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + testhelpers.SelfServiceHookSettingsSetDefaultRedirectTo("https://www.ory.sh/not-kratos") + testhelpers.SelfServiceHookSettingsSetDefaultRedirectToStrategy(strategy, "https://www.ory.sh/kratos") + + res, _ := makeRequestPost(t, newServer(t), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/kratos/", res.Request.URL.String()) + }) + + t.Run("case=pass if hooks pass", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + viperSetPost(strategy, []configuration.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) + res, _ := makeRequestPost(t, newServer(t), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), uiURL) + }) + + t.Run("case=send a json response for API clients", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset) + viperSetPost(strategy, nil) + res, body := makeRequestPost(t, newServer(t), true, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.NotEmpty(t, gjson.Get(body, "identity.id")) + }) }) - } - }) + }) + } } diff --git a/selfservice/flow/settings/persistence.go b/selfservice/flow/settings/persistence.go index 077f128b77df..d82b1c39bb9d 100644 --- a/selfservice/flow/settings/persistence.go +++ b/selfservice/flow/settings/persistence.go @@ -76,10 +76,10 @@ func TestRequestPersister(p interface { actual, err := p.GetSettingsRequest(context.Background(), expected.ID) require.NoError(t, err) - factual, _ := json.Marshal(actual.Methods[StrategyTraitsID].Config) - fexpected, _ := json.Marshal(expected.Methods[StrategyTraitsID].Config) + factual, _ := json.Marshal(actual.Methods[StrategyProfile].Config) + fexpected, _ := json.Marshal(expected.Methods[StrategyProfile].Config) - require.NotEmpty(t, actual.Methods[StrategyTraitsID].Config.RequestMethodConfigurator.(*form.HTMLForm).Action) + require.NotEmpty(t, actual.Methods[StrategyProfile].Config.RequestMethodConfigurator.(*form.HTMLForm).Action) assert.EqualValues(t, expected.ID, actual.ID) assert.JSONEq(t, string(fexpected), string(factual)) x.AssertEqualTime(t, expected.IssuedAt, actual.IssuedAt) @@ -104,14 +104,14 @@ func TestRequestPersister(p interface { err := p.CreateSettingsRequest(context.Background(), expected) require.NoError(t, err) - expected.Methods[StrategyTraitsID].Config.RequestMethodConfigurator.(*form.HTMLForm).Action = "/new-action" + expected.Methods[StrategyProfile].Config.RequestMethodConfigurator.(*form.HTMLForm).Action = "/new-action" expected.RequestURL = "/new-request-url" require.NoError(t, p.UpdateSettingsRequest(context.Background(), expected)) actual, err := p.GetSettingsRequest(context.Background(), expected.ID) require.NoError(t, err) - assert.Equal(t, "/new-action", actual.Methods[StrategyTraitsID].Config.RequestMethodConfigurator.(*form.HTMLForm).Action) + assert.Equal(t, "/new-action", actual.Methods[StrategyProfile].Config.RequestMethodConfigurator.(*form.HTMLForm).Action) assert.Equal(t, "/new-request-url", actual.RequestURL) }) } diff --git a/selfservice/flow/settings/request.go b/selfservice/flow/settings/request.go index e1df147204a1..b6fbaf68b087 100644 --- a/selfservice/flow/settings/request.go +++ b/selfservice/flow/settings/request.go @@ -11,7 +11,6 @@ import ( "github.com/ory/x/sqlxx" "github.com/ory/herodot" - "github.com/ory/x/urlx" "github.com/ory/kratos/identity" "github.com/ory/kratos/session" @@ -86,21 +85,11 @@ type Request struct { } func NewRequest(exp time.Duration, r *http.Request, s *session.Session) *Request { - source := urlx.Copy(r.URL) - source.Host = r.Host - - if len(source.Scheme) == 0 { - source.Scheme = "http" - if r.TLS != nil { - source.Scheme = "https" - } - } - return &Request{ ID: x.NewUUID(), ExpiresAt: time.Now().UTC().Add(exp), IssuedAt: time.Now().UTC(), - RequestURL: source.String(), + RequestURL: x.RequestURL(r).String(), IdentityID: s.Identity.ID, Identity: s.Identity, Methods: map[string]*RequestMethod{}, diff --git a/selfservice/flow/settings/strategy_profile.go b/selfservice/flow/settings/strategy_profile.go index acec038cf32d..da6d1b3b62d6 100644 --- a/selfservice/flow/settings/strategy_profile.go +++ b/selfservice/flow/settings/strategy_profile.go @@ -14,7 +14,6 @@ import ( "github.com/ory/herodot" "github.com/ory/jsonschema/v3" "github.com/ory/x/decoderx" - "github.com/ory/x/errorsx" "github.com/ory/x/urlx" "github.com/ory/kratos/continuity" @@ -28,7 +27,7 @@ import ( ) const ( - StrategyTraitsID = "profile" + StrategyProfile = "profile" PublicSettingsProfilePath = "/self-service/browser/flows/settings/strategies/profile" strategyProfileContinuityName = "settings_profile" ) @@ -77,7 +76,7 @@ func NewStrategyTraits(d strategyDependencies, c configuration.Provider) *Strate } func (s *StrategyTraits) SettingsStrategyID() string { - return StrategyTraitsID + return StrategyProfile } func (s *StrategyTraits) RegisterSettingsRoutes(public *x.RouterPublic) { @@ -202,33 +201,24 @@ func (s *StrategyTraits) continueFlow(w http.ResponseWriter, r *http.Request, ss } if err := s.hydrateForm(r, ar, ss, p.Traits); err != nil { - s.d.SettingsRequestErrorHandler().HandleSettingsError(w, r, ar, err, StrategyTraitsID) + s.d.SettingsRequestErrorHandler().HandleSettingsError(w, r, ar, err, StrategyProfile) return } update, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), ss.Identity.ID) if err != nil { - s.d.SettingsRequestErrorHandler().HandleSettingsError(w, r, ar, err, StrategyTraitsID) + s.d.SettingsRequestErrorHandler().HandleSettingsError(w, r, ar, err, StrategyProfile) return } update.Traits = identity.Traits(p.Traits) - if err := s.d.SettingsExecutor().PostSettingsHook(w, r, - s.d.PostSettingsHooks(StrategyTraitsID), + if err := s.d.SettingsHookExecutor().PostSettingsHook(w, r, + StrategyProfile, ar, ss, update, - ); errorsx.Cause(err) == ErrHookAbortRequest { - return - } else if err != nil { + ); err != nil { s.handleSettingsError(w, r, ar, ss, p.Traits, p, err) return } - - if len(w.Header().Get("Location")) == 0 { - http.Redirect(w, r, - urlx.CopyWithQuery(s.c.SettingsURL(), url.Values{"request": {ar.ID.String()}}).String(), - http.StatusFound, - ) - } } // Complete profile update payload @@ -270,20 +260,20 @@ func (s *StrategyTraits) hydrateForm(r *http.Request, ar *Request, ss *session.S url.Values{"request": {ar.ID.String()}}, ) - ar.Methods[StrategyTraitsID].Config.Reset() + ar.Methods[StrategyProfile].Config.Reset() if traits != nil { for _, field := range form.NewHTMLFormFromJSON(action.String(), traits, "traits").Fields { - ar.Methods[StrategyTraitsID].Config.SetField(field) + ar.Methods[StrategyProfile].Config.SetField(field) } } - ar.Methods[StrategyTraitsID].Config.SetCSRF(s.d.GenerateCSRFToken(r)) + ar.Methods[StrategyProfile].Config.SetCSRF(s.d.GenerateCSRFToken(r)) traitsSchema, err := s.c.IdentityTraitsSchemas().FindSchemaByID(ss.Identity.TraitsSchemaID) if err != nil { return err } - if err = ar.Methods[StrategyTraitsID].Config.SortFields(traitsSchema.URL, "traits"); err != nil { + if err = ar.Methods[StrategyProfile].Config.SortFields(traitsSchema.URL, "traits"); err != nil { return err } diff --git a/selfservice/flow/settings/strategy_profile_test.go b/selfservice/flow/settings/strategy_profile_test.go index 51e35b291038..1793748a608f 100644 --- a/selfservice/flow/settings/strategy_profile_test.go +++ b/selfservice/flow/settings/strategy_profile_test.go @@ -34,8 +34,10 @@ import ( ) func TestStrategyTraits(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/identity.schema.json") + viper.Set(configuration.ViperKeyURLsDefaultReturnTo, "https://www.ory.sh/") + viper.Set(configuration.ViperKeyURLsDefaultReturnTo, "https://www.ory.sh/") ui := testhelpers.NewSettingsUITestServer(t) viper.Set(configuration.ViperKeySelfServicePrivilegedAuthenticationAfter, "1ns") @@ -82,7 +84,7 @@ func TestStrategyTraits(t *testing.T) { t.Run("daemon=public", func(t *testing.T) { t.Run("description=should fail to post data if CSRF is missing", func(t *testing.T) { - f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyTraitsID) + f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyProfile) res, err := primaryUser.PostForm(pointerx.StringR(f.Action), url.Values{}) require.NoError(t, err) assert.EqualValues(t, 400, res.StatusCode, "should return a 400 error because CSRF token is not set") @@ -112,8 +114,8 @@ func TestStrategyTraits(t *testing.T) { found := false - require.NotNil(t, pr.Payload.Methods[settings.StrategyTraitsID].Config) - f := pr.Payload.Methods[settings.StrategyTraitsID].Config + require.NotNil(t, pr.Payload.Methods[settings.StrategyProfile].Config) + f := pr.Payload.Methods[settings.StrategyProfile].Config for i := range f.Fields { if pointerx.StringR(f.Fields[i].Name) == form.CSRFTokenName { @@ -140,7 +142,7 @@ func TestStrategyTraits(t *testing.T) { }) t.Run("description=should come back with form errors if some profile data is invalid", func(t *testing.T) { - config := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyTraitsID) + config := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyProfile) values := testhelpers.SDKFormFieldsToURLValues(config.Fields) values.Set("traits.should_long_string", "too-short") @@ -160,7 +162,7 @@ func TestStrategyTraits(t *testing.T) { viper.Set(configuration.ViperKeySelfServicePrivilegedAuthenticationAfter, "1ns") }) - config := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyTraitsID) + config := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyProfile) newEmail := "not-john-doe@mail.com" values := testhelpers.SDKFormFieldsToURLValues(config.Fields) values.Set("traits.email", newEmail) @@ -184,7 +186,7 @@ func TestStrategyTraits(t *testing.T) { viper.Set(configuration.ViperKeyURLsLogin, loginTS.URL+"/login") var run = func(t *testing.T) { - f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyTraitsID) + f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyProfile) values := testhelpers.SDKFormFieldsToURLValues(f.Fields) values.Set("traits.email", "not-john-doe@foo.bar") @@ -200,17 +202,15 @@ func TestStrategyTraits(t *testing.T) { t.Run("case=should fail without hooks", run) t.Run("case=should fail with hooks", func(t *testing.T) { - testhelpers.SetSettingsStrategyAfterHooks(t, settings.StrategyTraitsID, publicTS.URL+"/return-ts") - t.Cleanup(func() { - viper.Set(configuration.ViperKeySelfServiceSettingsAfterConfig+"."+settings.StrategyTraitsID, nil) - }) + testhelpers.SelfServiceHookSettingsSetDefaultRedirectTo(publicTS.URL + "/return-ts") + t.Cleanup(testhelpers.SelfServiceHookConfigReset) run(t) }) }) t.Run("description=should retry with invalid payloads multiple times before succeeding", func(t *testing.T) { t.Run("flow=fail first update", func(t *testing.T) { - f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyTraitsID) + f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyProfile) values := testhelpers.SDKFormFieldsToURLValues(f.Fields) values.Set("traits.should_big_number", "1") @@ -224,7 +224,7 @@ func TestStrategyTraits(t *testing.T) { }) t.Run("flow=fail second update", func(t *testing.T) { - f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyTraitsID) + f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyProfile) values := testhelpers.SDKFormFieldsToURLValues(f.Fields) values.Del("traits.should_big_number") @@ -246,7 +246,7 @@ func TestStrategyTraits(t *testing.T) { }) t.Run("flow=succeed with final request", func(t *testing.T) { - f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyTraitsID) + f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyProfile) values := testhelpers.SDKFormFieldsToURLValues(f.Fields) // set email to the one that is in the db as it should not be modified @@ -269,7 +269,7 @@ func TestStrategyTraits(t *testing.T) { }) t.Run("flow=try another update with invalid data", func(t *testing.T) { - f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyTraitsID) + f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyProfile) values := testhelpers.SDKFormFieldsToURLValues(f.Fields) values.Set("traits.should_long_string", "short") @@ -286,11 +286,12 @@ func TestStrategyTraits(t *testing.T) { returned = true }) rts := httptest.NewServer(router) - defer rts.Close() + t.Cleanup(rts.Close) - viper.Set(configuration.ViperKeySelfServiceSettingsAfterConfig+"."+settings.StrategyTraitsID, testhelpers.HookConfigRedirectTo(t, rts.URL+"/return-ts")) + testhelpers.SelfServiceHookSettingsSetDefaultRedirectTo(rts.URL + "/return-ts") + t.Cleanup(testhelpers.SelfServiceHookConfigReset) - f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyTraitsID) + f := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyProfile) values := testhelpers.SDKFormFieldsToURLValues(f.Fields) values.Set("traits.should_big_number", "9001") @@ -306,15 +307,16 @@ func TestStrategyTraits(t *testing.T) { t.Run("description=should send email with verifiable address", func(t *testing.T) { _ = testhelpers.NewSettingsLoginAcceptAPIServer(t, adminClient) - viper.Set(configuration.ViperKeySelfServiceSettingsAfterConfig+"."+settings.StrategyTraitsID, testhelpers.HookVerify(t)) + viper.Set(configuration.HookStrategyKey(configuration.ViperKeySelfServiceSettingsAfter, settings.StrategyProfile), []configuration.SelfServiceHook{{Name: "verify"}}) viper.Set(configuration.ViperKeySelfServicePrivilegedAuthenticationAfter, "1h") viper.Set(configuration.ViperKeyCourierSMTPURL, "smtp://foo:bar@irrelevant.com/") + t.Cleanup(func() { viper.Set(configuration.ViperKeySelfServicePrivilegedAuthenticationAfter, "1ns") - viper.Set(configuration.ViperKeySelfServiceSettingsAfterConfig+"."+settings.StrategyTraitsID, nil) + viper.Set(configuration.HookStrategyKey(configuration.ViperKeySelfServiceSettingsAfter, settings.StrategyProfile), nil) }) - config := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyTraitsID) + config := testhelpers.GetSettingsMethodConfig(t, primaryUser, publicTS, settings.StrategyProfile) newEmail := "update-verify@mail.com" values := testhelpers.SDKFormFieldsToURLValues(config.Fields) values.Set("traits.email", newEmail) diff --git a/selfservice/flow/verify/handler_test.go b/selfservice/flow/verify/handler_test.go index 8cd6aacf7a18..5b9541ceb23f 100644 --- a/selfservice/flow/verify/handler_test.go +++ b/selfservice/flow/verify/handler_test.go @@ -36,7 +36,7 @@ func init() { } func TestHandler(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) publicTS, adminTS := func() (*httptest.Server, *httptest.Server) { public := x.NewRouterPublic() diff --git a/selfservice/flow/verify/sender_test.go b/selfservice/flow/verify/sender_test.go index a8000d1e9480..e19bdf002a53 100644 --- a/selfservice/flow/verify/sender_test.go +++ b/selfservice/flow/verify/sender_test.go @@ -17,7 +17,7 @@ import ( ) func TestManager(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/extension/schema.json") viper.Set(configuration.ViperKeyURLsSelfPublic, "https://www.ory.sh/") viper.Set(configuration.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") diff --git a/selfservice/hook/error.go b/selfservice/hook/error.go new file mode 100644 index 000000000000..6c0f23e821c8 --- /dev/null +++ b/selfservice/hook/error.go @@ -0,0 +1,69 @@ +package hook + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/tidwall/gjson" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/session" +) + +var ( + _ registration.PostHookPrePersistExecutor = new(Error) + _ registration.PostHookPostPersistExecutor = new(Error) + _ registration.PreHookExecutor = new(Error) + + _ login.PreHookExecutor = new(Error) + _ login.PostHookExecutor = new(Error) + + _ settings.PostHookPostPersistExecutor = new(Error) + _ settings.PostHookPrePersistExecutor = new(Error) +) + +type Error struct { + Config json.RawMessage +} + +func (e Error) err(path string, abort error) error { + switch gjson.GetBytes(e.Config, path).String() { + case "err": + return errors.New("err") + case "abort": + return abort + } + return nil +} + +func (e Error) ExecuteSettingsPrePersistHook(w http.ResponseWriter, r *http.Request, a *settings.Request, s *identity.Identity) error { + return e.err("ExecuteSettingsPrePersistHook", settings.ErrHookAbortRequest) +} + +func (e Error) ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http.Request, a *settings.Request, s *identity.Identity) error { + return e.err("ExecuteSettingsPostPersistHook", settings.ErrHookAbortRequest) +} + +func (e Error) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, a *login.Request, s *session.Session) error { + return e.err("ExecuteLoginPostHook", login.ErrHookAbortRequest) +} + +func (e Error) ExecuteLoginPreHook(w http.ResponseWriter, r *http.Request, a *login.Request) error { + return e.err("ExecuteLoginPreHook", login.ErrHookAbortRequest) +} + +func (e Error) ExecuteRegistrationPreHook(w http.ResponseWriter, r *http.Request, a *registration.Request) error { + return e.err("ExecuteRegistrationPreHook", registration.ErrHookAbortRequest) +} + +func (e Error) ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *registration.Request, s *session.Session) error { + return e.err("ExecutePostRegistrationPostPersistHook", registration.ErrHookAbortRequest) +} + +func (e Error) ExecutePostRegistrationPrePersistHook(w http.ResponseWriter, r *http.Request, a *registration.Request, i *identity.Identity) error { + return e.err("ExecutePostRegistrationPrePersistHook", registration.ErrHookAbortRequest) +} diff --git a/selfservice/hook/redirector.go b/selfservice/hook/redirector.go index c3029a9fedbf..a9f5ac4f9d7d 100644 --- a/selfservice/hook/redirector.go +++ b/selfservice/hook/redirector.go @@ -1,74 +1,85 @@ package hook import ( + "encoding/json" "net/http" - "net/url" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" "github.com/ory/herodot" - "github.com/ory/x/urlx" + "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/session" - "github.com/ory/kratos/x" ) var ( - _ login.PostHookExecutor = new(Redirector) - _ registration.PostHookExecutor = new(Redirector) + _ login.PostHookExecutor = new(Redirector) + _ registration.PostHookPrePersistExecutor = new(Redirector) + _ registration.PreHookExecutor = new(Redirector) + _ login.PreHookExecutor = new(Redirector) + _ settings.PostHookPostPersistExecutor = new(Redirector) ) +func NewRedirector(config json.RawMessage) *Redirector { + return &Redirector{config: config} +} + type Redirector struct { - returnTo func() *url.URL - whitelist func() []url.URL - allowUserDefined func() bool - publicURL func() *url.URL + config json.RawMessage } -func NewRedirector( - returnTo func() *url.URL, - whitelist func() []url.URL, - allowUserDefined func() bool, - publicURL func() *url.URL, -) *Redirector { - return &Redirector{ - returnTo: returnTo, - whitelist: whitelist, - allowUserDefined: allowUserDefined, - publicURL: publicURL, +func (e *Redirector) ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http.Request, _ *settings.Request, _ *identity.Identity) error { + if err := e.do(w, r); err != nil { + return err } + return errors.WithStack(settings.ErrHookAbortRequest) } -func (e *Redirector) ExecuteRegistrationPostHook(w http.ResponseWriter, r *http.Request, sr *registration.Request, _ *session.Session) error { - return e.do(w, r, sr.RequestURL) +func (e *Redirector) ExecuteLoginPreHook(w http.ResponseWriter, r *http.Request, _ *login.Request) error { + if err := e.do(w, r); err != nil { + return err + } + return errors.WithStack(login.ErrHookAbortRequest) } -func (e *Redirector) ExecuteSettingsPostHook(w http.ResponseWriter, r *http.Request, pr *settings.Request, _ *session.Session) error { - return e.do(w, r, pr.RequestURL) +func (e *Redirector) ExecuteRegistrationPreHook(w http.ResponseWriter, r *http.Request, _ *registration.Request) error { + if err := e.do(w, r); err != nil { + return err + } + return errors.WithStack(registration.ErrHookAbortRequest) } -func (e *Redirector) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, sr *login.Request, _ *session.Session) error { - return e.do(w, r, sr.RequestURL) +func (e *Redirector) ExecutePostRegistrationPrePersistHook(w http.ResponseWriter, r *http.Request, _ *registration.Request, _ *identity.Identity) error { + if err := e.do(w, r); err != nil { + return err + } + return errors.WithStack(registration.ErrHookAbortRequest) } -func (e *Redirector) do(w http.ResponseWriter, r *http.Request, originalURL string) error { - ou, err := url.ParseRequestURI(originalURL) - if err != nil { - return herodot.ErrInternalServerError.WithReasonf("The redirect hook was unable to parse the original request URL: %s", err) +func (e *Redirector) ExecuteSettingsPrePersistHook(w http.ResponseWriter, r *http.Request, _ *settings.Request, _ *identity.Identity) error { + if err := e.do(w, r); err != nil { + return err } + return errors.WithStack(settings.ErrHookAbortRequest) +} - var returnTo string - if e.allowUserDefined() { - returnTo, err = x.DetermineReturnToURL(ou, e.returnTo(), e.whitelist()) - } else { - returnTo, err = x.DetermineReturnToURL(ou, e.returnTo(), []url.URL{*urlx.AppendPaths(e.publicURL(), "self-service")}) +func (e *Redirector) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, _ *login.Request, _ *session.Session) error { + if err := e.do(w, r); err != nil { + return err } + return errors.WithStack(login.ErrHookAbortRequest) +} - if err != nil { - return err +func (e *Redirector) do(w http.ResponseWriter, r *http.Request) error { + rt := gjson.GetBytes(e.config, "to").String() + if rt == "" { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("A redirector hook was configured without a redirect_to value set.")) } - http.Redirect(w, r, returnTo, http.StatusFound) + http.Redirect(w, r, rt, http.StatusFound) return nil } diff --git a/selfservice/hook/redirector_test.go b/selfservice/hook/redirector_test.go index 067b507f4280..c3e3dfb010ce 100644 --- a/selfservice/hook/redirector_test.go +++ b/selfservice/hook/redirector_test.go @@ -1,87 +1,59 @@ package hook import ( - "fmt" + "encoding/json" "net/http" - "net/http/httptest" - "net/url" "testing" + "github.com/gobuffalo/httptest" + "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/x/errorsx" - - "github.com/ory/viper" - - "github.com/ory/herodot" - "github.com/ory/x/urlx" - - "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flow/settings" ) func TestRedirector(t *testing.T) { - r := http.Request{ - Header: http.Header{}, - URL: urlx.ParseOrPanic("https://www.ory.sh"), - } - - type testCase struct { - requrl string - e string - expectErr string - allowUser bool - } - - viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/stub.schema.json") - - var assert = func(t *testing.T, tc testCase, w *httptest.ResponseRecorder, err error) { - if tc.expectErr != "" { - require.Error(t, err) - assert.Contains(t, errorsx.Cause(err).(*herodot.DefaultError).Reason(), tc.expectErr) - return - } - require.NoError(t, err) - assert.Equal(t, w.Code, http.StatusFound) - assert.Equal(t, w.Header().Get("Location"), tc.e) - } - - for k, tc := range []testCase{ - {allowUser: true, requrl: "https://www.ory.sh/?return_to=/foo", e: "https://www.ory.sh/foo"}, - {allowUser: true, requrl: "https://login.ory.sh/?return_to=https://not-allowed/foo", e: "https://www.ory.sh/foo", expectErr: "not a whitelisted return domain"}, - {allowUser: true, requrl: "https://login.ory.sh/?return_to=https://apis.ory.sh/foo", e: "https://apis.ory.sh/foo"}, - {requrl: "https://www.ory.sh/", e: "https://www.ory.sh/fallback"}, - {requrl: "https://login.ory.sh/?return_to=https://kratos.ory.sh/public/self-service/foo", e: "https://kratos.ory.sh/public/self-service/foo"}, - {expectErr: "not a whitelisted return domain", requrl: "https://login.ory.sh/?return_to=https://not-kratos.ory.sh/public/self-service/foo", e: "https://www.ory.sh/foo"}, - } { - h := NewRedirector( - func() *url.URL { - return urlx.ParseOrPanic("https://www.ory.sh/fallback") - }, - func() []url.URL { - return []url.URL{ - *urlx.ParseOrPanic("https://www.ory.sh"), - *urlx.ParseOrPanic("https://apis.ory.sh"), - } - }, - func() bool { - return tc.allowUser - }, - func() *url.URL { - return urlx.ParseOrPanic("https://kratos.ory.sh/public") - }, - ) - - t.Run(fmt.Sprintf("method=register/case=%d", k), func(t *testing.T) { - w := httptest.NewRecorder() - assert(t, tc, w, h.ExecuteRegistrationPostHook(w, &r, ®istration.Request{RequestURL: tc.requrl}, nil)) - }) - - t.Run(fmt.Sprintf("method=Login/case=%d", k), func(t *testing.T) { - w := httptest.NewRecorder() - assert(t, tc, w, h.ExecuteLoginPostHook(w, &r, &login.Request{RequestURL: tc.requrl}, nil)) + l := NewRedirector(json.RawMessage("")) + assert.Error(t, l.ExecuteSettingsPrePersistHook(nil, nil, nil, nil)) + assert.Error(t, l.ExecuteLoginPostHook(nil, nil, nil, nil)) + assert.Error(t, l.ExecutePostRegistrationPrePersistHook(nil, nil, nil, nil)) + assert.Error(t, l.ExecuteSettingsPostPersistHook(nil, nil, nil, nil)) + assert.Error(t, l.ExecuteLoginPreHook(nil, nil, nil)) + assert.Error(t, l.ExecuteRegistrationPreHook(nil, nil, nil)) + + l = NewRedirector(json.RawMessage(`{"to":"https://www.ory.sh/"}`)) + router := httprouter.New() + router.GET("/a", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.Error(t, l.ExecuteSettingsPrePersistHook(w, r, nil, nil), settings.ErrHookAbortRequest) + }) + router.GET("/b", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.Error(t, l.ExecuteLoginPostHook(w, r, nil, nil), login.ErrHookAbortRequest) + }) + router.GET("/c", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.Error(t, l.ExecutePostRegistrationPrePersistHook(w, r, nil, nil), registration.ErrHookAbortRequest) + }) + router.GET("/d", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.Error(t, l.ExecuteSettingsPostPersistHook(w, r, nil, nil), settings.ErrHookAbortRequest) + }) + router.GET("/e", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.Error(t, l.ExecutePostRegistrationPrePersistHook(w, r, nil, nil), registration.ErrHookAbortRequest) + }) + router.GET("/f", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.Error(t, l.ExecuteLoginPreHook(w, r, nil), login.ErrHookAbortRequest) + }) + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + for _, p := range []string{"a", "b", "c", "d", "e", "f"} { + t.Run("route="+p, func(t *testing.T) { + res, err := http.Get(ts.URL + "/" + p) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "https://www.ory.sh/", res.Request.URL.String()) }) } } diff --git a/selfservice/hook/session_destroyer_test.go b/selfservice/hook/session_destroyer_test.go index 1daedea88733..30193c674a20 100644 --- a/selfservice/hook/session_destroyer_test.go +++ b/selfservice/hook/session_destroyer_test.go @@ -27,7 +27,7 @@ func init() { } func TestSessionDestroyer(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) viper.Set(configuration.ViperKeyURLsSelfPublic, "http://localhost/") viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/stub.schema.json") diff --git a/selfservice/hook/session_issuer.go b/selfservice/hook/session_issuer.go index b71eb0a305dd..957c2d859e80 100644 --- a/selfservice/hook/session_issuer.go +++ b/selfservice/hook/session_issuer.go @@ -10,8 +10,8 @@ import ( ) var ( - _ login.PostHookExecutor = new(SessionIssuer) - _ registration.PostHookExecutor = new(SessionIssuer) + _ login.PostHookExecutor = new(SessionIssuer) + _ registration.PostHookPostPersistExecutor = new(SessionIssuer) ) type ( @@ -19,6 +19,9 @@ type ( session.ManagementProvider session.PersistenceProvider } + SessionIssuerProvider interface { + HookSessionIssuer() *SessionIssuer + } SessionIssuer struct { r sessionIssuerDependencies } @@ -28,12 +31,12 @@ func NewSessionIssuer(r sessionIssuerDependencies) *SessionIssuer { return &SessionIssuer{r: r} } -func (e *SessionIssuer) ExecuteRegistrationPostHook(w http.ResponseWriter, r *http.Request, a *registration.Request, s *session.Session) error { +func (e *SessionIssuer) ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *registration.Request, s *session.Session) error { s.AuthenticatedAt = time.Now().UTC() if err := e.r.SessionPersister().CreateSession(r.Context(), s); err != nil { return err } - return e.r.SessionManager().SaveToRequest(r.Context(), s, w, r) + return e.r.SessionManager().SaveToRequest(r.Context(), w, r, s) } func (e *SessionIssuer) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, a *login.Request, s *session.Session) error { @@ -41,5 +44,5 @@ func (e *SessionIssuer) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Requ if err := e.r.SessionPersister().CreateSession(r.Context(), s); err != nil { return err } - return e.r.SessionManager().SaveToRequest(r.Context(), s, w, r) + return e.r.SessionManager().SaveToRequest(r.Context(), w, r, s) } diff --git a/selfservice/hook/session_issuer_test.go b/selfservice/hook/session_issuer_test.go index 11a72cba643a..9f3944349d51 100644 --- a/selfservice/hook/session_issuer_test.go +++ b/selfservice/hook/session_issuer_test.go @@ -21,7 +21,7 @@ import ( ) func TestSessionIssuer(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) viper.Set(configuration.ViperKeyURLsSelfPublic, "http://localhost/") viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/stub.schema.json") @@ -48,7 +48,7 @@ func TestSessionIssuer(t *testing.T) { i := identity.NewIdentity(configuration.DefaultIdentityTraitsSchemaID) require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) - require.NoError(t, h.ExecuteRegistrationPostHook(w, &r, nil, &session.Session{ID: sid, Identity: i})) + require.NoError(t, h.ExecutePostRegistrationPostPersistHook(w, &r, nil, &session.Session{ID: sid, Identity: i})) got, err := reg.SessionPersister().GetSession(context.Background(), sid) require.NoError(t, err) diff --git a/selfservice/hook/verify.go b/selfservice/hook/verify.go index 5818d1a9ab3d..6e3fdfd9c118 100644 --- a/selfservice/hook/verify.go +++ b/selfservice/hook/verify.go @@ -3,13 +3,15 @@ package hook import ( "net/http" + "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/flow/verify" "github.com/ory/kratos/session" ) -var _ registration.PostHookExecutor = new(Verifier) +var _ registration.PostHookPostPersistExecutor = new(Verifier) +var _ settings.PostHookPostPersistExecutor = new(Verifier) type ( verifierDependencies interface { @@ -24,24 +26,24 @@ func NewVerifier(r verifierDependencies) *Verifier { return &Verifier{r: r} } -func (e *Verifier) ExecuteRegistrationPostHook(_ http.ResponseWriter, r *http.Request, _ *registration.Request, s *session.Session) error { - return e.do(r, s) +func (e *Verifier) ExecutePostRegistrationPostPersistHook(_ http.ResponseWriter, r *http.Request, _ *registration.Request, s *session.Session) error { + return e.do(r, s.Identity) } -func (e *Verifier) ExecuteSettingsPostHook(_ http.ResponseWriter, r *http.Request, _ *settings.Request, s *session.Session) error { - return e.do(r, s) +func (e *Verifier) ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http.Request, a *settings.Request, i *identity.Identity) error { + return e.do(r, i) } -func (e *Verifier) do(r *http.Request, s *session.Session) error { +func (e *Verifier) do(r *http.Request, i *identity.Identity) error { // Ths is called after the identity has been created so we can safely assume that all addresses are available // already. - for k, address := range s.Identity.Addresses { + for k, address := range i.Addresses { sent, err := e.r.VerificationSender().SendCode(r.Context(), address.Via, address.Value) if err != nil { return err } - s.Identity.Addresses[k] = *sent + i.Addresses[k] = *sent } return nil diff --git a/selfservice/hook/verify_test.go b/selfservice/hook/verify_test.go index 4e6a8071a41d..ecc8125bcce7 100644 --- a/selfservice/hook/verify_test.go +++ b/selfservice/hook/verify_test.go @@ -22,16 +22,16 @@ import ( func TestVerifier(t *testing.T) { for k, hf := range map[string]func(*hook.Verifier, *identity.Identity) error{ "settings": func(h *hook.Verifier, i *identity.Identity) error { - return h.ExecuteSettingsPostHook( - httptest.NewRecorder(), new(http.Request), nil, &session.Session{ID: x.NewUUID(), Identity: i}) + return h.ExecuteSettingsPostPersistHook( + httptest.NewRecorder(), new(http.Request), nil, i) }, "register": func(h *hook.Verifier, i *identity.Identity) error { - return h.ExecuteRegistrationPostHook( + return h.ExecutePostRegistrationPostPersistHook( httptest.NewRecorder(), new(http.Request), nil, &session.Session{ID: x.NewUUID(), Identity: i}) }, } { t.Run("name="+k, func(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/verify.schema.json") viper.Set(configuration.ViperKeyURLsSelfPublic, "https://www.ory.sh/") viper.Set(configuration.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") diff --git a/selfservice/strategy/oidc/provider_config_test.go b/selfservice/strategy/oidc/provider_config_test.go index 0fd687dba748..d2c643308920 100644 --- a/selfservice/strategy/oidc/provider_config_test.go +++ b/selfservice/strategy/oidc/provider_config_test.go @@ -16,7 +16,7 @@ import ( ) func TestConfig(t *testing.T) { - conf, reg := internal.NewRegistryDefault(t) + conf, reg := internal.NewFastRegistryWithMocks(t) viper.Set(configuration.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC), json.RawMessage(`{"config":{"providers": [{"provider": "generic"}]}}`)) s := oidc.NewStrategy(reg, conf) diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index c52d886185ba..6e4953b88807 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -347,7 +347,7 @@ func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, a *login for _, c := range o { if c.Subject == claims.Subject && c.Provider == provider.Config().ID { - if err = s.d.LoginHookExecutor().PostLoginHook(w, r, s.d.PostLoginHooks(identity.CredentialsTypeOIDC), a, i); err != nil { + if err = s.d.LoginHookExecutor().PostLoginHook(w, r, identity.CredentialsTypeOIDC, a, i); err != nil { s.handleError(w, r, a.GetID(), nil, err) return } @@ -450,7 +450,7 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, a Config: b.Bytes(), }) - if err := s.d.RegistrationExecutor().PostRegistrationHook(w, r, s.d.PostRegistrationHooks(identity.CredentialsTypeOIDC), a, i); err != nil { + if err := s.d.RegistrationExecutor().PostRegistrationHook(w, r, identity.CredentialsTypeOIDC, a, i); err != nil { s.handleError(w, r, a.GetID(), traits, err) return } diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 3027e1f0a49d..28370ae6bb11 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -1,9 +1,7 @@ package oidc_test import ( - "bytes" "context" - "encoding/json" "fmt" "io/ioutil" "net/http" @@ -38,34 +36,6 @@ import ( "github.com/ory/kratos/x" ) -func hookConfig(u string) (m []map[string]interface{}) { - var b bytes.Buffer - if _, err := fmt.Fprintf(&b, `[ - { - "job": "session" - }, - { - "job": "redirect", - "config": { - "default_redirect_url": "%s", - "allow_user_defined_redirect": true - } - } -]`, u); err != nil { - panic(err) - } - - if err := json.NewDecoder(&b).Decode(&m); err != nil { - panic(err) - } - - return m -} - -type withTokenGenerator interface { - WithTokenGenerator(g form.CSRFGenerator) -} - const debugRedirects = false func TestStrategy(t *testing.T) { @@ -115,7 +85,7 @@ func TestStrategy(t *testing.T) { remoteAdmin = "http://127.0.0.1:" + hydra.GetPort("4445/tcp") } - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) public := x.NewRouterPublic() admin := x.NewRouterAdmin() @@ -179,9 +149,10 @@ func TestStrategy(t *testing.T) { viper.Set(configuration.ViperKeyURLsLogin, uiTS.URL+"/login") viper.Set(configuration.ViperKeyURLsRegistration, uiTS.URL+"/registration") viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/registration.schema.json") - viper.Set(configuration.ViperKeySelfServiceRegistrationAfterConfig+"."+string(identity.CredentialsTypeOIDC), hookConfig(returnTS.URL)) - viper.Set(configuration.ViperKeySelfServiceLoginAfterConfig+"."+string(identity.CredentialsTypeOIDC), hookConfig(returnTS.URL)) viper.Set(configuration.ViperKeyURLsDefaultReturnTo, returnTS.URL) + viper.Set( + configuration.HookStrategyKey(configuration.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeOIDC.String()), + []configuration.SelfServiceHook{{Name: "session"}}) t.Logf("Kratos Public URL: %s", ts.URL) t.Logf("Kratos Error URL: %s", errTS.URL) @@ -487,6 +458,6 @@ func TestStrategy(t *testing.T) { authAt2, err := time.Parse(time.RFC3339, gjson.GetBytes(body2, "authenticated_at").String()) require.NoError(t, err) // authenticated at is newer in the second body - assert.Greater(t, authAt2.Sub(authAt1).Milliseconds(), int64(0), "%s - %s", authAt2, authAt1) + assert.Greater(t, authAt2.Sub(authAt1).Milliseconds(), int64(0), "%s - %s : %s - %s", authAt2, authAt1, body2, body1) }) } diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 78e2b75e51a1..5a5a013f597a 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -102,8 +102,7 @@ func (s *Strategy) handleLogin(w http.ResponseWriter, r *http.Request, _ httprou return } - if err := s.d.LoginHookExecutor().PostLoginHook(w, r, - s.d.PostLoginHooks(identity.CredentialsTypePassword), ar, i); err != nil { + if err := s.d.LoginHookExecutor().PostLoginHook(w, r, identity.CredentialsTypePassword, ar, i); err != nil { s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) return } @@ -116,7 +115,6 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, sr *login.Request) error var identifier string if !sr.IsForced() { - print("forced") // do nothing } else if sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r); err != nil { print("sm") diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go index ca5af653346f..a9fe2d0af17d 100644 --- a/selfservice/strategy/password/login_test.go +++ b/selfservice/strategy/password/login_test.go @@ -75,7 +75,7 @@ func nlr(exp time.Duration) *login.Request { } func TestLoginNew(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) router := x.NewRouterPublic() admin := x.NewRouterAdmin() @@ -95,7 +95,6 @@ func TestLoginNew(t *testing.T) { viper.Set(configuration.ViperKeyURLsError, errTs.URL+"/error-ts") viper.Set(configuration.ViperKeyURLsLogin, uiTs.URL+"/login-ts") viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL) - viper.Set(configuration.ViperKeySelfServiceLoginAfterConfig+"."+string(identity.CredentialsTypePassword), hookConfig(returnTs.URL+"/return-ts")) viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/login.schema.json") viper.Set(configuration.ViperKeySecretsSession, []string{"not-a-secure-session-key"}) viper.Set(configuration.ViperKeyURLsDefaultReturnTo, returnTs.URL+"/return-ts") diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go index 91d836fb9e1e..19e76d19e984 100644 --- a/selfservice/strategy/password/registration.go +++ b/selfservice/strategy/password/registration.go @@ -162,13 +162,7 @@ func (s *Strategy) handleRegistration(w http.ResponseWriter, r *http.Request, _ return } - if err := s.d.RegistrationExecutor().PostRegistrationHook(w, r, - s.d.PostRegistrationHooks(identity.CredentialsTypePassword), - ar, - i, - ); errorsx.Cause(err) == registration.ErrHookAbortRequest { - return - } else if err != nil { + if err := s.d.RegistrationExecutor().PostRegistrationHook(w, r, identity.CredentialsTypePassword, ar, i); err != nil { s.handleRegistrationError(w, r, ar, &p, err) return } diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go index 27f05c2e09a4..a51f44fe8881 100644 --- a/selfservice/strategy/password/registration_test.go +++ b/selfservice/strategy/password/registration_test.go @@ -56,7 +56,7 @@ func formMethodIsPOST(t *testing.T, body []byte) { func TestRegistration(t *testing.T) { t.Run("case=registration", func(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) router := x.NewRouterPublic() admin := x.NewRouterAdmin() @@ -78,7 +78,8 @@ func TestRegistration(t *testing.T) { viper.Set(configuration.ViperKeyURLsError, errTs.URL+"/error-ts") viper.Set(configuration.ViperKeyURLsRegistration, uiTs.URL+"/signup-ts") viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL) - viper.Set(configuration.ViperKeySelfServiceRegistrationAfterConfig+"."+string(identity.CredentialsTypePassword), hookConfig(returnTs.URL+"/return-ts")) + viper.Set(configuration.ViperKeyURLsDefaultReturnTo, returnTs.URL+"/default-return-to") + viper.Set(configuration.ViperKeySelfServiceRegistrationAfter+"."+configuration.ViperKeyDefaultReturnTo, returnTs.URL+"/return-ts") viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/registration.schema.json") var newRegistrationRequest = func(t *testing.T, exp time.Duration) *registration.Request { @@ -207,6 +208,10 @@ func TestRegistration(t *testing.T) { t.Run("case=should pass and set up a session", func(t *testing.T) { viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/registration.schema.json") + viper.Set( + configuration.HookStrategyKey(configuration.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), + []configuration.SelfServiceHook{{Name: "session"}}) + rr := newRegistrationRequest(t, time.Minute) body, res := makeRequest(t, rr.ID, url.Values{ "traits.username": {"registration-identifier-8"}, @@ -287,7 +292,6 @@ func TestRegistration(t *testing.T) { }) t.Run("case=register and then send same request", func(t *testing.T) { - viper.Set(configuration.ViperKeyURLsDefaultReturnTo, returnTs.URL+"/default-return-to") jar, _ := cookiejar.New(&cookiejar.Options{}) formValues := url.Values{ "traits.username": {"registration-identifier-11"}, @@ -305,7 +309,7 @@ func TestRegistration(t *testing.T) { }) t.Run("method=PopulateSignUpMethod", func(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) viper.Set(configuration.ViperKeyURLsSelfPublic, urlx.ParseOrPanic("https://foo/")) viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://stub/registration.schema.json") diff --git a/selfservice/strategy/password/settings.go b/selfservice/strategy/password/settings.go index 9c1cb93ea005..82d7d36a1289 100644 --- a/selfservice/strategy/password/settings.go +++ b/selfservice/strategy/password/settings.go @@ -12,7 +12,6 @@ import ( "github.com/ory/x/sqlxx" "github.com/ory/herodot" - "github.com/ory/x/errorsx" "github.com/ory/x/urlx" "github.com/ory/kratos/continuity" @@ -175,12 +174,10 @@ func (s *Strategy) completeSettingsFlow( return } - if err := s.d.SettingsExecutor().PostSettingsHook(w, r, - s.d.PostSettingsHooks(s.SettingsStrategyID()), + if err := s.d.SettingsHookExecutor().PostSettingsHook(w, r, + s.SettingsStrategyID(), ar, ss, i, - ); errorsx.Cause(err) == settings.ErrHookAbortRequest { - return - } else if err != nil { + ); err != nil { s.handleSettingsError(w, r, ar, ss, p, err) return } diff --git a/selfservice/strategy/password/settings_test.go b/selfservice/strategy/password/settings_test.go index 807098e2111c..9ab0595e0e03 100644 --- a/selfservice/strategy/password/settings_test.go +++ b/selfservice/strategy/password/settings_test.go @@ -28,7 +28,8 @@ func init() { } func TestProfile(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) + viper.Set(configuration.ViperKeyURLsDefaultReturnTo, "https://www.ory.sh/") viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/profile.schema.json") _ = testhelpers.NewSettingsUITestServer(t) @@ -127,9 +128,9 @@ func TestProfile(t *testing.T) { rts := httptest.NewServer(router) defer rts.Close() - viper.Set(configuration.ViperKeySelfServiceSettingsAfterConfig+"."+identity.CredentialsTypePassword.String(), testhelpers.HookConfigRedirectTo(t, rts.URL+"/return-ts")) + viper.Set(configuration.ViperKeySelfServiceSettingsAfter+"."+configuration.ViperKeyDefaultReturnTo, rts.URL+"/return-ts") t.Cleanup(func() { - viper.Set(configuration.ViperKeySelfServiceLoginAfterConfig+"."+string(identity.CredentialsTypePassword), nil) + viper.Set(configuration.ViperKeySelfServiceSettingsAfter, nil) }) rs := testhelpers.GetSettingsRequest(t, primaryUser, publicTS) diff --git a/selfservice/strategy/password/strategy_test.go b/selfservice/strategy/password/strategy_test.go index a98c5b1f3be2..3c93fe083db7 100644 --- a/selfservice/strategy/password/strategy_test.go +++ b/selfservice/strategy/password/strategy_test.go @@ -41,10 +41,10 @@ func hookConfig(u string) (m []map[string]interface{}) { var b bytes.Buffer if _, err := fmt.Fprintf(&b, `[ { - "job": "session" + "hook": "session" }, { - "job": "redirect", + "hook": "redirect", "config": { "default_redirect_url": "%s", "allow_user_defined_redirect": true diff --git a/session/handler.go b/session/handler.go index 76a72bebc608..b4d8514d69e7 100644 --- a/session/handler.go +++ b/session/handler.go @@ -2,7 +2,6 @@ package session import ( "net/http" - "net/url" "github.com/julienschmidt/httprouter" "github.com/pkg/errors" @@ -127,12 +126,13 @@ func (h *Handler) IsNotAuthenticated(wrap httprouter.Handle, onAuthenticated htt func RedirectOnAuthenticated(c configuration.Provider) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - returnTo, err := x.DetermineReturnToURL(r.URL, c.DefaultReturnToURL(), []url.URL{*c.SelfPublicURL()}) + returnTo, err := x.SecureRedirectTo(r, c.DefaultReturnToURL(), x.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL())) if err != nil { http.Redirect(w, r, c.DefaultReturnToURL().String(), http.StatusFound) + return } - http.Redirect(w, r, returnTo, http.StatusFound) + http.Redirect(w, r, returnTo.String(), http.StatusFound) } } diff --git a/session/handler_test.go b/session/handler_test.go index a4b037790607..7edaaa092a23 100644 --- a/session/handler_test.go +++ b/session/handler_test.go @@ -17,6 +17,7 @@ import ( "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" . "github.com/ory/kratos/session" "github.com/ory/kratos/x" ) @@ -33,12 +34,12 @@ func send(code int) httprouter.Handle { func TestSessionWhoAmI(t *testing.T) { t.Run("public", func(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) r := x.NewRouterPublic() // set this intermediate because kratos needs some valid url for CRUDE operations viper.Set(configuration.ViperKeyURLsSelfPublic, "http://example.com") - h, _ := MockSessionCreateHandler(t, reg) + h, _ := testhelpers.MockSessionCreateHandler(t, reg) r.GET("/set", h) NewHandler(reg).RegisterPublicRoutes(r) @@ -47,7 +48,7 @@ func TestSessionWhoAmI(t *testing.T) { viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL) - client := MockCookieClient(t) + client := testhelpers.MockCookieClient(t) // No cookie yet -> 401 res, err := client.Get(ts.URL + SessionsWhoamiPath) @@ -55,7 +56,7 @@ func TestSessionWhoAmI(t *testing.T) { assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode) // Set cookie - MockHydrateCookieClient(t, client, ts.URL+"/set") + testhelpers.MockHydrateCookieClient(t, client, ts.URL+"/set") // Cookie set -> 200 (GET) for _, method := range []string{ @@ -77,7 +78,7 @@ func TestSessionWhoAmI(t *testing.T) { } func TestIsNotAuthenticatedSecurecookie(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) r := x.NewRouterPublic() r.GET("/public/with-callback", reg.SessionHandler().IsNotAuthenticated(send(http.StatusOK), send(http.StatusBadRequest))) @@ -85,7 +86,7 @@ func TestIsNotAuthenticatedSecurecookie(t *testing.T) { defer ts.Close() viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL) - c := MockCookieClient(t) + c := testhelpers.MockCookieClient(t) c.Jar.SetCookies(urlx.ParseOrPanic(ts.URL), []*http.Cookie{ { Name: DefaultSessionCookieName, @@ -104,13 +105,13 @@ func TestIsNotAuthenticatedSecurecookie(t *testing.T) { } func TestIsNotAuthenticated(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) r := x.NewRouterPublic() // set this intermediate because kratos needs some valid url for CRUDE operations viper.Set(configuration.ViperKeyURLsSelfPublic, "http://example.com") reg.WithCSRFHandler(new(x.FakeCSRFHandler)) - h, _ := MockSessionCreateHandler(t, reg) + h, _ := testhelpers.MockSessionCreateHandler(t, reg) r.GET("/set", h) r.GET("/public/with-callback", reg.SessionHandler().IsNotAuthenticated(send(http.StatusOK), send(http.StatusBadRequest))) r.GET("/public/without-callback", reg.SessionHandler().IsNotAuthenticated(send(http.StatusOK), nil)) @@ -118,8 +119,8 @@ func TestIsNotAuthenticated(t *testing.T) { defer ts.Close() viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL) - sessionClient := MockCookieClient(t) - MockHydrateCookieClient(t, sessionClient, ts.URL+"/set") + sessionClient := testhelpers.MockCookieClient(t) + testhelpers.MockHydrateCookieClient(t, sessionClient, ts.URL+"/set") for k, tc := range []struct { c *http.Client @@ -158,13 +159,13 @@ func TestIsNotAuthenticated(t *testing.T) { } func TestIsAuthenticated(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) reg.WithCSRFHandler(new(x.FakeCSRFHandler)) r := x.NewRouterPublic() // set this intermediate because kratos needs some valid url for CRUDE operations viper.Set(configuration.ViperKeyURLsSelfPublic, "http://example.com") - h, _ := MockSessionCreateHandler(t, reg) + h, _ := testhelpers.MockSessionCreateHandler(t, reg) r.GET("/set", h) r.GET("/privileged/with-callback", reg.SessionHandler().IsAuthenticated(send(http.StatusOK), send(http.StatusBadRequest))) r.GET("/privileged/without-callback", reg.SessionHandler().IsAuthenticated(send(http.StatusOK), nil)) @@ -172,8 +173,8 @@ func TestIsAuthenticated(t *testing.T) { defer ts.Close() viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL) - sessionClient := MockCookieClient(t) - MockHydrateCookieClient(t, sessionClient, ts.URL+"/set") + sessionClient := testhelpers.MockCookieClient(t) + testhelpers.MockHydrateCookieClient(t, sessionClient, ts.URL+"/set") for k, tc := range []struct { c *http.Client diff --git a/session/manager.go b/session/manager.go index b5f414bd4481..5a907f47d20a 100644 --- a/session/manager.go +++ b/session/manager.go @@ -5,8 +5,6 @@ import ( "net/http" "github.com/ory/herodot" - - "github.com/ory/kratos/identity" ) // DefaultSessionCookieName returns the default cookie name for the kratos session. @@ -19,10 +17,10 @@ var ( // Manager handles identity sessions. type Manager interface { - CreateToRequest(context.Context, *identity.Identity, http.ResponseWriter, *http.Request) (*Session, error) + CreateToRequest(context.Context, http.ResponseWriter, *http.Request, *Session) error // SaveToRequest creates an HTTP session using cookies. - SaveToRequest(context.Context, *Session, http.ResponseWriter, *http.Request) error + SaveToRequest(context.Context, http.ResponseWriter, *http.Request, *Session) error // FetchFromRequest creates an HTTP session using cookies. FetchFromRequest(context.Context, *http.Request) (*Session, error) diff --git a/session/manager_http.go b/session/manager_http.go index 780ddf210c25..7f32f84601ed 100644 --- a/session/manager_http.go +++ b/session/manager_http.go @@ -44,20 +44,19 @@ func NewManagerHTTP( } } -func (s *ManagerHTTP) CreateToRequest(ctx context.Context, i *identity.Identity, w http.ResponseWriter, r *http.Request) (*Session, error) { - p := NewSession(i, r, s.c) - if err := s.r.SessionPersister().CreateSession(ctx, p); err != nil { - return nil, err +func (s *ManagerHTTP) CreateToRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ss *Session) error { + if err := s.r.SessionPersister().CreateSession(ctx, ss); err != nil { + return err } - if err := s.SaveToRequest(ctx, p, w, r); err != nil { - return nil, err + if err := s.SaveToRequest(ctx, w, r, ss); err != nil { + return err } - return p, nil + return nil } -func (s *ManagerHTTP) SaveToRequest(ctx context.Context, session *Session, w http.ResponseWriter, r *http.Request) error { +func (s *ManagerHTTP) SaveToRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, session *Session) error { _ = s.r.CSRFHandler().RegenerateToken(w, r) cookie, _ := s.r.CookieManager().Get(r, s.cookieName) cookie.Values["sid"] = session.ID.String() diff --git a/session/manager_http_test.go b/session/manager_http_test.go index 9d730574ebb2..6d9a0e2aabfe 100644 --- a/session/manager_http_test.go +++ b/session/manager_http_test.go @@ -28,12 +28,12 @@ func (f *mockCSRFHandler) RegenerateToken(w http.ResponseWriter, r *http.Request func TestManagerHTTP(t *testing.T) { t.Run("method=SaveToRequest", func(t *testing.T) { - _, reg := internal.NewRegistryDefault(t) + _, reg := internal.NewFastRegistryWithMocks(t) mock := new(mockCSRFHandler) reg.WithCSRFHandler(mock) - require.NoError(t, reg.SessionManager().SaveToRequest(context.Background(), new(session.Session), httptest.NewRecorder(), new(http.Request))) + require.NoError(t, reg.SessionManager().SaveToRequest(context.Background(), httptest.NewRecorder(), new(http.Request), new(session.Session))) assert.Equal(t, 1, mock.c) }) } diff --git a/session/session.go b/session/session.go index 6d9f4b69e77a..b2e189c2363a 100644 --- a/session/session.go +++ b/session/session.go @@ -1,7 +1,6 @@ package session import ( - "net/http" "time" "github.com/gofrs/uuid" @@ -33,20 +32,19 @@ type Session struct { CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` // UpdatedAt is a helper struct field for gobuffalo.pop. UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` - - modifiedIdentity bool `faker:"-" db:"-"` } func (s Session) TableName() string { return "sessions" } -func NewSession(i *identity.Identity, r *http.Request, c interface { +func NewSession(i *identity.Identity, c interface { SessionLifespan() time.Duration -}) *Session { +}, authenticatedAt time.Time) *Session { return &Session{ ID: x.NewUUID(), - ExpiresAt: time.Now().UTC().Add(c.SessionLifespan()), + ExpiresAt: authenticatedAt.Add(c.SessionLifespan()), + AuthenticatedAt: authenticatedAt, IssuedAt: time.Now().UTC(), Identity: i, } @@ -57,21 +55,7 @@ type Device struct { SeenAt []time.Time `json:"seen_at" faker:"time_types"` } -func (s *Session) UpdateIdentity(i *identity.Identity) *Session { - s.Identity = i - s.modifiedIdentity = true - return s -} - -func (s *Session) GetIdentity() *identity.Identity { - return s.Identity -} - -func (s *Session) WasIdentityModified() bool { - return s.modifiedIdentity -} - -func (s *Session) ResetModifiedIdentityFlag() *Session { - s.modifiedIdentity = false +func (s *Session) Declassify() *Session { + s.Identity = s.Identity.CopyWithoutCredentials() return s } diff --git a/tests/fixtures/config.schema.test.failure/wrongTypes.main.yaml b/tests/fixtures/config.schema.test.failure/wrongTypes.main.yaml index d81c2ac0a4b3..15e12b5c139c 100644 --- a/tests/fixtures/config.schema.test.failure/wrongTypes.main.yaml +++ b/tests/fixtures/config.schema.test.failure/wrongTypes.main.yaml @@ -52,7 +52,7 @@ urls: default_return_to: 1 registration_ui: 1 error_ui: 1 - whitelisted_return_to_domains: + whitelisted_return_to_urls: - 1 - 1 diff --git a/tests/fixtures/config.schema.test.success/allKeys.main.yaml b/tests/fixtures/config.schema.test.success/allKeys.main.yaml index 60a951453343..0b84634d6bae 100644 --- a/tests/fixtures/config.schema.test.success/allKeys.main.yaml +++ b/tests/fixtures/config.schema.test.success/allKeys.main.yaml @@ -14,17 +14,31 @@ selfservice: profile: request_lifespan: 10m + after: + profile: + hooks: "#/definitions/selfServiceAfterLoginStrategy" + password: + hooks: "#/definitions/selfServiceAfterLoginStrategy" login: request_lifespan: 10m - before: "#/definitions/selfServiceBefore" - after: "#/definitions/selfServiceAfterLogin" + before: + hooks: "#/definitions/selfServiceBefore" + after: + password: + hooks: "#/definitions/selfServiceAfterLoginStrategy" + oidc: + hooks: "#/definitions/selfServiceAfterLoginStrategy" registration: request_lifespan: 10m before: - - "#/definitions/selfServiceRedirectHook" - after: "#/definitions/selfServiceAfterRegistration" + hooks: "#/definitions/selfServiceBefore" + after: + password: + hooks: "#/definitions/selfServiceAfterRegistrationStrategy" + oidc: + hooks: "#/definitions/selfServiceAfterRegistrationStrategy" dsn: foo @@ -50,10 +64,10 @@ urls: login_ui: https://example.com settings_ui: https://example.com verify_ui: https://example.com - default_return_to: https://example.com + default_return_to: "#/definitions/defaultReturnTo" registration_ui: https://example.com error_ui: https://example.com - whitelisted_return_to_domains: + whitelisted_return_to_urls: - https://example0.com - https://example1.com diff --git a/tests/schema_test.go b/tests/schema_test.go index e775ddab3917..c5d0052d0407 100644 --- a/tests/schema_test.go +++ b/tests/schema_test.go @@ -95,6 +95,7 @@ func (ss *schemas) getByName(n string) (*schema, error) { } func TestSchemas(t *testing.T) { + t.Skip("See https://github.com/ory/kratos/issues/347") t.Run("test .schema/config.schema.json", SchemaTestRunner("../.schema", "config")) } diff --git a/x/http.go b/x/http.go index 9b49dfdf591a..4d1d3aa56e57 100644 --- a/x/http.go +++ b/x/http.go @@ -5,9 +5,12 @@ import ( "io/ioutil" "net/http" "net/http/cookiejar" + "net/url" "testing" "github.com/stretchr/testify/require" + + "github.com/ory/x/stringsx" ) func NewTestHTTPRequest(t *testing.T, method, url string, body io.Reader) *http.Request { @@ -35,3 +38,16 @@ func EasyCookieJar(t *testing.T, o *cookiejar.Options) *cookiejar.Jar { require.NoError(t, err) return cj } + +func RequestURL(r *http.Request) *url.URL { + source := *r.URL + source.Host = stringsx.Coalesce(source.Host, r.Host) + if source.Scheme == "" { + source.Scheme = "https" + if r.TLS == nil { + source.Scheme = "http" + } + } + + return &source +} diff --git a/x/http_secure_redirect.go b/x/http_secure_redirect.go new file mode 100644 index 000000000000..85a73df6a157 --- /dev/null +++ b/x/http_secure_redirect.go @@ -0,0 +1,136 @@ +package x + +import ( + "net/http" + "net/url" + "strings" + + "github.com/golang/gddo/httputil" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/x/stringsx" + "github.com/ory/x/urlx" + + "github.com/ory/kratos/driver/configuration" +) + +type secureRedirectOptions struct { + whitelist []url.URL + defaultReturnTo *url.URL + sourceURL string +} + +type SecureRedirectOption func(*secureRedirectOptions) + +// SecureRedirectAllowURLs whitelists the given URLs for redirects. +func SecureRedirectAllowURLs(urls []url.URL) SecureRedirectOption { + return func(o *secureRedirectOptions) { + o.whitelist = append(o.whitelist, urls...) + } +} + +// SecureRedirectUseSourceURL uses the given source URL (checks the `?return_to` value) +// instead of r.URL. +func SecureRedirectUseSourceURL(source string) SecureRedirectOption { + return func(o *secureRedirectOptions) { + o.sourceURL = source + } +} + +// SecureRedirectAllowSelfServiceURLs allows the caller to define `?return_to=` values +// which contain the server's URL and `/self-service` path prefix. Useful for redirecting +// to the login endpoint, for example. +func SecureRedirectAllowSelfServiceURLs(publicURL *url.URL) SecureRedirectOption { + return func(o *secureRedirectOptions) { + o.whitelist = append(o.whitelist, *urlx.AppendPaths(publicURL, "/self-service")) + } +} + +// SecureRedirectOverrideDefaultReturnTo overrides the defaultReturnTo address specified +// as the second arg. +func SecureRedirectOverrideDefaultReturnTo(defaultReturnTo *url.URL) SecureRedirectOption { + return func(o *secureRedirectOptions) { + o.defaultReturnTo = defaultReturnTo + } +} + +// SecureRedirectTo implements a HTTP redirector who mitigates open redirect vulnerabilities by +// working with whitelisting. +func SecureRedirectTo(r *http.Request, defaultReturnTo *url.URL, opts ...SecureRedirectOption) (returnTo *url.URL, err error) { + o := &secureRedirectOptions{defaultReturnTo: defaultReturnTo} + for _, opt := range opts { + opt(o) + } + + if len(o.whitelist) == 0 { + return o.defaultReturnTo, nil + } + + source := RequestURL(r) + if o.sourceURL != "" { + source, err = url.ParseRequestURI(o.sourceURL) + if err != nil { + return nil, herodot.ErrInternalServerError.WithWrap(err).WithReasonf("Unable to parse the original request URL: %s", err) + } + } + + if len(source.Query().Get("return_to")) == 0 { + return o.defaultReturnTo, nil + } else if returnTo, err = url.ParseRequestURI(source.Query().Get("return_to")); err != nil { + return nil, herodot.ErrInternalServerError.WithWrap(err).WithReasonf("Unable to parse the return_to query parameter as an URL: %s", err) + } + + returnTo.Host = stringsx.Coalesce(returnTo.Host, o.defaultReturnTo.Host) + returnTo.Scheme = stringsx.Coalesce(returnTo.Scheme, o.defaultReturnTo.Scheme) + + var found bool + for _, allowed := range o.whitelist { + if strings.EqualFold(allowed.Scheme, returnTo.Scheme) && + strings.EqualFold(allowed.Host, returnTo.Host) && + strings.HasPrefix( + stringsx.Coalesce(returnTo.Path, "/"), + stringsx.Coalesce(allowed.Path, "/")) { + found = true + } + } + + if !found { + return nil, errors.WithStack(herodot.ErrBadRequest. + WithReasonf("Requested return_to URL \"%s\" is not whitelisted.", returnTo). + WithDebugf("Whitelisted domains are: %v", o.whitelist)) + } + + return returnTo, nil +} + +func SecureContentNegotiationRedirection( + w http.ResponseWriter, r *http.Request, out interface{}, + requestURL string, writer herodot.Writer, c configuration.Provider, + opts ...SecureRedirectOption, +) error { + switch httputil.NegotiateContentType(r, []string{ + "text/html", + "application/json", + }, "text/html") { + case "application/json": + writer.Write(w, r, out) + case "text/html": + fallthrough + default: + ret, err := SecureRedirectTo(r, c.DefaultReturnToURL(), + append([]SecureRedirectOption{ + SecureRedirectUseSourceURL(requestURL), + SecureRedirectAllowURLs(c.WhitelistedReturnToDomains()), + SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL()), + }, opts...)..., + ) + if err != nil { + return err + } + + http.Redirect(w, r, ret.String(), http.StatusFound) + } + + return nil +} diff --git a/x/http_secure_redirect_test.go b/x/http_secure_redirect_test.go new file mode 100644 index 000000000000..c1cb3c8277e4 --- /dev/null +++ b/x/http_secure_redirect_test.go @@ -0,0 +1,191 @@ +package x_test + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + "github.com/ory/viper" + "github.com/ory/x/urlx" + + "github.com/ory/kratos/driver/configuration" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/x" +) + +func TestSecureContentNegotiationRedirection(t *testing.T) { + conf, _ := internal.NewFastRegistryWithMocks(t) + var jsonActual = json.RawMessage(`{"foo":"bar"}`) + writer := herodot.NewJSONWriter(nil) + + router := httprouter.New() + router.GET("/redir", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.NoError(t, x.SecureContentNegotiationRedirection(w, r, jsonActual, x.RequestURL(r).String(), writer, conf)) + }) + router.GET("/default-return-to", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + w.WriteHeader(http.StatusNoContent) + }) + router.GET("/return-to", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + w.WriteHeader(http.StatusNoContent) + }) + + ts := httptest.NewServer(router) + defer ts.Close() + + defaultReturnTo := ts.URL + "/default-return-to" + viper.Set(configuration.ViperKeyURLsDefaultReturnTo, defaultReturnTo) + viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL) + viper.Set(configuration.ViperKeyURLsWhitelistedReturnToDomains, []string{ts.URL}) + + run := func(t *testing.T, href string, contentType string) (*http.Response, string) { + req, err := http.NewRequest("GET", href, nil) + require.NoError(t, err) + req.Header.Add("Accept", contentType) + res, err := ts.Client().Do(req) + require.NoError(t, err) + body, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + return res, string(body) + } + + t.Run("case=html browser causes redirect", func(t *testing.T) { + res, _ := run(t, ts.URL+"/redir", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9") + assert.EqualValues(t, res.Request.URL.String(), defaultReturnTo) + }) + + t.Run("case=html browser causes redirect with redirect_to", func(t *testing.T) { + res, _ := run(t, ts.URL+"/redir?return_to=/redirect-to", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9") + assert.EqualValues(t, res.Request.URL.String(), ts.URL+"/redirect-to") + }) + + t.Run("case=html browser causes redirect with redirect_to", func(t *testing.T) { + res, body := run(t, ts.URL+"/redir?return_to=/redirect-to", "application/json") + assert.EqualValues(t, res.Request.URL.String(), ts.URL+"/redir?return_to=/redirect-to") + assert.EqualValues(t, body, jsonActual) + }) +} + +func TestSecureRedirectTo(t *testing.T) { + const defaultReturnTo = "/default-return-to" + + var newServer = func(t *testing.T, isTLS bool, expectErr bool, opts func(ts *httptest.Server) []x.SecureRedirectOption) *httptest.Server { + var ts *httptest.Server + f := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if opts == nil { + opts = func(ts *httptest.Server) []x.SecureRedirectOption { + return nil + } + } + ru, err := x.SecureRedirectTo(r, urlx.ParseOrPanic(ts.URL+defaultReturnTo), opts(ts)...) + if expectErr { + require.Error(t, err) + _, _ = w.Write([]byte("error")) + return + } + require.NoError(t, err) + + _, _ = w.Write([]byte(ru.String())) + }) + + if isTLS { + ts = httptest.NewTLSServer(f) + } else { + ts = httptest.NewServer(f) + } + + t.Cleanup(ts.Close) + return ts + } + + var makeRequest = func(t *testing.T, ts *httptest.Server, path string) (*http.Response, string) { + res, err := ts.Client().Get(ts.URL + "/" + path) + require.NoError(t, err) + + body, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + return res, string(body) + } + + t.Run("case=return to default URL if nothing is allowed", func(t *testing.T) { + s := newServer(t, false, false, nil) + _, body := makeRequest(t, s, "?return_to=/foo") + assert.EqualValues(t, body, s.URL+"/default-return-to") + }) + + t.Run("case=return to foo with server baseURL if allowed", func(t *testing.T) { + s := newServer(t, false, false, func(ts *httptest.Server) []x.SecureRedirectOption { + return []x.SecureRedirectOption{x.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic(ts.URL)})} + }) + _, body := makeRequest(t, s, "?return_to=/foo") + assert.Equal(t, body, s.URL+"/foo") + }) + + t.Run("case=return to another domain works", func(t *testing.T) { + s := newServer(t, false, false, func(ts *httptest.Server) []x.SecureRedirectOption { + return []x.SecureRedirectOption{x.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://www.ory.sh/foo")})} + }) + _, body := makeRequest(t, s, "?return_to=https://www.ory.sh/foo/kratos") + assert.Equal(t, body, "https://www.ory.sh/foo/kratos") + }) + + t.Run("case=return to another domain fails if host mismatches", func(t *testing.T) { + s := newServer(t, false, true, func(ts *httptest.Server) []x.SecureRedirectOption { + return []x.SecureRedirectOption{x.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://www.not-ory.sh/")})} + }) + _, body := makeRequest(t, s, "?return_to=https://www.ory.sh/kratos") + assert.Equal(t, body, "error") + }) + + t.Run("case=return to another domain fails if path mismatches", func(t *testing.T) { + s := newServer(t, false, true, func(ts *httptest.Server) []x.SecureRedirectOption { + return []x.SecureRedirectOption{x.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://www.ory.sh/not-kratos")})} + }) + _, body := makeRequest(t, s, "?return_to=https://www.ory.sh/kratos") + assert.Equal(t, body, "error") + }) + + t.Run("case=return to another domain fails if scheme mismatches", func(t *testing.T) { + s := newServer(t, false, true, func(ts *httptest.Server) []x.SecureRedirectOption { + return []x.SecureRedirectOption{x.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("http://www.ory.sh/")})} + }) + _, body := makeRequest(t, s, "?return_to=https://www.ory.sh/kratos") + assert.Equal(t, body, "error") + }) + + t.Run("case=should work with self-service modifier", func(t *testing.T) { + s := newServer(t, false, false, func(ts *httptest.Server) []x.SecureRedirectOption { + return []x.SecureRedirectOption{x.SecureRedirectAllowSelfServiceURLs(urlx.ParseOrPanic(ts.URL))} + }) + _, body := makeRequest(t, s, "?return_to=/self-service/foo") + assert.Equal(t, body, s.URL+"/self-service/foo") + }) + + t.Run("case=should work with default return to", func(t *testing.T) { + s := newServer(t, false, false, func(ts *httptest.Server) []x.SecureRedirectOption { + return []x.SecureRedirectOption{x.SecureRedirectOverrideDefaultReturnTo(urlx.ParseOrPanic(ts.URL + "/another-default"))} + }) + _, body := makeRequest(t, s, "") + assert.Equal(t, body, s.URL+"/another-default") + }) + + t.Run("case=should override return_to", func(t *testing.T) { + s := newServer(t, false, false, func(ts *httptest.Server) []x.SecureRedirectOption { + return []x.SecureRedirectOption{ + x.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic(ts.URL)}), + x.SecureRedirectUseSourceURL("https://foo/bar?return_to=/override"), + } + }) + _, body := makeRequest(t, s, "?return_to=/original") + assert.Equal(t, body, s.URL+"/override") + }) +} diff --git a/x/http_test.go b/x/http_test.go new file mode 100644 index 000000000000..bd124084e2e3 --- /dev/null +++ b/x/http_test.go @@ -0,0 +1,20 @@ +package x + +import ( + "crypto/tls" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/x/urlx" +) + +func TestRequestURL(t *testing.T) { + assert.EqualValues(t, RequestURL(&http.Request{ + URL: urlx.ParseOrPanic("/foo"), Host: "foobar", TLS: &tls.ConnectionState{}, + }).String(), "https://foobar/foo") + assert.EqualValues(t, RequestURL(&http.Request{ + URL: urlx.ParseOrPanic("/foo"), Host: "foobar", + }).String(), "http://foobar/foo") +} diff --git a/x/return_to.go b/x/return_to.go deleted file mode 100644 index 1b54ced651eb..000000000000 --- a/x/return_to.go +++ /dev/null @@ -1,38 +0,0 @@ -package x - -import ( - "net/url" - "strings" - - "github.com/pkg/errors" - - "github.com/ory/herodot" -) - -func DetermineReturnToURL(request *url.URL, defaultReturnTo *url.URL, whitelistedDomains []url.URL) (string, error) { - u, err := url.ParseRequestURI(request.Query().Get("return_to")) - if len(request.Query().Get("return_to")) == 0 || err != nil { - return defaultReturnTo.String(), nil - } - - var found bool - for _, wd := range whitelistedDomains { - if strings.EqualFold(wd.Scheme, u.Scheme) && strings.EqualFold(wd.Host, u.Host) { - found = true - } - } - - if !found && len(u.Host) > 0 { - return "", errors.WithStack(herodot.ErrBadRequest.WithReasonf("Requested return_to domain \"%s\" is not a whitelisted return domain", u.Host)) - } - - if len(u.Host) == 0 { - u.Host = defaultReturnTo.Host - } - - if len(u.Scheme) == 0 { - u.Scheme = defaultReturnTo.Scheme - } - - return u.String(), nil -} diff --git a/x/return_to_test.go b/x/return_to_test.go deleted file mode 100644 index 9026a74012bc..000000000000 --- a/x/return_to_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package x - -import ( - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "sync" - "testing" - - "github.com/ory/x/urlx" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestDetermineReturnToURL(t *testing.T) { - for k, tc := range []struct { - isTLS bool - expect func(t *testing.T, got string, ts *httptest.Server) - expectErr bool - path string - }{ - { - path: "?return_to=/foo", - expectErr: false, - expect: func(t *testing.T, got string, ts *httptest.Server) { - assert.Equal(t, ts.URL+"/foo", got) - }, - }, - { - path: "", - expectErr: false, - expect: func(t *testing.T, got string, ts *httptest.Server) { - assert.Equal(t, ts.URL, got) - }, - }, - { - path: "?return_to=https://ory.sh/asdf", - expectErr: false, - expect: func(t *testing.T, got string, ts *httptest.Server) { - assert.Equal(t, "https://ory.sh/asdf", got) - }, - }, - { - path: "?return_to=http://ory.sh/asdf", - expectErr: true, - }, - { - path: "?return_to=https://not-ory.sh/asdf", - expectErr: true, - }, - } { - t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - var wg sync.WaitGroup - wg.Add(1) - - var ts *httptest.Server - f := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer wg.Done() - ru, err := DetermineReturnToURL(r.URL, urlx.ParseOrPanic(ts.URL), []url.URL{*urlx.ParseOrPanic("https://ory.sh/")}) - if tc.expectErr { - require.Error(t, err) - } else { - require.NoError(t, err) - tc.expect(t, ru, ts) - } - - w.WriteHeader(http.StatusNoContent) - }) - - if tc.isTLS { - ts = httptest.NewServer(f) - } else { - ts = httptest.NewTLSServer(f) - } - - defer ts.Close() - - res, err := ts.Client().Get(ts.URL + "/" + tc.path) - require.NoError(t, err) - assert.EqualValues(t, http.StatusNoContent, res.StatusCode) - - wg.Wait() - }) - } -}