Repository: ory/kratos
Pull request #784 (2020-10-22 08:17:13)

Bump gobufalo/pop
## Related issue

<!--
Please link the GitHub issue this pull request resolves in the format of `#1234`. If you discussed this change
with a maintainer, please mention her/him using the `@` syntax (e.g. `@aeneasr`).

If this change neither resolves an existing issue nor has sign-off from one of the maintainers, there is a
chance substantial changes will be requested or that the changes will be rejected.

You can discuss changes with maintainers either in the [ORY Community Forums](https://community.ory.sh/) or
join the [ORY Chat](https://www.ory.sh/chat).
-->

## Proposed changes

<!--
Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.
-->

## Checklist

<!--
Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of
them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.
-->

- [ ] I have read the [contributing guidelines](../blob/master/CONTRIBUTING.md).
- [ ] I have read the [security policy](../security/policy).
- [ ] I confirm that this pull request does not address a security
      vulnerability. If this pull request addresses a security. vulnerability, I
      confirm that I got green light (please contact
      [security@ory.sh](mailto:security@ory.sh)) from the maintainers to push
      the changes.
- [ ] I have added tests that prove my fix is effective or that my feature
      works.
- [ ] I have added or changed [the documentation](docs/docs).

## Further comments

<!--
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution
you did and what alternatives you considered, etc...
-->



Patch:
diff --git a/.schema/config.schema.json b/.schema/config.schema.json
index 67ca84863..63e535d51 100644
--- a/.schema/config.schema.json
+++ b/.schema/config.schema.json
@@ -624,13 +624,13 @@
                 }
               }
             },
