package handlers

import (
	"database/sql"
	"encoding/base64"
	"fmt"
	"net"
	"regexp"
	"testing"
	"time"

	"github.com/go-webauthn/webauthn/webauthn"
	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/valyala/fasthttp"
	"go.uber.org/mock/gomock"

	"github.com/authelia/authelia/v4/internal/authentication"
	"github.com/authelia/authelia/v4/internal/configuration/schema"
	"github.com/authelia/authelia/v4/internal/mocks"
	"github.com/authelia/authelia/v4/internal/model"
	"github.com/authelia/authelia/v4/internal/regulation"
	"github.com/authelia/authelia/v4/internal/session"
)

func TestFirstFactorPasskeyGET(t *testing.T) {
	testCases := []struct {
		name             string
		config           *schema.WebAuthn
		setup            func(t *testing.T, mock *mocks.MockAutheliaCtx)
		expected         *regexp.Regexp
		expectedStatus   int
		validateResponse func(t *testing.T, mock *mocks.MockAutheliaCtx)
	}{
		{
			"ShouldSuccess",
			&schema.DefaultWebAuthnConfiguration,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			regexp.MustCompile(`^\{"status":"OK","data":\{"publicKey":\{"challenge":"[a-zA-Z0-9/_-]+={0,2}","timeout":60000,"rpId":"login.example.com"}}}$`),
			fasthttp.StatusOK,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				require.NotNil(t, us.WebAuthn)
				require.NotNil(t, us.WebAuthn.SessionData)

				assert.Equal(t, "", us.WebAuthn.Description)
				assert.Equal(t, []byte(nil), us.WebAuthn.UserID)
			},
		},
		{
			"ShouldFailAlreadyLoggedIn",
			&schema.DefaultWebAuthnConfiguration,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.AuthenticationMethodRefs.UsernameAndPassword = true
				us.AuthenticationMethodRefs.KnowledgeBasedAuthentication = true

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			regexp.MustCompile(`^\{"status":"KO","message":"Authentication failed, please retry later."}$`),
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				assert.Nil(t, us.WebAuthn)
				assert.NoError(t, err)

				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred generating a WebAuthn passkey authentication challenge: error occurred retrieving the user session data", "user is already authenticated")
			},
		},
		{
			"ShouldFailGetSession",
			&schema.DefaultWebAuthnConfiguration,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				mock.Ctx.Request.Header.Set("X-Original-URL", "123")
				mock.Ctx.Request.Header.Set("X-Forwarded-Host", "____")
			},
			regexp.MustCompile(`^\{"status":"KO","message":"Authentication failed, please retry later."}$`),
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred generating a WebAuthn passkey authentication challenge: error occurred retrieving the user session data", "unable to retrieve session cookie domain: failed to parse X-Original-URL header: parse \"123\": invalid URI for request")
			},
		},
		{
			"ShouldFailCantGetProvider",
			&schema.DefaultWebAuthnConfiguration,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)
				require.NoError(t, mock.Ctx.SaveSession(us))

				mock.Ctx.Request.Header.Set("X-Original-URL", "123")
				mock.Ctx.Request.Header.Set("X-Forwarded-Host", "____")
			},
			regexp.MustCompile(`^\{"status":"KO","message":"Authentication failed, please retry later."}$`),
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				assert.Nil(t, us.WebAuthn)
				assert.NoError(t, err)

				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred generating a WebAuthn passkey authentication challenge: error occurred provisioning the configuration", "failed to parse X-Original-URL header: parse \"123\": invalid URI for request")
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			mock := mocks.NewMockAutheliaCtx(t)

			defer mock.Close()

			if tc.config != nil {
				mock.Ctx.Configuration.WebAuthn = *tc.config
			}

			mock.Ctx.Request.Header.Set("X-Original-URL", "https://login.example.com:8080")

			if tc.setup != nil {
				tc.setup(t, mock)
			}

			FirstFactorPasskeyGET(mock.Ctx)

			assert.Equal(t, tc.expectedStatus, mock.Ctx.Response.StatusCode())
			assert.Regexp(t, tc.expected, string(mock.Ctx.Response.Body()))

			if tc.validateResponse != nil {
				tc.validateResponse(t, mock)
			}
		})
	}
}

