authelia/internal/handlers/handler_sign_webauthn_test.go

853 lines
39 KiB
Go

package handlers
import (
"database/sql"
"encoding/base64"
"fmt"
"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/session"
)
func TestWebAuthnAssertionGET(t *testing.T) {
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)
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)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
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: 4,
CloneWarning: false,
Discoverable: false,
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().
LoadWebAuthnUser(mock.Ctx, "login.example.com", testUsername).
Return(&model.WebAuthnUser{ID: 1, RPID: "login.example.com", Username: testUsername, UserID: "ZytlJlVuWzdgN2BxTyI8Uy9uS2xpJSdsT2ZsJUA5UEBve1c2NENCKDNSWWphaGVCJEhlQ3wpYT9HQGBwIi8zQA=="}, nil),
mock.StorageMock.
EXPECT().
LoadWebAuthnCredentialsByUsername(mock.Ctx, "login.example.com", testUsername).
Return([]model.WebAuthnCredential{credential}, nil),
)
},
regexp.MustCompile(`^\{"status":"OK","data":\{"publicKey":\{"challenge":"[a-zA-Z0-9/_-]+={0,2}","timeout":60000,"rpId":"login.example.com","allowCredentials":\[\{"type":"public-key","id":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","transports":\["usb"]}],"userVerification":"preferred"}}}$`),
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{0x5a, 0x79, 0x74, 0x6c, 0x4a, 0x6c, 0x56, 0x75, 0x57, 0x7a, 0x64, 0x67, 0x4e, 0x32, 0x42, 0x78, 0x54, 0x79, 0x49, 0x38, 0x55, 0x79, 0x39, 0x75, 0x53, 0x32, 0x78, 0x70, 0x4a, 0x53, 0x64, 0x73, 0x54, 0x32, 0x5a, 0x73, 0x4a, 0x55, 0x41, 0x35, 0x55, 0x45, 0x42, 0x76, 0x65, 0x31, 0x63, 0x32, 0x4e, 0x45, 0x4e, 0x43, 0x4b, 0x44, 0x4e, 0x53, 0x57, 0x57, 0x70, 0x68, 0x61, 0x47, 0x56, 0x43, 0x4a, 0x45, 0x68, 0x6c, 0x51, 0x33, 0x77, 0x70, 0x59, 0x54, 0x39, 0x48, 0x51, 0x47, 0x42, 0x77, 0x49, 0x69, 0x38, 0x7a, 0x51, 0x41, 0x3d, 0x3d}, us.WebAuthn.UserID)
},
},
{
"ShouldHandleCredentialError",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
require.NoError(t, mock.Ctx.SaveSession(us))
gomock.InOrder(
mock.StorageMock.
EXPECT().
LoadWebAuthnUser(mock.Ctx, "login.example.com", testUsername).
Return(&model.WebAuthnUser{ID: 1, RPID: "login.example.com", Username: testUsername, UserID: "ZytlJlVuWzdgN2BxTyI8Uy9uS2xpJSdsT2ZsJUA5UEBve1c2NENCKDNSWWphaGVCJEhlQ3wpYT9HQGBwIi8zQA=="}, nil),
mock.StorageMock.
EXPECT().
LoadWebAuthnCredentialsByUsername(mock.Ctx, "login.example.com", testUsername).
Return(nil, fmt.Errorf("failed")),
)
},
regexp.MustCompile(`^\{"status":"KO","message":"Authentication failed, please retry later."}`),
fasthttp.StatusForbidden,
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(), "Error occurred generating a WebAuthn authentication challenge for user 'john': error occurred retrieving the WebAuthn user configuration from the storage backend", "failed")
},
},
{
"ShouldHandleAnonymous",
&schema.DefaultWebAuthnConfiguration,
nil,
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 authentication challenge", "user is anonymous")
},
},
{
"ShouldHandleBadCookieDomain",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
mock.Ctx.Request.Header.Set("X-Original-URL", "https://auth.notexample.com")
},
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 authentication challenge: error occurred retrieving the user session data", "unable to retrieve session cookie domain provider: no configured session cookie domain matches the url 'https://auth.notexample.com'")
},
},
{
"ShouldHandleBadOrigin",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
require.NoError(t, mock.Ctx.SaveSession(us))
mock.Ctx.Request.Header.Set("X-Original-URL", "!@NJK#N!@#IKJ!@NJK")
},
regexp.MustCompile(`^\{"status":"KO","message":"Authentication failed, please retry later."}`),
fasthttp.StatusForbidden,
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(), "Error occurred generating a WebAuthn authentication challenge for user 'john': error occurred provisioning the configuration", "failed to parse X-Original-URL header: parse \"!@NJK#N!@#IKJ!@NJK\": 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)
}
WebAuthnAssertionGET(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 TestWebAuthnAssertionPOST(t *testing.T) {
const (
dataReqFmt = `{"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"))))
dataReqBadRPIDHash = fmt.Sprintf(dataReqFmt, base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf(dataClientJSON, "http://example.com"))))
)
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)
}{
{
"ShouldSuccess",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
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))
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: 0,
CloneWarning: false,
Discoverable: false,
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().
LoadWebAuthnUser(mock.Ctx, "login.example.com", testUsername).
Return(&model.WebAuthnUser{ID: 1, RPID: "login.example.com", Username: testUsername, UserID: string(decode("OiRQc3wmemUzdHlkVjhVSk5Pe35YMCRCOklLYzVzIkMpaEglNkF5dnVKRSlTPCJbRDZDP102WXpiYXdNekRiTA=="))}, nil),
mock.StorageMock.
EXPECT().
LoadWebAuthnCredentialsByUsername(mock.Ctx, "login.example.com", testUsername).
Return([]model.WebAuthnCredential{credential}, nil),
mock.StorageMock.
EXPECT().
UpdateWebAuthnCredentialSignIn(mock.Ctx, gomock.Any()).
Return(nil),
mock.StorageMock.
EXPECT().
AppendAuthenticationLog(mock.Ctx, gomock.Any()).
Return(nil),
)
},
dataReqGood,
"",
fasthttp.StatusOK,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
assert.Nil(t, us.WebAuthn)
},
},
{
"ShouldFailClone",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
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))
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: 10000,
CloneWarning: false,
Discoverable: false,
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().
LoadWebAuthnUser(mock.Ctx, "login.example.com", testUsername).
Return(&model.WebAuthnUser{ID: 1, RPID: "login.example.com", Username: testUsername, UserID: string(decode("OiRQc3wmemUzdHlkVjhVSk5Pe35YMCRCOklLYzVzIkMpaEglNkF5dnVKRSlTPCJbRDZDP102WXpiYXdNekRiTA=="))}, nil),
mock.StorageMock.
EXPECT().
LoadWebAuthnCredentialsByUsername(mock.Ctx, "login.example.com", testUsername).
Return([]model.WebAuthnCredential{credential}, nil),
mock.StorageMock.
EXPECT().
UpdateWebAuthnCredentialSignIn(mock.Ctx, gomock.Any()).
Return(nil),
)
},
dataReqGood,
"",
fasthttp.StatusForbidden,
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(), "Error occurred validating a WebAuthn authentication challenge for user 'john': error occurred validating the authenticator response", "authenticator sign count indicates that it is cloned")
},
},
{
"ShouldFailUpdateSignIn",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
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))
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: 0,
CloneWarning: false,
Discoverable: false,
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().
LoadWebAuthnUser(mock.Ctx, "login.example.com", testUsername).
Return(&model.WebAuthnUser{ID: 1, RPID: "login.example.com", Username: testUsername, UserID: string(decode("OiRQc3wmemUzdHlkVjhVSk5Pe35YMCRCOklLYzVzIkMpaEglNkF5dnVKRSlTPCJbRDZDP102WXpiYXdNekRiTA=="))}, nil),
mock.StorageMock.
EXPECT().
LoadWebAuthnCredentialsByUsername(mock.Ctx, "login.example.com", testUsername).
Return([]model.WebAuthnCredential{credential}, nil),
mock.StorageMock.
EXPECT().
UpdateWebAuthnCredentialSignIn(mock.Ctx, gomock.Any()).
Return(fmt.Errorf("failed to update")),
)
},
dataReqGood,
"",
fasthttp.StatusForbidden,
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(), "Error occurred validating a WebAuthn authentication challenge for user 'john': error occurred saving the credential sign-in information to the storage backend", "failed to update")
},
},
{
"ShouldFailAuthLogFailure",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
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))
credential := model.WebAuthnCredential{
ID: 1,
CreatedAt: mock.Clock.Now().UTC(),
LastUsedAt: sql.NullTime{Time: mock.Clock.Now().Add(time.Minute).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: 0,
CloneWarning: false,
Discoverable: false,
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().
LoadWebAuthnUser(mock.Ctx, "login.example.com", testUsername).
Return(&model.WebAuthnUser{ID: 1, RPID: "login.example.com", Username: testUsername, UserID: string(decode("OiRQc3wmemUzdHlkVjhVSk5Pe35YMCRCOklLYzVzIkMpaEglNkF5dnVKRSlTPCJbRDZDP102WXpiYXdNekRiTA=="))}, nil),
mock.StorageMock.
EXPECT().
LoadWebAuthnCredentialsByUsername(mock.Ctx, "login.example.com", testUsername).
Return([]model.WebAuthnCredential{credential}, nil),
mock.StorageMock.
EXPECT().
UpdateWebAuthnCredentialSignIn(mock.Ctx, gomock.Any()).
Return(nil),
mock.StorageMock.
EXPECT().
AppendAuthenticationLog(mock.Ctx, gomock.Any()).
Return(fmt.Errorf("bad record")),
)
mock.Clock.Set(mock.Clock.Now().Add(2 * time.Minute))
},
dataReqGood,
"",
fasthttp.StatusForbidden,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Unable to mark WebAuthn authentication attempt by user 'john'", "bad record")
},
},
{
"ShouldFailBadRPIDHash",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
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))
credential := model.WebAuthnCredential{
ID: 1,
CreatedAt: mock.Clock.Now().UTC(),
LastUsedAt: sql.NullTime{Time: mock.Clock.Now().Add(time.Minute).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: 0,
CloneWarning: false,
Discoverable: false,
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().
LoadWebAuthnUser(mock.Ctx, "login.example.com", testUsername).
Return(&model.WebAuthnUser{ID: 1, RPID: "login.example.com", Username: testUsername, UserID: string(decode("OiRQc3wmemUzdHlkVjhVSk5Pe35YMCRCOklLYzVzIkMpaEglNkF5dnVKRSlTPCJbRDZDP102WXpiYXdNekRiTA=="))}, nil),
mock.StorageMock.
EXPECT().
LoadWebAuthnCredentialsByUsername(mock.Ctx, "login.example.com", testUsername).
Return([]model.WebAuthnCredential{credential}, nil),
mock.StorageMock.
EXPECT().
AppendAuthenticationLog(mock.Ctx, gomock.Any()).
Return(nil),
)
mock.Clock.Set(mock.Clock.Now().Add(2 * time.Minute))
},
dataReqBadRPIDHash,
"",
fasthttp.StatusForbidden,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Unsuccessful WebAuthn authentication attempt by user 'john'", "Error validating origin (verification_error): Expected Values: [https://login.example.com:8080], Received: http://example.com")
},
},
{
"ShouldFailBadResponse",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
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))
mock.Clock.Set(mock.Clock.Now().Add(2 * time.Minute))
},
`{"response":{"id":true,"rawId":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","response":{"authenticatorData":"DGygg5w6VoNVeDP2GKJVZmXfKgiJZHh9U4ULStTTvtwFAAAAAw","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaW4xY0wtb1dmU2pTZDd1dXdVdnYybmRPQW1SWGIwY09BYlVvVHRBcXZHRSIsIm9yaWdpbiI6Imh0dHBzOi8vbG9naW4uZXhhbXBsZS5jb206ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","signature":"MEQCIBlJ2Fxf6ZwLNTCQglz0AW0pD4HlU8W5Yk696jjfxVxhAiAhAMkLh8iKyhW6zSmzwfQDjMF2nKjVHzEs7jLHRPDZ2A"},"type":"public-key","clientExtensionResults":{},"authenticatorAttachment":"cross-platform"},"targetURL":null}`,
"",
fasthttp.StatusBadRequest,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating a WebAuthn authentication challenge for user 'john': error parsing the request body", "Parse error for Assertion (invalid_request): json: cannot unmarshal bool into Go struct field CredentialAssertionResponse.id of type string")
},
},
{
"ShouldFailBadJSON",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
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))
mock.Clock.Set(mock.Clock.Now().Add(2 * time.Minute))
},
`{"response:{"id":true,"rawId":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","response":{"authenticatorData":"DGygg5w6VoNVeDP2GKJVZmXfKgiJZHh9U4ULStTTvtwFAAAAAw","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaW4xY0wtb1dmU2pTZDd1dXdVdnYybmRPQW1SWGIwY09BYlVvVHRBcXZHRSIsIm9yaWdpbiI6Imh0dHBzOi8vbG9naW4uZXhhbXBsZS5jb206ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","signature":"MEQCIBlJ2Fxf6ZwLNTCQglz0AW0pD4HlU8W5Yk696jjfxVxhAiAhAMkLh8iKyhW6zSmzwfQDjMF2nKjVHzEs7jLHRPDZ2A"},"type":"public-key","clientExtensionResults":{},"authenticatorAttachment":"cross-platform"},"targetURL":null}`,
"",
fasthttp.StatusBadRequest,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating a WebAuthn authentication challenge for user 'john': error parsing the request body", "unable to parse body: invalid character 'i' after object key")
},
},
{
"ShouldFailOrigin",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
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))
mock.Clock.Set(mock.Clock.Now().Add(2 * time.Minute))
// This malformed URL is chosen to invoke the url.Parse errors.
mock.Ctx.Request.Header.Set("X-Original-URL", "!@#*(&jklqnwdkjqwe")
},
dataReqGood,
"",
fasthttp.StatusForbidden,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating a WebAuthn authentication challenge for user 'john': error occurred provisioning the configuration", "failed to parse X-Original-URL header: parse \"!@#*(&jklqnwdkjqwe\": invalid URI for request")
},
},
{
"ShouldFailNilSession",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
require.NoError(t, mock.Ctx.SaveSession(us))
},
dataReqGood,
"",
fasthttp.StatusForbidden,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating a WebAuthn authentication challenge for user 'john': error occurred retrieving the user session data", "challenge session data is not present")
},
},
{
"ShouldFailAnonymous",
&schema.DefaultWebAuthnConfiguration,
nil,
`{"response:{"id":true,"rawId":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","response":{"authenticatorData":"DGygg5w6VoNVeDP2GKJVZmXfKgiJZHh9U4ULStTTvtwFAAAAAw","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaW4xY0wtb1dmU2pTZDd1dXdVdnYybmRPQW1SWGIwY09BYlVvVHRBcXZHRSIsIm9yaWdpbiI6Imh0dHBzOi8vbG9naW4uZXhhbXBsZS5jb206ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","signature":"MEQCIBlJ2Fxf6ZwLNTCQglz0AW0pD4HlU8W5Yk696jjfxVxhAiAhAMkLh8iKyhW6zSmzwfQDjMF2nKjVHzEs7jLHRPDZ2A"},"type":"public-key","clientExtensionResults":{},"authenticatorAttachment":"cross-platform"},"targetURL":null}`,
"",
fasthttp.StatusForbidden,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating a WebAuthn authentication challenge", "user is anonymous")
},
},
{
"ShouldFailBadSessionDomain",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
mock.Ctx.Request.Header.Set("X-Original-URL", "https://auth.notexample.com")
},
`{"response:{"id":true,"rawId":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","response":{"authenticatorData":"DGygg5w6VoNVeDP2GKJVZmXfKgiJZHh9U4ULStTTvtwFAAAAAw","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaW4xY0wtb1dmU2pTZDd1dXdVdnYybmRPQW1SWGIwY09BYlVvVHRBcXZHRSIsIm9yaWdpbiI6Imh0dHBzOi8vbG9naW4uZXhhbXBsZS5jb206ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","signature":"MEQCIBlJ2Fxf6ZwLNTCQglz0AW0pD4HlU8W5Yk696jjfxVxhAiAhAMkLh8iKyhW6zSmzwfQDjMF2nKjVHzEs7jLHRPDZ2A"},"type":"public-key","clientExtensionResults":{},"authenticatorAttachment":"cross-platform"},"targetURL":null}`,
"",
fasthttp.StatusForbidden,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating a WebAuthn authentication challenge: error occurred retrieving the user session data", "unable to retrieve session cookie domain provider: no configured session cookie domain matches the url 'https://auth.notexample.com'")
},
},
{
"ShouldFailLoadWebAuthnUser",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
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))
gomock.InOrder(
mock.StorageMock.
EXPECT().
LoadWebAuthnUser(mock.Ctx, "login.example.com", testUsername).
Return(&model.WebAuthnUser{ID: 1, RPID: "login.example.com", Username: testUsername, UserID: string(decode("OiRQc3wmemUzdHlkVjhVSk5Pe35YMCRCOklLYzVzIkMpaEglNkF5dnVKRSlTPCJbRDZDP102WXpiYXdNekRiTA=="))}, nil),
mock.StorageMock.
EXPECT().
LoadWebAuthnCredentialsByUsername(mock.Ctx, "login.example.com", testUsername).
Return(nil, fmt.Errorf("failed to load credentials")),
)
},
dataReqGood,
"",
fasthttp.StatusForbidden,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating a WebAuthn authentication challenge for user 'john': error occurred retrieving the WebAuthn user configuration from the storage backend", "failed to load credentials")
},
},
{
"ShouldFailUpdateAuthLog",
&schema.DefaultWebAuthnConfiguration,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
us, err := mock.Ctx.GetSession()
require.NoError(t, err)
us.Username = testUsername
us.AuthenticationLevel = authentication.OneFactor
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))
gomock.InOrder(
mock.StorageMock.
EXPECT().
LoadWebAuthnUser(mock.Ctx, "login.example.com", testUsername).
Return(nil, fmt.Errorf("failed load user")),
)
},
dataReqGood,
"",
fasthttp.StatusForbidden,
func(t *testing.T, mock *mocks.MockAutheliaCtx) {
AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating a WebAuthn authentication challenge for user 'john': error occurred retrieving the WebAuthn user configuration from the storage backend", "failed load user")
},
},
}
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
}
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)
}
WebAuthnAssertionPOST(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)
}
})
}
}
//nolint:godot
/*
[00] sign challenge response post body {"response":{"id":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","rawId":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","response":{"authenticatorData":"DGygg5w6VoNVeDP2GKJVZmXfKgiJZHh9U4ULStTTvtwFAAAAAw","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaW4xY0wtb1dmU2pTZDd1dXdVdnYybmRPQW1SWGIwY09BYlVvVHRBcXZHRSIsIm9yaWdpbiI6Imh0dHBzOi8vbG9naW4uZXhhbXBsZS5jb206ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","signature":"MEQCIBlJ2Fxf6ZwLNTCQglz0AW0pD4HlU8W5Yk696jjfxVxhAiAhAMkLh8iKyhW6zSmzwfQDjMF2nKjVHzEs7jLHRPDZ2A"},"type":"public-key","clientExtensionResults":{},"authenticatorAttachment":"cross-platform"},"targetURL":null}
[00] sign challenge session data {"challenge":"in1cL-oWfSjSd7uuwUvv2ndOAmRXb0cOAbUoTtAqvGE","user_id":"OiRQc3wmemUzdHlkVjhVSk5Pe35YMCRCOklLYzVzIkMpaEglNkF5dnVKRSlTPCJbRDZDP102WXpiYXdNekRiTA==","allowed_credentials":["rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU="],"expires":"2023-11-26T08:02:44.624158134Z","userVerification":"preferred","description":""}
[00] registration challenge response post body {"id":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","rawId":"rwOwV8WCh1hrE0M6mvaoRGpGHidqK6IlhkDJ2xERhPU","response":{"attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIhAI505i2XKRL3xsFcSNRz6crTg7_AIpJIsVOjuv8MKW6jAiBJCrqGIc9kKSgS1x54lq53SWUpVNXmlakZfp5NIXrJcmN4NWOBWQHeMIIB2jCCAX2gAwIBAgIBATANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJVUzERMA8GA1UECgwIQ2hyb21pdW0xIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xGjAYBgNVBAMMEUJhdGNoIENlcnRpZmljYXRlMB4XDTE3MDcxNDAyNDAwMFoXDTQzMTEyMTA4MDAzOVowYDELMAkGA1UEBhMCVVMxETAPBgNVBAoMCENocm9taXVtMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMRowGAYDVQQDDBFCYXRjaCBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI1hfmXJUI5kvMVnOsgqZ5naPBRGaCwljEY__99Y39L6Pmw3i1PXlcSk3_tBme3Xhi8jq68CA7S4kRugVpmU4QGjJTAjMAwGA1UdEwEB_wQCMAAwEwYLKwYBBAGC5RwCAQEEBAMCBSAwDQYJKoZIhvcNAQELBQADSAAwRQIgI4PXvgxbCt2L3tk_p22e3QmDCw0ZOPJ6dIJcp2LoTRACIQDqhWGzBtSCdnTiGq2CjhApHJxER1tBy9vRbRaioTz-ZGhhdXRoRGF0YVikDGygg5w6VoNVeDP2GKJVZmXfKgiJZHh9U4ULStTTvtxFAAAAAQECAwQFBgcIAQIDBAUGBwgAIK8DsFfFgodYaxNDOpr2qERqRh4naiuiJYZAydsREYT1pQECAyYgASFYILgRxqoOURftZNp7ejBMOJQXb631Q--w5cfN1S7vW963Ilggq410SkS0UUJRf1Ep7K0mBwkigKdlMxlU72QKfHWlsrM","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYXFfQVhkdnNETXNLV18xYVkzMVhRaFUxN1pNZzFpMFRLMDEzRHd1a0IyVSIsIm9yaWdpbiI6Imh0dHBzOi8vbG9naW4uZXhhbXBsZS5jb206ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","transports":["usb"],"publicKeyAlgorithm":-7,"publicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuBHGqg5RF-1k2nt6MEw4lBdvrfVD77Dlx83VLu9b3rerjXRKRLRRQlF_USnsrSYHCSKAp2UzGVTvZAp8daWysw","authenticatorData":"DGygg5w6VoNVeDP2GKJVZmXfKgiJZHh9U4ULStTTvtxFAAAAAQECAwQFBgcIAQIDBAUGBwgAIK8DsFfFgodYaxNDOpr2qERqRh4naiuiJYZAydsREYT1pQECAyYgASFYILgRxqoOURftZNp7ejBMOJQXb631Q--w5cfN1S7vW963Ilggq410SkS0UUJRf1Ep7K0mBwkigKdlMxlU72QKfHWlsrM"},"type":"public-key","clientExtensionResults":{"credProps":{"rk":false}},"authenticatorAttachment":"cross-platform"}
[00] registration challenge session data {"challenge":"aq_AXdvsDMsKW_1aY31XQhU17ZMg1i0TK013DwukB2U","user_id":"OiRQc3wmemUzdHlkVjhVSk5Pe35YMCRCOklLYzVzIkMpaEglNkF5dnVKRSlTPCJbRDZDP102WXpiYXdNekRiTA==","expires":"2023-11-26T08:01:39.891837286Z","userVerification":"preferred","description":"test"}
*/