diff --git a/.mockery.yaml b/.mockery.yaml index 2c18235..2ca1dca 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -6,5 +6,5 @@ packages: config: dir: "internal/mocks" interfaces: - Challenges: Credentials: + Tokener: diff --git a/authenticate_create.go b/authenticate_create.go index b437492..46073c4 100644 --- a/authenticate_create.go +++ b/authenticate_create.go @@ -9,6 +9,7 @@ import ( // AuthenticationChallenge is the challenge that is sent to the client to initiate an authentication ceremony. type AuthenticationChallenge struct { + Token string `json:"token"` Challenge string `json:"challenge"` RPID string `json:"rpId"` AllowCredentials []AllowedCredential `json:"allowCredentials"` @@ -36,15 +37,18 @@ func (w *webauthn) CreateAuthentication(ctx context.Context, user User) (*Authen return nil, errutil.Wrapf(err, "generating challenge") } - // Store the challenge in the challenge store - if err := w.options.Challenges.StoreChallenge(ctx, user, challengeBytes); err != nil { - return nil, errutil.Wrapf(err, "storing challenge") + // Create the token for the challenge + token, err := w.options.Tokener.CreateToken(challengeBytes, user) + if err != nil { + return nil, errutil.Wrapf(err, "creating token") } // Format the response - var res AuthenticationChallenge - res.Challenge = w.options.Codec.EncodeToString(challengeBytes[:]) - res.RPID = w.options.RP.ID + res := AuthenticationChallenge{ + Token: token, + Challenge: w.options.Codec.EncodeToString(challengeBytes[:]), + RPID: w.options.RP.ID, + } for _, cred := range credentials { res.AllowCredentials = append(res.AllowCredentials, AllowedCredential{ Type: cred.Type, diff --git a/authenticate_create_test.go b/authenticate_create_test.go index 4d54376..3bca2e8 100644 --- a/authenticate_create_test.go +++ b/authenticate_create_test.go @@ -7,9 +7,7 @@ import ( "github.com/spiretechnology/go-webauthn" "github.com/spiretechnology/go-webauthn/internal/testutil" - "github.com/spiretechnology/go-webauthn/pkg/challenge" "github.com/spiretechnology/go-webauthn/pkg/errs" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -22,7 +20,7 @@ func TestCreateAuthentication(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { t.Run("user has no credentials", func(t *testing.T) { - w, credentials, challenges := setupMocks(tc, nil) + w, credentials, tokener := setupMocks(tc, tc.AuthenticationChallenge) credentials.On("GetCredentials", ctx, tc.User).Return([]webauthn.Credential{}, nil).Once() challenge, err := w.CreateAuthentication(ctx, tc.User) @@ -30,41 +28,38 @@ func TestCreateAuthentication(t *testing.T) { require.ErrorIs(t, err, errs.ErrNoCredentials, "error should be ErrNoCredentials") credentials.AssertExpectations(t) - challenges.AssertExpectations(t) + tokener.AssertExpectations(t) }) - t.Run("storing challenge fails", func(t *testing.T) { - w, credentials, challenges := setupMocks(tc, nil) + t.Run("creating challenge token fails", func(t *testing.T) { + w, credentials, tokener := setupMocks(tc, tc.AuthenticationChallenge) credentials.On("GetCredentials", ctx, tc.User).Return([]webauthn.Credential{testCred}, nil).Once() - challenges.On("StoreChallenge", mock.Anything, tc.User, mock.Anything).Return(errors.New("test error")).Once() + tokener.On("CreateToken", tcChallenge, tc.User).Return("", errors.New("token creation failed")).Once() challenge, err := w.CreateAuthentication(ctx, tc.User) require.Nil(t, challenge, "challenge should be nil") require.Error(t, err, "error should not be nil") credentials.AssertExpectations(t) - challenges.AssertExpectations(t) + tokener.AssertExpectations(t) }) t.Run("creates authentication successfully", func(t *testing.T) { - w, credentials, challenges := setupMocks(tc, &webauthn.Options{ - ChallengeFunc: func() (challenge.Challenge, error) { - return tcChallenge, nil - }, - }) + w, credentials, tokener := setupMocks(tc, tc.AuthenticationChallenge) credentials.On("GetCredentials", ctx, tc.User).Return([]webauthn.Credential{testCred}, nil).Once() - challenges.On("StoreChallenge", mock.Anything, tc.User, mock.Anything).Return(nil).Once() + tokener.On("CreateToken", tcChallenge, tc.User).Return(tc.Authentication.Token, nil).Once() challenge, err := w.CreateAuthentication(ctx, tc.User) require.NotNil(t, challenge, "challenge should not be nil") require.Nil(t, err, "error should be nil") + require.Equal(t, tc.Authentication.Token, challenge.Token, "token should match") require.Equal(t, testutil.Encode(tcChallenge[:]), challenge.Challenge, "challenge should match") require.Equal(t, tc.RelyingParty.ID, challenge.RPID, "relying party should match") require.Equal(t, 1, len(challenge.AllowCredentials), "allow credentials should match") credentials.AssertExpectations(t) - challenges.AssertExpectations(t) + tokener.AssertExpectations(t) }) }) } diff --git a/authenticate_verify.go b/authenticate_verify.go index c7b4b4f..b2c2c46 100644 --- a/authenticate_verify.go +++ b/authenticate_verify.go @@ -12,6 +12,7 @@ import ( // AuthenticationResponse is the response sent back by the client after an authentication ceremony. type AuthenticationResponse struct { + Token string `json:"token"` Challenge string `json:"challenge"` CredentialID string `json:"credentialId"` Response AuthenticatorAssertionResponse `json:"response"` @@ -32,17 +33,10 @@ func (w *webauthn) VerifyAuthentication(ctx context.Context, user User, res *Aut return nil, errutil.Wrap(errs.ErrInvalidChallenge) } challengeBytes := challenge.Challenge(challengeBytesSlice) - ok, err := w.options.Challenges.HasChallenge(ctx, user, challengeBytes) - if err != nil { - return nil, errutil.Wrapf(err, "checking challenge") - } - if !ok { - return nil, errutil.Wrap(errs.ErrUnrecognizedChallenge) - } - // Remove the challenge from the store. It's no longer needed. - if err := w.options.Challenges.RemoveChallenge(ctx, user, challengeBytes); err != nil { - return nil, errutil.Wrapf(err, "removing challenge") + // Verify the challenge token + if err := w.options.Tokener.VerifyToken(res.Token, challengeBytes, user); err != nil { + return nil, errutil.Wrapf(err, "verifying token") } // Decode the received credential ID diff --git a/authenticate_verify_test.go b/authenticate_verify_test.go index be4a4d6..da9bdd8 100644 --- a/authenticate_verify_test.go +++ b/authenticate_verify_test.go @@ -2,21 +2,19 @@ package webauthn_test import ( "context" + "errors" "testing" "github.com/spiretechnology/go-webauthn" "github.com/spiretechnology/go-webauthn/internal/mocks" "github.com/spiretechnology/go-webauthn/internal/testutil" - "github.com/spiretechnology/go-webauthn/pkg/challenge" - "github.com/spiretechnology/go-webauthn/pkg/errs" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) -func seedMockWithCredential(t *testing.T, tc testutil.TestCase, w webauthn.WebAuthn, credentials *mocks.MockCredentials, challenges *mocks.MockChallenges) webauthn.Credential { +func seedMockWithCredential(t *testing.T, tc testutil.TestCase, w webauthn.WebAuthn, credentials *mocks.MockCredentials, tokener *mocks.MockTokener) webauthn.Credential { // Seed the store with a valid credential - challenges.On("HasChallenge", mock.Anything, tc.User, tc.RegistrationChallenge()).Return(true, nil).Once() - challenges.On("RemoveChallenge", mock.Anything, tc.User, tc.RegistrationChallenge()).Return(nil).Once() + tokener.On("VerifyToken", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() credentials.On("StoreCredential", mock.Anything, tc.User, mock.Anything, mock.Anything).Return(nil).Once() reg, err := w.VerifyRegistration(context.Background(), tc.User, &tc.Registration) require.NoError(t, err, "seeding credential should not error") @@ -29,38 +27,34 @@ func TestVerifyAuthentication(t *testing.T) { tcChallenge := tc.AuthenticationChallenge() t.Run(tc.Name, func(t *testing.T) { - t.Run("challenge doesn't exist", func(t *testing.T) { - w, credentials, challenges := setupMocks(tc, nil) - challenges.On("HasChallenge", mock.Anything, tc.User, tcChallenge).Return(false, nil).Once() + t.Run("challenge token is invalid", func(t *testing.T) { + w, credentials, tokener := setupMocks(tc, tc.AuthenticationChallenge) + tokener.On("VerifyToken", tc.Authentication.Token, tcChallenge, tc.User).Return(errors.New("invalid token")).Once() result, err := w.VerifyAuthentication(ctx, tc.User, &tc.Authentication) require.Nil(t, result, "result should be nil") - require.ErrorIs(t, err, errs.ErrUnrecognizedChallenge, "error should be errTest") + require.Error(t, err, "verify authentication should error") credentials.AssertExpectations(t) - challenges.AssertExpectations(t) + tokener.AssertExpectations(t) }) t.Run("verifies registration successfully", func(t *testing.T) { - w, credentials, challenges := setupMocks(tc, &webauthn.Options{ - ChallengeFunc: func() (challenge.Challenge, error) { - return tcChallenge, nil - }, - }) + w, credentials, tokener := setupMocks(tc, tc.AuthenticationChallenge) // Seed the store with a valid credential - credential := seedMockWithCredential(t, tc, w, credentials, challenges) + credential := seedMockWithCredential(t, tc, w, credentials, tokener) - challenges.On("HasChallenge", mock.Anything, tc.User, tcChallenge).Return(true, nil).Once() - challenges.On("RemoveChallenge", mock.Anything, tc.User, tcChallenge).Return(nil).Once() + tokener.On("VerifyToken", tc.Authentication.Token, tcChallenge, tc.User).Return(nil).Once() credentials.On("GetCredential", mock.Anything, tc.User, mock.Anything).Return(&credential, nil).Once() result, err := w.VerifyAuthentication(ctx, tc.User, &tc.Authentication) require.Nil(t, err, "error should be nil") require.NotNil(t, result, "result should not be nil") + require.Equal(t, credential, result.Credential, "credential should match") credentials.AssertExpectations(t) - challenges.AssertExpectations(t) + tokener.AssertExpectations(t) }) }) } diff --git a/go.mod b/go.mod index 0cddd9e..d318051 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.20 require ( github.com/fxamacker/cbor/v2 v2.4.0 + github.com/spiretechnology/go-jwt/v2 v2.1.0 github.com/stretchr/testify v1.8.4 golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb ) diff --git a/go.sum b/go.sum index 4efc2b0..9745960 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spiretechnology/go-jwt/v2 v2.1.0 h1:hBSS6rdg+Zg0AdxJqheG9rnFz+0tkyJkmyNfHSznl9k= +github.com/spiretechnology/go-jwt/v2 v2.1.0/go.mod h1:G4t9Mf53sTy+zvYFzcfdnMijb34fywdym5vtm+uSQB8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= diff --git a/internal/mocks/mock_Challenges.go b/internal/mocks/mock_Challenges.go deleted file mode 100644 index eb9438d..0000000 --- a/internal/mocks/mock_Challenges.go +++ /dev/null @@ -1,179 +0,0 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. - -package mocks - -import ( - context "context" - - webauthn "github.com/spiretechnology/go-webauthn" - mock "github.com/stretchr/testify/mock" -) - -// MockChallenges is an autogenerated mock type for the Challenges type -type MockChallenges struct { - mock.Mock -} - -type MockChallenges_Expecter struct { - mock *mock.Mock -} - -func (_m *MockChallenges) EXPECT() *MockChallenges_Expecter { - return &MockChallenges_Expecter{mock: &_m.Mock} -} - -// HasChallenge provides a mock function with given fields: ctx, user, challege -func (_m *MockChallenges) HasChallenge(ctx context.Context, user webauthn.User, challege [32]byte) (bool, error) { - ret := _m.Called(ctx, user, challege) - - var r0 bool - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, webauthn.User, [32]byte) (bool, error)); ok { - return rf(ctx, user, challege) - } - if rf, ok := ret.Get(0).(func(context.Context, webauthn.User, [32]byte) bool); ok { - r0 = rf(ctx, user, challege) - } else { - r0 = ret.Get(0).(bool) - } - - if rf, ok := ret.Get(1).(func(context.Context, webauthn.User, [32]byte) error); ok { - r1 = rf(ctx, user, challege) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockChallenges_HasChallenge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasChallenge' -type MockChallenges_HasChallenge_Call struct { - *mock.Call -} - -// HasChallenge is a helper method to define mock.On call -// - ctx context.Context -// - user webauthn.User -// - challege [32]byte -func (_e *MockChallenges_Expecter) HasChallenge(ctx interface{}, user interface{}, challege interface{}) *MockChallenges_HasChallenge_Call { - return &MockChallenges_HasChallenge_Call{Call: _e.mock.On("HasChallenge", ctx, user, challege)} -} - -func (_c *MockChallenges_HasChallenge_Call) Run(run func(ctx context.Context, user webauthn.User, challege [32]byte)) *MockChallenges_HasChallenge_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(webauthn.User), args[2].([32]byte)) - }) - return _c -} - -func (_c *MockChallenges_HasChallenge_Call) Return(_a0 bool, _a1 error) *MockChallenges_HasChallenge_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockChallenges_HasChallenge_Call) RunAndReturn(run func(context.Context, webauthn.User, [32]byte) (bool, error)) *MockChallenges_HasChallenge_Call { - _c.Call.Return(run) - return _c -} - -// RemoveChallenge provides a mock function with given fields: ctx, user, challege -func (_m *MockChallenges) RemoveChallenge(ctx context.Context, user webauthn.User, challege [32]byte) error { - ret := _m.Called(ctx, user, challege) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, webauthn.User, [32]byte) error); ok { - r0 = rf(ctx, user, challege) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockChallenges_RemoveChallenge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveChallenge' -type MockChallenges_RemoveChallenge_Call struct { - *mock.Call -} - -// RemoveChallenge is a helper method to define mock.On call -// - ctx context.Context -// - user webauthn.User -// - challege [32]byte -func (_e *MockChallenges_Expecter) RemoveChallenge(ctx interface{}, user interface{}, challege interface{}) *MockChallenges_RemoveChallenge_Call { - return &MockChallenges_RemoveChallenge_Call{Call: _e.mock.On("RemoveChallenge", ctx, user, challege)} -} - -func (_c *MockChallenges_RemoveChallenge_Call) Run(run func(ctx context.Context, user webauthn.User, challege [32]byte)) *MockChallenges_RemoveChallenge_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(webauthn.User), args[2].([32]byte)) - }) - return _c -} - -func (_c *MockChallenges_RemoveChallenge_Call) Return(_a0 error) *MockChallenges_RemoveChallenge_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockChallenges_RemoveChallenge_Call) RunAndReturn(run func(context.Context, webauthn.User, [32]byte) error) *MockChallenges_RemoveChallenge_Call { - _c.Call.Return(run) - return _c -} - -// StoreChallenge provides a mock function with given fields: ctx, user, challenge -func (_m *MockChallenges) StoreChallenge(ctx context.Context, user webauthn.User, challenge [32]byte) error { - ret := _m.Called(ctx, user, challenge) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, webauthn.User, [32]byte) error); ok { - r0 = rf(ctx, user, challenge) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockChallenges_StoreChallenge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StoreChallenge' -type MockChallenges_StoreChallenge_Call struct { - *mock.Call -} - -// StoreChallenge is a helper method to define mock.On call -// - ctx context.Context -// - user webauthn.User -// - challenge [32]byte -func (_e *MockChallenges_Expecter) StoreChallenge(ctx interface{}, user interface{}, challenge interface{}) *MockChallenges_StoreChallenge_Call { - return &MockChallenges_StoreChallenge_Call{Call: _e.mock.On("StoreChallenge", ctx, user, challenge)} -} - -func (_c *MockChallenges_StoreChallenge_Call) Run(run func(ctx context.Context, user webauthn.User, challenge [32]byte)) *MockChallenges_StoreChallenge_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(webauthn.User), args[2].([32]byte)) - }) - return _c -} - -func (_c *MockChallenges_StoreChallenge_Call) Return(_a0 error) *MockChallenges_StoreChallenge_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockChallenges_StoreChallenge_Call) RunAndReturn(run func(context.Context, webauthn.User, [32]byte) error) *MockChallenges_StoreChallenge_Call { - _c.Call.Return(run) - return _c -} - -// NewMockChallenges creates a new instance of MockChallenges. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockChallenges(t interface { - mock.TestingT - Cleanup(func()) -}) *MockChallenges { - mock := &MockChallenges{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/mocks/mock_Tokener.go b/internal/mocks/mock_Tokener.go new file mode 100644 index 0000000..243e6c5 --- /dev/null +++ b/internal/mocks/mock_Tokener.go @@ -0,0 +1,132 @@ +// Code generated by mockery v2.32.4. DO NOT EDIT. + +package mocks + +import ( + webauthn "github.com/spiretechnology/go-webauthn" + mock "github.com/stretchr/testify/mock" +) + +// MockTokener is an autogenerated mock type for the Tokener type +type MockTokener struct { + mock.Mock +} + +type MockTokener_Expecter struct { + mock *mock.Mock +} + +func (_m *MockTokener) EXPECT() *MockTokener_Expecter { + return &MockTokener_Expecter{mock: &_m.Mock} +} + +// CreateToken provides a mock function with given fields: challenge, user +func (_m *MockTokener) CreateToken(challenge [32]byte, user webauthn.User) (string, error) { + ret := _m.Called(challenge, user) + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func([32]byte, webauthn.User) (string, error)); ok { + return rf(challenge, user) + } + if rf, ok := ret.Get(0).(func([32]byte, webauthn.User) string); ok { + r0 = rf(challenge, user) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func([32]byte, webauthn.User) error); ok { + r1 = rf(challenge, user) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockTokener_CreateToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateToken' +type MockTokener_CreateToken_Call struct { + *mock.Call +} + +// CreateToken is a helper method to define mock.On call +// - challenge [32]byte +// - user webauthn.User +func (_e *MockTokener_Expecter) CreateToken(challenge interface{}, user interface{}) *MockTokener_CreateToken_Call { + return &MockTokener_CreateToken_Call{Call: _e.mock.On("CreateToken", challenge, user)} +} + +func (_c *MockTokener_CreateToken_Call) Run(run func(challenge [32]byte, user webauthn.User)) *MockTokener_CreateToken_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([32]byte), args[1].(webauthn.User)) + }) + return _c +} + +func (_c *MockTokener_CreateToken_Call) Return(_a0 string, _a1 error) *MockTokener_CreateToken_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockTokener_CreateToken_Call) RunAndReturn(run func([32]byte, webauthn.User) (string, error)) *MockTokener_CreateToken_Call { + _c.Call.Return(run) + return _c +} + +// VerifyToken provides a mock function with given fields: token, challenge, user +func (_m *MockTokener) VerifyToken(token string, challenge [32]byte, user webauthn.User) error { + ret := _m.Called(token, challenge, user) + + var r0 error + if rf, ok := ret.Get(0).(func(string, [32]byte, webauthn.User) error); ok { + r0 = rf(token, challenge, user) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockTokener_VerifyToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyToken' +type MockTokener_VerifyToken_Call struct { + *mock.Call +} + +// VerifyToken is a helper method to define mock.On call +// - token string +// - challenge [32]byte +// - user webauthn.User +func (_e *MockTokener_Expecter) VerifyToken(token interface{}, challenge interface{}, user interface{}) *MockTokener_VerifyToken_Call { + return &MockTokener_VerifyToken_Call{Call: _e.mock.On("VerifyToken", token, challenge, user)} +} + +func (_c *MockTokener_VerifyToken_Call) Run(run func(token string, challenge [32]byte, user webauthn.User)) *MockTokener_VerifyToken_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].([32]byte), args[2].(webauthn.User)) + }) + return _c +} + +func (_c *MockTokener_VerifyToken_Call) Return(_a0 error) *MockTokener_VerifyToken_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockTokener_VerifyToken_Call) RunAndReturn(run func(string, [32]byte, webauthn.User) error) *MockTokener_VerifyToken_Call { + _c.Call.Return(run) + return _c +} + +// NewMockTokener creates a new instance of MockTokener. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockTokener(t interface { + mock.TestingT + Cleanup(func()) +}) *MockTokener { + mock := &MockTokener{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/testutil/testcases.json b/internal/testutil/testcases.json index 573d5a1..15b29d6 100644 --- a/internal/testutil/testcases.json +++ b/internal/testutil/testcases.json @@ -3,8 +3,8 @@ "name": "yubikey 1", "relyingParty": {"id": "localhost", "name": "Test"}, "user": {"id": "AQIDBA", "name": "test", "displayName": "Test"}, - "registration": {"challenge":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8","credentialId":"X-IUuDEypIEmRhA2fy3Nu6vEE6BQqx-VDAaqD269vOSQm-GQnyM8mE6y4oijXPJ8tuKiUp7TtY3xb1Kizn29Ow","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EeEFSRWhNVUZSWVhHQmthR3h3ZEhoOCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAAAgAAAAAAAAAAAAAAAAAAAAAAQF_iFLgxMqSBJkYQNn8tzburxBOgUKsflQwGqg9uvbzkkJvhkJ8jPJhOsuKIo1zyfLbiolKe07WN8W9Sos59vTulAQIDJiABIVggLb0gNXeJOo1SwN4LF2StsRVbkEdhgAs9jHTYo6cXmHgiWCDgL2ZzTsVFtXGPuare0-8_oBkJ_4bO0WM5G30FdTZg7g"}}, - "authentication": {"challenge":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8","credentialId":"X-IUuDEypIEmRhA2fy3Nu6vEE6BQqx-VDAaqD269vOSQm-GQnyM8mE6y4oijXPJ8tuKiUp7TtY3xb1Kizn29Ow","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAABA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EeEFSRWhNVUZSWVhHQmthR3h3ZEhoOCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","signature":"MEYCIQC-BozuJn4mY5PEqDlEkO2N1_I-EqDZ6W8rWhPbyv8S6QIhAK_ii2WQpanc4jkWc2XktFf_5o2nHOXE1-h8ARnr134W","userHandle":null}}, + "registration": {"token":"mytoken","challenge":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8","credentialId":"X-IUuDEypIEmRhA2fy3Nu6vEE6BQqx-VDAaqD269vOSQm-GQnyM8mE6y4oijXPJ8tuKiUp7TtY3xb1Kizn29Ow","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EeEFSRWhNVUZSWVhHQmthR3h3ZEhoOCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAAAgAAAAAAAAAAAAAAAAAAAAAAQF_iFLgxMqSBJkYQNn8tzburxBOgUKsflQwGqg9uvbzkkJvhkJ8jPJhOsuKIo1zyfLbiolKe07WN8W9Sos59vTulAQIDJiABIVggLb0gNXeJOo1SwN4LF2StsRVbkEdhgAs9jHTYo6cXmHgiWCDgL2ZzTsVFtXGPuare0-8_oBkJ_4bO0WM5G30FdTZg7g"}}, + "authentication": {"token":"mytoken","challenge":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8","credentialId":"X-IUuDEypIEmRhA2fy3Nu6vEE6BQqx-VDAaqD269vOSQm-GQnyM8mE6y4oijXPJ8tuKiUp7TtY3xb1Kizn29Ow","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAABA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EeEFSRWhNVUZSWVhHQmthR3h3ZEhoOCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","signature":"MEYCIQC-BozuJn4mY5PEqDlEkO2N1_I-EqDZ6W8rWhPbyv8S6QIhAK_ii2WQpanc4jkWc2XktFf_5o2nHOXE1-h8ARnr134W","userHandle":null}}, "attestation": { "fmt": "none", "flags": ["UserPresent", "AttestedCredentialData"], @@ -21,8 +21,8 @@ "name": "yubikey 2", "relyingParty": {"id": "localhost", "name": "Test"}, "user": {"id": "AQIDBA", "name": "test", "displayName": "Test"}, - "registration": {"challenge":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8","credentialId":"OV8mzVAK474Mpq1Bv-Jp686qsd1G0nMnx9G8_ZQLqCemGSTL459261Rk5evgpyROMNo4upt88EbooRMQ4pbQJg","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EeEFSRWhNVUZSWVhHQmthR3h3ZEhoOCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAAAwAAAAAAAAAAAAAAAAAAAAAAQDlfJs1QCuO-DKatQb_iaevOqrHdRtJzJ8fRvP2UC6gnphkky-OfdutUZOXr4KckTjDaOLqbfPBG6KETEOKW0CalAQIDJiABIVggXIFhM06nTGhSjjX7b01SMrhoWW9gYvE2-nVZ6bUTOMsiWCBbMZRb31ULcC6h49_Lv8Drx-Hhbn-BddWGagvjtf7exw"}}, - "authentication": {"challenge":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8","credentialId":"OV8mzVAK474Mpq1Bv-Jp686qsd1G0nMnx9G8_ZQLqCemGSTL459261Rk5evgpyROMNo4upt88EbooRMQ4pbQJg","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAABw","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EeEFSRWhNVUZSWVhHQmthR3h3ZEhoOCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","signature":"MEUCIBqT4_MOE9okSZWCsxrXmv6HrCSLU3D6p-dy3fiOVMQbAiEApmISjHvgfDZlp0E7wbL53U8GBTNCkN7u5AXJ90SLJfM","userHandle":null}}, + "registration": {"token":"mytoken","challenge":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8","credentialId":"OV8mzVAK474Mpq1Bv-Jp686qsd1G0nMnx9G8_ZQLqCemGSTL459261Rk5evgpyROMNo4upt88EbooRMQ4pbQJg","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EeEFSRWhNVUZSWVhHQmthR3h3ZEhoOCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAAAwAAAAAAAAAAAAAAAAAAAAAAQDlfJs1QCuO-DKatQb_iaevOqrHdRtJzJ8fRvP2UC6gnphkky-OfdutUZOXr4KckTjDaOLqbfPBG6KETEOKW0CalAQIDJiABIVggXIFhM06nTGhSjjX7b01SMrhoWW9gYvE2-nVZ6bUTOMsiWCBbMZRb31ULcC6h49_Lv8Drx-Hhbn-BddWGagvjtf7exw"}}, + "authentication": {"token":"mytoken","challenge":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8","credentialId":"OV8mzVAK474Mpq1Bv-Jp686qsd1G0nMnx9G8_ZQLqCemGSTL459261Rk5evgpyROMNo4upt88EbooRMQ4pbQJg","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAABw","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EeEFSRWhNVUZSWVhHQmthR3h3ZEhoOCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","signature":"MEUCIBqT4_MOE9okSZWCsxrXmv6HrCSLU3D6p-dy3fiOVMQbAiEApmISjHvgfDZlp0E7wbL53U8GBTNCkN7u5AXJ90SLJfM","userHandle":null}}, "attestation": { "fmt": "none", "flags": ["UserPresent", "AttestedCredentialData"], @@ -39,8 +39,8 @@ "name": "yubikey direct attestation 1", "relyingParty": {"id": "localhost", "name": "Test"}, "user": {"id": "AQIDBA", "name": "test", "displayName": "Test"}, - "registration": {"challenge":"9nhj8NiAGWQzRhJopGZ7bGVLV8kHz1mvRFx1OxpEHXs","credentialId":"wo4lZWXEBYas7gUcT7wIf4Q3N4kL7sDHRc5oL39RcMVd4eurKyuOc0gBXS-4WO_tHqxFqroxrmrmM4iqUdln9A","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiOW5oajhOaUFHV1F6UmhKb3BHWjdiR1ZMVjhrSHoxbXZSRngxT3hwRUhYcyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEgwRgIhAJPniWPMBB2q7Jt9r9sOLuqAAK0Uuh6BEbLmPsrB7XPTAiEAoU3Kn3CIZu5OUo8XivMeRhH6tIFOyPxbufswuJz357tjeDVjgVkC3TCCAtkwggHBoAMCAQICCQDVW5xol6LKiDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbzELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTc1NTA3NzU4OTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAEGqdB_6ln-y6Pfqh_F49qvhLTfg_4ztCVsX6MmnwHUEXCByIVDIsaragfX3eTPeS9BeM0uz723uRCAP7Z7O52jgYEwfzATBgorBgEEAYLECg0BBAUEAwUEAzAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNzATBgsrBgEEAYLlHAIBAQQEAwIFIDAhBgsrBgEEAYLlHAEBBAQSBBDuiCh5chxJE5d1PfzOlwcqMAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBAIQ0yvrqF8jVCr8z5Ppk40cpGpBnycegl1iRyQEf83ZB0B2jQPkgfM92tpZp_bASiNv_vU9z2rI-IGml4kMajl24n6fCL-Z8-6yrZpjLrq_7uPlzJDqPsC3Wb3I8I_o1nV9HWhRpkVNGHJOLWMOvmP4Sfy_JjU_znbto6mN_vlpWfE_R_nPQWIc93RtTAokKWB_7cObM9C17khazN7Rf9MhHoYLcA8ADW8vThuyqlH-ztAK76QXBRT4_JSX_9f-ql5MBUmMWWcylx8DbLphHaQe4rPl-jOLFhv3XJeprI_oUHbVqcaJAls8pnZVBuZpOeNb7c6qTIzOlL0RINcFo6F5oYXV0aERhdGFYxEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAATuiCh5chxJE5d1PfzOlwcqAEDCjiVlZcQFhqzuBRxPvAh_hDc3iQvuwMdFzmgvf1FwxV3h66srK45zSAFdL7hY7-0erEWqujGuauYziKpR2Wf0pQECAyYgASFYIAJRGmt-o0aIA5SAF0ykytGP4HJgqWZRLEmXzJyd691RIlggxjFkOUnCce3ErgXzQIx31GrbwVwllqdmGo4ERgtUdZA"}}, - "authentication": {"challenge":"zh9st9_ahGrxW_WEtApCOdMTK-MLrBJUdFBAaikLaQ0","credentialId":"wo4lZWXEBYas7gUcT7wIf4Q3N4kL7sDHRc5oL39RcMVd4eurKyuOc0gBXS-4WO_tHqxFqroxrmrmM4iqUdln9A","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAABg","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiemg5c3Q5X2FoR3J4V19XRXRBcENPZE1USy1NTHJCSlVkRkJBYWlrTGFRMCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","signature":"MEQCIGnsHYiuQf4psGtkh_ZXIcXgCFHxk-lhH6fBBa8GeZA1AiAgOHgfYQdlOY-uknthrTDOyOBRfEhA_5Gxfo-1Tw49Iw","userHandle":null}}, + "registration": {"token":"mytoken","challenge":"9nhj8NiAGWQzRhJopGZ7bGVLV8kHz1mvRFx1OxpEHXs","credentialId":"wo4lZWXEBYas7gUcT7wIf4Q3N4kL7sDHRc5oL39RcMVd4eurKyuOc0gBXS-4WO_tHqxFqroxrmrmM4iqUdln9A","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiOW5oajhOaUFHV1F6UmhKb3BHWjdiR1ZMVjhrSHoxbXZSRngxT3hwRUhYcyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEgwRgIhAJPniWPMBB2q7Jt9r9sOLuqAAK0Uuh6BEbLmPsrB7XPTAiEAoU3Kn3CIZu5OUo8XivMeRhH6tIFOyPxbufswuJz357tjeDVjgVkC3TCCAtkwggHBoAMCAQICCQDVW5xol6LKiDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbzELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTc1NTA3NzU4OTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAEGqdB_6ln-y6Pfqh_F49qvhLTfg_4ztCVsX6MmnwHUEXCByIVDIsaragfX3eTPeS9BeM0uz723uRCAP7Z7O52jgYEwfzATBgorBgEEAYLECg0BBAUEAwUEAzAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNzATBgsrBgEEAYLlHAIBAQQEAwIFIDAhBgsrBgEEAYLlHAEBBAQSBBDuiCh5chxJE5d1PfzOlwcqMAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBAIQ0yvrqF8jVCr8z5Ppk40cpGpBnycegl1iRyQEf83ZB0B2jQPkgfM92tpZp_bASiNv_vU9z2rI-IGml4kMajl24n6fCL-Z8-6yrZpjLrq_7uPlzJDqPsC3Wb3I8I_o1nV9HWhRpkVNGHJOLWMOvmP4Sfy_JjU_znbto6mN_vlpWfE_R_nPQWIc93RtTAokKWB_7cObM9C17khazN7Rf9MhHoYLcA8ADW8vThuyqlH-ztAK76QXBRT4_JSX_9f-ql5MBUmMWWcylx8DbLphHaQe4rPl-jOLFhv3XJeprI_oUHbVqcaJAls8pnZVBuZpOeNb7c6qTIzOlL0RINcFo6F5oYXV0aERhdGFYxEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAATuiCh5chxJE5d1PfzOlwcqAEDCjiVlZcQFhqzuBRxPvAh_hDc3iQvuwMdFzmgvf1FwxV3h66srK45zSAFdL7hY7-0erEWqujGuauYziKpR2Wf0pQECAyYgASFYIAJRGmt-o0aIA5SAF0ykytGP4HJgqWZRLEmXzJyd691RIlggxjFkOUnCce3ErgXzQIx31GrbwVwllqdmGo4ERgtUdZA"}}, + "authentication": {"token":"mytoken","challenge":"zh9st9_ahGrxW_WEtApCOdMTK-MLrBJUdFBAaikLaQ0","credentialId":"wo4lZWXEBYas7gUcT7wIf4Q3N4kL7sDHRc5oL39RcMVd4eurKyuOc0gBXS-4WO_tHqxFqroxrmrmM4iqUdln9A","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAABg","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiemg5c3Q5X2FoR3J4V19XRXRBcENPZE1USy1NTHJCSlVkRkJBYWlrTGFRMCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","signature":"MEQCIGnsHYiuQf4psGtkh_ZXIcXgCFHxk-lhH6fBBa8GeZA1AiAgOHgfYQdlOY-uknthrTDOyOBRfEhA_5Gxfo-1Tw49Iw","userHandle":null}}, "attestation": { "fmt": "packed", "flags": ["UserPresent", "AttestedCredentialData"], @@ -57,8 +57,8 @@ "name": "yubikey direct attestation 2", "relyingParty": {"id": "localhost", "name": "Test"}, "user": {"id": "AQIDBA", "name": "test", "displayName": "Test"}, - "registration": {"challenge":"4ySalBM_9QTdDFmyI7dZOf_q3oAUPd4H9ZU255uhXAc","credentialId":"Tns5lnsfa7lk5z14hF0CRF9HOjPSVMIaCGGbfM9CJUrsR_C4NAOfDOqnAvhAqJBMFQqRSEyt7WDNAEGhkyvp3w","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiNHlTYWxCTV85UVRkREZteUk3ZFpPZl9xM29BVVBkNEg5WlUyNTV1aFhBYyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgGEDYwUoLfuFU0aSql79-5RktVslwzgqiUl7lhn6kCIoCIQCu-CxusuNt6w7S9S7nRes3uZeB5uB4-vTNztWG0C1hK2N4NWOBWQLdMIIC2TCCAcGgAwIBAgIJANVbnGiXosqIMA0GCSqGSIb3DQEBCwUAMC4xLDAqBgNVBAMTI1l1YmljbyBVMkYgUm9vdCBDQSBTZXJpYWwgNDU3MjAwNjMxMCAXDTE0MDgwMTAwMDAwMFoYDzIwNTAwOTA0MDAwMDAwWjBvMQswCQYDVQQGEwJTRTESMBAGA1UECgwJWXViaWNvIEFCMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMSgwJgYDVQQDDB9ZdWJpY28gVTJGIEVFIFNlcmlhbCAxNzU1MDc3NTg5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAQap0H_qWf7Lo9-qH8Xj2q-EtN-D_jO0JWxfoyafAdQRcIHIhUMixqtqB9fd5M95L0F4zS7Pvbe5EIA_tns7naOBgTB_MBMGCisGAQQBgsQKDQEEBQQDBQQDMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS43MBMGCysGAQQBguUcAgEBBAQDAgUgMCEGCysGAQQBguUcAQEEBBIEEO6IKHlyHEkTl3U9_M6XByowDAYDVR0TAQH_BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAhDTK-uoXyNUKvzPk-mTjRykakGfJx6CXWJHJAR_zdkHQHaNA-SB8z3a2lmn9sBKI2_-9T3Pasj4gaaXiQxqOXbifp8Iv5nz7rKtmmMuur_u4-XMkOo-wLdZvcjwj-jWdX0daFGmRU0Yck4tYw6-Y_hJ_L8mNT_Odu2jqY3--WlZ8T9H-c9BYhz3dG1MCiQpYH_tw5sz0LXuSFrM3tF_0yEehgtwDwANby9OG7KqUf7O0ArvpBcFFPj8lJf_1_6qXkwFSYxZZzKXHwNsumEdpB7is-X6M4sWG_dcl6msj-hQdtWpxokCWzymdlUG5mk541vtzqpMjM6UvREg1wWjoXmhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAAAu6IKHlyHEkTl3U9_M6XByoAQE57OZZ7H2u5ZOc9eIRdAkRfRzoz0lTCGghhm3zPQiVK7EfwuDQDnwzqpwL4QKiQTBUKkUhMre1gzQBBoZMr6d-lAQIDJiABIVggmLj0rynrf81E_0aDgSJry5v_z8gQxvoJgEMPciwO_ZIiWCAQrukwI-WTeQNBHYTXiMHxJkWE50VqmTqxtZMNp6xgIA"}}, - "authentication": {"challenge":"A3Owx5eHSS5NRiGA0ZqZkO9p5QAvB_LQ667GwXconic","credentialId":"Tns5lnsfa7lk5z14hF0CRF9HOjPSVMIaCGGbfM9CJUrsR_C4NAOfDOqnAvhAqJBMFQqRSEyt7WDNAEGhkyvp3w","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAAAw","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQTNPd3g1ZUhTUzVOUmlHQTBacVprTzlwNVFBdkJfTFE2NjdHd1hjb25pYyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","signature":"MEUCIF6GLzlQL1w3Unls6jEtg691KFUm9_gfZP_UBRPvG04BAiEA_Ll5Uwxw1azP-MOijCAJhYJCCIOSahNkIYHZTMzb3ic","userHandle":null}}, + "registration": {"token":"mytoken","challenge":"4ySalBM_9QTdDFmyI7dZOf_q3oAUPd4H9ZU255uhXAc","credentialId":"Tns5lnsfa7lk5z14hF0CRF9HOjPSVMIaCGGbfM9CJUrsR_C4NAOfDOqnAvhAqJBMFQqRSEyt7WDNAEGhkyvp3w","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiNHlTYWxCTV85UVRkREZteUk3ZFpPZl9xM29BVVBkNEg5WlUyNTV1aFhBYyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgGEDYwUoLfuFU0aSql79-5RktVslwzgqiUl7lhn6kCIoCIQCu-CxusuNt6w7S9S7nRes3uZeB5uB4-vTNztWG0C1hK2N4NWOBWQLdMIIC2TCCAcGgAwIBAgIJANVbnGiXosqIMA0GCSqGSIb3DQEBCwUAMC4xLDAqBgNVBAMTI1l1YmljbyBVMkYgUm9vdCBDQSBTZXJpYWwgNDU3MjAwNjMxMCAXDTE0MDgwMTAwMDAwMFoYDzIwNTAwOTA0MDAwMDAwWjBvMQswCQYDVQQGEwJTRTESMBAGA1UECgwJWXViaWNvIEFCMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMSgwJgYDVQQDDB9ZdWJpY28gVTJGIEVFIFNlcmlhbCAxNzU1MDc3NTg5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAQap0H_qWf7Lo9-qH8Xj2q-EtN-D_jO0JWxfoyafAdQRcIHIhUMixqtqB9fd5M95L0F4zS7Pvbe5EIA_tns7naOBgTB_MBMGCisGAQQBgsQKDQEEBQQDBQQDMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS43MBMGCysGAQQBguUcAgEBBAQDAgUgMCEGCysGAQQBguUcAQEEBBIEEO6IKHlyHEkTl3U9_M6XByowDAYDVR0TAQH_BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAhDTK-uoXyNUKvzPk-mTjRykakGfJx6CXWJHJAR_zdkHQHaNA-SB8z3a2lmn9sBKI2_-9T3Pasj4gaaXiQxqOXbifp8Iv5nz7rKtmmMuur_u4-XMkOo-wLdZvcjwj-jWdX0daFGmRU0Yck4tYw6-Y_hJ_L8mNT_Odu2jqY3--WlZ8T9H-c9BYhz3dG1MCiQpYH_tw5sz0LXuSFrM3tF_0yEehgtwDwANby9OG7KqUf7O0ArvpBcFFPj8lJf_1_6qXkwFSYxZZzKXHwNsumEdpB7is-X6M4sWG_dcl6msj-hQdtWpxokCWzymdlUG5mk541vtzqpMjM6UvREg1wWjoXmhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAAAu6IKHlyHEkTl3U9_M6XByoAQE57OZZ7H2u5ZOc9eIRdAkRfRzoz0lTCGghhm3zPQiVK7EfwuDQDnwzqpwL4QKiQTBUKkUhMre1gzQBBoZMr6d-lAQIDJiABIVggmLj0rynrf81E_0aDgSJry5v_z8gQxvoJgEMPciwO_ZIiWCAQrukwI-WTeQNBHYTXiMHxJkWE50VqmTqxtZMNp6xgIA"}}, + "authentication": {"token":"mytoken","challenge":"A3Owx5eHSS5NRiGA0ZqZkO9p5QAvB_LQ667GwXconic","credentialId":"Tns5lnsfa7lk5z14hF0CRF9HOjPSVMIaCGGbfM9CJUrsR_C4NAOfDOqnAvhAqJBMFQqRSEyt7WDNAEGhkyvp3w","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAAAw","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQTNPd3g1ZUhTUzVOUmlHQTBacVprTzlwNVFBdkJfTFE2NjdHd1hjb25pYyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","signature":"MEUCIF6GLzlQL1w3Unls6jEtg691KFUm9_gfZP_UBRPvG04BAiEA_Ll5Uwxw1azP-MOijCAJhYJCCIOSahNkIYHZTMzb3ic","userHandle":null}}, "attestation": { "fmt": "packed", "flags": ["UserPresent", "AttestedCredentialData"], @@ -75,8 +75,8 @@ "name": "touchid 1", "relyingParty": {"id": "localhost", "name": "Test"}, "user": {"id": "AQIDBA", "name": "test", "displayName": "Test"}, - "registration": {"challenge":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8","credentialId":"sF1j8tUniIBMm6D25knMoFo78_c","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EeEFSRWhNVUZSWVhHQmthR3h3ZEhoOCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAAAAAAAAAAAAAAAAAAAAAAAAFLBdY_LVJ4iATJug9uZJzKBaO_P3pQECAyYgASFYIFD9Km3kX7Rcmcn5qY34qTCe1w1Veg2Cl3scv8wU3-KlIlggRjFcsG6zPRicnEgLI6VdYoI0YFAuhRiSCrzT2ejIogE"}}, - "authentication": {"challenge":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8","credentialId":"sF1j8tUniIBMm6D25knMoFo78_c","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EeEFSRWhNVUZSWVhHQmthR3h3ZEhoOCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","signature":"MEUCIQD288F5ndy_OvPPjlxZCMVLZnIuWb4NL13soOtUeGuIzwIgGTCmWR4TqTgFyMr5Zj2JCQzRi8Fw0Qya2MV0mdkSfMM","userHandle":"AQIDBA"}}, + "registration": {"token":"mytoken","challenge":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8","credentialId":"sF1j8tUniIBMm6D25knMoFo78_c","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EeEFSRWhNVUZSWVhHQmthR3h3ZEhoOCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAAAAAAAAAAAAAAAAAAAAAAAAFLBdY_LVJ4iATJug9uZJzKBaO_P3pQECAyYgASFYIFD9Km3kX7Rcmcn5qY34qTCe1w1Veg2Cl3scv8wU3-KlIlggRjFcsG6zPRicnEgLI6VdYoI0YFAuhRiSCrzT2ejIogE"}}, + "authentication": {"token":"mytoken","challenge":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8","credentialId":"sF1j8tUniIBMm6D25knMoFo78_c","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EeEFSRWhNVUZSWVhHQmthR3h3ZEhoOCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","signature":"MEUCIQD288F5ndy_OvPPjlxZCMVLZnIuWb4NL13soOtUeGuIzwIgGTCmWR4TqTgFyMr5Zj2JCQzRi8Fw0Qya2MV0mdkSfMM","userHandle":"AQIDBA"}}, "attestation": { "fmt": "none", "flags": ["UserPresent", "UserVerified", "RFU2", "RFU3", "AttestedCredentialData"], @@ -93,8 +93,8 @@ "name": "touchid 2", "relyingParty": {"id": "localhost", "name": "Test"}, "user": {"id": "AQIDBA", "name": "test", "displayName": "Test"}, - "registration": {"challenge":"qwpphdaakQIY6nj38xrzT_Fv6E6rTkp-cVf4KqG5dds","credentialId":"iNoCFwrwzmTJg12Dq19J3e0FaK4","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoicXdwcGhkYWFrUUlZNm5qMzh4cnpUX0Z2NkU2clRrcC1jVmY0S3FHNWRkcyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAAAAAAAAAAAAAAAAAAAAAAAAFIjaAhcK8M5kyYNdg6tfSd3tBWiupQECAyYgASFYIOrn5xzwjOzDjZRJgMytQz-Mc3WKdTaRipGuqhYcqC8CIlggALld712ougeXgzMdE0sAzk-Y1xI7Lf-3yMhqnrPNH6o"}}, - "authentication": {"challenge":"u1opD5oUNJALsrYFJUrLpJOyPApU2pw0wC5jKoe1JKs","credentialId":"iNoCFwrwzmTJg12Dq19J3e0FaK4","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoidTFvcEQ1b1VOSkFMc3JZRkpVckxwSk95UEFwVTJwdzB3QzVqS29lMUpLcyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","signature":"MEYCIQC6wWQxlzK8xV5Wv9l2GzzSOBH2PImLDamWEcnoIOBStQIhAKNAASoESPHL90Ylaa6eBAsVfDcXo8m6UALIwbbgNYAH","userHandle":"AQIDBA"}}, + "registration": {"token":"mytoken","challenge":"qwpphdaakQIY6nj38xrzT_Fv6E6rTkp-cVf4KqG5dds","credentialId":"iNoCFwrwzmTJg12Dq19J3e0FaK4","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoicXdwcGhkYWFrUUlZNm5qMzh4cnpUX0Z2NkU2clRrcC1jVmY0S3FHNWRkcyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAAAAAAAAAAAAAAAAAAAAAAAAFIjaAhcK8M5kyYNdg6tfSd3tBWiupQECAyYgASFYIOrn5xzwjOzDjZRJgMytQz-Mc3WKdTaRipGuqhYcqC8CIlggALld712ougeXgzMdE0sAzk-Y1xI7Lf-3yMhqnrPNH6o"}}, + "authentication": {"token":"mytoken","challenge":"u1opD5oUNJALsrYFJUrLpJOyPApU2pw0wC5jKoe1JKs","credentialId":"iNoCFwrwzmTJg12Dq19J3e0FaK4","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoidTFvcEQ1b1VOSkFMc3JZRkpVckxwSk95UEFwVTJwdzB3QzVqS29lMUpLcyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","signature":"MEYCIQC6wWQxlzK8xV5Wv9l2GzzSOBH2PImLDamWEcnoIOBStQIhAKNAASoESPHL90Ylaa6eBAsVfDcXo8m6UALIwbbgNYAH","userHandle":"AQIDBA"}}, "attestation": { "fmt": "none", "flags": ["UserPresent", "UserVerified", "RFU2", "RFU3", "AttestedCredentialData"], @@ -111,8 +111,8 @@ "name": "touchid direct attestation 1", "relyingParty": {"id": "localhost", "name": "Test"}, "user": {"id": "AQIDBA", "name": "test", "displayName": "Test"}, - "registration": {"challenge":"KbXTAV5q2iKyaPeAdoGT75_L_hDTYPx0tWGJ2VwIw3g","credentialId":"BHvShvi2_uZarht1ruEBhwsgTog","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiS2JYVEFWNXEyaUt5YVBlQWRvR1Q3NV9MX2hEVFlQeDB0V0dKMlZ3SXczZyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAR70ob4tv7mWq4bda7hAYcLIE6IpQECAyYgASFYILxfp9_C29H142JpdlVPjAHQmPOkAkZpVPcmCYSTgOlQIlggQmnVXkpd2AhRiFYPrgXeGjwfYWmqA4wiYiaUmMl0bhc"}}, - "authentication": {"challenge":"yjzHdIU1BYH8zAyt_EZN77KhlKWPfxftoqN0JFR8CRE","credentialId":"BHvShvi2_uZarht1ruEBhwsgTog","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieWp6SGRJVTFCWUg4ekF5dF9FWk43N0tobEtXUGZ4ZnRvcU4wSkZSOENSRSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","signature":"MEUCIQDlkbSijx3EJUd43m326WqAAKdMtvAA-g0_RY4Y5d4tQQIgFMziwcuHAtbQyuTlybnSXxCcJA2RHP7Xlx8UM2TvdDQ","userHandle":"AQIDBA"}}, + "registration": {"token":"mytoken","challenge":"KbXTAV5q2iKyaPeAdoGT75_L_hDTYPx0tWGJ2VwIw3g","credentialId":"BHvShvi2_uZarht1ruEBhwsgTog","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiS2JYVEFWNXEyaUt5YVBlQWRvR1Q3NV9MX2hEVFlQeDB0V0dKMlZ3SXczZyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAR70ob4tv7mWq4bda7hAYcLIE6IpQECAyYgASFYILxfp9_C29H142JpdlVPjAHQmPOkAkZpVPcmCYSTgOlQIlggQmnVXkpd2AhRiFYPrgXeGjwfYWmqA4wiYiaUmMl0bhc"}}, + "authentication": {"token":"mytoken","challenge":"yjzHdIU1BYH8zAyt_EZN77KhlKWPfxftoqN0JFR8CRE","credentialId":"BHvShvi2_uZarht1ruEBhwsgTog","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieWp6SGRJVTFCWUg4ekF5dF9FWk43N0tobEtXUGZ4ZnRvcU4wSkZSOENSRSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","signature":"MEUCIQDlkbSijx3EJUd43m326WqAAKdMtvAA-g0_RY4Y5d4tQQIgFMziwcuHAtbQyuTlybnSXxCcJA2RHP7Xlx8UM2TvdDQ","userHandle":"AQIDBA"}}, "attestation": { "fmt": "none", "flags": ["UserPresent", "UserVerified", "RFU2", "RFU3", "AttestedCredentialData"], @@ -129,8 +129,8 @@ "name": "touchid direct attestation 2", "relyingParty": {"id": "localhost", "name": "Test"}, "user": {"id": "AQIDBA", "name": "test", "displayName": "Test"}, - "registration": {"challenge":"btJBYO7wR4iA-MvqrSUUHJJw9aReqnIVJwTACbxUWss","credentialId":"qgoljeD3LMo68-oyMzr67YfGf_A","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYnRKQllPN3dSNGlBLU12cXJTVVVISkp3OWFSZXFuSVZKd1RBQ2J4VVdzcyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAAAAAAAAAAAAAAAAAAAAAAAAFKoKJY3g9yzKOvPqMjM6-u2Hxn_wpQECAyYgASFYIBXn8f6UEIcIBz9BqnOpPhdgn0HBSQZVIXrjxxFIliEOIlggsU0GGNUti9CqZMtulG0ooOkBrbZemWZdo8WhVLKrQEQ"}}, - "authentication": {"challenge":"u_PHux0VehI8OJUQ6-79RRW_A1ubvdbKf7HzfGwk_Gs","credentialId":"qgoljeD3LMo68-oyMzr67YfGf_A","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoidV9QSHV4MFZlaEk4T0pVUTYtNzlSUldfQTF1YnZkYktmN0h6Zkd3a19HcyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","signature":"MEYCIQCnrqN-P0BsptzamsPnkklFr-c5XT2-Eiu7S4BLZfuOcQIhAOv4PopJAD75fz0caQftXh3Y-yVXlQeHj2ogzn73jN7A","userHandle":"AQIDBA"}}, + "registration": {"token":"mytoken","challenge":"btJBYO7wR4iA-MvqrSUUHJJw9aReqnIVJwTACbxUWss","credentialId":"qgoljeD3LMo68-oyMzr67YfGf_A","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYnRKQllPN3dSNGlBLU12cXJTVVVISkp3OWFSZXFuSVZKd1RBQ2J4VVdzcyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAAAAAAAAAAAAAAAAAAAAAAAAFKoKJY3g9yzKOvPqMjM6-u2Hxn_wpQECAyYgASFYIBXn8f6UEIcIBz9BqnOpPhdgn0HBSQZVIXrjxxFIliEOIlggsU0GGNUti9CqZMtulG0ooOkBrbZemWZdo8WhVLKrQEQ"}}, + "authentication": {"token":"mytoken","challenge":"u_PHux0VehI8OJUQ6-79RRW_A1ubvdbKf7HzfGwk_Gs","credentialId":"qgoljeD3LMo68-oyMzr67YfGf_A","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoidV9QSHV4MFZlaEk4T0pVUTYtNzlSUldfQTF1YnZkYktmN0h6Zkd3a19HcyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","signature":"MEYCIQCnrqN-P0BsptzamsPnkklFr-c5XT2-Eiu7S4BLZfuOcQIhAOv4PopJAD75fz0caQftXh3Y-yVXlQeHj2ogzn73jN7A","userHandle":"AQIDBA"}}, "attestation": { "fmt": "none", "flags": ["UserPresent", "UserVerified", "RFU2", "RFU3", "AttestedCredentialData"], @@ -147,8 +147,8 @@ "name": "icloud keychain direct attestation 1", "relyingParty": {"id": "localhost", "name": "Test"}, "user": {"id": "AQIDBA", "name": "test", "displayName": "Test"}, - "registration": {"challenge":"tldgL6RIy03npx1p1ff2H1Wce0lWGsJOWxB4KJLDbO0","credentialId":"0CCOokMhLFQYmmH9xvSZDy-xT3o","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoidGxkZ0w2Ukl5MDNucHgxcDFmZjJIMVdjZTBsV0dzSk9XeEI0S0pMRGJPMCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAAAAAAAAAAAAAAAAAAAAAAAAFNAgjqJDISxUGJph_cb0mQ8vsU96pQECAyYgASFYIJrSs4AjYp1c40uIn3472kLbxdZ9tLsXZBF1jxGveBj5Ilggnjh7YGWt8Lwnmd0Ku-ALllohiyHzai1zrN9O_OXWYyI"}}, - "authentication": {"challenge":"PSK6zBnFj4jSaXbWoU7NNBcGVbIJNGjv01_A_aFA5FU","credentialId":"0CCOokMhLFQYmmH9xvSZDy-xT3o","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiUFNLNnpCbkZqNGpTYVhiV29VN05OQmNHVmJJSk5HanYwMV9BX2FGQTVGVSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","signature":"MEUCIC7F58qfcNpo5G5LdOQYrcvBTLCDJe54H7pPl_GYL0BOAiEAthsuEOzVMO-XyOreMOCuGThNUpsYeTgHTWPv4wVskgU","userHandle":"AQIDBA"}}, + "registration": {"token":"mytoken","challenge":"tldgL6RIy03npx1p1ff2H1Wce0lWGsJOWxB4KJLDbO0","credentialId":"0CCOokMhLFQYmmH9xvSZDy-xT3o","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoidGxkZ0w2Ukl5MDNucHgxcDFmZjJIMVdjZTBsV0dzSk9XeEI0S0pMRGJPMCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAAAAAAAAAAAAAAAAAAAAAAAAFNAgjqJDISxUGJph_cb0mQ8vsU96pQECAyYgASFYIJrSs4AjYp1c40uIn3472kLbxdZ9tLsXZBF1jxGveBj5Ilggnjh7YGWt8Lwnmd0Ku-ALllohiyHzai1zrN9O_OXWYyI"}}, + "authentication": {"token":"mytoken","challenge":"PSK6zBnFj4jSaXbWoU7NNBcGVbIJNGjv01_A_aFA5FU","credentialId":"0CCOokMhLFQYmmH9xvSZDy-xT3o","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiUFNLNnpCbkZqNGpTYVhiV29VN05OQmNHVmJJSk5HanYwMV9BX2FGQTVGVSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCJ9","signature":"MEUCIC7F58qfcNpo5G5LdOQYrcvBTLCDJe54H7pPl_GYL0BOAiEAthsuEOzVMO-XyOreMOCuGThNUpsYeTgHTWPv4wVskgU","userHandle":"AQIDBA"}}, "attestation": { "fmt": "none", "flags": ["UserPresent", "UserVerified", "RFU2", "RFU3", "AttestedCredentialData"], @@ -165,8 +165,8 @@ "name": "google chrome passkey direct attestation 1", "relyingParty": {"id": "localhost", "name": "Test"}, "user": {"id": "AQIDBA", "name": "test", "displayName": "Test"}, - "registration": {"challenge":"EYc31P9FFCnl598wRB2i8cmcz6ThyhW-zNGUmm2JzDc","credentialId":"PiLRkCr976lMNnLDM2uRlWh0rNO73f_pAVZM7DEMYJk","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiRVljMzFQOUZGQ25sNTk4d1JCMmk4Y21jejZUaHloVy16TkdVbW0ySnpEYyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEgwRgIhAK16kBjarVM6zvj8aWxsi051DcCH69gSz1Q10zUI_9VrAiEAmcZXqwGdnKhZ8ID9IRmt8A1fkZVnlCjGdZ8b_NLWt05oYXV0aERhdGFYpEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjRQAAAACtzgACNbzGCmSLCyXx8FUDACA-ItGQKv3vqUw2csMza5GVaHSs07vd_-kBVkzsMQxgmaUBAgMmIAEhWCBhVrbGZmJhDIrfPQk9Ewkiz3xIwXly_48eSgFqWK_FyyJYILzm0AwdinOdkkkFRMrgvh5pv0ibtBJiFa81zROSgHRH"}}, - "authentication": {"challenge":"1EuG9mJH0DlUFOtqMelAoLNrcivZrxLo-sf_MtczoAU","credentialId":"PiLRkCr976lMNnLDM2uRlWh0rNO73f_pAVZM7DEMYJk","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiMUV1RzltSkgwRGxVRk90cU1lbEFvTE5yY2l2WnJ4TG8tc2ZfTXRjem9BVSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","signature":"MEUCIA3NMkv55WGGSjNKphJN7CHoX8qrCYUC-jfGfbLxyNtJAiEA_96kZOR-CD2CkUWi1kOtEt9x8jngm_VMhCTPHaPAlVY","userHandle":"AQIDBA"}}, + "registration": {"token":"mytoken","challenge":"EYc31P9FFCnl598wRB2i8cmcz6ThyhW-zNGUmm2JzDc","credentialId":"PiLRkCr976lMNnLDM2uRlWh0rNO73f_pAVZM7DEMYJk","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiRVljMzFQOUZGQ25sNTk4d1JCMmk4Y21jejZUaHloVy16TkdVbW0ySnpEYyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEgwRgIhAK16kBjarVM6zvj8aWxsi051DcCH69gSz1Q10zUI_9VrAiEAmcZXqwGdnKhZ8ID9IRmt8A1fkZVnlCjGdZ8b_NLWt05oYXV0aERhdGFYpEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjRQAAAACtzgACNbzGCmSLCyXx8FUDACA-ItGQKv3vqUw2csMza5GVaHSs07vd_-kBVkzsMQxgmaUBAgMmIAEhWCBhVrbGZmJhDIrfPQk9Ewkiz3xIwXly_48eSgFqWK_FyyJYILzm0AwdinOdkkkFRMrgvh5pv0ibtBJiFa81zROSgHRH"}}, + "authentication": {"token":"mytoken","challenge":"1EuG9mJH0DlUFOtqMelAoLNrcivZrxLo-sf_MtczoAU","credentialId":"PiLRkCr976lMNnLDM2uRlWh0rNO73f_pAVZM7DEMYJk","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiMUV1RzltSkgwRGxVRk90cU1lbEFvTE5yY2l2WnJ4TG8tc2ZfTXRjem9BVSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","signature":"MEUCIA3NMkv55WGGSjNKphJN7CHoX8qrCYUC-jfGfbLxyNtJAiEA_96kZOR-CD2CkUWi1kOtEt9x8jngm_VMhCTPHaPAlVY","userHandle":"AQIDBA"}}, "attestation": { "fmt": "packed", "flags": ["UserPresent", "UserVerified", "AttestedCredentialData"], diff --git a/pkg/errs/errors.go b/pkg/errs/errors.go index f8fcf1f..77a1449 100644 --- a/pkg/errs/errors.go +++ b/pkg/errs/errors.go @@ -5,12 +5,11 @@ import ( ) var ( - ErrUnsupportedPublicKey = errors.New("unsupported public key type") - ErrInvalidKeyForAlg = errors.New("invalid key for alg") - ErrSignatureMismatch = errors.New("signature mismatch") - ErrUserNotFound = errors.New("user not found") - ErrCredentialNotFound = errors.New("credential not found") - ErrNoCredentials = errors.New("user has no credential") - ErrUnrecognizedChallenge = errors.New("unrecognized challenge") - ErrInvalidChallenge = errors.New("invalid challenge size") + ErrUnsupportedPublicKey = errors.New("unsupported public key type") + ErrInvalidKeyForAlg = errors.New("invalid key for alg") + ErrSignatureMismatch = errors.New("signature mismatch") + ErrUserNotFound = errors.New("user not found") + ErrCredentialNotFound = errors.New("credential not found") + ErrNoCredentials = errors.New("user has no credential") + ErrInvalidChallenge = errors.New("invalid challenge size") ) diff --git a/register_create.go b/register_create.go index af8a101..80d109d 100644 --- a/register_create.go +++ b/register_create.go @@ -9,6 +9,7 @@ import ( // RegistrationChallenge is the challenge that is sent to the client to initiate a registration ceremony. type RegistrationChallenge struct { + Token string `json:"token"` Challenge string `json:"challenge"` RP RelyingParty `json:"rp"` User User `json:"user"` @@ -22,9 +23,10 @@ func (w *webauthn) CreateRegistration(ctx context.Context, user User) (*Registra return nil, errutil.Wrapf(err, "generating challenge") } - // Store the challenge in the challenge store - if err := w.options.Challenges.StoreChallenge(ctx, user, challengeBytes); err != nil { - return nil, errutil.Wrapf(err, "storing challenge") + // Create the token for the challenge + token, err := w.options.Tokener.CreateToken(challengeBytes, user) + if err != nil { + return nil, errutil.Wrapf(err, "creating token") } // Format the public key credential params for the client @@ -37,6 +39,7 @@ func (w *webauthn) CreateRegistration(ctx context.Context, user User) (*Registra } return &RegistrationChallenge{ + Token: token, Challenge: w.options.Codec.EncodeToString(challengeBytes[:]), RP: w.options.RP, User: user, diff --git a/register_create_test.go b/register_create_test.go index 1d3c1cd..dcf0133 100644 --- a/register_create_test.go +++ b/register_create_test.go @@ -5,10 +5,7 @@ import ( "errors" "testing" - "github.com/spiretechnology/go-webauthn" "github.com/spiretechnology/go-webauthn/internal/testutil" - "github.com/spiretechnology/go-webauthn/pkg/challenge" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -18,30 +15,27 @@ func TestCreateRegistration(t *testing.T) { tcChallenge := tc.RegistrationChallenge() t.Run(tc.Name, func(t *testing.T) { - t.Run("storing challenge fails", func(t *testing.T) { - w, credentials, challenges := setupMocks(tc, nil) - challenges.On("StoreChallenge", mock.Anything, tc.User, mock.Anything).Return(errors.New("test error")).Once() + t.Run("creating challenge token fails", func(t *testing.T) { + w, credentials, tokener := setupMocks(tc, tc.RegistrationChallenge) + tokener.On("CreateToken", tcChallenge, tc.User).Return("", errors.New("test error")).Once() challenge, err := w.CreateRegistration(ctx, tc.User) require.Nil(t, challenge, "challenge should be nil") require.Error(t, err, "error should not be nil") credentials.AssertExpectations(t) - challenges.AssertExpectations(t) + tokener.AssertExpectations(t) }) t.Run("creates registration successfully", func(t *testing.T) { - w, credentials, challenges := setupMocks(tc, &webauthn.Options{ - ChallengeFunc: func() (challenge.Challenge, error) { - return tcChallenge, nil - }, - }) - challenges.On("StoreChallenge", mock.Anything, tc.User, mock.Anything).Return(nil).Once() + w, credentials, tokener := setupMocks(tc, tc.RegistrationChallenge) + tokener.On("CreateToken", tcChallenge, tc.User).Return(tc.Registration.Token, nil).Once() challenge, err := w.CreateRegistration(ctx, tc.User) require.NotNil(t, challenge, "challenge should not be nil") require.Nil(t, err, "error should be nil") + require.Equal(t, tc.Registration.Token, challenge.Token, "token should match") require.Equal(t, testutil.Encode(tcChallenge[:]), challenge.Challenge, "challenge should match") require.Equal(t, tc.RelyingParty, challenge.RP, "relying party should match") require.Equal(t, tc.User.ID, challenge.User.ID, "user id should match") @@ -50,7 +44,7 @@ func TestCreateRegistration(t *testing.T) { require.Equal(t, 9, len(challenge.PubKeyCredParams), "pub key cred params should match") credentials.AssertExpectations(t) - challenges.AssertExpectations(t) + tokener.AssertExpectations(t) }) }) } diff --git a/register_verify.go b/register_verify.go index e2f200b..3d7a7b8 100644 --- a/register_verify.go +++ b/register_verify.go @@ -15,6 +15,7 @@ import ( // RegistrationResponse is the response sent back by the client after a registration ceremony. type RegistrationResponse struct { + Token string `json:"token"` Challenge string `json:"challenge"` CredentialID string `json:"credentialId"` Response AuthenticatorAttestationResponse `json:"response"` @@ -35,17 +36,10 @@ func (w *webauthn) VerifyRegistration(ctx context.Context, user User, res *Regis return nil, errutil.Wrap(errs.ErrInvalidChallenge) } challengeBytes := challenge.Challenge(challengeBytesSlice) - ok, err := w.options.Challenges.HasChallenge(ctx, user, challengeBytes) - if err != nil { - return nil, errutil.Wrapf(err, "checking challenge") - } - if !ok { - return nil, errutil.Wrap(errs.ErrUnrecognizedChallenge) - } - // Remove the challenge from the store. It's no longer needed. - if err := w.options.Challenges.RemoveChallenge(ctx, user, challengeBytes); err != nil { - return nil, errutil.Wrapf(err, "removing challenge") + // Verify the challenge token + if err := w.options.Tokener.VerifyToken(res.Token, challengeBytes, user); err != nil { + return nil, errutil.Wrapf(err, "verifying token") } // Decode the attestation response to spec types diff --git a/register_verify_test.go b/register_verify_test.go index 8a93b6f..4827bcb 100644 --- a/register_verify_test.go +++ b/register_verify_test.go @@ -2,12 +2,10 @@ package webauthn_test import ( "context" + "errors" "testing" - "github.com/spiretechnology/go-webauthn" "github.com/spiretechnology/go-webauthn/internal/testutil" - "github.com/spiretechnology/go-webauthn/pkg/challenge" - "github.com/spiretechnology/go-webauthn/pkg/errs" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -18,26 +16,21 @@ func TestVerifyRegistration(t *testing.T) { tcChallenge := tc.RegistrationChallenge() t.Run(tc.Name, func(t *testing.T) { - t.Run("challenge doesn't exist", func(t *testing.T) { - w, credentials, challenges := setupMocks(tc, nil) - challenges.On("HasChallenge", mock.Anything, tc.User, tcChallenge).Return(false, nil).Once() + t.Run("challenge token is invalid", func(t *testing.T) { + w, credentials, tokener := setupMocks(tc, tc.RegistrationChallenge) + tokener.On("VerifyToken", tc.Registration.Token, tcChallenge, tc.User).Return(errors.New("invalid token")).Once() result, err := w.VerifyRegistration(ctx, tc.User, &tc.Registration) require.Nil(t, result, "result should be nil") - require.ErrorIs(t, err, errs.ErrUnrecognizedChallenge, "error should be errTest") + require.Error(t, err, "verify registration should error") credentials.AssertExpectations(t) - challenges.AssertExpectations(t) + tokener.AssertExpectations(t) }) t.Run("verifies registration successfully", func(t *testing.T) { - w, credentials, challenges := setupMocks(tc, &webauthn.Options{ - ChallengeFunc: func() (challenge.Challenge, error) { - return tcChallenge, nil - }, - }) - challenges.On("HasChallenge", mock.Anything, tc.User, tcChallenge).Return(true, nil).Once() - challenges.On("RemoveChallenge", mock.Anything, tc.User, tcChallenge).Return(nil).Once() + w, credentials, tokener := setupMocks(tc, tc.RegistrationChallenge) + tokener.On("VerifyToken", tc.Registration.Token, tcChallenge, tc.User).Return(nil).Once() credentials.On("StoreCredential", mock.Anything, tc.User, mock.Anything, mock.Anything).Return(nil).Once() result, err := w.VerifyRegistration(ctx, tc.User, &tc.Registration) @@ -45,29 +38,8 @@ func TestVerifyRegistration(t *testing.T) { require.NotNil(t, result, "result should not be nil") credentials.AssertExpectations(t) - challenges.AssertExpectations(t) + tokener.AssertExpectations(t) }) - - // t.Run("fails with invalid public key alg", func(t *testing.T) { - // w, credentials, challenges := setupMocks(tc, &webauthn.Options{ - // ChallengeFunc: func() (challenge.Challenge, error) { - // return tcChallenge, nil - // }, - // }) - // challenges.On("HasChallenge", mock.Anything, tc.User, tcChallenge).Return(true, nil).Once() - // challenges.On("RemoveChallenge", mock.Anything, tc.User, tcChallenge).Return(nil).Once() - - // // Switch to an unsupported public key alg - // res := &tc.Registration - // res.PublicKeyAlg = 0 - - // result, err := w.VerifyRegistration(ctx, tc.User, res) - // require.Nil(t, result, "result should be nil") - // require.Error(t, err, "error should not be nil") - - // credentials.AssertExpectations(t) - // challenges.AssertExpectations(t) - // }) }) } } diff --git a/store_challenges.go b/store_challenges.go deleted file mode 100644 index 64147d8..0000000 --- a/store_challenges.go +++ /dev/null @@ -1,14 +0,0 @@ -package webauthn - -import ( - "context" - - "github.com/spiretechnology/go-webauthn/pkg/challenge" -) - -// Challenges defines the interface for storing and recalling challenges that have been issued to users. -type Challenges interface { - StoreChallenge(ctx context.Context, user User, challenge challenge.Challenge) error - HasChallenge(ctx context.Context, user User, challege challenge.Challenge) (bool, error) - RemoveChallenge(ctx context.Context, user User, challege challenge.Challenge) error -} diff --git a/store_challenges_inmem.go b/store_challenges_inmem.go deleted file mode 100644 index 56dd689..0000000 --- a/store_challenges_inmem.go +++ /dev/null @@ -1,53 +0,0 @@ -package webauthn - -import ( - "context" - "sync" - - "github.com/spiretechnology/go-webauthn/pkg/challenge" -) - -type storedChallege struct { - userID string - challenge challenge.Challenge -} - -func challengeKey(user User, challenge challenge.Challenge) storedChallege { - return storedChallege{ - userID: user.ID, - challenge: challenge, - } -} - -// NewChallengesInMemory returns a new in-memory implementation of the Challenges interface. -func NewChallengesInMemory() Challenges { - return &inMemChallenges{ - challenges: make(map[storedChallege]struct{}), - } -} - -type inMemChallenges struct { - challenges map[storedChallege]struct{} - mut sync.RWMutex -} - -func (c *inMemChallenges) StoreChallenge(ctx context.Context, user User, challenge challenge.Challenge) error { - c.mut.Lock() - defer c.mut.Unlock() - c.challenges[challengeKey(user, challenge)] = struct{}{} - return nil -} - -func (c *inMemChallenges) HasChallenge(ctx context.Context, user User, challenge challenge.Challenge) (bool, error) { - c.mut.RLock() - defer c.mut.RUnlock() - _, ok := c.challenges[challengeKey(user, challenge)] - return ok, nil -} - -func (c *inMemChallenges) RemoveChallenge(ctx context.Context, user User, challenge challenge.Challenge) error { - c.mut.Lock() - defer c.mut.Unlock() - delete(c.challenges, challengeKey(user, challenge)) - return nil -} diff --git a/tokener.go b/tokener.go new file mode 100644 index 0000000..28995ca --- /dev/null +++ b/tokener.go @@ -0,0 +1,12 @@ +package webauthn + +import ( + "github.com/spiretechnology/go-webauthn/pkg/challenge" +) + +// Tokener defines the interface for creating tokens to ensure the authenticity of registration and +// authentication responses from users. +type Tokener interface { + CreateToken(challenge challenge.Challenge, user User) (string, error) + VerifyToken(token string, challenge challenge.Challenge, user User) error +} diff --git a/tokener_jwt.go b/tokener_jwt.go new file mode 100644 index 0000000..0d85134 --- /dev/null +++ b/tokener_jwt.go @@ -0,0 +1,77 @@ +package webauthn + +import ( + "crypto/sha256" + "encoding/hex" + "time" + + "github.com/spiretechnology/go-jwt/v2" + "github.com/spiretechnology/go-webauthn/internal/errutil" + "github.com/spiretechnology/go-webauthn/pkg/challenge" +) + +// NewJwtTokener creates a new tokener that issues JWT tokens. +func NewJwtTokener(signer jwt.Signer, verifier jwt.Verifier) Tokener { + return &jwtTokener{signer, verifier} +} + +type jwtTokener struct { + signer jwt.Signer + verifier jwt.Verifier +} + +type jwtTokenClaims struct { + UserID string `json:"uid"` + ChallengeHash string `json:"chash"` + ExpiresAt int64 `json:"exp"` +} + +func (t *jwtTokener) CreateToken(challenge challenge.Challenge, user User) (string, error) { + challengeHash := sha256.Sum256(challenge[:]) + claims := jwtTokenClaims{ + UserID: user.ID, + ChallengeHash: hex.EncodeToString(challengeHash[:]), + ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), + } + return jwt.Create(claims, t.signer) +} + +func (t *jwtTokener) VerifyToken(token string, challenges challenge.Challenge, user User) error { + // Parse the token to a JWT + jwtToken, err := jwt.Parse(token) + if err != nil { + return errutil.Wrapf(err, "parsing jwt token") + } + + // Verify the token's signature + valid, err := jwtToken.Verify(t.verifier) + if err != nil { + return errutil.Wrapf(err, "verifying jwt token") + } + if !valid { + return errutil.New("invalid jwt token") + } + + // Unmarshal the claims in the token + var claims jwtTokenClaims + if err := jwtToken.Claims(&claims); err != nil { + return errutil.Wrapf(err, "unmarshaling jwt token claims") + } + + // Verify the challenge hash in the token matches the challenge hash in the request + challengeHash := sha256.Sum256(challenges[:]) + if claims.ChallengeHash != hex.EncodeToString(challengeHash[:]) { + return errutil.New("invalid challenge hash") + } + + // Verify the expiration time of the token + if time.Now().Unix() > claims.ExpiresAt { + return errutil.New("token is expired") + } + + // Verify the user ID in the token matches the user ID in the request + if claims.UserID != user.ID { + return errutil.New("invalid user ID") + } + return nil +} diff --git a/webauthn.go b/webauthn.go index 9fc365b..de88f3e 100644 --- a/webauthn.go +++ b/webauthn.go @@ -2,8 +2,10 @@ package webauthn import ( "context" + "crypto/rand" "encoding/base64" + "github.com/spiretechnology/go-jwt/v2" "github.com/spiretechnology/go-webauthn/pkg/challenge" "github.com/spiretechnology/go-webauthn/pkg/codec" "github.com/spiretechnology/go-webauthn/pkg/pubkey" @@ -21,7 +23,7 @@ type Options struct { Codec codec.Codec PublicKeyTypes []pubkey.KeyType Credentials Credentials - Challenges Challenges + Tokener Tokener ChallengeFunc func() (challenge.Challenge, error) } @@ -29,15 +31,20 @@ func New(options Options) WebAuthn { if options.Codec == nil { options.Codec = base64.RawURLEncoding } - if options.Challenges == nil { - options.Challenges = NewChallengesInMemory() - } if options.PublicKeyTypes == nil { options.PublicKeyTypes = pubkey.AllKeyTypes } if options.ChallengeFunc == nil { options.ChallengeFunc = challenge.GenerateChallenge } + if options.Tokener == nil { + secret := make([]byte, 64) + rand.Read(secret) + options.Tokener = NewJwtTokener( + jwt.HS256Signer(secret), + jwt.HS256Verifier(secret), + ) + } return &webauthn{options} } diff --git a/webauthn_test.go b/webauthn_test.go index bd210b5..65e97d0 100644 --- a/webauthn_test.go +++ b/webauthn_test.go @@ -4,21 +4,23 @@ import ( "github.com/spiretechnology/go-webauthn" "github.com/spiretechnology/go-webauthn/internal/mocks" "github.com/spiretechnology/go-webauthn/internal/testutil" + "github.com/spiretechnology/go-webauthn/pkg/challenge" ) -func setupMocks(tc testutil.TestCase, opts *webauthn.Options) (webauthn.WebAuthn, *mocks.MockCredentials, *mocks.MockChallenges) { +func setupMocks(tc testutil.TestCase, challengeFunc func() challenge.Challenge) (webauthn.WebAuthn, *mocks.MockCredentials, *mocks.MockTokener) { credentials := &mocks.MockCredentials{} - challenges := &mocks.MockChallenges{} + tokener := &mocks.MockTokener{} var options webauthn.Options - if opts != nil { - options = *opts - } options.RP = tc.RelyingParty options.Credentials = credentials - options.Challenges = challenges + options.Tokener = tokener + if challengeFunc != nil { + options.ChallengeFunc = func() (challenge.Challenge, error) { + return challengeFunc(), nil + } + } w := webauthn.New(options) - - return w, credentials, challenges + return w, credentials, tokener }