func TestFirstFactorPasskeyPOST(t *testing.T) {
	const (
		dataReqFmt         = `{"response":{"id":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","rawId":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","response":{"authenticatorData":"DGygg5w6VoNVeDP2GKJVZmXfKgiJZHh9U4ULStTTvtwFAAAAAw","userHandle":"ZXhhbXBsZQ","clientDataJSON":"%s","signature":"MEQCIBlJ2Fxf6ZwLNTCQglz0AW0pD4HlU8W5Yk696jjfxVxhAiAhAMkLh8iKyhW6zSmzwfQDjMF2nKjVHzEs7jLHRPDZ2A"},"type":"public-key","clientExtensionResults":{},"authenticatorAttachment":"cross-platform"},"targetURL":null}`
		dataReqFmtKLI      = `{"keepMeLoggedIn":true,"response":{"id":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","rawId":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","response":{"authenticatorData":"DGygg5w6VoNVeDP2GKJVZmXfKgiJZHh9U4ULStTTvtwFAAAAAw","userHandle":"ZXhhbXBsZQ","clientDataJSON":"%s","signature":"MEQCIBlJ2Fxf6ZwLNTCQglz0AW0pD4HlU8W5Yk696jjfxVxhAiAhAMkLh8iKyhW6zSmzwfQDjMF2nKjVHzEs7jLHRPDZ2A"},"type":"public-key","clientExtensionResults":{},"authenticatorAttachment":"cross-platform"},"targetURL":null}`
		dataReqNoHandleFmt = `{"response":{"id":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","rawId":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","response":{"authenticatorData":"DGygg5w6VoNVeDP2GKJVZmXfKgiJZHh9U4ULStTTvtwFAAAAAw","clientDataJSON":"%s","signature":"MEQCIBlJ2Fxf6ZwLNTCQglz0AW0pD4HlU8W5Yk696jjfxVxhAiAhAMkLh8iKyhW6zSmzwfQDjMF2nKjVHzEs7jLHRPDZ2A"},"type":"public-key","clientExtensionResults":{},"authenticatorAttachment":"cross-platform"},"targetURL":null}`
		dataClientJSON     = `{"type":"webauthn.get","challenge":"in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE","origin":"%s","crossOrigin":false,"other_keys_can_be_added_here":"do not compare clientDataJSON against a template. See https://goo.gl/yabPex"}`
	)

	var (
		dataReqGood         = fmt.Sprintf(dataReqFmt, base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf(dataClientJSON, "https://login.example.com:8080"))))
		dataReqGoodKLI      = fmt.Sprintf(dataReqFmtKLI, base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf(dataClientJSON, "https://login.example.com:8080"))))
		dataReqBadRPIDHash  = fmt.Sprintf(dataReqFmt, base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf(dataClientJSON, "http://example.com"))))
		dataReqNoHandleGood = fmt.Sprintf(dataReqNoHandleFmt, base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf(dataClientJSON, "https://login.example.com:8080"))))
	)

	decode := func(in string) []byte {
		value, err := base64.StdEncoding.DecodeString(in)
		if err != nil {
			t.Fatal("Failed to decode base64 string:", err)
		}

		return value
	}

	testCases := []struct {
		name           string
		config         *schema.WebAuthn
		setup          func(t *testing.T, mock *mocks.MockAutheliaCtx)
		have           string
		expected       string
		expectedStatus int
		expectedf      func(t *testing.T, mock *mocks.MockAutheliaCtx)
	}{
		{
			"ShouldFailAlreadyLoggedIn",
			&schema.DefaultWebAuthnConfiguration,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				gomock.InOrder(
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)

				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.AuthenticationMethodRefs.UsernameAndPassword = true
				us.AuthenticationMethodRefs.KnowledgeBasedAuthentication = true

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						UserID:           decode("OiRQc3wmemUzdHlkVjhVSk5Pe35YMCRCOklLYzVzIkMpaEglNkF5dnVKRSlTPCJbRDZDP102WXpiYXdNekRiTA=="),
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			dataReqGood,
			"",
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.NotNil(t, us.WebAuthn)
			},
		},
		{
			"ShouldFailGetSession",
			&schema.DefaultWebAuthnConfiguration,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				mock.Ctx.Request.Header.Set("X-Original-URL", "123")
				mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "123")
			},
			dataReqGood,
			"",
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating a WebAuthn passkey authentication challenge: error occurred retrieving the user session data", "unable to retrieve session cookie domain: failed to parse X-Original-URL header: parse \"123\": invalid URI for request")
			},
		},
		{
			"ShouldFailSessionData",
			&schema.DefaultWebAuthnConfiguration,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				gomock.InOrder(
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)

				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)
				require.NoError(t, mock.Ctx.SaveSession(us))
				mock.Ctx.Request.Header.Set("X-Original-URL", "123")
				mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "123")
			},
			dataReqGood,
			"",
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				assert.Nil(t, us.WebAuthn)
				assert.NoError(t, err)

				AssertLogEntryMessageAndError(t, mock.LogEntryN(1), "Error occurred validating a WebAuthn passkey authentication challenge: error occurred retrieving the user session data", "challenge session data is not present")
			},
		},
		{
			"ShouldFailGetProvider",
			&schema.DefaultWebAuthnConfiguration,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				gomock.InOrder(
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)

				us, err := mock.Ctx.GetSession()

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						UserID:           decode("OiRQc3wmemUzdHlkVjhVSk5Pe35YMCRCOklLYzVzIkMpaEglNkF5dnVKRSlTPCJbRDZDP102WXpiYXdNekRiTA=="),
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, err)
				require.NoError(t, mock.Ctx.SaveSession(us))

				mock.Ctx.Request.Header.Set("X-Original-URL", "123")
				mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "123")
			},
			dataReqGood,
			"",
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				assert.Nil(t, us.WebAuthn)
				assert.NoError(t, err)

				AssertLogEntryMessageAndError(t, mock.LogEntryN(1), "Error occurred validating a WebAuthn passkey authentication challenge: error occurred provisioning the configuration", "failed to parse X-Original-URL header: parse \"123\": invalid URI for request")
			},
		},
		{
			name:   "ShouldSuccess",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				credential := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       time.Now(),
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC().Add(time.Second * -10), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       2,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				updated := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       credential.CreatedAt,
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC(), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       3,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadWebAuthnPasskeyCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
						Return([]model.WebAuthnCredential{credential}, nil),
					mock.StorageMock.EXPECT().
						UpdateWebAuthnCredentialSignIn(mock.Ctx, updated).
						Return(nil),
					mock.UserProviderMock.EXPECT().
						GetDetails(gomock.Eq(testUsername)).
						Return(&authentication.UserDetails{Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadBannedIP(mock.Ctx, gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).
						Return(nil, nil),
					mock.StorageMock.EXPECT().
						LoadBannedUser(mock.Ctx, gomock.Eq(testUsername)).
						Return(nil, nil),
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: true,
							Banned:     false,
							Username:   testUsername,
							Type:       regulation.AuthTypePasskey,
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)
			},
			have:           dataReqGood,
			expectedStatus: fasthttp.StatusOK,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)
			},
		},
		{
			name:   "ShouldSuccessUpgrade",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				mock.Ctx.Configuration.WebAuthn.EnablePasskeyUpgrade = true

				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				credential := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       time.Now(),
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC().Add(time.Second * -10), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       2,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				updated := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       credential.CreatedAt,
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC(), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       3,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadWebAuthnCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
						Return([]model.WebAuthnCredential{credential}, nil),
					mock.StorageMock.EXPECT().
						UpdateWebAuthnCredentialSignIn(mock.Ctx, updated).
						Return(nil),
					mock.UserProviderMock.EXPECT().
						GetDetails(gomock.Eq(testUsername)).
						Return(&authentication.UserDetails{Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadBannedIP(mock.Ctx, gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).
						Return(nil, nil),
					mock.StorageMock.EXPECT().
						LoadBannedUser(mock.Ctx, gomock.Eq(testUsername)).
						Return(nil, nil),
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: true,
							Banned:     false,
							Username:   testUsername,
							Type:       regulation.AuthTypePasskey,
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)
			},
			have:           dataReqGood,
			expectedStatus: fasthttp.StatusOK,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)
			},
		},
		{
			name:   "ShouldNotAllowBannedUserToUsePasskey",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          mock.Ctx.Clock.Now().UTC().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				credential := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       mock.Ctx.Clock.Now().UTC().Add(time.Second * -10),
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC().Add(time.Second * -10), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       2,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				updated := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       credential.CreatedAt,
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC(), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       3,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadWebAuthnPasskeyCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
						Return([]model.WebAuthnCredential{credential}, nil),
					mock.StorageMock.EXPECT().
						UpdateWebAuthnCredentialSignIn(mock.Ctx, updated).
						Return(nil),
					mock.UserProviderMock.EXPECT().
						GetDetails(gomock.Eq(testUsername)).
						Return(&authentication.UserDetails{Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadBannedIP(mock.Ctx, gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).
						Return(nil, nil),
					mock.StorageMock.EXPECT().
						LoadBannedUser(mock.Ctx, gomock.Eq(testUsername)).
						Return([]model.BannedUser{{ID: 1, Time: mock.Ctx.Clock.Now().UTC().Add(time.Second - 10), Expires: sql.NullTime{Time: mock.Ctx.Clock.Now().UTC().Add(time.Minute), Valid: true}, Username: testUsername, Source: "Passkey"}}, nil),
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     true,
							Username:   testUsername,
							Type:       regulation.AuthTypePasskey,
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)
			},
			have:           dataReqGood,
			expectedStatus: fasthttp.StatusUnauthorized,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), regexp.MustCompile(`^Unsuccessful Passkey authentication attempt by user 'john' and they are banned until \d+:\d+:\d+(AM|PM) on \w+ \d+ \d+ \(\+\d+:\d+\)$`), nil)
			},
		},
		{
			name:   "ShouldNotAllowBannedIPToUsePasskey",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          mock.Ctx.Clock.Now().UTC().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				credential := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       mock.Ctx.Clock.Now().UTC().Add(time.Second * -10),
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC().Add(time.Second * -10), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       2,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				updated := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       credential.CreatedAt,
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC(), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       3,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadWebAuthnPasskeyCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
						Return([]model.WebAuthnCredential{credential}, nil),
					mock.StorageMock.EXPECT().
						UpdateWebAuthnCredentialSignIn(mock.Ctx, updated).
						Return(nil),
					mock.UserProviderMock.EXPECT().
						GetDetails(gomock.Eq(testUsername)).
						Return(&authentication.UserDetails{Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadBannedIP(mock.Ctx, gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).
						Return([]model.BannedIP{{ID: 1, Time: mock.Ctx.Clock.Now().UTC().Add(time.Second - 10), Expires: sql.NullTime{Time: mock.Ctx.Clock.Now().UTC().Add(time.Minute), Valid: true}}}, nil),
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     true,
							Username:   testUsername,
							Type:       regulation.AuthTypePasskey,
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)
			},
			have:           dataReqGood,
			expectedStatus: fasthttp.StatusUnauthorized,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), regexp.MustCompile(`^Unsuccessful Passkey authentication attempt by user 'john' and they are banned until \d+:\d+:\d+(AM|PM) on \w+ \d+ \d+ \(\+\d+:\d+\)$`), nil)
			},
		},
		{
			name:   "ShouldHandleBanCheckError",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          mock.Ctx.Clock.Now().UTC().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				credential := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       mock.Ctx.Clock.Now().UTC().Add(time.Second * -10),
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC().Add(time.Second * -10), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       2,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				updated := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       credential.CreatedAt,
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC(), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       3,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadWebAuthnPasskeyCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
						Return([]model.WebAuthnCredential{credential}, nil),
					mock.StorageMock.EXPECT().
						UpdateWebAuthnCredentialSignIn(mock.Ctx, updated).
						Return(nil),
					mock.UserProviderMock.EXPECT().
						GetDetails(gomock.Eq(testUsername)).
						Return(&authentication.UserDetails{Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadBannedIP(mock.Ctx, gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).
						Return(nil, fmt.Errorf("broken")),
				)
			},
			have:           dataReqGood,
			expectedStatus: fasthttp.StatusUnauthorized,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Failed to perform Passkey authentication regulation for user 'john'", "broken")
			},
		},
		{
			name:   "ShouldSuccessKeepLoggedIn",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				mock.Ctx.Configuration.Session.RememberMe = time.Hour

				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				credential := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       time.Now(),
					LastUsedAt:      sql.NullTime{Time: time.Now().UTC(), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       2,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				updated := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       credential.CreatedAt,
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC(), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       3,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadWebAuthnPasskeyCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
						Return([]model.WebAuthnCredential{credential}, nil),
					mock.StorageMock.EXPECT().
						UpdateWebAuthnCredentialSignIn(mock.Ctx, updated).
						Return(nil),
					mock.UserProviderMock.EXPECT().
						GetDetails(gomock.Eq(testUsername)).
						Return(&authentication.UserDetails{Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadBannedIP(mock.Ctx, gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).
						Return(nil, nil),
					mock.StorageMock.EXPECT().
						LoadBannedUser(mock.Ctx, gomock.Eq(testUsername)).
						Return(nil, nil),
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: true,
							Banned:     false,
							Username:   testUsername,
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)
			},
			have:           dataReqGoodKLI,
			expectedStatus: fasthttp.StatusOK,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)
				assert.True(t, us.KeepMeLoggedIn)
			},
		},
		{
			name:   "ShouldFailMark",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				credential := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       time.Now(),
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC().Add(time.Second * -10), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       2,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				updated := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       credential.CreatedAt,
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC(), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       3,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadWebAuthnPasskeyCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
						Return([]model.WebAuthnCredential{credential}, nil),
					mock.StorageMock.EXPECT().
						UpdateWebAuthnCredentialSignIn(mock.Ctx, updated).
						Return(nil),
					mock.UserProviderMock.EXPECT().
						GetDetails(gomock.Eq(testUsername)).
						Return(&authentication.UserDetails{Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadBannedIP(mock.Ctx, gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).
						Return(nil, nil),
					mock.StorageMock.EXPECT().
						LoadBannedUser(mock.Ctx, gomock.Eq(testUsername)).
						Return(nil, nil),
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: true,
							Banned:     false,
							Username:   testUsername,
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(fmt.Errorf("error marking auth")),
				)
			},
			have:           dataReqGood,
			expectedStatus: fasthttp.StatusOK,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Failed to record Passkey authentication attempt", "error marking auth")
			},
		},
		{
			name:   "ShouldFailUserDetails",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				credential := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       time.Now(),
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC().Add(time.Second * -10), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       2,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				updated := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       credential.CreatedAt,
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC(), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       3,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadWebAuthnPasskeyCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
						Return([]model.WebAuthnCredential{credential}, nil),
					mock.StorageMock.EXPECT().
						UpdateWebAuthnCredentialSignIn(mock.Ctx, updated).
						Return(nil),
					mock.UserProviderMock.EXPECT().
						GetDetails(gomock.Eq(testUsername)).
						Return(nil, fmt.Errorf("failed to get details")),
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)
			},
			have:           dataReqGood,
			expectedStatus: fasthttp.StatusForbidden,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.LogEntryN(1), "Error occurred validating a WebAuthn passkey authentication challenge for user 'john': error retrieving user details", "failed to get details")
			},
		},
		{
			name:   "ShouldFailCloned",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				credential := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       time.Now(),
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().Add(-1 * time.Hour), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       10000,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				updated := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       credential.CreatedAt,
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC(), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       10000,
					CloneWarning:    true,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadWebAuthnPasskeyCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
						Return([]model.WebAuthnCredential{credential}, nil),
					mock.StorageMock.EXPECT().
						UpdateWebAuthnCredentialSignIn(mock.Ctx, updated).
						Return(nil),
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)
			},
			have:           dataReqGood,
			expectedStatus: fasthttp.StatusForbidden,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.LogEntryN(1), "Error occurred validating a WebAuthn passkey authentication challenge for user 'john': error occurred validating the authenticator response", "authenticator sign count indicates that it is cloned")
			},
		},
		{
			name:   "ShouldFailCredentialNotFound",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				credential := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       time.Now(),
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().Add(-1 * time.Hour), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64([]byte("wrong-key")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       1,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadWebAuthnPasskeyCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
						Return([]model.WebAuthnCredential{credential}, nil),
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)
			},
			have:           dataReqGood,
			expectedStatus: fasthttp.StatusForbidden,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.LogEntryN(1), "Error occurred validating a WebAuthn passkey authentication challenge: error performing the login validation", "Unable to find the credential for the returned credential ID (invalid_request)")
			},
		},
		{
			name:   "ShouldFailUpdateCredential",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				credential := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       time.Now(),
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC().Add(time.Second * -10), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       2,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				updated := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       credential.CreatedAt,
					LastUsedAt:      sql.NullTime{Time: mock.Clock.Now().UTC(), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       3,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadWebAuthnPasskeyCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
						Return([]model.WebAuthnCredential{credential}, nil),
					mock.StorageMock.EXPECT().
						UpdateWebAuthnCredentialSignIn(mock.Ctx, updated).
						Return(fmt.Errorf("bad data")),
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)
			},
			have:           dataReqGood,
			expectedStatus: fasthttp.StatusForbidden,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.LogEntryN(1), "Error occurred validating a WebAuthn passkey authentication challenge for user 'john': error occurred saving the credential sign-in information to the storage backend", "bad data")
			},
		},
		{
			name:   "ShouldFailGetCredentials",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadWebAuthnPasskeyCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
						Return(nil, fmt.Errorf("failed to get creds")),
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)
			},
			have:           dataReqGood,
			expectedStatus: fasthttp.StatusForbidden,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.LogEntryN(1), "Error occurred validating a WebAuthn passkey authentication challenge: error performing the login validation", "Failed to lookup Client-side Discoverable Credential: failed to get creds (invalid_request)")
			},
		},
		{
			name:   "ShouldFailGetHandle",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(nil, fmt.Errorf("bad handle")),
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)
			},
			have:           dataReqGood,
			expectedStatus: fasthttp.StatusForbidden,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.LogEntryN(1), "Error occurred validating a WebAuthn passkey authentication challenge: error performing the login validation", "Failed to lookup Client-side Discoverable Credential: bad handle (invalid_request)")
			},
		},
		{
			name:   "ShouldFailGetHandleBlank",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				gomock.InOrder(
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)

				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			have:           dataReqNoHandleGood,
			expectedStatus: fasthttp.StatusForbidden,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.LogEntryN(1), "Error occurred validating a WebAuthn passkey authentication challenge: error performing the login validation", "Client-side Discoverable Assertion was attempted with a blank User Handle (invalid_request)")
			},
		},
		{
			name:   "ShouldFailHeaders",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				gomock.InOrder(
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)

				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				mock.Ctx.Request.Header.Set("X-Original-URL", "123")
				mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "123")
				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			have:           dataReqGood,
			expectedStatus: fasthttp.StatusForbidden,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.LogEntryN(1), "Error occurred validating a WebAuthn passkey authentication challenge: error occurred provisioning the configuration", "failed to parse X-Original-URL header: parse \"123\": invalid URI for request")
			},
		},
		{
			name:   "ShouldFailBadRPIDHash",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))

				credential := model.WebAuthnCredential{
					ID:              1,
					CreatedAt:       time.Now(),
					LastUsedAt:      sql.NullTime{Time: time.Now(), Valid: true},
					RPID:            "login.example.com",
					Username:        testUsername,
					Description:     "test",
					KID:             model.NewBase64(decode("rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU=")),
					AAGUID:          uuid.NullUUID{UUID: uuid.Must(uuid.Parse("01020304-0506-0708-0102-030405060708")), Valid: true},
					AttestationType: "packed",
					Attachment:      "cross-platform",
					Transport:       "usb",
					SignCount:       2,
					CloneWarning:    false,
					Discoverable:    true,
					Present:         true,
					Verified:        true,
					BackupEligible:  false,
					BackupState:     false,
					PublicKey:       []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 184, 17, 198, 170, 14, 81, 23, 237, 100, 218, 123, 122, 48, 76, 56, 148, 23, 111, 173, 245, 67, 239, 176, 229, 199, 205, 213, 46, 239, 91, 222, 183, 34, 88, 32, 171, 141, 116, 74, 68, 180, 81, 66, 81, 127, 81, 41, 236, 173, 38, 7, 9, 34, 128, 167, 101, 51, 25, 84, 239, 100, 10, 124, 117, 165, 178, 179},
				}

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						LoadWebAuthnUserByUserID(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq("example")).
						Return(&model.WebAuthnUser{UserID: "example", Username: testUsername}, nil),
					mock.StorageMock.EXPECT().
						LoadWebAuthnPasskeyCredentialsByUsername(mock.Ctx, gomock.Eq("login.example.com"), gomock.Eq(testUsername)).
						Return([]model.WebAuthnCredential{credential}, nil),
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)
			},
			have:           dataReqBadRPIDHash,
			expectedStatus: fasthttp.StatusForbidden,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.LogEntryN(1), "Error occurred validating a WebAuthn passkey authentication challenge: error performing the login validation", "Error validating origin (verification_error): Expected Values: [https://login.example.com:8080], Received: http://example.com")
			},
		},
		{
			name:   "ShouldFailBadJSON",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				setUpMockClock(mock)

				gomock.InOrder(
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)

				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			have:           "not json",
			expectedStatus: fasthttp.StatusBadRequest,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.LogEntryN(1), "Error occurred validating a WebAuthn passkey authentication challenge: error parsing the request body", "unable to parse body: invalid character 'o' in literal null (expecting 'u')")
			},
		},
		{
			name:   "ShouldFailBadResponseJSON",
			config: &schema.DefaultWebAuthnConfiguration,
			setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				gomock.InOrder(
					mock.StorageMock.EXPECT().
						AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
							Time:       mock.Ctx.Clock.Now(),
							Successful: false,
							Banned:     false,
							Username:   "",
							Type:       "Passkey",
							RemoteIP:   model.NullIP{IP: net.ParseIP("0.0.0.0")},
						})).
						Return(nil),
				)

				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.WebAuthn = &session.WebAuthn{
					SessionData: &webauthn.SessionData{
						Challenge:        "in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE",
						Expires:          time.Now().Add(time.Minute),
						UserVerification: "preferred",
					},
				}

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			have:           `{"response":true}`,
			expectedStatus: fasthttp.StatusBadRequest,
			expectedf: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.WebAuthn)

				AssertLogEntryMessageAndError(t, mock.LogEntryN(1), "Error occurred validating a WebAuthn passkey authentication challenge: error parsing the request body", "Parse error for Assertion (invalid_request): json: cannot unmarshal bool into Go value of type protocol.CredentialAssertionResponse")
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			mock := mocks.NewMockAutheliaCtx(t)

			defer mock.Close()

			setUpMockClock(mock)

			if tc.config != nil {
				mock.Ctx.Configuration.WebAuthn = *tc.config
			}

			if len(tc.have) != 0 {
				mock.Ctx.Request.SetBodyString(tc.have)
			}

			mock.Ctx.Request.Header.Set("X-Original-URL", "https://login.example.com:8080")

			if tc.setup != nil {
				tc.setup(t, mock)
			}

			FirstFactorPasskeyPOST(mock.Ctx)

			assert.Equal(t, tc.expectedStatus, mock.Ctx.Response.StatusCode())
			assert.Regexp(t, tc.expected, string(mock.Ctx.Response.Body()))

			if tc.expectedf != nil {
				tc.expectedf(t, mock)
			}
		})
	}
}
