diff --git a/server/api/handler/admin/user.go b/server/api/handler/admin/user.go index 04b6519..13deeb7 100644 --- a/server/api/handler/admin/user.go +++ b/server/api/handler/admin/user.go @@ -32,9 +32,16 @@ func NewUserHandler(persister persistence.Persister) UserHandler { func (uh *userHandler) List(ctx echo.Context) error { var request adminRequest.UserListRequest - err := (&echo.DefaultBinder{}).BindQueryParams(ctx, &request) + err := ctx.Bind(&request) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "unable to parse request") + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusBadRequest, "unable to list users").SetInternal(err) + } + + err = ctx.Validate(&request) + if err != nil { + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusBadRequest, "unable to list users").SetInternal(err) } if request.Page == 0 { @@ -92,12 +99,12 @@ func (uh *userHandler) Get(ctx echo.Context) error { userIdString := ctx.Param("user_id") if userIdString == "" { - return echo.NewHTTPError(http.StatusBadRequest, "missing user_id") + return echo.NewHTTPError(http.StatusBadRequest, "user_id must be a valid uuid4") } userId, err := uuid.FromString(userIdString) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "invalid user_id") + return echo.NewHTTPError(http.StatusBadRequest, "user_id must be a valid uuid4") } return uh.persister.GetConnection().Transaction(func(tx *pop.Connection) error { @@ -126,12 +133,12 @@ func (uh *userHandler) Remove(ctx echo.Context) error { userIdString := ctx.Param("user_id") if userIdString == "" { - return echo.NewHTTPError(http.StatusBadRequest, "missing user_id") + return echo.NewHTTPError(http.StatusBadRequest, "user_id must be a valid uuid4") } userId, err := uuid.FromString(userIdString) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "invalid user_id") + return echo.NewHTTPError(http.StatusBadRequest, "user_id must be a valid uuid4") } return uh.persister.GetConnection().Transaction(func(tx *pop.Connection) error { diff --git a/server/api/router/admin_secrets_test.go b/server/api/router/admin_secrets_test.go new file mode 100644 index 0000000..b0620ee --- /dev/null +++ b/server/api/router/admin_secrets_test.go @@ -0,0 +1,615 @@ +package router + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/api/dto/admin/request" + "github.com/teamhanko/passkey-server/api/dto/admin/response" + "github.com/teamhanko/passkey-server/config" + "net/http" + "net/http/httptest" +) + +func (s *adminSuite) TestAdminRouter_ApiKey_List() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + ExpectedCount int + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + ExpectedCount: 1, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "\"name\":\"API KEY 1\",\"secret\":", + }, + { + Name: "unknown tenant", + TenantID: "00000000-0000-0000-0000-000000000000", + + ExpectedCount: 0, + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "no api key", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396d", + + ExpectedCount: 0, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "[]", + }, + { + Name: "malformed tenant", + TenantID: "malformed", + + ExpectedCount: 0, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + SimulateBrokenDB: true, + + ExpectedCount: 0, + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadFixtures("../../test/fixtures/common") + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/tenants/%s/secrets/api", currentTest.TenantID), nil) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + var requestBody response.SecretResponseListDto + err = json.Unmarshal(rec.Body.Bytes(), &requestBody) + s.Require().NoError(err) + + s.Assert().Len(requestBody, currentTest.ExpectedCount) + } + }) + } +} + +func (s *adminSuite) TestAdminRouter_ApiKey_Create() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + RequestBody interface{} + + OmitRequestBody bool + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: request.CreateSecretDto{Name: "test-secret"}, + + ExpectedStatusCode: http.StatusCreated, + ExpectedStatusMessage: "\"name\":\"test-secret\",\"secret\":", + }, + { + Name: "api key already exists", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: request.CreateSecretDto{Name: "API KEY 1"}, + + ExpectedStatusCode: http.StatusConflict, + ExpectedStatusMessage: "Secret with this name already exists", + }, + { + Name: "malformed request body", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Lorem ipsum", + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "name is a required field", + }, + { + Name: "missing request body", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + OmitRequestBody: true, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "name is a required field", + }, + { + Name: "unknown tenant", + TenantID: "00000000-0000-0000-0000-000000000000", + RequestBody: request.CreateSecretDto{Name: "test-secret"}, + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant", + TenantID: "malformed", + RequestBody: request.CreateSecretDto{Name: "test-secret"}, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: request.CreateSecretDto{Name: "test-secret"}, + + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadFixtures("../../test/fixtures/common") + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/tenants/%s/secrets/api", currentTest.TenantID), nil) + } else { + body, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/tenants/%s/secrets/api", currentTest.TenantID), bytes.NewReader(body)) + } + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err = s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + }) + } +} + +func (s *adminSuite) TestAdminRouter_ApiKey_Remove() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + ApiKeyID string + + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKeyID: "b8abe127-d122-459f-a0f9-86a569e684b5", + + ExpectedStatusCode: http.StatusNoContent, + }, + { + Name: "unknown tenant", + TenantID: "00000000-0000-0000-0000-000000000000", + ApiKeyID: "b8abe127-d122-459f-a0f9-86a569e684b5", + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant", + TenantID: "malformed", + ApiKeyID: "b8abe127-d122-459f-a0f9-86a569e684b5", + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "unknown secret", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKeyID: "b8abe128-d122-459f-a0f9-86a569e684b5", + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "secret with ID 'b8abe128-d122-459f-a0f9-86a569e684b5' not found", + }, + { + Name: "malformed secret", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKeyID: "malformed", + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "SecretId must be a valid", + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKeyID: "b8abe127-d122-459f-a0f9-86a569e684b5", + + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadFixtures("../../test/fixtures/common") + s.Require().NoError(err) + + oldTenant, err := s.Storage.GetTenantPersister(nil).Get(uuid.FromStringOrNil(currentTest.TenantID)) + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/tenants/%s/secrets/api/%s", currentTest.TenantID, currentTest.ApiKeyID), nil) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err = s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusNoContent { + tenant, err := s.Storage.GetTenantPersister(nil).Get(uuid.FromStringOrNil(currentTest.TenantID)) + s.Require().NoError(err) + + s.Assert().NotEqual(len(oldTenant.Config.Secrets), len(tenant.Config.Secrets)) + } + }) + } +} + +func (s *adminSuite) TestAdminRouter_JWK_List() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + ExpectedCount int + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + ExpectedCount: 1, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "\"name\":\"JWK KEY 1\",\"secret\":", + }, + { + Name: "unknown tenant", + TenantID: "00000000-0000-0000-0000-000000000000", + + ExpectedCount: 0, + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "no jwk key", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396d", + + ExpectedCount: 0, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "[]", + }, + { + Name: "malformed tenant", + TenantID: "malformed", + + ExpectedCount: 0, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + SimulateBrokenDB: true, + + ExpectedCount: 0, + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadFixtures("../../test/fixtures/common") + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/tenants/%s/secrets/jwk", currentTest.TenantID), nil) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + var requestBody response.SecretResponseListDto + err = json.Unmarshal(rec.Body.Bytes(), &requestBody) + s.Require().NoError(err) + + s.Assert().Len(requestBody, currentTest.ExpectedCount) + } + }) + } +} + +func (s *adminSuite) TestAdminRouter_JWK_Create() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + RequestBody interface{} + + OmitRequestBody bool + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: request.CreateSecretDto{Name: "test-secret"}, + + ExpectedStatusCode: http.StatusCreated, + ExpectedStatusMessage: "\"name\":\"test-secret\",\"secret\":", + }, + { + Name: "jwk key already exists", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: request.CreateSecretDto{Name: "JWK KEY 1"}, + + ExpectedStatusCode: http.StatusConflict, + ExpectedStatusMessage: "Secret with this name already exists", + }, + { + Name: "malformed request body", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Lorem ipsum", + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "name is a required field", + }, + { + Name: "missing request body", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + OmitRequestBody: true, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "name is a required field", + }, + { + Name: "unknown tenant", + TenantID: "00000000-0000-0000-0000-000000000000", + RequestBody: request.CreateSecretDto{Name: "test-secret"}, + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant", + TenantID: "malformed", + RequestBody: request.CreateSecretDto{Name: "test-secret"}, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: request.CreateSecretDto{Name: "test-secret"}, + + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadFixtures("../../test/fixtures/common") + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/tenants/%s/secrets/jwk", currentTest.TenantID), nil) + } else { + body, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/tenants/%s/secrets/jwk", currentTest.TenantID), bytes.NewReader(body)) + } + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err = s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + }) + } +} + +func (s *adminSuite) TestAdminRouter_JWK_Remove() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + JwkKeyID string + + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + JwkKeyID: "b8abe127-d122-459f-a0f9-86a569e684b6", + + ExpectedStatusCode: http.StatusNoContent, + }, + { + Name: "unknown tenant", + TenantID: "00000000-0000-0000-0000-000000000000", + JwkKeyID: "b8abe127-d122-459f-a0f9-86a569e684b6", + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant", + TenantID: "malformed", + JwkKeyID: "b8abe127-d122-459f-a0f9-86a569e684b6", + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "unknown secret", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + JwkKeyID: "b8abe128-d122-459f-a0f9-86a569e684b5", + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "secret with ID 'b8abe128-d122-459f-a0f9-86a569e684b5' not found", + }, + { + Name: "malformed secret", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + JwkKeyID: "malformed", + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "SecretId must be a valid", + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + JwkKeyID: "b8abe127-d122-459f-a0f9-86a569e684b6", + + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadFixtures("../../test/fixtures/common") + s.Require().NoError(err) + + oldTenant, err := s.Storage.GetTenantPersister(nil).Get(uuid.FromStringOrNil(currentTest.TenantID)) + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/tenants/%s/secrets/jwk/%s", currentTest.TenantID, currentTest.JwkKeyID), nil) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err = s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusNoContent { + tenant, err := s.Storage.GetTenantPersister(nil).Get(uuid.FromStringOrNil(currentTest.TenantID)) + s.Require().NoError(err) + + s.Assert().NotEqual(len(oldTenant.Config.Secrets), len(tenant.Config.Secrets)) + } + }) + } +} diff --git a/server/api/router/admin_tenants_test.go b/server/api/router/admin_tenants_test.go new file mode 100644 index 0000000..382db2a --- /dev/null +++ b/server/api/router/admin_tenants_test.go @@ -0,0 +1,997 @@ +package router + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/go-webauthn/webauthn/protocol" + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/api/dto/admin/request" + "github.com/teamhanko/passkey-server/api/dto/admin/response" + "github.com/teamhanko/passkey-server/config" + "github.com/teamhanko/passkey-server/persistence/models" + "github.com/teamhanko/passkey-server/test/helper" + "net/http" + "net/http/httptest" + "time" +) + +func (s *adminSuite) TestAdminRouter_Tenants_Lists() { + s.SkipOnShort() + + tests := []struct { + Name string + + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + ExpectedTenantCount int + }{ + { + Name: "success", + ExpectedStatusCode: http.StatusOK, + ExpectedTenantCount: 2, + }, + { + Name: "broken db", + SimulateBrokenDB: true, + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadFixtures("../../test/fixtures/common") + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + req := httptest.NewRequest(http.MethodGet, "/tenants", nil) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + var tenants response.ListTenantResponses + err := json.Unmarshal(rec.Body.Bytes(), &tenants) + s.Require().NoError(err) + + s.Assert().Len(tenants, 4) + } + + }) + } +} + +func (s *adminSuite) TestAdminRouter_Tenants_Create() { + s.SkipOnShort() + + tests := []struct { + Name string + + RequestBody request.CreateTenantDto + + SimulateBrokenDB bool + OmitRequestBody bool + InvertExpectMessage bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + RequestBody: request.CreateTenantDto{ + DisplayName: "Test-Tenant", + Config: request.CreateConfigDto{ + Cors: request.CreateCorsDto{ + AllowedOrigins: []string{"http://localhost"}, + AllowUnsafeWildcard: helper.ToPointer(true), + }, + Passkey: request.CreatePasskeyConfigDto{ + RelyingParty: request.CreateRelyingPartyDto{ + Id: "localhost", + DisplayName: "Localhost", + Icon: nil, + Origins: []string{"http://localhost"}, + }, + Timeout: 6000, + UserVerification: helper.ToPointer(protocol.VerificationDiscouraged), + AttestationPreference: helper.ToPointer(protocol.PreferDirectAttestation), + ResidentKeyRequirement: helper.ToPointer(protocol.ResidentKeyRequirementDiscouraged), + }, + Mfa: &request.CreateMFAConfigDto{ + Timeout: 5000, + UserVerification: helper.ToPointer(protocol.VerificationRequired), + AttestationPreference: helper.ToPointer(protocol.PreferIndirectAttestation), + ResidentKeyRequirement: helper.ToPointer(protocol.ResidentKeyRequirementPreferred), + Attachment: helper.ToPointer(protocol.CrossPlatform), + }, + }, + CreateApiKey: true, + }, + + ExpectedStatusCode: http.StatusCreated, + ExpectedStatusMessage: `"name":"Initial API Key"`, + }, + { + Name: "success with missing mfa config", + RequestBody: request.CreateTenantDto{ + DisplayName: "Test-Tenant", + Config: request.CreateConfigDto{ + Cors: request.CreateCorsDto{ + AllowedOrigins: []string{"http://localhost"}, + AllowUnsafeWildcard: helper.ToPointer(true), + }, + Passkey: request.CreatePasskeyConfigDto{ + RelyingParty: request.CreateRelyingPartyDto{ + Id: "localhost", + DisplayName: "Localhost", + Icon: nil, + Origins: []string{"http://localhost"}, + }, + Timeout: 6000, + UserVerification: helper.ToPointer(protocol.VerificationDiscouraged), + AttestationPreference: helper.ToPointer(protocol.PreferDirectAttestation), + ResidentKeyRequirement: helper.ToPointer(protocol.ResidentKeyRequirementDiscouraged), + }, + }, + CreateApiKey: true, + }, + + ExpectedStatusCode: http.StatusCreated, + ExpectedStatusMessage: `"name":"Initial API Key"`, + }, + { + Name: "success with minimal config", + RequestBody: request.CreateTenantDto{ + DisplayName: "Test-Tenant", + Config: request.CreateConfigDto{ + Cors: request.CreateCorsDto{ + AllowedOrigins: []string{"http://localhost"}, + AllowUnsafeWildcard: helper.ToPointer(true), + }, + Passkey: request.CreatePasskeyConfigDto{ + RelyingParty: request.CreateRelyingPartyDto{ + Id: "localhost", + DisplayName: "Localhost", + Origins: []string{"http://localhost"}, + }, + Timeout: 6000, + }, + }, + CreateApiKey: true, + }, + + ExpectedStatusCode: http.StatusCreated, + ExpectedStatusMessage: `"name":"Initial API Key"`, + }, + { + Name: "success without api key", + RequestBody: request.CreateTenantDto{ + DisplayName: "Test-Tenant", + Config: request.CreateConfigDto{ + Cors: request.CreateCorsDto{ + AllowedOrigins: []string{"http://localhost"}, + AllowUnsafeWildcard: helper.ToPointer(true), + }, + Passkey: request.CreatePasskeyConfigDto{ + RelyingParty: request.CreateRelyingPartyDto{ + Id: "localhost", + DisplayName: "Localhost", + Origins: []string{"http://localhost"}, + }, + Timeout: 6000, + }, + }, + CreateApiKey: false, + }, + + InvertExpectMessage: true, + + ExpectedStatusCode: http.StatusCreated, + ExpectedStatusMessage: `"name":"Initial API Key"`, + }, + { + Name: "missing required attributes", + RequestBody: request.CreateTenantDto{ + DisplayName: "Test-Tenant", + Config: request.CreateConfigDto{ + Cors: request.CreateCorsDto{ + AllowedOrigins: []string{"http://localhost"}, + }, + Passkey: request.CreatePasskeyConfigDto{ + RelyingParty: request.CreateRelyingPartyDto{ + Id: "localhost", + DisplayName: "Localhost", + Origins: []string{"http://localhost"}, + }, + }, + }, + CreateApiKey: true, + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: `"details":"allow_unsafe_wildcard is a required field and timeout is a required field"`, + }, + { + Name: "missing request body", + OmitRequestBody: true, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: `"details":"display_name is a required field and allowed_origins is a required field and allow_unsafe_wildcard is a required field and id is a required field and display_name is a required field and origins is a required field and timeout is a required field"`, + }, + { + Name: "broken db", + RequestBody: request.CreateTenantDto{ + DisplayName: "Test-Tenant", + Config: request.CreateConfigDto{ + Cors: request.CreateCorsDto{ + AllowedOrigins: []string{"http://localhost"}, + AllowUnsafeWildcard: helper.ToPointer(true), + }, + Passkey: request.CreatePasskeyConfigDto{ + RelyingParty: request.CreateRelyingPartyDto{ + Id: "localhost", + DisplayName: "Localhost", + Icon: nil, + Origins: []string{"http://localhost"}, + }, + Timeout: 6000, + UserVerification: helper.ToPointer(protocol.VerificationDiscouraged), + AttestationPreference: helper.ToPointer(protocol.PreferDirectAttestation), + ResidentKeyRequirement: helper.ToPointer(protocol.ResidentKeyRequirementDiscouraged), + }, + Mfa: &request.CreateMFAConfigDto{ + Timeout: 5000, + UserVerification: helper.ToPointer(protocol.VerificationRequired), + AttestationPreference: helper.ToPointer(protocol.PreferIndirectAttestation), + ResidentKeyRequirement: helper.ToPointer(protocol.ResidentKeyRequirementPreferred), + Attachment: helper.ToPointer(protocol.CrossPlatform), + }, + }, + CreateApiKey: true, + }, + + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadFixtures("../../test/fixtures/common") + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + var req *http.Request + if !currentTest.OmitRequestBody { + body, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodPost, "/tenants", bytes.NewReader(body)) + } else { + req = httptest.NewRequest(http.MethodPost, "/tenants", nil) + } + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + + if currentTest.InvertExpectMessage { + s.Assert().NotContains(rec.Body.String(), currentTest.ExpectedStatusMessage) + } else { + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + } + + }) + } +} + +func (s *adminSuite) TestAdminRouter_Tenants_Get() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + ExpectedStatusCode: http.StatusOK, + }, + { + Name: "unknown tenant", + TenantID: "00000000-0000-0000-0000-000000000000", + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant id", + TenantID: "malformed", + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant must be valid uuid4", + }, + { + Name: "missing tenant id", + TenantID: "/", + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant must be valid uuid4", + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadFixtures("../../test/fixtures/common") + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/tenants/%s", currentTest.TenantID), nil) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err = s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + + if rec.Code == http.StatusOK { + var tenant response.GetTenantResponse + err = json.Unmarshal(rec.Body.Bytes(), &tenant) + s.Require().NoError(err) + + s.Assert().NotEmpty(tenant) + s.Assert().Equal(currentTest.TenantID, tenant.Id.String()) + s.Assert().Equal("Success Tenant", tenant.DisplayName) + s.Assert().NotEmpty(tenant.Config) + } + }) + } +} + +func (s *adminSuite) TestAdminRouter_Tenants_Update() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + DisplayName string + RequestBody interface{} + + OmitRequestBody bool + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + DisplayName: "Lorem", + RequestBody: request.UpdateTenantDto{DisplayName: "Lorem"}, + + ExpectedStatusCode: http.StatusNoContent, + }, + { + Name: "tenant not found", + TenantID: "00000000-0000-0000-0000-000000000000", + DisplayName: "Lorem", + RequestBody: request.UpdateTenantDto{DisplayName: "Lorem"}, + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant id", + TenantID: "malformed", + DisplayName: "Lorem", + RequestBody: request.UpdateTenantDto{DisplayName: "Lorem"}, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "missing request body", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + DisplayName: "Lorem", + + OmitRequestBody: true, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "display_name is a required field", + }, + { + Name: "malformed request body", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + DisplayName: "Lorem", + RequestBody: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "display_name is a required field", + }, + { + Name: "malformed display name", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + DisplayName: "Lorem", + RequestBody: request.UpdateTenantDto{DisplayName: ""}, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "display_name is a required field", + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + DisplayName: "Lorem", + RequestBody: request.UpdateTenantDto{DisplayName: "Lorem"}, + + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadFixtures("../../test/fixtures/common") + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPut, fmt.Sprintf("/tenants/%s", currentTest.TenantID), nil) + } else { + body, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodPut, fmt.Sprintf("/tenants/%s", currentTest.TenantID), bytes.NewReader(body)) + } + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err = s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + tenant, err := s.Storage.GetTenantPersister(nil).Get(uuid.FromStringOrNil(currentTest.TenantID)) + s.Require().NoError(err) + s.Assert().Equal(currentTest.DisplayName, tenant.DisplayName) + } + }) + } +} + +func (s *adminSuite) TestAdminRouter_Tenants_Remove() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + ExpectedStatusCode: http.StatusNoContent, + }, + { + Name: "tenant not found", + TenantID: "00000000-0000-0000-0000-000000000000", + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant id", + TenantID: "malformed", + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadFixtures("../../test/fixtures/common") + s.Require().NoError(err) + + beforeDelete, err := s.Storage.GetTenantPersister(nil).List() + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/tenants/%s", currentTest.TenantID), nil) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err = s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + afterDelete, err := s.Storage.GetTenantPersister(nil).List() + s.Require().NoError(err) + + s.Assert().Less(afterDelete, beforeDelete) + + tenant, err := s.Storage.GetTenantPersister(nil).Get(uuid.FromStringOrNil(currentTest.TenantID)) + s.Require().NoError(err) + + s.Assert().Empty(tenant) + } + }) + } +} + +func (s *adminSuite) TestAdminRouter_Tenants_UpdateConfig() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + RequestBody interface{} + + OmitRequestBody bool + SkipConfigCheck bool + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: request.UpdateConfigDto{ + CreateConfigDto: request.CreateConfigDto{ + Cors: request.CreateCorsDto{ + AllowedOrigins: []string{"http://localhost"}, + AllowUnsafeWildcard: helper.ToPointer(true), + }, + Passkey: request.CreatePasskeyConfigDto{ + RelyingParty: request.CreateRelyingPartyDto{ + Id: "localhost", + DisplayName: "Localhost", + Icon: helper.ToPointer("http://localhost/img.png"), + Origins: []string{"http://localhost"}, + }, + Timeout: 1234, + UserVerification: helper.ToPointer(protocol.VerificationDiscouraged), + Attachment: helper.ToPointer(protocol.Platform), + AttestationPreference: helper.ToPointer(protocol.PreferDirectAttestation), + ResidentKeyRequirement: helper.ToPointer(protocol.ResidentKeyRequirementDiscouraged), + }, + Mfa: helper.ToPointer(request.CreateMFAConfigDto{ + Timeout: 1234, + UserVerification: helper.ToPointer(protocol.VerificationPreferred), + Attachment: helper.ToPointer(protocol.Platform), + AttestationPreference: helper.ToPointer(protocol.PreferIndirectAttestation), + ResidentKeyRequirement: helper.ToPointer(protocol.ResidentKeyRequirementPreferred), + }), + }, + }, + + ExpectedStatusCode: http.StatusNoContent, + }, + { + Name: "success with minimal config", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: request.UpdateConfigDto{ + CreateConfigDto: request.CreateConfigDto{ + Cors: request.CreateCorsDto{ + AllowedOrigins: []string{"http://localhost"}, + AllowUnsafeWildcard: helper.ToPointer(true), + }, + Passkey: request.CreatePasskeyConfigDto{ + RelyingParty: request.CreateRelyingPartyDto{ + Id: "localhost", + DisplayName: "Localhost", + Origins: []string{"http://localhost"}, + }, + Timeout: 1234, + }, + }, + }, + + ExpectedStatusCode: http.StatusNoContent, + }, + { + Name: "tenant not found", + TenantID: "00000000-0000-0000-0000-000000000000", + RequestBody: request.UpdateConfigDto{ + CreateConfigDto: request.CreateConfigDto{ + Cors: request.CreateCorsDto{ + AllowedOrigins: []string{"http://localhost"}, + AllowUnsafeWildcard: helper.ToPointer(true), + }, + Passkey: request.CreatePasskeyConfigDto{ + RelyingParty: request.CreateRelyingPartyDto{ + Id: "localhost", + DisplayName: "Localhost", + Icon: helper.ToPointer("http://localhost/img.png"), + Origins: []string{"http://localhost"}, + }, + Timeout: 1234, + UserVerification: helper.ToPointer(protocol.VerificationDiscouraged), + Attachment: helper.ToPointer(protocol.Platform), + AttestationPreference: helper.ToPointer(protocol.PreferDirectAttestation), + ResidentKeyRequirement: helper.ToPointer(protocol.ResidentKeyRequirementDiscouraged), + }, + Mfa: helper.ToPointer(request.CreateMFAConfigDto{ + Timeout: 1234, + UserVerification: helper.ToPointer(protocol.VerificationPreferred), + Attachment: helper.ToPointer(protocol.Platform), + AttestationPreference: helper.ToPointer(protocol.PreferIndirectAttestation), + ResidentKeyRequirement: helper.ToPointer(protocol.ResidentKeyRequirementPreferred), + }), + }, + }, + + SkipConfigCheck: true, + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant", + TenantID: "malformed", + RequestBody: request.UpdateConfigDto{ + CreateConfigDto: request.CreateConfigDto{ + Cors: request.CreateCorsDto{ + AllowedOrigins: []string{"http://localhost"}, + AllowUnsafeWildcard: helper.ToPointer(true), + }, + Passkey: request.CreatePasskeyConfigDto{ + RelyingParty: request.CreateRelyingPartyDto{ + Id: "localhost", + DisplayName: "Localhost", + Icon: helper.ToPointer("http://localhost/img.png"), + Origins: []string{"http://localhost"}, + }, + Timeout: 1234, + UserVerification: helper.ToPointer(protocol.VerificationDiscouraged), + Attachment: helper.ToPointer(protocol.Platform), + AttestationPreference: helper.ToPointer(protocol.PreferDirectAttestation), + ResidentKeyRequirement: helper.ToPointer(protocol.ResidentKeyRequirementDiscouraged), + }, + Mfa: helper.ToPointer(request.CreateMFAConfigDto{ + Timeout: 1234, + UserVerification: helper.ToPointer(protocol.VerificationPreferred), + Attachment: helper.ToPointer(protocol.Platform), + AttestationPreference: helper.ToPointer(protocol.PreferIndirectAttestation), + ResidentKeyRequirement: helper.ToPointer(protocol.ResidentKeyRequirementPreferred), + }), + }, + }, + + SkipConfigCheck: true, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "malformed attribute", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: request.UpdateConfigDto{ + CreateConfigDto: request.CreateConfigDto{ + Cors: request.CreateCorsDto{ + AllowedOrigins: []string{"http://localhost"}, + AllowUnsafeWildcard: helper.ToPointer(true), + }, + Passkey: request.CreatePasskeyConfigDto{ + RelyingParty: request.CreateRelyingPartyDto{ + Id: "localhost", + DisplayName: "Localhost", + Icon: helper.ToPointer("lorem"), + Origins: []string{"http://localhost"}, + }, + Timeout: 1234, + }, + }, + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "icon must be a valid URL", + }, + { + Name: "malformed request body", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "allowed_origins is a required field and allow_unsafe_wildcard is a required field and id is a required field and display_name is a required field and origins is a required field and timeout is a required field", + }, + { + Name: "missing request body", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + OmitRequestBody: true, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "allowed_origins is a required field and allow_unsafe_wildcard is a required field and id is a required field and display_name is a required field and origins is a required field and timeout is a required field", + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: request.UpdateConfigDto{ + CreateConfigDto: request.CreateConfigDto{ + Cors: request.CreateCorsDto{ + AllowedOrigins: []string{"http://localhost"}, + AllowUnsafeWildcard: helper.ToPointer(true), + }, + Passkey: request.CreatePasskeyConfigDto{ + RelyingParty: request.CreateRelyingPartyDto{ + Id: "localhost", + DisplayName: "Localhost", + Origins: []string{"http://localhost"}, + }, + Timeout: 1234, + }, + }, + }, + + SkipConfigCheck: true, + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusNoContent, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadFixtures("../../test/fixtures/common") + s.Require().NoError(err) + + var oldConfig models.Config + if !currentTest.SkipConfigCheck { + oldTenant, err := s.Storage.GetTenantPersister(nil).Get(uuid.FromStringOrNil(currentTest.TenantID)) + s.Require().NoError(err) + + oldConfig = oldTenant.Config + + } + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPut, fmt.Sprintf("/tenants/%s/config", currentTest.TenantID), nil) + } else { + body, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodPut, fmt.Sprintf("/tenants/%s/config", currentTest.TenantID), bytes.NewReader(body)) + } + + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if !currentTest.SkipConfigCheck && rec.Code == http.StatusOK { + updatedTenant, err := s.Storage.GetTenantPersister(nil).Get(uuid.FromStringOrNil(currentTest.TenantID)) + s.Require().NoError(err) + + updatedConfig := updatedTenant.Config + + s.Assert().Equal(oldConfig.ID, updatedConfig.ID) + s.Assert().NotEqual(oldConfig.WebauthnConfig.Timeout, updatedConfig.WebauthnConfig.Timeout) + } + }) + } +} + +func (s *adminSuite) TestAdminRouter_Tenants_AuditLogs() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + RequestBody interface{} + + OmitRequestBody bool + SimulateBrokenDB bool + + ExpectedLogCount int + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: request.ListAuditLogDto{ + Page: 1, + PerPage: 2, + StartTime: helper.ToPointer(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), + EndTime: helper.ToPointer(time.Now()), + Types: []string{"webauthn_transaction_init_succeeded"}, + UserId: "test-passkey", + IP: "192.168.65.1", + SearchString: "192.168.", + }, + + ExpectedLogCount: 1, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "[", + }, + { + Name: "success with minimal request body", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: request.ListAuditLogDto{}, + + ExpectedLogCount: 2, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "[", + }, + { + Name: "success with no request body", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + OmitRequestBody: true, + + ExpectedLogCount: 2, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "[", + }, + { + Name: "unknown tenant", + TenantID: "00000000-0000-0000-0000-000000000000", + + OmitRequestBody: true, + + ExpectedLogCount: 0, + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant", + TenantID: "malformed", + + OmitRequestBody: true, + + ExpectedLogCount: 0, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + OmitRequestBody: true, + SimulateBrokenDB: true, + + ExpectedLogCount: 0, + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/admin_router", + }) + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/tenants/%s/audit_logs", currentTest.TenantID), nil) + } else { + body, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/tenants/%s/audit_logs", currentTest.TenantID), bytes.NewReader(body)) + } + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + var responseBody models.AuditLogs + err := json.Unmarshal(rec.Body.Bytes(), &responseBody) + s.Require().NoError(err) + + s.Assert().Len(responseBody, currentTest.ExpectedLogCount) + } + }) + } +} diff --git a/server/api/router/admin_test.go b/server/api/router/admin_test.go new file mode 100644 index 0000000..ca9f39c --- /dev/null +++ b/server/api/router/admin_test.go @@ -0,0 +1,169 @@ +package router + +import ( + "github.com/stretchr/testify/suite" + "github.com/teamhanko/passkey-server/config" + "github.com/teamhanko/passkey-server/test" + "net/http" + "net/http/httptest" + "testing" +) + +type adminSuite struct { + test.Suite +} + +func TestAdminRouteSuite(t *testing.T) { + t.Parallel() + suite.Run(t, new(adminSuite)) +} + +func (s *adminSuite) TestAdminRouter_New() { + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + s.Assert().NotEmpty(adminRouter) +} + +func (s *adminSuite) TestAdminRouter_Status() { + s.SkipOnShort() + + tests := []struct { + Name string + + SimulateBrokenDB bool + + ExpectedStatusCode int + }{ + { + Name: "success", + + ExpectedStatusCode: http.StatusOK, + }, + { + Name: "broken db", + + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusInternalServerError, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + }) + } +} + +func (s *adminSuite) TestAdminRouter_Alive() { + s.SkipOnShort() + + tests := []struct { + Name string + + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + ExpectedStatusContentType string + }{ + { + Name: "success", + + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "{\"alive\":true}\n", + ExpectedStatusContentType: "application/json; charset=UTF-8", + }, + { + Name: "broken db but still alive", + + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "{\"alive\":true}\n", + ExpectedStatusContentType: "application/json; charset=UTF-8", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + req := httptest.NewRequest(http.MethodGet, "/health/alive", nil) + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + s.Assert().Equal(currentTest.ExpectedStatusContentType, rec.Header().Get("Content-Type")) + }) + } +} + +func (s *adminSuite) TestAdminRouter_Ready() { + s.SkipOnShort() + + tests := []struct { + Name string + + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + ExpectedStatusContentType string + }{ + { + Name: "success", + + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "{\"ready\":true}\n", + ExpectedStatusContentType: "application/json; charset=UTF-8", + }, + { + Name: "broken db but still alive", + + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "{\"ready\":true}\n", + ExpectedStatusContentType: "application/json; charset=UTF-8", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + req := httptest.NewRequest(http.MethodGet, "/health/ready", nil) + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + s.Assert().Equal(currentTest.ExpectedStatusContentType, rec.Header().Get("Content-Type")) + }) + } +} diff --git a/server/api/router/admin_users_test.go b/server/api/router/admin_users_test.go new file mode 100644 index 0000000..f0b2a93 --- /dev/null +++ b/server/api/router/admin_users_test.go @@ -0,0 +1,388 @@ +package router + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/api/dto/admin/request" + "github.com/teamhanko/passkey-server/api/dto/admin/response" + publicResponse "github.com/teamhanko/passkey-server/api/dto/response" + "github.com/teamhanko/passkey-server/config" + "net/http" + "net/http/httptest" +) + +func (s *adminSuite) TestAdminRouter_Users_List() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + RequestBody interface{} + + OmitRequestBody bool + SimulateBrokenDB bool + + ExpectedCount int + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + RequestBody: request.UserListRequest{ + PerPage: 3, + Page: 1, + SortDirection: "asc", + }, + + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "[", + ExpectedCount: 3, + }, + { + Name: "success with reduced per page", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + RequestBody: request.UserListRequest{ + PerPage: 1, + Page: 1, + SortDirection: "asc", + }, + + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "[", + ExpectedCount: 1, + }, + { + Name: "success with minimal request", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + RequestBody: request.UserListRequest{}, + + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "[", + ExpectedCount: 3, + }, + { + Name: "success without request body", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + OmitRequestBody: true, + + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "[", + ExpectedCount: 3, + }, + { + Name: "success with malformed request body", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "lorem ipsum", + }, + + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: "[", + ExpectedCount: 3, + }, + { + Name: "unknown tenant", + TenantID: "00000000-0000-0000-0000-000000000000", + + OmitRequestBody: true, + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + ExpectedCount: 0, + }, + { + Name: "malformed tenant", + TenantID: "malformed", + + OmitRequestBody: true, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + ExpectedCount: 0, + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + + OmitRequestBody: true, + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + ExpectedCount: 0, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/admin_router", + }) + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/tenants/%s/users", currentTest.TenantID), nil) + } else { + body, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/tenants/%s/users", currentTest.TenantID), bytes.NewReader(body)) + } + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + var responseBody []response.UserListDto + err = json.Unmarshal(rec.Body.Bytes(), &responseBody) + s.Require().NoError(err) + + s.Assert().Len(responseBody, currentTest.ExpectedCount) + } + }) + } +} + +func (s *adminSuite) TestAdminRouter_Users_Get() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + UserID string + + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + ExpectedUser response.UserGetDto + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserID: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", + + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `"id":"b4fc06d2-2651-47e9-b1c3-3ba19ade9375"`, + ExpectedUser: response.UserGetDto{ + UserListDto: response.UserListDto{ + ID: uuid.FromStringOrNil("b4fc06d2-2651-47e9-b1c3-3ba19ade9375"), + UserID: "test-passkey", + Name: "passkey", + Icon: "", + DisplayName: "Test Passkey", + }, + Credentials: []publicResponse.CredentialDto{}, + Transactions: []publicResponse.TransactionDto{}, + }, + }, + { + Name: "unknown tenant", + TenantID: "00000000-0000-0000-0000-000000000000", + UserID: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant", + TenantID: "malformed", + UserID: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "unknown user", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserID: "00000000-0000-0000-0000-000000000000", + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "user not found", + }, + { + Name: "malformed user", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserID: "malformed", + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "user_id must be a valid uuid4", + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserID: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", + + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/admin_router", + }) + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/tenants/%s/users/%s", currentTest.TenantID, currentTest.UserID), nil) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + var responseBody response.UserGetDto + err = json.Unmarshal(rec.Body.Bytes(), &responseBody) + s.Require().NoError(err) + + s.Assert().Equal(currentTest.ExpectedUser, responseBody) + } + }) + } +} + +func (s *adminSuite) TestAdminRouter_Users_Remove() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantID string + UserID string + + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserID: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", + + ExpectedStatusCode: http.StatusNoContent, + ExpectedStatusMessage: "", + }, + { + Name: "unknown tenant", + TenantID: "00000000-0000-0000-0000-000000000000", + UserID: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant", + TenantID: "malformed", + UserID: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "unknown user", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserID: "00000000-0000-0000-0000-000000000000", + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "user not found", + }, + { + Name: "malformed user", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserID: "malformed", + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "user_id must be a valid uuid4", + }, + { + Name: "broken db", + TenantID: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserID: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", + + SimulateBrokenDB: true, + + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/admin_router", + }) + s.Require().NoError(err) + + oldCount, err := s.Storage.GetWebauthnUserPersister(nil).Count(uuid.FromStringOrNil(currentTest.TenantID)) + s.Require().NoError(err) + + adminRouter := NewAdminRouter(&config.Config{}, s.Storage, nil) + + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/tenants/%s/users/%s", currentTest.TenantID, currentTest.UserID), nil) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + adminRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusNoContent { + usr, err := s.Storage.GetWebauthnUserPersister(nil).GetById(uuid.FromStringOrNil(currentTest.UserID)) + s.Require().NoError(err) + + s.Assert().Empty(usr) + + newCount, err := s.Storage.GetWebauthnUserPersister(nil).Count(uuid.FromStringOrNil(currentTest.TenantID)) + s.Require().NoError(err) + + s.Assert().Greater(oldCount, newCount) + } + }) + } +} diff --git a/server/api/router/main_credentials_test.go b/server/api/router/main_credentials_test.go new file mode 100644 index 0000000..25a3f31 --- /dev/null +++ b/server/api/router/main_credentials_test.go @@ -0,0 +1,488 @@ +package router + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/api/dto/request" + "github.com/teamhanko/passkey-server/api/dto/response" + "github.com/teamhanko/passkey-server/config" + "net/http" + "net/http/httptest" +) + +func (s *mainRouterSuite) TestMainRouter_ListCredentials() { + s.SkipOnShort() + + tests := []struct { + Name string + + TenantId string + ApiKey string + RequestBody interface{} + + SkipApiKey bool + SkipBody bool + SimulateBrokenDb bool + + ExpectedStatusCode int + ExpectedErrorMessage string + }{ + { + Name: "success", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.ListCredentialsDto{UserId: "test-passkey"}, + ExpectedStatusCode: http.StatusOK, + ExpectedErrorMessage: "", + }, + { + Name: "missing api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: request.ListCredentialsDto{UserId: "test-passkey"}, + SkipApiKey: true, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedErrorMessage: "The api key is invalid", + }, + { + Name: "invalid api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg", + RequestBody: request.ListCredentialsDto{UserId: "test-passkey"}, + SkipApiKey: true, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedErrorMessage: "The api key is invalid", + }, + { + Name: "tenant not found", + TenantId: "00000000-0000-0000-0000-000000000000", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.ListCredentialsDto{UserId: "test-passkey"}, + ExpectedStatusCode: http.StatusNotFound, + ExpectedErrorMessage: "tenant not found", + }, + { + Name: "invalid tenant", + TenantId: "malformed", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.ListCredentialsDto{UserId: "test-passkey"}, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedErrorMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "user not found", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.ListCredentialsDto{UserId: "not_found"}, + ExpectedStatusCode: http.StatusNotFound, + ExpectedErrorMessage: "User not found", + }, + { + Name: "user not found in tenant", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.ListCredentialsDto{UserId: "not_found"}, + ExpectedStatusCode: http.StatusNotFound, + ExpectedErrorMessage: "User not found", + }, + { + Name: "malformed body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: struct { + Lorem string + }{Lorem: "Ipsum"}, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedErrorMessage: "UserId is a required field", + }, + { + Name: "missing body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + SkipBody: true, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedErrorMessage: "UserId is a required field", + }, + { + Name: "broken db", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.ListCredentialsDto{UserId: "test-passkey"}, + SimulateBrokenDb: true, + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedErrorMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/credentials", + }) + s.Require().NoError(err) + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) + + path := fmt.Sprintf("/%s/credentials", currentTest.TenantId) + var req *http.Request + + if !currentTest.SkipBody { + jsonBody, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodGet, path, bytes.NewReader(jsonBody)) + } else { + req = httptest.NewRequest(http.MethodGet, path, nil) + } + req.Header.Set("Content-Type", "application/json") + + if !currentTest.SkipApiKey { + req.Header.Set("apiKey", currentTest.ApiKey) + } + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDb { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + mainRouter.ServeHTTP(rec, req) + + if rec.Code == http.StatusOK { + var credentials response.CredentialDtoList + s.NoError(json.Unmarshal(rec.Body.Bytes(), &credentials)) + s.Equal(1, len(credentials)) + } else { + if s.Equal(currentTest.ExpectedStatusCode, rec.Code) { + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedErrorMessage) + } + } + }) + } +} + +func (s *mainRouterSuite) TestMainRouter_UpdateCredentials() { + s.SkipOnShort() + + tests := []struct { + Name string + + TenantId string + CredentialId string + ApiKey string + CredentialName string + RequestBody interface{} + + SkipApiKey bool + SkipBody bool + SimulateBrokenDb bool + + ExpectedStatusCode int + ExpectedErrorMessage string + }{ + { + Name: "success", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + CredentialName: "Ipsum", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: struct { + Name string `json:"name"` + }{ + Name: "Ipsum", + }, + ExpectedStatusCode: http.StatusNoContent, + ExpectedErrorMessage: "", + }, + { + Name: "missing api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + CredentialName: "Ipsum", + SkipApiKey: true, + RequestBody: struct { + Name string `json:"name"` + }{ + Name: "Ipsum", + }, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedErrorMessage: "The api key is invalid", + }, + { + Name: "invalid api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + CredentialName: "Ipsum", + ApiKey: "invalid", + RequestBody: struct { + Name string `json:"name"` + }{ + Name: "Ipsum", + }, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedErrorMessage: "The api key is invalid", + }, + { + Name: "tenant not found", + TenantId: "00000000-0000-0000-0000-000000000000", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + CredentialName: "Ipsum", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: struct { + Name string `json:"name"` + }{ + Name: "Ipsum", + }, + ExpectedStatusCode: http.StatusNotFound, + ExpectedErrorMessage: "tenant not found", + }, + { + Name: "malformed tenant id", + TenantId: "malformed", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + CredentialName: "Ipsum", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: struct { + Name string `json:"name"` + }{ + Name: "Ipsum", + }, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedErrorMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "credential not found", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + CredentialId: "4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E", + CredentialName: "Ipsum", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: struct { + Name string `json:"name"` + }{ + Name: "Ipsum", + }, + ExpectedStatusCode: http.StatusNotFound, + ExpectedErrorMessage: "credential with id '4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E' not found", + }, + { + Name: "invalid request body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + CredentialName: "Ipsum", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedErrorMessage: "name is a required field", + }, + { + Name: "missing request body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + CredentialName: "Ipsum", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + SkipBody: true, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedErrorMessage: "name is a required field", + }, + { + Name: "broken db", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + CredentialName: "Ipsum", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: struct { + Name string `json:"name"` + }{ + Name: "Ipsum", + }, + SimulateBrokenDb: true, + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedErrorMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/credentials", + }) + s.Require().NoError(err) + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) + + path := fmt.Sprintf("/%s/credentials/%s", currentTest.TenantId, currentTest.CredentialId) + var req *http.Request + + testCred, err := s.Storage.GetWebauthnCredentialPersister(nil).Get(currentTest.CredentialId, uuid.FromStringOrNil(currentTest.TenantId)) + s.Require().NoError(err) + + if !currentTest.SkipBody { + jsonBody, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodPatch, path, bytes.NewReader(jsonBody)) + } else { + req = httptest.NewRequest(http.MethodPatch, path, nil) + } + req.Header.Set("Content-Type", "application/json") + + if !currentTest.SkipApiKey { + req.Header.Set("apiKey", currentTest.ApiKey) + } + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDb { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + mainRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + if rec.Code == http.StatusNoContent { + updatedCred, err := s.Storage.GetWebauthnCredentialPersister(nil).Get(currentTest.CredentialId, uuid.FromStringOrNil(currentTest.TenantId)) + s.Require().NoError(err) + + s.Assert().NotEqual(*testCred.Name, *updatedCred.Name) + s.Assert().Equal(currentTest.CredentialName, *updatedCred.Name) + s.Assert().True(updatedCred.UpdatedAt.After(testCred.UpdatedAt)) + } else { + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedErrorMessage) + } + }) + } +} + +func (s *mainRouterSuite) TestMainRouter_DeleteCredentials() { + s.SkipOnShort() + + tests := []struct { + Name string + + TenantId string + CredentialId string + ApiKey string + + SkipApiKey bool + SimulateBrokenDb bool + + ExpectedStatusCode int + ExpectedErrorMessage string + }{ + { + Name: "success", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + ExpectedStatusCode: http.StatusNoContent, + ExpectedErrorMessage: "", + }, + { + Name: "missing api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + SkipApiKey: true, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedErrorMessage: "The api key is invalid", + }, + { + Name: "invalid api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + ApiKey: "invalid", + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedErrorMessage: "The api key is invalid", + }, + { + Name: "tenant not found", + TenantId: "00000000-0000-0000-0000-000000000000", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + ExpectedStatusCode: http.StatusNotFound, + ExpectedErrorMessage: "tenant not found", + }, + { + Name: "invalid tenant", + TenantId: "malformed", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + ExpectedStatusCode: http.StatusBadRequest, + ExpectedErrorMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "credential not found", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + CredentialId: "00000000-0000-0000-0000-000000000000", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + ExpectedStatusCode: http.StatusNotFound, + ExpectedErrorMessage: "credential with id '00000000-0000-0000-0000-000000000000' not found", + }, + { + Name: "existing credential for another tenant not found", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + CredentialId: "4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + ExpectedStatusCode: http.StatusNotFound, + ExpectedErrorMessage: "credential with id '4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E' not found", + }, + { + Name: "broken db", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + ExpectedStatusCode: http.StatusInternalServerError, + SimulateBrokenDb: true, + ExpectedErrorMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/credentials", + }) + s.Require().NoError(err) + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) + + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/%s/credentials/%s", currentTest.TenantId, currentTest.CredentialId), nil) + req.Header.Set("Content-Type", "application/json") + + if !currentTest.SkipApiKey { + req.Header.Set("apiKey", currentTest.ApiKey) + } + + rec := httptest.NewRecorder() + + if currentTest.SimulateBrokenDb { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + mainRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + if rec.Code == http.StatusNoContent { + updatedCred, err := s.Storage.GetWebauthnCredentialPersister(nil).Get(currentTest.CredentialId, uuid.FromStringOrNil(currentTest.TenantId)) + s.Require().NoError(err) + + s.Assert().Nil(updatedCred) + } else { + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedErrorMessage) + } + }) + } +} diff --git a/server/api/router/main_mfa_test.go b/server/api/router/main_mfa_test.go new file mode 100644 index 0000000..4de7719 --- /dev/null +++ b/server/api/router/main_mfa_test.go @@ -0,0 +1,709 @@ +package router + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "github.com/go-webauthn/webauthn/protocol" + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/api/dto/request" + "github.com/teamhanko/passkey-server/config" + "github.com/teamhanko/passkey-server/mapper" + "github.com/teamhanko/passkey-server/test/helper" + "net/http" + "net/http/httptest" + "strings" +) + +func (s *mainRouterSuite) TestMainRouter_Mfa_Registration_Init() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantId string + ApiKey string + UserId string + + RequestBody interface{} + + SkipApiKey bool + OmitRequestBody bool + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + UserId: "Lorem", + RequestBody: request.InitRegistrationDto{ + UserId: "Lorem", + Username: "Ipsum", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusOK, + }, + { + Name: "success without optional parameters", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + UserId: "Lorem", + RequestBody: request.InitRegistrationDto{ + UserId: "Lorem", + Username: "Ipsum", + }, + + ExpectedStatusCode: http.StatusOK, + }, + { + Name: "tenant not found", + TenantId: "00000000-0000-0000-0000-000000000000", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "Ipsum", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "wrong tenant", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "Ipsum", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "missing api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + SkipApiKey: true, + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "Ipsum", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "malformed api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "malformed", + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "Ipsum", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "display_name too long", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "Ipsum", + DisplayName: helper.ToPointer("LSiV5QsqY9Q9oiT03sIUC7imvSYM6UR9X6VtaYxfZyhyIGNrLoxolOunCoOxe8kOAAjEkMQadSezOBFpzdtIIpuceNkyHLc7cXCywx8JkRutyqpqotaUskXyGh78c5NZYcx"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "display_name cannot have more than 128 entries", + }, + { + Name: "username too long", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "LSiV5QsqY9Q9oiT03sIUC7imvSYM6UR9X6VtaYxfZyhyIGNrLoxolOunCoOxe8kOAAjEkMQadSezOBFpzdtIIpuceNkyHLc7cXCywx8JkRutyqpqotaUskXyGh78c5NZYcx", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "username cannot have more than 128 entries", + }, + { + Name: "icon must be URL", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "LSiV5QsqY9Q9oiT03sIUC7imvSYM6UR9X6VtaYxfZyhyIGNrLoxolOunCoOxe8kOAAjEkMQadSezOBFpzdtIIpuceNkyHLc7cXCywx8JkRutyqpqotaUskXyGh78c5NZYcx", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("no-url"), + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "icon must be a valid URL", + }, + { + Name: "malformed body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "user_id is a required field and username is a required field", + }, + { + Name: "missing body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + OmitRequestBody: true, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "user_id is a required field and username is a required field", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/registration", + }) + s.Require().NoError(err) + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/registration/initialize", currentTest.TenantId), nil) + } else { + body, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/registration/initialize", currentTest.TenantId), bytes.NewReader(body)) + } + + if !currentTest.SkipApiKey { + req.Header.Set("apiKey", currentTest.ApiKey) + } + + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + mainRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + + if rec.Code == http.StatusOK { + tenant, err := s.Storage.GetTenantPersister(nil).Get(uuid.FromStringOrNil(currentTest.TenantId)) + s.Require().NoError(err) + + creationOptions := protocol.CredentialCreation{} + err = json.Unmarshal(rec.Body.Bytes(), &creationOptions) + s.NoError(err) + + uId, err := base64.RawURLEncoding.DecodeString(creationOptions.Response.User.ID.(string)) + s.Require().NoError(err) + + s.NotEmpty(creationOptions.Response.Challenge) + s.Equal([]byte(currentTest.UserId), uId) + s.Equal(tenant.Config.WebauthnConfig.RelyingParty.RPId, creationOptions.Response.RelyingParty.ID) + s.Equal(protocol.ResidentKeyRequirementDiscouraged, creationOptions.Response.AuthenticatorSelection.ResidentKey) + s.Equal(protocol.VerificationDiscouraged, creationOptions.Response.AuthenticatorSelection.UserVerification) + s.False(*creationOptions.Response.AuthenticatorSelection.RequireResidentKey) + } else { + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + } + }) + } + +} + +func (s *mainRouterSuite) TestMainRouter_Mfa_Registration_Finalize() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantId string + UserId string + ApiKey string + CredName string + RequestBody string + + SkipApiKey bool + OmitRequestBody bool + SimulateBrokenDB bool + UseAAGUIDMapping bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success without mapping", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + UserId: "test-mfa", + CredName: "cred-7ylGZMEWkocCJH6zzRx4BW12xu_BjXx2U9cvRTTVsJI", + RequestBody: `{"type":"public-key","id":"7ylGZMEWkocCJH6zzRx4BW12xu_BjXx2U9cvRTTVsJI","rawId":"7ylGZMEWkocCJH6zzRx4BW12xu_BjXx2U9cvRTTVsJI","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiaW1FTVp3clNoc3REYm8tVXVWU0xXSkVzX2gyYWhoM3UtTDg2T1hIeXByRSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgOUg2Oxj9_nr0Ecd1QRxNpvyH7X9bqVzCLfMmjADX7ccCIQDdKCLK5pPV8_dtGni9vBaOwo37NtZq1YKyCg5ElHeWUGN4NWOBWQHZMIIB1TCCAXqgAwIBAgIBATAKBggqhkjOPQQDAjBgMQswCQYDVQQGEwJVUzERMA8GA1UECgwIQ2hyb21pdW0xIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xGjAYBgNVBAMMEUJhdGNoIENlcnRpZmljYXRlMB4XDTE3MDcxNDAyNDAwMFoXDTQ0MDUzMDExNDYyMVowYDELMAkGA1UEBhMCVVMxETAPBgNVBAoMCENocm9taXVtMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMRowGAYDVQQDDBFCYXRjaCBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI1hfmXJUI5kvMVnOsgqZ5naPBRGaCwljEY__99Y39L6Pmw3i1PXlcSk3_tBme3Xhi8jq68CA7S4kRugVpmU4QGjJTAjMAwGA1UdEwEB_wQCMAAwEwYLKwYBBAGC5RwCAQEEBAMCBSAwCgYIKoZIzj0EAwIDSQAwRgIhAMyq3nMojMBhy72bTW_GsKcpTmCnASaUU-XSJFzmPpiKAiEAv2MDR9dsfZWYfkk3MQwRyqIgDKUFr_NbDOcKkOmIv1poYXV0aERhdGFYpEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjRQAAAAEBAgMEBQYHCAECAwQFBgcIACDvKUZkwRaShwIkfrPNHHgFbXbG78GNfHZT1y9FNNWwkqUBAgMmIAEhWCC2T8LO1U5gt6dWYinNf0X_imOxyPtSIIqq3-tH0IUB8iJYIC2o7XHv3zDWr2vykXxZWMPybcnXWcAS5oWHDkZmSwqN","transports":["usb"]},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `"token"`, + }, + { + Name: "success with mapping", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + UserId: "test-mfa", + CredName: "Test Provider", + RequestBody: `{"type":"public-key","id":"7ylGZMEWkocCJH6zzRx4BW12xu_BjXx2U9cvRTTVsJI","rawId":"7ylGZMEWkocCJH6zzRx4BW12xu_BjXx2U9cvRTTVsJI","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiaW1FTVp3clNoc3REYm8tVXVWU0xXSkVzX2gyYWhoM3UtTDg2T1hIeXByRSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgOUg2Oxj9_nr0Ecd1QRxNpvyH7X9bqVzCLfMmjADX7ccCIQDdKCLK5pPV8_dtGni9vBaOwo37NtZq1YKyCg5ElHeWUGN4NWOBWQHZMIIB1TCCAXqgAwIBAgIBATAKBggqhkjOPQQDAjBgMQswCQYDVQQGEwJVUzERMA8GA1UECgwIQ2hyb21pdW0xIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xGjAYBgNVBAMMEUJhdGNoIENlcnRpZmljYXRlMB4XDTE3MDcxNDAyNDAwMFoXDTQ0MDUzMDExNDYyMVowYDELMAkGA1UEBhMCVVMxETAPBgNVBAoMCENocm9taXVtMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMRowGAYDVQQDDBFCYXRjaCBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI1hfmXJUI5kvMVnOsgqZ5naPBRGaCwljEY__99Y39L6Pmw3i1PXlcSk3_tBme3Xhi8jq68CA7S4kRugVpmU4QGjJTAjMAwGA1UdEwEB_wQCMAAwEwYLKwYBBAGC5RwCAQEEBAMCBSAwCgYIKoZIzj0EAwIDSQAwRgIhAMyq3nMojMBhy72bTW_GsKcpTmCnASaUU-XSJFzmPpiKAiEAv2MDR9dsfZWYfkk3MQwRyqIgDKUFr_NbDOcKkOmIv1poYXV0aERhdGFYpEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjRQAAAAEBAgMEBQYHCAECAwQFBgcIACDvKUZkwRaShwIkfrPNHHgFbXbG78GNfHZT1y9FNNWwkqUBAgMmIAEhWCC2T8LO1U5gt6dWYinNf0X_imOxyPtSIIqq3-tH0IUB8iJYIC2o7XHv3zDWr2vykXxZWMPybcnXWcAS5oWHDkZmSwqN","transports":["usb"]},"clientExtensionResults":{}}`, + UseAAGUIDMapping: true, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `"token"`, + }, + { + Name: "malformed api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "malformed", + UserId: "Lorem", + CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", + RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: `"The api key is invalid"`, + }, + { + Name: "wrong api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "bXta7uDpBt6nBn4j2sX7vm1KGqp7ma9GXPx835IiEdnJxWu_HA-rCdmdTk7I9oYQnYo4MebHkQ3khSosXJ6y5A==", + UserId: "Lorem", + CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", + RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: `"The api key is invalid"`, + }, + { + Name: "missing api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + SkipApiKey: true, + UserId: "Lorem", + CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", + RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: `"The api key is invalid"`, + }, + { + Name: "malformed tenant", + TenantId: "malformed", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + UserId: "Lorem", + CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", + RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: `"tenant_id must be a valid uuid4"`, + }, + { + Name: "unknown tenant", + TenantId: "00000000-0000-0000-0000-000000000000", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + UserId: "Lorem", + CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", + RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: `"tenant not found"`, + }, + { + Name: "expired registration request", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"_z-CcHsvOffFBgOFVStr4rvRg9UWmdXIS6ay8Gtu97g","rawId":"_z-CcHsvOffFBgOFVStr4rvRg9UWmdXIS6ay8Gtu97g","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiRDdScXRaTVhONDhsazVTN2haUGFiUVJrNTZudzNkeWFSNWhxaDRTbDQ2VSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEYwRAIgYtyvs2Wd9_dH522_Qjynemk6bNZYZCeV8-SmEf8cCD8CIHpKIuP6PyfDG0EyhYzjXZVRJpzS9UcKMGzt0HhinW1HY3g1Y4FZAdgwggHUMIIBeqADAgECAgEBMAoGCCqGSM49BAMCMGAxCzAJBgNVBAYTAlVTMREwDwYDVQQKDAhDaHJvbWl1bTEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEaMBgGA1UEAwwRQmF0Y2ggQ2VydGlmaWNhdGUwHhcNMTcwNzE0MDI0MDAwWhcNNDQwNTMwMTIxNTU1WjBgMQswCQYDVQQGEwJVUzERMA8GA1UECgwIQ2hyb21pdW0xIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xGjAYBgNVBAMMEUJhdGNoIENlcnRpZmljYXRlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjWF-ZclQjmS8xWc6yCpnmdo8FEZoLCWMRj__31jf0vo-bDeLU9eVxKTf-0GZ7deGLyOrrwIDtLiRG6BWmZThAaMlMCMwDAYDVR0TAQH_BAIwADATBgsrBgEEAYLlHAIBAQQEAwIFIDAKBggqhkjOPQQDAgNIADBFAiAdS_eryqvNTJUZefLA7ROApbRRoFWhTMMdeyqOuaVEwQIhANxdTGF0hhmKp-1qJtOx4bJ_jqTY3-IgYAecVcjH1DVLaGF1dGhEYXRhWKRJlg3liA6MaHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY0UAAAABAQIDBAUGBwgBAgMEBQYHCAAg_z-CcHsvOffFBgOFVStr4rvRg9UWmdXIS6ay8Gtu97ilAQIDJiABIVggsKAEIjWe5HmX75vyyL_rZO01oeypMur48b11Un0NIvciWCCLJ4hDtnq1kD7IFdWBqwA4wj0L3oQ2Eq1x6TNPwUJ_Dg","transports":["usb"]},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "Session has Expired", + }, + { + Name: "session challenge mismatch", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", + ApiKey: "bXta7uDpBt6nBn4j2sX7vm1KGqp7ma9GXPx835IiEdnJxWu_HA-rCdmdTk7I9oYQnYo4MebHkQ3khSosXJ6y5A==", + RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: `"received challenge does not match with any stored one"`, + }, + { + Name: "malformed request body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", + ApiKey: "bXta7uDpBt6nBn4j2sX7vm1KGqp7ma9GXPx835IiEdnJxWu_HA-rCdmdTk7I9oYQnYo4MebHkQ3khSosXJ6y5A==", + RequestBody: `{ "Lorem": "Ipsum" }`, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: `"unable to parse credential creation response"`, + }, + { + Name: "missing request body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", + ApiKey: "bXta7uDpBt6nBn4j2sX7vm1KGqp7ma9GXPx835IiEdnJxWu_HA-rCdmdTk7I9oYQnYo4MebHkQ3khSosXJ6y5A==", + OmitRequestBody: true, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: `"unable to parse credential creation response"`, + }, + { + Name: "broken db", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, + SimulateBrokenDB: true, + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: `"Internal Server Error"`, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/registration", + }) + s.Require().NoError(err) + + var metadata mapper.AuthenticatorMetadata + if currentTest.UseAAGUIDMapping { + filePath := helper.ToPointer("") + + metadata = mapper.LoadAuthenticatorMetadata(filePath) + + // add mfa test aaguid for mapping + metadata["01020304-0506-0708-0102-030405060708"] = mapper.Authenticator{ + Name: "Test Provider", + IconLight: "", + IconDark: "", + } + } + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, metadata) + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/registration/finalize", currentTest.TenantId), nil) + } else { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/registration/finalize", currentTest.TenantId), strings.NewReader(currentTest.RequestBody)) + } + + if !currentTest.SkipApiKey { + req.Header.Set("apiKey", currentTest.ApiKey) + } + + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + mainRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + creds, err := s.Storage.GetWebauthnCredentialPersister(nil).GetFromUser(currentTest.UserId, uuid.FromStringOrNil(currentTest.TenantId)) + s.Require().NoError(err) + + s.Assert().Len(creds, 1) + s.Assert().Equal(currentTest.CredName, *creds[0].Name) + } + }) + } +} + +func (s *mainRouterSuite) TestMainRouter_Mfa_Login_Init() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantId string + UserId string + ApiKey string + RequestBody interface{} + + SkipApiKey bool + SimulateBrokenDB bool + OmitRequestBody bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitMfaLoginDto{UserId: "test-passkey"}, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `"publicKey":{"challenge":`, + }, + { + Name: "malformed request body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "malformed", + }, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "user_id is a required field", + }, + { + Name: "missing request body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + OmitRequestBody: true, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "user_id is a required field", + }, + { + Name: "missing api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + RequestBody: request.InitMfaLoginDto{UserId: "test-passkey"}, + SkipApiKey: true, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "unknown user", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitMfaLoginDto{UserId: "4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E"}, + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "user not found", + }, + { + Name: "malformed tenant", + TenantId: "malformed", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitMfaLoginDto{UserId: "test-passkey"}, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "missing tenant", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitMfaLoginDto{UserId: "test-passkey"}, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "broken db", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitMfaLoginDto{UserId: "test-passkey"}, + SimulateBrokenDB: true, + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/login", + }) + s.Require().NoError(err) + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/login/initialize", currentTest.TenantId), nil) + } else { + body, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/login/initialize", currentTest.TenantId), bytes.NewReader(body)) + } + + if !currentTest.SkipApiKey { + req.Header.Set("apiKey", currentTest.ApiKey) + } + + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + mainRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + var ca protocol.CredentialAssertion + err = json.Unmarshal(rec.Body.Bytes(), &ca) + s.Require().NoError(err) + + sessionData, err := s.Storage.GetWebauthnSessionDataPersister(nil).GetByChallenge(ca.Response.Challenge.String(), uuid.FromStringOrNil(currentTest.TenantId)) + s.Require().NoError(err) + s.Assert().False(sessionData.IsDiscoverable) + } + }) + } +} + +func (s *mainRouterSuite) TestMainRouter_MFA_Login_Finish() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantId string + UserId string + ApiKey string + RequestBody string + + SkipApiKey bool + SimulateBrokenDB bool + OmitRequestBody bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-mfa", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","rawId":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQVNFZjc3X2RaYUNfM1lJSzZ0bWN6ME9NZXZDMWR3bzVNdXo1VWZfd0pNQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAg","signature":"MEUCIF7rXAv3VxIalxvMp7z55dBU5qr16hd6A_PJTjuhJ6jhAiEAnsUYUYrNcTpAT98nHmjVjyn3sH9vKJUUl2Y3bJazE1w","userHandle":"bWZhLXRlc3Q"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `{"token":"`, + }, + { + Name: "wrong tenant", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", + UserId: "test-mfa", + ApiKey: "bXta7uDpBt6nBn4j2sX7vm1KGqp7ma9GXPx835IiEdnJxWu_HA-rCdmdTk7I9oYQnYo4MebHkQ3khSosXJ6y5A==", + RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "received challenge does not match with any stored one", + }, + { + Name: "tenant not found", + TenantId: "00000000-0000-0000-0000-000000000000", + UserId: "test-mfa", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant", + TenantId: "malformed", + UserId: "test-mfa", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "missing api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-mfa", + RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, + SkipApiKey: true, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "malformed api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-mfa", + ApiKey: "malformed", + RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "malformed user handle should be ignored", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-mfa", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","rawId":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQVNFZjc3X2RaYUNfM1lJSzZ0bWN6ME9NZXZDMWR3bzVNdXo1VWZfd0pNQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAg","signature":"MEUCIF7rXAv3VxIalxvMp7z55dBU5qr16hd6A_PJTjuhJ6jhAiEAnsUYUYrNcTpAT98nHmjVjyn3sH9vKJUUl2Y3bJazE1w","userHandle":"bWZhLXRlc3QtMgs"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `{"token":"`, + }, + { + Name: "wrong credential", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-mfa", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"TaCJSxWWK3UI7UovgNJHfWXDPQMm0lo6shGO6iaLGNo","rawId":"TaCJSxWWK3UI7UovgNJHfWXDPQMm0lo6shGO6iaLGNo","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoidEZzbGxaNTZGdUJheUxqeTJmYy1NM0Q3MGt4QU9zY3RVenVwdjk0MldrSSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAg","signature":"MEYCIQCRYwDVmkcrpgH7idU35qiJH9XFs5zi9PFQcunCYe7YvwIhAK3nsXNGAyf2G1kBMJGiOaqQbyiSTDlD2itE0euh_jnJ","userHandle":"bWZhLXRlc3Q"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "Unable to find the credential for the returned credential ID", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/login", + }) + s.Require().NoError(err) + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/login/finalize", currentTest.TenantId), nil) + } else { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/login/finalize", currentTest.TenantId), strings.NewReader(currentTest.RequestBody)) + } + + if !currentTest.SkipApiKey { + req.Header.Set("apiKey", currentTest.ApiKey) + } + + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + mainRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + }) + } +} diff --git a/server/api/router/main_passkey_test.go b/server/api/router/main_passkey_test.go new file mode 100644 index 0000000..49b145e --- /dev/null +++ b/server/api/router/main_passkey_test.go @@ -0,0 +1,696 @@ +package router + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "github.com/go-webauthn/webauthn/protocol" + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/api/dto/request" + "github.com/teamhanko/passkey-server/config" + "github.com/teamhanko/passkey-server/mapper" + "github.com/teamhanko/passkey-server/test/helper" + "net/http" + "net/http/httptest" + "strings" +) + +func (s *mainRouterSuite) TestMainRouter_Registration_Init() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantId string + ApiKey string + UserId string + + RequestBody interface{} + + SkipApiKey bool + OmitRequestBody bool + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + UserId: "Lorem", + RequestBody: request.InitRegistrationDto{ + UserId: "Lorem", + Username: "Ipsum", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusOK, + }, + { + Name: "success without optional parameters", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + UserId: "Lorem", + RequestBody: request.InitRegistrationDto{ + UserId: "Lorem", + Username: "Ipsum", + }, + + ExpectedStatusCode: http.StatusOK, + }, + { + Name: "tenant not found", + TenantId: "00000000-0000-0000-0000-000000000000", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "Ipsum", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "wrong tenant", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "Ipsum", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "missing api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + SkipApiKey: true, + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "Ipsum", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "malformed api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "malformed", + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "Ipsum", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "display_name too long", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "Ipsum", + DisplayName: helper.ToPointer("LSiV5QsqY9Q9oiT03sIUC7imvSYM6UR9X6VtaYxfZyhyIGNrLoxolOunCoOxe8kOAAjEkMQadSezOBFpzdtIIpuceNkyHLc7cXCywx8JkRutyqpqotaUskXyGh78c5NZYcx"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "display_name cannot have more than 128 entries", + }, + { + Name: "username too long", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "LSiV5QsqY9Q9oiT03sIUC7imvSYM6UR9X6VtaYxfZyhyIGNrLoxolOunCoOxe8kOAAjEkMQadSezOBFpzdtIIpuceNkyHLc7cXCywx8JkRutyqpqotaUskXyGh78c5NZYcx", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("http://localhost/my/icon.png"), + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "username cannot have more than 128 entries", + }, + { + Name: "icon must be URL", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitRegistrationDto{ + UserId: "lorem", + Username: "LSiV5QsqY9Q9oiT03sIUC7imvSYM6UR9X6VtaYxfZyhyIGNrLoxolOunCoOxe8kOAAjEkMQadSezOBFpzdtIIpuceNkyHLc7cXCywx8JkRutyqpqotaUskXyGh78c5NZYcx", + DisplayName: helper.ToPointer("Lorem Ipsum"), + Icon: helper.ToPointer("no-url"), + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "icon must be a valid URL", + }, + { + Name: "malformed body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "user_id is a required field and username is a required field", + }, + { + Name: "missing body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + OmitRequestBody: true, + + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "user_id is a required field and username is a required field", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/registration", + }) + s.Require().NoError(err) + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/registration/initialize", currentTest.TenantId), nil) + } else { + body, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/registration/initialize", currentTest.TenantId), bytes.NewReader(body)) + } + + if !currentTest.SkipApiKey { + req.Header.Set("apiKey", currentTest.ApiKey) + } + + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + mainRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + + if rec.Code == http.StatusOK { + tenant, err := s.Storage.GetTenantPersister(nil).Get(uuid.FromStringOrNil(currentTest.TenantId)) + s.Require().NoError(err) + + creationOptions := protocol.CredentialCreation{} + err = json.Unmarshal(rec.Body.Bytes(), &creationOptions) + s.NoError(err) + + uId, err := base64.RawURLEncoding.DecodeString(creationOptions.Response.User.ID.(string)) + s.Require().NoError(err) + + s.NotEmpty(creationOptions.Response.Challenge) + s.Equal([]byte(currentTest.UserId), uId) + s.Equal(tenant.Config.WebauthnConfig.RelyingParty.RPId, creationOptions.Response.RelyingParty.ID) + s.Equal(protocol.ResidentKeyRequirementRequired, creationOptions.Response.AuthenticatorSelection.ResidentKey) + s.Equal(protocol.VerificationPreferred, creationOptions.Response.AuthenticatorSelection.UserVerification) + s.True(*creationOptions.Response.AuthenticatorSelection.RequireResidentKey) + } else { + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + } + }) + } + +} + +func (s *mainRouterSuite) TestMainRouter_Registration_Finalize() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantId string + UserId string + CredName string + RequestBody string + + OmitRequestBody bool + SimulateBrokenDB bool + UseAAGUIDMapping bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success without mapping", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + CredName: "cred-Nx1ClZkkM2ahHzFrx75xRSHaVmJVeHfYJG96ebzU_w8", + RequestBody: `{"type":"public-key","id":"Nx1ClZkkM2ahHzFrx75xRSHaVmJVeHfYJG96ebzU_w8","rawId":"Nx1ClZkkM2ahHzFrx75xRSHaVmJVeHfYJG96ebzU_w8","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiOVVoMlByWUh5ejBBQVAxUTdlT24xS0oxc2QtR1A5amRzSi0wTWZSSmNFRSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgWMkrzdLrZuwXfvfVg6uRtDRqIRkUTODEIKiLm-PWc-ICIQDPwr0JXMJzumKMvywSgUylC4oYS6Wn3Bkq5onnv8scqGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIDcdQpWZJDNmoR8xa8e-cUUh2lZiVXh32CRvenm81P8PpQECAyYgASFYIHrhErnErli1kll2h-IZRYOuJjUb-quJ9axUJ2OhPw8UIlgglE2cEofNgKw5Nvx6dlVokaXQLQ_CxthLHIJ-IDWaxDg","transports":["internal"]},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `"token"`, + }, + { + Name: "success with mapping", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + CredName: "Chrome on Mac", + RequestBody: `{"type":"public-key","id":"Nx1ClZkkM2ahHzFrx75xRSHaVmJVeHfYJG96ebzU_w8","rawId":"Nx1ClZkkM2ahHzFrx75xRSHaVmJVeHfYJG96ebzU_w8","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiOVVoMlByWUh5ejBBQVAxUTdlT24xS0oxc2QtR1A5amRzSi0wTWZSSmNFRSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgWMkrzdLrZuwXfvfVg6uRtDRqIRkUTODEIKiLm-PWc-ICIQDPwr0JXMJzumKMvywSgUylC4oYS6Wn3Bkq5onnv8scqGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIDcdQpWZJDNmoR8xa8e-cUUh2lZiVXh32CRvenm81P8PpQECAyYgASFYIHrhErnErli1kll2h-IZRYOuJjUb-quJ9axUJ2OhPw8UIlgglE2cEofNgKw5Nvx6dlVokaXQLQ_CxthLHIJ-IDWaxDg","transports":["internal"]},"clientExtensionResults":{}}`, + UseAAGUIDMapping: true, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `"token"`, + }, + { + Name: "malformed tenant", + TenantId: "malformed", + UserId: "Lorem", + CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", + RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: `"tenant_id must be a valid uuid4"`, + }, + { + Name: "unknown tenant", + TenantId: "00000000-0000-0000-0000-000000000000", + UserId: "Lorem", + CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", + RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: `"tenant not found"`, + }, + { + Name: "expired registration request", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: `{"type":"public-key","id":"Jqujwmwsy7WxLZ6czY3DVP4COna_n2jMQa9_xVQSzfM","rawId":"Jqujwmwsy7WxLZ6czY3DVP4COna_n2jMQa9_xVQSzfM","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiM1dTUGNIQVFhXzdRdHR2M0pQZEVHRFhfcG1ISnRKNGZRNl91RERDMFFkYyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEYwRAIgO3FYNOi5bGuC38yswBmnY7ktTRYNTQg0IMzQQ1hRhmsCIGxct34SgyWtpfIUJA5tT9Sr9TU5H51b5WHitxVuPNOdaGF1dGhEYXRhWKRJlg3liA6MaHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY0UAAAAArc4AAjW8xgpkiwsl8fBVAwAgJqujwmwsy7WxLZ6czY3DVP4COna_n2jMQa9_xVQSzfOlAQIDJiABIVggt-w6lzMq9rD8Zzd8ec-E9t5shm0-QUOvDVqLSF46JE4iWCBK8XoTbSF50pI-YZ1rzCdtpTsmp4TE_RwhEQu0M19yxw","transports":["internal"]},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "Session has Expired", + }, + { + Name: "session challenge mismatch", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", + RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: `"received challenge does not match with any stored one"`, + }, + { + Name: "malformed request body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", + RequestBody: `{ "Lorem": "Ipsum" }`, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: `"unable to parse credential creation response"`, + }, + { + Name: "missing request body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", + OmitRequestBody: true, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: `"unable to parse credential creation response"`, + }, + { + Name: "broken db", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, + SimulateBrokenDB: true, + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: `"Internal Server Error"`, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/registration", + }) + s.Require().NoError(err) + + var filePath *string + if !currentTest.UseAAGUIDMapping { + filePath = helper.ToPointer(".") + } + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, mapper.LoadAuthenticatorMetadata(filePath)) + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/registration/finalize", currentTest.TenantId), nil) + } else { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/registration/finalize", currentTest.TenantId), strings.NewReader(currentTest.RequestBody)) + } + + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + mainRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + creds, err := s.Storage.GetWebauthnCredentialPersister(nil).GetFromUser(currentTest.UserId, uuid.FromStringOrNil(currentTest.TenantId)) + s.Require().NoError(err) + + s.Assert().Len(creds, 1) + s.Assert().Equal(currentTest.CredName, *creds[0].Name) + } + }) + } +} + +func (s *mainRouterSuite) TestMainRouter_Login_Init() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantId string + UserId string + ApiKey string + RequestBody interface{} + IsDiscoverLogin bool + + SkipApiKey bool + SimulateBrokenDB bool + OmitRequestBody bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success with discovery", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + OmitRequestBody: true, + SkipApiKey: true, + IsDiscoverLogin: true, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `"publicKey":{"challenge":`, + }, + { + Name: "success without discovery", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitLoginDto{UserId: helper.ToPointer("test-passkey")}, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `"publicKey":{"challenge":`, + }, + { + Name: "perform discovery on malformed request body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + IsDiscoverLogin: true, + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "malformed", + }, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `"publicKey":{"challenge":`, + }, + { + Name: "perform discovery on missing request body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + OmitRequestBody: true, + IsDiscoverLogin: true, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `"publicKey":{"challenge":`, + }, + { + Name: "missing api key on non discovery", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + RequestBody: request.InitLoginDto{UserId: helper.ToPointer("test-passkey")}, + SkipApiKey: true, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "api key is missing", + }, + { + Name: "malformed api key on non discovery", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "malformed", + RequestBody: request.InitLoginDto{UserId: helper.ToPointer("test-passkey")}, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "unknown user", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitLoginDto{UserId: helper.ToPointer("not_found")}, + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "user not found", + }, + { + Name: "malformed tenant", + TenantId: "malformed", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitLoginDto{UserId: helper.ToPointer("test-passkey")}, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "missing tenant", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitLoginDto{UserId: helper.ToPointer("test-passkey")}, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "broken db", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitLoginDto{UserId: helper.ToPointer("a1B2c3D3")}, + SimulateBrokenDB: true, + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/login", + }) + s.Require().NoError(err) + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/login/initialize", currentTest.TenantId), nil) + } else { + body, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/login/initialize", currentTest.TenantId), bytes.NewReader(body)) + } + + if !currentTest.SkipApiKey { + req.Header.Set("apiKey", currentTest.ApiKey) + } + + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + mainRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + var ca protocol.CredentialAssertion + err = json.Unmarshal(rec.Body.Bytes(), &ca) + s.Require().NoError(err) + + sessionData, err := s.Storage.GetWebauthnSessionDataPersister(nil).GetByChallenge(ca.Response.Challenge.String(), uuid.FromStringOrNil(currentTest.TenantId)) + s.Require().NoError(err) + s.Assert().Equal(currentTest.IsDiscoverLogin, sessionData.IsDiscoverable) + } + }) + } +} + +func (s *mainRouterSuite) TestMainRouter_Login_Finish() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantId string + UserId string + ApiKey string + RequestBody string + + SkipApiKey bool + SimulateBrokenDB bool + OmitRequestBody bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success without discovery", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiMEw3bTUyTmVEM0xpMXFteVhUMVp4Mk5nQ1lnNEstQTNiamQ4TzA5dmxVMCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIQCzxd6TRox_Dx4lOPrLYpfj4umnx4aNPZ1Tg7QA4OZtWwIgMSPL_oMUENDy1I5rGSmUjNs73eDtOVTq_D6wNs4qeDE","userHandle":"dGVzdC1wYXNza2V5"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `{"token":"`, + }, + { + Name: "success with discovery", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey-discover", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","rawId":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVWljUDNmb0dQZmFoZF9kdVlHbnVCeVNtVldhUGw5VkNOSzdCNDJpM2Z4ayIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIF-LTHRYEHX4r9pWj39-o2NglZV8U3wGvOkmT-KH2ecjAiEA_1dYDRgiJsx7_1i9kIX8YWqzUlOzzHlz9HJgj0dGPmQ","userHandle":"dGVzdC1wYXNza2V5LWRpc2NvdmVy"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `{"token":"`, + }, + { + Name: "wrong tenant", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", + UserId: "a1B2c3D4", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "received challenge does not match with any stored one", + }, + { + Name: "tenant not found", + TenantId: "00000000-0000-0000-0000-000000000000", + UserId: "a1B2c3D4", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant", + TenantId: "malformed", + UserId: "a1B2c3D4", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "success with missing api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + RequestBody: `{"type":"public-key","id":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","rawId":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVWljUDNmb0dQZmFoZF9kdVlHbnVCeVNtVldhUGw5VkNOSzdCNDJpM2Z4ayIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIF-LTHRYEHX4r9pWj39-o2NglZV8U3wGvOkmT-KH2ecjAiEA_1dYDRgiJsx7_1i9kIX8YWqzUlOzzHlz9HJgj0dGPmQ","userHandle":"dGVzdC1wYXNza2V5LWRpc2NvdmVy"},"clientExtensionResults":{}}`, + SkipApiKey: true, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `{"token":"`, + }, + { + Name: "success with malformed api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "malformed", + RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiMEw3bTUyTmVEM0xpMXFteVhUMVp4Mk5nQ1lnNEstQTNiamQ4TzA5dmxVMCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIQCzxd6TRox_Dx4lOPrLYpfj4umnx4aNPZ1Tg7QA4OZtWwIgMSPL_oMUENDy1I5rGSmUjNs73eDtOVTq_D6wNs4qeDE","userHandle":"dGVzdC1wYXNza2V5"},"clientExtensionResults":{}}`, + SkipApiKey: false, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `{"token":"`, + }, + { + Name: "wrong user handle", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "a1B2c3D4", + RequestBody: `{"type":"public-key","id":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","rawId":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVWljUDNmb0dQZmFoZF9kdVlHbnVCeVNtVldhUGw5VkNOSzdCNDJpM2Z4ayIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIF-LTHRYEHX4r9pWj39-o2NglZV8U3wGvOkmT-KH2ecjAiEA_1dYDRgiJsx7_1i9kIX8YWqzUlOzzHlz9HJgj0dGPmQ","userHandle":"dGVzdC13cm9uZy11c2VyLWhhbmRsZQ=="},"clientExtensionResults":{}}`, + SkipApiKey: true, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "failed to get user by user handle", + }, + { + Name: "wrong credential", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "a1B2c3D4", + RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZGNqSmJtSDh3eHVCZ1p3QXdzeF84WFFuSFgxYlJCVjFoWHo3TDJ0UF91QSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIG9UopCXxa8o_Lk_-fJRgzV-6Bi8-DlTjkvAQYFpKZliAiEAgbZe4h24DHpzqKhk84LuQJmOHiXAuQz67fbo8DGJZZ8","userHandle":"dGVzdC1wYXNza2V5LWRpc2NvdmVy"},"clientExtensionResults":{}}`, + SkipApiKey: true, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "received challenge does not match with any stored one", + }, + { + Name: "fail to use mfa credential", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "mfa-test", + RequestBody: `{"type":"public-key","id":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","rawId":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQVNFZjc3X2RaYUNfM1lJSzZ0bWN6ME9NZXZDMWR3bzVNdXo1VWZfd0pNQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAg","signature":"MEUCIF7rXAv3VxIalxvMp7z55dBU5qr16hd6A_PJTjuhJ6jhAiEAnsUYUYrNcTpAT98nHmjVjyn3sH9vKJUUl2Y3bJazE1w","userHandle":"bWZhLXRlc3Q"},"clientExtensionResults":{}}`, + SkipApiKey: true, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "Unable to find the credential for the returned credential ID", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/login", + }) + s.Require().NoError(err) + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/login/finalize", currentTest.TenantId), nil) + } else { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/login/finalize", currentTest.TenantId), strings.NewReader(currentTest.RequestBody)) + } + + if !currentTest.SkipApiKey { + req.Header.Set("apiKey", currentTest.ApiKey) + } + + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + mainRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + }) + } +} diff --git a/server/api/router/main_test.go b/server/api/router/main_test.go index 26bd931..16d298c 100644 --- a/server/api/router/main_test.go +++ b/server/api/router/main_test.go @@ -1,23 +1,14 @@ package router import ( - "bytes" - "encoding/base64" - "encoding/json" "fmt" - "github.com/go-webauthn/webauthn/protocol" "github.com/gofrs/uuid" "github.com/stretchr/testify/suite" - "github.com/teamhanko/passkey-server/api/dto/request" - "github.com/teamhanko/passkey-server/api/dto/response" "github.com/teamhanko/passkey-server/config" hankoJwk "github.com/teamhanko/passkey-server/crypto/jwk" - "github.com/teamhanko/passkey-server/mapper" "github.com/teamhanko/passkey-server/test" - "github.com/teamhanko/passkey-server/test/helper" "net/http" "net/http/httptest" - "strings" "testing" ) @@ -35,491 +26,8 @@ func (s *mainRouterSuite) TestMainRouter_New() { s.Assert().NotEmpty(mainRouter) } -func (s *mainRouterSuite) TestMainRouter_ListCredentials() { - if testing.Short() { - s.T().Skip("skipping test in short mode") - } - - tests := []struct { - Name string - - TenantId string - ApiKey string - RequestBody interface{} - - SkipApiKey bool - SkipBody bool - SimulateBrokenDb bool - - ExpectedStatusCode int - ExpectedErrorMessage string - }{ - { - Name: "success", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.ListCredentialsDto{UserId: "test-passkey"}, - ExpectedStatusCode: http.StatusOK, - ExpectedErrorMessage: "", - }, - { - Name: "missing api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - RequestBody: request.ListCredentialsDto{UserId: "test-passkey"}, - SkipApiKey: true, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedErrorMessage: "The api key is invalid", - }, - { - Name: "invalid api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg", - RequestBody: request.ListCredentialsDto{UserId: "test-passkey"}, - SkipApiKey: true, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedErrorMessage: "The api key is invalid", - }, - { - Name: "tenant not found", - TenantId: "00000000-0000-0000-0000-000000000000", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.ListCredentialsDto{UserId: "test-passkey"}, - ExpectedStatusCode: http.StatusNotFound, - ExpectedErrorMessage: "tenant not found", - }, - { - Name: "invalid tenant", - TenantId: "malformed", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.ListCredentialsDto{UserId: "test-passkey"}, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedErrorMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "user not found", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.ListCredentialsDto{UserId: "not_found"}, - ExpectedStatusCode: http.StatusNotFound, - ExpectedErrorMessage: "User not found", - }, - { - Name: "user not found in tenant", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.ListCredentialsDto{UserId: "not_found"}, - ExpectedStatusCode: http.StatusNotFound, - ExpectedErrorMessage: "User not found", - }, - { - Name: "malformed body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: struct { - Lorem string - }{Lorem: "Ipsum"}, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedErrorMessage: "UserId is a required field", - }, - { - Name: "missing body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - SkipBody: true, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedErrorMessage: "UserId is a required field", - }, - { - Name: "broken db", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.ListCredentialsDto{UserId: "test-passkey"}, - SimulateBrokenDb: true, - ExpectedStatusCode: http.StatusInternalServerError, - ExpectedErrorMessage: "Internal Server Error", - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/credentials", - }) - s.Require().NoError(err) - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) - - path := fmt.Sprintf("/%s/credentials", currentTest.TenantId) - var req *http.Request - - if !currentTest.SkipBody { - jsonBody, err := json.Marshal(currentTest.RequestBody) - s.Require().NoError(err) - - req = httptest.NewRequest(http.MethodGet, path, bytes.NewReader(jsonBody)) - } else { - req = httptest.NewRequest(http.MethodGet, path, nil) - } - req.Header.Set("Content-Type", "application/json") - - if !currentTest.SkipApiKey { - req.Header.Set("apiKey", currentTest.ApiKey) - } - - rec := httptest.NewRecorder() - - if currentTest.SimulateBrokenDb { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - mainRouter.ServeHTTP(rec, req) - - if rec.Code == http.StatusOK { - var credentials response.CredentialDtoList - s.NoError(json.Unmarshal(rec.Body.Bytes(), &credentials)) - s.Equal(1, len(credentials)) - } else { - if s.Equal(currentTest.ExpectedStatusCode, rec.Code) { - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedErrorMessage) - } - } - }) - } -} - -func (s *mainRouterSuite) TestMainRouter_UpdateCredentials() { - if testing.Short() { - s.T().Skip("skipping test in short mode") - } - - tests := []struct { - Name string - - TenantId string - CredentialId string - ApiKey string - CredentialName string - RequestBody interface{} - - SkipApiKey bool - SkipBody bool - SimulateBrokenDb bool - - ExpectedStatusCode int - ExpectedErrorMessage string - }{ - { - Name: "success", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - CredentialName: "Ipsum", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: struct { - Name string `json:"name"` - }{ - Name: "Ipsum", - }, - ExpectedStatusCode: http.StatusNoContent, - ExpectedErrorMessage: "", - }, - { - Name: "missing api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - CredentialName: "Ipsum", - SkipApiKey: true, - RequestBody: struct { - Name string `json:"name"` - }{ - Name: "Ipsum", - }, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedErrorMessage: "The api key is invalid", - }, - { - Name: "invalid api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - CredentialName: "Ipsum", - ApiKey: "invalid", - RequestBody: struct { - Name string `json:"name"` - }{ - Name: "Ipsum", - }, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedErrorMessage: "The api key is invalid", - }, - { - Name: "tenant not found", - TenantId: "00000000-0000-0000-0000-000000000000", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - CredentialName: "Ipsum", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: struct { - Name string `json:"name"` - }{ - Name: "Ipsum", - }, - ExpectedStatusCode: http.StatusNotFound, - ExpectedErrorMessage: "tenant not found", - }, - { - Name: "malformed tenant id", - TenantId: "malformed", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - CredentialName: "Ipsum", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: struct { - Name string `json:"name"` - }{ - Name: "Ipsum", - }, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedErrorMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "credential not found", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - CredentialId: "4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E", - CredentialName: "Ipsum", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: struct { - Name string `json:"name"` - }{ - Name: "Ipsum", - }, - ExpectedStatusCode: http.StatusNotFound, - ExpectedErrorMessage: "credential with id '4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E' not found", - }, - { - Name: "invalid request body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - CredentialName: "Ipsum", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "Ipsum", - }, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedErrorMessage: "name is a required field", - }, - { - Name: "missing request body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - CredentialName: "Ipsum", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - SkipBody: true, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedErrorMessage: "name is a required field", - }, - { - Name: "broken db", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - CredentialName: "Ipsum", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: struct { - Name string `json:"name"` - }{ - Name: "Ipsum", - }, - SimulateBrokenDb: true, - ExpectedStatusCode: http.StatusInternalServerError, - ExpectedErrorMessage: "Internal Server Error", - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/credentials", - }) - s.Require().NoError(err) - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) - - path := fmt.Sprintf("/%s/credentials/%s", currentTest.TenantId, currentTest.CredentialId) - var req *http.Request - - testCred, err := s.Storage.GetWebauthnCredentialPersister(nil).Get(currentTest.CredentialId, uuid.FromStringOrNil(currentTest.TenantId)) - s.Require().NoError(err) - - if !currentTest.SkipBody { - jsonBody, err := json.Marshal(currentTest.RequestBody) - s.Require().NoError(err) - - req = httptest.NewRequest(http.MethodPatch, path, bytes.NewReader(jsonBody)) - } else { - req = httptest.NewRequest(http.MethodPatch, path, nil) - } - req.Header.Set("Content-Type", "application/json") - - if !currentTest.SkipApiKey { - req.Header.Set("apiKey", currentTest.ApiKey) - } - - rec := httptest.NewRecorder() - - if currentTest.SimulateBrokenDb { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - mainRouter.ServeHTTP(rec, req) - - s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) - if rec.Code == http.StatusNoContent { - updatedCred, err := s.Storage.GetWebauthnCredentialPersister(nil).Get(currentTest.CredentialId, uuid.FromStringOrNil(currentTest.TenantId)) - s.Require().NoError(err) - - s.Assert().NotEqual(*testCred.Name, *updatedCred.Name) - s.Assert().Equal(currentTest.CredentialName, *updatedCred.Name) - s.Assert().True(updatedCred.UpdatedAt.After(testCred.UpdatedAt)) - } else { - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedErrorMessage) - } - }) - } -} - -func (s *mainRouterSuite) TestMainRouter_DeleteCredentials() { - if testing.Short() { - s.T().Skip("skipping test in short mode") - } - - tests := []struct { - Name string - - TenantId string - CredentialId string - ApiKey string - - SkipApiKey bool - SimulateBrokenDb bool - - ExpectedStatusCode int - ExpectedErrorMessage string - }{ - { - Name: "success", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - ExpectedStatusCode: http.StatusNoContent, - ExpectedErrorMessage: "", - }, - { - Name: "missing api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - SkipApiKey: true, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedErrorMessage: "The api key is invalid", - }, - { - Name: "invalid api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - ApiKey: "invalid", - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedErrorMessage: "The api key is invalid", - }, - { - Name: "tenant not found", - TenantId: "00000000-0000-0000-0000-000000000000", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - ExpectedStatusCode: http.StatusNotFound, - ExpectedErrorMessage: "tenant not found", - }, - { - Name: "invalid tenant", - TenantId: "malformed", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - ExpectedStatusCode: http.StatusBadRequest, - ExpectedErrorMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "credential not found", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - CredentialId: "00000000-0000-0000-0000-000000000000", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - ExpectedStatusCode: http.StatusNotFound, - ExpectedErrorMessage: "credential with id '00000000-0000-0000-0000-000000000000' not found", - }, - { - Name: "existing credential for another tenant not found", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - CredentialId: "4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - ExpectedStatusCode: http.StatusNotFound, - ExpectedErrorMessage: "credential with id '4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E' not found", - }, - { - Name: "broken db", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - CredentialId: "kV8CJn6hoh2wIhKyw6x2fI9nGiEN5Sdczhx3o6ejgcY", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - ExpectedStatusCode: http.StatusInternalServerError, - SimulateBrokenDb: true, - ExpectedErrorMessage: "Internal Server Error", - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/credentials", - }) - s.Require().NoError(err) - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) - - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/%s/credentials/%s", currentTest.TenantId, currentTest.CredentialId), nil) - req.Header.Set("Content-Type", "application/json") - - if !currentTest.SkipApiKey { - req.Header.Set("apiKey", currentTest.ApiKey) - } - - rec := httptest.NewRecorder() - - if currentTest.SimulateBrokenDb { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - mainRouter.ServeHTTP(rec, req) - - s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) - if rec.Code == http.StatusNoContent { - updatedCred, err := s.Storage.GetWebauthnCredentialPersister(nil).Get(currentTest.CredentialId, uuid.FromStringOrNil(currentTest.TenantId)) - s.Require().NoError(err) - - s.Assert().Nil(updatedCred) - } else { - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedErrorMessage) - } - }) - } -} - func (s *mainRouterSuite) TestMainRouter_Status_Success() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } + s.SkipOnShort() mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) @@ -533,9 +41,7 @@ func (s *mainRouterSuite) TestMainRouter_Status_Success() { } func (s *mainRouterSuite) TestMainRouter_Status_Broken_DB() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } + s.SkipOnShort() err := s.Storage.MigrateDown(-1) s.Require().NoError(err) @@ -552,9 +58,7 @@ func (s *mainRouterSuite) TestMainRouter_Status_Broken_DB() { } func (s *mainRouterSuite) TestMainRouter_WellKnown() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } + s.SkipOnShort() tests := []struct { Name string @@ -605,7 +109,7 @@ func (s *mainRouterSuite) TestMainRouter_WellKnown() { for _, currentTest := range tests { s.Run(currentTest.Name, func() { - err := s.LoadMultipleFixtures([]string{"../../test/fixtures/main_router/common"}) + err := s.LoadMultipleFixtures([]string{"../../test/fixtures/common"}) s.Require().NoError(err) mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) @@ -641,1917 +145,3 @@ func (s *mainRouterSuite) TestMainRouter_WellKnown() { }) } } - -func (s *mainRouterSuite) TestMainRouter_Registration_Init() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } - - tests := []struct { - Name string - TenantId string - ApiKey string - UserId string - - RequestBody interface{} - - SkipApiKey bool - OmitRequestBody bool - SimulateBrokenDB bool - - ExpectedStatusCode int - ExpectedStatusMessage string - }{ - { - Name: "success", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - UserId: "Lorem", - RequestBody: request.InitRegistrationDto{ - UserId: "Lorem", - Username: "Ipsum", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusOK, - }, - { - Name: "success without optional parameters", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - UserId: "Lorem", - RequestBody: request.InitRegistrationDto{ - UserId: "Lorem", - Username: "Ipsum", - }, - - ExpectedStatusCode: http.StatusOK, - }, - { - Name: "tenant not found", - TenantId: "00000000-0000-0000-0000-000000000000", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "Ipsum", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusNotFound, - ExpectedStatusMessage: "tenant not found", - }, - { - Name: "wrong tenant", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "Ipsum", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "missing api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - SkipApiKey: true, - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "Ipsum", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "malformed api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "malformed", - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "Ipsum", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "display_name too long", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "Ipsum", - DisplayName: helper.ToPointer("LSiV5QsqY9Q9oiT03sIUC7imvSYM6UR9X6VtaYxfZyhyIGNrLoxolOunCoOxe8kOAAjEkMQadSezOBFpzdtIIpuceNkyHLc7cXCywx8JkRutyqpqotaUskXyGh78c5NZYcx"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "display_name cannot have more than 128 entries", - }, - { - Name: "username too long", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "LSiV5QsqY9Q9oiT03sIUC7imvSYM6UR9X6VtaYxfZyhyIGNrLoxolOunCoOxe8kOAAjEkMQadSezOBFpzdtIIpuceNkyHLc7cXCywx8JkRutyqpqotaUskXyGh78c5NZYcx", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "username cannot have more than 128 entries", - }, - { - Name: "icon must be URL", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "LSiV5QsqY9Q9oiT03sIUC7imvSYM6UR9X6VtaYxfZyhyIGNrLoxolOunCoOxe8kOAAjEkMQadSezOBFpzdtIIpuceNkyHLc7cXCywx8JkRutyqpqotaUskXyGh78c5NZYcx", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("no-url"), - }, - - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "icon must be a valid URL", - }, - { - Name: "malformed body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "Ipsum", - }, - - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "user_id is a required field and username is a required field", - }, - { - Name: "missing body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - OmitRequestBody: true, - - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "user_id is a required field and username is a required field", - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/registration", - }) - s.Require().NoError(err) - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) - - if currentTest.SimulateBrokenDB { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - var req *http.Request - if currentTest.OmitRequestBody { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/registration/initialize", currentTest.TenantId), nil) - } else { - body, err := json.Marshal(currentTest.RequestBody) - s.Require().NoError(err) - - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/registration/initialize", currentTest.TenantId), bytes.NewReader(body)) - } - - if !currentTest.SkipApiKey { - req.Header.Set("apiKey", currentTest.ApiKey) - } - - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - mainRouter.ServeHTTP(rec, req) - - s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) - - if rec.Code == http.StatusOK { - tenant, err := s.Storage.GetTenantPersister(nil).Get(uuid.FromStringOrNil(currentTest.TenantId)) - s.Require().NoError(err) - - creationOptions := protocol.CredentialCreation{} - err = json.Unmarshal(rec.Body.Bytes(), &creationOptions) - s.NoError(err) - - uId, err := base64.RawURLEncoding.DecodeString(creationOptions.Response.User.ID.(string)) - s.Require().NoError(err) - - s.NotEmpty(creationOptions.Response.Challenge) - s.Equal([]byte(currentTest.UserId), uId) - s.Equal(tenant.Config.WebauthnConfig.RelyingParty.RPId, creationOptions.Response.RelyingParty.ID) - s.Equal(protocol.ResidentKeyRequirementRequired, creationOptions.Response.AuthenticatorSelection.ResidentKey) - s.Equal(protocol.VerificationPreferred, creationOptions.Response.AuthenticatorSelection.UserVerification) - s.True(*creationOptions.Response.AuthenticatorSelection.RequireResidentKey) - } else { - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) - } - }) - } - -} - -func (s *mainRouterSuite) TestMainRouter_Registration_Finalize() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } - - tests := []struct { - Name string - TenantId string - UserId string - CredName string - RequestBody string - - OmitRequestBody bool - SimulateBrokenDB bool - UseAAGUIDMapping bool - - ExpectedStatusCode int - ExpectedStatusMessage string - }{ - { - Name: "success without mapping", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - CredName: "cred-Nx1ClZkkM2ahHzFrx75xRSHaVmJVeHfYJG96ebzU_w8", - RequestBody: `{"type":"public-key","id":"Nx1ClZkkM2ahHzFrx75xRSHaVmJVeHfYJG96ebzU_w8","rawId":"Nx1ClZkkM2ahHzFrx75xRSHaVmJVeHfYJG96ebzU_w8","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiOVVoMlByWUh5ejBBQVAxUTdlT24xS0oxc2QtR1A5amRzSi0wTWZSSmNFRSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgWMkrzdLrZuwXfvfVg6uRtDRqIRkUTODEIKiLm-PWc-ICIQDPwr0JXMJzumKMvywSgUylC4oYS6Wn3Bkq5onnv8scqGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIDcdQpWZJDNmoR8xa8e-cUUh2lZiVXh32CRvenm81P8PpQECAyYgASFYIHrhErnErli1kll2h-IZRYOuJjUb-quJ9axUJ2OhPw8UIlgglE2cEofNgKw5Nvx6dlVokaXQLQ_CxthLHIJ-IDWaxDg","transports":["internal"]},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `"token"`, - }, - { - Name: "success with mapping", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - CredName: "Chrome on Mac", - RequestBody: `{"type":"public-key","id":"Nx1ClZkkM2ahHzFrx75xRSHaVmJVeHfYJG96ebzU_w8","rawId":"Nx1ClZkkM2ahHzFrx75xRSHaVmJVeHfYJG96ebzU_w8","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiOVVoMlByWUh5ejBBQVAxUTdlT24xS0oxc2QtR1A5amRzSi0wTWZSSmNFRSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgWMkrzdLrZuwXfvfVg6uRtDRqIRkUTODEIKiLm-PWc-ICIQDPwr0JXMJzumKMvywSgUylC4oYS6Wn3Bkq5onnv8scqGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIDcdQpWZJDNmoR8xa8e-cUUh2lZiVXh32CRvenm81P8PpQECAyYgASFYIHrhErnErli1kll2h-IZRYOuJjUb-quJ9axUJ2OhPw8UIlgglE2cEofNgKw5Nvx6dlVokaXQLQ_CxthLHIJ-IDWaxDg","transports":["internal"]},"clientExtensionResults":{}}`, - UseAAGUIDMapping: true, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `"token"`, - }, - { - Name: "malformed tenant", - TenantId: "malformed", - UserId: "Lorem", - CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", - RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: `"tenant_id must be a valid uuid4"`, - }, - { - Name: "unknown tenant", - TenantId: "00000000-0000-0000-0000-000000000000", - UserId: "Lorem", - CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", - RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusNotFound, - ExpectedStatusMessage: `"tenant not found"`, - }, - { - Name: "expired registration request", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - RequestBody: `{"type":"public-key","id":"Jqujwmwsy7WxLZ6czY3DVP4COna_n2jMQa9_xVQSzfM","rawId":"Jqujwmwsy7WxLZ6czY3DVP4COna_n2jMQa9_xVQSzfM","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiM1dTUGNIQVFhXzdRdHR2M0pQZEVHRFhfcG1ISnRKNGZRNl91RERDMFFkYyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEYwRAIgO3FYNOi5bGuC38yswBmnY7ktTRYNTQg0IMzQQ1hRhmsCIGxct34SgyWtpfIUJA5tT9Sr9TU5H51b5WHitxVuPNOdaGF1dGhEYXRhWKRJlg3liA6MaHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY0UAAAAArc4AAjW8xgpkiwsl8fBVAwAgJqujwmwsy7WxLZ6czY3DVP4COna_n2jMQa9_xVQSzfOlAQIDJiABIVggt-w6lzMq9rD8Zzd8ec-E9t5shm0-QUOvDVqLSF46JE4iWCBK8XoTbSF50pI-YZ1rzCdtpTsmp4TE_RwhEQu0M19yxw","transports":["internal"]},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "Session has Expired", - }, - { - Name: "session challenge mismatch", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", - RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: `"received challenge does not match with any stored one"`, - }, - { - Name: "malformed request body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", - RequestBody: `{ "Lorem": "Ipsum" }`, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: `"unable to parse credential creation response"`, - }, - { - Name: "missing request body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", - OmitRequestBody: true, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: `"unable to parse credential creation response"`, - }, - { - Name: "broken db", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, - SimulateBrokenDB: true, - ExpectedStatusCode: http.StatusInternalServerError, - ExpectedStatusMessage: `"Internal Server Error"`, - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/registration", - }) - s.Require().NoError(err) - - var filePath *string - if !currentTest.UseAAGUIDMapping { - filePath = helper.ToPointer(".") - } - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, mapper.LoadAuthenticatorMetadata(filePath)) - - if currentTest.SimulateBrokenDB { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - var req *http.Request - if currentTest.OmitRequestBody { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/registration/finalize", currentTest.TenantId), nil) - } else { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/registration/finalize", currentTest.TenantId), strings.NewReader(currentTest.RequestBody)) - } - - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - mainRouter.ServeHTTP(rec, req) - - s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) - - if rec.Code == http.StatusOK { - creds, err := s.Storage.GetWebauthnCredentialPersister(nil).GetFromUser(currentTest.UserId, uuid.FromStringOrNil(currentTest.TenantId)) - s.Require().NoError(err) - - s.Assert().Len(creds, 1) - s.Assert().Equal(currentTest.CredName, *creds[0].Name) - } - }) - } -} - -func (s *mainRouterSuite) TestMainRouter_Registration_Mfa_Init() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } - - tests := []struct { - Name string - TenantId string - ApiKey string - UserId string - - RequestBody interface{} - - SkipApiKey bool - OmitRequestBody bool - SimulateBrokenDB bool - - ExpectedStatusCode int - ExpectedStatusMessage string - }{ - { - Name: "success", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - UserId: "Lorem", - RequestBody: request.InitRegistrationDto{ - UserId: "Lorem", - Username: "Ipsum", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusOK, - }, - { - Name: "success without optional parameters", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - UserId: "Lorem", - RequestBody: request.InitRegistrationDto{ - UserId: "Lorem", - Username: "Ipsum", - }, - - ExpectedStatusCode: http.StatusOK, - }, - { - Name: "tenant not found", - TenantId: "00000000-0000-0000-0000-000000000000", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "Ipsum", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusNotFound, - ExpectedStatusMessage: "tenant not found", - }, - { - Name: "wrong tenant", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "Ipsum", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "missing api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - SkipApiKey: true, - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "Ipsum", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "malformed api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "malformed", - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "Ipsum", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "display_name too long", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "Ipsum", - DisplayName: helper.ToPointer("LSiV5QsqY9Q9oiT03sIUC7imvSYM6UR9X6VtaYxfZyhyIGNrLoxolOunCoOxe8kOAAjEkMQadSezOBFpzdtIIpuceNkyHLc7cXCywx8JkRutyqpqotaUskXyGh78c5NZYcx"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "display_name cannot have more than 128 entries", - }, - { - Name: "username too long", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "LSiV5QsqY9Q9oiT03sIUC7imvSYM6UR9X6VtaYxfZyhyIGNrLoxolOunCoOxe8kOAAjEkMQadSezOBFpzdtIIpuceNkyHLc7cXCywx8JkRutyqpqotaUskXyGh78c5NZYcx", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("http://localhost/my/icon.png"), - }, - - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "username cannot have more than 128 entries", - }, - { - Name: "icon must be URL", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitRegistrationDto{ - UserId: "lorem", - Username: "LSiV5QsqY9Q9oiT03sIUC7imvSYM6UR9X6VtaYxfZyhyIGNrLoxolOunCoOxe8kOAAjEkMQadSezOBFpzdtIIpuceNkyHLc7cXCywx8JkRutyqpqotaUskXyGh78c5NZYcx", - DisplayName: helper.ToPointer("Lorem Ipsum"), - Icon: helper.ToPointer("no-url"), - }, - - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "icon must be a valid URL", - }, - { - Name: "malformed body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "Ipsum", - }, - - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "user_id is a required field and username is a required field", - }, - { - Name: "missing body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - OmitRequestBody: true, - - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "user_id is a required field and username is a required field", - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/registration", - }) - s.Require().NoError(err) - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) - - if currentTest.SimulateBrokenDB { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - var req *http.Request - if currentTest.OmitRequestBody { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/registration/initialize", currentTest.TenantId), nil) - } else { - body, err := json.Marshal(currentTest.RequestBody) - s.Require().NoError(err) - - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/registration/initialize", currentTest.TenantId), bytes.NewReader(body)) - } - - if !currentTest.SkipApiKey { - req.Header.Set("apiKey", currentTest.ApiKey) - } - - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - mainRouter.ServeHTTP(rec, req) - - s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) - - if rec.Code == http.StatusOK { - tenant, err := s.Storage.GetTenantPersister(nil).Get(uuid.FromStringOrNil(currentTest.TenantId)) - s.Require().NoError(err) - - creationOptions := protocol.CredentialCreation{} - err = json.Unmarshal(rec.Body.Bytes(), &creationOptions) - s.NoError(err) - - uId, err := base64.RawURLEncoding.DecodeString(creationOptions.Response.User.ID.(string)) - s.Require().NoError(err) - - s.NotEmpty(creationOptions.Response.Challenge) - s.Equal([]byte(currentTest.UserId), uId) - s.Equal(tenant.Config.WebauthnConfig.RelyingParty.RPId, creationOptions.Response.RelyingParty.ID) - s.Equal(protocol.ResidentKeyRequirementDiscouraged, creationOptions.Response.AuthenticatorSelection.ResidentKey) - s.Equal(protocol.VerificationDiscouraged, creationOptions.Response.AuthenticatorSelection.UserVerification) - s.False(*creationOptions.Response.AuthenticatorSelection.RequireResidentKey) - } else { - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) - } - }) - } - -} - -func (s *mainRouterSuite) TestMainRouter_Registration_Mfa_Finalize() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } - - tests := []struct { - Name string - TenantId string - UserId string - ApiKey string - CredName string - RequestBody string - - SkipApiKey bool - OmitRequestBody bool - SimulateBrokenDB bool - UseAAGUIDMapping bool - - ExpectedStatusCode int - ExpectedStatusMessage string - }{ - { - Name: "success without mapping", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - UserId: "test-mfa", - CredName: "cred-7ylGZMEWkocCJH6zzRx4BW12xu_BjXx2U9cvRTTVsJI", - RequestBody: `{"type":"public-key","id":"7ylGZMEWkocCJH6zzRx4BW12xu_BjXx2U9cvRTTVsJI","rawId":"7ylGZMEWkocCJH6zzRx4BW12xu_BjXx2U9cvRTTVsJI","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiaW1FTVp3clNoc3REYm8tVXVWU0xXSkVzX2gyYWhoM3UtTDg2T1hIeXByRSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgOUg2Oxj9_nr0Ecd1QRxNpvyH7X9bqVzCLfMmjADX7ccCIQDdKCLK5pPV8_dtGni9vBaOwo37NtZq1YKyCg5ElHeWUGN4NWOBWQHZMIIB1TCCAXqgAwIBAgIBATAKBggqhkjOPQQDAjBgMQswCQYDVQQGEwJVUzERMA8GA1UECgwIQ2hyb21pdW0xIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xGjAYBgNVBAMMEUJhdGNoIENlcnRpZmljYXRlMB4XDTE3MDcxNDAyNDAwMFoXDTQ0MDUzMDExNDYyMVowYDELMAkGA1UEBhMCVVMxETAPBgNVBAoMCENocm9taXVtMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMRowGAYDVQQDDBFCYXRjaCBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI1hfmXJUI5kvMVnOsgqZ5naPBRGaCwljEY__99Y39L6Pmw3i1PXlcSk3_tBme3Xhi8jq68CA7S4kRugVpmU4QGjJTAjMAwGA1UdEwEB_wQCMAAwEwYLKwYBBAGC5RwCAQEEBAMCBSAwCgYIKoZIzj0EAwIDSQAwRgIhAMyq3nMojMBhy72bTW_GsKcpTmCnASaUU-XSJFzmPpiKAiEAv2MDR9dsfZWYfkk3MQwRyqIgDKUFr_NbDOcKkOmIv1poYXV0aERhdGFYpEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjRQAAAAEBAgMEBQYHCAECAwQFBgcIACDvKUZkwRaShwIkfrPNHHgFbXbG78GNfHZT1y9FNNWwkqUBAgMmIAEhWCC2T8LO1U5gt6dWYinNf0X_imOxyPtSIIqq3-tH0IUB8iJYIC2o7XHv3zDWr2vykXxZWMPybcnXWcAS5oWHDkZmSwqN","transports":["usb"]},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `"token"`, - }, - { - Name: "success with mapping", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - UserId: "test-mfa", - CredName: "Test Provider", - RequestBody: `{"type":"public-key","id":"7ylGZMEWkocCJH6zzRx4BW12xu_BjXx2U9cvRTTVsJI","rawId":"7ylGZMEWkocCJH6zzRx4BW12xu_BjXx2U9cvRTTVsJI","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiaW1FTVp3clNoc3REYm8tVXVWU0xXSkVzX2gyYWhoM3UtTDg2T1hIeXByRSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgOUg2Oxj9_nr0Ecd1QRxNpvyH7X9bqVzCLfMmjADX7ccCIQDdKCLK5pPV8_dtGni9vBaOwo37NtZq1YKyCg5ElHeWUGN4NWOBWQHZMIIB1TCCAXqgAwIBAgIBATAKBggqhkjOPQQDAjBgMQswCQYDVQQGEwJVUzERMA8GA1UECgwIQ2hyb21pdW0xIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xGjAYBgNVBAMMEUJhdGNoIENlcnRpZmljYXRlMB4XDTE3MDcxNDAyNDAwMFoXDTQ0MDUzMDExNDYyMVowYDELMAkGA1UEBhMCVVMxETAPBgNVBAoMCENocm9taXVtMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMRowGAYDVQQDDBFCYXRjaCBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI1hfmXJUI5kvMVnOsgqZ5naPBRGaCwljEY__99Y39L6Pmw3i1PXlcSk3_tBme3Xhi8jq68CA7S4kRugVpmU4QGjJTAjMAwGA1UdEwEB_wQCMAAwEwYLKwYBBAGC5RwCAQEEBAMCBSAwCgYIKoZIzj0EAwIDSQAwRgIhAMyq3nMojMBhy72bTW_GsKcpTmCnASaUU-XSJFzmPpiKAiEAv2MDR9dsfZWYfkk3MQwRyqIgDKUFr_NbDOcKkOmIv1poYXV0aERhdGFYpEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjRQAAAAEBAgMEBQYHCAECAwQFBgcIACDvKUZkwRaShwIkfrPNHHgFbXbG78GNfHZT1y9FNNWwkqUBAgMmIAEhWCC2T8LO1U5gt6dWYinNf0X_imOxyPtSIIqq3-tH0IUB8iJYIC2o7XHv3zDWr2vykXxZWMPybcnXWcAS5oWHDkZmSwqN","transports":["usb"]},"clientExtensionResults":{}}`, - UseAAGUIDMapping: true, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `"token"`, - }, - { - Name: "malformed api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "malformed", - UserId: "Lorem", - CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", - RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: `"The api key is invalid"`, - }, - { - Name: "wrong api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "bXta7uDpBt6nBn4j2sX7vm1KGqp7ma9GXPx835IiEdnJxWu_HA-rCdmdTk7I9oYQnYo4MebHkQ3khSosXJ6y5A==", - UserId: "Lorem", - CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", - RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: `"The api key is invalid"`, - }, - { - Name: "missing api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - SkipApiKey: true, - UserId: "Lorem", - CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", - RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: `"The api key is invalid"`, - }, - { - Name: "malformed tenant", - TenantId: "malformed", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - UserId: "Lorem", - CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", - RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: `"tenant_id must be a valid uuid4"`, - }, - { - Name: "unknown tenant", - TenantId: "00000000-0000-0000-0000-000000000000", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - UserId: "Lorem", - CredName: "cred-b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o", - RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusNotFound, - ExpectedStatusMessage: `"tenant not found"`, - }, - { - Name: "expired registration request", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"_z-CcHsvOffFBgOFVStr4rvRg9UWmdXIS6ay8Gtu97g","rawId":"_z-CcHsvOffFBgOFVStr4rvRg9UWmdXIS6ay8Gtu97g","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiRDdScXRaTVhONDhsazVTN2haUGFiUVJrNTZudzNkeWFSNWhxaDRTbDQ2VSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEYwRAIgYtyvs2Wd9_dH522_Qjynemk6bNZYZCeV8-SmEf8cCD8CIHpKIuP6PyfDG0EyhYzjXZVRJpzS9UcKMGzt0HhinW1HY3g1Y4FZAdgwggHUMIIBeqADAgECAgEBMAoGCCqGSM49BAMCMGAxCzAJBgNVBAYTAlVTMREwDwYDVQQKDAhDaHJvbWl1bTEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEaMBgGA1UEAwwRQmF0Y2ggQ2VydGlmaWNhdGUwHhcNMTcwNzE0MDI0MDAwWhcNNDQwNTMwMTIxNTU1WjBgMQswCQYDVQQGEwJVUzERMA8GA1UECgwIQ2hyb21pdW0xIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xGjAYBgNVBAMMEUJhdGNoIENlcnRpZmljYXRlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjWF-ZclQjmS8xWc6yCpnmdo8FEZoLCWMRj__31jf0vo-bDeLU9eVxKTf-0GZ7deGLyOrrwIDtLiRG6BWmZThAaMlMCMwDAYDVR0TAQH_BAIwADATBgsrBgEEAYLlHAIBAQQEAwIFIDAKBggqhkjOPQQDAgNIADBFAiAdS_eryqvNTJUZefLA7ROApbRRoFWhTMMdeyqOuaVEwQIhANxdTGF0hhmKp-1qJtOx4bJ_jqTY3-IgYAecVcjH1DVLaGF1dGhEYXRhWKRJlg3liA6MaHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY0UAAAABAQIDBAUGBwgBAgMEBQYHCAAg_z-CcHsvOffFBgOFVStr4rvRg9UWmdXIS6ay8Gtu97ilAQIDJiABIVggsKAEIjWe5HmX75vyyL_rZO01oeypMur48b11Un0NIvciWCCLJ4hDtnq1kD7IFdWBqwA4wj0L3oQ2Eq1x6TNPwUJ_Dg","transports":["usb"]},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "Session has Expired", - }, - { - Name: "session challenge mismatch", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", - ApiKey: "bXta7uDpBt6nBn4j2sX7vm1KGqp7ma9GXPx835IiEdnJxWu_HA-rCdmdTk7I9oYQnYo4MebHkQ3khSosXJ6y5A==", - RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: `"received challenge does not match with any stored one"`, - }, - { - Name: "malformed request body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", - ApiKey: "bXta7uDpBt6nBn4j2sX7vm1KGqp7ma9GXPx835IiEdnJxWu_HA-rCdmdTk7I9oYQnYo4MebHkQ3khSosXJ6y5A==", - RequestBody: `{ "Lorem": "Ipsum" }`, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: `"unable to parse credential creation response"`, - }, - { - Name: "missing request body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", - ApiKey: "bXta7uDpBt6nBn4j2sX7vm1KGqp7ma9GXPx835IiEdnJxWu_HA-rCdmdTk7I9oYQnYo4MebHkQ3khSosXJ6y5A==", - OmitRequestBody: true, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: `"unable to parse credential creation response"`, - }, - { - Name: "broken db", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","rawId":"b0JJ8QlACObapxKZxGeJShWqR2vJTT35mYOEJ4iuK-o","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMnYxUEVsV2ljTGY4NGt2NFdyRURCSXJpSnF6SDJvQVhQejFWVjN0Nm9PUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgOtouIIha-G3rnDJvlVdHcNkFPC99rPWcsEQPlwwm_ukCIQD1W_2RutWFdBm6ipujAo_NjqEZa9iIde9eiWmD099AlWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG9CSfEJQAjm2qcSmcRniUoVqkdryU09-ZmDhCeIrivqpQECAyYgASFYIIQD9mTg5b-8jeUm4WMTjiPUnBJU0ybAjrcB2yuPVPaLIlgggf9CClQmZRnc88XPeJzqXpyw2eBOFbmNvEUFIe_-6w4","transports":["internal"]},"clientExtensionResults":{}}`, - SimulateBrokenDB: true, - ExpectedStatusCode: http.StatusInternalServerError, - ExpectedStatusMessage: `"Internal Server Error"`, - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/registration", - }) - s.Require().NoError(err) - - var metadata mapper.AuthenticatorMetadata - if currentTest.UseAAGUIDMapping { - filePath := helper.ToPointer("") - - metadata = mapper.LoadAuthenticatorMetadata(filePath) - - // add mfa test aaguid for mapping - metadata["01020304-0506-0708-0102-030405060708"] = mapper.Authenticator{ - Name: "Test Provider", - IconLight: "", - IconDark: "", - } - } - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, metadata) - - if currentTest.SimulateBrokenDB { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - var req *http.Request - if currentTest.OmitRequestBody { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/registration/finalize", currentTest.TenantId), nil) - } else { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/registration/finalize", currentTest.TenantId), strings.NewReader(currentTest.RequestBody)) - } - - if !currentTest.SkipApiKey { - req.Header.Set("apiKey", currentTest.ApiKey) - } - - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - mainRouter.ServeHTTP(rec, req) - - s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) - - if rec.Code == http.StatusOK { - creds, err := s.Storage.GetWebauthnCredentialPersister(nil).GetFromUser(currentTest.UserId, uuid.FromStringOrNil(currentTest.TenantId)) - s.Require().NoError(err) - - s.Assert().Len(creds, 1) - s.Assert().Equal(currentTest.CredName, *creds[0].Name) - } - }) - } -} - -func (s *mainRouterSuite) TestMainRouter_Login_Init() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } - - tests := []struct { - Name string - TenantId string - UserId string - ApiKey string - RequestBody interface{} - IsDiscoverLogin bool - - SkipApiKey bool - SimulateBrokenDB bool - OmitRequestBody bool - - ExpectedStatusCode int - ExpectedStatusMessage string - }{ - { - Name: "success with discovery", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - OmitRequestBody: true, - SkipApiKey: true, - IsDiscoverLogin: true, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `"publicKey":{"challenge":`, - }, - { - Name: "success without discovery", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitLoginDto{UserId: helper.ToPointer("test-passkey")}, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `"publicKey":{"challenge":`, - }, - { - Name: "perform discovery on malformed request body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - IsDiscoverLogin: true, - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "malformed", - }, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `"publicKey":{"challenge":`, - }, - { - Name: "perform discovery on missing request body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - OmitRequestBody: true, - IsDiscoverLogin: true, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `"publicKey":{"challenge":`, - }, - { - Name: "missing api key on non discovery", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - RequestBody: request.InitLoginDto{UserId: helper.ToPointer("test-passkey")}, - SkipApiKey: true, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "api key is missing", - }, - { - Name: "malformed api key on non discovery", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "malformed", - RequestBody: request.InitLoginDto{UserId: helper.ToPointer("test-passkey")}, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "unknown user", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitLoginDto{UserId: helper.ToPointer("not_found")}, - ExpectedStatusCode: http.StatusNotFound, - ExpectedStatusMessage: "user not found", - }, - { - Name: "malformed tenant", - TenantId: "malformed", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitLoginDto{UserId: helper.ToPointer("test-passkey")}, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "missing tenant", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitLoginDto{UserId: helper.ToPointer("test-passkey")}, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "broken db", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitLoginDto{UserId: helper.ToPointer("a1B2c3D3")}, - SimulateBrokenDB: true, - ExpectedStatusCode: http.StatusInternalServerError, - ExpectedStatusMessage: "Internal Server Error", - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/login", - }) - s.Require().NoError(err) - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) - - if currentTest.SimulateBrokenDB { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - var req *http.Request - if currentTest.OmitRequestBody { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/login/initialize", currentTest.TenantId), nil) - } else { - body, err := json.Marshal(currentTest.RequestBody) - s.Require().NoError(err) - - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/login/initialize", currentTest.TenantId), bytes.NewReader(body)) - } - - if !currentTest.SkipApiKey { - req.Header.Set("apiKey", currentTest.ApiKey) - } - - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - mainRouter.ServeHTTP(rec, req) - - s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) - - if rec.Code == http.StatusOK { - var ca protocol.CredentialAssertion - err = json.Unmarshal(rec.Body.Bytes(), &ca) - s.Require().NoError(err) - - sessionData, err := s.Storage.GetWebauthnSessionDataPersister(nil).GetByChallenge(ca.Response.Challenge.String(), uuid.FromStringOrNil(currentTest.TenantId)) - s.Require().NoError(err) - s.Assert().Equal(currentTest.IsDiscoverLogin, sessionData.IsDiscoverable) - } - }) - } -} - -func (s *mainRouterSuite) TestMainRouter_Login_Finish() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } - - tests := []struct { - Name string - TenantId string - UserId string - ApiKey string - RequestBody string - - SkipApiKey bool - SimulateBrokenDB bool - OmitRequestBody bool - - ExpectedStatusCode int - ExpectedStatusMessage string - }{ - { - Name: "success without discovery", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiMEw3bTUyTmVEM0xpMXFteVhUMVp4Mk5nQ1lnNEstQTNiamQ4TzA5dmxVMCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIQCzxd6TRox_Dx4lOPrLYpfj4umnx4aNPZ1Tg7QA4OZtWwIgMSPL_oMUENDy1I5rGSmUjNs73eDtOVTq_D6wNs4qeDE","userHandle":"dGVzdC1wYXNza2V5"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `{"token":"`, - }, - { - Name: "success with discovery", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey-discover", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","rawId":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVWljUDNmb0dQZmFoZF9kdVlHbnVCeVNtVldhUGw5VkNOSzdCNDJpM2Z4ayIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIF-LTHRYEHX4r9pWj39-o2NglZV8U3wGvOkmT-KH2ecjAiEA_1dYDRgiJsx7_1i9kIX8YWqzUlOzzHlz9HJgj0dGPmQ","userHandle":"dGVzdC1wYXNza2V5LWRpc2NvdmVy"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `{"token":"`, - }, - { - Name: "wrong tenant", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", - UserId: "a1B2c3D4", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "received challenge does not match with any stored one", - }, - { - Name: "tenant not found", - TenantId: "00000000-0000-0000-0000-000000000000", - UserId: "a1B2c3D4", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusNotFound, - ExpectedStatusMessage: "tenant not found", - }, - { - Name: "malformed tenant", - TenantId: "malformed", - UserId: "a1B2c3D4", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "success with missing api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - RequestBody: `{"type":"public-key","id":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","rawId":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVWljUDNmb0dQZmFoZF9kdVlHbnVCeVNtVldhUGw5VkNOSzdCNDJpM2Z4ayIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIF-LTHRYEHX4r9pWj39-o2NglZV8U3wGvOkmT-KH2ecjAiEA_1dYDRgiJsx7_1i9kIX8YWqzUlOzzHlz9HJgj0dGPmQ","userHandle":"dGVzdC1wYXNza2V5LWRpc2NvdmVy"},"clientExtensionResults":{}}`, - SkipApiKey: true, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `{"token":"`, - }, - { - Name: "success with malformed api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "malformed", - RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiMEw3bTUyTmVEM0xpMXFteVhUMVp4Mk5nQ1lnNEstQTNiamQ4TzA5dmxVMCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIQCzxd6TRox_Dx4lOPrLYpfj4umnx4aNPZ1Tg7QA4OZtWwIgMSPL_oMUENDy1I5rGSmUjNs73eDtOVTq_D6wNs4qeDE","userHandle":"dGVzdC1wYXNza2V5"},"clientExtensionResults":{}}`, - SkipApiKey: false, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `{"token":"`, - }, - { - Name: "wrong user handle", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "a1B2c3D4", - RequestBody: `{"type":"public-key","id":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","rawId":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVWljUDNmb0dQZmFoZF9kdVlHbnVCeVNtVldhUGw5VkNOSzdCNDJpM2Z4ayIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIF-LTHRYEHX4r9pWj39-o2NglZV8U3wGvOkmT-KH2ecjAiEA_1dYDRgiJsx7_1i9kIX8YWqzUlOzzHlz9HJgj0dGPmQ","userHandle":"dGVzdC13cm9uZy11c2VyLWhhbmRsZQ=="},"clientExtensionResults":{}}`, - SkipApiKey: true, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "failed to get user by user handle", - }, - { - Name: "wrong credential", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "a1B2c3D4", - RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZGNqSmJtSDh3eHVCZ1p3QXdzeF84WFFuSFgxYlJCVjFoWHo3TDJ0UF91QSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIG9UopCXxa8o_Lk_-fJRgzV-6Bi8-DlTjkvAQYFpKZliAiEAgbZe4h24DHpzqKhk84LuQJmOHiXAuQz67fbo8DGJZZ8","userHandle":"dGVzdC1wYXNza2V5LWRpc2NvdmVy"},"clientExtensionResults":{}}`, - SkipApiKey: true, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "received challenge does not match with any stored one", - }, - { - Name: "fail to use mfa credential", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "mfa-test", - RequestBody: `{"type":"public-key","id":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","rawId":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQVNFZjc3X2RaYUNfM1lJSzZ0bWN6ME9NZXZDMWR3bzVNdXo1VWZfd0pNQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAg","signature":"MEUCIF7rXAv3VxIalxvMp7z55dBU5qr16hd6A_PJTjuhJ6jhAiEAnsUYUYrNcTpAT98nHmjVjyn3sH9vKJUUl2Y3bJazE1w","userHandle":"bWZhLXRlc3Q"},"clientExtensionResults":{}}`, - SkipApiKey: true, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "Unable to find the credential for the returned credential ID", - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/login", - }) - s.Require().NoError(err) - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) - - if currentTest.SimulateBrokenDB { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - var req *http.Request - if currentTest.OmitRequestBody { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/login/finalize", currentTest.TenantId), nil) - } else { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/login/finalize", currentTest.TenantId), strings.NewReader(currentTest.RequestBody)) - } - - if !currentTest.SkipApiKey { - req.Header.Set("apiKey", currentTest.ApiKey) - } - - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - mainRouter.ServeHTTP(rec, req) - - s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) - }) - } -} - -func (s *mainRouterSuite) TestMainRouter_Mfa_Login_Init() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } - - tests := []struct { - Name string - TenantId string - UserId string - ApiKey string - RequestBody interface{} - - SkipApiKey bool - SimulateBrokenDB bool - OmitRequestBody bool - - ExpectedStatusCode int - ExpectedStatusMessage string - }{ - { - Name: "success", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitMfaLoginDto{UserId: "test-passkey"}, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `"publicKey":{"challenge":`, - }, - { - Name: "malformed request body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "malformed", - }, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "user_id is a required field", - }, - { - Name: "missing request body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - OmitRequestBody: true, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "user_id is a required field", - }, - { - Name: "missing api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - RequestBody: request.InitMfaLoginDto{UserId: "test-passkey"}, - SkipApiKey: true, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "unknown user", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitMfaLoginDto{UserId: "4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E"}, - ExpectedStatusCode: http.StatusNotFound, - ExpectedStatusMessage: "user not found", - }, - { - Name: "malformed tenant", - TenantId: "malformed", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitMfaLoginDto{UserId: "test-passkey"}, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "missing tenant", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitMfaLoginDto{UserId: "test-passkey"}, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "broken db", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitMfaLoginDto{UserId: "test-passkey"}, - SimulateBrokenDB: true, - ExpectedStatusCode: http.StatusInternalServerError, - ExpectedStatusMessage: "Internal Server Error", - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/login", - }) - s.Require().NoError(err) - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) - - if currentTest.SimulateBrokenDB { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - var req *http.Request - if currentTest.OmitRequestBody { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/login/initialize", currentTest.TenantId), nil) - } else { - body, err := json.Marshal(currentTest.RequestBody) - s.Require().NoError(err) - - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/login/initialize", currentTest.TenantId), bytes.NewReader(body)) - } - - if !currentTest.SkipApiKey { - req.Header.Set("apiKey", currentTest.ApiKey) - } - - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - mainRouter.ServeHTTP(rec, req) - - s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) - - if rec.Code == http.StatusOK { - var ca protocol.CredentialAssertion - err = json.Unmarshal(rec.Body.Bytes(), &ca) - s.Require().NoError(err) - - sessionData, err := s.Storage.GetWebauthnSessionDataPersister(nil).GetByChallenge(ca.Response.Challenge.String(), uuid.FromStringOrNil(currentTest.TenantId)) - s.Require().NoError(err) - s.Assert().False(sessionData.IsDiscoverable) - } - }) - } -} - -func (s *mainRouterSuite) TestMainRouter_MFA_Login_Finish() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } - - tests := []struct { - Name string - TenantId string - UserId string - ApiKey string - RequestBody string - - SkipApiKey bool - SimulateBrokenDB bool - OmitRequestBody bool - - ExpectedStatusCode int - ExpectedStatusMessage string - }{ - { - Name: "success", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-mfa", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","rawId":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQVNFZjc3X2RaYUNfM1lJSzZ0bWN6ME9NZXZDMWR3bzVNdXo1VWZfd0pNQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAg","signature":"MEUCIF7rXAv3VxIalxvMp7z55dBU5qr16hd6A_PJTjuhJ6jhAiEAnsUYUYrNcTpAT98nHmjVjyn3sH9vKJUUl2Y3bJazE1w","userHandle":"bWZhLXRlc3Q"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `{"token":"`, - }, - { - Name: "wrong tenant", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", - UserId: "test-mfa", - ApiKey: "bXta7uDpBt6nBn4j2sX7vm1KGqp7ma9GXPx835IiEdnJxWu_HA-rCdmdTk7I9oYQnYo4MebHkQ3khSosXJ6y5A==", - RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "received challenge does not match with any stored one", - }, - { - Name: "tenant not found", - TenantId: "00000000-0000-0000-0000-000000000000", - UserId: "test-mfa", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusNotFound, - ExpectedStatusMessage: "tenant not found", - }, - { - Name: "malformed tenant", - TenantId: "malformed", - UserId: "test-mfa", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "missing api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-mfa", - RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, - SkipApiKey: true, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "malformed api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-mfa", - ApiKey: "malformed", - RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "malformed user handle should be ignored", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-mfa", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","rawId":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQVNFZjc3X2RaYUNfM1lJSzZ0bWN6ME9NZXZDMWR3bzVNdXo1VWZfd0pNQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAg","signature":"MEUCIF7rXAv3VxIalxvMp7z55dBU5qr16hd6A_PJTjuhJ6jhAiEAnsUYUYrNcTpAT98nHmjVjyn3sH9vKJUUl2Y3bJazE1w","userHandle":"bWZhLXRlc3QtMgs"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `{"token":"`, - }, - { - Name: "wrong credential", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-mfa", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"TaCJSxWWK3UI7UovgNJHfWXDPQMm0lo6shGO6iaLGNo","rawId":"TaCJSxWWK3UI7UovgNJHfWXDPQMm0lo6shGO6iaLGNo","authenticatorAttachment":"cross-platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoidEZzbGxaNTZGdUJheUxqeTJmYy1NM0Q3MGt4QU9zY3RVenVwdjk0MldrSSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAg","signature":"MEYCIQCRYwDVmkcrpgH7idU35qiJH9XFs5zi9PFQcunCYe7YvwIhAK3nsXNGAyf2G1kBMJGiOaqQbyiSTDlD2itE0euh_jnJ","userHandle":"bWZhLXRlc3Q"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "Unable to find the credential for the returned credential ID", - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/login", - }) - s.Require().NoError(err) - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) - - if currentTest.SimulateBrokenDB { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - var req *http.Request - if currentTest.OmitRequestBody { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/login/finalize", currentTest.TenantId), nil) - } else { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/mfa/login/finalize", currentTest.TenantId), strings.NewReader(currentTest.RequestBody)) - } - - if !currentTest.SkipApiKey { - req.Header.Set("apiKey", currentTest.ApiKey) - } - - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - mainRouter.ServeHTTP(rec, req) - - s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) - }) - } -} - -func (s *mainRouterSuite) TestMainRouter_Transaction_List() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } - - tests := []struct { - Name string - TenantId string - UserId string - ApiKey string - - SkipApiKey bool - SimulateBrokenDB bool - OmitRequestBody bool - - ExpectedStatusCode int - ExpectedStatusMessage string - }{ - { - Name: "success", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `[{"id":"`, - }, - { - Name: "missing user id", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - ExpectedStatusCode: http.StatusNotFound, - ExpectedStatusMessage: "Not Found", - }, - { - Name: "malformed user id", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "malformed", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "invalid user id", - }, - { - Name: "unknown user", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "000000000-0000-0000-0000-000000000000", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "invalid user id", - }, - { - Name: "missing api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", - SkipApiKey: true, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "malformed api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", - ApiKey: "malformed", - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "malformed tenant", - TenantId: "malformed", - UserId: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "missing tenant", - UserId: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "broken db", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - SimulateBrokenDB: true, - ExpectedStatusCode: http.StatusInternalServerError, - ExpectedStatusMessage: "Internal Server Error", - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/transaction", - }) - s.Require().NoError(err) - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) - - if currentTest.SimulateBrokenDB { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/%s/transaction/%s", currentTest.TenantId, currentTest.UserId), nil) - - if !currentTest.SkipApiKey { - req.Header.Set("apiKey", currentTest.ApiKey) - } - - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - mainRouter.ServeHTTP(rec, req) - - s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) - - if rec.Code == http.StatusOK { - var transactions []response.TransactionDto - err = json.Unmarshal(rec.Body.Bytes(), &transactions) - s.Require().NoError(err) - - s.Assert().Len(transactions, 2) - } - }) - } -} - -func (s *mainRouterSuite) TestMainRouter_Transaction_Init() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } - - tests := []struct { - Name string - TenantId string - UserId string - ApiKey string - RequestBody interface{} - - SkipApiKey bool - SimulateBrokenDB bool - OmitRequestBody bool - - ExpectedStatusCode int - ExpectedStatusMessage string - }{ - { - Name: "success", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitTransactionDto{ - UserId: "test-passkey", - TransactionId: "00000001-0001-0001-0001-000000000001", - TransactionData: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "Ipsum", - }, - }, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `"publicKey":{"challenge":`, - }, - { - Name: "missing user id", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitTransactionDto{ - TransactionId: "00000001-0001-0001-0001-000000000001", - TransactionData: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "Ipsum", - }, - }, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "user_id is a required field", - }, - { - Name: "missing transaction id", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitTransactionDto{ - UserId: "test-passkey", - TransactionData: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "Ipsum", - }, - }, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "transaction_id is a required field", - }, - { - Name: "transaction id to long", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitTransactionDto{ - UserId: "test-passkey", - TransactionId: "00000001-0001-0001-0001-000000000001--00000001-0001-0001-0001-000000000001--00000001-0001-0001-0001-000000000001--00000001-0001-0001-0001-000000000001", - TransactionData: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "Ipsum", - }, - }, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "transaction_id cannot have more than 128 entries", - }, - { - Name: "missing transaction data", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitTransactionDto{ - UserId: "test-passkey", - TransactionId: "00000001-0001-0001-0001-000000000001", - }, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "transaction_data is a required field", - }, - { - Name: "malformed request body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "malformed", - }, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "user_id is a required field and transaction_id is a required field and transaction_data is a required field", - }, - { - Name: "missing request body", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - OmitRequestBody: true, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "user_id is a required field and transaction_id is a required field and transaction_data is a required field", - }, - { - Name: "missing api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - RequestBody: request.InitTransactionDto{ - UserId: "test-passkey", - TransactionId: "00000001-0001-0001-0001-000000000001", - TransactionData: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "Ipsum", - }, - }, - SkipApiKey: true, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "unknown user", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitTransactionDto{ - UserId: "4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E", - TransactionId: "00000001-0001-0001-0001-000000000001", - TransactionData: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "Ipsum", - }, - }, - ExpectedStatusCode: http.StatusNotFound, - ExpectedStatusMessage: "unable to find user", - }, - { - Name: "malformed tenant", - TenantId: "malformed", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitTransactionDto{ - UserId: "test-passkey", - TransactionId: "00000001-0001-0001-0001-000000000001", - TransactionData: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "Ipsum", - }, - }, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "missing tenant", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitTransactionDto{ - UserId: "test-passkey", - TransactionId: "00000001-0001-0001-0001-000000000001", - TransactionData: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "Ipsum", - }, - }, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "broken db", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: request.InitTransactionDto{ - UserId: "test-passkey", - TransactionId: "00000001-0001-0001-0001-000000000001", - TransactionData: struct { - Lorem string `json:"lorem"` - }{ - Lorem: "Ipsum", - }, - }, - SimulateBrokenDB: true, - ExpectedStatusCode: http.StatusInternalServerError, - ExpectedStatusMessage: "Internal Server Error", - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/transaction", - }) - s.Require().NoError(err) - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) - - if currentTest.SimulateBrokenDB { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - var req *http.Request - if currentTest.OmitRequestBody { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/transaction/initialize", currentTest.TenantId), nil) - } else { - body, err := json.Marshal(currentTest.RequestBody) - s.Require().NoError(err) - - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/transaction/initialize", currentTest.TenantId), bytes.NewReader(body)) - } - - if !currentTest.SkipApiKey { - req.Header.Set("apiKey", currentTest.ApiKey) - } - - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - mainRouter.ServeHTTP(rec, req) - - s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) - - if rec.Code == http.StatusOK { - var ca protocol.CredentialAssertion - err = json.Unmarshal(rec.Body.Bytes(), &ca) - s.Require().NoError(err) - - sessionData, err := s.Storage.GetWebauthnSessionDataPersister(nil).GetByChallenge(ca.Response.Challenge.String(), uuid.FromStringOrNil(currentTest.TenantId)) - s.Require().NoError(err) - s.Assert().False(sessionData.IsDiscoverable) - } - }) - } -} - -func (s *mainRouterSuite) TestMainRouter_Transaction_Finish() { - if testing.Short() { - s.T().Skip("skipping test in short mode.") - } - - tests := []struct { - Name string - TenantId string - UserId string - ApiKey string - RequestBody string - - SkipApiKey bool - SimulateBrokenDB bool - OmitRequestBody bool - - ExpectedStatusCode int - ExpectedStatusMessage string - }{ - { - Name: "success", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey-discover", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRHlwQUlpODVuclVlWkR4THltMkNqeEhRRE1kRzNNTmRiaU13eXVKZ0VDZmh5ZVJsY3dDMEV5Z1ZFb2FEbDkxYlJVM1hLYWMwSnZLbU5oMi1XM01MV3ciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEYCIQDUYA_5bvdEf0LT5Xq2qMQvEKUflVBd5pcl2wkz87KlYgIhAO3XNLPOSwffYdnwht5h3pJTexxc-KwuLpsCSsWCfbLc","userHandle":"dGVzdC1wYXNza2V5"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusOK, - ExpectedStatusMessage: `{"token":"`, - }, - { - Name: "wrong tenant", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", - UserId: "a1B2c3D4", - ApiKey: "bXta7uDpBt6nBn4j2sX7vm1KGqp7ma9GXPx835IiEdnJxWu_HA-rCdmdTk7I9oYQnYo4MebHkQ3khSosXJ6y5A==", - RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "failed to get transaction data", - }, - { - Name: "tenant not found", - TenantId: "00000000-0000-0000-0000-000000000000", - UserId: "a1B2c3D4", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusNotFound, - ExpectedStatusMessage: "tenant not found", - }, - { - Name: "malformed tenant", - TenantId: "malformed", - UserId: "a1B2c3D4", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusBadRequest, - ExpectedStatusMessage: "tenant_id must be a valid uuid4", - }, - { - Name: "success with missing api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - RequestBody: `{"type":"public-key","id":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","rawId":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVWljUDNmb0dQZmFoZF9kdVlHbnVCeVNtVldhUGw5VkNOSzdCNDJpM2Z4ayIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIF-LTHRYEHX4r9pWj39-o2NglZV8U3wGvOkmT-KH2ecjAiEA_1dYDRgiJsx7_1i9kIX8YWqzUlOzzHlz9HJgj0dGPmQ","userHandle":"dGVzdC1wYXNza2V5LWRpc2NvdmVy"},"clientExtensionResults":{}}`, - SkipApiKey: true, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "malformed api key", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "test-passkey", - ApiKey: "malformed", - RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiMEw3bTUyTmVEM0xpMXFteVhUMVp4Mk5nQ1lnNEstQTNiamQ4TzA5dmxVMCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIQCzxd6TRox_Dx4lOPrLYpfj4umnx4aNPZ1Tg7QA4OZtWwIgMSPL_oMUENDy1I5rGSmUjNs73eDtOVTq_D6wNs4qeDE","userHandle":"dGVzdC1wYXNza2V5"},"clientExtensionResults":{}}`, - SkipApiKey: false, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "The api key is invalid", - }, - { - Name: "wrong user handle", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "a1B2c3D4", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRHlwQUlpODVuclVlWkR4THltMkNqeEhRRE1kRzNNTmRiaU13eXVKZ0VDZmh5ZVJsY3dDMEV5Z1ZFb2FEbDkxYlJVM1hLYWMwSnZLbU5oMi1XM01MV3ciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEYCIQDUYA_5bvdEf0LT5Xq2qMQvEKUflVBd5pcl2wkz87KlYgIhAO3XNLPOSwffYdnwht5h3pJTexxc-KwuLpsCSsWCfbLc","userHandle":"dGVzdA=="},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "failed to get user by user handle", - }, - { - Name: "wrong credential", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "a1B2c3D4", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZGNqSmJtSDh3eHVCZ1p3QXdzeF84WFFuSFgxYlJCVjFoWHo3TDJ0UF91QSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIG9UopCXxa8o_Lk_-fJRgzV-6Bi8-DlTjkvAQYFpKZliAiEAgbZe4h24DHpzqKhk84LuQJmOHiXAuQz67fbo8DGJZZ8","userHandle":"dGVzdC1wYXNza2V5LWRpc2NvdmVy"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "received challenge does not match with any stored one", - }, - { - Name: "fail to use mfa credential", - TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", - UserId: "mfa-test", - ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", - RequestBody: `{"type":"public-key","id":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","rawId":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRHlwQUlpODVuclVlWkR4THltMkNqeEhRRE1kRzNNTmRiaU13eXVKZ0VDZmh5ZVJsY3dDMEV5Z1ZFb2FEbDkxYlJVM1hLYWMwSnZLbU5oMi1XM01MV3ciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEYCIQDUYA_5bvdEf0LT5Xq2qMQvEKUflVBd5pcl2wkz87KlYgIhAO3XNLPOSwffYdnwht5h3pJTexxc-KwuLpsCSsWCfbLc","userHandle":"dGVzdC1wYXNza2V5"},"clientExtensionResults":{}}`, - ExpectedStatusCode: http.StatusUnauthorized, - ExpectedStatusMessage: "Unable to find the credential for the returned credential ID", - }, - } - - for _, currentTest := range tests { - s.Run(currentTest.Name, func() { - err := s.LoadMultipleFixtures([]string{ - "../../test/fixtures/main_router/common", - "../../test/fixtures/main_router/transaction", - }) - s.Require().NoError(err) - - mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) - - if currentTest.SimulateBrokenDB { - err := s.Storage.MigrateDown(-1) - s.Require().NoError(err) - } - - var req *http.Request - if currentTest.OmitRequestBody { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/transaction/finalize", currentTest.TenantId), nil) - } else { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/transaction/finalize", currentTest.TenantId), strings.NewReader(currentTest.RequestBody)) - } - - if !currentTest.SkipApiKey { - req.Header.Set("apiKey", currentTest.ApiKey) - } - - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - mainRouter.ServeHTTP(rec, req) - - s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) - s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) - }) - } -} diff --git a/server/api/router/main_transaction_test.go b/server/api/router/main_transaction_test.go new file mode 100644 index 0000000..5d9dcac --- /dev/null +++ b/server/api/router/main_transaction_test.go @@ -0,0 +1,535 @@ +package router + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/go-webauthn/webauthn/protocol" + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/api/dto/request" + "github.com/teamhanko/passkey-server/api/dto/response" + "github.com/teamhanko/passkey-server/config" + "net/http" + "net/http/httptest" + "strings" +) + +func (s *mainRouterSuite) TestMainRouter_Transaction_List() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantId string + UserId string + ApiKey string + + SkipApiKey bool + SimulateBrokenDB bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `[{"id":"`, + }, + { + Name: "missing user id", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "Not Found", + }, + { + Name: "malformed user id", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "malformed", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "invalid user id", + }, + { + Name: "unknown user", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "000000000-0000-0000-0000-000000000000", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "invalid user id", + }, + { + Name: "missing api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", + SkipApiKey: true, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "malformed api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", + ApiKey: "malformed", + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "malformed tenant", + TenantId: "malformed", + UserId: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "missing tenant", + UserId: "b4fc06d2-2651-47e9-b1c3-3ba19ade9375", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "broken db", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + SimulateBrokenDB: true, + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/transaction", + }) + s.Require().NoError(err) + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/%s/transaction/%s", currentTest.TenantId, currentTest.UserId), nil) + + if !currentTest.SkipApiKey { + req.Header.Set("apiKey", currentTest.ApiKey) + } + + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + mainRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + var transactions []response.TransactionDto + err = json.Unmarshal(rec.Body.Bytes(), &transactions) + s.Require().NoError(err) + + s.Assert().Len(transactions, 2) + } + }) + } +} + +func (s *mainRouterSuite) TestMainRouter_Transaction_Init() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantId string + UserId string + ApiKey string + RequestBody interface{} + + SkipApiKey bool + SimulateBrokenDB bool + OmitRequestBody bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitTransactionDto{ + UserId: "test-passkey", + TransactionId: "00000001-0001-0001-0001-000000000001", + TransactionData: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + }, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `"publicKey":{"challenge":`, + }, + { + Name: "missing user id", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitTransactionDto{ + TransactionId: "00000001-0001-0001-0001-000000000001", + TransactionData: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + }, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "user_id is a required field", + }, + { + Name: "missing transaction id", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitTransactionDto{ + UserId: "test-passkey", + TransactionData: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + }, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "transaction_id is a required field", + }, + { + Name: "transaction id to long", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitTransactionDto{ + UserId: "test-passkey", + TransactionId: "00000001-0001-0001-0001-000000000001--00000001-0001-0001-0001-000000000001--00000001-0001-0001-0001-000000000001--00000001-0001-0001-0001-000000000001", + TransactionData: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + }, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "transaction_id cannot have more than 128 entries", + }, + { + Name: "missing transaction data", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitTransactionDto{ + UserId: "test-passkey", + TransactionId: "00000001-0001-0001-0001-000000000001", + }, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "transaction_data is a required field", + }, + { + Name: "malformed request body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "malformed", + }, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "user_id is a required field and transaction_id is a required field and transaction_data is a required field", + }, + { + Name: "missing request body", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + OmitRequestBody: true, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "user_id is a required field and transaction_id is a required field and transaction_data is a required field", + }, + { + Name: "missing api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + RequestBody: request.InitTransactionDto{ + UserId: "test-passkey", + TransactionId: "00000001-0001-0001-0001-000000000001", + TransactionData: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + }, + SkipApiKey: true, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "unknown user", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitTransactionDto{ + UserId: "4Yx3hvHKiJgqq3BRsmY-5zDzS52GSKcQpWumEl5aF-E", + TransactionId: "00000001-0001-0001-0001-000000000001", + TransactionData: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + }, + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "unable to find user", + }, + { + Name: "malformed tenant", + TenantId: "malformed", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitTransactionDto{ + UserId: "test-passkey", + TransactionId: "00000001-0001-0001-0001-000000000001", + TransactionData: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + }, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "missing tenant", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitTransactionDto{ + UserId: "test-passkey", + TransactionId: "00000001-0001-0001-0001-000000000001", + TransactionData: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + }, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "broken db", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: request.InitTransactionDto{ + UserId: "test-passkey", + TransactionId: "00000001-0001-0001-0001-000000000001", + TransactionData: struct { + Lorem string `json:"lorem"` + }{ + Lorem: "Ipsum", + }, + }, + SimulateBrokenDB: true, + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedStatusMessage: "Internal Server Error", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/transaction", + }) + s.Require().NoError(err) + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/transaction/initialize", currentTest.TenantId), nil) + } else { + body, err := json.Marshal(currentTest.RequestBody) + s.Require().NoError(err) + + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/transaction/initialize", currentTest.TenantId), bytes.NewReader(body)) + } + + if !currentTest.SkipApiKey { + req.Header.Set("apiKey", currentTest.ApiKey) + } + + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + mainRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + + if rec.Code == http.StatusOK { + var ca protocol.CredentialAssertion + err = json.Unmarshal(rec.Body.Bytes(), &ca) + s.Require().NoError(err) + + sessionData, err := s.Storage.GetWebauthnSessionDataPersister(nil).GetByChallenge(ca.Response.Challenge.String(), uuid.FromStringOrNil(currentTest.TenantId)) + s.Require().NoError(err) + s.Assert().False(sessionData.IsDiscoverable) + } + }) + } +} + +func (s *mainRouterSuite) TestMainRouter_Transaction_Finish() { + s.SkipOnShort() + + tests := []struct { + Name string + TenantId string + UserId string + ApiKey string + RequestBody string + + SkipApiKey bool + SimulateBrokenDB bool + OmitRequestBody bool + + ExpectedStatusCode int + ExpectedStatusMessage string + }{ + { + Name: "success", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey-discover", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRHlwQUlpODVuclVlWkR4THltMkNqeEhRRE1kRzNNTmRiaU13eXVKZ0VDZmh5ZVJsY3dDMEV5Z1ZFb2FEbDkxYlJVM1hLYWMwSnZLbU5oMi1XM01MV3ciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEYCIQDUYA_5bvdEf0LT5Xq2qMQvEKUflVBd5pcl2wkz87KlYgIhAO3XNLPOSwffYdnwht5h3pJTexxc-KwuLpsCSsWCfbLc","userHandle":"dGVzdC1wYXNza2V5"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusOK, + ExpectedStatusMessage: `{"token":"`, + }, + { + Name: "wrong tenant", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396f", + UserId: "a1B2c3D4", + ApiKey: "bXta7uDpBt6nBn4j2sX7vm1KGqp7ma9GXPx835IiEdnJxWu_HA-rCdmdTk7I9oYQnYo4MebHkQ3khSosXJ6y5A==", + RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "failed to get transaction data", + }, + { + Name: "tenant not found", + TenantId: "00000000-0000-0000-0000-000000000000", + UserId: "a1B2c3D4", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusNotFound, + ExpectedStatusMessage: "tenant not found", + }, + { + Name: "malformed tenant", + TenantId: "malformed", + UserId: "a1B2c3D4", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","rawId":"AINIobIYxCyd9-4CtiRLM2T7qiR0QRyG28yg6XrgaOY","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoieFVpRmNLdlY3V1RQRW1FdWdTVERkWFpkcjc2RnBabC0ycVpDczBodmFodyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCICR40uXfNpSwlTIxuNBWRvRU8UTMFJcsUi9WCNzDhKqrAiEA87nlHkGPNYVoFvDe3NeODAj_EQ7auL00G8kmYjvq62U","userHandle":"YTFCMmMzRDQ"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedStatusMessage: "tenant_id must be a valid uuid4", + }, + { + Name: "success with missing api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + RequestBody: `{"type":"public-key","id":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","rawId":"af5_nnDDP1eN3BPORT5cDfbSwfiPGy-9j85KdB3WQ6w","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVWljUDNmb0dQZmFoZF9kdVlHbnVCeVNtVldhUGw5VkNOSzdCNDJpM2Z4ayIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIF-LTHRYEHX4r9pWj39-o2NglZV8U3wGvOkmT-KH2ecjAiEA_1dYDRgiJsx7_1i9kIX8YWqzUlOzzHlz9HJgj0dGPmQ","userHandle":"dGVzdC1wYXNza2V5LWRpc2NvdmVy"},"clientExtensionResults":{}}`, + SkipApiKey: true, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "malformed api key", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "test-passkey", + ApiKey: "malformed", + RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiMEw3bTUyTmVEM0xpMXFteVhUMVp4Mk5nQ1lnNEstQTNiamQ4TzA5dmxVMCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIQCzxd6TRox_Dx4lOPrLYpfj4umnx4aNPZ1Tg7QA4OZtWwIgMSPL_oMUENDy1I5rGSmUjNs73eDtOVTq_D6wNs4qeDE","userHandle":"dGVzdC1wYXNza2V5"},"clientExtensionResults":{}}`, + SkipApiKey: false, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "The api key is invalid", + }, + { + Name: "wrong user handle", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "a1B2c3D4", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRHlwQUlpODVuclVlWkR4THltMkNqeEhRRE1kRzNNTmRiaU13eXVKZ0VDZmh5ZVJsY3dDMEV5Z1ZFb2FEbDkxYlJVM1hLYWMwSnZLbU5oMi1XM01MV3ciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEYCIQDUYA_5bvdEf0LT5Xq2qMQvEKUflVBd5pcl2wkz87KlYgIhAO3XNLPOSwffYdnwht5h3pJTexxc-KwuLpsCSsWCfbLc","userHandle":"dGVzdA=="},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "failed to get user by user handle", + }, + { + Name: "wrong credential", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "a1B2c3D4", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","rawId":"9QEeUpkDJqEy4sa7JUe1PjpYMSO4nQQNN9X-kK0wTFQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZGNqSmJtSDh3eHVCZ1p3QXdzeF84WFFuSFgxYlJCVjFoWHo3TDJ0UF91QSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEUCIG9UopCXxa8o_Lk_-fJRgzV-6Bi8-DlTjkvAQYFpKZliAiEAgbZe4h24DHpzqKhk84LuQJmOHiXAuQz67fbo8DGJZZ8","userHandle":"dGVzdC1wYXNza2V5LWRpc2NvdmVy"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "received challenge does not match with any stored one", + }, + { + Name: "fail to use mfa credential", + TenantId: "6eb4710c-72df-4941-984d-f2cf3dbe396e", + UserId: "mfa-test", + ApiKey: "d3917w2_RXsixVaJn2QZn4BmqrRs-G_rNmTTA2Few_lxXMNzv_7aI1uJCg_mJp7h5PdstRSD5LTrvWfwEF0PNg==", + RequestBody: `{"type":"public-key","id":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","rawId":"x_ofcjYOjqIIfJWBTlWbk62Bi37v-myfpr1Dhrs0-VQ","authenticatorAttachment":"platform","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRHlwQUlpODVuclVlWkR4THltMkNqeEhRRE1kRzNNTmRiaU13eXVKZ0VDZmh5ZVJsY3dDMEV5Z1ZFb2FEbDkxYlJVM1hLYWMwSnZLbU5oMi1XM01MV3ciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA","signature":"MEYCIQDUYA_5bvdEf0LT5Xq2qMQvEKUflVBd5pcl2wkz87KlYgIhAO3XNLPOSwffYdnwht5h3pJTexxc-KwuLpsCSsWCfbLc","userHandle":"dGVzdC1wYXNza2V5"},"clientExtensionResults":{}}`, + ExpectedStatusCode: http.StatusUnauthorized, + ExpectedStatusMessage: "Unable to find the credential for the returned credential ID", + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.Name, func() { + err := s.LoadMultipleFixtures([]string{ + "../../test/fixtures/common", + "../../test/fixtures/main_router/transaction", + }) + s.Require().NoError(err) + + mainRouter := NewMainRouter(&config.Config{}, s.Storage, nil) + + if currentTest.SimulateBrokenDB { + err := s.Storage.MigrateDown(-1) + s.Require().NoError(err) + } + + var req *http.Request + if currentTest.OmitRequestBody { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/transaction/finalize", currentTest.TenantId), nil) + } else { + req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/transaction/finalize", currentTest.TenantId), strings.NewReader(currentTest.RequestBody)) + } + + if !currentTest.SkipApiKey { + req.Header.Set("apiKey", currentTest.ApiKey) + } + + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + mainRouter.ServeHTTP(rec, req) + + s.Assert().Equal(currentTest.ExpectedStatusCode, rec.Code) + s.Assert().Contains(rec.Body.String(), currentTest.ExpectedStatusMessage) + }) + } +} diff --git a/server/api/services/admin/secret_service.go b/server/api/services/admin/secret_service.go index 35af295..c558034 100644 --- a/server/api/services/admin/secret_service.go +++ b/server/api/services/admin/secret_service.go @@ -51,6 +51,16 @@ func (ses *secretService) Create(dto request.CreateSecretDto, isApiSecret bool) return nil, err } + foundSecret, err := ses.secretPersister.GetByName(secret.Name, secret.IsAPISecret) + if err != nil { + ses.logger.Error(err) + return nil, err + } + + if foundSecret.ID != uuid.Nil { + return nil, echo.NewHTTPError(http.StatusConflict, "Secret with this name already exists") + } + err = ses.secretPersister.Create(secret) if err != nil { ses.logger.Error(err) diff --git a/server/persistence/persisters/secrets_persister.go b/server/persistence/persisters/secrets_persister.go index 9ce399d..9b12e75 100644 --- a/server/persistence/persisters/secrets_persister.go +++ b/server/persistence/persisters/secrets_persister.go @@ -1,6 +1,8 @@ package persisters import ( + "database/sql" + "errors" "fmt" "github.com/gobuffalo/pop/v6" @@ -8,6 +10,7 @@ import ( ) type SecretsPersister interface { + GetByName(name string, isApiSecret bool) (*models.Secret, error) Create(secret *models.Secret) error Delete(secret *models.Secret) error Update(secret *models.Secret) error @@ -21,6 +24,21 @@ func NewSecretsPersister(database *pop.Connection) SecretsPersister { return &secretsPersister{database: database} } +func (sp secretsPersister) GetByName(name string, isApiSecret bool) (*models.Secret, error) { + secret := &models.Secret{} + err := sp.database.Where("name = ? AND is_api_secret = ?", name, isApiSecret).First(secret) + + if err != nil && errors.Is(err, sql.ErrNoRows) { + return secret, nil + } + + if err != nil { + return nil, fmt.Errorf("failed to get secrets: %w", err) + } + + return secret, nil +} + func (sp secretsPersister) Create(secret *models.Secret) error { validationErr, err := sp.database.ValidateAndCreate(secret) if err != nil { diff --git a/server/persistence/persisters/webauthn_user_persister.go b/server/persistence/persisters/webauthn_user_persister.go index 7481d11..a95aa60 100644 --- a/server/persistence/persisters/webauthn_user_persister.go +++ b/server/persistence/persisters/webauthn_user_persister.go @@ -46,7 +46,7 @@ func (p *webauthnUserPersister) AllForTenant(tenantId uuid.UUID, page int, perPa webauthnUsers := models.WebauthnUsers{} err := p.database. Where("tenant_id = ?", tenantId). - Order(fmt.Sprintf("webauthn_users.created_at %s", sort)). + Order(fmt.Sprintf("created_at %s", sort)). Paginate(page, perPage). All(&webauthnUsers) diff --git a/server/test/fixtures/admin_router/audit_logs.yaml b/server/test/fixtures/admin_router/audit_logs.yaml new file mode 100644 index 0000000..c36b7a6 --- /dev/null +++ b/server/test/fixtures/admin_router/audit_logs.yaml @@ -0,0 +1,30 @@ +- id: 728e8991-2f32-497a-b05a-e61b97999c44 + type: webauthn_transaction_init_succeeded + meta_http_request_id: atjAIuTpOGAQmzRNsCxQCEXwREvECCKR + meta_source_ip: 192.168.65.1 + meta_user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" + actor_user_id: test-passkey + tenant_id: 6eb4710c-72df-4941-984d-f2cf3dbe396e + created_at: 2024-05-21 23:59:59 + updated_at: 2024-05-21 23:59:59 + transaction_id: 681a8dda-81f4-42f6-96eb-e46709290043 + +- id: 728e8991-2f32-497a-b05a-e61b97999c45 + type: webauthn_authentication_init_succeeded + meta_http_request_id: SECOND_REQUEST_ID + meta_source_ip: 192.168.65.1 + meta_user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" + actor_user_id: test-passkey + tenant_id: 6eb4710c-72df-4941-984d-f2cf3dbe396e + created_at: 2024-05-21 23:59:59 + updated_at: 2024-05-21 23:59:59 + +- id: 728e8991-2f32-497a-b05a-e61b97999c46 + type: webauthn_authentication_init_succeeded + meta_http_request_id: ANOTHER_TENANT_ID + meta_source_ip: 192.168.65.1 + meta_user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" + actor_user_id: test-passkey + tenant_id: 6eb4710c-72df-4941-984d-f2cf3dbe396f + created_at: 2024-05-21 23:59:59 + updated_at: 2024-05-21 23:59:59 \ No newline at end of file diff --git a/server/test/fixtures/admin_router/webauthn_users.yaml b/server/test/fixtures/admin_router/webauthn_users.yaml new file mode 100644 index 0000000..99739cd --- /dev/null +++ b/server/test/fixtures/admin_router/webauthn_users.yaml @@ -0,0 +1,35 @@ +- id: b4fc06d2-2651-47e9-b1c3-3ba19ade9375 + user_id: test-passkey + name: passkey + icon: "" + display_name: "Test Passkey" + tenant_id: 6eb4710c-72df-4941-984d-f2cf3dbe396e + created_at: 2024-05-21 23:59:59 + updated_at: 2024-05-21 23:59:59 + +- id: 4b6a4b07-47ad-40a8-84a6-ea20ee7a6547 + user_id: test-passkey-2 + name: passkey-2 + icon: "" + display_name: "Test Passkey 2" + tenant_id: 6eb4710c-72df-4941-984d-f2cf3dbe396e + created_at: 2024-05-21 23:59:59 + updated_at: 2024-05-21 23:59:59 + +- id: 66684bba-943f-480b-afa9-6184194bb16f + user_id: test-mfa-key + name: test-mfa-key + icon: "" + display_name: "Test MFA" + tenant_id: 6eb4710c-72df-4941-984d-f2cf3dbe396e + created_at: 2024-05-21 23:59:59 + updated_at: 2024-05-21 23:59:59 + +- id: 2808fdb6-6547-43dd-933f-2750293a2ab6 + user_id: another-tenant-passkey + name: another-tenant + icon: "" + display_name: "Another Tenant Passkey" + tenant_id: 6eb4710c-72df-4941-984d-f2cf3dbe396f + created_at: 2024-05-21 23:59:59 + updated_at: 2024-05-21 23:59:59 \ No newline at end of file diff --git a/server/test/fixtures/main_router/common/configs.yaml b/server/test/fixtures/common/configs.yaml similarity index 100% rename from server/test/fixtures/main_router/common/configs.yaml rename to server/test/fixtures/common/configs.yaml diff --git a/server/test/fixtures/main_router/common/cors.yaml b/server/test/fixtures/common/cors.yaml similarity index 100% rename from server/test/fixtures/main_router/common/cors.yaml rename to server/test/fixtures/common/cors.yaml diff --git a/server/test/fixtures/main_router/common/cors_origins.yaml b/server/test/fixtures/common/cors_origins.yaml similarity index 100% rename from server/test/fixtures/main_router/common/cors_origins.yaml rename to server/test/fixtures/common/cors_origins.yaml diff --git a/server/test/fixtures/main_router/common/jwks.yaml b/server/test/fixtures/common/jwks.yaml similarity index 98% rename from server/test/fixtures/main_router/common/jwks.yaml rename to server/test/fixtures/common/jwks.yaml index 6e7fdad..3f0a8ab 100644 --- a/server/test/fixtures/main_router/common/jwks.yaml +++ b/server/test/fixtures/common/jwks.yaml @@ -1,14 +1,11 @@ -- id: 1 - key_data: S1kOOSI6Ft5DqxDJr6BUsrM3BEee5Udb6hSS98lkcj3v6nD8P5eWRRMrzlVWjWVp5GgMcTgMxiooPb6yC_psU-UgUZ1xSxyIrj7rlqh2KRYWexT2LLGWZjZLFdUgfijpMyF7gKqdOLDrZCucvwdZu383nI_O7gJg3EUdQq_0qyN900LDFCEK3Gy6OF5iBos31NR5N7X19oAUa8F-o3FmY5HNRa3YwsO_oIF_2fS9GI7yhUiui3rbLKbv-OWv9HeWZrjt0xl3AyV7N6GRQPXMuNJBgh6ateAyoXYXYf-6wcRFKEUGFvTt0eyYNQDPDjGALyovmC_R8dCgTrhaCj7c8w3-hrQ_Yh9mfholbECMe7GZWrLVyGzklC19_KQE8yfpcqSJgAst7uu7qCtq39e3OUhLcPb2sjI6YA_Xh3wP5BmJ-c933uGW13mf48GSbIQx_LbOQV-UQVfsJOdtmCWefZTHwg6gP2LzYAHQeWXzY_22OCKq8OFYvmJoCbb1kNCU36qyu2hs7mirrOmpSF6zqK8uTZQJiiPj4lze1o46t033LAllRHI9E4qTqSvy5qifiK2FHsPuKozwo1yNJpranXgYORcWid4u1_9QZNo_x9GFvAY3aVAzKuIME0qumuZImUukfX4hxUQ0G3LbCrHZa_SSuWrwXt0HP5xlZ2cSI087f_xfz2M_ssbMn5Isejf58Je4GOuU46ZJXDN3bFGjT_LLtYutAkkPxrtILEtoQ01hpTZkN05W33sw6_x4fG0AVN7apRYh0JqeiJGcqIntW5w2erMs_TCTc1XGOmqRapbhR-su2qDByPj68BZOmNQSdCwdDdDRT58Q6SwyAOYt2jBfYeMGhYNj47vZiYpGJL3oxujML5dwYQDcuq9ibjxA4ti_vVjv3DtAwff5Shc64NN7PEuzaOPWh1lDxiasjkv4ozUuZBJ61ix23MnY2uzz5za9X8zeb5S97tspUFxN9dXP_Yt6-CCacgacHPq_S4oGZhnfo325QHF0GZiuSd1y3L7cgy3Payq6rJr9ITiZT9_5MLFuuEUwlO5QRHxs0zG64IzenV8JaCm0-HsSQ7b7vOee6hY29wJHWi-VsQVZX2g6oa-Sax-YuBqU-OTsjq9OzXw358hmspdfnnQHRcRvALoTaASDXxxIv3PAHPO4KrHwtgKs25ax2vuzikO8kjN4GKfp8TxAKHh7Bp1DPUnmX_q2AKJn7XnVI2kaS92-4WXr8N0akO07fYwO-iMgGNIwAaxKK7eU18SN0Vn3vzT0hjXiaV5HmDre0hAetLFD3O_dFrPFCyziTchgBicOPyzsIVJrRAEJF6UGj1PLfBMO2SynTyIvg34sxDNNveSL-D7F_NkDCXm6qdswJpauf6CfgoYlIO6gwoDnyGObXWMLI9ztTzk13L9uoDbrXTgPiMrLKjRG85J8wK4LbKWD0-jmxVmQebAsXRnOsM3xbh0VPVtikvkxKcWIsre9ySrnm5GKMI2IIe0qUtkuJU04t3z9Ui7LiV7wPZ2V-oxjlNjWVus_M1EvNaiBaZl4xxt94yub6y7GLhL6xoQhkyW24DSitYpDDXVBxXRuBTRh4UPBRvdxDvn1ZHlTjbGCeBqjEA7OFtbGYm1B8Ieo_i-UWRWmN07f-vvQzlCBB8TTppG9Lzl4rs2PiWVnWfXorIGnz18o7yNIIgmB4UTqaFj_WHMQgCuOsDxqxfNxwNx-NIpEQOl9zXW8xlCVimgGTFUfLx7aBFbQF3UJGLXws2fy-R8WgibSmQk_7-wMZd9U3XL09jhPSdaXsyHvRi8XVnvGUwGLbVTZzH17A-D_QEwc4OMbKdxv1UOZJq_VsMP0OetJAJP3ApgQA-iwL5-_GdWPKIBnU9eNya5qDYPFKWK79JqG6T4h7UTVJBkc9OlW9iVEAsjp5HtOtb6ct8_Rr_YMjW558mywKywHLFWmSj6gRZX1-DNqK3Wt8KhY2R42jz5OzgOZ0u2MB0rMDFmWqXE029gN3oby0JrZ_MRxu_dNM7at4KZ8c3Ie5vDaLrnIOJtfnI9xuGpNoAcDkmK1KpaxDT-T0KGGBEC4xF7RsmFIobgedDJAMTXDQ64DLEA3SiFUDjXcafWs1xR0tztGxHqMTXUF95Nx41SO-Rk7jVt0cPlzj0Q2C0poUAi-YgSKqeFcXrawuXwXha1oUdRugVWI8sTYy3IXL4Zhk0HwFGK5QO7E1IOlbOtvhIAvRRsaHExRCAFtWnetS6i53DYD7gVJrpLWjXbwrXTW-UrHd6C2xt9qpIKYCr-yj1vPZMxRhyq_G8j8Sd_MmZih_awijpo9JA6DgXhP9b9CQMFCPauzQpVlCHk0h0Z7WeIbjNQ4rr0abS04P-Key8Q_4UmN-b8juHEjNMmwC6DCIGBNkMX-R-oZhi5mUdCAXeTemR9s0bZu--nSDMsWyvm8xMd5aXCwv71ak3EtoieNrNApJb6uUL601qeqIM3siLhZq7Vwkti8cE-RergpTRVPq0wUXJGUKV41DP4bMLwYmrRoUdUXGhJffbjfpX0HEdL1QIPyponabAY5KDmDoKWxjIT0trM3ZOALwFV0ELfLB71NqcmrmdvTGqdC_oGokVNaTFp_Nr8IPdDT_IPCuiDYUQDTSOFiS5vQAJpq2jT0a0V-HMF9z4KsjfvNvTutasDkr6m8LNPaxi-B6_6aSu4bgS7y9JvMdMNVN2gbfwDgQAldHxa6nxa-QVuyBWg_qYtE0sGA2da01rCRlx4HVN0zDipGDkYfsc0T49clfPeFC2QHeIHee5b-EIOdgjuYk6QrUrlKmm8lw90ojhnnV8JD4KTKFEJAf5hcKhvX0hDNvI7HH43qKXF0BT528PejTv7We-DIVxuK9EPntQI3U24kPgDN_ZwZtxXo0AhMCIcYj1u4l2x2uvhcitWRFVVcIEzYYNJrJwehTGDp2-JHbUVr_ul7jSlrSgo6y0IgROVcITL7JfdlgnyzJ7v3GMMvDp_spsWN5FZ7dfHiCZfke6kRUHxkGVY_Skgzbq1-ZsNeN6dp-ROqiBL8Xuh45Ym0a5gvRNzp01ES7HZLpsU2yPmesiZ0WF-48yZzmP_ZiGz0jtKaLFnVLvYItpG7zHXVbim_-y88077WwcFVDtWe90xtXoD2x4ktCyrNoJHtHVmxFMEKtV1FHHX5HZxbbnb0DfL6eFhC-rJUxZMrPVS30_n457sSs2XmZen245hMILKj5COU8D87d7K6VvckMt-0ulQL3vggudTmWIVrRr3EaLKw_jcp6xdK-hU7GbSzrUOlikEHAZ-irt-8-pvFDFxKtvkIFHrNzk9YCnI-D-18cP4FfripoiEYobPj1Kyu9uY0B9zhRgMAZAZR05wXUoe50e5CBnL2DxjgvuliF1rUCnzrD7WQW5z3PG6uUnKGTRbLXzA4BTek0aYQjbi8MR5d2fzbeXHXqh4n-yCVfqNCBxC86bqjMgLIst9olX394jbP-KMTpSsNcbuoAFb_ItH4XVCstavxtoMCai4iPSbxXWt4DHBVD40MaB5UcjFoFuvi92BNHdp2FvXwrsiEn39X3p0QIPpc6YLZNOz9dR8t8qs6vnTHgITm31OOY7AL2fxV1Dz1v8y4N7FE366QLKGIOBnHhTsPzANLC93-cSWJO0sudT7f60Bj5f8Swnf_4Gsl5Oa4brxNbTeFAx5BKz-m1okH3B7QcvPmGDVwE149-uzEKukEO6Tja0hEbW8064Qq7Qr6q_cVl2lz9a8K9f9dwQ1x0bzUub1zVWuqrjtC7eHPWtm7MXd7o37yfzme9NhE56DT2gszQe1iHEL8LPGpJehb_i7oVRMKI6yxMwctr8N-be1tNxuOnqsnKKVrO59LuRR6zbefEMtgi2605jjXvqhX2rBLogR9HO5lEsnTl27Gd6Kwfk-7bjaFKR7LLkD1rdkiHz60OvhawL9AB7aH6PLRIQhHcksF_aNsJQgszfetfuEbT572OZOqZ8b5NKINoUtgJy81lKIU_JyNIyAKnlO9nc6Ffg4wtYrwxiwwWxNHsptdsnI4bav1p_7tAez-KMqdjHwPYkEsQhtN5JRw2x4QnMa--v94yjWLtNhyvDWRB5Mt6-9g0YKiZ1edq-YXGWdiOGOpAuIeebUW-2gHhyFZMR9mC5l2KcH3OLkI06UVIC0Pgxb24wVD5p-nIO6wImmZZfa3CwblGKRIhMCRtzkg3Oqs7frxJ37QnrmRiH2pU2IjifH4YIQPm9hpCUq8VV0yhVt8VENc6UokGy12HpGPPaePJeEgRIDAsdo_Y6LV21AtQo3TtgzkrazPISoY0eqK4Bk0SXZF8pc= +- key_data: S1kOOSI6Ft5DqxDJr6BUsrM3BEee5Udb6hSS98lkcj3v6nD8P5eWRRMrzlVWjWVp5GgMcTgMxiooPb6yC_psU-UgUZ1xSxyIrj7rlqh2KRYWexT2LLGWZjZLFdUgfijpMyF7gKqdOLDrZCucvwdZu383nI_O7gJg3EUdQq_0qyN900LDFCEK3Gy6OF5iBos31NR5N7X19oAUa8F-o3FmY5HNRa3YwsO_oIF_2fS9GI7yhUiui3rbLKbv-OWv9HeWZrjt0xl3AyV7N6GRQPXMuNJBgh6ateAyoXYXYf-6wcRFKEUGFvTt0eyYNQDPDjGALyovmC_R8dCgTrhaCj7c8w3-hrQ_Yh9mfholbECMe7GZWrLVyGzklC19_KQE8yfpcqSJgAst7uu7qCtq39e3OUhLcPb2sjI6YA_Xh3wP5BmJ-c933uGW13mf48GSbIQx_LbOQV-UQVfsJOdtmCWefZTHwg6gP2LzYAHQeWXzY_22OCKq8OFYvmJoCbb1kNCU36qyu2hs7mirrOmpSF6zqK8uTZQJiiPj4lze1o46t033LAllRHI9E4qTqSvy5qifiK2FHsPuKozwo1yNJpranXgYORcWid4u1_9QZNo_x9GFvAY3aVAzKuIME0qumuZImUukfX4hxUQ0G3LbCrHZa_SSuWrwXt0HP5xlZ2cSI087f_xfz2M_ssbMn5Isejf58Je4GOuU46ZJXDN3bFGjT_LLtYutAkkPxrtILEtoQ01hpTZkN05W33sw6_x4fG0AVN7apRYh0JqeiJGcqIntW5w2erMs_TCTc1XGOmqRapbhR-su2qDByPj68BZOmNQSdCwdDdDRT58Q6SwyAOYt2jBfYeMGhYNj47vZiYpGJL3oxujML5dwYQDcuq9ibjxA4ti_vVjv3DtAwff5Shc64NN7PEuzaOPWh1lDxiasjkv4ozUuZBJ61ix23MnY2uzz5za9X8zeb5S97tspUFxN9dXP_Yt6-CCacgacHPq_S4oGZhnfo325QHF0GZiuSd1y3L7cgy3Payq6rJr9ITiZT9_5MLFuuEUwlO5QRHxs0zG64IzenV8JaCm0-HsSQ7b7vOee6hY29wJHWi-VsQVZX2g6oa-Sax-YuBqU-OTsjq9OzXw358hmspdfnnQHRcRvALoTaASDXxxIv3PAHPO4KrHwtgKs25ax2vuzikO8kjN4GKfp8TxAKHh7Bp1DPUnmX_q2AKJn7XnVI2kaS92-4WXr8N0akO07fYwO-iMgGNIwAaxKK7eU18SN0Vn3vzT0hjXiaV5HmDre0hAetLFD3O_dFrPFCyziTchgBicOPyzsIVJrRAEJF6UGj1PLfBMO2SynTyIvg34sxDNNveSL-D7F_NkDCXm6qdswJpauf6CfgoYlIO6gwoDnyGObXWMLI9ztTzk13L9uoDbrXTgPiMrLKjRG85J8wK4LbKWD0-jmxVmQebAsXRnOsM3xbh0VPVtikvkxKcWIsre9ySrnm5GKMI2IIe0qUtkuJU04t3z9Ui7LiV7wPZ2V-oxjlNjWVus_M1EvNaiBaZl4xxt94yub6y7GLhL6xoQhkyW24DSitYpDDXVBxXRuBTRh4UPBRvdxDvn1ZHlTjbGCeBqjEA7OFtbGYm1B8Ieo_i-UWRWmN07f-vvQzlCBB8TTppG9Lzl4rs2PiWVnWfXorIGnz18o7yNIIgmB4UTqaFj_WHMQgCuOsDxqxfNxwNx-NIpEQOl9zXW8xlCVimgGTFUfLx7aBFbQF3UJGLXws2fy-R8WgibSmQk_7-wMZd9U3XL09jhPSdaXsyHvRi8XVnvGUwGLbVTZzH17A-D_QEwc4OMbKdxv1UOZJq_VsMP0OetJAJP3ApgQA-iwL5-_GdWPKIBnU9eNya5qDYPFKWK79JqG6T4h7UTVJBkc9OlW9iVEAsjp5HtOtb6ct8_Rr_YMjW558mywKywHLFWmSj6gRZX1-DNqK3Wt8KhY2R42jz5OzgOZ0u2MB0rMDFmWqXE029gN3oby0JrZ_MRxu_dNM7at4KZ8c3Ie5vDaLrnIOJtfnI9xuGpNoAcDkmK1KpaxDT-T0KGGBEC4xF7RsmFIobgedDJAMTXDQ64DLEA3SiFUDjXcafWs1xR0tztGxHqMTXUF95Nx41SO-Rk7jVt0cPlzj0Q2C0poUAi-YgSKqeFcXrawuXwXha1oUdRugVWI8sTYy3IXL4Zhk0HwFGK5QO7E1IOlbOtvhIAvRRsaHExRCAFtWnetS6i53DYD7gVJrpLWjXbwrXTW-UrHd6C2xt9qpIKYCr-yj1vPZMxRhyq_G8j8Sd_MmZih_awijpo9JA6DgXhP9b9CQMFCPauzQpVlCHk0h0Z7WeIbjNQ4rr0abS04P-Key8Q_4UmN-b8juHEjNMmwC6DCIGBNkMX-R-oZhi5mUdCAXeTemR9s0bZu--nSDMsWyvm8xMd5aXCwv71ak3EtoieNrNApJb6uUL601qeqIM3siLhZq7Vwkti8cE-RergpTRVPq0wUXJGUKV41DP4bMLwYmrRoUdUXGhJffbjfpX0HEdL1QIPyponabAY5KDmDoKWxjIT0trM3ZOALwFV0ELfLB71NqcmrmdvTGqdC_oGokVNaTFp_Nr8IPdDT_IPCuiDYUQDTSOFiS5vQAJpq2jT0a0V-HMF9z4KsjfvNvTutasDkr6m8LNPaxi-B6_6aSu4bgS7y9JvMdMNVN2gbfwDgQAldHxa6nxa-QVuyBWg_qYtE0sGA2da01rCRlx4HVN0zDipGDkYfsc0T49clfPeFC2QHeIHee5b-EIOdgjuYk6QrUrlKmm8lw90ojhnnV8JD4KTKFEJAf5hcKhvX0hDNvI7HH43qKXF0BT528PejTv7We-DIVxuK9EPntQI3U24kPgDN_ZwZtxXo0AhMCIcYj1u4l2x2uvhcitWRFVVcIEzYYNJrJwehTGDp2-JHbUVr_ul7jSlrSgo6y0IgROVcITL7JfdlgnyzJ7v3GMMvDp_spsWN5FZ7dfHiCZfke6kRUHxkGVY_Skgzbq1-ZsNeN6dp-ROqiBL8Xuh45Ym0a5gvRNzp01ES7HZLpsU2yPmesiZ0WF-48yZzmP_ZiGz0jtKaLFnVLvYItpG7zHXVbim_-y88077WwcFVDtWe90xtXoD2x4ktCyrNoJHtHVmxFMEKtV1FHHX5HZxbbnb0DfL6eFhC-rJUxZMrPVS30_n457sSs2XmZen245hMILKj5COU8D87d7K6VvckMt-0ulQL3vggudTmWIVrRr3EaLKw_jcp6xdK-hU7GbSzrUOlikEHAZ-irt-8-pvFDFxKtvkIFHrNzk9YCnI-D-18cP4FfripoiEYobPj1Kyu9uY0B9zhRgMAZAZR05wXUoe50e5CBnL2DxjgvuliF1rUCnzrD7WQW5z3PG6uUnKGTRbLXzA4BTek0aYQjbi8MR5d2fzbeXHXqh4n-yCVfqNCBxC86bqjMgLIst9olX394jbP-KMTpSsNcbuoAFb_ItH4XVCstavxtoMCai4iPSbxXWt4DHBVD40MaB5UcjFoFuvi92BNHdp2FvXwrsiEn39X3p0QIPpc6YLZNOz9dR8t8qs6vnTHgITm31OOY7AL2fxV1Dz1v8y4N7FE366QLKGIOBnHhTsPzANLC93-cSWJO0sudT7f60Bj5f8Swnf_4Gsl5Oa4brxNbTeFAx5BKz-m1okH3B7QcvPmGDVwE149-uzEKukEO6Tja0hEbW8064Qq7Qr6q_cVl2lz9a8K9f9dwQ1x0bzUub1zVWuqrjtC7eHPWtm7MXd7o37yfzme9NhE56DT2gszQe1iHEL8LPGpJehb_i7oVRMKI6yxMwctr8N-be1tNxuOnqsnKKVrO59LuRR6zbefEMtgi2605jjXvqhX2rBLogR9HO5lEsnTl27Gd6Kwfk-7bjaFKR7LLkD1rdkiHz60OvhawL9AB7aH6PLRIQhHcksF_aNsJQgszfetfuEbT572OZOqZ8b5NKINoUtgJy81lKIU_JyNIyAKnlO9nc6Ffg4wtYrwxiwwWxNHsptdsnI4bav1p_7tAez-KMqdjHwPYkEsQhtN5JRw2x4QnMa--v94yjWLtNhyvDWRB5Mt6-9g0YKiZ1edq-YXGWdiOGOpAuIeebUW-2gHhyFZMR9mC5l2KcH3OLkI06UVIC0Pgxb24wVD5p-nIO6wImmZZfa3CwblGKRIhMCRtzkg3Oqs7frxJ37QnrmRiH2pU2IjifH4YIQPm9hpCUq8VV0yhVt8VENc6UokGy12HpGPPaePJeEgRIDAsdo_Y6LV21AtQo3TtgzkrazPISoY0eqK4Bk0SXZF8pc= created_at: 2024-05-21 23:59:59 tenant_id: 6eb4710c-72df-4941-984d-f2cf3dbe396e -- id: 2 - key_data: EvHXh1kamz8UuTGimGqhUhAeNHgbaAFTsC2Dnx8OtMRFbx0fyWUOADh4F7vnJbNgfrLyLjyRJkgc0EHYhqr9gh3VFzAW5KwxaUpA4wswDSfyg1hByhiegzS10PftcBYJ2KJrNL3DpE-WtJZ0HJGaKy0jRpXGfAdG98s3-QPen02yieNx_HumXyE_GifyREJgQcc4pK1w-unfBTznZ17G8Z534wyHeHkwqmgTndaaCVYj88wRSzSt1M4O7EzTaAl0j7ehqSN9qbP4AxQLMvaYSBJhsfyk_f-4uCt5M4vU1JsQbIZ6bQuHluHf4UgZ-IvE9SnpM3lw2WvcnDndVpVaH89LlIcKYrLlh-P9boEStcsbkheUyc9UP3G1-TIQ-9CDBcbiJvNu07YYsX6Xou5U--oD-EmbqUGYpakLXIWnbVv3cE3D61I4dCt-E0pUaTeJACA_LRC7AhIu-J17IpKED5Rr7Jke6YrTWdt1luqvFFLrQUXSdIqIVpXb-pgz9zuHy4n7CwJo39dzwDrnR_LE0ukkX99VFyyB2du7I86aA-JLFGTKr-dCvokQjG5CeYQqmljMi0BFC8--K4ts6c5mIelB4xp9q_u7mRtLftxuuCNC7O9XGAWfMO9ys-QBeS3YICOL2YBpaMrOU7cSPYUjm3q2MPP1CQzOed2oEc3JJqLuKojOxJRp3UhJLTPA_AIY76odx3qL8kA-Hl3UUIpS2ElcG8NX8zFMpro7kwj9gm160-SFsoyOFAUav49y3RWv_4KlE5ZraKNCx1Sk62uObP27iHQjaqI4RIxRg4pcNYkJMGPtKW7kyGg64Fhh2NVOj59CpQf1jgBu5aaVlGn0-ojDNlRFQ-VdRDvHNKTAaRsVGnjH0I6CqpS-dJj_3WyWAcq6kr4NkKcT8F3u6eMRYSqJE2TGRugu6SsHfflLBsFW35ljdCpz-td6GIVTNFRK65VONfqhZufrqySVcQxODEMMNq1gIGOe11qABDE8Sbv9OaXzpaA6egKVvH2ujJtJjOtWFLZSqyfBMEYxa9HrpJXkBzIw5KfGjpIepuH1w5BShY7jXIE6MghwvyWtc9q4uXWfjKuI5wO_punX7jKxSi44kSgFtxRFKcX-uir_4vnCTPkoUA7hT5CJlOi_5jUY6UclxojM_7_yRVTH-8LqmjWAEgD1roycHLE0Yk3yvYJxw0_c4FCSKGfL2XrGaoQ1mYx1SLSq--sx49lz-M9mr8I7QqN-sXBvI5qYGAGFmMSZsmqyvI6AxKTW24lnxI8p5kcSCxIgXv0ySFol_TT1W-UbkP_v9VRnSwsuSJDw-xNFLS-4JOx04sw3DjXKL2f3OaeGfog3xVdo0EqtIXL2Vk2ylNUa0e0JCzL-zraOhZhmLOwgFpg8PAQUEpfGArsmRg2YXrdIaaC8YZKD_3B44lId1eKtJxWEj9JjETLUOvv8t7oVFtEvI8c-X6Jdd7RgoYMAURFUbibWlWNDASZzqT04AKkjoEZOkuXz8jUzodVQRrl7S1XmaduTHYJ4sg3-Hs0R07AA0CN9nu72utrLtE093JCWewMeNmC8OKiuscYkHCQBhh-zjqmc4bgg1G_bk45KUOa-HEn-VNTx0R7VQfdiGj2dxdPNN082pEXlo3GWB_BIBNAHNfDOatXE6SaL2bnYbssct92T9l-0YMQT-Cw2m592PwSf3Ihq5qQ24pcfGaN34JL11P24kWaBqv1ZH3cVjfKtJRJ4RxI_ZkeHtfIrM8ilGDwoxITg5eKcjXjfQDPNRm66SwgX-BKOhReA7QINcPJsD2t7uIBcxWiBJQ-tH_oQaxjX5HomWWW6Q6aBuehKMqyDiqAyWuaWTY70ONQ_E23rIQBpPHih9qW6rkcCNtp35suMsTFupTm_marZb5nJ8oZ9iqh-NVw3lV3Ts7zU3dPB211nFsw7oSEHgWYqMQzyBm2iX7KRRxSy_K_Vb_g41MZprqQSGXc5tjC0I6jbZzz8NsUBDAO7tHLmByyGpUrO8R0Zjxeopf0ERgyUDo8tcKRa4234szQ5D1cNgvzbgoZVkU1Uw3APK19ipxMNJcLbFRR2cFCTd76dW8sWVLYYFxWr5gB9sWl3t2EprGhO84j4ojZPsK_1Lzr0pQzlR0zXmOgQ_xuGB_pxLIks2kmUfRktVn_MyYo-5wqbgQtpDfLqR2r9N8qJQoFmpmvRb98WnUdzrqcvaFrB41DugVKLU4PctUqaRbbs7bKOzuZimaWiCTv8RqmyUdKjg6-d7Uemx5JKle-bjal2vNS5uUPfbYhADjoUNr5omrFpllnY-w-wJg6nolQwcdm438ONvYo-51QsnoRwy9VRJ8SokY3q3G6yuNERG5nfz_1M6YjBFS5Ytb-1dF9c4rVCXIGs5KaUiqTefeu9bxsLJb93d72JcYwFfq495yqtjiIPKE5aKYrTrALAQk_3QJq6nSHf7On-S4yBkAIDh3Fw3RwijkZt59FJgk116UKbnrYm0J0t6TtMdIz04vUNlapyf_c3023XmpIbsPYGDG6gf1c399wqpPF-EHwna9CPNbKvmvdI66LhfVk65TorM_bNNGjwMPX5w2C6yKHuf7_TxKsUArSy9m6hYJBLQutvuA5guhvTcTUAaiNJhOCSSAYJCPYD1oCaNDTFpml60-n_Y8BU5OeA8d5O6z8M6XM1JFgBU0wFRCojUcmcT4mYCV-rLX--ReHjaWD3Cpl7XnJKdsZWrX-t2VUHr4vqTDMDqysIVrGmgeSU3epB7NvPcWYn5mO3giXSumLUcwEe2A2QpvgWsfnuit2RCNWmF3E1WmbTRxbWMn1CK-ltnHazRIbWswyphSF8GU-jbCFGiTsfx1mJcKgZt2QeoTfmSODTbWnmy8bRl0JEKwQoQ4K0eAvCajwpJqx-ZFyWzk3JUxH4V_SS7jNrVOVW6VyU11FktGdmO8eLWqot4fhUTUDTBGnsByOG1Fh8TpplUsFgiG91SD2XMaWmu8btP2RUVhskAOgz1-DEMoCjigboUn4IWRL4e70iU8qT_9hjKvd9LDkh9jPFVdHfMFK0f7wbEESqeItmimruMEmyiu4xdAuqIwOpT71GlaEtOQnPNKNwZ1afDCoGz3sOA_kvfddpGXvMZRsMfnJxhcMTm-tcsN25UcTIay-_epkaZtkkeQPCq-gDpVRaJqgENzhRTCDO7G0VNpsgBEDcfyCjqxxWx650XkQAW8vuZYspLm6UI-OP3cJntsKY0L1cb3q7c8xVHrLUCWmSXWZjzsNXhOh0H2iUsWRMN-smnQS68AMjMzyAjDWOanjt1OU525ZuFvOIPKWorelAxPnxd9E4YG08n6dQuPiZ5NQuBZxkJkMdFigqOBqrA_yfuZfgwG4E2REEHdKizgXxe35wCBaWzfErSNx0qVe14g2_P8UYfRxiW45BQGdgqF6fXYhkIP2RdqsXq5cmMrF2IQte1YtHnFsX1A45u-a9i3QIfcpOAr7Fklb1nulJqH1pVnbmHktI4GnBenZUSZ2Kx3oU0NSuqr_2NuT7oagXWpv0o772cpaZdj_k0oDO4g5RtnEYBT-LBW8ahzao8-E9qrBpt9b1yS_Wdy-YaBYjBrY_FqikULeV50EJvrCWu9l7Q8x3zYx8eMypkFva6nV0ci1w_VSFDPRGll3avMu6NFPdJpy97vvsLE2SnHUKn3q3rIbHUZGuqtJuEamqUG937onj-e7HQrU9vfQZbp6rDLecTazpz0vgbV0pSIWQ-wUB6yy2dz8o0LSdOb2qvUWBRmaReeFxagltcZyBDRE2EIP7EsfMyGVQXN_T-4JtQvGL3duiPUFBapb_VpBITa36bd4ybFe3XjS8YWbb8EawPmLV8VStmxdywFTQh_9rzo3htZr9KI1Mt-qyq53VyWhvhCIAKAfQnVMeW88j7wZ1XL5qs3mcCJp_LX1eKlJrOFRGvSzDaVZpq0K15o6s-5f8suYfvMu_9D7Cg6tuIAWBZEMjZzlzgL-0PIdt1l4sBbfn6A2BXCVYA6DkkrGFZyrpud458QNXo5oFiRyOQEeyjS7auXGyCzHQB0efrTVFV08lSQ2rMgBTq0eyc7lQWLbIF5Xte3jNp_umNnt255NT0wfBBojcyABjmQPeVaLRgA41nKv9ww2aYAxhy4TFUE3glxH5OvWk1qdZBioNeo3vXivz1zazr6j_hi7tGYXLLvquccqwhXAaIXqXjb-2c0um5Cvip6ivd1Th28XzWj_C6yqpeC-v-olCUDAiLNLqaKU_FVQfv96t33ruS8vEeT65vxTKSn1JH94c4yeddZCdknW9HxLurBu3AB1llufz3kVG1-s= +- key_data: EvHXh1kamz8UuTGimGqhUhAeNHgbaAFTsC2Dnx8OtMRFbx0fyWUOADh4F7vnJbNgfrLyLjyRJkgc0EHYhqr9gh3VFzAW5KwxaUpA4wswDSfyg1hByhiegzS10PftcBYJ2KJrNL3DpE-WtJZ0HJGaKy0jRpXGfAdG98s3-QPen02yieNx_HumXyE_GifyREJgQcc4pK1w-unfBTznZ17G8Z534wyHeHkwqmgTndaaCVYj88wRSzSt1M4O7EzTaAl0j7ehqSN9qbP4AxQLMvaYSBJhsfyk_f-4uCt5M4vU1JsQbIZ6bQuHluHf4UgZ-IvE9SnpM3lw2WvcnDndVpVaH89LlIcKYrLlh-P9boEStcsbkheUyc9UP3G1-TIQ-9CDBcbiJvNu07YYsX6Xou5U--oD-EmbqUGYpakLXIWnbVv3cE3D61I4dCt-E0pUaTeJACA_LRC7AhIu-J17IpKED5Rr7Jke6YrTWdt1luqvFFLrQUXSdIqIVpXb-pgz9zuHy4n7CwJo39dzwDrnR_LE0ukkX99VFyyB2du7I86aA-JLFGTKr-dCvokQjG5CeYQqmljMi0BFC8--K4ts6c5mIelB4xp9q_u7mRtLftxuuCNC7O9XGAWfMO9ys-QBeS3YICOL2YBpaMrOU7cSPYUjm3q2MPP1CQzOed2oEc3JJqLuKojOxJRp3UhJLTPA_AIY76odx3qL8kA-Hl3UUIpS2ElcG8NX8zFMpro7kwj9gm160-SFsoyOFAUav49y3RWv_4KlE5ZraKNCx1Sk62uObP27iHQjaqI4RIxRg4pcNYkJMGPtKW7kyGg64Fhh2NVOj59CpQf1jgBu5aaVlGn0-ojDNlRFQ-VdRDvHNKTAaRsVGnjH0I6CqpS-dJj_3WyWAcq6kr4NkKcT8F3u6eMRYSqJE2TGRugu6SsHfflLBsFW35ljdCpz-td6GIVTNFRK65VONfqhZufrqySVcQxODEMMNq1gIGOe11qABDE8Sbv9OaXzpaA6egKVvH2ujJtJjOtWFLZSqyfBMEYxa9HrpJXkBzIw5KfGjpIepuH1w5BShY7jXIE6MghwvyWtc9q4uXWfjKuI5wO_punX7jKxSi44kSgFtxRFKcX-uir_4vnCTPkoUA7hT5CJlOi_5jUY6UclxojM_7_yRVTH-8LqmjWAEgD1roycHLE0Yk3yvYJxw0_c4FCSKGfL2XrGaoQ1mYx1SLSq--sx49lz-M9mr8I7QqN-sXBvI5qYGAGFmMSZsmqyvI6AxKTW24lnxI8p5kcSCxIgXv0ySFol_TT1W-UbkP_v9VRnSwsuSJDw-xNFLS-4JOx04sw3DjXKL2f3OaeGfog3xVdo0EqtIXL2Vk2ylNUa0e0JCzL-zraOhZhmLOwgFpg8PAQUEpfGArsmRg2YXrdIaaC8YZKD_3B44lId1eKtJxWEj9JjETLUOvv8t7oVFtEvI8c-X6Jdd7RgoYMAURFUbibWlWNDASZzqT04AKkjoEZOkuXz8jUzodVQRrl7S1XmaduTHYJ4sg3-Hs0R07AA0CN9nu72utrLtE093JCWewMeNmC8OKiuscYkHCQBhh-zjqmc4bgg1G_bk45KUOa-HEn-VNTx0R7VQfdiGj2dxdPNN082pEXlo3GWB_BIBNAHNfDOatXE6SaL2bnYbssct92T9l-0YMQT-Cw2m592PwSf3Ihq5qQ24pcfGaN34JL11P24kWaBqv1ZH3cVjfKtJRJ4RxI_ZkeHtfIrM8ilGDwoxITg5eKcjXjfQDPNRm66SwgX-BKOhReA7QINcPJsD2t7uIBcxWiBJQ-tH_oQaxjX5HomWWW6Q6aBuehKMqyDiqAyWuaWTY70ONQ_E23rIQBpPHih9qW6rkcCNtp35suMsTFupTm_marZb5nJ8oZ9iqh-NVw3lV3Ts7zU3dPB211nFsw7oSEHgWYqMQzyBm2iX7KRRxSy_K_Vb_g41MZprqQSGXc5tjC0I6jbZzz8NsUBDAO7tHLmByyGpUrO8R0Zjxeopf0ERgyUDo8tcKRa4234szQ5D1cNgvzbgoZVkU1Uw3APK19ipxMNJcLbFRR2cFCTd76dW8sWVLYYFxWr5gB9sWl3t2EprGhO84j4ojZPsK_1Lzr0pQzlR0zXmOgQ_xuGB_pxLIks2kmUfRktVn_MyYo-5wqbgQtpDfLqR2r9N8qJQoFmpmvRb98WnUdzrqcvaFrB41DugVKLU4PctUqaRbbs7bKOzuZimaWiCTv8RqmyUdKjg6-d7Uemx5JKle-bjal2vNS5uUPfbYhADjoUNr5omrFpllnY-w-wJg6nolQwcdm438ONvYo-51QsnoRwy9VRJ8SokY3q3G6yuNERG5nfz_1M6YjBFS5Ytb-1dF9c4rVCXIGs5KaUiqTefeu9bxsLJb93d72JcYwFfq495yqtjiIPKE5aKYrTrALAQk_3QJq6nSHf7On-S4yBkAIDh3Fw3RwijkZt59FJgk116UKbnrYm0J0t6TtMdIz04vUNlapyf_c3023XmpIbsPYGDG6gf1c399wqpPF-EHwna9CPNbKvmvdI66LhfVk65TorM_bNNGjwMPX5w2C6yKHuf7_TxKsUArSy9m6hYJBLQutvuA5guhvTcTUAaiNJhOCSSAYJCPYD1oCaNDTFpml60-n_Y8BU5OeA8d5O6z8M6XM1JFgBU0wFRCojUcmcT4mYCV-rLX--ReHjaWD3Cpl7XnJKdsZWrX-t2VUHr4vqTDMDqysIVrGmgeSU3epB7NvPcWYn5mO3giXSumLUcwEe2A2QpvgWsfnuit2RCNWmF3E1WmbTRxbWMn1CK-ltnHazRIbWswyphSF8GU-jbCFGiTsfx1mJcKgZt2QeoTfmSODTbWnmy8bRl0JEKwQoQ4K0eAvCajwpJqx-ZFyWzk3JUxH4V_SS7jNrVOVW6VyU11FktGdmO8eLWqot4fhUTUDTBGnsByOG1Fh8TpplUsFgiG91SD2XMaWmu8btP2RUVhskAOgz1-DEMoCjigboUn4IWRL4e70iU8qT_9hjKvd9LDkh9jPFVdHfMFK0f7wbEESqeItmimruMEmyiu4xdAuqIwOpT71GlaEtOQnPNKNwZ1afDCoGz3sOA_kvfddpGXvMZRsMfnJxhcMTm-tcsN25UcTIay-_epkaZtkkeQPCq-gDpVRaJqgENzhRTCDO7G0VNpsgBEDcfyCjqxxWx650XkQAW8vuZYspLm6UI-OP3cJntsKY0L1cb3q7c8xVHrLUCWmSXWZjzsNXhOh0H2iUsWRMN-smnQS68AMjMzyAjDWOanjt1OU525ZuFvOIPKWorelAxPnxd9E4YG08n6dQuPiZ5NQuBZxkJkMdFigqOBqrA_yfuZfgwG4E2REEHdKizgXxe35wCBaWzfErSNx0qVe14g2_P8UYfRxiW45BQGdgqF6fXYhkIP2RdqsXq5cmMrF2IQte1YtHnFsX1A45u-a9i3QIfcpOAr7Fklb1nulJqH1pVnbmHktI4GnBenZUSZ2Kx3oU0NSuqr_2NuT7oagXWpv0o772cpaZdj_k0oDO4g5RtnEYBT-LBW8ahzao8-E9qrBpt9b1yS_Wdy-YaBYjBrY_FqikULeV50EJvrCWu9l7Q8x3zYx8eMypkFva6nV0ci1w_VSFDPRGll3avMu6NFPdJpy97vvsLE2SnHUKn3q3rIbHUZGuqtJuEamqUG937onj-e7HQrU9vfQZbp6rDLecTazpz0vgbV0pSIWQ-wUB6yy2dz8o0LSdOb2qvUWBRmaReeFxagltcZyBDRE2EIP7EsfMyGVQXN_T-4JtQvGL3duiPUFBapb_VpBITa36bd4ybFe3XjS8YWbb8EawPmLV8VStmxdywFTQh_9rzo3htZr9KI1Mt-qyq53VyWhvhCIAKAfQnVMeW88j7wZ1XL5qs3mcCJp_LX1eKlJrOFRGvSzDaVZpq0K15o6s-5f8suYfvMu_9D7Cg6tuIAWBZEMjZzlzgL-0PIdt1l4sBbfn6A2BXCVYA6DkkrGFZyrpud458QNXo5oFiRyOQEeyjS7auXGyCzHQB0efrTVFV08lSQ2rMgBTq0eyc7lQWLbIF5Xte3jNp_umNnt255NT0wfBBojcyABjmQPeVaLRgA41nKv9ww2aYAxhy4TFUE3glxH5OvWk1qdZBioNeo3vXivz1zazr6j_hi7tGYXLLvquccqwhXAaIXqXjb-2c0um5Cvip6ivd1Th28XzWj_C6yqpeC-v-olCUDAiLNLqaKU_FVQfv96t33ruS8vEeT65vxTKSn1JH94c4yeddZCdknW9HxLurBu3AB1llufz3kVG1-s= created_at: 2024-05-21 23:59:59 tenant_id: 6eb4710c-72df-4941-984d-f2cf3dbe396f -- id: 3 - key_data: DvHXh1kamz8UuTGimGqhUhAeNHgbaAFTsC2Dnx8OtMRFbx0fyWUOADh4F7vnJbNgfrLyLjyRJkgc0EHYhqr9gh3VFzAW5KwxaUpA4wswDSfyg1hByhiegzS10PftcBYJ2KJrNL3DpE-WtJZ0HJGaKy0jRpXGfAdG98s3-QPen02yieNx_HumXyE_GifyREJgQcc4pK1w-unfBTznZ17G8Z534wyHeHkwqmgTndaaCVYj88wRSzSt1M4O7EzTaAl0j7ehqSN9qbP4AxQLMvaYSBJhsfyk_f-4uCt5M4vU1JsQbIZ6bQuHluHf4UgZ-IvE9SnpM3lw2WvcnDndVpVaH89LlIcKYrLlh-P9boEStcsbkheUyc9UP3G1-TIQ-9CDBcbiJvNu07YYsX6Xou5U--oD-EmbqUGYpakLXIWnbVv3cE3D61I4dCt-E0pUaTeJACA_LRC7AhIu-J17IpKED5Rr7Jke6YrTWdt1luqvFFLrQUXSdIqIVpXb-pgz9zuHy4n7CwJo39dzwDrnR_LE0ukkX99VFyyB2du7I86aA-JLFGTKr-dCvokQjG5CeYQqmljMi0BFC8--K4ts6c5mIelB4xp9q_u7mRtLftxuuCNC7O9XGAWfMO9ys-QBeS3YICOL2YBpaMrOU7cSPYUjm3q2MPP1CQzOed2oEc3JJqLuKojOxJRp3UhJLTPA_AIY76odx3qL8kA-Hl3UUIpS2ElcG8NX8zFMpro7kwj9gm160-SFsoyOFAUav49y3RWv_4KlE5ZraKNCx1Sk62uObP27iHQjaqI4RIxRg4pcNYkJMGPtKW7kyGg64Fhh2NVOj59CpQf1jgBu5aaVlGn0-ojDNlRFQ-VdRDvHNKTAaRsVGnjH0I6CqpS-dJj_3WyWAcq6kr4NkKcT8F3u6eMRYSqJE2TGRugu6SsHfflLBsFW35ljdCpz-td6GIVTNFRK65VONfqhZufrqySVcQxODEMMNq1gIGOe11qABDE8Sbv9OaXzpaA6egKVvH2ujJtJjOtWFLZSqyfBMEYxa9HrpJXkBzIw5KfGjpIepuH1w5BShY7jXIE6MghwvyWtc9q4uXWfjKuI5wO_punX7jKxSi44kSgFtxRFKcX-uir_4vnCTPkoUA7hT5CJlOi_5jUY6UclxojM_7_yRVTH-8LqmjWAEgD1roycHLE0Yk3yvYJxw0_c4FCSKGfL2XrGaoQ1mYx1SLSq--sx49lz-M9mr8I7QqN-sXBvI5qYGAGFmMSZsmqyvI6AxKTW24lnxI8p5kcSCxIgXv0ySFol_TT1W-UbkP_v9VRnSwsuSJDw-xNFLS-4JOx04sw3DjXKL2f3OaeGfog3xVdo0EqtIXL2Vk2ylNUa0e0JCzL-zraOhZhmLOwgFpg8PAQUEpfGArsmRg2YXrdIaaC8YZKD_3B44lId1eKtJxWEj9JjETLUOvv8t7oVFtEvI8c-X6Jdd7RgoYMAURFUbibWlWNDASZzqT04AKkjoEZOkuXz8jUzodVQRrl7S1XmaduTHYJ4sg3-Hs0R07AA0CN9nu72utrLtE093JCWewMeNmC8OKiuscYkHCQBhh-zjqmc4bgg1G_bk45KUOa-HEn-VNTx0R7VQfdiGj2dxdPNN082pEXlo3GWB_BIBNAHNfDOatXE6SaL2bnYbssct92T9l-0YMQT-Cw2m592PwSf3Ihq5qQ24pcfGaN34JL11P24kWaBqv1ZH3cVjfKtJRJ4RxI_ZkeHtfIrM8ilGDwoxITg5eKcjXjfQDPNRm66SwgX-BKOhReA7QINcPJsD2t7uIBcxWiBJQ-tH_oQaxjX5HomWWW6Q6aBuehKMqyDiqAyWuaWTY70ONQ_E23rIQBpPHih9qW6rkcCNtp35suMsTFupTm_marZb5nJ8oZ9iqh-NVw3lV3Ts7zU3dPB211nFsw7oSEHgWYqMQzyBm2iX7KRRxSy_K_Vb_g41MZprqQSGXc5tjC0I6jbZzz8NsUBDAO7tHLmByyGpUrO8R0Zjxeopf0ERgyUDo8tcKRa4234szQ5D1cNgvzbgoZVkU1Uw3APK19ipxMNJcLbFRR2cFCTd76dW8sWVLYYFxWr5gB9sWl3t2EprGhO84j4ojZPsK_1Lzr0pQzlR0zXmOgQ_xuGB_pxLIks2kmUfRktVn_MyYo-5wqbgQtpDfLqR2r9N8qJQoFmpmvRb98WnUdzrqcvaFrB41DugVKLU4PctUqaRbbs7bKOzuZimaWiCTv8RqmyUdKjg6-d7Uemx5JKle-bjal2vNS5uUPfbYhADjoUNr5omrFpllnY-w-wJg6nolQwcdm438ONvYo-51QsnoRwy9VRJ8SokY3q3G6yuNERG5nfz_1M6YjBFS5Ytb-1dF9c4rVCXIGs5KaUiqTefeu9bxsLJb93d72JcYwFfq495yqtjiIPKE5aKYrTrALAQk_3QJq6nSHf7On-S4yBkAIDh3Fw3RwijkZt59FJgk116UKbnrYm0J0t6TtMdIz04vUNlapyf_c3023XmpIbsPYGDG6gf1c399wqpPF-EHwna9CPNbKvmvdI66LhfVk65TorM_bNNGjwMPX5w2C6yKHuf7_TxKsUArSy9m6hYJBLQutvuA5guhvTcTUAaiNJhOCSSAYJCPYD1oCaNDTFpml60-n_Y8BU5OeA8d5O6z8M6XM1JFgBU0wFRCojUcmcT4mYCV-rLX--ReHjaWD3Cpl7XnJKdsZWrX-t2VUHr4vqTDMDqysIVrGmgeSU3epB7NvPcWYn5mO3giXSumLUcwEe2A2QpvgWsfnuit2RCNWmF3E1WmbTRxbWMn1CK-ltnHazRIbWswyphSF8GU-jbCFGiTsfx1mJcKgZt2QeoTfmSODTbWnmy8bRl0JEKwQoQ4K0eAvCajwpJqx-ZFyWzk3JUxH4V_SS7jNrVOVW6VyU11FktGdmO8eLWqot4fhUTUDTBGnsByOG1Fh8TpplUsFgiG91SD2XMaWmu8btP2RUVhskAOgz1-DEMoCjigboUn4IWRL4e70iU8qT_9hjKvd9LDkh9jPFVdHfMFK0f7wbEESqeItmimruMEmyiu4xdAuqIwOpT71GlaEtOQnPNKNwZ1afDCoGz3sOA_kvfddpGXvMZRsMfnJxhcMTm-tcsN25UcTIay-_epkaZtkkeQPCq-gDpVRaJqgENzhRTCDO7G0VNpsgBEDcfyCjqxxWx650XkQAW8vuZYspLm6UI-OP3cJntsKY0L1cb3q7c8xVHrLUCWmSXWZjzsNXhOh0H2iUsWRMN-smnQS68AMjMzyAjDWOanjt1OU525ZuFvOIPKWorelAxPnxd9E4YG08n6dQuPiZ5NQuBZxkJkMdFigqOBqrA_yfuZfgwG4E2REEHdKizgXxe35wCBaWzfErSNx0qVe14g2_P8UYfRxiW45BQGdgqF6fXYhkIP2RdqsXq5cmMrF2IQte1YtHnFsX1A45u-a9i3QIfcpOAr7Fklb1nulJqH1pVnbmHktI4GnBenZUSZ2Kx3oU0NSuqr_2NuT7oagXWpv0o772cpaZdj_k0oDO4g5RtnEYBT-LBW8ahzao8-E9qrBpt9b1yS_Wdy-YaBYjBrY_FqikULeV50EJvrCWu9l7Q8x3zYx8eMypkFva6nV0ci1w_VSFDPRGll3avMu6NFPdJpy97vvsLE2SnHUKn3q3rIbHUZGuqtJuEamqUG937onj-e7HQrU9vfQZbp6rDLecTazpz0vgbV0pSIWQ-wUB6yy2dz8o0LSdOb2qvUWBRmaReeFxagltcZyBDRE2EIP7EsfMyGVQXN_T-4JtQvGL3duiPUFBapb_VpBITa36bd4ybFe3XjS8YWbb8EawPmLV8VStmxdywFTQh_9rzo3htZr9KI1Mt-qyq53VyWhvhCIAKAfQnVMeW88j7wZ1XL5qs3mcCJp_LX1eKlJrOFRGvSzDaVZpq0K15o6s-5f8suYfvMu_9D7Cg6tuIAWBZEMjZzlzgL-0PIdt1l4sBbfn6A2BXCVYA6DkkrGFZyrpud458QNXo5oFiRyOQEeyjS7auXGyCzHQB0efrTVFV08lSQ2rMgBTq0eyc7lQWLbIF5Xte3jNp_umNnt255NT0wfBBojcyABjmQPeVaLRgA41nKv9ww2aYAxhy4TFUE3glxH5OvWk1qdZBioNeo3vXivz1zazr6j_hi7tGYXLLvquccqwhXAaIXqXjb-2c0um5Cvip6ivd1Th28XzWj_C6yqpeC-v-olCUDAiLNLqaKU_FVQfv96t33ruS8vEeT65vxTKSn1JH94c4yeddZCdknW9HxLurBu3AB1llufz3kVG1-s= +- key_data: DvHXh1kamz8UuTGimGqhUhAeNHgbaAFTsC2Dnx8OtMRFbx0fyWUOADh4F7vnJbNgfrLyLjyRJkgc0EHYhqr9gh3VFzAW5KwxaUpA4wswDSfyg1hByhiegzS10PftcBYJ2KJrNL3DpE-WtJZ0HJGaKy0jRpXGfAdG98s3-QPen02yieNx_HumXyE_GifyREJgQcc4pK1w-unfBTznZ17G8Z534wyHeHkwqmgTndaaCVYj88wRSzSt1M4O7EzTaAl0j7ehqSN9qbP4AxQLMvaYSBJhsfyk_f-4uCt5M4vU1JsQbIZ6bQuHluHf4UgZ-IvE9SnpM3lw2WvcnDndVpVaH89LlIcKYrLlh-P9boEStcsbkheUyc9UP3G1-TIQ-9CDBcbiJvNu07YYsX6Xou5U--oD-EmbqUGYpakLXIWnbVv3cE3D61I4dCt-E0pUaTeJACA_LRC7AhIu-J17IpKED5Rr7Jke6YrTWdt1luqvFFLrQUXSdIqIVpXb-pgz9zuHy4n7CwJo39dzwDrnR_LE0ukkX99VFyyB2du7I86aA-JLFGTKr-dCvokQjG5CeYQqmljMi0BFC8--K4ts6c5mIelB4xp9q_u7mRtLftxuuCNC7O9XGAWfMO9ys-QBeS3YICOL2YBpaMrOU7cSPYUjm3q2MPP1CQzOed2oEc3JJqLuKojOxJRp3UhJLTPA_AIY76odx3qL8kA-Hl3UUIpS2ElcG8NX8zFMpro7kwj9gm160-SFsoyOFAUav49y3RWv_4KlE5ZraKNCx1Sk62uObP27iHQjaqI4RIxRg4pcNYkJMGPtKW7kyGg64Fhh2NVOj59CpQf1jgBu5aaVlGn0-ojDNlRFQ-VdRDvHNKTAaRsVGnjH0I6CqpS-dJj_3WyWAcq6kr4NkKcT8F3u6eMRYSqJE2TGRugu6SsHfflLBsFW35ljdCpz-td6GIVTNFRK65VONfqhZufrqySVcQxODEMMNq1gIGOe11qABDE8Sbv9OaXzpaA6egKVvH2ujJtJjOtWFLZSqyfBMEYxa9HrpJXkBzIw5KfGjpIepuH1w5BShY7jXIE6MghwvyWtc9q4uXWfjKuI5wO_punX7jKxSi44kSgFtxRFKcX-uir_4vnCTPkoUA7hT5CJlOi_5jUY6UclxojM_7_yRVTH-8LqmjWAEgD1roycHLE0Yk3yvYJxw0_c4FCSKGfL2XrGaoQ1mYx1SLSq--sx49lz-M9mr8I7QqN-sXBvI5qYGAGFmMSZsmqyvI6AxKTW24lnxI8p5kcSCxIgXv0ySFol_TT1W-UbkP_v9VRnSwsuSJDw-xNFLS-4JOx04sw3DjXKL2f3OaeGfog3xVdo0EqtIXL2Vk2ylNUa0e0JCzL-zraOhZhmLOwgFpg8PAQUEpfGArsmRg2YXrdIaaC8YZKD_3B44lId1eKtJxWEj9JjETLUOvv8t7oVFtEvI8c-X6Jdd7RgoYMAURFUbibWlWNDASZzqT04AKkjoEZOkuXz8jUzodVQRrl7S1XmaduTHYJ4sg3-Hs0R07AA0CN9nu72utrLtE093JCWewMeNmC8OKiuscYkHCQBhh-zjqmc4bgg1G_bk45KUOa-HEn-VNTx0R7VQfdiGj2dxdPNN082pEXlo3GWB_BIBNAHNfDOatXE6SaL2bnYbssct92T9l-0YMQT-Cw2m592PwSf3Ihq5qQ24pcfGaN34JL11P24kWaBqv1ZH3cVjfKtJRJ4RxI_ZkeHtfIrM8ilGDwoxITg5eKcjXjfQDPNRm66SwgX-BKOhReA7QINcPJsD2t7uIBcxWiBJQ-tH_oQaxjX5HomWWW6Q6aBuehKMqyDiqAyWuaWTY70ONQ_E23rIQBpPHih9qW6rkcCNtp35suMsTFupTm_marZb5nJ8oZ9iqh-NVw3lV3Ts7zU3dPB211nFsw7oSEHgWYqMQzyBm2iX7KRRxSy_K_Vb_g41MZprqQSGXc5tjC0I6jbZzz8NsUBDAO7tHLmByyGpUrO8R0Zjxeopf0ERgyUDo8tcKRa4234szQ5D1cNgvzbgoZVkU1Uw3APK19ipxMNJcLbFRR2cFCTd76dW8sWVLYYFxWr5gB9sWl3t2EprGhO84j4ojZPsK_1Lzr0pQzlR0zXmOgQ_xuGB_pxLIks2kmUfRktVn_MyYo-5wqbgQtpDfLqR2r9N8qJQoFmpmvRb98WnUdzrqcvaFrB41DugVKLU4PctUqaRbbs7bKOzuZimaWiCTv8RqmyUdKjg6-d7Uemx5JKle-bjal2vNS5uUPfbYhADjoUNr5omrFpllnY-w-wJg6nolQwcdm438ONvYo-51QsnoRwy9VRJ8SokY3q3G6yuNERG5nfz_1M6YjBFS5Ytb-1dF9c4rVCXIGs5KaUiqTefeu9bxsLJb93d72JcYwFfq495yqtjiIPKE5aKYrTrALAQk_3QJq6nSHf7On-S4yBkAIDh3Fw3RwijkZt59FJgk116UKbnrYm0J0t6TtMdIz04vUNlapyf_c3023XmpIbsPYGDG6gf1c399wqpPF-EHwna9CPNbKvmvdI66LhfVk65TorM_bNNGjwMPX5w2C6yKHuf7_TxKsUArSy9m6hYJBLQutvuA5guhvTcTUAaiNJhOCSSAYJCPYD1oCaNDTFpml60-n_Y8BU5OeA8d5O6z8M6XM1JFgBU0wFRCojUcmcT4mYCV-rLX--ReHjaWD3Cpl7XnJKdsZWrX-t2VUHr4vqTDMDqysIVrGmgeSU3epB7NvPcWYn5mO3giXSumLUcwEe2A2QpvgWsfnuit2RCNWmF3E1WmbTRxbWMn1CK-ltnHazRIbWswyphSF8GU-jbCFGiTsfx1mJcKgZt2QeoTfmSODTbWnmy8bRl0JEKwQoQ4K0eAvCajwpJqx-ZFyWzk3JUxH4V_SS7jNrVOVW6VyU11FktGdmO8eLWqot4fhUTUDTBGnsByOG1Fh8TpplUsFgiG91SD2XMaWmu8btP2RUVhskAOgz1-DEMoCjigboUn4IWRL4e70iU8qT_9hjKvd9LDkh9jPFVdHfMFK0f7wbEESqeItmimruMEmyiu4xdAuqIwOpT71GlaEtOQnPNKNwZ1afDCoGz3sOA_kvfddpGXvMZRsMfnJxhcMTm-tcsN25UcTIay-_epkaZtkkeQPCq-gDpVRaJqgENzhRTCDO7G0VNpsgBEDcfyCjqxxWx650XkQAW8vuZYspLm6UI-OP3cJntsKY0L1cb3q7c8xVHrLUCWmSXWZjzsNXhOh0H2iUsWRMN-smnQS68AMjMzyAjDWOanjt1OU525ZuFvOIPKWorelAxPnxd9E4YG08n6dQuPiZ5NQuBZxkJkMdFigqOBqrA_yfuZfgwG4E2REEHdKizgXxe35wCBaWzfErSNx0qVe14g2_P8UYfRxiW45BQGdgqF6fXYhkIP2RdqsXq5cmMrF2IQte1YtHnFsX1A45u-a9i3QIfcpOAr7Fklb1nulJqH1pVnbmHktI4GnBenZUSZ2Kx3oU0NSuqr_2NuT7oagXWpv0o772cpaZdj_k0oDO4g5RtnEYBT-LBW8ahzao8-E9qrBpt9b1yS_Wdy-YaBYjBrY_FqikULeV50EJvrCWu9l7Q8x3zYx8eMypkFva6nV0ci1w_VSFDPRGll3avMu6NFPdJpy97vvsLE2SnHUKn3q3rIbHUZGuqtJuEamqUG937onj-e7HQrU9vfQZbp6rDLecTazpz0vgbV0pSIWQ-wUB6yy2dz8o0LSdOb2qvUWBRmaReeFxagltcZyBDRE2EIP7EsfMyGVQXN_T-4JtQvGL3duiPUFBapb_VpBITa36bd4ybFe3XjS8YWbb8EawPmLV8VStmxdywFTQh_9rzo3htZr9KI1Mt-qyq53VyWhvhCIAKAfQnVMeW88j7wZ1XL5qs3mcCJp_LX1eKlJrOFRGvSzDaVZpq0K15o6s-5f8suYfvMu_9D7Cg6tuIAWBZEMjZzlzgL-0PIdt1l4sBbfn6A2BXCVYA6DkkrGFZyrpud458QNXo5oFiRyOQEeyjS7auXGyCzHQB0efrTVFV08lSQ2rMgBTq0eyc7lQWLbIF5Xte3jNp_umNnt255NT0wfBBojcyABjmQPeVaLRgA41nKv9ww2aYAxhy4TFUE3glxH5OvWk1qdZBioNeo3vXivz1zazr6j_hi7tGYXLLvquccqwhXAaIXqXjb-2c0um5Cvip6ivd1Th28XzWj_C6yqpeC-v-olCUDAiLNLqaKU_FVQfv96t33ruS8vEeT65vxTKSn1JH94c4yeddZCdknW9HxLurBu3AB1llufz3kVG1-s= created_at: 2024-05-21 23:59:59 tenant_id: 6eb4710c-72df-4941-984d-f2cf3dbe396c \ No newline at end of file diff --git a/server/test/fixtures/main_router/common/mfa_configs.yaml b/server/test/fixtures/common/mfa_configs.yaml similarity index 100% rename from server/test/fixtures/main_router/common/mfa_configs.yaml rename to server/test/fixtures/common/mfa_configs.yaml diff --git a/server/test/fixtures/main_router/common/relying_parties.yaml b/server/test/fixtures/common/relying_parties.yaml similarity index 100% rename from server/test/fixtures/main_router/common/relying_parties.yaml rename to server/test/fixtures/common/relying_parties.yaml diff --git a/server/test/fixtures/main_router/common/secrets.yaml b/server/test/fixtures/common/secrets.yaml similarity index 100% rename from server/test/fixtures/main_router/common/secrets.yaml rename to server/test/fixtures/common/secrets.yaml diff --git a/server/test/fixtures/main_router/common/tenants.yaml b/server/test/fixtures/common/tenants.yaml similarity index 100% rename from server/test/fixtures/main_router/common/tenants.yaml rename to server/test/fixtures/common/tenants.yaml diff --git a/server/test/fixtures/main_router/common/webauthn_configs.yaml b/server/test/fixtures/common/webauthn_configs.yaml similarity index 100% rename from server/test/fixtures/main_router/common/webauthn_configs.yaml rename to server/test/fixtures/common/webauthn_configs.yaml diff --git a/server/test/fixtures/main_router/common/webauthn_origins.yaml b/server/test/fixtures/common/webauthn_origins.yaml similarity index 100% rename from server/test/fixtures/main_router/common/webauthn_origins.yaml rename to server/test/fixtures/common/webauthn_origins.yaml diff --git a/server/test/suite.go b/server/test/suite.go index f2a570d..23d05dc 100644 --- a/server/test/suite.go +++ b/server/test/suite.go @@ -113,6 +113,12 @@ func (s *Suite) LoadMultipleFixtures(paths []string) error { return nil } +func (s *Suite) SkipOnShort() { + if testing.Short() { + s.T().Skip("skipping test in short mode") + } +} + func testLogger(_ logging.Level, _ string, _ ...interface{}) { }