-            "recovery_token": {
+            "link": {
               "type": "object",
               "additionalProperties": false,
               "properties": {
                 "enabled": {
                   "type": "boolean",
-                  "title": "Enables Token-based Account Recovery Method",
+                  "title": "Enables Link Method",
                   "default": true
                 }
               }
diff --git a/CHANGELOG.md b/CHANGELOG.md
index af1544012..2188c0240 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,18 +4,19 @@
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 **Table of Contents**
 
-- [Unreleased (2020-10-20)](#unreleased-2020-10-20)
+- [Unreleased (2020-10-22)](#unreleased-2020-10-22)
     - [Bug Fixes](#bug-fixes)
+    - [Documentation](#documentation)
   - [0.5.1-alpha.1 (2020-10-20)](#051-alpha1-2020-10-20)
     - [Bug Fixes](#bug-fixes-1)
-    - [Documentation](#documentation)
+    - [Documentation](#documentation-1)
     - [Features](#features)
     - [Tests](#tests)
     - [Unclassified](#unclassified)
 - [0.5.0-alpha.1 (2020-10-15)](#050-alpha1-2020-10-15)
     - [Bug Fixes](#bug-fixes-2)
     - [Code Refactoring](#code-refactoring)
-    - [Documentation](#documentation-1)
+    - [Documentation](#documentation-2)
     - [Features](#features-1)
     - [Tests](#tests-1)
     - [Unclassified](#unclassified-1)
@@ -26,7 +27,7 @@
     - [Bug Fixes](#bug-fixes-4)
   - [0.4.4-alpha.1 (2020-07-10)](#044-alpha1-2020-07-10)
     - [Bug Fixes](#bug-fixes-5)
-    - [Documentation](#documentation-2)
+    - [Documentation](#documentation-3)
   - [0.4.3-alpha.1 (2020-07-08)](#043-alpha1-2020-07-08)
     - [Bug Fixes](#bug-fixes-6)
   - [0.4.2-alpha.1 (2020-07-08)](#042-alpha1-2020-07-08)
@@ -34,48 +35,48 @@
 - [0.4.0-alpha.1 (2020-07-08)](#040-alpha1-2020-07-08)
     - [Bug Fixes](#bug-fixes-8)
     - [Code Refactoring](#code-refactoring-1)
-    - [Documentation](#documentation-3)
+    - [Documentation](#documentation-4)
     - [Features](#features-2)
     - [Unclassified](#unclassified-2)
     - [BREAKING CHANGES](#breaking-changes-1)
 - [0.3.0-alpha.1 (2020-05-15)](#030-alpha1-2020-05-15)
     - [Bug Fixes](#bug-fixes-9)
     - [Code Refactoring](#code-refactoring-2)
-    - [Documentation](#documentation-4)
+    - [Documentation](#documentation-5)
     - [Features](#features-3)
     - [Unclassified](#unclassified-3)
     - [BREAKING CHANGES](#breaking-changes-2)
   - [0.2.1-alpha.1 (2020-05-05)](#021-alpha1-2020-05-05)
-    - [Documentation](#documentation-5)
+    - [Documentation](#documentation-6)
 - [0.2.0-alpha.2 (2020-05-04)](#020-alpha2-2020-05-04)
     - [Bug Fixes](#bug-fixes-10)
     - [Code Refactoring](#code-refactoring-3)
-    - [Documentation](#documentation-6)
+    - [Documentation](#documentation-7)
     - [Features](#features-4)
     - [Unclassified](#unclassified-4)
     - [BREAKING CHANGES](#breaking-changes-3)
   - [0.1.1-alpha.1 (2020-02-18)](#011-alpha1-2020-02-18)
     - [Bug Fixes](#bug-fixes-11)
     - [Code Refactoring](#code-refactoring-4)
-    - [Documentation](#documentation-7)
+    - [Documentation](#documentation-8)
 - [0.1.0-alpha.6 (2020-02-16)](#010-alpha6-2020-02-16)
     - [Bug Fixes](#bug-fixes-12)
     - [Code Refactoring](#code-refactoring-5)
-    - [Documentation](#documentation-8)
+    - [Documentation](#documentation-9)
     - [Features](#features-5)
 - [0.1.0-alpha.5 (2020-02-06)](#010-alpha5-2020-02-06)
-    - [Documentation](#documentation-9)
+    - [Documentation](#documentation-10)
     - [Features](#features-6)
 - [0.1.0-alpha.4 (2020-02-06)](#010-alpha4-2020-02-06)
-    - [Documentation](#documentation-10)
+    - [Documentation](#documentation-11)
 - [0.1.0-alpha.3 (2020-02-06)](#010-alpha3-2020-02-06)
 - [0.1.0-alpha.2 (2020-02-03)](#010-alpha2-2020-02-03)
     - [Bug Fixes](#bug-fixes-13)
-    - [Documentation](#documentation-11)
+    - [Documentation](#documentation-12)
     - [Features](#features-7)
     - [Unclassified](#unclassified-5)
 - [0.1.0-alpha.1 (2020-01-31)](#010-alpha1-2020-01-31)
-    - [Documentation](#documentation-12)
+    - [Documentation](#documentation-13)
   - [0.0.3-alpha.15 (2020-01-31)](#003-alpha15-2020-01-31)
     - [Unclassified](#unclassified-6)
   - [0.0.3-alpha.14 (2020-01-31)](#003-alpha14-2020-01-31)
@@ -105,12 +106,12 @@
   - [0.0.1-alpha.3 (2020-01-28)](#001-alpha3-2020-01-28)
   - [0.0.1-alpha.2 (2020-01-28)](#001-alpha2-2020-01-28)
   - [0.0.1-alpha.1 (2020-01-28)](#001-alpha1-2020-01-28)
-    - [Documentation](#documentation-13)
+    - [Documentation](#documentation-14)
     - [Unclassified](#unclassified-18)
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
-# [Unreleased](https://github.com/ory/kratos/compare/v0.5.1-alpha.1...16e6b4d76d297182ea9a1f5dc6367570f02f7b42) (2020-10-20)
+# [Unreleased](https://github.com/ory/kratos/compare/v0.5.1-alpha.1...6a3c45330885eb95015fa7ee9b58a72c38132499) (2020-10-22)
 
 
 ### Bug Fixes
@@ -118,6 +119,11 @@
 * Add debug quickstart yml ([#780](https://github.com/ory/kratos/issues/780)) ([16e6b4d](https://github.com/ory/kratos/commit/16e6b4d76d297182ea9a1f5dc6367570f02f7b42))
 
 
+### Documentation
+
+* Small improvements to discord oidc provider guide ([#783](https://github.com/ory/kratos/issues/783)) ([6a3c453](https://github.com/ory/kratos/commit/6a3c45330885eb95015fa7ee9b58a72c38132499))
+
+
 
 ## [0.5.1-alpha.1](https://github.com/ory/kratos/compare/v0.5.0-alpha.1...v0.5.1-alpha.1) (2020-10-20)
 
diff --git a/cmd/daemon/serve.go b/cmd/daemon/serve.go
index 48ba9174a..7fce0fc26 100644
--- a/cmd/daemon/serve.go
+++ b/cmd/daemon/serve.go
@@ -58,6 +58,8 @@ func servePublic(d driver.Driver, wg *sync.WaitGroup, cmd *cobra.Command, args [
 		c.SelfPublicURL().Hostname(),
 		!flagx.MustGetBool(cmd, "dev"),
 	)
+
+	n.UseFunc(x.CleanPath) // Prevent double slashes from breaking CSRF.
 	r.WithCSRFHandler(csrf)
 	n.UseHandler(r.CSRFHandler())
 
diff --git a/docs/docs/guides/sign-in-with-github-google-facebook-linkedin.mdx b/docs/docs/guides/sign-in-with-github-google-facebook-linkedin.mdx
index c6e69b6fb..c470883c0 100644
--- a/docs/docs/guides/sign-in-with-github-google-facebook-linkedin.mdx
+++ b/docs/docs/guides/sign-in-with-github-google-facebook-linkedin.mdx
@@ -59,9 +59,11 @@ configuration file (explained in further detail at
 
 Discord does not implement OpenID Connect. Therefore, ORY Kratos makes a request to
 [Discord's User API](https://discord.com/developers/docs/resources/user#get-current-user)
-and adds the following claims to `std.extVar('claims')`:
+and adds that data to `std.extVar('claims')`.
+However, not all fields are supported. Check the list of supported fields
+[in Kratos' source code](https://github.com/ory/kratos/blob/v0.5.1-alpha.1/selfservice/strategy/oidc/provider_discord.go#L81-L91).
 
-With scope `identify`:
+The `identify` scope will add following fields:
 
 ```text
 iss                 # always https://discord.com/api/v6/oauth2/
@@ -73,11 +75,10 @@ picture             # avatar url
 locale              # user locale
 ```
 
-With Scope `email`:
+Additionally, the `email` scope will add:
 
 ```text
-iss                 # always https://discord.com/api/v6/oauth2/
-email               # email
+email               # user email
 email_verified      # whether email is verified
 ```
 
diff --git a/go.mod b/go.mod
index 501fd7c36..ecd603f69 100644
--- a/go.mod
+++ b/go.mod
@@ -33,7 +33,7 @@ require (
 	github.com/go-swagger/go-swagger v0.25.0
 	github.com/gobuffalo/fizz v1.13.1-0.20200903094245-046abeb7de46
 	github.com/gobuffalo/httptest v1.0.2
-	github.com/gobuffalo/pop/v5 v5.3.1-0.20201019113858-a0323d6d218f
+	github.com/gobuffalo/pop/v5 v5.3.1
 	github.com/gobuffalo/uuid v2.0.5+incompatible
 	github.com/gofrs/uuid v3.2.0+incompatible
 	github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2
diff --git a/go.sum b/go.sum
index 4a5bf38be..85a12fee3 100644
--- a/go.sum
+++ b/go.sum
@@ -561,8 +561,8 @@ github.com/gobuffalo/pop/v5 v5.0.11/go.mod h1:mZJHJbA3cy2V18abXYuVop2ldEJ8UZ2DK6
 github.com/gobuffalo/pop/v5 v5.2.0/go.mod h1:Hj586Cr7FoTFNmvzyNdUcajv3r0A+W+bkil4RIX/zKo=
 github.com/gobuffalo/pop/v5 v5.2.4 h1:mvAjx8k1oYXWBTNFbvlxFBNEfoXTnEnVBcCmvQ48Lnw=
 github.com/gobuffalo/pop/v5 v5.2.4/go.mod h1:4mYDlrcXTnG2n9rG8p+c10X1QnfMc4YIRVmiC8ihMAI=
-github.com/gobuffalo/pop/v5 v5.3.1-0.20201019113858-a0323d6d218f h1:pMNiBY2h1SDrX5GpnfqTvnNAUPnrJrRZjjGmFgA8YYI=
-github.com/gobuffalo/pop/v5 v5.3.1-0.20201019113858-a0323d6d218f/go.mod h1:vcEDhh6cJ3WVENqJDFt/6z7zNb7lLnlN8vj3n5G9rYA=
+github.com/gobuffalo/pop/v5 v5.3.1 h1:dJbBPy6e0G0VRjn28md3fk16wpYIBv5iYVQWd0eqmkQ=
+github.com/gobuffalo/pop/v5 v5.3.1/go.mod h1:vcEDhh6cJ3WVENqJDFt/6z7zNb7lLnlN8vj3n5G9rYA=
 github.com/gobuffalo/release v1.0.35/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4=
 github.com/gobuffalo/release v1.0.38/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4=
 github.com/gobuffalo/release v1.0.42/go.mod h1:RPs7EtafH4oylgetOJpGP0yCZZUiO4vqHfTHJjSdpug=
diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go
index db3d4bc10..c37434e7a 100644
--- a/selfservice/flow/login/handler.go
+++ b/selfservice/flow/login/handler.go
@@ -33,6 +33,7 @@ type (
 		session.ManagementProvider
 		x.WriterProvider
 		x.CSRFTokenGeneratorProvider
+		x.CSRFProvider
 	}
 	HandlerProvider interface {
 		LoginHandler() *Handler
@@ -48,6 +49,7 @@ func NewHandler(d handlerDependencies, c configuration.Provider) *Handler {
 }
 
 func (h *Handler) RegisterPublicRoutes(public *x.RouterPublic) {
+	h.d.CSRFHandler().ExemptPath(RouteInitAPIFlow)
 	public.GET(RouteInitBrowserFlow, h.initBrowserFlow)
 	public.GET(RouteInitAPIFlow, h.initAPIFlow)
 	public.GET(RouteGetFlow, h.fetchFlow)
diff --git a/selfservice/flow/recovery/handler.go b/selfservice/flow/recovery/handler.go
index fcbcdc8c2..df254eeb2 100644
--- a/selfservice/flow/recovery/handler.go
+++ b/selfservice/flow/recovery/handler.go
@@ -36,6 +36,7 @@ type (
 		FlowPersistenceProvider
 		x.CSRFTokenGeneratorProvider
 		x.WriterProvider
+		x.CSRFProvider
 	}
 	Handler struct {
 		d handlerDependencies
@@ -48,6 +49,8 @@ func NewHandler(d handlerDependencies, c configuration.Provider) *Handler {
 }
 
 func (h *Handler) RegisterPublicRoutes(public *x.RouterPublic) {
+	h.d.CSRFHandler().ExemptPath(RouteInitAPIFlow)
+
 	redirect := session.RedirectOnAuthenticated(h.c)
 	public.GET(RouteInitBrowserFlow, h.d.SessionHandler().IsNotAuthenticated(h.initBrowserFlow, redirect))
 	public.GET(RouteInitAPIFlow, h.d.SessionHandler().IsNotAuthenticated(h.initAPIFlow,
diff --git a/selfservice/flow/registration/handler.go b/selfservice/flow/registration/handler.go
index 29c934cca..259a11eaa 100644
--- a/selfservice/flow/registration/handler.go
+++ b/selfservice/flow/registration/handler.go
@@ -33,6 +33,7 @@ type (
 		x.CSRFTokenGeneratorProvider
 		HookExecutorProvider
 		FlowPersistenceProvider
+		x.CSRFProvider
 	}
 	HandlerProvider interface {
 		RegistrationHandler() *Handler
@@ -48,6 +49,8 @@ func NewHandler(d handlerDependencies, c configuration.Provider) *Handler {
 }
 
 func (h *Handler) RegisterPublicRoutes(public *x.RouterPublic) {
+	h.d.CSRFHandler().ExemptPath(RouteInitAPIFlow)
+
 	public.GET(RouteInitBrowserFlow, h.d.SessionHandler().IsNotAuthenticated(h.initBrowserFlow, session.RedirectOnAuthenticated(h.c)))
 	public.GET(RouteInitAPIFlow, h.d.SessionHandler().IsNotAuthenticated(h.initApiFlow,
 		session.RespondWithJSONErrorOnAuthenticated(h.d.Writer(), errors.WithStack(ErrAlreadyLoggedIn))))
diff --git a/selfservice/flow/request.go b/selfservice/flow/request.go
index f434f7c5a..bec6415b0 100644
--- a/selfservice/flow/request.go
+++ b/selfservice/flow/request.go
@@ -39,7 +39,7 @@ func VerifyRequest(
 		return nil
 	default:
 		if !nosurf.VerifyToken(generator(r), actual) {
-			return x.ErrInvalidCSRFToken
+			return errors.WithStack(x.ErrInvalidCSRFToken)
 		}
 	}
 
diff --git a/selfservice/flow/settings/handler.go b/selfservice/flow/settings/handler.go
index f87a684c8..820af5cdf 100644
--- a/selfservice/flow/settings/handler.go
+++ b/selfservice/flow/settings/handler.go
@@ -57,6 +57,7 @@ type (
 		StrategyProvider
 
 		IdentityTraitsSchemas() schema.Schemas
+		x.CSRFProvider
 	}
 	HandlerProvider interface {
 		SettingsHandler() *Handler
@@ -73,6 +74,8 @@ func NewHandler(d handlerDependencies, c configuration.Provider) *Handler {
 }
 
 func (h *Handler) RegisterPublicRoutes(public *x.RouterPublic) {
+	h.d.CSRFHandler().ExemptPath(RouteInitAPIFlow)
+
 	redirect := session.RedirectOnUnauthenticated(h.c.SelfServiceFlowLoginUI().String())
 	public.GET(RouteInitBrowserFlow, h.d.SessionHandler().IsAuthenticated(h.initBrowserFlow, redirect))
 	public.GET(RouteInitAPIFlow, h.d.SessionHandler().IsAuthenticated(h.initApiFlow, nil))
diff --git a/selfservice/flow/verification/handler.go b/selfservice/flow/verification/handler.go
index 8a689cc8c..d171156b8 100644
--- a/selfservice/flow/verification/handler.go
+++ b/selfservice/flow/verification/handler.go
@@ -36,6 +36,7 @@ type (
 		FlowPersistenceProvider
 		ErrorHandlerProvider
 		StrategyProvider
+		x.CSRFProvider
 	}
 	Handler struct {
 		d handlerDependencies
@@ -48,6 +49,8 @@ func NewHandler(d handlerDependencies, c configuration.Provider) *Handler {
 }
 
 func (h *Handler) RegisterPublicRoutes(public *x.RouterPublic) {
+	h.d.CSRFHandler().ExemptPath(RouteInitAPIFlow)
+
 	public.GET(RouteInitBrowserFlow, h.initBrowserFlow)
 	public.GET(RouteInitAPIFlow, h.initAPIFlow)
 	public.GET(RouteGetFlow, h.fetch)
diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go
index 4f704b854..e5dd3e0c0 100644
--- a/selfservice/strategy/password/login.go
+++ b/selfservice/strategy/password/login.go
@@ -113,7 +113,7 @@ func (s *Strategy) handleLogin(w http.ResponseWriter, r *http.Request, _ httprou
 	}
 
 	if err := flow.VerifyRequest(r, ar.Type, s.d.GenerateCSRFToken, p.CSRFToken); err != nil {
-		s.handleLoginError(w, r, ar, &p, x.ErrInvalidCSRFToken)
+		s.handleLoginError(w, r, ar, &p, err)
 		return
 	}
 
diff --git a/x/clean_url.go b/x/clean_url.go
new file mode 100644
index 000000000..f367c18d6
--- /dev/null
+++ b/x/clean_url.go
@@ -0,0 +1,13 @@
+package x
+
+import (
+	"net/http"
+
+	"github.com/julienschmidt/httprouter"
+	"github.com/urfave/negroni"
+)
+
+var CleanPath negroni.HandlerFunc = func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
+	r.URL.Path = httprouter.CleanPath(r.URL.Path)
+	next(rw, r)
+}


Test patch:
diff --git a/internal/testhelpers/httptest.go b/internal/testhelpers/httptest.go
new file mode 100644
index 000000000..9408ab91f
--- /dev/null
+++ b/internal/testhelpers/httptest.go
@@ -0,0 +1,13 @@
+package testhelpers
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+)
+
+func NewHTTPTestServer(t *testing.T, h http.Handler) *httptest.Server {
+	ts := httptest.NewServer(h)
+	t.Cleanup(ts.Close)
+	return ts
+}
diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go
index 699c3ac25..ea0e0e8a2 100644
--- a/selfservice/strategy/password/login_test.go
+++ b/selfservice/strategy/password/login_test.go
@@ -1,6 +1,7 @@
 package password_test
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -179,6 +180,42 @@ func TestCompleteLogin(t *testing.T) {
 			assert.EqualValues(t, http.StatusBadRequest, res.StatusCode)
 			assert.Contains(t, actual, "provided credentials are invalid")
 		})
+
+		t.Run("case=should fail with correct CSRF error cause/type=api", func(t *testing.T) {
+			for k, tc := range []struct {
+				mod func(http.Header)
+				exp string
+			}{
+				{
+					mod: func(h http.Header) {
+						h.Add("Cookie", "name=bar")
+					},
+					exp: "The HTTP Request Header included the \\\"Cookie\\\" key",
+				},
+				{
+					mod: func(h http.Header) {
+						h.Add("Origin", "www.bar.com")
+					},
+					exp: "The HTTP Request Header included the \\\"Origin\\\" key",
+				},
+			} {
+				t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
+					f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false)
+					c := testhelpers.GetLoginFlowMethodConfig(t, f.Payload, identity.CredentialsTypePassword.String())
+
+					req := testhelpers.NewRequest(t, true, "POST", pointerx.StringR(c.Action), bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values)))
+					tc.mod(req.Header)
+
+					res, err := apiClient.Do(req)
+					require.NoError(t, err)
+					defer res.Body.Close()
+
+					actual := string(x.MustReadAll(res.Body))
+					assert.EqualValues(t, http.StatusBadRequest, res.StatusCode)
+					assert.Contains(t, actual, tc.exp)
+				})
+			}
+		})
 	})
 
 	var expectValidationError = func(t *testing.T, isAPI, forced bool, values func(url.Values)) string {
diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go
index 0f3c59fa4..dee055e28 100644
--- a/selfservice/strategy/password/registration_test.go
+++ b/selfservice/strategy/password/registration_test.go
@@ -1,6 +1,7 @@
 package password_test
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"net/http"
@@ -259,6 +260,42 @@ func TestRegistration(t *testing.T) {
 				assert.EqualValues(t, http.StatusOK, res.StatusCode)
 				assert.NotEmpty(t, gjson.Get(actual, "identity.id").Raw, "%s", actual) // registration successful
 			})
+
+			t.Run("case=should fail with correct CSRF error cause/type=api", func(t *testing.T) {
+				for k, tc := range []struct {
+					mod func(http.Header)
+					exp string
+				}{
+					{
+						mod: func(h http.Header) {
+							h.Add("Cookie", "name=bar")
+						},
+						exp: "The HTTP Request Header included the \\\"Cookie\\\" key",
+					},
+					{
+						mod: func(h http.Header) {
+							h.Add("Origin", "www.bar.com")
+						},
+						exp: "The HTTP Request Header included the \\\"Origin\\\" key",
+					},
+				} {
+					t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
+						f := testhelpers.InitializeRegistrationFlowViaAPI(t, apiClient, publicTS)
+						c := testhelpers.GetRegistrationFlowMethodConfig(t, f.Payload, identity.CredentialsTypePassword.String())
+
+						req := testhelpers.NewRequest(t, true, "POST", pointerx.StringR(c.Action), bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values)))
+						tc.mod(req.Header)
+
+						res, err := apiClient.Do(req)
+						require.NoError(t, err)
+						defer res.Body.Close()
+
+						actual := string(x.MustReadAll(res.Body))
+						assert.EqualValues(t, http.StatusBadRequest, res.StatusCode)
+						assert.Contains(t, actual, tc.exp)
+					})
+				}
+			})
 		})
 
 		t.Run("case=should fail because schema did not specify an identifier", func(t *testing.T) {
diff --git a/selfservice/strategy/password/settings_test.go b/selfservice/strategy/password/settings_test.go
index 053b5932c..ccbeafd30 100644
--- a/selfservice/strategy/password/settings_test.go
+++ b/selfservice/strategy/password/settings_test.go
@@ -1,13 +1,17 @@
 package password_test
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"net/url"
 	"strings"
 	"testing"
 
+	"github.com/ory/x/pointerx"
+
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/tidwall/gjson"
@@ -224,6 +228,44 @@ func TestSettings(t *testing.T) {
 		assert.NotEmpty(t, gjson.Get(actual, "identity.id").String(), "%s", actual)
 	})
 
+	t.Run("case=should fail with correct CSRF error cause/type=api", func(t *testing.T) {
+		for k, tc := range []struct {
+			mod func(http.Header)
+			exp string
+		}{
+			{
+				mod: func(h http.Header) {
+					h.Add("Cookie", "name=bar")
+				},
+				exp: "The HTTP Request Header included the \\\"Cookie\\\" key",
+			},
+			{
+				mod: func(h http.Header) {
+					h.Add("Origin", "www.bar.com")
+				},
+				exp: "The HTTP Request Header included the \\\"Origin\\\" key",
+			},
+		} {
+			t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
+				f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
+				c := testhelpers.GetSettingsFlowMethodConfig(t, f.Payload, identity.CredentialsTypePassword.String())
+				values := testhelpers.SDKFormFieldsToURLValues(c.Fields)
+				values.Set("password", x.NewUUID().String())
+
+				req := testhelpers.NewRequest(t, true, "POST", pointerx.StringR(c.Action), bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values)))
+				tc.mod(req.Header)
+
+				res, err := apiUser1.Do(req)
+				require.NoError(t, err)
+				defer res.Body.Close()
+
+				actual := string(x.MustReadAll(res.Body))
+				assert.EqualValues(t, http.StatusBadRequest, res.StatusCode)
+				assert.Contains(t, actual, tc.exp)
+			})
+		}
+	})
+
 	var expectSuccess = func(t *testing.T, isAPI bool, hc *http.Client, values func(url.Values)) string {
 		return testhelpers.SubmitSettingsForm(t, isAPI, hc, publicTS, values,
 			identity.CredentialsTypePassword.String(), http.StatusOK,
diff --git a/selfservice/strategy/profile/strategy_test.go b/selfservice/strategy/profile/strategy_test.go
index 6d3b3fef7..8c73931e7 100644
--- a/selfservice/strategy/profile/strategy_test.go
+++ b/selfservice/strategy/profile/strategy_test.go
@@ -1,8 +1,10 @@
 package profile_test
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
+	"fmt"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
@@ -109,14 +111,50 @@ func TestStrategyTraits(t *testing.T) {
 	})
 
 	t.Run("description=should not fail if CSRF token is invalid/type=api", func(t *testing.T) {
-		rs := testhelpers.InitializeSettingsFlowViaAPI(t, browserUser1, publicTS)
+		rs := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
 		f := testhelpers.GetSettingsFlowMethodConfig(t, rs.Payload, settings.StrategyProfile)
 
-		actual, res := testhelpers.SettingsMakeRequest(t, true, f, browserUser1, `{"traits":{},"csrf_token":"invalid"}`)
+		actual, res := testhelpers.SettingsMakeRequest(t, true, f, apiUser1, `{"traits":{},"csrf_token":"invalid"}`)
 		assert.EqualValues(t, http.StatusBadRequest, res.StatusCode)
 		assert.EqualValues(t, "api", gjson.Get(actual, "type").String())
 	})
 
+	t.Run("case=should fail with correct CSRF error cause/type=api", func(t *testing.T) {
+		for k, tc := range []struct {
+			mod func(http.Header)
+			exp string
+		}{
+			{
+				mod: func(h http.Header) {
+					h.Add("Cookie", "name=bar")
+				},
+				exp: "The HTTP Request Header included the \\\"Cookie\\\" key",
+			},
+			{
+				mod: func(h http.Header) {
+					h.Add("Origin", "www.bar.com")
+				},
+				exp: "The HTTP Request Header included the \\\"Origin\\\" key",
+			},
+		} {
+			t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
+				f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
+				c := testhelpers.GetSettingsFlowMethodConfig(t, f.Payload, identity.CredentialsTypePassword.String())
+
+				req := testhelpers.NewRequest(t, true, "POST", pointerx.StringR(c.Action), bytes.NewBufferString(`{"traits":{},"csrf_token":"invalid"}`))
+				tc.mod(req.Header)
+
+				res, err := apiUser1.Do(req)
+				require.NoError(t, err)
+				defer res.Body.Close()
+
+				actual := string(x.MustReadAll(res.Body))
+				assert.EqualValues(t, http.StatusBadRequest, res.StatusCode)
+				assert.Contains(t, actual, tc.exp)
+			})
+		}
+	})
+
 	t.Run("description=hydrate the proper fields", func(t *testing.T) {
 		var run = func(t *testing.T, id *identity.Identity, payload *models.SettingsFlow, route string) {
 			assert.NotEmpty(t, payload.Identity)
diff --git a/test/e2e/profiles/recovery/.kratos.yml b/test/e2e/profiles/recovery/.kratos.yml
index adcb18de4..6ad587fd8 100644
--- a/test/e2e/profiles/recovery/.kratos.yml
+++ b/test/e2e/profiles/recovery/.kratos.yml
@@ -2,6 +2,8 @@ selfservice:
   methods:
     password:
       enabled: true
+    link:
+      enabled: true
 
   flows:
     settings:
diff --git a/test/e2e/profiles/verification/.kratos.yml b/test/e2e/profiles/verification/.kratos.yml
index c548a6d18..c11febf92 100644
--- a/test/e2e/profiles/verification/.kratos.yml
+++ b/test/e2e/profiles/verification/.kratos.yml
@@ -2,6 +2,8 @@ selfservice:
   methods:
     password:
       enabled: true
+    link:
+      enabled: true
 
   flows:
     settings:
diff --git a/test/schema/fixtures/config.schema.test.success/root.full.yml b/test/schema/fixtures/config.schema.test.success/root.full.yml
index c655ffcea..53dc8af46 100644
--- a/test/schema/fixtures/config.schema.test.success/root.full.yml
+++ b/test/schema/fixtures/config.schema.test.success/root.full.yml
@@ -46,7 +46,7 @@ selfservice:
   methods:
     profile:
       enabled: true
-    recovery_token:
+    link:
       enabled: true
     password:
       enabled: true
diff --git a/x/clean_url_test.go b/x/clean_url_test.go
new file mode 100644
index 000000000..e8c0e0fbf
--- /dev/null
+++ b/x/clean_url_test.go
@@ -0,0 +1,35 @@
+package x
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/urfave/negroni"
+)
+
+func TestCleanPath(t *testing.T) {
+	n := negroni.New(CleanPath)
+	n.UseHandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(r.URL.String()))
+	})
+	ts := httptest.NewServer(n)
+	defer ts.Close()
+
+	for k, tc := range [][]string{
+		{"//foo", "/foo"},
+		{"//foo//bar", "/foo/bar"},
+	} {
+		t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
+			res, err := ts.Client().Get(ts.URL + tc[0])
+			require.NoError(t, err)
+			defer res.Body.Close()
+			body, err := ioutil.ReadAll(res.Body)
+			assert.Equal(t, string(body), tc[1])
+		})
+	}
+}