Files
upstream-ory-kratos/selfservice/flow/login/handler_test.go
upstream-mirror c60cc7ca75
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

995 lines
45 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package login_test
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"testing"
"time"
"github.com/gobuffalo/httptest"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
stdtotp "github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/x/clock"
"github.com/ory/x/httprouterx"
"github.com/ory/kratos/corpx"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/hydra"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/selfservice/strategy/totp"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/container"
"github.com/ory/kratos/x"
"github.com/ory/kratos/x/nosurfx"
"github.com/ory/x/assertx"
"github.com/ory/x/sqlxx"
"github.com/ory/x/urlx"
)
func init() {
corpx.RegisterFakes()
}
func TestFlowLifecycle(t *testing.T) {
ctx := context.Background()
conf, reg := pkg.NewFastRegistryWithMocks(t)
fakeHydra := hydra.NewFake()
reg.SetHydra(fakeHydra)
routerPublic := httprouterx.NewRouterPublic()
ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, routerPublic, httprouterx.NewRouterAdminWithPrefix())
loginTS := testhelpers.NewLoginUIFlowEchoServer(t, reg)
returnToTS := testhelpers.NewRedirTS(t, "return_to", conf)
errorTS := testhelpers.NewErrorTestServer(t, reg)
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToTS.URL)
conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{
{ID: "default", URL: "file://./stub/password.schema.json"},
{ID: "email", URL: "file://./stub/email.schema.json", SelfserviceSelectable: true},
{ID: "phone", URL: "file://./stub/phone.schema.json", SelfserviceSelectable: true},
{ID: "not-allowed", URL: "file://./stub/password.schema.json"},
})
conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default")
assertion := func(body []byte, isForced, isApi bool) {
r := gjson.GetBytes(body, "refresh")
assert.True(t, r.Exists(), "%s", body)
assert.Equal(t, isForced, r.Bool(), "%s", body)
if isApi {
assert.Equal(t, "api", gjson.GetBytes(body, "type").String())
} else {
assert.Equal(t, "browser", gjson.GetBytes(body, "type").String())
}
}
initAuthenticatedFlow := func(t *testing.T, extQuery url.Values, isAPI bool) (*http.Response, []byte) {
route := login.RouteInitBrowserFlow
if isAPI {
route = login.RouteInitAPIFlow
}
req := testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+route, nil)
req.URL.RawQuery = extQuery.Encode()
body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, routerPublic, req)
if isAPI {
assert.Len(t, res.Header.Get("Set-Cookie"), 0)
}
return res, body
}
initUnauthenticatedFlow := func(t *testing.T, extQuery url.Values, isAPI bool) (*http.Response, []byte) {
route := login.RouteInitBrowserFlow
if isAPI {
route = login.RouteInitAPIFlow
}
client := ts.Client()
req := testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+route, nil)
req.URL.RawQuery = extQuery.Encode()
res, err := client.Do(req)
require.NoError(t, errors.WithStack(err))
body, err := io.ReadAll(res.Body)
require.NoError(t, errors.WithStack(err))
require.NoError(t, res.Body.Close())
return res, body
}
initFlowWithAccept := func(t *testing.T, query url.Values, isAPI bool, accept string) (*http.Response, []byte) {
route := login.RouteInitBrowserFlow
if isAPI {
route = login.RouteInitAPIFlow
}
c := ts.Client()
req, err := http.NewRequest("GET", ts.URL+route+"?"+query.Encode(), nil)
require.NoError(t, err)
if accept != "" {
req.Header.Set("Accept", accept)
}
res, err := c.Do(req)
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
return res, body
}
initFlow := func(t *testing.T, query url.Values, isAPI bool) (*http.Response, []byte) {
return initFlowWithAccept(t, query, isAPI, "")
}
initSPAFlow := func(t *testing.T, query url.Values) (*http.Response, []byte) {
return initFlowWithAccept(t, query, false, "application/json")
}
id1mail, id2mail := x.NewUUID().String(), x.NewUUID().String()
identity1 := &identity.Identity{
Credentials: map[identity.CredentialsType]identity.Credentials{
identity.CredentialsTypePassword: {
Type: identity.CredentialsTypePassword,
Identifiers: []string{id1mail},
Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), // foobar
},
},
State: identity.StateActive,
Traits: identity.Traits(`{"username":"` + id1mail + `"}`),
}
identity2 := &identity.Identity{
Credentials: map[identity.CredentialsType]identity.Credentials{
identity.CredentialsTypePassword: {
Type: identity.CredentialsTypePassword,
Identifiers: []string{id2mail},
Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), // foobar
},
},
State: identity.StateActive,
Traits: identity.Traits(`{"username":"` + id2mail + `"}`),
}
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), identity1))
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), identity2))
t.Run("lifecycle=submit", func(t *testing.T) {
t.Run("interaction=unauthenticated", func(t *testing.T) {
run := func(t *testing.T, tt flow.Type, aal string, values url.Values) (string, *http.Response) {
f := login.Flow{
Type: tt, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(),
UI: container.New(""), Refresh: false, RequestedAAL: identity.AuthenticatorAssuranceLevel(aal),
}
require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f))
res, err := testhelpers.NewTestClient(t).PostForm(ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), values)
require.NoError(t, err)
body := x.MustReadAll(res.Body)
require.NoError(t, res.Body.Close())
return string(body), res
}
t.Run("case=ensure aal can only be upgraded with a session on submit", func(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
body, res := run(t, flow.TypeAPI, "aal2", url.Values{"method": {"password"}})
assert.Contains(t, res.Request.URL.String(), login.RouteSubmitFlow)
assertx.EqualAsJSON(t, "You can not requested a higher AAL (AAL2/AAL3) without an active session.", gjson.Get(body, "error.reason").String())
})
t.Run("type=browser", func(t *testing.T) {
body, res := run(t, flow.TypeBrowser, "aal2", url.Values{"method": {"password"}})
assert.Contains(t, res.Request.URL.String(), errorTS.URL)
assertx.EqualAsJSON(t, "You can not requested a higher AAL (AAL2/AAL3) without an active session.", gjson.Get(body, "reason").String())
})
})
t.Run("case=end up with method missing when aal is ok", func(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
body, res := run(t, flow.TypeAPI, "aal1", url.Values{"method": {"not-exist"}})
assert.Contains(t, res.Request.URL.String(), login.RouteSubmitFlow)
assertx.EqualAsJSON(t, text.NewErrorValidationLoginNoStrategyFound().Text, gjson.Get(body, "ui.messages.0.text").String(), body)
})
t.Run("type=browser", func(t *testing.T) {
body, res := run(t, flow.TypeBrowser, "aal1", url.Values{"method": {"not-exist"}})
assert.Contains(t, res.Request.URL.String(), loginTS.URL)
assertx.EqualAsJSON(t, text.NewErrorValidationLoginNoStrategyFound().Text, gjson.Get(body, "ui.messages.0.text").String(), body)
})
})
t.Run("case=reset the session when refresh is true but identity is different", func(t *testing.T) {
testhelpers.NewRedirSessionEchoTS(t, reg)
t.Cleanup(func() {
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToTS.URL)
})
run := func(t *testing.T, tt flow.Type) (string, string) {
f := login.Flow{Type: tt, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), UI: container.New(""), Refresh: false, RequestedAAL: "aal1"}
require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f))
hc := testhelpers.NewClientWithCookies(t)
res, err := hc.PostForm(ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), url.Values{"method": {"password"}, "password_identifier": {id1mail}, "password": {"foobar"}, "csrf_token": {nosurfx.FakeCSRFToken}})
require.NoError(t, err)
firstSession := x.MustReadAll(res.Body)
require.NoError(t, res.Body.Close())
f = login.Flow{Type: tt, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), UI: container.New(""), Refresh: true, RequestedAAL: "aal1"}
require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f))
vv := testhelpers.EncodeFormAsJSON(t, tt == flow.TypeAPI, url.Values{"method": {"password"}, "password_identifier": {id2mail}, "password": {"foobar"}, "csrf_token": {nosurfx.FakeCSRFToken}})
req, err := http.NewRequest("POST", ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), strings.NewReader(vv))
require.NoError(t, err)
if tt == flow.TypeAPI {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+gjson.GetBytes(firstSession, "session_token").String())
} else {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
res, err = hc.Do(req)
require.NoError(t, err)
secondSession := x.MustReadAll(res.Body)
require.NoError(t, res.Body.Close())
return string(firstSession), string(secondSession)
}
t.Run("type=browser", func(t *testing.T) {
a, b := run(t, flow.TypeBrowser)
assert.Equal(t, id1mail, gjson.Get(a, "identity.traits.username").String())
assert.Equal(t, id2mail, gjson.Get(b, "identity.traits.username").String())
assert.NotEmpty(t, gjson.Get(b, "id").String())
assert.NotEqual(t, gjson.Get(b, "id").String(), gjson.Get(a, "id").String())
})
t.Run("type=api", func(t *testing.T) {
a, b := run(t, flow.TypeAPI)
assert.Equal(t, id1mail, gjson.Get(a, "session.identity.traits.username").String())
assert.Equal(t, id2mail, gjson.Get(b, "session.identity.traits.username").String())
assert.NotEmpty(t, gjson.Get(a, "session_token").String())
assert.NotEqual(t, gjson.Get(a, "session_token").String(), gjson.Get(b, "session_token").String())
assert.NotEmpty(t, gjson.Get(b, "session.id").String())
assert.NotEqual(t, gjson.Get(b, "session.id").String(), gjson.Get(a, "id").String())
})
})
t.Run("case=changed kratos session identifiers when refresh is true", func(t *testing.T) {
t.Cleanup(func() {
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToTS.URL)
})
t.Run("type=browser", func(t *testing.T) {
// Setup flow
f := login.Flow{Type: flow.TypeBrowser, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), UI: container.New(""), Refresh: false, RequestedAAL: "aal1"}
require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f))
// Submit Login
hc := testhelpers.NewClientWithCookies(t)
res, err := hc.PostForm(ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), url.Values{"method": {"password"}, "password_identifier": {id1mail}, "password": {"foobar"}, "csrf_token": {nosurfx.FakeCSRFToken}})
require.NoError(t, err)
// Check response and session cookie presence
assert.Equal(t, http.StatusOK, res.StatusCode)
require.Len(t, hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL+login.RouteGetFlow)), 1)
require.Contains(t, fmt.Sprintf("%v", hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL))), "ory_kratos_session")
cookies1 := hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL + login.RouteGetFlow))
req, err := http.NewRequest("GET", ts.URL+"/sessions/whoami", nil)
require.NoError(t, err)
res, err = hc.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
firstSession := x.MustReadAll(res.Body)
require.NoError(t, res.Body.Close())
// Refresh
f = login.Flow{Type: flow.TypeBrowser, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), UI: container.New(""), Refresh: true, RequestedAAL: "aal1"}
require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f))
vv := testhelpers.EncodeFormAsJSON(t, false, url.Values{"method": {"password"}, "password_identifier": {id1mail}, "password": {"foobar"}, "csrf_token": {nosurfx.FakeCSRFToken}})
req, err = http.NewRequest("POST", ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), strings.NewReader(vv))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// Submit Login
res, err = hc.Do(req)
require.NoError(t, err)
// Check response and session cookie presence
assert.Equal(t, http.StatusOK, res.StatusCode)
require.Len(t, hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL+login.RouteGetFlow)), 1)
require.Contains(t, fmt.Sprintf("%v", hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL))), "ory_kratos_session")
cookies2 := hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL + login.RouteGetFlow))
req, err = http.NewRequest("GET", ts.URL+"/sessions/whoami", nil)
require.NoError(t, err)
res, err = hc.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
secondSession := x.MustReadAll(res.Body)
require.NoError(t, res.Body.Close())
// Sessions should still be resolvable despite different kratos session identifier due to nonce
assert.NotEqual(t, cookies1[0].String(), cookies2[0].String())
assert.Equal(t, id1mail, gjson.Get(string(firstSession), "identity.traits.username").String())
assert.Equal(t, id1mail, gjson.Get(string(secondSession), "identity.traits.username").String())
assert.Equal(t, gjson.Get(string(secondSession), "id").String(), gjson.Get(string(firstSession), "id").String())
})
})
})
t.Run("case=ensure aal is checked for upgradeability on session", func(t *testing.T) {
run := func(t *testing.T, tt flow.Type, values url.Values) (string, *http.Response) {
f := login.Flow{
Type: tt, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(),
UI: container.New(""), Refresh: false, RequestedAAL: "aal1",
}
require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f))
req, err := http.NewRequest("GET", ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), strings.NewReader(values.Encode()))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, routerPublic, req)
return string(body), res
}
t.Run("type=api", func(t *testing.T) {
body, res := run(t, flow.TypeAPI, url.Values{"method": {"password"}})
assert.Contains(t, res.Request.URL.String(), login.RouteSubmitFlow)
assertx.EqualAsJSON(t, login.ErrAlreadyLoggedIn().Reason(), gjson.Get(body, "ui.messages.0.text").String(), body)
})
t.Run("type=browser", func(t *testing.T) {
_, res := run(t, flow.TypeBrowser, url.Values{"method": {"password"}})
assert.Contains(t, res.Request.URL.String(), returnToTS.URL)
})
})
t.Run("case=should return an error because the request is expired", func(t *testing.T) {
conf.MustSet(ctx, config.ViperKeySelfServiceLoginRequestLifespan, "50ms")
t.Cleanup(func() {
conf.MustSet(ctx, config.ViperKeySelfServiceLoginRequestLifespan, "10m")
})
// Freeze the clock so the handler and the expected error read the same instant. The "expired N minutes
// ago" reason is then deterministic instead of depending on the request round-trip time.
now := time.Now().UTC()
reg.SetClock(clock.NewMock(now))
t.Cleanup(func() { reg.SetClock(clock.New()) })
expired := now.Add(-time.Minute)
run := func(t *testing.T, tt flow.Type, aal string, values string, isSPA bool) (string, *http.Response) {
f := login.Flow{
Type: tt, ExpiresAt: expired, IssuedAt: now,
UI: container.New(""), Refresh: false, RequestedAAL: identity.AuthenticatorAssuranceLevel(aal),
}
require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f))
req, err := http.NewRequest("POST", ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), strings.NewReader(values))
require.NoError(t, err)
if isSPA {
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
} else {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
res, err := testhelpers.NewTestClient(t).Do(req)
require.NoError(t, err)
body := x.MustReadAll(res.Body)
require.NoError(t, res.Body.Close())
return string(body), res
}
t.Run("type=api", func(t *testing.T) {
actual, res := run(t, flow.TypeAPI, "aal1", `{"method":"password"}`, false)
assert.Contains(t, res.Request.URL.String(), login.RouteSubmitFlow)
assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String())
assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(reg.Clock(), expired), json.RawMessage(actual), []string{"use_flow_id", "since"}, "expired", "%s", actual)
})
t.Run("type=browser", func(t *testing.T) {
body, res := run(t, flow.TypeBrowser, "aal1", url.Values{"method": {"password"}}.Encode(), false)
assert.Contains(t, res.Request.URL.String(), loginTS.URL)
assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "expired", "%s", body)
})
t.Run("type=SPA", func(t *testing.T) {
actual, res := run(t, flow.TypeBrowser, "aal1", `{"method":"password"}`, true)
assert.Contains(t, res.Request.URL.String(), login.RouteSubmitFlow)
assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String())
assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(reg.Clock(), expired), json.RawMessage(actual), []string{"use_flow_id", "since"}, "expired", "%s", actual)
})
})
t.Run("case=should return to settings flow after successful mfa login after recovery", func(t *testing.T) {
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL)
conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL)
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeTOTP.String(), true)
conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToTS.URL})
t.Cleanup(func() {
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, string(identity.AuthenticatorAssuranceLevel1))
conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, string(identity.AuthenticatorAssuranceLevel1))
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeTOTP.String(), false)
})
key, err := totp.NewKey(context.Background(), "foo", reg)
require.NoError(t, err)
email := testhelpers.RandomEmail()
id := &identity.Identity{
Credentials: map[identity.CredentialsType]identity.Credentials{
"password": {
Type: "password",
Identifiers: []string{email},
Config: sqlxx.JSONRawMessage(`{"hashed_password": "$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`),
},
},
Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, email)),
SchemaID: config.DefaultIdentityTraitsSchemaID,
}
require.NoError(t, reg.IdentityManager().CreateIdentities(context.Background(), []*identity.Identity{id}, identity.ManagerAllowWriteProtectedTraits))
id.SetCredentials(identity.CredentialsTypeTOTP, identity.Credentials{
Type: identity.CredentialsTypeTOTP,
Identifiers: []string{id.ID.String()},
Config: sqlxx.JSONRawMessage(`{"totp_url":"` + string(key.URL()) + `"}`),
})
require.NoError(t, reg.IdentityManager().Update(context.Background(), id, identity.ManagerAllowWriteProtectedTraits))
h := func(w http.ResponseWriter, r *http.Request) {
sess, err := testhelpers.NewActiveSession(r, reg, id, time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1)
require.NoError(t, err)
sess.AuthenticatorAssuranceLevel = identity.AuthenticatorAssuranceLevel1
require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), sess))
require.NoError(t, reg.SessionManager().IssueCookie(context.Background(), w, r, sess))
require.Equal(t, identity.AuthenticatorAssuranceLevel1, sess.AuthenticatorAssuranceLevel)
}
routerPublic.GET("/mock-session", h)
client := testhelpers.NewClientWithCookies(t)
testhelpers.MockHydrateCookieClient(t, client, ts.URL+"/mock-session")
settingsURL := ts.URL + settings.RouteInitBrowserFlow + "?return_to=" + url.QueryEscape(returnToTS.URL)
req, err := http.NewRequest("GET", settingsURL, nil)
require.NoError(t, err)
// we initialize the settings flow with a session that has AAL1 set
resp, err := client.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// we expect the request to redirect to the login flow because the AAL1 session is not sufficient
requestURL, err := url.Parse(resp.Request.Referer())
require.NoError(t, err)
require.Equal(t, login.RouteInitBrowserFlow, requestURL.Path)
require.Equal(t, "aal2", requestURL.Query().Get("aal"))
require.Equal(t, settingsURL, requestURL.Query().Get("return_to"))
// we expect to be on the login page now
respURL := resp.Request.URL
require.NoError(t, err)
require.Equal(t, "/login-ts", respURL.Path)
flowID := respURL.Query().Get("flow")
require.NotEmpty(t, flowID)
code, err := stdtotp.GenerateCode(key.Secret(), time.Now())
require.NoError(t, err)
req, err = http.NewRequest("GET", ts.URL+login.RouteGetFlow+"?id="+flowID, nil)
require.NoError(t, err)
req.Header.Add("Content-Type", "application/json")
resp, err = client.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
body := string(x.MustReadAll(resp.Body))
defer func() { _ = resp.Body.Close() }()
totpNode := gjson.Get(body, "ui.nodes.#(attributes.name==totp_code)").String()
require.NotEmpty(t, totpNode)
require.NotEmpty(t, gjson.Get(body, "ui.action").String())
csrfToken := gjson.Get(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String()
req, err = http.NewRequest("POST", ts.URL+login.RouteSubmitFlow+"?flow="+flowID, strings.NewReader(url.Values{
"method": {"totp"},
"totp_code": {code},
"csrf_token": {csrfToken},
}.Encode()))
require.NoError(t, err)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
resp, err = client.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusSeeOther, resp.StatusCode)
location, err := resp.Location()
require.NoError(t, err)
require.Equal(t, settings.RouteInitBrowserFlow, location.Path)
})
})
t.Run("lifecycle=init", func(t *testing.T) {
t.Run("suite=identity schema in query", func(t *testing.T) {
for _, tc := range []struct {
name string
query url.Values
wantErr bool
wantIdentifier string
}{{
name: "not-allowed",
query: url.Values{"identity_schema": {"not-allowed"}},
wantErr: true,
}, {
name: "not-found",
query: url.Values{"identity_schema": {"not-found"}},
wantErr: true,
}, {
name: "phone",
query: url.Values{"identity_schema": {"phone"}},
wantIdentifier: "Phone Number",
}, {
name: "email",
query: url.Values{"identity_schema": {"email"}},
wantIdentifier: "E-Mail Address",
}, {
name: "default",
query: url.Values{"identity_schema": {"default"}},
wantIdentifier: "Username",
}} {
t.Run("case="+tc.name, func(t *testing.T) {
t.Run("flow=api", func(t *testing.T) {
res, body := initFlow(t, tc.query, true)
if tc.wantErr {
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
return
}
assert.Equalf(t, tc.wantIdentifier, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier).meta.label.text").String(), "%s", body)
})
t.Run("flow=browser", func(t *testing.T) {
res, body := initFlow(t, tc.query, false)
if tc.wantErr {
require.Contains(t, res.Request.URL.String(), errorTS.URL, "%s", body)
assert.EqualValues(t, "Bad Request", gjson.GetBytes(body, "status").String(), "%s", body)
return
}
assert.Equalf(t, tc.wantIdentifier, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier).meta.label.context.title").String(), "%s", body)
})
})
}
})
t.Run("flow=api", func(t *testing.T) {
t.Run("case=does not set forced flag on unauthenticated request", func(t *testing.T) {
res, body := initFlow(t, url.Values{}, true)
assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow)
assertion(body, false, true)
assert.Empty(t, gjson.GetBytes(body, "session_token_exchange_code").String())
})
t.Run("case=returns session exchange code with any truthy value", func(t *testing.T) {
conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToTS.URL, "https://example.com"})
parameters := []string{"true", "True", "1"}
for _, param := range parameters {
t.Run("return_session_token_exchange_code="+param, func(t *testing.T) {
res, body := initFlow(t, url.Values{
"return_session_token_exchange_code": {param},
"return_to": {"https://example.com/redirect"},
}, true)
assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow)
assert.NotEmpty(t, gjson.GetBytes(body, "session_token_exchange_code").String())
assert.Equal(t, "https://example.com/redirect", gjson.GetBytes(body, "return_to").String())
})
}
})
t.Run("case=can not request refresh and aal at the same time on unauthenticated request", func(t *testing.T) {
res, body := initFlow(t, url.Values{"refresh": {"true"}, "aal": {"aal2"}}, true)
assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow)
assertx.EqualAsJSON(t, "You can not requested a higher AAL (AAL2/AAL3) without an active session.", gjson.GetBytes(body, "error.reason").String())
})
t.Run("case=can request refresh and aal at the same time on authenticated request", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{"refresh": {"true"}, "aal": {"aal2"}}, true)
assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow)
assertx.EqualAsJSON(t, "Please confirm this action by verifying that it is you.", gjson.GetBytes(body, "ui.messages.0.text").String(), "%s", body)
assertx.EqualAsJSON(t, "Please complete the second authentication challenge.", gjson.GetBytes(body, "ui.messages.1.text").String(), "%s", body)
})
t.Run("case=can not request aal2 on unauthenticated request", func(t *testing.T) {
res, body := initFlow(t, url.Values{"aal": {"aal2"}}, true)
assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow)
assertx.EqualAsJSON(t, "You can not requested a higher AAL (AAL2/AAL3) without an active session.", gjson.GetBytes(body, "error.reason").String())
})
t.Run("case=ignores aal1 if session has aal1 already", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{"aal": {"aal1"}}, true)
assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow)
assertx.EqualAsJSON(t, "A valid session was detected and thus login is not possible. Did you forget to set `?refresh=true`?", gjson.GetBytes(body, "error.reason").String())
})
t.Run("case=aal0 is not a valid value", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{"aal": {"aal0"}}, true)
assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow)
assertx.EqualAsJSON(t, "Unable to parse AuthenticationMethod Assurance Level (AAL): expected one of [aal1, aal2] but got aal0", gjson.GetBytes(body, "error.reason").String())
})
t.Run("case=indicates two factor auth", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{"aal": {"aal2"}}, true)
assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow)
assert.Equal(t, gjson.GetBytes(body, "ui.messages.0.text").String(), text.NewInfoLoginMFA().Text)
})
t.Run("case=does not set forced flag on unauthenticated request with refresh=true", func(t *testing.T) {
res, body := initFlow(t, url.Values{"refresh": {"true"}}, true)
assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow)
assertion(body, false, true)
})
t.Run("case=does not set forced flag on authenticated request without refresh=true", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{}, true)
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
assertx.EqualAsJSON(t, login.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body)
})
t.Run("case=does not set forced flag on authenticated request with refresh=false", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{"refresh": {"false"}}, true)
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
assertx.EqualAsJSON(t, login.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body)
})
t.Run("case=does set forced flag on authenticated request with refresh=true", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{"refresh": {"true"}}, true)
assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow)
assertion(body, true, true)
})
t.Run("case=check info message on authenticated request with refresh=true", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{"refresh": {"true"}}, true)
assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow)
assertion(body, true, true)
assert.Equal(t, gjson.GetBytes(body, "ui.messages.0.text").String(), text.NewInfoLoginReAuth().Text)
})
})
t.Run("flow=browser", func(t *testing.T) {
t.Run("case=does not set forced flag on unauthenticated request", func(t *testing.T) {
res, body := initFlow(t, url.Values{}, false)
assertion(body, false, false)
assert.Contains(t, res.Request.URL.String(), loginTS.URL)
})
t.Run("case=never returns a session token exchange code", func(t *testing.T) {
_, body := initFlow(t, urlx.ParseOrPanic("/?return_session_token_exchange_code=true").Query(), false)
assertion(body, false, false)
assert.Empty(t, gjson.GetBytes(body, "session_token_exchange_code").String())
})
t.Run("case=can not request refresh and aal at the same time on unauthenticated request", func(t *testing.T) {
res, body := initFlow(t, url.Values{"refresh": {"true"}, "aal": {"aal2"}}, false)
assert.Contains(t, res.Request.URL.String(), errorTS.URL)
assertx.EqualAsJSON(t, "You can not requested a higher AAL (AAL2/AAL3) without an active session.", gjson.GetBytes(body, "reason").String(), "%s", body)
})
t.Run("case=can request refresh and aal at the same time on authenticated request", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{"refresh": {"true"}, "aal": {"aal2"}}, false)
assert.Contains(t, res.Request.URL.String(), loginTS.URL)
assertx.EqualAsJSON(t, "Please confirm this action by verifying that it is you.", gjson.GetBytes(body, "ui.messages.0.text").String(), "%s", body)
assertx.EqualAsJSON(t, "Please complete the second authentication challenge.", gjson.GetBytes(body, "ui.messages.1.text").String(), "%s", body)
})
t.Run("case=redirects if aal2 is requested and set up already without refresh", func(t *testing.T) {
res, _ := initAuthenticatedFlow(t, url.Values{"aal": {"aal2"}, "set_aal": {"aal2"}}, false)
assert.Contains(t, res.Request.URL.String(), returnToTS.URL)
})
t.Run("case=can not request aal2 on unauthenticated request", func(t *testing.T) {
res, body := initFlow(t, url.Values{"aal": {"aal2"}}, false)
assert.Contains(t, res.Request.URL.String(), errorTS.URL)
assertx.EqualAsJSON(t, "You can not requested a higher AAL (AAL2/AAL3) without an active session.", gjson.GetBytes(body, "reason").String())
})
t.Run("case=ignores aal1 if session has aal1 already", func(t *testing.T) {
res, _ := initAuthenticatedFlow(t, url.Values{"aal": {"aal1"}}, false)
assert.Contains(t, res.Request.URL.String(), returnToTS.URL)
})
t.Run("case=aal0 is not a valid value", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{"aal": {"aal0"}}, false)
assert.Contains(t, res.Request.URL.String(), errorTS.URL)
assertx.EqualAsJSON(t, "Unable to parse AuthenticationMethod Assurance Level (AAL): expected one of [aal1, aal2] but got aal0", gjson.GetBytes(body, "reason").String())
})
t.Run("case=indicates two factor auth", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{"aal": {"aal2"}}, false)
assert.Contains(t, res.Request.URL.String(), loginTS.URL)
assert.Equal(t, gjson.GetBytes(body, "ui.messages.0.text").String(), text.NewInfoLoginMFA().Text)
})
t.Run("case=makes request with JSON", func(t *testing.T) {
res, body := initSPAFlow(t, url.Values{})
assertion(body, false, false)
assert.NotContains(t, res.Request.URL.String(), loginTS.URL)
})
t.Run("case=does not set forced flag on unauthenticated request with refresh=true", func(t *testing.T) {
res, body := initFlow(t, url.Values{"refresh": {"true"}}, false)
assertion(body, false, false)
assert.Contains(t, res.Request.URL.String(), loginTS.URL)
})
t.Run("case=does not set forced flag on authenticated request without refresh=true", func(t *testing.T) {
res, _ := initAuthenticatedFlow(t, url.Values{}, false)
assert.Contains(t, res.Request.URL.String(), returnToTS.URL)
})
t.Run("case=does not set forced flag on authenticated request with refresh=false", func(t *testing.T) {
res, _ := initAuthenticatedFlow(t, url.Values{"refresh": {"false"}}, false)
assert.Contains(t, res.Request.URL.String(), returnToTS.URL)
})
t.Run("case=does set forced flag on authenticated request with refresh=true", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{"refresh": {"true"}}, false)
assertion(body, true, false)
assert.Contains(t, res.Request.URL.String(), loginTS.URL)
})
t.Run("case=redirects with 303", func(t *testing.T) {
c := &http.Client{}
// don't get the reference, instead copy the values, so we don't alter the client directly.
*c = *ts.Client()
// prevent the redirect
c.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
req, err := http.NewRequest("GET", ts.URL+login.RouteInitBrowserFlow, nil)
require.NoError(t, err)
res, err := c.Do(req)
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
// here we check that the redirect status is 303
require.Equal(t, http.StatusSeeOther, res.StatusCode)
})
t.Run("case=refuses to parse oauth2 login challenge when Hydra is not configured", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{"login_challenge": {hydra.FakeValidLoginChallenge}}, false)
require.Contains(t, res.Request.URL.String(), errorTS.URL)
require.Contains(t, string(body), "refusing to parse")
})
conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, "https://fake-hydra")
t.Run("case=oauth2 flow init should override return_to to the oauth2 request_url", func(t *testing.T) {
conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.com", "https://example.com"})
conf.MustSet(ctx, config.ViperKeyOAuth2ProviderOverrideReturnTo, true)
t.Cleanup(func() {
conf.MustSet(ctx, config.ViperKeyOAuth2ProviderOverrideReturnTo, false)
})
res, _ := initUnauthenticatedFlow(t, url.Values{
"return_to": {"https://example.com"},
"login_challenge": {hydra.FakeValidLoginChallenge},
}, false)
require.Equal(t, http.StatusOK, res.StatusCode)
require.Contains(t, res.Request.URL.String(), loginTS.URL)
c := ts.Client()
req := testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+login.RouteGetFlow, nil)
req.URL.RawQuery = url.Values{"id": {res.Request.URL.Query().Get("flow")}}.Encode()
res, err := c.Do(req)
require.NoError(t, err)
body, err := io.ReadAll(res.Body)
require.NoError(t, errors.WithStack(err))
require.NoError(t, res.Body.Close())
assert.Equal(t, "https://www.ory.com", gjson.GetBytes(body, "return_to").Value())
})
t.Run("case=invalid oauth2 login challenge returns 400 Bad Request", func(t *testing.T) {
res, body := initAuthenticatedFlow(t, url.Values{"login_challenge": {hydra.FakeInvalidLoginChallenge}}, false)
assert.Contains(t, res.Request.URL.String(), errorTS.URL)
assert.Equal(t, int64(http.StatusBadRequest), gjson.GetBytes(body, "code").Int())
assert.Contains(t, gjson.GetBytes(body, "reason").String(), "Unable to get OAuth 2.0 Login Challenge")
})
t.Run("case=oauth2 flow init succeeds", func(t *testing.T) {
res, _ := initAuthenticatedFlow(t, url.Values{"login_challenge": {hydra.FakeValidLoginChallenge}}, false)
require.Contains(t, res.Request.URL.String(), loginTS.URL)
})
t.Run("case=oauth2 flow init adds oauth2_login_request field", func(t *testing.T) {
res, body := initSPAFlow(t, url.Values{"login_challenge": {hydra.FakeValidLoginChallenge}})
assert.NotContains(t, res.Request.URL.String(), loginTS.URL)
assert.NotEmpty(t, gjson.GetBytes(body, "oauth2_login_request").Value(), "%s", body)
})
t.Run("case=oauth2 flow with existing session and JSON request should not return null", func(t *testing.T) {
// This test reproduces issue #10255 where the /self-service/login/browser endpoint
// returns null when called with an existing session, a Hydra login challenge, and
// an Accept: application/json header.
// Has a side effect on this test suite but since its running serial and there is no significantly easier way it's acceptable.
fakeHydra.Skip = true
t.Cleanup(func() {
fakeHydra.Skip = false
})
// Make a login request with an authenticated session, Hydra login challenge, and JSON accept
req := testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+login.RouteInitBrowserFlow+"?login_challenge="+hydra.FakeValidLoginChallenge, nil)
req.Header.Set("Accept", "application/json")
body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, routerPublic, req)
// Before the fix, this would return 200 OK with "null" as body because of variable shadowing
// After the fix, it should return a proper error response with ErrAlreadyLoggedIn
assert.Equal(t, http.StatusBadRequest, res.StatusCode, "Response should be 400 Bad Request, got body: %s", body)
assert.NotEqual(t, "null", string(body), "Response should not be null")
assert.NotEmpty(t, gjson.GetBytes(body, "error.id").String(), "Should have error.id field, got body: %s", body)
assert.Equal(t, "session_already_available", gjson.GetBytes(body, "error.id").String(), "Should return session_already_available error, got body: %s", body)
assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), "A valid session was detected", "Should have proper error reason, got body: %s", body)
})
})
t.Run("case=relative redirect when self-service login ui is a relative URL", func(t *testing.T) {
reg.Config().MustSet(ctx, config.ViperKeySelfServiceLoginUI, "/login-ts")
assert.Regexp(
t,
"^/login-ts.*$",
testhelpers.GetSelfServiceRedirectLocation(t, ts.URL+login.RouteInitBrowserFlow),
)
})
})
}
func TestGetFlow(t *testing.T) {
ctx := context.Background()
conf, reg := pkg.NewFastRegistryWithMocks(t)
public, _ := testhelpers.NewKratosServerWithCSRF(t, reg)
_ = testhelpers.NewErrorTestServer(t, reg)
returnToTS := testhelpers.NewRedirTS(t, "", conf)
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/password.schema.json")
conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{
{ID: "default", URL: "file://./stub/password.schema.json"},
{ID: "email", URL: "file://./stub/email.schema.json", SelfserviceSelectable: true},
{ID: "phone", URL: "file://./stub/phone.schema.json", SelfserviceSelectable: true},
{ID: "not-allowed", URL: "file://./stub/password.schema.json"},
})
setupLoginUI := func(t *testing.T, c *http.Client) *httptest.Server {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// It is important that we use a HTTP request to fetch the flow because that will show us if CSRF works or not
_, err := w.Write(testhelpers.EasyGetBody(t, c, public.URL+login.RouteGetFlow+"?id="+r.URL.Query().Get("flow")))
require.NoError(t, err)
}))
conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, ts.URL)
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, returnToTS.URL)
t.Cleanup(ts.Close)
return ts
}
_ = testhelpers.NewLoginUIFlowEchoServer(t, reg)
conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{
"enabled": true,
})
t.Run("case=fetching successful", func(t *testing.T) {
client := testhelpers.NewClientWithCookies(t)
setupLoginUI(t, client)
body := testhelpers.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow)
assert.NotEmpty(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", body)
assert.NotEmpty(t, gjson.GetBytes(body, "id").String(), "%s", body)
assert.Empty(t, gjson.GetBytes(body, "headers").Value(), "%s", body)
assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), gjson.GetBytes(body, "id").String(), "%s", body)
assert.Contains(t, gjson.GetBytes(body, "ui.action").String(), public.URL, "%s", body)
})
t.Run("case=csrf cookie missing", func(t *testing.T) {
client := testhelpers.NewTestClient(t)
setupLoginUI(t, client)
body := testhelpers.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow)
assert.EqualValues(t, nosurfx.ErrInvalidCSRFToken().ReasonField, gjson.GetBytes(body, "error.reason").String(), "%s", body)
})
t.Run("case=expired", func(t *testing.T) {
client := testhelpers.NewClientWithCookies(t)
setupLoginUI(t, client)
body := testhelpers.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow)
// Expire the flow
f, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String()))
require.NoError(t, err)
f.ExpiresAt = time.Now().Add(-time.Second)
require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(context.Background(), f))
// Try the flow but it is expired
res, body := testhelpers.EasyGet(t, client, public.URL+login.RouteGetFlow+"?id="+f.ID.String())
assert.EqualValues(t, http.StatusGone, res.StatusCode)
assert.Equal(t, public.URL+login.RouteInitBrowserFlow, gjson.GetBytes(body, "error.details.redirect_to").String(), "%s", body)
})
t.Run("case=expired with return_to and schema_id", func(t *testing.T) {
returnTo := returnToTS.URL
conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo})
client := testhelpers.NewClientWithCookies(t)
setupLoginUI(t, client)
body := testhelpers.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow+"?return_to="+returnTo+"&identity_schema=email")
// Expire the flow
f, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String()))
require.NoError(t, err)
f.ExpiresAt = time.Now().Add(-time.Second)
require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(context.Background(), f))
// Retrieve the flow and verify that return_to is in the response
getURL := fmt.Sprintf("%s%s?id=%s&return_to=%s", public.URL, login.RouteGetFlow, f.ID, returnTo)
getBody := testhelpers.EasyGetBody(t, client, getURL)
assert.Equal(t, gjson.GetBytes(getBody, "error.details.return_to").String(), returnTo)
// submit the flow but it is expired
u := public.URL + login.RouteSubmitFlow + "?flow=" + f.ID.String()
res, err := client.PostForm(u, url.Values{"password_identifier": {"email@ory.sh"}, "csrf_token": {f.CSRFToken}, "password": {"password"}, "method": {"password"}})
require.NoError(t, err)
resBody, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.NoError(t, res.Body.Close())
f, err = reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(resBody, "id").String()))
require.NoError(t, err)
assert.Equal(t, public.URL+login.RouteInitBrowserFlow+"?return_to="+returnTo+"&identity_schema=email", f.RequestURL)
})
t.Run("case=not found", func(t *testing.T) {
client := testhelpers.NewClientWithCookies(t)
setupLoginUI(t, client)
res, _ := testhelpers.EasyGet(t, client, public.URL+login.RouteGetFlow+"?id="+x.NewUUID().String())
assert.EqualValues(t, http.StatusNotFound, res.StatusCode)
})
}