Repository: ory/kratos
Pull request #248 (2020-02-14 14:31:35)

feat(selfservice/login): enable reauthentication functionality
## Related issue
contributes to #243 
<!--
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 necessary documentation within the code base (if appropriate)
- [ ] I have documented my changes in the [developer guide](https://github.com/ory/docs) (if appropriate)

## 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/CHANGELOG.md b/CHANGELOG.md
index 5a401816f..b5eacb4cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 - [Changelog](#changelog)
   - [Unreleased](#unreleased)
+  - [v0.1.1-alpha.1 (2020-02-18)](#v011-alpha1-2020-02-18)
   - [v0.1.0-alpha.6 (2020-02-16)](#v010-alpha6-2020-02-16)
   - [v0.1.0-alpha.5 (2020-02-06)](#v010-alpha5-2020-02-06)
   - [v0.1.0-alpha.4 (2020-02-06)](#v010-alpha4-2020-02-06)
@@ -42,7 +43,27 @@
 
 ## [Unreleased](https://github.com/ory/kratos/tree/HEAD)
 
-[Full Changelog](https://github.com/ory/kratos/compare/v0.1.0-alpha.6...HEAD)
+[Full Changelog](https://github.com/ory/kratos/compare/v0.1.1-alpha.1...HEAD)
+
+**Fixed bugs:**
+
+- feat: Enable CockroachDB integration [\#260](https://github.com/ory/kratos/pull/260) ([aeneasr](https://github.com/aeneasr))
+
+**Security fixes:**
+
+- Implement Password Strength Meter API [\#136](https://github.com/ory/kratos/issues/136)
+
+**Closed issues:**
+
+- Enable CockroachDB test suite and integration [\#132](https://github.com/ory/kratos/issues/132)
+
+**Merged pull requests:**
+
+- docs: Typo in README.md [\#265](https://github.com/ory/kratos/pull/265) ([cuttlefish](https://github.com/cuttlefish))
+
+## [v0.1.1-alpha.1](https://github.com/ory/kratos/tree/v0.1.1-alpha.1) (2020-02-18)
+
+[Full Changelog](https://github.com/ory/kratos/compare/v0.1.0-alpha.6...v0.1.1-alpha.1)
 
 **Fixed bugs:**
 
@@ -50,6 +71,7 @@
 
 **Merged pull requests:**
 
+- fix: Update verify URLs [\#258](https://github.com/ory/kratos/pull/258) ([aeneasr](https://github.com/aeneasr))
 - fix: Clean up docker quickstart [\#255](https://github.com/ory/kratos/pull/255) ([aeneasr](https://github.com/aeneasr))
 - refactor\(persistence/sql\): move connection to context to enable transactions [\#254](https://github.com/ory/kratos/pull/254) ([zepatrik](https://github.com/zepatrik))
 - fix: Add verify return to address [\#252](https://github.com/ory/kratos/pull/252) ([aeneasr](https://github.com/aeneasr))
@@ -114,6 +136,10 @@
 
 - Rework errors API [\#204](https://github.com/ory/kratos/issues/204)
 
+**Fixed bugs:**
+
+- refactor!: Improve user-facing error APIs [\#219](https://github.com/ory/kratos/pull/219) ([aeneasr](https://github.com/aeneasr))
+
 **Closed issues:**
 
 - Discrepancy in documentation [\#218](https://github.com/ory/kratos/issues/218)
@@ -158,7 +184,6 @@
 - Rework public and admin fetch strategy [\#203](https://github.com/ory/kratos/pull/203) ([aeneasr](https://github.com/aeneasr))
 - Update HTTP routes for a consistent API naming [\#199](https://github.com/ory/kratos/pull/199) ([aeneasr](https://github.com/aeneasr))
 - ss: Use JSON Schema to type assert form body [\#116](https://github.com/ory/kratos/pull/116) ([aeneasr](https://github.com/aeneasr))
-- errorx: Add SQL adapter [\#100](https://github.com/ory/kratos/pull/100) ([aeneasr](https://github.com/aeneasr))
 
 **Fixed bugs:**
 
@@ -233,10 +258,6 @@
 
 [Full Changelog](https://github.com/ory/kratos/compare/v0.0.3-alpha.10...v0.0.3-alpha.11)
 
-**Fixed bugs:**
-
-- refactor!: Improve user-facing error APIs [\#219](https://github.com/ory/kratos/pull/219) ([aeneasr](https://github.com/aeneasr))
-
 **Merged pull requests:**
 
 - Improve field types [\#209](https://github.com/ory/kratos/pull/209) ([zepatrik](https://github.com/zepatrik))
@@ -290,7 +311,6 @@
 - Update quickstart [\#207](https://github.com/ory/kratos/pull/207) ([aeneasr](https://github.com/aeneasr))
 - Take field order from schema [\#205](https://github.com/ory/kratos/pull/205) ([zepatrik](https://github.com/zepatrik))
 - ss/profile: Use request ID as query param everywhere [\#202](https://github.com/ory/kratos/pull/202) ([aeneasr](https://github.com/aeneasr))
-- Make form fields an array [\#197](https://github.com/ory/kratos/pull/197) ([zepatrik](https://github.com/zepatrik))
 
 ## [v0.0.1-alpha.9](https://github.com/ory/kratos/tree/v0.0.1-alpha.9) (2020-01-29)
 
@@ -330,6 +350,7 @@
 
 **Merged pull requests:**
 
+- Make form fields an array [\#197](https://github.com/ory/kratos/pull/197) ([zepatrik](https://github.com/zepatrik))
 - Resolve build issues with CGO [\#196](https://github.com/ory/kratos/pull/196) ([aeneasr](https://github.com/aeneasr))
 
 ## [v0.0.1-alpha.3](https://github.com/ory/kratos/tree/v0.0.1-alpha.3) (2020-01-28)
diff --git a/Makefile b/Makefile
index 372418507..ca44df50d 100644
--- a/Makefile
+++ b/Makefile
@@ -62,7 +62,7 @@ test-resetdb:
 
 .PHONY: test
 test: test-resetdb
-		source scripts/test-env.sh && go test -tags sqlite ./...
+		source scripts/test-envs.sh && go test -tags sqlite ./...
 
 # Generates the SDKs
 .PHONY: sdk
diff --git a/README.md b/README.md
index 6ab0a2a22..0c68ea683 100644
--- a/README.md
+++ b/README.md
@@ -256,7 +256,7 @@ to perform a certain action on a resource.
 
 ## Security
 
-Running identity infrastructure requires [attention and knowledge of thread models](https://www.ory.sh/docs/next/kratos/concepts/security).
+Running identity infrastructure requires [attention and knowledge of threat models](https://www.ory.sh/docs/next/kratos/concepts/security).
 
 ### Disclosing vulnerabilities
 
diff --git a/contrib/quickstart/oathkeeper/access-rules.yml b/contrib/quickstart/oathkeeper/access-rules.yml
index 260dd0c09..7fa243166 100644
--- a/contrib/quickstart/oathkeeper/access-rules.yml
+++ b/contrib/quickstart/oathkeeper/access-rules.yml
@@ -26,7 +26,7 @@
     preserve_host: true
     url: "http://kratos-selfservice-ui-node:4435"
   match:
-    url: "http://127.0.0.1:4455/<(error|auth/login|auth/registration|(.+\\.css))(|/)>"
+    url: "http://127.0.0.1:4455/<(error|verify|auth/login|auth/registration|(.+\\.css))(|/)>"
     methods:
       - GET
   authenticators:
@@ -44,7 +44,7 @@
     preserve_host: true
     url: "http://kratos-selfservice-ui-node:4435"
   match:
-    url: "http://127.0.0.1:4455<(/debug|/|/dashboard|/profile|/verify|)(|/)>"
+    url: "http://127.0.0.1:4455<(/debug|/|/dashboard|/profile|)(|/)>"
     methods:
       - GET
   authenticators:
diff --git a/docs/api.swagger.json b/docs/api.swagger.json
index be033441b..5f1b7dbe3 100755
--- a/docs/api.swagger.json
+++ b/docs/api.swagger.json
@@ -667,13 +667,9 @@
         }
       }
     },
-    "/self-service/browser/flows/verification/complete": {
-      "post": {
-        "description": "This endpoint completes a browser-based profile management flow. This is usually achieved by POSTing data to this\nendpoint.\n\nIf the provided profile data is valid against the Identity's Traits JSON Schema, the data will be updated and\nthe browser redirected to `url.profile_ui` for further steps.\n\n\u003e This endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...) and HTML Forms.\n\nMore information can be found at [ORY Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/selfservice/flows/verify-email-account-activation).",
-        "consumes": [
-          "application/json",
-          "application/x-www-form-urlencoded"
-        ],
+    "/self-service/browser/flows/verification/init/{via}": {
+      "get": {
+        "description": "This endpoint initializes a browser-based profile management flow. Once initialized, the browser will be redirected to\n`urls.profile_ui` with the request ID set as a query parameter. If no valid user session exists, a login\nflow will be initialized.\n\n\u003e This endpoint is NOT INTENDED for API clients and only works\nwith browsers (Chrome, Firefox, ...).\n\nMore information can be found at [ORY Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/selfservice/flows/verify-email-account-activation).",
         "schemes": [
           "http",
           "https"
@@ -681,14 +677,14 @@
         "tags": [
           "public"
         ],
-        "summary": "Complete the browser-based profile management flows",
-        "operationId": "completeSelfServiceBrowserVerificationFlow",
+        "summary": "Initialize browser-based verification flow",
+        "operationId": "initializeSelfServiceBrowserVerificationFlow",
         "parameters": [
           {
             "type": "string",
-            "description": "Request is the Request ID\n\nThe value for this parameter comes from `request` URL Query parameter sent to your\napplication (e.g. `/verify?request=abcde`).",
-            "name": "request",
-            "in": "query",
+            "description": "What to verify\n\nCurrently only \"email\" is supported.",
+            "name": "via",
+            "in": "path",
             "required": true
           }
         ],
@@ -705,9 +701,13 @@
         }
       }
     },
-    "/self-service/browser/flows/verification/init/{via}": {
-      "get": {
-        "description": "This endpoint initializes a browser-based profile management flow. Once initialized, the browser will be redirected to\n`urls.profile_ui` with the request ID set as a query parameter. If no valid user session exists, a login\nflow will be initialized.\n\n\u003e This endpoint is NOT INTENDED for API clients and only works\nwith browsers (Chrome, Firefox, ...).\n\nMore information can be found at [ORY Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/selfservice/flows/verify-email-account-activation).",
+    "/self-service/browser/flows/verification/{via}/complete": {
+      "post": {
+        "description": "This endpoint completes a browser-based profile management flow. This is usually achieved by POSTing data to this\nendpoint.\n\nIf the provided profile data is valid against the Identity's Traits JSON Schema, the data will be updated and\nthe browser redirected to `url.profile_ui` for further steps.\n\n\u003e This endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...) and HTML Forms.\n\nMore information can be found at [ORY Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/selfservice/flows/verify-email-account-activation).",
+        "consumes": [
+          "application/json",
+          "application/x-www-form-urlencoded"
+        ],
         "schemes": [
           "http",
           "https"
@@ -715,9 +715,16 @@
         "tags": [
           "public"
         ],
-        "summary": "Initialize browser-based verification flow",
-        "operationId": "initializeSelfServiceBrowserVerificationFlow",
+        "summary": "Complete the browser-based profile management flows",
+        "operationId": "completeSelfServiceBrowserVerificationFlow",
         "parameters": [
+          {
+            "type": "string",
+            "description": "Request is the Request ID\n\nThe value for this parameter comes from `request` URL Query parameter sent to your\napplication (e.g. `/verify?request=abcde`).",
+            "name": "request",
+            "in": "query",
+            "required": true
+          },
           {
             "type": "string",
             "description": "What to verify\n\nCurrently only \"email\" is supported.",
@@ -1170,6 +1177,10 @@
           "type": "string",
           "format": "date-time"
         },
+        "forced": {
+          "description": "Forced stores whether this login request should enforce reauthentication.",
+          "type": "boolean"
+        },
         "id": {
           "$ref": "#/definitions/UUID"
         },
diff --git a/go.mod b/go.mod
index a2fcf8dc6..b8c1f2e1c 100644
--- a/go.mod
+++ b/go.mod
@@ -34,6 +34,7 @@ require (
 	github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2
 	github.com/golang/mock v1.3.1
 	github.com/google/go-github/v27 v27.0.1
+	github.com/google/go-querystring v1.0.0
 	github.com/google/uuid v1.1.1
 	github.com/gorilla/context v1.1.1
 	github.com/gorilla/securecookie v1.1.1
diff --git a/identity/pool.go b/identity/pool.go
index 8502c195e..8ee12241a 100644
--- a/identity/pool.go
+++ b/identity/pool.go
@@ -405,12 +405,12 @@ func TestPool(p PrivilegedPool) func(t *testing.T) {
 				}
 
 				compare := func(t *testing.T, expected, actual VerifiableAddress) {
-					actual.CreatedAt = actual.CreatedAt.UTC().Round(time.Hour * 24)
-					actual.UpdatedAt = actual.UpdatedAt.UTC().Round(time.Hour * 24)
-					actual.ExpiresAt = actual.ExpiresAt.UTC().Round(time.Hour * 24)
-					expected.CreatedAt = expected.CreatedAt.UTC().Round(time.Hour * 24)
-					expected.UpdatedAt = expected.UpdatedAt.UTC().Round(time.Hour * 24)
-					expected.ExpiresAt = expected.ExpiresAt.UTC().Round(time.Hour * 24)
+					actual.CreatedAt = actual.CreatedAt.UTC().Truncate(time.Hour * 24)
+					actual.UpdatedAt = actual.UpdatedAt.UTC().Truncate(time.Hour * 24)
+					actual.ExpiresAt = actual.ExpiresAt.UTC().Truncate(time.Hour * 24)
+					expected.CreatedAt = expected.CreatedAt.UTC().Truncate(time.Hour * 24)
+					expected.UpdatedAt = expected.UpdatedAt.UTC().Truncate(time.Hour * 24)
+					expected.ExpiresAt = expected.ExpiresAt.UTC().Truncate(time.Hour * 24)
 					assert.EqualValues(t, expected, actual)
 				}
 
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 679379f25..c0783b3d6 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
@@ -69,6 +69,13 @@ type CompleteSelfServiceBrowserVerificationFlowParams struct {
 
 	*/
 	Request string
+	/*Via
+	  What to verify
+
+	Currently only "email" is supported.
+
+	*/
+	Via string
 
 	timeout    time.Duration
 	Context    context.Context
@@ -119,6 +126,17 @@ func (o *CompleteSelfServiceBrowserVerificationFlowParams) SetRequest(request st
 	o.Request = request
 }
 
+// WithVia adds the via to the complete self service browser verification flow params
+func (o *CompleteSelfServiceBrowserVerificationFlowParams) WithVia(via string) *CompleteSelfServiceBrowserVerificationFlowParams {
+	o.SetVia(via)
+	return o
+}
+
+// SetVia adds the via to the complete self service browser verification flow params
+func (o *CompleteSelfServiceBrowserVerificationFlowParams) SetVia(via string) {
+	o.Via = via
+}
+
 // WriteToRequest writes these params to a swagger request
 func (o *CompleteSelfServiceBrowserVerificationFlowParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
 
@@ -136,6 +154,11 @@ func (o *CompleteSelfServiceBrowserVerificationFlowParams) WriteToRequest(r runt
 		}
 	}
 
+	// path param via
+	if err := r.SetPathParam("via", o.Via); err != nil {
+		return err
+	}
+
 	if len(res) > 0 {
 		return errors.CompositeValidationError(res...)
 	}
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 b66207355..c9e48fe53 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
@@ -56,7 +56,7 @@ type CompleteSelfServiceBrowserVerificationFlowFound struct {
 }
 
 func (o *CompleteSelfServiceBrowserVerificationFlowFound) Error() string {
-	return fmt.Sprintf("[POST /self-service/browser/flows/verification/complete][%d] completeSelfServiceBrowserVerificationFlowFound ", 302)
+	return fmt.Sprintf("[POST /self-service/browser/flows/verification/{via}/complete][%d] completeSelfServiceBrowserVerificationFlowFound ", 302)
 }
 
 func (o *CompleteSelfServiceBrowserVerificationFlowFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
@@ -78,7 +78,7 @@ type CompleteSelfServiceBrowserVerificationFlowInternalServerError struct {
 }
 
 func (o *CompleteSelfServiceBrowserVerificationFlowInternalServerError) Error() string {
-	return fmt.Sprintf("[POST /self-service/browser/flows/verification/complete][%d] completeSelfServiceBrowserVerificationFlowInternalServerError  %+v", 500, o.Payload)
+	return fmt.Sprintf("[POST /self-service/browser/flows/verification/{via}/complete][%d] completeSelfServiceBrowserVerificationFlowInternalServerError  %+v", 500, o.Payload)
 }
 
 func (o *CompleteSelfServiceBrowserVerificationFlowInternalServerError) GetPayload() *models.GenericError {
diff --git a/internal/httpclient/client/public/public_client.go b/internal/httpclient/client/public/public_client.go
index f2382ed65..db84fb456 100644
--- a/internal/httpclient/client/public/public_client.go
+++ b/internal/httpclient/client/public/public_client.go
@@ -107,7 +107,7 @@ func (a *Client) CompleteSelfServiceBrowserVerificationFlow(params *CompleteSelf
 	_, err := a.transport.Submit(&runtime.ClientOperation{
 		ID:                 "completeSelfServiceBrowserVerificationFlow",
 		Method:             "POST",
-		PathPattern:        "/self-service/browser/flows/verification/complete",
+		PathPattern:        "/self-service/browser/flows/verification/{via}/complete",
 		ProducesMediaTypes: []string{"application/json"},
 		ConsumesMediaTypes: []string{"application/json", "application/x-www-form-urlencoded"},
 		Schemes:            []string{"http", "https"},
diff --git a/internal/httpclient/models/login_request.go b/internal/httpclient/models/login_request.go
index 06497aa6e..6ac167cd2 100644
--- a/internal/httpclient/models/login_request.go
+++ b/internal/httpclient/models/login_request.go
@@ -25,6 +25,9 @@ type LoginRequest struct {
 	// Format: date-time
 	ExpiresAt *strfmt.DateTime `json:"expires_at"`
 
+	// Forced stores whether this login request should enforce reauthentication.
+	Forced bool `json:"forced,omitempty"`
+
 	// id
 	// Required: true
 	// Format: uuid4
diff --git a/persistence/sql/migrations/20191100000010_login_request_forced.down.fizz b/persistence/sql/migrations/20191100000010_login_request_forced.down.fizz
new file mode 100644
index 000000000..43e866fe0
--- /dev/null
+++ b/persistence/sql/migrations/20191100000010_login_request_forced.down.fizz
@@ -0,0 +1,1 @@
+drop_column("selfservice_login_requests", "forced")
diff --git a/persistence/sql/migrations/20191100000010_login_request_forced.up.fizz b/persistence/sql/migrations/20191100000010_login_request_forced.up.fizz
new file mode 100644
index 000000000..c02f2c8cf
--- /dev/null
+++ b/persistence/sql/migrations/20191100000010_login_request_forced.up.fizz
@@ -0,0 +1,1 @@
+add_column("selfservice_login_requests", "forced", "bool", {default: false})
diff --git a/persistence/sql/persister_login.go b/persistence/sql/persister_login.go
index ff7af73b9..c76593745 100644
--- a/persistence/sql/persister_login.go
+++ b/persistence/sql/persister_login.go
@@ -33,7 +33,20 @@ func (p *Persister) GetLoginRequest(ctx context.Context, id uuid.UUID) (*login.R
 	return &r, nil
 }
 
-func (p *Persister) UpdateLoginRequest(ctx context.Context, id uuid.UUID, ct identity.CredentialsType, rm *login.RequestMethod) error {
+func (p *Persister) MarkRequestForced(ctx context.Context, id uuid.UUID) error {
+	return p.Transaction(ctx, func(tx *pop.Connection) error {
+		ctx := WithTransaction(ctx, tx)
+		lr, err := p.GetLoginRequest(ctx, id)
+		if err != nil {
+			return err
+		}
+
+		lr.Forced = true
+		return tx.Save(lr)
+	})
+}
+
+func (p *Persister) UpdateLoginRequestMethod(ctx context.Context, id uuid.UUID, ct identity.CredentialsType, rm *login.RequestMethod) error {
 	return p.Transaction(ctx, func(tx *pop.Connection) error {
 		ctx := WithTransaction(ctx, tx)
 		rr, err := p.GetLoginRequest(ctx, id)
diff --git a/selfservice/flow/login/error.go b/selfservice/flow/login/error.go
index 6f60439a8..f08e06551 100644
--- a/selfservice/flow/login/error.go
+++ b/selfservice/flow/login/error.go
@@ -1,7 +1,6 @@
 package login
 
 import (
-	"context"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -81,7 +80,7 @@ func (s *ErrorHandler) HandleLoginError(
 		if err = s.d.LoginHandler().NewLoginRequest(w, r, func(a *Request) (string, error) {
 			for name, method := range a.Methods {
 				method.Config.AddError(&form.Error{Message: "Your session expired, please try again."})
-				if err := s.d.LoginRequestPersister().UpdateLoginRequest(context.TODO(), a.ID, name, method); err != nil {
+				if err := s.d.LoginRequestPersister().UpdateLoginRequestMethod(r.Context(), a.ID, name, method); err != nil {
 					return s.d.SelfServiceErrorManager().Create(r.Context(), w, r, err)
 				}
 				a.Methods[name] = method
@@ -114,7 +113,7 @@ func (s *ErrorHandler) HandleLoginError(
 		return
 	}
 
-	if err := s.d.LoginRequestPersister().UpdateLoginRequest(r.Context(), rr.ID, ct, method); err != nil {
+	if err := s.d.LoginRequestPersister().UpdateLoginRequestMethod(r.Context(), rr.ID, ct, method); err != nil {
 		s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
 		return
 	}
diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go
index 98cf9f568..beaefc1f6 100644
--- a/selfservice/flow/login/handler.go
+++ b/selfservice/flow/login/handler.go
@@ -29,6 +29,7 @@ type (
 		errorx.ManagementProvider
 		StrategyProvider
 		session.HandlerProvider
+		session.ManagementProvider
 		x.WriterProvider
 	}
 	HandlerProvider interface {
@@ -50,7 +51,7 @@ func (h *Handler) WithTokenGenerator(f func(r *http.Request) string) {
 }
 
 func (h *Handler) RegisterPublicRoutes(public *x.RouterPublic) {
-	public.GET(BrowserLoginPath, h.d.SessionHandler().IsNotAuthenticated(h.initLoginRequest, session.RedirectOnAuthenticated(h.c)))
+	public.GET(BrowserLoginPath, h.initLoginRequest)
 	public.GET(BrowserLoginRequestsPath, h.publicFetchLoginRequest)
 }
 
@@ -110,6 +111,12 @@ func (h *Handler) NewLoginRequest(w http.ResponseWriter, r *http.Request, redir
 //       500: genericError
 func (h *Handler) initLoginRequest(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
 	if err := h.NewLoginRequest(w, r, func(a *Request) (string, error) {
+		// we assume an error means the user has no session
+		if _, err := h.d.SessionManager().FetchFromRequest(r.Context(), w, r); err == nil && r.URL.Query().Get("prompt") == "login" {
+			if err := h.d.LoginRequestPersister().MarkRequestForced(r.Context(), a.ID); err != nil {
+				return "", err
+			}
+		}
 		return urlx.CopyWithQuery(h.c.LoginURL(), url.Values{"request": {a.ID.String()}}).String(), nil
 	}); err != nil {
 		h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
diff --git a/selfservice/flow/login/persistence.go b/selfservice/flow/login/persistence.go
index 2d0d0b1d0..418404aca 100644
--- a/selfservice/flow/login/persistence.go
+++ b/selfservice/flow/login/persistence.go
@@ -18,7 +18,8 @@ type (
 	RequestPersister interface {
 		CreateLoginRequest(context.Context, *Request) error
 		GetLoginRequest(context.Context, uuid.UUID) (*Request, error)
-		UpdateLoginRequest(context.Context, uuid.UUID, identity.CredentialsType, *RequestMethod) error
+		UpdateLoginRequestMethod(context.Context, uuid.UUID, identity.CredentialsType, *RequestMethod) error
+		MarkRequestForced(ctx context.Context, id uuid.UUID) error
 	}
 	RequestPersistenceProvider interface {
 		LoginRequestPersister() RequestPersister
@@ -97,12 +98,12 @@ func TestRequestPersister(p RequestPersister) func(t *testing.T) {
 			require.NoError(t, err)
 			assert.Len(t, actual.Methods, 1)
 
-			require.NoError(t, p.UpdateLoginRequest(context.Background(), expected.ID, identity.CredentialsTypeOIDC, &RequestMethod{
+			require.NoError(t, p.UpdateLoginRequestMethod(context.Background(), expected.ID, identity.CredentialsTypeOIDC, &RequestMethod{
 				Method: identity.CredentialsTypeOIDC,
 				Config: &RequestMethodConfig{RequestMethodConfigurator: form.NewHTMLForm(string(identity.CredentialsTypeOIDC))},
 			}))
 
-			require.NoError(t, p.UpdateLoginRequest(context.Background(), expected.ID, identity.CredentialsTypePassword, &RequestMethod{
+			require.NoError(t, p.UpdateLoginRequestMethod(context.Background(), expected.ID, identity.CredentialsTypePassword, &RequestMethod{
 				Method: identity.CredentialsTypePassword,
 				Config: &RequestMethodConfig{RequestMethodConfigurator: form.NewHTMLForm(string(identity.CredentialsTypePassword))},
 			}))
diff --git a/selfservice/flow/login/request.go b/selfservice/flow/login/request.go
index 47192ba24..56634484a 100644
--- a/selfservice/flow/login/request.go
+++ b/selfservice/flow/login/request.go
@@ -1,9 +1,13 @@
 package login
 
 import (
+	"context"
 	"net/http"
+	"testing"
 	"time"
 
+	"github.com/stretchr/testify/require"
+
 	"github.com/gobuffalo/pop/v5"
 	"github.com/gofrs/uuid"
 	"github.com/pkg/errors"
@@ -61,6 +65,9 @@ type Request struct {
 
 	// CSRFToken contains the anti-csrf token associated with this request.
 	CSRFToken string `json:"-" db:"csrf_token"`
+
+	// Forced stores whether this login request should enforce reauthentication.
+	Forced bool `json:"forced" db:"forced"`
 }
 
 func NewLoginRequest(exp time.Duration, csrf string, r *http.Request) *Request {
@@ -130,3 +137,20 @@ func (r *Request) Valid() error {
 func (r *Request) GetID() uuid.UUID {
 	return r.ID
 }
+
+func (r *Request) IsForced() bool {
+	return r.Forced
+}
+
+type testRequestHandlerDependencies interface {
+	RequestPersistenceProvider
+	x.WriterProvider
+}
+
+func TestRequestHandler(t *testing.T, reg testRequestHandlerDependencies) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		e, err := reg.LoginRequestPersister().GetLoginRequest(context.Background(), x.ParseUUID(r.URL.Query().Get("request")))
+		require.NoError(t, err)
+		reg.Writer().Write(w, r, e)
+	}
+}
diff --git a/selfservice/flow/registration/request.go b/selfservice/flow/registration/request.go
index 491b72cc9..5b7ee928b 100644
--- a/selfservice/flow/registration/request.go
+++ b/selfservice/flow/registration/request.go
@@ -122,6 +122,10 @@ func (r *Request) GetID() uuid.UUID {
 	return r.ID
 }
 
+func (r *Request) IsForced() bool {
+	return false
+}
+
 func (r *Request) Valid() error {
 	if r.ExpiresAt.Before(time.Now()) {
 		return errors.WithStack(newRequestExpiredError(time.Since(r.ExpiresAt)))
diff --git a/selfservice/flow/verify/handler.go b/selfservice/flow/verify/handler.go
index 5e7816b93..a8f32fd89 100644
--- a/selfservice/flow/verify/handler.go
+++ b/selfservice/flow/verify/handler.go
@@ -3,6 +3,7 @@ package verify
 import (
 	"net/http"
 	"net/url"
+	"strings"
 
 	"github.com/julienschmidt/httprouter"
 	"github.com/justinas/nosurf"
@@ -25,7 +26,7 @@ import (
 
 const (
 	PublicVerificationInitPath     = "/self-service/browser/flows/verification/:via"
-	PublicVerificationCompletePath = "/self-service/browser/flows/verification/complete"
+	PublicVerificationCompletePath = "/self-service/browser/flows/verification/:via/complete"
 	PublicVerificationRequestPath  = "/self-service/browser/flows/requests/verification"
 	PublicVerificationConfirmPath  = "/self-service/browser/flows/verification/:via/confirm/:code"
 )
@@ -105,7 +106,7 @@ func (h *Handler) init(w http.ResponseWriter, r *http.Request, ps httprouter.Par
 
 	a := NewRequest(
 		h.c.SelfServiceProfileRequestLifespan(), r, via,
-		urlx.AppendPaths(h.c.SelfPublicURL(), PublicVerificationCompletePath), h.d.GenerateCSRFToken,
+		urlx.AppendPaths(h.c.SelfPublicURL(), strings.ReplaceAll(PublicVerificationCompletePath, ":via", string(via))), h.d.GenerateCSRFToken,
 	)
 
 	if err := h.d.VerificationPersister().CreateVerifyRequest(r.Context(), a); err != nil {
@@ -192,9 +193,17 @@ type completeSelfServiceBrowserVerificationFlowParameters struct {
 	// required: true
 	// in: query
 	Request string `json:"request"`
+
+	// What to verify
+	//
+	// Currently only "email" is supported.
+	//
+	// required: true
+	// in: path
+	Via string `json:"via"`
 }
 
-// swagger:route POST /self-service/browser/flows/verification/complete public completeSelfServiceBrowserVerificationFlow
+// swagger:route POST /self-service/browser/flows/verification/{via}/complete public completeSelfServiceBrowserVerificationFlow
 //
 // Complete the browser-based profile management flows
 //
@@ -217,7 +226,12 @@ type completeSelfServiceBrowserVerificationFlowParameters struct {
 //     Responses:
 //       302: emptyResponse
 //       500: genericError
-func (h *Handler) complete(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+func (h *Handler) complete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+	if _, err := h.toVia(ps); err != nil {
+		h.handleError(w, r, nil, err)
+		return
+	}
+
 	if err := r.ParseForm(); err != nil {
 		h.handleError(w, r, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to parse the request: %s", err)))
 		return
@@ -325,7 +339,7 @@ func (h *Handler) verify(w http.ResponseWriter, r *http.Request, ps httprouter.P
 		if errorsx.Cause(err) == sqlcon.ErrNoRows {
 			a := NewRequest(
 				h.c.SelfServiceProfileRequestLifespan(), r, via,
-				urlx.AppendPaths(h.c.SelfPublicURL(), PublicVerificationCompletePath), h.d.GenerateCSRFToken,
+				urlx.AppendPaths(h.c.SelfPublicURL(), strings.ReplaceAll(PublicVerificationCompletePath, ":via", string(via))), h.d.GenerateCSRFToken,
 			)
 			a.Form.AddError(&form.Error{Message: "The verification code has expired or was otherwise invalid. Please request another code."})
 
diff --git a/selfservice/strategy/oidc/provider.go b/selfservice/strategy/oidc/provider.go
index 7424909fd..cab9d472d 100644
--- a/selfservice/strategy/oidc/provider.go
+++ b/selfservice/strategy/oidc/provider.go
@@ -10,6 +10,7 @@ type Provider interface {
 	Config() *Configuration
 	OAuth2(ctx context.Context) (*oauth2.Config, error)
 	Claims(ctx context.Context, exchange *oauth2.Token) (*Claims, error)
+	AuthCodeURLOptions(r request) []oauth2.AuthCodeOption
 }
 
 type Claims struct {
diff --git a/selfservice/strategy/oidc/provider_generic_oidc.go b/selfservice/strategy/oidc/provider_generic_oidc.go
index e3a26893c..f26d17420 100644
--- a/selfservice/strategy/oidc/provider_generic_oidc.go
+++ b/selfservice/strategy/oidc/provider_generic_oidc.go
@@ -68,6 +68,15 @@ func (g *ProviderGenericOIDC) OAuth2(ctx context.Context) (*oauth2.Config, error
 	}, nil
 }
 
+func (g *ProviderGenericOIDC) AuthCodeURLOptions(r request) []oauth2.AuthCodeOption {
+	if r.IsForced() {
+		return []oauth2.AuthCodeOption{
+			oauth2.SetAuthURLParam("prompt", "login"),
+		}
+	}
+	return []oauth2.AuthCodeOption{}
+}
+
 func (g *ProviderGenericOIDC) Claims(ctx context.Context, exchange *oauth2.Token) (*Claims, error) {
 	raw, ok := exchange.Extra("id_token").(string)
 	if !ok || len(raw) == 0 {
diff --git a/selfservice/strategy/oidc/provider_github.go b/selfservice/strategy/oidc/provider_github.go
index c2b17c98a..9f9c6545f 100644
--- a/selfservice/strategy/oidc/provider_github.go
+++ b/selfservice/strategy/oidc/provider_github.go
@@ -50,6 +50,10 @@ func (g *ProviderGitHub) OAuth2(ctx context.Context) (*oauth2.Config, error) {
 	return g.oauth2(), nil
 }
 
+func (g *ProviderGitHub) AuthCodeURLOptions(r request) []oauth2.AuthCodeOption {
+	return []oauth2.AuthCodeOption{}
+}
+
 func (g *ProviderGitHub) Claims(ctx context.Context, exchange *oauth2.Token) (*Claims, error) {
 	grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), ",")
 	for _, check := range g.Config().Scope {
diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go
index dcb4c87dd..002a5e14f 100644
--- a/selfservice/strategy/oidc/strategy.go
+++ b/selfservice/strategy/oidc/strategy.go
@@ -103,15 +103,15 @@ func (s *Strategy) RegisterRegistrationRoutes(r *x.RouterPublic) {
 
 func (s *Strategy) setRoutes(r *x.RouterPublic) {
 	if handle, _, _ := r.Lookup("GET", CallbackPath); handle == nil {
-		r.GET(CallbackPath, s.d.SessionHandler().IsNotAuthenticated(s.handleCallback, session.RedirectOnAuthenticated(s.c)))
+		r.GET(CallbackPath, s.handleCallback)
 	}
 
 	if handle, _, _ := r.Lookup("POST", AuthPath); handle == nil {
-		r.POST(AuthPath, s.d.SessionHandler().IsNotAuthenticated(s.handleAuth, session.RedirectOnAuthenticated(s.c)))
+		r.POST(AuthPath, s.handleAuth)
 	}
 
 	if handle, _, _ := r.Lookup("GET", AuthPath); handle == nil {
-		r.GET(AuthPath, s.d.SessionHandler().IsNotAuthenticated(s.handleAuth, session.RedirectOnAuthenticated(s.c)))
+		r.GET(AuthPath, s.handleAuth)
 	}
 }
 
@@ -168,11 +168,20 @@ func (s *Strategy) handleAuth(w http.ResponseWriter, r *http.Request, ps httprou
 		return
 	}
 
-	if _, err := s.validateRequest(r.Context(), rid); err != nil {
+	ar, err := s.validateRequest(r.Context(), rid)
+	if err != nil {
 		s.handleError(w, r, rid, nil, err)
 		return
 	}
 
+	// we assume an error means the user has no session
+	if _, err := s.d.SessionManager().FetchFromRequest(r.Context(), w, r); err == nil {
+		if !ar.IsForced() {
+			http.Redirect(w, r, s.c.DefaultReturnToURL().String(), http.StatusFound)
+			return
+		}
+	}
+
 	state := x.NewUUID().String()
 	// Any data that is posted to this endpoint will be used to fill out missing data from the oidc provider.
 	if err := x.SessionPersistValues(w, r, s.d.CookieManager(), sessionName, map[string]interface{}{
@@ -184,7 +193,7 @@ func (s *Strategy) handleAuth(w http.ResponseWriter, r *http.Request, ps httprou
 		return
 	}
 
-	http.Redirect(w, r, config.AuthCodeURL(state), http.StatusFound)
+	http.Redirect(w, r, config.AuthCodeURL(state, provider.AuthCodeURLOptions(ar)...), http.StatusFound)
 }
 
 func (s *Strategy) validateRequest(ctx context.Context, rid uuid.UUID) (request, error) {
@@ -253,6 +262,14 @@ func (s *Strategy) handleCallback(w http.ResponseWriter, r *http.Request, ps htt
 		return
 	}
 
+	// we assume an error means the user has no session
+	if _, err := s.d.SessionManager().FetchFromRequest(r.Context(), w, r); err == nil {
+		if !ar.IsForced() {
+			http.Redirect(w, r, s.c.DefaultReturnToURL().String(), http.StatusFound)
+			return
+		}
+	}
+
 	provider, err := s.provider(pid)
 	if err != nil {
 		s.handleError(w, r, ar.GetID(), nil, err)
diff --git a/selfservice/strategy/oidc/types.go b/selfservice/strategy/oidc/types.go
index fd604e1fa..bd6190c79 100644
--- a/selfservice/strategy/oidc/types.go
+++ b/selfservice/strategy/oidc/types.go
@@ -31,4 +31,5 @@ func NewRequestMethodConfig(f *form.HTMLForm) *RequestMethod {
 
 type request interface {
 	GetID() uuid.UUID
+	IsForced() bool
 }
diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go
index 14f69b0a8..fac353581 100644
--- a/selfservice/strategy/password/login.go
+++ b/selfservice/strategy/password/login.go
@@ -16,7 +16,6 @@ import (
 	"github.com/ory/kratos/schema"
 	"github.com/ory/kratos/selfservice/flow/login"
 	"github.com/ory/kratos/selfservice/form"
-	"github.com/ory/kratos/session"
 	"github.com/ory/kratos/x"
 )
 
@@ -25,7 +24,7 @@ const (
 )
 
 func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) {
-	r.POST(LoginPath, s.d.SessionHandler().IsNotAuthenticated(s.handleLogin, session.RedirectOnAuthenticated(s.c)))
+	r.POST(LoginPath, s.handleLogin)
 }
 
 func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, rr *login.Request, err error) {
@@ -54,6 +53,13 @@ func (s *Strategy) handleLogin(w http.ResponseWriter, r *http.Request, _ httprou
 		return
 	}
 
+	if _, err := s.d.SessionManager().FetchFromRequest(r.Context(), w, r); err == nil {
+		if !ar.Forced {
+			http.Redirect(w, r, s.c.DefaultReturnToURL().String(), http.StatusFound)
+			return
+		}
+	}
+
 	var p LoginFormPayload
 	if err := r.ParseForm(); err != nil {
 		s.handleLoginError(w, r, ar, errors.WithStack(herodot.ErrBadRequest.WithDebug(err.Error()).WithReasonf("Unable to parse HTTP form request: %s", err.Error())))
diff --git a/selfservice/strategy/password/strategy.go b/selfservice/strategy/password/strategy.go
index 583a317e4..7ac4d2f50 100644
--- a/selfservice/strategy/password/strategy.go
+++ b/selfservice/strategy/password/strategy.go
@@ -41,6 +41,7 @@ type registrationStrategyDependencies interface {
 	identity.ValidationProvider
 
 	session.HandlerProvider
+	session.ManagementProvider
 }
 
 type Strategy struct {
diff --git a/x/nosurf.go b/x/nosurf.go
index 9191afb01..263aef844 100644
--- a/x/nosurf.go
+++ b/x/nosurf.go
@@ -24,8 +24,10 @@ func DefaultCSRFToken(r *http.Request) string {
 	return nosurf.Token(r)
 }
 
+const FakeCSRFToken = "nosurf"
+
 func FakeCSRFTokenGenerator(r *http.Request) string {
-	return "nosurf"
+	return FakeCSRFToken
 }
 
 func FakeCSRFTokenGeneratorWithToken(token string) func(r *http.Request) string {


Test patch:
diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go
index f84bcc718..b065a480f 100644
--- a/persistence/sql/persister_test.go
+++ b/persistence/sql/persister_test.go
@@ -12,9 +12,10 @@ import (
 	"github.com/go-errors/errors"
 	"github.com/stretchr/testify/assert"
 
+	"github.com/ory/x/sqlcon"
+
 	"github.com/ory/kratos/persistence/sql"
 	"github.com/ory/kratos/x"
-	"github.com/ory/x/sqlcon"
 
 	"github.com/gobuffalo/pop/v5"
 	"github.com/gobuffalo/pop/v5/logging"
@@ -90,9 +91,9 @@ func TestPersister(t *testing.T) {
 	var l sync.Mutex
 	if !testing.Short() {
 		funcs := map[string]func(t *testing.T) string{
-			"postgres": dockertest.RunTestPostgreSQL,
-			"mysql":    dockertest.RunTestMySQL,
-			// "cockroach": dockertest.RunTestCockroachDB, // pending: https://github.com/gobuffalo/fizz/pull/69
+			"postgres":  dockertest.RunTestPostgreSQL,
+			"mysql":     dockertest.RunTestMySQL,
+			"cockroach": dockertest.RunTestCockroachDB,
 		}
 
 		var wg sync.WaitGroup
@@ -195,7 +196,7 @@ func TestPersister_Transaction(t *testing.T) {
 		err := c.Transaction(func(tx *pop.Connection) error {
 			ctx := sql.WithTransaction(context.Background(), tx)
 			require.NoError(t, p.CreateLoginRequest(ctx, lr), "%+v", lr)
-			require.NoError(t, p.UpdateLoginRequest(ctx, lr.ID, identity.CredentialsTypePassword, &login.RequestMethod{}))
+			require.NoError(t, p.UpdateLoginRequestMethod(ctx, lr.ID, identity.CredentialsTypePassword, &login.RequestMethod{}))
 			require.NoError(t, getErr(p.GetLoginRequest(ctx, lr.ID)), "%+v", lr)
 			return errors.Errorf(errMessage)
 		})
diff --git a/selfservice/flow/login/handler_test.go b/selfservice/flow/login/handler_test.go
index 90324678f..d18e150cb 100644
--- a/selfservice/flow/login/handler_test.go
+++ b/selfservice/flow/login/handler_test.go
@@ -1,67 +1,132 @@
 package login_test
 
 import (
-	"fmt"
+	"context"
+	"io/ioutil"
 	"net/http"
 	"net/http/cookiejar"
+	"net/url"
 	"testing"
+	"time"
 
 	"github.com/gobuffalo/httptest"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/tidwall/gjson"
 
+	"github.com/ory/kratos/selfservice/form"
 	"github.com/ory/viper"
 
 	"github.com/ory/kratos/driver/configuration"
 	"github.com/ory/kratos/internal"
 	"github.com/ory/kratos/selfservice/errorx"
 	"github.com/ory/kratos/selfservice/flow/login"
-	"github.com/ory/kratos/selfservice/strategy/oidc"
-	"github.com/ory/kratos/selfservice/strategy/password"
 	"github.com/ory/kratos/session"
 	"github.com/ory/kratos/x"
 )
 
+type (
+	withTokenGenerator interface {
+		WithTokenGenerator(g form.CSRFGenerator)
+	}
+)
+
 func init() {
 	internal.RegisterFakes()
 }
 
-func TestEnsureSessionRedirect(t *testing.T) {
+func TestHandlerSettingForced(t *testing.T) {
 	_, reg := internal.NewRegistryDefault(t)
+	for _, strategy := range reg.LoginStrategies() {
+		// We need to know the csrf token
+		strategy.(withTokenGenerator).WithTokenGenerator(x.FakeCSRFTokenGenerator)
+	}
 
 	router := x.NewRouterPublic()
 	admin := x.NewRouterAdmin()
 	reg.LoginHandler().RegisterPublicRoutes(router)
 	reg.LoginHandler().RegisterAdminRoutes(admin)
+	reg.LoginHandler().WithTokenGenerator(x.FakeCSRFTokenGenerator)
 	reg.LoginStrategies().RegisterPublicRoutes(router)
 	ts := httptest.NewServer(router)
 	defer ts.Close()
 
-	redirTS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		_, _ = w.Write([]byte("already authenticated"))
-	}))
-	defer redirTS.Close()
+	loginTS := httptest.NewServer(login.TestRequestHandler(t, reg))
 
-	viper.Set(configuration.ViperKeyURLsDefaultReturnTo, redirTS.URL)
 	viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL)
+	viper.Set(configuration.ViperKeyURLsLogin, loginTS.URL)
 	viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/login.schema.json")
 
-	for k, tc := range [][]string{
-		{"GET", login.BrowserLoginPath},
+	// assert bool
+	ab := func(body []byte, exp bool) {
+		r := gjson.GetBytes(body, "forced")
+		assert.True(t, r.Exists(), "%s", body)
+		assert.Equal(t, exp, r.Bool(), "%s", body)
+	}
 
-		{"POST", password.LoginPath},
+	// make authenticated request
+	mar := func(extQuery url.Values) []byte {
+		rid := x.NewUUID()
+		req := x.NewTestHTTPRequest(t, "GET", ts.URL+login.BrowserLoginPath, nil)
+		loginReq := login.NewLoginRequest(time.Minute, x.FakeCSRFToken, req)
+		loginReq.ID = rid
+		for _, s := range reg.LoginStrategies() {
+			require.NoError(t, s.PopulateLoginMethod(req, loginReq))
+		}
+		require.NoError(t, reg.LoginRequestPersister().CreateLoginRequest(context.TODO(), loginReq), "%+v", loginReq)
+
+		q := url.Values{
+			"request": {rid.String()},
+		}
+		for key := range extQuery {
+			q.Set(key, extQuery.Get(key))
+		}
+		req.URL.RawQuery = q.Encode()
+
+		body, _ := session.MockMakeAuthenticatedRequest(t, reg, router.Router, req)
+		return body
+	}
 
-		// it is ok that these contain the parameters as raw strings as we are only interested in checking if the middleware is working
-		{"POST", oidc.AuthPath},
-		{"GET", oidc.AuthPath},
-		{"GET", oidc.CallbackPath},
-	} {
-		t.Run(fmt.Sprintf("case=%d/method=%s/path=%s", k, tc[0], tc[1]), func(t *testing.T) {
-			body, _ := session.MockMakeAuthenticatedRequest(t, reg, router.Router, x.NewTestHTTPRequest(t, tc[0], ts.URL+tc[1], nil))
-			assert.EqualValues(t, "already authenticated", string(body))
-		})
+	// make unauthenticated request
+	mur := func(query url.Values) []byte {
+		c := ts.Client()
+		u, err := url.ParseRequestURI(ts.URL)
+		require.NoError(t, err)
+		u.Path = login.BrowserLoginPath
+		u.RawQuery = query.Encode()
+		res, err := c.Get(u.String())
+		require.NoError(t, err)
+		defer res.Body.Close()
+		body, err := ioutil.ReadAll(res.Body)
+		require.NoError(t, err)
+		return body
 	}
+
+	t.Run("case=does not set forced flag on unauthenticated request", func(t *testing.T) {
+		ab(mur(url.Values{}), false)
+	})
+
+	t.Run("case=does not set forced flag on unauthenticated request with prompt=login", func(t *testing.T) {
+		ab(mur(url.Values{
+			"prompt": {"login"},
+		}), false)
+	})
+
+	t.Run("case=does not set forced flag on authenticated request without prompt=login", func(t *testing.T) {
+		ab(mar(url.Values{}), false)
+	})
+
+	t.Run("case=does not set forced flag on authenticated request with prompt=false", func(t *testing.T) {
+		ab(mar(url.Values{
+			"prompt": {"false"},
+		}), false)
+	})
+
+	t.Run("case=does set forced flag on authenticated request with prompt=login", func(t *testing.T) {
+		ab(mar(url.Values{
+			"prompt": {"login"},
+		}), true)
+	})
 }
 
 func TestLoginHandler(t *testing.T) {
diff --git a/selfservice/flow/registration/handler_test.go b/selfservice/flow/registration/handler_test.go
index 790ee4706..bbbb29839 100644
--- a/selfservice/flow/registration/handler_test.go
+++ b/selfservice/flow/registration/handler_test.go
@@ -1,7 +1,6 @@
 package registration_test
 
 import (
-	"fmt"
 	"net/http"
 	"net/http/cookiejar"
 	"net/http/httptest"
@@ -17,8 +16,6 @@ import (
 	"github.com/ory/kratos/internal"
 	"github.com/ory/kratos/selfservice/errorx"
 	"github.com/ory/kratos/selfservice/flow/registration"
-	"github.com/ory/kratos/selfservice/strategy/oidc"
-	"github.com/ory/kratos/selfservice/strategy/password"
 	"github.com/ory/kratos/session"
 	"github.com/ory/kratos/x"
 )
@@ -27,7 +24,7 @@ func init() {
 	internal.RegisterFakes()
 }
 
-func TestEnsureSessionRedirect(t *testing.T) {
+func TestHandlerRedirectOnAuthenticated(t *testing.T) {
 	_, reg := internal.NewRegistryDefault(t)
 
 	router := x.NewRouterPublic()
@@ -45,21 +42,10 @@ func TestEnsureSessionRedirect(t *testing.T) {
 	viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL)
 	viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/registration.schema.json")
 
-	for k, tc := range [][]string{
-		{"GET", registration.BrowserRegistrationPath},
-
-		{"POST", password.RegistrationPath},
-
-		// it is ok that these contain the parameters as arw strings as we are only interested in checking if the middleware is working
-		{"POST", oidc.AuthPath},
-		{"GET", oidc.AuthPath},
-		{"GET", oidc.CallbackPath},
-	} {
-		t.Run(fmt.Sprintf("case=%d/method=%s/path=%s", k, tc[0], tc[1]), func(t *testing.T) {
-			body, _ := session.MockMakeAuthenticatedRequest(t, reg, router.Router, x.NewTestHTTPRequest(t, tc[0], ts.URL+tc[1], nil))
-			assert.EqualValues(t, "already authenticated", string(body))
-		})
-	}
+	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))
+		assert.EqualValues(t, "already authenticated", string(body))
+	})
 }
 
 func TestRegistrationHandler(t *testing.T) {
diff --git a/selfservice/flow/verify/handler_test.go b/selfservice/flow/verify/handler_test.go
index 87c379016..eae09e354 100644
--- a/selfservice/flow/verify/handler_test.go
+++ b/selfservice/flow/verify/handler_test.go
@@ -113,7 +113,7 @@ func TestHandler(t *testing.T) {
 		assert.True(t, time.Time(svr.Payload.ExpiresAt).After(time.Now()))
 		assert.Contains(t, svr.Payload.RequestURL, initURL)
 		assert.Contains(t, svr.Payload.ID, rid)
-		assert.Equal(t, publicTS.URL+verify.PublicVerificationCompletePath+"?request="+rid, *svr.Payload.Form.Action)
+		assert.Equal(t, publicTS.URL+strings.Replace(verify.PublicVerificationCompletePath, ":via", "email", 1)+"?request="+rid, *svr.Payload.Form.Action)
 		assert.Contains(t, "csrf_token", *svr.Payload.Form.Fields[0].Name)
 		assert.Contains(t, "to_verify", *svr.Payload.Form.Fields[1].Name)
 		assert.Contains(t, "email", *svr.Payload.Form.Fields[1].Type)
diff --git a/selfservice/strategy/oidc/provider_generic_test.go b/selfservice/strategy/oidc/provider_generic_test.go
new file mode 100644
index 000000000..f3485aa50
--- /dev/null
+++ b/selfservice/strategy/oidc/provider_generic_test.go
@@ -0,0 +1,46 @@
+package oidc
+
+import (
+	"context"
+	"net/url"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/ory/kratos/selfservice/flow/login"
+	"github.com/ory/kratos/x"
+)
+
+func makeAuthCodeURL(t *testing.T, r *login.Request) string {
+	public, err := url.Parse("https://ory.sh")
+	require.NoError(t, err)
+	p := NewProviderGenericOIDC(&Configuration{
+		Provider:     "generic",
+		ID:           "valid",
+		ClientID:     "client",
+		ClientSecret: "secret",
+		IssuerURL:    "https://accounts.google.com",
+		SchemaURL:    "file://./stub/hydra.schema.json",
+	}, public)
+	c, err := p.OAuth2(context.TODO())
+	require.NoError(t, err)
+	return c.AuthCodeURL("state", p.AuthCodeURLOptions(r)...)
+}
+
+func TestProviderGenericOIDC_AddAuthCodeURLOptions(t *testing.T) {
+	t.Run("case=expect prompt to be login with forced flag", func(t *testing.T) {
+		r := &login.Request{
+			ID:     x.NewUUID(),
+			Forced: true,
+		}
+		assert.Contains(t, makeAuthCodeURL(t, r), "prompt=login")
+	})
+
+	t.Run("case=expect prompt to not be login without forced flag", func(t *testing.T) {
+		r := &login.Request{
+			ID: x.NewUUID(),
+		}
+		assert.NotContains(t, makeAuthCodeURL(t, r), "prompt=login")
+	})
+}
diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go
index c5ab17e69..e013b2337 100644
--- a/selfservice/strategy/oidc/strategy_test.go
+++ b/selfservice/strategy/oidc/strategy_test.go
@@ -118,18 +118,14 @@ func TestStrategy(t *testing.T) {
 	_, reg := internal.NewRegistryDefault(t)
 	for _, strategy := range reg.LoginStrategies() {
 		// We need to replace the password strategy token generator because it is being used by the error handler...
-		strategy.(withTokenGenerator).WithTokenGenerator(func(r *http.Request) string {
-			return "nosurf"
-		})
+		strategy.(withTokenGenerator).WithTokenGenerator(x.FakeCSRFTokenGenerator)
 	}
 
 	public := x.NewRouterPublic()
 	admin := x.NewRouterAdmin()
 	reg.LoginHandler().RegisterPublicRoutes(public)
 	reg.LoginHandler().RegisterAdminRoutes(admin)
-	reg.LoginHandler().WithTokenGenerator(func(r *http.Request) string {
-		return "nosurf"
-	})
+	reg.LoginHandler().WithTokenGenerator(x.FakeCSRFTokenGenerator)
 	reg.LoginStrategies().RegisterPublicRoutes(public)
 	reg.RegistrationStrategies().RegisterPublicRoutes(public)
 	reg.RegistrationHandler().RegisterPublicRoutes(public)
@@ -192,8 +188,7 @@ func TestStrategy(t *testing.T) {
 	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.ViperKeySignupDefaultReturnToURL, returnTS.URL)
-	// viper.Set(configuration.ViperKeyAuthnDefaultReturnToURL, returnTS.URL)
+	viper.Set(configuration.ViperKeyURLsDefaultReturnTo, returnTS.URL)
 
 	t.Logf("Kratos Public URL: %s", ts.URL)
 	t.Logf("Kratos Error URL: %s", errTS.URL)
@@ -202,9 +197,12 @@ func TestStrategy(t *testing.T) {
 	t.Logf("Hydra Integration URL: %s", hydraIntegrationTSURL)
 	t.Logf("Return URL: %s", returnTS.URL)
 
-	var newClient = func(t *testing.T) *http.Client {
-		jar, err := cookiejar.New(nil)
-		require.NoError(t, err)
+	var newClient = func(t *testing.T, jar *cookiejar.Jar) *http.Client {
+		if jar == nil {
+			j, err := cookiejar.New(nil)
+			jar = j
+			require.NoError(t, err)
+		}
 		return &http.Client{
 			Jar: jar,
 			CheckRedirect: func(req *http.Request, via []*http.Request) error {
@@ -225,10 +223,10 @@ func TestStrategy(t *testing.T) {
 	subject = "foo@bar.com"
 	scope = []string{}
 
-	// make request
-	var mr = func(t *testing.T, provider string, request uuid.UUID, fv url.Values) (*http.Response, []byte) {
+	// make request with cookie jar
+	var mrj = func(t *testing.T, provider string, request uuid.UUID, fv url.Values, jar *cookiejar.Jar) (*http.Response, []byte) {
 		fv.Set("provider", provider)
-		res, err := newClient(t).PostForm(ts.URL+oidc.BasePath+"/auth/"+request.String(), fv)
+		res, err := newClient(t, jar).PostForm(ts.URL+oidc.BasePath+"/auth/"+request.String(), fv)
 		require.NoError(t, err)
 
 		body, err := ioutil.ReadAll(res.Body)
@@ -240,6 +238,11 @@ func TestStrategy(t *testing.T) {
 		return res, body
 	}
 
+	// make request
+	var mr = func(t *testing.T, provider string, request uuid.UUID, fv url.Values) (*http.Response, []byte) {
+		return mrj(t, provider, request, fv, nil)
+	}
+
 	// assert system error (redirect to error endpoint)
 	var ase = func(t *testing.T, res *http.Response, body []byte, code int, reason string) {
 		require.Contains(t, res.Request.URL.String(), errTS.URL, "%s", body)
@@ -456,4 +459,41 @@ func TestStrategy(t *testing.T) {
 			aue(t, res, body, "an account with the same identifier (email, phone, username, ...) exists already")
 		})
 	})
+
+	t.Run("case=should redirect to default return ts when sending authenticated login request without forced flag", func(t *testing.T) {
+		subject = "no-reauth-login@ory.sh"
+		scope = []string{"openid"}
+
+		fv := url.Values{"traits.name": {"valid-name"}}
+		jar, _ := cookiejar.New(nil)
+		r1 := nlr(t, returnTS.URL, time.Minute)
+		res1, body1 := mrj(t, "valid", r1.ID, fv, jar)
+		ai(t, res1, body1)
+		r2 := nlr(t, returnTS.URL, time.Minute)
+		res2, body2 := mrj(t, "valid", r2.ID, fv, jar)
+		ai(t, res2, body2)
+		assert.Equal(t, body1, body2)
+	})
+
+	t.Run("case=should reauthenticate when sending authenticated login request with forced flag", func(t *testing.T) {
+		subject = "reauth-login@ory.sh"
+		scope = []string{"openid"}
+
+		fv := url.Values{"traits.name": {"valid-name"}}
+		jar, _ := cookiejar.New(nil)
+		r1 := nlr(t, returnTS.URL, time.Minute)
+		res1, body1 := mrj(t, "valid", r1.ID, fv, jar)
+		ai(t, res1, body1)
+		r2 := nlr(t, returnTS.URL, time.Minute)
+		require.NoError(t, reg.LoginRequestPersister().MarkRequestForced(context.Background(), r2.ID))
+		res2, body2 := mrj(t, "valid", r2.ID, fv, jar)
+		ai(t, res2, body2)
+		assert.NotEqual(t, gjson.GetBytes(body1, "sid"), gjson.GetBytes(body2, "sid"))
+		authAt1, err := time.Parse(time.RFC3339, gjson.GetBytes(body1, "authenticated_at").String())
+		require.NoError(t, err)
+		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)
+	})
 }
diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go
index 3a35568a1..f7260e1e3 100644
--- a/selfservice/strategy/password/login_test.go
+++ b/selfservice/strategy/password/login_test.go
@@ -76,250 +76,239 @@ func nlr(exp time.Duration) *login.Request {
 	}
 }
 
-type loginStrategyDependencies interface {
-	password.ValidationProvider
-	identity.PrivilegedPoolProvider
-	password.HashProvider
-}
+func TestLoginNew(t *testing.T) {
+	_, reg := internal.NewRegistryDefault(t)
+	s := reg.LoginStrategies().MustStrategy(identity.CredentialsTypePassword).(*password.Strategy)
+	s.WithTokenGenerator(x.FakeCSRFTokenGeneratorWithToken("anti-rf-token"))
+	reg.LoginStrategies().MustStrategy(identity.CredentialsTypeOIDC).(*oidc.Strategy).WithTokenGenerator(x.FakeCSRFTokenGeneratorWithToken("anti-rf-token"))
+	reg.LoginHandler().WithTokenGenerator(x.FakeCSRFTokenGeneratorWithToken("anti-rf-token"))
+	reg.SelfServiceErrorManager().WithTokenGenerator(x.FakeCSRFTokenGeneratorWithToken("anti-rf-token"))
+
+	router := x.NewRouterPublic()
+	admin := x.NewRouterAdmin()
+	reg.LoginHandler().RegisterPublicRoutes(router)
+	reg.LoginHandler().RegisterAdminRoutes(admin)
+	s.RegisterLoginRoutes(router)
+	ts := httptest.NewServer(router)
+	defer ts.Close()
+
+	errTs, uiTs, returnTs := errorx.NewErrorTestServer(t, reg), httptest.NewServer(login.TestRequestHandler(t, reg)), newReturnTs(t, reg)
+	defer errTs.Close()
+	defer uiTs.Close()
+	defer returnTs.Close()
+
+	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")
+
+	makeRequest := func(lr *login.Request, payload string, forceRequestID *string, jar *cookiejar.Jar) (*http.Response, []byte) {
+		lr.RequestURL = ts.URL
+		require.NoError(t, reg.LoginRequestPersister().CreateLoginRequest(context.TODO(), lr))
+
+		c := ts.Client()
+		if jar == nil {
+			c.Jar, _ = cookiejar.New(&cookiejar.Options{})
+		} else {
+			c.Jar = jar
+		}
+
+		requestID := lr.ID.String()
+		if forceRequestID != nil {
+			requestID = *forceRequestID
+		}
+
+		res, err := c.Post(ts.URL+password.LoginPath+"?request="+requestID, "application/x-www-form-urlencoded", strings.NewReader(payload))
+		require.NoError(t, err)
+		defer res.Body.Close()
+		require.EqualValues(t, http.StatusOK, res.StatusCode, "Request: %+v\n\t\tResponse: %s", res.Request, res)
+		body, err := ioutil.ReadAll(res.Body)
+		require.NoError(t, err)
+		return res, body
+	}
 
-func TestLogin(t *testing.T) {
-	var ensureFieldsExist = func(t *testing.T, body []byte) {
+	ensureFieldsExist := func(t *testing.T, body []byte) {
 		checkFormContent(t, body, "identifier",
 			"password",
 			"csrf_token")
 	}
 
-	type testCase struct {
-		d              string
-		prep           func(t *testing.T, r loginStrategyDependencies)
-		ar             *login.Request
-		forceRequestID *string
-		payload        string
-		assert         func(t *testing.T, tc testCase, r *http.Response)
+	createIdentity := func(identifier, password string) {
+		p, _ := reg.PasswordHasher().Generate([]byte(password))
+		require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &identity.Identity{
+			ID:     x.NewUUID(),
+			Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)),
+			Credentials: map[identity.CredentialsType]identity.Credentials{
+				identity.CredentialsTypePassword: {
+					Type:        identity.CredentialsTypePassword,
+					Identifiers: []string{identifier},
+					Config:      json.RawMessage(`{"hashed_password":"` + string(p) + `"}`),
+				},
+			},
+		}))
 	}
 
-	for k, tc := range []testCase{
-
-		{
-			d:       "should show the error ui because the request is malformed",
-			ar:      nlr(0),
-			payload: "14=)=!(%)$/ZP()GHIÖ",
-			assert: func(t *testing.T, tc testCase, r *http.Response) {
-				require.Contains(t, r.Request.URL.Path, "login-ts", "%+v", r.Request)
-				body, err := ioutil.ReadAll(r.Body)
-				require.NoError(t, err)
-
-				assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body)
-				assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String(), "%s", body)
-				assert.Contains(t, gjson.GetBytes(body, "methods.password.config.errors.0.message").String(), `invalid URL escape`)
-			},
-		},
-		{
-			d:              "should show the error ui because the request id missing",
-			ar:             nlr(time.Minute),
-			forceRequestID: pointerx.String(""),
-			payload:        url.Values{}.Encode(),
-			assert: func(t *testing.T, tc testCase, r *http.Response) {
-				assert.Contains(t, r.Request.URL.Path, "error-ts")
-				body, err := ioutil.ReadAll(r.Body)
-				require.NoError(t, err)
-
-				assert.Equal(t, int64(http.StatusBadRequest), gjson.GetBytes(body, "0.code").Int(), "%s", body)
-				assert.Equal(t, "Bad Request", gjson.GetBytes(body, "0.status").String(), "%s", body)
-				assert.Contains(t, gjson.GetBytes(body, "0.reason").String(), "request query parameter is missing or invalid", "%s", body)
-			},
-		},
-		{
-			d:              "should return an error because the request does not exist",
-			ar:             nlr(0),
-			forceRequestID: pointerx.String(x.NewUUID().String()),
-			payload: url.Values{
-				"identifier": {"identifier"},
-				"password":   {"password"},
-			}.Encode(),
-			assert: func(t *testing.T, tc testCase, r *http.Response) {
-				assert.Contains(t, r.Request.URL.Path, "error-ts")
-				body, err := ioutil.ReadAll(r.Body)
-				require.NoError(t, err)
-
-				assert.Equal(t, int64(http.StatusNotFound), gjson.GetBytes(body, "0.code").Int(), "%s", body)
-				assert.Equal(t, "Not Found", gjson.GetBytes(body, "0.status").String(), "%s", body)
-				assert.Contains(t, gjson.GetBytes(body, "0.message").String(), "Unable to locate the resource", "%s", body)
-			},
-		},
-		{
-			d:  "should redirect to login init because the request is expired",
-			ar: nlr(-time.Hour),
-			payload: url.Values{
-				"identifier": {"identifier"},
-				"password":   {"password"},
-			}.Encode(),
-			assert: func(t *testing.T, tc testCase, r *http.Response) {
-				assert.Contains(t, r.Request.URL.Path, "login-ts")
-				body, err := ioutil.ReadAll(r.Body)
-				require.NoError(t, err)
-
-				assert.NotEqual(t, tc.ar.ID, gjson.GetBytes(body, "id"))
-
-				assert.Contains(t, gjson.GetBytes(body, "methods.oidc.config.errors.0").String(), "expired", "%s", body)
-				assert.Contains(t, gjson.GetBytes(body, "methods.password.config.errors.0").String(), "expired", "%s", body)
-			},
-		},
-		{
-			d:  "should return an error because the credentials are invalid (user does not exist)",
-			ar: nlr(time.Hour),
-			payload: url.Values{
-				"identifier": {"identifier"},
-				"password":   {"password"},
-			}.Encode(),
-			assert: func(t *testing.T, tc testCase, r *http.Response) {
-				require.Contains(t, r.Request.URL.Path, "login-ts")
-				body, err := ioutil.ReadAll(r.Body)
-				require.NoError(t, err)
-
-				assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body)
-				assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String())
-				assert.Equal(t, `the provided credentials are invalid, check for spelling mistakes in your password or username, email address, or phone number`, gjson.GetBytes(body, "methods.password.config.errors.0.message").String())
-			},
-		},
-		{
-			d:  "should return an error because no identifier is set",
-			ar: nlr(time.Hour),
-			payload: url.Values{
-				"password": {"password"},
-			}.Encode(),
-			assert: func(t *testing.T, tc testCase, r *http.Response) {
-				require.Contains(t, r.Request.URL.Path, "login-ts")
-				body, err := ioutil.ReadAll(r.Body)
-				require.NoError(t, err)
-
-				// Let's ensure that the payload is being propagated properly.
-				assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String())
-				assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String())
-				ensureFieldsExist(t, body)
-				assert.Equal(t, "missing properties: identifier", gjson.GetBytes(body, "methods.password.config.fields.#(name==identifier).errors.0.message").String(), "%s", body)
-
-				// The password value should not be returned!
-				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==password).value").String())
-			},
-		},
-		{
-			d:  "should return an error because no password is set",
-			ar: nlr(time.Hour),
-			payload: url.Values{
-				"identifier": {"identifier"},
-			}.Encode(),
-			assert: func(t *testing.T, tc testCase, r *http.Response) {
-				require.Contains(t, r.Request.URL.Path, "login-ts")
-				body, err := ioutil.ReadAll(r.Body)
-				require.NoError(t, err)
-
-				// Let's ensure that the payload is being propagated properly.
-				assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String())
-				assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String())
-				ensureFieldsExist(t, body)
-				assert.Equal(t, "missing properties: password", gjson.GetBytes(body, "methods.password.config.fields.#(name==password).errors.0.message").String(), "%s", body)
-
-				assert.Equal(t, "anti-rf-token", gjson.GetBytes(body, "methods.password.config.fields.#(name==csrf_token).value").String())
-				assert.Equal(t, "identifier", gjson.GetBytes(body, "methods.password.config.fields.#(name==identifier).value").String(), "%s", body)
-
-				// This must not include the password!
-				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==password).value").String())
-			},
-		},
-		{
-			d:  "should return an error because the credentials are invalid (password not correct)",
-			ar: nlr(time.Hour),
-			prep: func(t *testing.T, r loginStrategyDependencies) {
-				p, _ := r.PasswordHasher().Generate([]byte("password"))
-				require.NoError(t, r.PrivilegedIdentityPool().CreateIdentity(context.Background(), &identity.Identity{
-					ID:     x.NewUUID(),
-					Traits: identity.Traits(`{}`),
-					Credentials: map[identity.CredentialsType]identity.Credentials{
-						identity.CredentialsTypePassword: {
-							Type:        identity.CredentialsTypePassword,
-							Identifiers: []string{"login-identifier-6"},
-							Config:      json.RawMessage(`{"hashed_password":"` + string(p) + `"}`),
-						},
-					},
-				}))
-			},
-			payload: url.Values{
-				"identifier": {"login-identifier-6"},
-				"password":   {"not-password"},
-			}.Encode(),
-			assert: func(t *testing.T, tc testCase, r *http.Response) {
-				require.Contains(t, r.Request.URL.Path, "login-ts")
-				body, err := ioutil.ReadAll(r.Body)
-				require.NoError(t, err)
-
-				assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String())
-				assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String())
-				ensureFieldsExist(t, body)
-				assert.Equal(t,
-					errorsx.Cause(schema.NewInvalidCredentialsError()).(*jsonschema.ValidationError).Message,
-					gjson.GetBytes(body, "methods.password.config.errors.0.message").String(),
-					"%s", body,
-				)
-
-				// This must not include the password!
-				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==password).value").String())
-			},
-		},
-		{
-			d:  "should pass because everything is a-ok",
-			ar: nlr(time.Hour),
-			prep: func(t *testing.T, r loginStrategyDependencies) {
-				p, _ := r.PasswordHasher().Generate([]byte("password"))
-				require.NoError(t, r.PrivilegedIdentityPool().CreateIdentity(context.Background(), &identity.Identity{
-					ID:     x.NewUUID(),
-					Traits: identity.Traits(`{"subject":"login-identifier-7"}`),
-					Credentials: map[identity.CredentialsType]identity.Credentials{
-						identity.CredentialsTypePassword: {
-							Type:        identity.CredentialsTypePassword,
-							Identifiers: []string{"login-identifier-7"},
-							Config:      json.RawMessage(`{"hashed_password":"` + string(p) + `"}`),
-						},
-					},
-				}))
-			},
-			payload: url.Values{
-				"identifier": {"login-identifier-7"},
-				"password":   {"password"},
-			}.Encode(),
-			assert: func(t *testing.T, tc testCase, r *http.Response) {
-				assert.Contains(t, r.Request.URL.Path, "return-ts", "%s", r.Request.URL.String())
-				body, err := ioutil.ReadAll(r.Body)
-				require.NoError(t, err)
-
-				assert.Equal(t, `login-identifier-7`, gjson.GetBytes(body, "identity.traits.subject").String(), "%s", body)
-			},
-		},
-		{
-			d: "should return an error because not passing validation and reset previous errors and values",
-			ar: &login.Request{
-				ID:        x.NewUUID(),
-				ExpiresAt: time.Now().Add(time.Minute),
-				Methods: map[identity.CredentialsType]*login.RequestMethod{
-					identity.CredentialsTypePassword: {
-						Method: identity.CredentialsTypePassword,
-						Config: &login.RequestMethodConfig{
-							RequestMethodConfigurator: &password.RequestMethod{
-								HTMLForm: &form.HTMLForm{
-									Method: "POST",
-									Action: "/action",
-									Errors: []form.Error{{Message: "some error"}},
-									Fields: form.Fields{
-										{
-											Value:  "baz",
-											Name:   "identifier",
-											Errors: []form.Error{{Message: "err"}},
-										},
-										{
-											Value:  "bar",
-											Name:   "password",
-											Errors: []form.Error{{Message: "err"}},
-										},
+	t.Run("should show the error ui because the request is malformed", func(t *testing.T) {
+		lr := nlr(0)
+		res, body := makeRequest(lr, "14=)=!(%)$/ZP()GHIÖ", nil, nil)
+
+		require.Contains(t, res.Request.URL.Path, "login-ts", "%+v", res.Request)
+		assert.Equal(t, lr.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body)
+		assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String(), "%s", body)
+		assert.Contains(t, gjson.GetBytes(body, "methods.password.config.errors.0.message").String(), `invalid URL escape`)
+	})
+
+	t.Run("should show the error ui because the request id missing", func(t *testing.T) {
+		lr := nlr(time.Minute)
+		res, body := makeRequest(lr, url.Values{}.Encode(), pointerx.String(""), nil)
+
+		require.Contains(t, res.Request.URL.Path, "error-ts")
+		assert.Equal(t, int64(http.StatusBadRequest), gjson.GetBytes(body, "0.code").Int(), "%s", body)
+		assert.Equal(t, "Bad Request", gjson.GetBytes(body, "0.status").String(), "%s", body)
+		assert.Contains(t, gjson.GetBytes(body, "0.reason").String(), "request query parameter is missing or invalid", "%s", body)
+	})
+
+	t.Run("should return an error because the request does not exist", func(t *testing.T) {
+		lr := nlr(0)
+		res, body := makeRequest(lr, url.Values{
+			"identifier": {"identifier"},
+			"password":   {"password"},
+		}.Encode(), pointerx.String(x.NewUUID().String()), nil)
+
+		require.Contains(t, res.Request.URL.Path, "error-ts")
+		assert.Equal(t, int64(http.StatusNotFound), gjson.GetBytes(body, "0.code").Int(), "%s", body)
+		assert.Equal(t, "Not Found", gjson.GetBytes(body, "0.status").String(), "%s", body)
+		assert.Contains(t, gjson.GetBytes(body, "0.message").String(), "Unable to locate the resource", "%s", body)
+	})
+
+	t.Run("should redirect to login init because the request is expired", func(t *testing.T) {
+		lr := nlr(-time.Hour)
+		res, body := makeRequest(lr, url.Values{
+			"identifier": {"identifier"},
+			"password":   {"password"},
+		}.Encode(), nil, nil)
+
+		require.Contains(t, res.Request.URL.Path, "login-ts")
+		assert.NotEqual(t, lr.ID, gjson.GetBytes(body, "id"))
+		assert.Contains(t, gjson.GetBytes(body, "methods.oidc.config.errors.0").String(), "expired", "%s", body)
+		assert.Contains(t, gjson.GetBytes(body, "methods.password.config.errors.0").String(), "expired", "%s", body)
+	})
+
+	t.Run("should return an error because the credentials are invalid (user does not exist)", func(t *testing.T) {
+		lr := nlr(time.Hour)
+		res, body := makeRequest(lr, url.Values{
+			"identifier": {"identifier"},
+			"password":   {"password"},
+		}.Encode(), nil, nil)
+
+		require.Contains(t, res.Request.URL.Path, "login-ts")
+		assert.Equal(t, lr.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body)
+		assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String())
+		assert.Equal(t, `the provided credentials are invalid, check for spelling mistakes in your password or username, email address, or phone number`, gjson.GetBytes(body, "methods.password.config.errors.0.message").String())
+	})
+
+	t.Run("should return an error because no identifier is set", func(t *testing.T) {
+		lr := nlr(time.Hour)
+		res, body := makeRequest(lr, url.Values{
+			"password": {"password"},
+		}.Encode(), nil, nil)
+
+		require.Contains(t, res.Request.URL.Path, "login-ts")
+		// Let's ensure that the payload is being propagated properly.
+		assert.Equal(t, lr.ID.String(), gjson.GetBytes(body, "id").String())
+		assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String())
+		ensureFieldsExist(t, body)
+		assert.Equal(t, "missing properties: identifier", gjson.GetBytes(body, "methods.password.config.fields.#(name==identifier).errors.0.message").String(), "%s", body)
+
+		// The password value should not be returned!
+		assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==password).value").String())
+	})
+
+	t.Run("should return an error because no password is set", func(t *testing.T) {
+		lr := nlr(time.Hour)
+		res, body := makeRequest(lr, url.Values{
+			"identifier": {"identifier"},
+		}.Encode(), nil, nil)
+
+		require.Contains(t, res.Request.URL.Path, "login-ts")
+		// Let's ensure that the payload is being propagated properly.
+		assert.Equal(t, lr.ID.String(), gjson.GetBytes(body, "id").String())
+		assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String())
+		ensureFieldsExist(t, body)
+		assert.Equal(t, "missing properties: password", gjson.GetBytes(body, "methods.password.config.fields.#(name==password).errors.0.message").String(), "%s", body)
+
+		assert.Equal(t, "anti-rf-token", gjson.GetBytes(body, "methods.password.config.fields.#(name==csrf_token).value").String())
+		assert.Equal(t, "identifier", gjson.GetBytes(body, "methods.password.config.fields.#(name==identifier).value").String(), "%s", body)
+
+		// This must not include the password!
+		assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==password).value").String())
+	})
+
+	t.Run("should return an error because the credentials are invalid (password not correct)", func(t *testing.T) {
+		identifier, pwd := "login-identifier-6", "password"
+		createIdentity(identifier, pwd)
+
+		lr := nlr(time.Hour)
+		res, body := makeRequest(lr, url.Values{
+			"identifier": {identifier},
+			"password":   {"not-password"},
+		}.Encode(), nil, nil)
+
+		require.Contains(t, res.Request.URL.Path, "login-ts")
+
+		assert.Equal(t, lr.ID.String(), gjson.GetBytes(body, "id").String())
+		assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String())
+		ensureFieldsExist(t, body)
+		assert.Equal(t,
+			errorsx.Cause(schema.NewInvalidCredentialsError()).(*jsonschema.ValidationError).Message,
+			gjson.GetBytes(body, "methods.password.config.errors.0.message").String(),
+			"%s", body,
+		)
+
+		// This must not include the password!
+		assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==password).value").String())
+	})
+
+	t.Run("should pass because everything is a-ok", func(t *testing.T) {
+		identifier, pwd := "login-identifier-7", "password"
+		createIdentity(identifier, pwd)
+
+		lr := nlr(time.Hour)
+		res, body := makeRequest(lr, url.Values{
+			"identifier": {identifier},
+			"password":   {pwd},
+		}.Encode(), nil, nil)
+
+		require.Contains(t, res.Request.URL.Path, "return-ts", "%s", res.Request.URL.String())
+		assert.Equal(t, identifier, gjson.GetBytes(body, "identity.traits.subject").String(), "%s", body)
+	})
+
+	t.Run("should return an error because not passing validation and reset previous errors and values", func(t *testing.T) {
+		lr := &login.Request{
+			ID:        x.NewUUID(),
+			ExpiresAt: time.Now().Add(time.Minute),
+			Methods: map[identity.CredentialsType]*login.RequestMethod{
+				identity.CredentialsTypePassword: {
+					Method: identity.CredentialsTypePassword,
+					Config: &login.RequestMethodConfig{
+						RequestMethodConfigurator: &password.RequestMethod{
+							HTMLForm: &form.HTMLForm{
+								Method: "POST",
+								Action: "/action",
+								Errors: []form.Error{{Message: "some error"}},
+								Fields: form.Fields{
+									{
+										Value:  "baz",
+										Name:   "identifier",
+										Errors: []form.Error{{Message: "err"}},
+									},
+									{
+										Value:  "bar",
+										Name:   "password",
+										Errors: []form.Error{{Message: "err"}},
 									},
 								},
 							},
@@ -327,78 +316,66 @@ func TestLogin(t *testing.T) {
 					},
 				},
 			},
-			payload: url.Values{
-				"identifier": {"registration-identifier-9"},
-				// "password": {uuid.New().String()},
-			}.Encode(),
-			assert: func(t *testing.T, tc testCase, r *http.Response) {
-				require.Contains(t, r.Request.URL.Path, "login-ts")
-				body, err := ioutil.ReadAll(r.Body)
-				require.NoError(t, err)
-
-				assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String())
-				assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String())
-				ensureFieldsExist(t, body)
-
-				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==identity).value"))
-				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==identity).error"))
-				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.error"))
-				assert.Contains(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==password).errors.0").String(), "missing properties: password", "%s", body)
-			},
-		},
-	} {
-		t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) {
-			_, reg := internal.NewRegistryDefault(t)
-			s := reg.LoginStrategies().MustStrategy(identity.CredentialsTypePassword).(*password.Strategy)
-			s.WithTokenGenerator(x.FakeCSRFTokenGeneratorWithToken("anti-rf-token"))
-			reg.LoginStrategies().MustStrategy(identity.CredentialsTypeOIDC).(*oidc.Strategy).WithTokenGenerator(x.FakeCSRFTokenGeneratorWithToken("anti-rf-token"))
-			reg.LoginHandler().WithTokenGenerator(x.FakeCSRFTokenGeneratorWithToken("anti-rf-token"))
-			reg.SelfServiceErrorManager().WithTokenGenerator(x.FakeCSRFTokenGeneratorWithToken("anti-rf-token"))
-
-			router := x.NewRouterPublic()
-			admin := x.NewRouterAdmin()
-			reg.LoginHandler().RegisterPublicRoutes(router)
-			reg.LoginHandler().RegisterAdminRoutes(admin)
-			s.RegisterLoginRoutes(router)
-			ts := httptest.NewServer(router)
-			defer ts.Close()
-
-			errTs, uiTs, returnTs := errorx.NewErrorTestServer(t, reg), httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-				e, err := reg.LoginRequestPersister().GetLoginRequest(context.Background(), x.ParseUUID(r.URL.Query().Get("request")))
-				require.NoError(t, err)
-				reg.Writer().Write(w, r, e)
-			})), newReturnTs(t, reg)
-			defer errTs.Close()
-			defer uiTs.Close()
-			defer returnTs.Close()
-
-			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")
-
-			if tc.prep != nil {
-				tc.prep(t, reg)
-			}
-
-			tc.ar.RequestURL = ts.URL
-			require.NoError(t, reg.LoginRequestPersister().CreateLoginRequest(context.TODO(), tc.ar))
-
-			c := ts.Client()
-			c.Jar, _ = cookiejar.New(&cookiejar.Options{})
-
-			requestID := tc.ar.ID.String()
-			if tc.forceRequestID != nil {
-				requestID = *tc.forceRequestID
-			}
-
-			res, err := c.Post(ts.URL+password.LoginPath+"?request="+requestID, "application/x-www-form-urlencoded", strings.NewReader(tc.payload))
-			require.NoError(t, err)
-			defer res.Body.Close()
-			require.EqualValues(t, http.StatusOK, res.StatusCode, "Request: %+v\n\t\tResponse: %s", res.Request, res)
-
-			tc.assert(t, tc, res)
-		})
-	}
+		}
+
+		res, body := makeRequest(lr, url.Values{
+			"identifier": {"registration-identifier-9"},
+			// "password": {uuid.New().String()},
+		}.Encode(), nil, nil)
+
+		require.Contains(t, res.Request.URL.Path, "login-ts")
+		assert.Equal(t, lr.ID.String(), gjson.GetBytes(body, "id").String())
+		assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String())
+		ensureFieldsExist(t, body)
+
+		assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==identity).value"))
+		assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==identity).error"))
+		assert.Empty(t, gjson.GetBytes(body, "methods.password.config.error"))
+		assert.Contains(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==password).errors.0").String(), "missing properties: password", "%s", body)
+	})
+
+	t.Run("should be a new session with forced flag", func(t *testing.T) {
+		identifier, pwd := "login-identifier-reauth", "password"
+		createIdentity(identifier, pwd)
+
+		jar, err := cookiejar.New(&cookiejar.Options{})
+		require.NoError(t, err)
+		_, body1 := makeRequest(nlr(time.Hour), url.Values{
+			"identifier": {identifier},
+			"password":   {pwd},
+		}.Encode(), nil, jar)
+
+		lr2 := nlr(time.Hour)
+		lr2.Forced = true
+		res, body2 := makeRequest(lr2, url.Values{
+			"identifier": {identifier},
+			"password":   {pwd},
+		}.Encode(), nil, jar)
+
+		require.Contains(t, res.Request.URL.Path, "return-ts", "%s", res.Request.URL.String())
+		assert.Equal(t, identifier, gjson.GetBytes(body2, "identity.traits.subject").String(), "%s", body2)
+		assert.NotEqual(t, gjson.GetBytes(body1, "sid").String(), gjson.GetBytes(body2, "sid").String(), "%s\n\n%s\n", body1, body2)
+	})
+
+	t.Run("should be the same session without forced flag", func(t *testing.T) {
+		identifier, pwd := "login-identifier-no-reauth", "password"
+		createIdentity(identifier, pwd)
+
+		jar, err := cookiejar.New(&cookiejar.Options{})
+		require.NoError(t, err)
+		_, body1 := makeRequest(nlr(time.Hour), url.Values{
+			"identifier": {identifier},
+			"password":   {pwd},
+		}.Encode(), nil, jar)
+
+		lr2 := nlr(time.Hour)
+		res, body2 := makeRequest(lr2, url.Values{
+			"identifier": {identifier},
+			"password":   {pwd},
+		}.Encode(), nil, jar)
+
+		require.Contains(t, res.Request.URL.Path, "return-ts", "%s", res.Request.URL.String())
+		assert.Equal(t, identifier, gjson.GetBytes(body2, "identity.traits.subject").String(), "%s", body2)
+		assert.Equal(t, gjson.GetBytes(body1, "sid").String(), gjson.GetBytes(body2, "sid").String(), "%s\n\n%s\n", body1, body2)
+	})
 }
diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go
index 2da98c0ec..92233f5b8 100644
--- a/selfservice/strategy/password/registration_test.go
+++ b/selfservice/strategy/password/registration_test.go
@@ -114,8 +114,7 @@ func TestRegistration(t *testing.T) {
 			return rr
 		}
 
-		var makeRequest = func(t *testing.T, rid uuid.UUID, body string, expectedStatusCode int) ([]byte, *http.Response) {
-			jar, _ := cookiejar.New(&cookiejar.Options{})
+		var makeRequestWithCookieJar = func(t *testing.T, rid uuid.UUID, body string, expectedStatusCode int, jar *cookiejar.Jar) ([]byte, *http.Response) {
 			client := http.Client{Jar: jar}
 			res, err := client.Post(ts.URL+password.RegistrationPath+"?request="+rid.String(), "application/x-www-form-urlencoded", strings.NewReader(body))
 			require.NoError(t, err)
@@ -126,6 +125,11 @@ func TestRegistration(t *testing.T) {
 			return result, res
 		}
 
+		var makeRequest = func(t *testing.T, rid uuid.UUID, body string, expectedStatusCode int) ([]byte, *http.Response) {
+			jar, _ := cookiejar.New(&cookiejar.Options{})
+			return makeRequestWithCookieJar(t, rid, body, expectedStatusCode, jar)
+		}
+
 		t.Run("case=should show the error ui because the request payload is malformed", func(t *testing.T) {
 			rr := newRegistrationRequest(t, time.Minute)
 			body, res := makeRequest(t, rr.ID, "14=)=!(%)$/ZP()GHIÖ", http.StatusOK)
@@ -288,6 +292,23 @@ func TestRegistration(t *testing.T) {
 			assert.Contains(t, res.Request.URL.Path, "return-ts")
 			assert.Equal(t, `registration-identifier-10`, gjson.GetBytes(body, "identity.traits.username").String(), "%s", body)
 		})
+
+		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"},
+				"password":        {"O(lf<ys87LÖ:(h<dsjfl"},
+				"traits.foobar":   {"bar"},
+			}.Encode()
+			rr1 := newRegistrationRequest(t, time.Minute)
+			body1, res1 := makeRequestWithCookieJar(t, rr1.ID, formValues, http.StatusOK, jar)
+			assert.Contains(t, res1.Request.URL.Path, "return-ts")
+			rr2 := newRegistrationRequest(t, time.Minute)
+			body2, res2 := makeRequestWithCookieJar(t, rr2.ID, formValues, http.StatusOK, jar)
+			assert.Contains(t, res2.Request.URL.Path, "default-return-to")
+			assert.Equal(t, body1, body2)
+		})
 	})
 
 	t.Run("method=PopulateSignUpMethod", func(t *testing.T) {