Files
upstream-ory-kratos/selfservice/strategy/webauthn/login_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

1030 lines
43 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package webauthn_test
import (
"context"
_ "embed"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/go-webauthn/webauthn/protocol"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/x/configx"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/pkg"
kratos "github.com/ory/kratos/pkg/httpclient"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/selfservice/strategy/idfirst"
"github.com/ory/kratos/selfservice/strategy/webauthn"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
"github.com/ory/x/assertx"
"github.com/ory/x/contextx"
"github.com/ory/x/jsonx"
"github.com/ory/x/snapshotx"
)
var (
//go:embed fixtures/login/success/mfa/response.invalid.json
loginFixtureSuccessResponseInvalid []byte
//go:embed fixtures/login/success/mfa/identity.json
loginFixtureSuccessIdentity []byte
//go:embed fixtures/login/success/mfa/v0/credentials.json
loginFixtureSuccessV0Credentials []byte
//go:embed fixtures/login/success/mfa/v0/internal_context.json
loginFixtureSuccessV0Context []byte
//go:embed fixtures/login/success/mfa/v0/response.json
loginFixtureSuccessV0Response []byte
//go:embed fixtures/login/success/mfa/v1/credentials.json
loginFixtureSuccessV1Credentials []byte
//go:embed fixtures/login/success/mfa/v1/internal_context.json
loginFixtureSuccessV1Context []byte
//go:embed fixtures/login/success/mfa/v1/response.json
loginFixtureSuccessV1Response []byte
//go:embed fixtures/login/success/mfa/v1_handle/credentials.json
loginFixtureSuccessV1WithHandleCredentials []byte
//go:embed fixtures/login/success/mfa/v1_handle/internal_context.json
loginFixtureSuccessV1WithHandleContext []byte
//go:embed fixtures/login/success/mfa/v1_handle/response.json
loginFixtureSuccessV1WithHandleResponse []byte
//go:embed fixtures/login/success/mfa/v1_passwordless/credentials.json
loginFixtureSuccessV1PasswordlessCredentials []byte
//go:embed fixtures/login/success/mfa/v1_passwordless/internal_context.json
loginFixtureSuccessV1PasswordlessContext []byte
//go:embed fixtures/login/success/mfa/v1_passwordless/response.json
loginFixtureSuccessV1PasswordlessResponse []byte
)
var loginFixtureSuccessEmail = gjson.GetBytes(loginFixtureSuccessIdentity, "traits.email").String()
func TestCompleteLogin(t *testing.T) {
conf, reg := pkg.NewFastRegistryWithMocks(t,
configx.WithValue(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", false),
enabledWebauthn,
configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/login.schema.json")),
)
publicTS, _ := testhelpers.NewKratosServer(t, reg)
_ = testhelpers.NewErrorTestServer(t, reg)
uiTS := testhelpers.NewLoginUIFlowEchoServer(t, reg)
redirTS := testhelpers.NewRedirSessionEchoTS(t, reg)
checkURL := func(t *testing.T, shouldRedirect bool, res *http.Response) {
if shouldRedirect {
assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login-ts")
} else {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
}
}
doAPIFlow := func(t *testing.T, v func(url.Values), apiClient *http.Client, opts ...testhelpers.InitFlowWithOption) (string, *http.Response) {
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, opts...)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
payload := testhelpers.EncodeFormAsJSON(t, true, values)
return testhelpers.LoginMakeRequest(t, true, false, f, apiClient, payload)
}
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), browserClient *http.Client, opts ...testhelpers.InitFlowWithOption) (string, *http.Response) {
f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, spa, false, false, opts...)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
return testhelpers.LoginMakeRequest(t, false, spa, f, browserClient, values.Encode())
}
createIdentityWithWebAuthn := func(t *testing.T, c identity.Credentials) *identity.Identity {
var id identity.Identity
require.NoError(t, json.Unmarshal(loginFixtureSuccessIdentity, &id))
id.SetCredentials(identity.CredentialsTypeWebAuthn, identity.Credentials{
Identifiers: []string{loginFixtureSuccessEmail},
Config: c.Config,
Type: identity.CredentialsTypeWebAuthn,
Version: c.Version,
})
// We clean up the identity in case it has been created before
_ = reg.PrivilegedIdentityPool().DeleteIdentity(context.Background(), id.ID)
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &id))
return &id
}
submitWebAuthnLoginFlowWithClient := func(t *testing.T, isSPA bool, f *kratos.LoginFlow, contextFixture []byte, client *http.Client, cb func(values url.Values)) (string, *http.Response, *kratos.LoginFlow) {
// We inject the session to replay
interim, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id))
require.NoError(t, err)
interim.InternalContext = contextFixture
require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(context.Background(), interim))
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
cb(values)
// We use the response replay
body, res := testhelpers.LoginMakeRequest(t, false, isSPA, f, client, values.Encode())
return body, res, f
}
submitWebAuthnLoginWithClient := func(t *testing.T, isSPA bool, id *identity.Identity, contextFixture []byte, client *http.Client, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) {
f := testhelpers.InitializeLoginFlowViaBrowser(t, client, publicTS, false, isSPA, false, false, opts...)
return submitWebAuthnLoginFlowWithClient(t, isSPA, f, contextFixture, client, cb)
}
submitWebAuthnLogin := func(t *testing.T, isSPA bool, id *identity.Identity, contextFixture []byte, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) {
browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id)
return submitWebAuthnLoginWithClient(t, isSPA, id, contextFixture, browserClient, cb, opts...)
}
t.Run("flow=refresh", func(t *testing.T) {
conf.MustSet(t.Context(), config.ViperKeySessionWhoAmIAAL, "aal1")
t.Cleanup(func() {
conf.MustSet(t.Context(), config.ViperKeySessionWhoAmIAAL, nil)
})
run := func(t *testing.T, id *identity.Identity, context, response []byte, isSPA bool, expectedAAL identity.AuthenticatorAssuranceLevel, expectTriggers bool) {
body, res, f := submitWebAuthnLogin(t, isSPA, id, context, func(values url.Values) {
values.Set("identifier", loginFixtureSuccessEmail)
values.Set(node.WebAuthnLogin, string(response))
},
testhelpers.InitFlowWithRefresh(),
testhelpers.InitFlowWithAAL(expectedAAL),
)
snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
"3.attributes.nonce",
"3.attributes.src",
"4.attributes.value",
"4.attributes.onclick",
})
nodes, err := json.Marshal(f.Ui.Nodes)
require.NoError(t, err)
if !expectTriggers {
assert.Falsef(t, gjson.GetBytes(nodes, "#(attributes.name==identifier)").Exists(), "%s", nodes)
return
}
assert.Equal(t, loginFixtureSuccessEmail, gjson.GetBytes(nodes, "#(attributes.name==identifier).attributes.value").String(), "%s", nodes)
prefix := ""
if isSPA {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow, "%s", body)
prefix = "session."
} else {
assert.Contains(t, res.Request.URL.String(), redirTS.URL, "%s", body)
}
assert.True(t, gjson.Get(body, prefix+"active").Bool(), "%s", body)
assert.EqualValues(t, expectedAAL, gjson.Get(body, prefix+"authenticator_assurance_level").String(), "%s", body)
assert.EqualValues(t, identity.CredentialsTypeWebAuthn, gjson.Get(body, prefix+"authentication_methods.#(method==webauthn).method").String(), "%s", body)
assert.Len(t, gjson.Get(body, prefix+"authentication_methods").Array(), 2, "%s", body)
assert.EqualValues(t, id.ID.String(), gjson.Get(body, prefix+"identity.id").String(), "%s", body)
}
t.Run("case=passwordless", func(t *testing.T) {
for _, e := range []bool{
true,
false,
} {
conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, e)
expectedAAL := identity.AuthenticatorAssuranceLevel1
if !e {
// If passwordless is disabled, using WebAuthn means that we have a second factor enabled.
// Thus, AAL2 :)
expectedAAL = identity.AuthenticatorAssuranceLevel2
}
for _, tc := range []struct {
creds identity.Credentials
response []byte
context []byte
descript string
expectTriggers bool
}{
{
creds: identity.Credentials{
Config: loginFixtureSuccessV0Credentials,
Version: 0,
},
context: loginFixtureSuccessV0Context,
response: loginFixtureSuccessV0Response,
descript: "mfa v0 credentials",
expectTriggers: !e,
},
{
creds: identity.Credentials{
Config: loginFixtureSuccessV1Credentials,
Version: 1,
},
context: loginFixtureSuccessV1Context,
response: loginFixtureSuccessV1Response,
descript: "mfa v1 credentials",
expectTriggers: !e,
},
{
creds: identity.Credentials{
Config: loginFixtureSuccessV1PasswordlessCredentials,
Version: 1,
},
context: loginFixtureSuccessV1PasswordlessContext,
response: loginFixtureSuccessV1PasswordlessResponse,
descript: "passwordless credentials",
expectTriggers: e,
},
} {
t.Run(fmt.Sprintf("passwordless enabled=%v/case=%s", e, tc.descript), func(t *testing.T) {
id := createIdentityWithWebAuthn(t, tc.creds)
for _, f := range []string{
"browser",
"spa",
} {
t.Run(f, func(t *testing.T) {
run(t, id, tc.context, tc.response, f == "spa", expectedAAL, tc.expectTriggers)
})
}
})
}
}
})
t.Run("case=no webauth credentials", func(t *testing.T) {
for _, e := range []bool{true, false} {
conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, e)
t.Run(fmt.Sprintf("passwordless=%v", e), func(t *testing.T) {
for _, f := range []string{"browser", "spa"} {
t.Run(f, func(t *testing.T) {
id := identity.NewIdentity("")
id.NID = x.NewUUID()
client := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, client, publicTS, true, f == "spa", false, false)
snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
})
nodes, err := json.Marshal(f.Ui.Nodes)
require.NoError(t, err)
assert.False(t, gjson.GetBytes(nodes, "#(attributes.name==identifier).attributes.value").Bool(), "%s", nodes)
assert.False(t, gjson.GetBytes(nodes, "#(attributes.name=="+node.WebAuthnLoginTrigger+").attributes.value").Bool(), "%s", nodes)
})
}
})
}
})
})
t.Run("flow=passwordless", func(t *testing.T) {
conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, true)
t.Cleanup(func() {
conf.MustSet(t.Context(), config.ViperKeyWebAuthnPasswordless, false)
})
t.Run("case=webauthn button exists", func(t *testing.T) {
client := testhelpers.NewClientWithCookies(t)
f := testhelpers.InitializeLoginFlowViaBrowser(t, client, publicTS, false, true, false, false)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"})
})
t.Run("case=webauthn shows error if user tries to sign in but no such user exists", func(t *testing.T) {
payload := func(v url.Values) {
v.Set("method", identity.CredentialsTypeWebAuthn.String())
v.Set("identifier", "doesnotexist")
}
check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) {
checkURL(t, shouldRedirect, res)
assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
assert.Equal(t, text.NewErrorValidationSuchNoWebAuthnUser().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
}
t.Run("type=browser", func(t *testing.T) {
body, res := doBrowserFlow(t, false, payload, testhelpers.NewClientWithCookies(t))
check(t, true, body, res)
})
t.Run("type=spa", func(t *testing.T) {
body, res := doBrowserFlow(t, true, payload, testhelpers.NewClientWithCookies(t))
check(t, false, body, res)
})
})
t.Run("case=webauthn shows error if user tries to sign in but user has no webauth credentials set up", func(t *testing.T) {
id, subject := createIdentityAndReturnIdentifier(t.Context(), t, reg, nil)
id.DeleteCredentialsType(identity.CredentialsTypeWebAuthn)
require.NoError(t, reg.IdentityManager().Update(t.Context(), id, identity.ManagerAllowWriteProtectedTraits))
payload := func(v url.Values) {
v.Set("method", identity.CredentialsTypeWebAuthn.String())
v.Set("identifier", subject)
}
check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) {
checkURL(t, shouldRedirect, res)
assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
assert.Equal(t, text.NewErrorValidationSuchNoWebAuthnUser().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
}
t.Run("type=browser", func(t *testing.T) {
body, res := doBrowserFlow(t, false, payload, testhelpers.NewClientWithCookies(t))
check(t, true, body, res)
})
t.Run("type=spa", func(t *testing.T) {
body, res := doBrowserFlow(t, true, payload, testhelpers.NewClientWithCookies(t))
check(t, false, body, res)
})
})
t.Run("case=webauthn MFA credentials can not be used for passwordless login", func(t *testing.T) {
_, subject := createIdentityAndReturnIdentifier(t.Context(), t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","is_passwordless":false}]}`))
payload := func(v url.Values) {
v.Set("method", identity.CredentialsTypeWebAuthn.String())
v.Set("identifier", subject)
}
check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) {
checkURL(t, shouldRedirect, res)
assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
assert.Equal(t, text.NewErrorValidationSuchNoWebAuthnUser().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
}
t.Run("type=browser", func(t *testing.T) {
body, res := doBrowserFlow(t, false, payload, testhelpers.NewClientWithCookies(t))
check(t, true, body, res)
})
t.Run("type=spa", func(t *testing.T) {
body, res := doBrowserFlow(t, true, payload, testhelpers.NewClientWithCookies(t))
check(t, false, body, res)
})
})
t.Run("case=should fail if webauthn login is invalid", func(t *testing.T) {
_, subject := createIdentityAndReturnIdentifier(t.Context(), t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
doBrowserFlow := func(t *testing.T, spa bool, browserClient *http.Client, opts ...testhelpers.InitFlowWithOption) {
f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, spa, false, false, opts...)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", identity.CredentialsTypeWebAuthn.String())
values.Set("identifier", subject)
body, res := testhelpers.LoginMakeRequest(t, false, spa, f, browserClient, values.Encode())
if spa {
assert.Equal(t, http.StatusUnprocessableEntity, res.StatusCode)
redir := gjson.Get(body, "redirect_browser_to").String()
assert.NotEmpty(t, redir)
res, err := browserClient.Get(redir)
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
raw, err := io.ReadAll(res.Body)
require.NoError(t, err)
body = string(raw)
} else {
checkURL(t, !spa, res)
}
assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptNestedKeys("value", "src", "nonce", "action", "request_url", "issued_at", "expires_at", "created_at", "updated_at", "id", "onclick"))
assert.Equal(t, text.NewInfoLoginWebAuthnPasswordless().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
values.Set(node.WebAuthnLogin, string(loginFixtureSuccessResponseInvalid))
values.Set("identifier", subject)
body, res = testhelpers.LoginMakeRequest(t, false, spa, f, browserClient, values.Encode())
checkURL(t, !spa, res)
assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
assert.Equal(t, "The provided web authn login is invalid, please try again.", gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
}
t.Run("type=browser", func(t *testing.T) {
doBrowserFlow(t, false, testhelpers.NewClientWithCookies(t))
})
t.Run("type=spa", func(t *testing.T) {
doBrowserFlow(t, true, testhelpers.NewClientWithCookies(t))
})
})
t.Run("case=succeeds with passwordless login", func(t *testing.T) {
run := func(t *testing.T, spa bool) {
conf.MustSet(t.Context(), config.ViperKeySessionWhoAmIAAL, "aal1")
// We load our identity which we will use to replay the webauth session
id := createIdentityWithWebAuthn(t, identity.Credentials{
Config: loginFixtureSuccessV1PasswordlessCredentials,
Version: 1,
})
browserClient := testhelpers.NewClientWithCookies(t)
body, res, f := submitWebAuthnLoginWithClient(t, spa, id, loginFixtureSuccessV1PasswordlessContext, browserClient, func(values url.Values) {
values.Set("identifier", loginFixtureSuccessEmail)
values.Set(node.WebAuthnLogin, string(loginFixtureSuccessV1PasswordlessResponse))
}, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel1))
prefix := ""
if spa {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
prefix = "session."
} else {
assert.Contains(t, res.Request.URL.String(), redirTS.URL)
}
assert.True(t, gjson.Get(body, prefix+"active").Bool(), "%s", body)
assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, gjson.Get(body, prefix+"authenticator_assurance_level").String(), "%s", body)
assert.EqualValues(t, identity.CredentialsTypeWebAuthn, gjson.Get(body, prefix+"authentication_methods.#(method==webauthn).method").String(), "%s", body)
assert.EqualValues(t, id.ID.String(), gjson.Get(body, prefix+"identity.id").String(), "%s", body)
actualFlow, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id))
require.NoError(t, err)
assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeWebAuthn, webauthn.InternalContextKeySessionData)))
if spa {
assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(t.Context()).String(), "%s", body)
} else {
assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
}
}
t.Run("type=browser", func(t *testing.T) {
run(t, false)
})
t.Run("type=spa", func(t *testing.T) {
run(t, true)
})
})
})
t.Run("flow=mfa", func(t *testing.T) {
t.Run("case=webauthn payload is set when identity has webauthn", func(t *testing.T) {
id := createIdentity(t.Context(), t, reg)
apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, apiClient, publicTS, false, true, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assert.Equal(t, gjson.GetBytes(id.Traits, "subject").String(), f.Ui.Nodes[1].Attributes.UiNodeInputAttributes.Value, jsonx.TestMarshalJSONString(t, f.Ui))
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
"1.attributes.value",
"3.attributes.src",
"3.attributes.nonce",
"4.attributes.onclick",
"4.attributes.onload",
"4.attributes.value",
})
ensureReplacement(t, "4", f.Ui, "allowCredentials")
})
t.Run("case=webauthn payload is not set when identity has no webauthn", func(t *testing.T) {
id := createIdentityWithoutWebAuthn(t, reg)
apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, apiClient, publicTS, false, true, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
})
})
t.Run("case=webauthn payload is not set for API clients", func(t *testing.T) {
id := createIdentity(t.Context(), t, reg)
apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assertx.EqualAsJSON(t, nil, f.Ui.Nodes)
})
doAPIFlowSignedIn := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
return doAPIFlow(t, v, testhelpers.NewHTTPClientWithIdentitySessionToken(t.Context(), t, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
}
doBrowserFlowSignIn := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
return doBrowserFlow(t, spa, v, testhelpers.NewHTTPClientWithIdentitySessionCookie(t.Context(), t, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
}
t.Run("case=should refuse to execute api flow", func(t *testing.T) {
id := createIdentity(t.Context(), t, reg)
payload := func(v url.Values) {
v.Set(node.WebAuthnLogin, "{}")
}
body, res := doAPIFlowSignedIn(t, payload, id)
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
assert.Equal(t, "Could not find a strategy to log you in with. Did you fill out the form correctly?", gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
})
t.Run("case=should fail if webauthn login is invalid", func(t *testing.T) {
id, sub := createIdentityAndReturnIdentifier(t.Context(), t, reg, nil)
payload := func(v url.Values) {
v.Set("identifier", sub)
v.Set(node.WebAuthnLogin, string(loginFixtureSuccessResponseInvalid))
}
check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) {
checkURL(t, shouldRedirect, res)
assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
assert.Equal(t, "The provided web authn login is invalid, please try again.", gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
}
t.Run("type=browser", func(t *testing.T) {
body, res := doBrowserFlowSignIn(t, false, payload, id)
check(t, true, body, res)
})
t.Run("type=spa", func(t *testing.T) {
body, res := doBrowserFlowSignIn(t, true, payload, id)
check(t, false, body, res)
})
})
t.Run("case=can not use security key for passwordless in mfa flow", func(t *testing.T) {
id := createIdentityWithWebAuthn(t, identity.Credentials{
Config: loginFixtureSuccessV1PasswordlessCredentials,
Version: 1,
})
body, res, _ := submitWebAuthnLogin(t, true, id, loginFixtureSuccessV1PasswordlessContext, func(values url.Values) {
values.Set("identifier", loginFixtureSuccessEmail)
values.Set(node.WebAuthnLogin, string(loginFixtureSuccessV1PasswordlessResponse))
}, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
snapshotx.SnapshotTExcept(t, json.RawMessage(gjson.Get(body, "ui.messages").Raw), []string{})
})
t.Run("case=login with a security key using", func(t *testing.T) {
idd := uuid.FromStringOrNil("44fc22c9-abae-4c3e-a56b-37c7b38d973e")
out, err := json.Marshal(identity.CredentialsWebAuthnConfig{UserHandle: idd[:]})
require.NoError(t, err)
t.Logf("json: %s", out)
out, err = json.Marshal(protocol.AuthenticatorAssertionResponse{UserHandle: idd[:]})
require.NoError(t, err)
t.Logf("wa: %s", out)
for _, tc := range []struct {
d string
v int
cf []byte
sf []byte
ix []byte
}{
{d: "v0 without userhandle", v: 0, cf: loginFixtureSuccessV0Credentials, sf: loginFixtureSuccessV0Response, ix: loginFixtureSuccessV0Context},
{d: "v1 without userhandle", v: 1, cf: loginFixtureSuccessV1Credentials, sf: loginFixtureSuccessV1Response, ix: loginFixtureSuccessV1Context},
{d: "v1 with differing userhandle", v: 1, cf: loginFixtureSuccessV1WithHandleCredentials, sf: loginFixtureSuccessV1WithHandleResponse, ix: loginFixtureSuccessV1WithHandleContext},
} {
t.Run(tc.d, func(t *testing.T) {
run := func(t *testing.T, spa bool) {
// We load our identity which we will use to replay the webauth session
id := createIdentityWithWebAuthn(t, identity.Credentials{
Config: tc.cf,
Version: tc.v,
})
body, res, f := submitWebAuthnLogin(t, spa, id, tc.ix, func(values url.Values) {
values.Set("identifier", loginFixtureSuccessEmail)
values.Set(node.WebAuthnLogin, string(tc.sf))
}, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
prefix := ""
if spa {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
prefix = "session."
} else {
assert.Contains(t, res.Request.URL.String(), redirTS.URL)
}
assert.True(t, gjson.Get(body, prefix+"active").Bool(), "%s", body)
assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, gjson.Get(body, prefix+"authenticator_assurance_level").String(), "%s", body)
assert.EqualValues(t, identity.CredentialsTypeWebAuthn, gjson.Get(body, prefix+"authentication_methods.#(method==webauthn).method").String(), "%s", body)
assert.EqualValues(t, id.ID.String(), gjson.Get(body, prefix+"identity.id").String(), "%s", body)
actualFlow, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id))
require.NoError(t, err)
assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeWebAuthn, webauthn.InternalContextKeySessionData)))
}
t.Run("type=browser", func(t *testing.T) {
run(t, false)
})
t.Run("type=spa", func(t *testing.T) {
run(t, true)
})
})
}
})
})
t.Run("case=should be able to log in with phone number identifier", func(t *testing.T) {
// Test backward compatibility with a legacy identity whose credential
// identifier is stored in non-normalized form in the database.
// Note: AAL2 (MFA) login resolves the identity from the session, not via
// credential identifier lookup, so both normalized and non-normalized
// identifiers succeed regardless of the DB state.
nonNormalizedPhone := "+44 20 7946 0958"
normalizedPhone := "+442079460958"
phoneID := &identity.Identity{
Traits: identity.Traits(fmt.Sprintf(`{"phone":"%s"}`, nonNormalizedPhone)),
}
phoneID.SetCredentials(identity.CredentialsTypeWebAuthn, identity.Credentials{
Identifiers: []string{nonNormalizedPhone},
Config: loginFixtureSuccessV1Credentials,
Type: identity.CredentialsTypeWebAuthn,
Version: 1,
})
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), phoneID))
t.Cleanup(func() {
_ = reg.PrivilegedIdentityPool().DeleteIdentity(context.Background(), phoneID.ID)
})
// CreateIdentity normalizes the credential identifier to E.164 format and
// may update the credential version. To simulate a legacy database record
// created before normalization was introduced, revert both via raw SQL.
require.NoError(t, reg.Persister().GetConnection(t.Context()).RawQuery(
"UPDATE identity_credential_identifiers SET identifier = ? WHERE identity_id = ? AND identifier = ?",
nonNormalizedPhone, phoneID.ID, normalizedPhone,
).Exec())
require.NoError(t, reg.Persister().GetConnection(t.Context()).RawQuery(
"UPDATE identity_credentials SET version = 0 WHERE identity_id = ?",
phoneID.ID,
).Exec())
run := func(t *testing.T, spa bool, identifier string) {
body, res, f := submitWebAuthnLogin(t, spa, phoneID, loginFixtureSuccessV1Context, func(values url.Values) {
values.Set("identifier", identifier)
values.Set(node.WebAuthnLogin, string(loginFixtureSuccessV1Response))
}, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
prefix := ""
if spa {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
prefix = "session."
} else {
assert.Contains(t, res.Request.URL.String(), redirTS.URL)
}
assert.True(t, gjson.Get(body, prefix+"active").Bool(), "%s", body)
assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, gjson.Get(body, prefix+"authenticator_assurance_level").String(), "%s", body)
assert.EqualValues(t, phoneID.ID.String(), gjson.Get(body, prefix+"identity.id").String(), "%s", body)
actualFlow, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id))
require.NoError(t, err)
assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeWebAuthn, webauthn.InternalContextKeySessionData)))
}
t.Run("identifier=non-normalized", func(t *testing.T) {
t.Run("type=browser", func(t *testing.T) {
run(t, false, nonNormalizedPhone)
})
t.Run("type=spa", func(t *testing.T) {
run(t, true, nonNormalizedPhone)
})
})
t.Run("identifier=normalized", func(t *testing.T) {
t.Run("type=browser", func(t *testing.T) {
run(t, false, normalizedPhone)
})
t.Run("type=spa", func(t *testing.T) {
run(t, true, normalizedPhone)
})
})
})
}
func TestFormHydration(t *testing.T) {
ctx := context.Background()
_, reg := pkg.NewFastRegistryWithMocks(t)
ctx = contextx.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".enabled", true)
ctx = contextx.WithConfigValue(
ctx,
config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config",
map[string]interface{}{
"rp": map[string]interface{}{
"display_name": "foo",
"id": "localhost",
"origins": []string{"http://localhost"},
},
},
)
ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/login.schema.json")
s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeWebAuthn)
require.NoError(t, err)
fhAAL1, ok := s.(login.AAL1FormHydrator)
require.True(t, ok)
fhAAL2, ok := s.(login.AAL2FormHydrator)
require.True(t, ok)
toSnapshot := func(t *testing.T, f *login.Flow) {
t.Helper()
// The CSRF token has a unique value that messes with the snapshot - ignore it.
f.UI.Nodes.ResetNodes("csrf_token")
f.UI.Nodes.ResetNodes("identifier")
f.UI.Nodes.ResetNodes("webauthn_login_trigger")
snapshotx.SnapshotT(t, f.UI.Nodes, snapshotx.ExceptNestedKeys("onclick", "nonce", "src"))
}
newFlowInternal := func(ctx context.Context, t *testing.T, identitySchema string) (*http.Request, *login.Flow) {
query := ""
if identitySchema != "" {
query = "?identity_schema=" + identitySchema
}
r := httptest.NewRequest("GET", "/self-service/login/browser"+query, nil)
r = r.WithContext(ctx)
t.Helper()
f, err := login.NewFlow(reg, r, flow.TypeBrowser)
f.UI.Nodes = make(node.Nodes, 0)
require.NoError(t, err)
return r, f
}
newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) {
return newFlowInternal(ctx, t, "")
}
newFlowWithIdentitySchema := func(ctx context.Context, t *testing.T, identitySchema string) (*http.Request, *login.Flow) {
return newFlowInternal(ctx, t, identitySchema)
}
passwordlessEnabled := contextx.WithConfigValue(ctx, config.ViperKeyWebAuthnPasswordless, true)
mfaEnabled := contextx.WithConfigValue(ctx, config.ViperKeyWebAuthnPasswordless, false)
t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
id := createIdentity(ctx, t, reg)
headers := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
t.Run("case=passwordless enabled", func(t *testing.T) {
r, f := newFlow(passwordlessEnabled, t)
r.Header = headers
f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
require.NoError(t, fhAAL2.PopulateLoginMethodSecondFactor(r, f))
toSnapshot(t, f)
})
t.Run("case=mfa enabled", func(t *testing.T) {
r, f := newFlow(mfaEnabled, t)
r.Header = headers
f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
require.NoError(t, fhAAL2.PopulateLoginMethodSecondFactor(r, f))
toSnapshot(t, f)
})
})
t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
t.Run("case=passwordless enabled", func(t *testing.T) {
r, f := newFlow(passwordlessEnabled, t)
require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactor(r, f))
toSnapshot(t, f)
})
t.Run("case=mfa enabled", func(t *testing.T) {
r, f := newFlow(mfaEnabled, t)
require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactor(r, f))
toSnapshot(t, f)
})
})
t.Run("method=PopulateLoginMethodRefresh", func(t *testing.T) {
t.Run("case=passwordless enabled but user has no passwordless credentials", func(t *testing.T) {
id := createIdentity(ctx, t, reg)
r, f := newFlow(passwordlessEnabled, t)
r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
f.Refresh = true
require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactorRefresh(r, f, nil))
toSnapshot(t, f)
})
t.Run("case=passwordless enabled and user has passwordless credentials", func(t *testing.T) {
id, _ := createIdentityAndReturnIdentifier(ctx, t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
r, f := newFlow(passwordlessEnabled, t)
r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
f.Refresh = true
require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactorRefresh(r, f, nil))
toSnapshot(t, f)
})
t.Run("case=mfa enabled and user has mfa credentials", func(t *testing.T) {
id := createIdentity(ctx, t, reg)
r, f := newFlow(mfaEnabled, t)
r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
f.Refresh = true
f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactorRefresh(r, f, nil))
toSnapshot(t, f)
})
t.Run("case=mfa enabled but user has passwordless credentials", func(t *testing.T) {
id, _ := createIdentityAndReturnIdentifier(ctx, t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
r, f := newFlow(mfaEnabled, t)
r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
f.Refresh = true
f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactorRefresh(r, f, nil))
toSnapshot(t, f)
})
})
t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) {
t.Run("case=no options", func(t *testing.T) {
t.Run("case=passwordless enabled", func(t *testing.T) {
t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
r, f := newFlow(
contextx.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false),
t,
)
require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
toSnapshot(t, f)
})
t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
r, f := newFlow(
contextx.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
t,
)
require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
toSnapshot(t, f)
})
})
t.Run("case=mfa enabled", func(t *testing.T) {
t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
r, f := newFlow(
contextx.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false),
t,
)
require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
toSnapshot(t, f)
})
t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
r, f := newFlow(
contextx.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
t,
)
require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
toSnapshot(t, f)
})
})
})
t.Run("case=WithIdentifier", func(t *testing.T) {
t.Run("case=passwordless enabled", func(t *testing.T) {
t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
r, f := newFlow(
contextx.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false),
t,
)
require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
toSnapshot(t, f)
})
t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
r, f := newFlow(
contextx.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
t,
)
require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
toSnapshot(t, f)
})
})
t.Run("case=mfa enabled", func(t *testing.T) {
t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
r, f := newFlow(
contextx.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false),
t,
)
require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
toSnapshot(t, f)
})
t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
r, f := newFlow(
contextx.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
t,
)
require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
toSnapshot(t, f)
})
})
})
t.Run("case=WithIdentityHint", func(t *testing.T) {
t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
mfaEnabled := contextx.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true)
passwordlessEnabled := contextx.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true)
id := identity.NewIdentity("test-provider")
t.Run("case=passwordless enabled", func(t *testing.T) {
r, f := newFlow(passwordlessEnabled, t)
require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
toSnapshot(t, f)
})
t.Run("case=mfa enabled", func(t *testing.T) {
r, f := newFlow(mfaEnabled, t)
require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
toSnapshot(t, f)
})
})
t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
mfaEnabled := contextx.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false)
passwordlessEnabled := contextx.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false)
id, _ := createIdentityAndReturnIdentifier(ctx, t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
t.Run("case=identity has webauthn", func(t *testing.T) {
t.Run("case=passwordless enabled", func(t *testing.T) {
r, f := newFlow(passwordlessEnabled, t)
require.NoError(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
toSnapshot(t, f)
})
t.Run("case=mfa enabled", func(t *testing.T) {
r, f := newFlow(mfaEnabled, t)
require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
toSnapshot(t, f)
})
})
t.Run("case=identity does not have a webauthn", func(t *testing.T) {
t.Run("case=passwordless enabled", func(t *testing.T) {
id := identity.NewIdentity("default")
r, f := newFlow(passwordlessEnabled, t)
require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
toSnapshot(t, f)
})
t.Run("case=mfa enabled", func(t *testing.T) {
id := identity.NewIdentity("default")
r, f := newFlow(mfaEnabled, t)
require.ErrorIs(t, fhAAL1.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
toSnapshot(t, f)
})
})
})
})
})
t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) {
t.Run("case=passwordless enabled", func(t *testing.T) {
r, f := newFlow(passwordlessEnabled, t)
require.NoError(t, fhAAL1.PopulateLoginMethodIdentifierFirstIdentification(r, f))
toSnapshot(t, f)
})
t.Run("case=mfa enabled", func(t *testing.T) {
r, f := newFlow(mfaEnabled, t)
require.NoError(t, fhAAL1.PopulateLoginMethodIdentifierFirstIdentification(r, f))
toSnapshot(t, f)
})
})
t.Run("case=Multi-Schema-method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
multiSchema := contextx.WithConfigValue(ctx, config.ViperKeyDefaultIdentitySchemaID, "default")
multiSchema = contextx.WithConfigValue(multiSchema, config.ViperKeyIdentitySchemas, config.Schemas{
{ID: "default", URL: "file://./stub/missing-identifier.schema.json"},
{ID: "not-default", URL: "file://./stub/login.schema.json", SelfserviceSelectable: true},
})
t.Run("case=passwordless enabled", func(t *testing.T) {
r, f := newFlowWithIdentitySchema(contextx.WithConfigValue(multiSchema, config.ViperKeyWebAuthnPasswordless, true), t, "not-default")
require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactor(r, f))
toSnapshot(t, f)
})
t.Run("case=mfa enabled", func(t *testing.T) {
r, f := newFlowWithIdentitySchema(contextx.WithConfigValue(multiSchema, config.ViperKeyWebAuthnPasswordless, false), t, "not-default")
require.NoError(t, fhAAL1.PopulateLoginMethodFirstFactor(r, f))
toSnapshot(t, f)
})
})
}