diff --git a/.secrets.baseline b/.secrets.baseline index 4990d5592..61a9f6f5c 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -75,6 +75,10 @@ { "path": "detect_secrets.filters.allowlist.is_line_allowlisted" }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, { "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", "min_level": 2 @@ -117,103 +121,68 @@ "line_number": 37 } ], + "core/apis.go": [ + { + "type": "Secret Keyword", + "filename": "core/apis.go", + "hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c", + "is_verified": false, + "line_number": 95 + } + ], "core/app_shared.go": [ { "type": "Secret Keyword", "filename": "core/app_shared.go", "hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c", "is_verified": false, - "line_number": 41 + "line_number": 42 } ], "core/auth/apis.go": [ + { + "type": "Secret Keyword", + "filename": "core/auth/apis.go", + "hashed_secret": "04e110541a2e8b44bc10939bfaf5d82adfe45158", + "is_verified": false, + "line_number": 1944 + }, { "type": "Secret Keyword", "filename": "core/auth/apis.go", "hashed_secret": "394e3412459f79523e12e1fa95a4cf141ccff122", "is_verified": false, - "line_number": 2100 + "line_number": 2277 } ], "core/auth/auth.go": [ { "type": "Secret Keyword", "filename": "core/auth/auth.go", - "hashed_secret": "417355fe2b66baa6826739a6d8006ab2ddcf5186", + "hashed_secret": "3fea7ef2cdd6ecf5280c66dbd062272664559d83", "is_verified": false, - "line_number": 151 + "line_number": 160 }, { "type": "Secret Keyword", "filename": "core/auth/auth.go", - "hashed_secret": "a358987289cd70bbf50fb10acbcb9bff73c66df6", + "hashed_secret": "4a0043e461375664a5656fbdda0d3c39a42a1af4", "is_verified": false, - "line_number": 153 + "line_number": 162 }, { "type": "Secret Keyword", "filename": "core/auth/auth.go", "hashed_secret": "58f3388441fbce0e48aef2bf74413a6f43f6dc70", "is_verified": false, - "line_number": 937 + "line_number": 982 }, { "type": "Secret Keyword", "filename": "core/auth/auth.go", "hashed_secret": "94a7f0195bbbd2260c4e4d02b6348fbcd90b2b30", "is_verified": false, - "line_number": 2441 - } - ], - "core/auth/auth_type_email.go": [ - { - "type": "Secret Keyword", - "filename": "core/auth/auth_type_email.go", - "hashed_secret": "f3f2fb17a3bf9f307cb6e79b61b9d4baf07dd681", - "is_verified": false, - "line_number": 75 - }, - { - "type": "Secret Keyword", - "filename": "core/auth/auth_type_email.go", - "hashed_secret": "fe70d8c51780596c0b3399573122bba943a461da", - "is_verified": false, - "line_number": 76 - }, - { - "type": "Secret Keyword", - "filename": "core/auth/auth_type_email.go", - "hashed_secret": "06354d205ab5a3b6c7ad2333c58f1ddc810c97ba", - "is_verified": false, - "line_number": 87 - }, - { - "type": "Secret Keyword", - "filename": "core/auth/auth_type_email.go", - "hashed_secret": "7cbe6dcf7274355d223e3174e4d8a7ffb55a9227", - "is_verified": false, - "line_number": 156 - }, - { - "type": "Secret Keyword", - "filename": "core/auth/auth_type_email.go", - "hashed_secret": "69411040443be576ce64fc793269d7c26dd0866a", - "is_verified": false, - "line_number": 253 - }, - { - "type": "Secret Keyword", - "filename": "core/auth/auth_type_email.go", - "hashed_secret": "cba104f0870345d3ec99d55c06441bdce9fcf584", - "is_verified": false, - "line_number": 390 - }, - { - "type": "Secret Keyword", - "filename": "core/auth/auth_type_email.go", - "hashed_secret": "c74f3640d83fd19d941a4f44b28fbd9e57f59eef", - "is_verified": false, - "line_number": 391 + "line_number": 2730 } ], "core/auth/auth_type_oidc.go": [ @@ -222,51 +191,32 @@ "filename": "core/auth/auth_type_oidc.go", "hashed_secret": "0ade4f3edccc8888bef404fe6b3c92c13cdfad6b", "is_verified": false, - "line_number": 376 + "line_number": 400 } ], - "core/auth/auth_type_username.go": [ - { - "type": "Secret Keyword", - "filename": "core/auth/auth_type_username.go", - "hashed_secret": "86f4f81d8dcd41f5f695464a3bba658467957bb3", - "is_verified": false, - "line_number": 64 - }, - { - "type": "Secret Keyword", - "filename": "core/auth/auth_type_username.go", - "hashed_secret": "d6f3638bf6ffed24773951f1a48460efa6766362", - "is_verified": false, - "line_number": 65 - }, - { - "type": "Secret Keyword", - "filename": "core/auth/auth_type_username.go", - "hashed_secret": "06354d205ab5a3b6c7ad2333c58f1ddc810c97ba", - "is_verified": false, - "line_number": 77 - }, + "core/auth/auth_type_password.go": [ { "type": "Secret Keyword", - "filename": "core/auth/auth_type_username.go", - "hashed_secret": "7cbe6dcf7274355d223e3174e4d8a7ffb55a9227", + "filename": "core/auth/auth_type_password.go", + "hashed_secret": "ed4434126edb03dc832260a730ccf3bb61af1396", "is_verified": false, - "line_number": 179 + "line_number": 91 }, { "type": "Secret Keyword", - "filename": "core/auth/auth_type_username.go", - "hashed_secret": "cba104f0870345d3ec99d55c06441bdce9fcf584", + "filename": "core/auth/auth_type_password.go", + "hashed_secret": "8a1618d670f9d2d7d0b26c1d80227ead407f66dd", "is_verified": false, - "line_number": 215 - }, + "line_number": 197 + } + ], + "core/auth/identifier_type_email.go": [ { "type": "Secret Keyword", - "filename": "core/auth/auth_type_username.go", - "hashed_secret": "c74f3640d83fd19d941a4f44b28fbd9e57f59eef", + "filename": "core/auth/identifier_type_email.go", + "hashed_secret": "69411040443be576ce64fc793269d7c26dd0866a", "is_verified": false, - "line_number": 216 + "line_number": 251 } ], "core/auth/service_static_token.go": [ @@ -275,7 +225,7 @@ "filename": "core/auth/service_static_token.go", "hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c", "is_verified": false, - "line_number": 78 + "line_number": 71 } ], "driven/emailer/adapter.go": [ @@ -293,7 +243,7 @@ "filename": "driven/profilebb/adapter.go", "hashed_secret": "36c48d6ac9d10902792fa78b9c2d7d535971c2cc", "is_verified": false, - "line_number": 224 + "line_number": 223 } ], "driven/storage/database.go": [ @@ -302,7 +252,23 @@ "filename": "driven/storage/database.go", "hashed_secret": "6547f385c6d867e20f8217018a4d468a7d67d638", "is_verified": false, - "line_number": 216 + "line_number": 224 + } + ], + "driven/storage/migrations.go": [ + { + "type": "Secret Keyword", + "filename": "driven/storage/migrations.go", + "hashed_secret": "fd9a601da67dbaa273e7fb64877518ee9e408057", + "is_verified": false, + "line_number": 141 + }, + { + "type": "Secret Keyword", + "filename": "driven/storage/migrations.go", + "hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c", + "is_verified": false, + "line_number": 156 } ], "driver/web/apis_system.go": [ @@ -329,7 +295,7 @@ "filename": "driver/web/docs/gen/gen_types.go", "hashed_secret": "c9739eab2dfa093cc0e450bf0ea81a43ae67b581", "is_verified": false, - "line_number": 1797 + "line_number": 1920 } ], "driver/web/docs/resources/admin/auth/login.yaml": [ @@ -347,7 +313,7 @@ "filename": "driver/web/docs/resources/services/auth/account/auth-type/link.yaml", "hashed_secret": "448ed7416fce2cb66c285d182b1ba3df1e90016d", "is_verified": false, - "line_number": 26 + "line_number": 23 } ], "driver/web/docs/resources/services/auth/login.yaml": [ @@ -360,5 +326,5 @@ } ] }, - "generated_at": "2023-10-06T19:34:36Z" + "generated_at": "2023-10-03T21:38:39Z" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f49b5e3c..1122836fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased - ### Added +- WebAuthn authentication [#659](https://github.com/rokwire/core-building-block/issues/659) - Searching follows looks for substring matches [#670](https://github.com/rokwire/core-building-block/issues/670) - -### Added - Support following accounts [#667](https://github.com/rokwire/core-building-block/issues/667) - Device ID not nullable [#672](https://github.com/rokwire/core-building-block/issues/672) +### Changed +- Decouple authentication and verification mechanisms [#665](https://github.com/rokwire/core-building-block/issues/665) +- Refactor account auth types [#674](https://github.com/rokwire/core-building-block/issues/674) ## [1.34.0] - 2023-07-06 ### Added diff --git a/core/apis.go b/core/apis.go index b4f43b3fa..54591a1d0 100644 --- a/core/apis.go +++ b/core/apis.go @@ -22,6 +22,7 @@ import ( "time" "github.com/google/uuid" + "github.com/rokwire/core-auth-library-go/v3/authutils" "github.com/rokwire/core-auth-library-go/v3/tokenauth" "github.com/rokwire/logging-library-go/v2/errors" "github.com/rokwire/logging-library-go/v2/logs" @@ -47,7 +48,9 @@ type APIs struct { systemAccountEmail string systemAccountPassword string - verifyEmail bool + verifyEmail bool + verifyWaitTime int + verifyExpiry int logger *logs.Logger } @@ -84,23 +87,56 @@ func (c *APIs) storeSystemData() error { transaction := func(context storage.TransactionContext) error { createAccount := false - //1. insert email auth type if does not exist - emailAuthType, err := c.app.storage.FindAuthType(auth.AuthTypeEmail) + //1. insert password auth type if does not exist + passwordAuthType, err := c.app.storage.FindAuthType(auth.AuthTypePassword) if err != nil { return errors.WrapErrorAction(logutils.ActionFind, model.TypeAuthType, nil, err) } - if emailAuthType == nil { + if passwordAuthType == nil { newDocuments["auth_type"] = uuid.NewString() - params := map[string]interface{}{"verify_email": c.verifyEmail} - emailAuthType = &model.AuthType{ID: newDocuments["auth_type"], Code: auth.AuthTypeEmail, Description: "Authentication type relying on email and password", - IsExternal: false, IsAnonymous: false, UseCredentials: true, IgnoreMFA: false, Params: params} - _, err = c.app.storage.InsertAuthType(context, *emailAuthType) + passwordAuthType = &model.AuthType{ID: newDocuments["auth_type"], Code: auth.AuthTypePassword, Description: "Authentication type relying on password", + IsExternal: false, IsAnonymous: false, UseCredentials: true, IgnoreMFA: false, Aliases: []string{auth.IdentifierTypeEmail, auth.IdentifierTypeUsername}} + _, err = c.app.storage.InsertAuthType(context, *passwordAuthType) if err != nil { return errors.WrapErrorAction(logutils.ActionInsert, model.TypeAuthType, nil, err) } } - //2. insert system org if does not exist + //2. update auth config or insert if it does not exist + config, err := c.app.storage.FindConfig(model.ConfigTypeAuth, authutils.AllApps, authutils.AllOrgs) + if err != nil { + return errors.WrapErrorAction(logutils.ActionFind, model.TypeConfig, &logutils.FieldArgs{"type": model.ConfigTypeAuth, "app_id": authutils.AllApps, "org_id": authutils.AllOrgs}, err) + } + if config == nil { + configData := model.AuthConfigData{EmailShouldVerify: &c.verifyEmail, EmailVerifyWaitTime: &c.verifyWaitTime, EmailVerifyExpiry: &c.verifyExpiry} + newConfig := model.Config{ID: uuid.NewString(), Type: model.ConfigTypeAuth, AppID: authutils.AllApps, OrgID: authutils.AllOrgs, System: true, Data: configData, DateCreated: time.Now().UTC()} + err = c.app.storage.InsertConfig(context, newConfig) + if err != nil { + return errors.WrapErrorAction(logutils.ActionInsert, model.TypeConfig, &logutils.FieldArgs{"type": model.ConfigTypeAuth, "app_id": authutils.AllApps, "org_id": authutils.AllOrgs}, err) + } + } else { + configData, err := model.GetConfigData[model.AuthConfigData](*config) + if err != nil { + return errors.WrapErrorAction(logutils.ActionParse, model.TypeAuthConfigData, nil, err) + } + + updateShouldVerify := configData.EmailShouldVerify == nil || (*configData.EmailShouldVerify != c.verifyEmail) + updateVerifyWaitTime := configData.EmailVerifyWaitTime == nil || (*configData.EmailVerifyWaitTime != c.verifyWaitTime) + updateVerifyExpiry := configData.EmailVerifyExpiry == nil || (*configData.EmailVerifyExpiry != c.verifyExpiry) + if updateShouldVerify || updateVerifyWaitTime || updateVerifyExpiry { + configData.EmailShouldVerify = &c.verifyEmail + configData.EmailVerifyWaitTime = &c.verifyWaitTime + configData.EmailVerifyExpiry = &c.verifyExpiry + config.Data = *configData + + err = c.app.storage.UpdateConfig(context, *config) + if err != nil { + return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeConfig, &logutils.FieldArgs{"id": config.ID}, err) + } + } + } + + //3. insert system org if does not exist systemOrg, err := c.app.storage.FindSystemOrganization() if err != nil { return errors.WrapErrorAction(logutils.ActionFind, model.TypeOrganization, nil, err) @@ -118,7 +154,7 @@ func (c *APIs) storeSystemData() error { createAccount = true } - //3. insert system app and appOrg if they do not exist + //4. insert system app and appOrg if they do not exist systemAdminAppOrgs, err := c.app.storage.FindApplicationsOrganizationsByOrgID(systemOrg.ID) if err != nil { return errors.WrapErrorAction(logutils.ActionFind, model.TypeApplicationOrganization, nil, err) @@ -142,7 +178,7 @@ func (c *APIs) storeSystemData() error { //insert system admin apporg supportedAuthTypes := make([]model.AuthTypesSupport, len(systemAdminApp.Types)) for i, appType := range systemAdminApp.Types { - supportedAuthTypes[i] = model.AuthTypesSupport{AppTypeID: appType.ID, SupportedAuthTypes: []model.SupportedAuthType{{AuthTypeID: emailAuthType.ID, Params: nil}}} + supportedAuthTypes[i] = model.AuthTypesSupport{AppTypeID: appType.ID, SupportedAuthTypes: []model.SupportedAuthType{{AuthTypeID: passwordAuthType.ID, Params: nil}}} } newDocuments["application_organization"] = uuid.NewString() @@ -159,7 +195,7 @@ func (c *APIs) storeSystemData() error { systemAppOrg := systemAdminAppOrgs[0] - //4. insert api key if does not exist + //5. insert api key if does not exist apiKeys, err := c.Auth.GetApplicationAPIKeys(systemAppOrg.Application.ID) if err != nil { return errors.WrapErrorAction(logutils.ActionFind, model.TypeAPIKey, nil, err) @@ -177,7 +213,7 @@ func (c *APIs) storeSystemData() error { } } - //5. insert all_system_core permission and grant_all_permissions permission if they do not exist + //6. insert all_system_core permission and grant_all_permissions permission if they do not exist requiredPermissions := map[string]string{ model.PermissionAllSystemCore: "Gives access to all admin and system APIs", model.PermissionGrantAllPermissions: "Gives the ability to grant any permission", @@ -222,12 +258,12 @@ func (c *APIs) storeSystemData() error { } } - //6. insert system account if needed + //7. insert system account if needed if createAccount { if c.systemAccountEmail == "" || c.systemAccountPassword == "" { return errors.ErrorData(logutils.StatusMissing, "initial system account email or password", nil) } - newDocuments["account"], err = c.Auth.InitializeSystemAccount(context, *emailAuthType, systemAppOrg, model.PermissionAllSystemCore, c.systemAccountEmail, c.systemAccountPassword, "", c.logger.NewRequestLog(nil)) + newDocuments["account"], err = c.Auth.InitializeSystemAccount(context, *passwordAuthType, systemAppOrg, model.PermissionAllSystemCore, c.systemAccountEmail, c.systemAccountPassword, "", c.logger.NewRequestLog(nil)) if err != nil { return errors.WrapErrorAction(logutils.ActionInitialize, "system account", nil, err) } @@ -245,7 +281,7 @@ func (c *APIs) storeSystemData() error { } fields := logutils.Fields{key: data} if doc == "auth_type" { - fields["code"] = auth.AuthTypeEmail + fields["code"] = auth.IdentifierTypeEmail } c.logger.InfoWithFields(fmt.Sprintf("new system %s created", doc), fields) } @@ -254,7 +290,8 @@ func (c *APIs) storeSystemData() error { } // NewCoreAPIs creates new CoreAPIs -func NewCoreAPIs(env string, version string, build string, serviceID string, storage Storage, auth auth.APIs, systemInitSettings map[string]string, verifyEmail bool, logger *logs.Logger) *APIs { +func NewCoreAPIs(env string, version string, build string, serviceID string, storage Storage, auth auth.APIs, systemInitSettings map[string]string, verifyEmail bool, + verifyWaitTime int, verifyExpiry int, logger *logs.Logger) *APIs { //add application instance listeners := []ApplicationListener{} application := application{env: env, version: version, build: build, serviceID: serviceID, storage: storage, listeners: listeners, auth: auth} @@ -271,7 +308,8 @@ func NewCoreAPIs(env string, version string, build string, serviceID string, sto coreAPIs := APIs{Services: servicesImpl, Administration: administrationImpl, Encryption: encryptionImpl, BBs: bbsImpl, TPS: tpsImpl, System: systemImpl, Auth: auth, app: &application, systemAppTypeIdentifier: systemInitSettings["app_type_id"], systemAppTypeName: systemInitSettings["app_type_name"], systemAPIKey: systemInitSettings["api_key"], - systemAccountEmail: systemInitSettings["email"], systemAccountPassword: systemInitSettings["password"], verifyEmail: verifyEmail, logger: logger} + systemAccountEmail: systemInitSettings["email"], systemAccountPassword: systemInitSettings["password"], verifyEmail: verifyEmail, + verifyWaitTime: verifyWaitTime, verifyExpiry: verifyExpiry, logger: logger} return &coreAPIs } diff --git a/core/apis_test.go b/core/apis_test.go index b78994fbc..25c4d64e7 100644 --- a/core/apis_test.go +++ b/core/apis_test.go @@ -29,7 +29,7 @@ import ( ) func buildTestCoreAPIs(storage core.Storage) *core.APIs { - return core.NewCoreAPIs("local", "1.1.1", "build", "core", storage, nil, nil, false, nil) + return core.NewCoreAPIs("local", "1.1.1", "build", "core", storage, nil, nil, false, 30, 24, nil) } //Services @@ -83,7 +83,7 @@ func TestAdmGetTest(t *testing.T) { func TestAdmCreateConfig(t *testing.T) { anyConfig := mock.AnythingOfType("model.Config") storage := genmocks.Storage{} - storage.On("InsertConfig", anyConfig).Return(nil) + storage.On("InsertConfig", nil, anyConfig).Return(nil) coreAPIs := buildTestCoreAPIs(&storage) @@ -96,7 +96,7 @@ func TestAdmCreateConfig(t *testing.T) { //second case - error storage2 := genmocks.Storage{} - storage2.On("InsertConfig", anyConfig).Return(errors.New("error occured")) + storage2.On("InsertConfig", nil, anyConfig).Return(errors.New("error occured")) coreAPIs = buildTestCoreAPIs(&storage2) diff --git a/core/app_administration.go b/core/app_administration.go index 958fe3485..7aa48487b 100644 --- a/core/app_administration.go +++ b/core/app_administration.go @@ -231,7 +231,7 @@ func (app *application) admCreateConfig(config model.Config, claims *tokenauth.C config.ID = uuid.NewString() config.DateCreated = time.Now().UTC() - err = app.storage.InsertConfig(config) + err = app.storage.InsertConfig(nil, config) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionInsert, model.TypeConfig, nil, err) } @@ -261,11 +261,8 @@ func (app *application) admUpdateConfig(config model.Config, claims *tokenauth.C return errors.WrapErrorAction(logutils.ActionValidate, "config access", nil, err) } - now := time.Now().UTC() config.ID = oldConfig.ID - config.DateUpdated = &now - - err = app.storage.UpdateConfig(config) + err = app.storage.UpdateConfig(nil, config) if err != nil { return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeConfig, nil, err) } diff --git a/core/app_services.go b/core/app_services.go index a59094325..5c55ab590 100644 --- a/core/app_services.go +++ b/core/app_services.go @@ -33,6 +33,7 @@ func (app *application) serGetProfile(accountID string) (*model.Profile, error) //get the profile for the account profile := account.Profile + profile.Accounts = []model.Account{*account} return &profile, nil } diff --git a/core/app_shared.go b/core/app_shared.go index 55ee9517f..a2f0320e7 100644 --- a/core/app_shared.go +++ b/core/app_shared.go @@ -17,6 +17,7 @@ package core import ( "core-building-block/core/model" "core-building-block/driven/storage" + "core-building-block/utils" "github.com/rokwire/logging-library-go/v2/errors" "github.com/rokwire/logging-library-go/v2/logutils" @@ -88,25 +89,19 @@ func (app *application) sharedGetAccountsCountByParams(searchParams map[string]i func (app *application) sharedUpdateAccountUsername(accountID string, appID string, orgID string, username string) error { if username == "" { - err := app.storage.UpdateAccountUsername(nil, accountID, username) - if err != nil { - return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountUsername, nil, err) - } + return errors.ErrorData(logutils.StatusMissing, model.TypeAccountUsername, nil) + } - return nil + appOrg, err := app.storage.FindApplicationOrganization(appID, orgID) + if err != nil { + return errors.WrapErrorAction(logutils.ActionFind, model.TypeApplicationOrganization, &logutils.FieldArgs{"app_id": appID, "org_id": orgID}, err) + } + if appOrg == nil { + return errors.ErrorData(logutils.StatusMissing, model.TypeApplicationOrganization, &logutils.FieldArgs{"app_id": appID, "org_id": orgID}) } transaction := func(context storage.TransactionContext) error { - //1. find the app/org - appOrg, err := app.storage.FindApplicationOrganization(appID, orgID) - if err != nil { - return errors.WrapErrorAction(logutils.ActionFind, model.TypeApplicationOrganization, &logutils.FieldArgs{"app_id": appID, "org_id": orgID}, err) - } - if appOrg == nil { - return errors.ErrorData(logutils.StatusMissing, model.TypeApplicationOrganization, &logutils.FieldArgs{"app_id": appID, "org_id": orgID}) - } - - //2. check if any accounts in the app/org use the username + //1. check if any accounts in the app/org use the username accounts, err := app.storage.FindAccountsByUsername(context, appOrg, username) if err != nil { return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) @@ -118,10 +113,10 @@ func (app *application) sharedUpdateAccountUsername(accountID string, appID stri return nil } } - return errors.ErrorData(logutils.StatusInvalid, model.TypeAccountUsername, logutils.StringArgs(username+" taken")) + return errors.ErrorData(logutils.StatusInvalid, model.TypeAccountUsername, logutils.StringArgs(username+" taken")).SetStatus(utils.ErrorStatusUsernameTaken) } - //3. update the username + //2. update the username err = app.storage.UpdateAccountUsername(context, accountID, username) if err != nil { return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountUsername, nil, err) diff --git a/core/app_system.go b/core/app_system.go index a04293ac7..86c0acaa3 100644 --- a/core/app_system.go +++ b/core/app_system.go @@ -47,7 +47,7 @@ func (app *application) sysCreateApplicationOrganization(appOrg model.Applicatio appOrgID, _ := uuid.NewUUID() appOrg.ID = appOrgID.String() - appOrg.DateCreated = time.Now() + appOrg.DateCreated = time.Now().UTC() insertedAppOrg, err := app.storage.InsertApplicationOrganization(nil, appOrg) if err != nil { @@ -68,7 +68,7 @@ func (app *application) sysUpdateApplicationOrganization(appOrg model.Applicatio } func (app *application) sysCreateOrganization(name string, requestType string, organizationDomains []string) (*model.Organization, error) { - now := time.Now() + now := time.Now().UTC() orgConfig := model.OrganizationConfig{ID: uuid.NewString(), Domains: organizationDomains, DateCreated: now} organization := model.Organization{ID: uuid.NewString(), Name: name, Type: requestType, Config: orgConfig, DateCreated: now} @@ -124,7 +124,7 @@ func (app *application) sysGetApplication(ID string) (*model.Application, error) } func (app *application) sysCreateApplication(name string, multiTenant bool, admin bool, sharedIdentities bool, appTypes []model.ApplicationType) (*model.Application, error) { - now := time.Now() + now := time.Now().UTC() // application for i, at := range appTypes { @@ -212,7 +212,7 @@ func (app *application) sysGetApplications() ([]model.Application, error) { func (app *application) sysCreatePermission(name string, description *string, serviceID *string, assigners *[]string) (*model.Permission, error) { id, _ := uuid.NewUUID() - now := time.Now() + now := time.Now().UTC() serviceIDVal := "" if serviceID != nil { serviceIDVal = *serviceID diff --git a/core/auth/apis.go b/core/auth/apis.go index 1dfc49b59..0bf234747 100644 --- a/core/auth/apis.go +++ b/core/auth/apis.go @@ -18,6 +18,7 @@ import ( "core-building-block/core/model" "core-building-block/driven/storage" "core-building-block/utils" + "encoding/json" "fmt" "strings" "time" @@ -50,37 +51,39 @@ func (a *Auth) GetHost() string { // Login logs a user into a specific application using the specified credentials and authentication method. // The authentication method must be one of the supported for the application. // -// Input: -// ipAddress (string): Client's IP address -// deviceType (string): "mobile" or "web" or "desktop" etc -// deviceOS (*string): Device OS -// deviceID (*string): Device ID -// authenticationType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") -// creds (string): Credentials/JSON encoded credential structure defined for the specified auth type -// apiKey (string): API key to validate the specified app -// appTypeIdentifier (string): identifier of the app type/client that the user is logging in from -// orgID (string): ID of the organization that the user is logging in -// params (string): JSON encoded params defined by specified auth type -// profile (Profile): Account profile -// preferences (map): Account preferences -// admin (bool): Is this an admin login? -// l (*logs.Log): Log object pointer for request -// Returns: -// Message (*string): message -// Login session (*LoginSession): Signed ROKWIRE access token to be used to authorize future requests -// Access token (string): Signed ROKWIRE access token to be used to authorize future requests -// Refresh Token (string): Refresh token that can be sent to refresh the access token once it expires -// AccountAuthType (AccountAuthType): AccountAuthType object for authenticated user -// Params (interface{}): authType-specific set of parameters passed back to client -// State (string): login state used if account is enrolled in MFA -// MFA types ([]model.MFAType): list of MFA types account is enrolled in +// Input: +// ipAddress (string): Client's IP address +// deviceType (string): "mobile" or "web" or "desktop" etc +// deviceOS (*string): Device OS +// deviceID (*string): Device ID +// authenticationType (string): Name of the authentication method for provided creds (eg. "password", "code", "illinois_oidc") +// creds (string): Credentials/JSON encoded credential structure defined for the specified auth type +// apiKey (string): API key to validate the specified app +// appTypeIdentifier (string): identifier of the app type/client that the user is logging in from +// orgID (string): ID of the organization that the user is logging in +// params (string): JSON encoded params defined by specified auth type +// clientVersion(*string): Most recent client version +// profile (Profile): Account profile +// preferences (map): Account preferences +// accountIdentifierID (*string): UUID of account identifier, meant to be used after using SignInOptions +// admin (bool): Is this an admin login? +// l (*logs.Log): Log object pointer for request +// Returns: +// Response parameters (map): any messages or parameters to send in response when requiring identifier verification and/or NOT logging in the user +// Login session (*LoginSession): Signed ROKWIRE access token to be used to authorize future requests +// Access token (string): Signed ROKWIRE access token to be used to authorize future requests +// Refresh Token (string): Refresh token that can be sent to refresh the access token once it expires +// AccountAuthType (AccountAuthType): AccountAuthType object for authenticated user +// Params (interface{}): authType-specific set of parameters passed back to client +// State (string): login state used if account is enrolled in MFA +// MFA types ([]model.MFAType): list of MFA types account is enrolled in func (a *Auth) Login(ipAddress string, deviceType string, deviceOS *string, deviceID *string, authenticationType string, creds string, apiKey string, appTypeIdentifier string, orgID string, params string, clientVersion *string, profile model.Profile, privacy model.Privacy, preferences map[string]interface{}, - username string, admin bool, l *logs.Log) (*string, *model.LoginSession, []model.MFAType, error) { + accountIdentifierID *string, admin bool, l *logs.Log) (map[string]interface{}, *model.LoginSession, []model.MFAType, error) { //TODO - analyse what should go in one transaction //validate if the provided auth type is supported by the provided application and organization - authType, appType, appOrg, err := a.validateAuthType(authenticationType, appTypeIdentifier, orgID) + authType, appType, appOrg, err := a.validateAuthType(authenticationType, &appTypeIdentifier, nil, orgID) if err != nil || authType == nil { return nil, nil, nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, nil, err) } @@ -98,61 +101,49 @@ func (a *Auth) Login(ipAddress string, deviceType string, deviceOS *string, devi return nil, nil, nil, errors.WrapErrorData(logutils.StatusInvalid, model.TypeAPIKey, nil, err) } - username = strings.TrimSpace(strings.ToLower(username)) - anonymous := false sub := "" - var message string - var accountAuthType *model.AccountAuthType + var account *model.Account var responseParams map[string]interface{} - var externalIDs map[string]string var mfaTypes []model.MFAType var state string //get the auth type implementation for the auth type - if authType.IsAnonymous && !admin { + if authType.AuthType.IsAnonymous && !admin { anonymous = true anonymousID := "" - var account *model.Account - anonymousID, account, responseParams, err = a.applyAnonymousAuthType(*authType, creds) + anonymousID, account, responseParams, err = a.applyAnonymousAuthType(authType.AuthType, creds) if err != nil { return nil, nil, nil, errors.WrapErrorAction(logutils.ActionApply, typeAnonymousAuthType, logutils.StringArgs("user"), err) } sub = anonymousID - - if account != nil { - accountAuthType = &model.AccountAuthType{Account: *account} - } - } else if authType.IsExternal { - accountAuthType, responseParams, mfaTypes, externalIDs, err = a.applyExternalAuthType(*authType, *appType, *appOrg, creds, params, clientVersion, profile, privacy, preferences, username, admin, l) + } else if authType.AuthType.IsExternal { + responseParams, account, mfaTypes, err = a.applyExternalAuthType(*authType, *appType, *appOrg, creds, params, clientVersion, profile, privacy, preferences, admin, l) if err != nil { return nil, nil, nil, errors.WrapErrorAction(logutils.ActionApply, typeExternalAuthType, logutils.StringArgs("user"), err) } - sub = accountAuthType.Account.ID + sub = account.ID } else { - message, accountAuthType, mfaTypes, externalIDs, err = a.applyAuthType(*authType, *appOrg, creds, params, clientVersion, profile, privacy, preferences, username, admin, l) + responseParams, account, mfaTypes, err = a.applyAuthType(*authType, *appOrg, appType, creds, params, clientVersion, profile, privacy, preferences, accountIdentifierID, admin, l) if err != nil { return nil, nil, nil, errors.WrapErrorAction(logutils.ActionApply, model.TypeAuthType, logutils.StringArgs("user"), err) } //message - if len(message) > 0 { - return &message, nil, nil, nil + if responseParams != nil { + return responseParams, nil, nil, nil } - sub = accountAuthType.Account.ID + sub = account.ID //the credentials are valid } //check if account is enrolled in MFA - if !authType.IgnoreMFA && len(mfaTypes) > 0 { - state, err = utils.GenerateRandomString(loginStateLength) - if err != nil { - return nil, nil, nil, errors.WrapErrorAction(logutils.ActionGenerate, "login state", nil, err) - } + if !authType.AuthType.IgnoreMFA && len(mfaTypes) > 0 { + state = utils.GenerateRandomString(loginStateLength) } //clear the expired sessions for the identifier - user or anonymous @@ -162,7 +153,7 @@ func (a *Auth) Login(ipAddress string, deviceType string, deviceOS *string, devi } //now we are ready to apply login for the user or anonymous - loginSession, err := a.applyLogin(anonymous, sub, *authType, *appOrg, accountAuthType, *appType, externalIDs, ipAddress, deviceType, deviceOS, deviceID, clientVersion, responseParams, state, l) + loginSession, err := a.applyLogin(anonymous, sub, authType.AuthType, *appOrg, account, *appType, ipAddress, deviceType, deviceOS, deviceID, clientVersion, responseParams, state, l) if err != nil { return nil, nil, nil, errors.WrapErrorAction(logutils.ActionApply, "login", logutils.StringArgs("user"), err) } @@ -197,15 +188,21 @@ func (a *Auth) Logout(appID string, orgID string, currentAccountID string, sessi // The authentication method must be one of the supported for the application. // // Input: -// authenticationType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") -// userIdentifier (string): User identifier for the specified auth type +// identifierJSON (string): json string representing the user identifier and its type // apiKey (string): API key to validate the specified app // appTypeIdentifier (string): identifier of the app type/client that the user is logging in from // orgID (string): ID of the organization that the user is logging in +// authenticationType (*string): Optional authentication type (for backwards compatibility) +// userIdentifier (*string): Optional identifier for the given authentication type (for backwards compatibility) // Returns: // accountExisted (bool): valid when error is nil -func (a *Auth) AccountExists(authenticationType string, userIdentifier string, apiKey string, appTypeIdentifier string, orgID string) (bool, error) { - account, _, err := a.getAccount(authenticationType, userIdentifier, apiKey, appTypeIdentifier, orgID) +func (a *Auth) AccountExists(identifierJSON string, apiKey string, appTypeIdentifier string, orgID string, authenticationType *string, userIdentifier *string) (bool, error) { + identifierImpl := a.getIdentifierTypeImpl(identifierJSON, authenticationType, userIdentifier) + if identifierImpl == nil { + return false, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, nil) + } + + account, err := a.getAccount(identifierImpl.getCode(), identifierImpl.getIdentifier(), apiKey, appTypeIdentifier, orgID) if err != nil { return false, errors.WrapErrorAction(logutils.ActionGet, model.TypeAccount, nil, err) } @@ -217,47 +214,116 @@ func (a *Auth) AccountExists(authenticationType string, userIdentifier string, a // The authentication method must be one of the supported for the application. // // Input: -// authenticationType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") -// userIdentifier (string): User identifier for the specified auth type +// identifierJSON (string): json string representing the user identifier and its type // apiKey (string): API key to validate the specified app -// appTypeIdentifier (string): identifier of the app type/client being used -// orgID (string): ID of the organization being used +// appTypeIdentifier (string): identifier of the app type/client that the user is logging in from +// orgID (string): ID of the organization that the user is logging in +// authenticationType (*string): Optional authentication type (for backwards compatibility) +// userIdentifier (*string): Optional identifier for the given authentication type (for backwards compatibility) // Returns: // canSignIn (bool): valid when error is nil -func (a *Auth) CanSignIn(authenticationType string, userIdentifier string, apiKey string, appTypeIdentifier string, orgID string) (bool, error) { - account, authTypeID, err := a.getAccount(authenticationType, userIdentifier, apiKey, appTypeIdentifier, orgID) +func (a *Auth) CanSignIn(identifierJSON string, apiKey string, appTypeIdentifier string, orgID string, authenticationType *string, userIdentifier *string) (bool, error) { + identifierImpl := a.getIdentifierTypeImpl(identifierJSON, authenticationType, userIdentifier) + if identifierImpl == nil { + return false, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, nil) + } + + code := identifierImpl.getCode() + identifier := identifierImpl.getIdentifier() + + account, err := a.getAccount(code, identifier, apiKey, appTypeIdentifier, orgID) if err != nil { return false, errors.WrapErrorAction(logutils.ActionGet, model.TypeAccount, nil, err) } - return a.canSignIn(account, authTypeID, userIdentifier), nil + return a.canSignIn(account, code, identifier), nil } // CanLink checks if a user can link a new auth type // The authentication method must be one of the supported for the application. // // Input: -// authenticationType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") -// userIdentifier (string): User identifier for the specified auth type +// identifierJSON (string): json string representing the user identifier and its type // apiKey (string): API key to validate the specified app -// appTypeIdentifier (string): identifier of the app type/client being used -// orgID (string): ID of the organization being used +// appTypeIdentifier (string): identifier of the app type/client that the user is logging in from +// orgID (string): ID of the organization that the user is logging in +// authenticationType (*string): Optional authentication type (for backwards compatibility) +// userIdentifier (*string): Optional identifier for the given authentication type (for backwards compatibility) // Returns: // canLink (bool): valid when error is nil -func (a *Auth) CanLink(authenticationType string, userIdentifier string, apiKey string, appTypeIdentifier string, orgID string) (bool, error) { - account, authTypeID, err := a.getAccount(authenticationType, userIdentifier, apiKey, appTypeIdentifier, orgID) +func (a *Auth) CanLink(identifierJSON string, apiKey string, appTypeIdentifier string, orgID string, authenticationType *string, userIdentifier *string) (bool, error) { + identifierImpl := a.getIdentifierTypeImpl(identifierJSON, authenticationType, userIdentifier) + if identifierImpl == nil { + return false, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, nil) + } + + code := identifierImpl.getCode() + identifier := identifierImpl.getIdentifier() + + account, err := a.getAccount(code, identifier, apiKey, appTypeIdentifier, orgID) if err != nil { return false, errors.WrapErrorAction(logutils.ActionGet, model.TypeAccount, nil, err) } if account != nil { - aat := account.GetAccountAuthType(authTypeID, userIdentifier) - return (aat != nil && aat.Unverified), nil + ai := account.GetAccountIdentifier(code, identifier) + if authenticationType == nil && userIdentifier == nil { + // if not an old client, treat as a request to check if can link identifier + return (ai != nil && !ai.Verified), nil + } } + // either there is no account with the provided identifier, or + // old client, so treat as request to check if can link identifier OR auth type (can always attempt to link an auth type) return true, nil } +// SignInOptions returns the identifiers and auth types that may be used to sign in to an account +// +// Input: +// userIdentifier (string): User identifier for the specified auth type +// apiKey (string): API key to validate the specified app +// appTypeIdentifier (string): identifier of the app type/client being used +// orgID (string): ID of the organization being used +// Returns: +// identifiers ([]model.AccountIdentifier): account identifiers that may be used for sign-in +// authTypes ([]model.AccountAuthType): account auth types that may be used for sign-in +func (a *Auth) SignInOptions(identifierJSON string, apiKey string, appTypeIdentifier string, orgID string, authenticationType *string, userIdentifier *string, l *logs.Log) ([]model.AccountIdentifier, []model.AccountAuthType, error) { + identifierImpl := a.getIdentifierTypeImpl(identifierJSON, authenticationType, userIdentifier) + if identifierImpl == nil { + return nil, nil, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, nil) + } + + code := identifierImpl.getCode() + identifier := identifierImpl.getIdentifier() + + account, err := a.getAccount(code, identifier, apiKey, appTypeIdentifier, orgID) + if err != nil { + return nil, nil, errors.WrapErrorAction(logutils.ActionGet, model.TypeAccount, nil, err) + } + if account == nil { + return nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, nil) + } + + identifiers := account.GetVerifiedAccountIdentifiers() + for i, id := range identifiers { + if id.Sensitive { + idImpl := a.getIdentifierTypeImpl("", &id.Code, &id.Identifier) + if idImpl == nil { + return nil, nil, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, &logutils.FieldArgs{"code": id.Code}) + } + + masked, err := idImpl.maskIdentifier() + if err != nil { + l.Errorf("error masking identifier for sign-in options: %v", err) + continue + } + identifiers[i].Identifier = masked + } + } + return identifiers, account.AuthTypes, nil +} + // Refresh refreshes an access token using a refresh token // // Input: @@ -330,11 +396,12 @@ func (a *Auth) Refresh(refreshToken string, apiKey string, clientVersion *string authType := loginSession.AuthType.Code anonymous := loginSession.Anonymous - uid := "" name := "" email := "" phone := "" + username := "" permissions := []string{} + externalIDs := make(map[string]string) // - generate new params and update the account if needed(if external auth type) if loginSession.AuthType.IsExternal { @@ -350,33 +417,53 @@ func (a *Auth) Refresh(refreshToken string, apiKey string, clientVersion *string return nil, errors.WrapErrorAction(logutils.ActionRefresh, "external auth type", nil, err) } + if loginSession.Account == nil { + return nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, &logutils.FieldArgs{"session_id": loginSession.ID, "anonymous": false}) + } + + aats := loginSession.Account.GetAccountAuthTypes(loginSession.AuthType.Code) + if len(aats) != 1 { + return nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAccountAuthType, &logutils.FieldArgs{"code": loginSession.AuthType.Code, "count": len(aats)}) + } + //check if need to update the account data - newAccount, err := a.updateExternalUserIfNeeded(*loginSession.AccountAuthType, *externalUser, loginSession.AuthType, loginSession.AppOrg, externalCreds, l) + newAccount, err := a.updateExternalUserIfNeeded(aats[0], *externalUser, loginSession.AuthType, loginSession.AppOrg, externalCreds, l) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionUpdate, model.TypeExternalSystemUser, logutils.StringArgs("refresh"), err) } loginSession.Params = refreshedData //assign the refreshed data if newAccount != nil { - loginSession.ExternalIDs = newAccount.ExternalIDs + for _, external := range newAccount.GetExternalAccountIdentifiers() { + externalIDs[external.Code] = external.Identifier + } } } scopes := []string{authorization.ScopeGlobal} if !anonymous { - accountAuthType := loginSession.AccountAuthType - if accountAuthType == nil { - l.Infof("for some reasons account auth type is null for not anonymous login - %s", loginSession.ID) - return nil, errors.ErrorAction("for some reasons account auth type is null for not anonymous login", "", nil) - } - uid = accountAuthType.Identifier - name = accountAuthType.Account.Profile.GetFullName() - email = accountAuthType.Account.Profile.Email - phone = accountAuthType.Account.Profile.Phone - permissions = accountAuthType.Account.GetPermissionNames() - scopes = append(scopes, accountAuthType.Account.GetScopes()...) - } - claims := a.getStandardClaims(sub, uid, name, email, phone, rokwireTokenAud, orgID, appID, authType, loginSession.ExternalIDs, nil, anonymous, false, loginSession.AppOrg.Application.Admin, loginSession.AppOrg.Organization.System, false, true, loginSession.ID) + if loginSession.Account == nil { + return nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, &logutils.FieldArgs{"session_id": loginSession.ID, "anonymous": false}) + } + if emailIdentifier := loginSession.Account.GetAccountIdentifier(IdentifierTypeEmail, ""); emailIdentifier != nil { + email = emailIdentifier.Identifier + } + if phoneIdentifier := loginSession.Account.GetAccountIdentifier(IdentifierTypePhone, ""); phoneIdentifier != nil { + phone = phoneIdentifier.Identifier + } + if usernameIdentifier := loginSession.Account.GetAccountIdentifier(IdentifierTypeUsername, ""); usernameIdentifier != nil { + username = usernameIdentifier.Identifier + } + name = loginSession.Account.Profile.GetFullName() + permissions = loginSession.Account.GetPermissionNames() + scopes = append(scopes, loginSession.Account.GetScopes()...) + if len(externalIDs) == 0 { + for _, external := range loginSession.Account.GetExternalAccountIdentifiers() { + externalIDs[external.Code] = external.Identifier + } + } + } + claims := a.getStandardClaims(sub, name, email, phone, username, rokwireTokenAud, orgID, appID, authType, externalIDs, nil, anonymous, false, loginSession.AppOrg.Application.Admin, loginSession.AppOrg.Organization.System, false, true, loginSession.ID) accessToken, err := a.buildAccessToken(claims, strings.Join(permissions, ","), strings.Join(scopes, " ")) if err != nil { l.Infof("error generating acccess token on refresh - %s", refreshToken) @@ -384,11 +471,7 @@ func (a *Auth) Refresh(refreshToken string, apiKey string, clientVersion *string } loginSession.AccessToken = accessToken //set the generated token // - generate new refresh token - refreshToken, err = a.buildRefreshToken() - if err != nil { - l.Infof("error generating refresh token on refresh - %s", refreshToken) - return nil, errors.WrapErrorAction(logutils.ActionCreate, logutils.TypeToken, nil, err) - } + refreshToken = utils.GenerateRandomString(refreshTokenLength) if loginSession.RefreshTokens == nil { loginSession.RefreshTokens = make([]string, 0) } @@ -421,7 +504,7 @@ func (a *Auth) Refresh(refreshToken string, apiKey string, clientVersion *string // GetLoginURL returns a pre-formatted login url for SSO providers // // Input: -// authenticationType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") +// authenticationType (string): Name of the authentication method for provided creds (eg. "illinois_oidc") // appTypeIdentifier (string): Identifier of the app type/client that the user is logging in from // orgID (string): ID of the organization that the user is logging in // redirectURI (string): Registered redirect URI where client will receive response @@ -432,7 +515,7 @@ func (a *Auth) Refresh(refreshToken string, apiKey string, clientVersion *string // Params (map[string]interface{}): Params to be sent in subsequent request (if necessary) func (a *Auth) GetLoginURL(authenticationType string, appTypeIdentifier string, orgID string, redirectURI string, apiKey string, l *logs.Log) (string, map[string]interface{}, error) { //validate if the provided auth type is supported by the provided application and organization - authType, appType, _, err := a.validateAuthType(authenticationType, appTypeIdentifier, orgID) + authType, appType, _, err := a.validateAuthType(authenticationType, &appTypeIdentifier, nil, orgID) if err != nil { return "", nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, nil, err) } @@ -444,13 +527,13 @@ func (a *Auth) GetLoginURL(authenticationType string, appTypeIdentifier string, } //get the auth type implementation for the auth type - authImpl, err := a.getExternalAuthTypeImpl(*authType) + authImpl, err := a.getExternalAuthTypeImpl(authType.AuthType) if err != nil { return "", nil, errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, nil, err) } //get login URL - loginURL, params, err := authImpl.getLoginURL(*authType, *appType, redirectURI, l) + loginURL, params, err := authImpl.getLoginURL(authType.AuthType, *appType, redirectURI, l) if err != nil { return "", nil, errors.WrapErrorAction(logutils.ActionGet, "login url", nil, err) } @@ -564,55 +647,66 @@ func (a *Auth) LoginMFA(apiKey string, accountID string, sessionID string, ident } // CreateAdminAccount creates an account for a new admin user -func (a *Auth) CreateAdminAccount(authenticationType string, appID string, orgID string, identifier string, profile model.Profile, privacy model.Privacy, username string, - permissions []string, roleIDs []string, groupIDs []string, scopes []string, creatorPermissions []string, clientVersion *string, l *logs.Log) (*model.Account, map[string]interface{}, error) { - //TODO: add admin authentication policies that specify which auth types may be used for each app org - if authenticationType != AuthTypeOidc && authenticationType != AuthTypeEmail && !strings.HasSuffix(authenticationType, "_oidc") { - return nil, nil, errors.ErrorData(logutils.StatusInvalid, "auth type", nil) - } - +func (a *Auth) CreateAdminAccount(authenticationType string, appID string, orgID string, identifierJSON string, profile model.Profile, privacy model.Privacy, permissions []string, + roleIDs []string, groupIDs []string, scopes []string, creatorPermissions []string, clientVersion *string, l *logs.Log) (*model.Account, map[string]interface{}, error) { // check if the provided auth type is supported by the provided application and organization - authType, appOrg, err := a.validateAuthTypeForAppOrg(authenticationType, appID, orgID) + supportedAuthType, _, appOrg, err := a.validateAuthType(authenticationType, nil, &appID, orgID) if err != nil { return nil, nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, nil, err) } + //TODO: add admin authentication policies that specify which auth types may be used for each app org + if supportedAuthType.AuthType.Code != AuthTypeOidc && supportedAuthType.AuthType.Code != AuthTypePassword && !strings.HasSuffix(supportedAuthType.AuthType.Code, "_oidc") { + return nil, nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAuthType, nil) + } + + identifierImpl := a.getIdentifierTypeImpl(identifierJSON, nil, nil) + if identifierImpl == nil { + return nil, nil, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, nil) + } + identifier := identifierImpl.getIdentifier() + // create account - var accountAuthType *model.AccountAuthType var newAccount *model.Account var params map[string]interface{} transaction := func(context storage.TransactionContext) error { //1. check if the user exists - account, err := a.storage.FindAccount(context, appOrg.ID, authType.ID, identifier) + account, err := a.storage.FindAccount(context, appOrg.ID, identifierImpl.getCode(), identifier) if err != nil { return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) } if account != nil { - return errors.ErrorData(logutils.StatusFound, model.TypeAccount, &logutils.FieldArgs{"app_org_id": appOrg.ID, "auth_type": authType.Code, "identifier": identifier}) + return errors.ErrorData(logutils.StatusFound, model.TypeAccount, &logutils.FieldArgs{"app_org_id": appOrg.ID, "identifier": identifier}) } //2. account does not exist, so apply sign up profile.DateCreated = time.Now().UTC() - if authType.IsExternal { - externalUser := model.ExternalSystemUser{Identifier: identifier} - accountAuthType, err = a.applySignUpAdminExternal(context, *authType, *appOrg, externalUser, profile, privacy, username, permissions, roleIDs, groupIDs, scopes, creatorPermissions, clientVersion, l) - if err != nil { - return errors.WrapErrorAction(logutils.ActionRegister, "admin user", &logutils.FieldArgs{"auth_type": authType.Code, "identifier": identifier}, err) + if supportedAuthType.AuthType.IsExternal { + identityProviderID, _ := supportedAuthType.AuthType.Params["identity_provider"].(string) + identityProviderSetting := appOrg.FindIdentityProviderSetting(identityProviderID) + if identityProviderSetting == nil { + return errors.ErrorData(logutils.StatusMissing, model.TypeIdentityProviderConfig, &logutils.FieldArgs{"app_org": appOrg.ID, "identity_provider_id": identityProviderID}) } - } else { - authImpl, err := a.getAuthTypeImpl(*authType) + + externalIDs := make(map[string]string) + for k, v := range identityProviderSetting.ExternalIDFields { + if v == identityProviderSetting.UserIdentifierField { + externalIDs[k] = identifier + break + } + } + externalUser := model.ExternalSystemUser{Identifier: identifier, ExternalIDs: externalIDs, SensitiveExternalIDs: identityProviderSetting.SensitiveExternalIDs} + newAccount, err = a.applySignUpAdminExternal(context, *supportedAuthType, *appOrg, externalUser, profile, privacy, permissions, roleIDs, groupIDs, scopes, creatorPermissions, clientVersion, l) if err != nil { - return errors.WrapErrorAction(logutils.ActionLoadCache, typeExternalAuthType, nil, err) + return errors.WrapErrorAction(logutils.ActionRegister, "admin user", &logutils.FieldArgs{"auth_type": supportedAuthType.AuthType.Code, "identifier": identifier}, err) } - - profile.Email = identifier - params, accountAuthType, err = a.applySignUpAdmin(context, authImpl, account, *authType, *appOrg, identifier, "", profile, privacy, username, permissions, roleIDs, groupIDs, scopes, creatorPermissions, clientVersion, l) + } else { + params, newAccount, err = a.signUpNewAccount(context, identifierImpl, *supportedAuthType, *appOrg, nil, "", "", clientVersion, profile, privacy, nil, permissions, roleIDs, groupIDs, scopes, creatorPermissions, l) if err != nil { - return errors.WrapErrorAction(logutils.ActionRegister, "admin user", &logutils.FieldArgs{"auth_type": authType.Code, "identifier": identifier}, err) + return errors.WrapErrorAction(logutils.ActionRegister, "admin user", &logutils.FieldArgs{"auth_type": supportedAuthType.AuthType.Code, "identifier": identifier}, err) } } - newAccount = &accountAuthType.Account return nil } @@ -625,40 +719,39 @@ func (a *Auth) CreateAdminAccount(authenticationType string, appID string, orgID } // UpdateAdminAccount updates an existing user's account with new permissions, roles, and groups -func (a *Auth) UpdateAdminAccount(authenticationType string, appID string, orgID string, identifier string, permissions []string, roleIDs []string, +func (a *Auth) UpdateAdminAccount(authenticationType string, appID string, orgID string, identifierJSON string, permissions []string, roleIDs []string, groupIDs []string, scopes []string, updaterPermissions []string, l *logs.Log) (*model.Account, map[string]interface{}, error) { + // check if the provided auth type is supported by the provided application and organization + supportedAuthType, _, appOrg, err := a.validateAuthType(authenticationType, nil, &appID, orgID) + if err != nil { + return nil, nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, nil, err) + } + //TODO: when elevating existing accounts to application level admin, need to enforce any authentication policies set up for the app org // when demoting from application level admin to standard user, may want to inform user of applicable authentication policy changes - - if authenticationType != AuthTypeOidc && authenticationType != AuthTypeEmail && !strings.HasSuffix(authenticationType, "_oidc") { + if supportedAuthType.AuthType.Code != AuthTypeOidc && supportedAuthType.AuthType.Code != AuthTypePassword && !strings.HasSuffix(supportedAuthType.AuthType.Code, "_oidc") { return nil, nil, errors.ErrorData(logutils.StatusInvalid, "auth type", nil) } - // check if the provided auth type is supported by the provided application and organization - authType, appOrg, err := a.validateAuthTypeForAppOrg(authenticationType, appID, orgID) - if err != nil { - return nil, nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, nil, err) + identifierImpl := a.getIdentifierTypeImpl(identifierJSON, nil, nil) + if identifierImpl == nil { + return nil, nil, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, nil) } + identifier := identifierImpl.getIdentifier() var updatedAccount *model.Account var params map[string]interface{} transaction := func(context storage.TransactionContext) error { //1. check if the user exists - account, err := a.storage.FindAccount(context, appOrg.ID, authType.ID, identifier) + account, err := a.storage.FindAccount(context, appOrg.ID, identifierImpl.getCode(), identifier) if err != nil { return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) } if account == nil { - return errors.ErrorData(logutils.StatusMissing, model.TypeAccount, &logutils.FieldArgs{"app_org_id": appOrg.ID, "auth_type": authType.Code, "identifier": identifier}) - } - - //2. check if the user's auth type is verified - accountAuthType := account.GetAccountAuthType(authType.ID, identifier) - if accountAuthType == nil || accountAuthType.Unverified { - return errors.ErrorData("Unverified", model.TypeAccountAuthType, &logutils.FieldArgs{"app_org_id": appOrg.ID, "auth_type": authType.Code, "identifier": identifier}).SetStatus(utils.ErrorStatusUnverified) + return errors.ErrorData(logutils.StatusMissing, model.TypeAccount, &logutils.FieldArgs{"app_org_id": appOrg.ID, "auth_type": supportedAuthType.AuthType.Code, "identifier": identifier}) } - //3. update account permissions + //2. update account permissions updatedAccount = account updated := false revoked := false @@ -699,7 +792,7 @@ func (a *Auth) UpdateAdminAccount(authenticationType string, appID string, orgID updated = true } - //4. update account roles + //3. update account roles added, removed, unchanged = utils.StringListDiff(roleIDs, account.GetAssignedRoleIDs()) if len(added) > 0 || len(removed) > 0 { newRoles := []model.AppOrgRole{} @@ -737,7 +830,7 @@ func (a *Auth) UpdateAdminAccount(authenticationType string, appID string, orgID updated = true } - //5. update account groups + //4. update account groups added, removed, unchanged = utils.StringListDiff(groupIDs, account.GetAssignedGroupIDs()) if len(added) > 0 || len(removed) > 0 { newGroups := []model.AppOrgGroup{} @@ -775,7 +868,7 @@ func (a *Auth) UpdateAdminAccount(authenticationType string, appID string, orgID updated = true } - //6. update account scopes + //5. update account scopes if scopes != nil && utils.Contains(updaterPermissions, model.UpdateScopesPermission) && !utils.DeepEqual(account.Scopes, scopes) { for i, scope := range scopes { parsedScope, err := authorization.ScopeFromString(scope) @@ -796,7 +889,7 @@ func (a *Auth) UpdateAdminAccount(authenticationType string, appID string, orgID updated = true } - //7. delete active login sessions if anything was revoked + //6. delete active login sessions if anything was revoked if revoked { err = a.storage.DeleteLoginSessionsByIdentifier(context, account.ID) if err != nil { @@ -824,7 +917,7 @@ func (a *Auth) UpdateAdminAccount(authenticationType string, appID string, orgID func (a *Auth) CreateAnonymousAccount(context storage.TransactionContext, appID string, orgID string, anonymousID string, preferences map[string]interface{}, systemConfigs map[string]interface{}, skipExistsCheck bool, l *logs.Log) (*model.Account, error) { // check if the provided auth type is supported by the provided application and organization - authType, appOrg, err := a.validateAuthTypeForAppOrg(AuthTypeAnonymous, appID, orgID) + supportedAuthType, _, appOrg, err := a.validateAuthType(AuthTypeAnonymous, nil, &appID, orgID) if err != nil || appOrg == nil { return nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, nil, err) } @@ -839,7 +932,7 @@ func (a *Auth) CreateAnonymousAccount(context storage.TransactionContext, appID return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) } if account != nil { - return errors.ErrorData(logutils.StatusFound, model.TypeAccount, &logutils.FieldArgs{"app_org_id": appOrg.ID, "auth_type": authType.Code, "account_id": anonymousID}) + return errors.ErrorData(logutils.StatusFound, model.TypeAccount, &logutils.FieldArgs{"app_org_id": appOrg.ID, "auth_type": supportedAuthType.AuthType.Code, "account_id": anonymousID}) } } @@ -863,43 +956,92 @@ func (a *Auth) CreateAnonymousAccount(context storage.TransactionContext, appID return newAccount, nil } -// VerifyCredential verifies credential (checks the verification code in the credentials collection) -func (a *Auth) VerifyCredential(id string, verification string, l *logs.Log) error { - credential, err := a.storage.FindCredential(nil, id) - if err != nil || credential == nil { - return errors.WrapErrorAction(logutils.ActionFind, model.TypeCredential, nil, err) +// VerifyIdentifier verifies credential (checks the verification code in the credentials collection) +func (a *Auth) VerifyIdentifier(id string, verification string, l *logs.Log) (*model.AccountIdentifier, error) { + //get the auth type + account, err := a.storage.FindAccountByIdentifierID(nil, id) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) } + if account == nil { + return nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, &logutils.FieldArgs{"identifiers.id": id}) + } + a.setLogContext(account, l) - if credential.Verified { - return errors.ErrorAction(logutils.ActionVerify, model.TypeCredential, logutils.StringArgs("already verified")) + accountIdentifier := account.GetAccountIdentifierByID(id) + if accountIdentifier == nil { + return nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccountIdentifier, &logutils.FieldArgs{"id": id}) + } + if accountIdentifier.Verified { + return accountIdentifier, nil } - //get the auth type - authType, err := a.storage.FindAuthType(credential.AuthType.ID) - if err != nil || authType == nil { - return errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, logutils.StringArgs(credential.AuthType.ID), err) + identifierImpl := a.getIdentifierTypeImpl("", &accountIdentifier.Code, &accountIdentifier.Identifier) + if identifierImpl == nil { + return nil, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, &logutils.FieldArgs{"code": accountIdentifier.Code, "identifier": accountIdentifier.Identifier}) } - if !authType.UseCredentials { - return errors.ErrorData(logutils.StatusInvalid, model.TypeAuthType, logutils.StringArgs("credential verification")) + + if identifierChannel, ok := identifierImpl.(authCommunicationChannel); ok { + err = identifierChannel.verifyIdentifier(accountIdentifier, verification) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionValidate, "verification code", nil, err) + } + } else { + return nil, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, logutils.StringArgs(accountIdentifier.Code)) } - authImpl, err := a.getAuthTypeImpl(*authType) + return accountIdentifier, a.updateAccountIdentifier(nil, account, accountIdentifier) +} + +// SendVerifyIdentifier sends the verification code to the identifier +func (a *Auth) SendVerifyIdentifier(appTypeIdentifier string, orgID string, apiKey string, identifierJSON string, l *logs.Log) error { + //validate if the provided auth type is supported by the provided application and organization + _, appOrg, err := a.validateAppOrg(&appTypeIdentifier, nil, orgID) if err != nil { - return errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, nil, err) + return errors.WrapErrorAction(logutils.ActionValidate, model.TypeApplicationOrganization, nil, err) } - authTypeCreds, err := authImpl.verifyCredential(credential, verification, l) - if err != nil || authTypeCreds == nil { - return errors.WrapErrorAction(logutils.ActionValidate, "verification code", nil, err) + //validate api key before making db calls + err = a.validateAPIKey(apiKey, appOrg.Application.ID) + if err != nil { + return errors.WrapErrorData(logutils.StatusInvalid, model.TypeAPIKey, nil, err) } - credential.Verified = true - credential.Value = authTypeCreds - if err = a.storage.UpdateCredential(nil, credential); err != nil { - return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeCredential, nil, err) + identifierImpl := a.getIdentifierTypeImpl(identifierJSON, nil, nil) + if identifierImpl == nil { + return errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, nil) } - return nil + code := identifierImpl.getCode() + identifier := identifierImpl.getIdentifier() + + account, err := a.storage.FindAccount(nil, appOrg.ID, code, identifier) + if err != nil { + return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) + } + if account == nil { + return errors.ErrorData(logutils.StatusMissing, model.TypeAccount, nil) + } + a.setLogContext(account, l) + + accountIdentifier := account.GetAccountIdentifier(code, identifier) + if accountIdentifier == nil { + return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccountIdentifier, &logutils.FieldArgs{"identifier": identifier}, err) + } + if accountIdentifier.Verified { + return errors.ErrorData(logutils.StatusInvalid, "identifier verification status", &logutils.FieldArgs{"verified": true}) + } + + if identifierChannel, ok := identifierImpl.(authCommunicationChannel); ok { + _, err = identifierChannel.sendVerifyIdentifier(accountIdentifier, appOrg.Application.Name) + if err != nil { + return errors.WrapErrorAction(logutils.ActionSend, "verification code", nil, err) + } + } else { + return errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, logutils.StringArgs(accountIdentifier.Code)) + } + + return a.updateAccountIdentifier(nil, account, accountIdentifier) } // UpdateCredential updates the credential object with the new value @@ -924,29 +1066,27 @@ func (a *Auth) UpdateCredential(accountID string, accountAuthTypeID string, para return errors.WrapErrorAction(logutils.ActionFind, model.TypeAuthType, nil, err) } if accountAuthType.Credential == nil { - return errors.ErrorData(logutils.StatusMissing, model.TypeCredential, logutils.StringArgs("reset password")) + return errors.ErrorData(logutils.StatusMissing, model.TypeCredential, nil) } - credential := accountAuthType.Credential //Determine the auth type for resetPassword - authType := accountAuthType.AuthType - if !authType.UseCredentials { - return errors.ErrorData(logutils.StatusInvalid, model.TypeAuthType, logutils.StringArgs("reset password")) + if !accountAuthType.SupportedAuthType.AuthType.UseCredentials { + return errors.ErrorData(logutils.StatusInvalid, model.TypeAuthType, nil) } - authImpl, err := a.getAuthTypeImpl(authType) + authImpl, err := a.getAuthTypeImpl(accountAuthType.SupportedAuthType) if err != nil { return errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, nil, err) } - authTypeCreds, err := authImpl.resetCredential(credential, nil, params, l) + authTypeCreds, err := authImpl.resetCredential(accountAuthType.Credential, nil, params) if err != nil || authTypeCreds == nil { - return errors.WrapErrorAction(logutils.ActionValidate, "reset password", nil, err) + return errors.WrapErrorAction(logutils.ActionValidate, "reset credential", nil, err) } //Update the credential with new password - credential.Value = authTypeCreds - if err = a.storage.UpdateCredential(nil, credential); err != nil { + accountAuthType.Credential.Value = authTypeCreds + if err = a.storage.UpdateCredentialValue(accountAuthType.Credential.ID, accountAuthType.Credential.Value); err != nil { return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeCredential, nil, err) } @@ -970,12 +1110,21 @@ func (a *Auth) ResetForgotCredential(credsID string, resetCode string, params st return errors.WrapErrorAction(logutils.ActionFind, model.TypeCredential, nil, err) } - //Determine the auth type for resetPassword - authType, err := a.storage.FindAuthType(credential.AuthType.ID) + //get account by the credential ID (this is valid for now because there are no credentials shared between app orgs) + account, err := a.storage.FindAccountByCredentialID(nil, credsID) + if err != nil { + return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, &logutils.FieldArgs{"credential_id": credsID}, err) + } + if account == nil { + return errors.ErrorData(logutils.StatusMissing, model.TypeAccount, &logutils.FieldArgs{"credential_id": credsID}) + } + + //validate if the provided auth type is supported by the provided application and organization + authType, _, _, err := a.validateAuthType(credential.AuthType.ID, nil, &account.AppOrg.Application.ID, account.AppOrg.Organization.ID) if err != nil || authType == nil { - return errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, logutils.StringArgs(credential.AuthType.ID), err) + return errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, nil, err) } - if !authType.UseCredentials { + if !authType.AuthType.UseCredentials { return errors.ErrorData(logutils.StatusInvalid, model.TypeAuthType, logutils.StringArgs("reset forgot credential")) } @@ -984,13 +1133,13 @@ func (a *Auth) ResetForgotCredential(credsID string, resetCode string, params st return errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, nil, err) } - authTypeCreds, err := authImpl.resetCredential(credential, &resetCode, params, l) + authTypeCreds, err := authImpl.resetCredential(credential, &resetCode, params) if err != nil || authTypeCreds == nil { return errors.WrapErrorAction(logutils.ActionValidate, model.TypeCredential, nil, err) } //Update the credential with new password credential.Value = authTypeCreds - if err = a.storage.UpdateCredential(nil, credential); err != nil { + if err = a.storage.UpdateCredentialValue(credential.ID, credential.Value); err != nil { return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeCredential, nil, err) } @@ -1000,16 +1149,17 @@ func (a *Auth) ResetForgotCredential(credsID string, resetCode string, params st // ForgotCredential initiate forgot credential process (generates a reset link and sends to the given identifier for email auth type) // // Input: -// authenticationType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") -// identifier: identifier of the account auth type +// authenticationType (string): Name of the authentication method for provided creds (eg. "password") +// identifierJSON (string): JSON string of the user's identifier and the identifier code // appTypeIdentifier (string): Identifier of the app type/client that the user is logging in from // orgID (string): ID of the organization that the user is logging in // apiKey (string): API key to validate the specified app +// userIdentifier (*string): Optional user identifier for backwards compatibility // Returns: // error: if any -func (a *Auth) ForgotCredential(authenticationType string, appTypeIdentifier string, orgID string, apiKey string, identifier string, l *logs.Log) error { +func (a *Auth) ForgotCredential(authenticationType string, identifierJSON string, appTypeIdentifier string, orgID string, apiKey string, l *logs.Log) error { //validate if the provided auth type is supported by the provided application and organization - authType, _, appOrg, err := a.validateAuthType(authenticationType, appTypeIdentifier, orgID) + authType, _, appOrg, err := a.validateAuthType(authenticationType, &appTypeIdentifier, nil, orgID) if err != nil || authType == nil || appOrg == nil { return errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, nil, err) } @@ -1027,95 +1177,67 @@ func (a *Auth) ForgotCredential(authenticationType string, appTypeIdentifier str } //check if the auth types uses credentials - if !authType.UseCredentials { + if !authType.AuthType.UseCredentials { return errors.ErrorData(logutils.StatusInvalid, model.TypeAuthType, logutils.StringArgs("credential reset")) } - authImpl, err := a.getAuthTypeImpl(*authType) - if err != nil { - return errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, nil, err) + identifierImpl := a.getIdentifierTypeImpl(identifierJSON, nil, nil) + if identifierImpl == nil { + return errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, nil) } - authTypeID := authType.ID + + code := identifierImpl.getCode() + identifier := identifierImpl.getIdentifier() //Find the credential for setting reset code and expiry and sending credID in reset link - account, err := a.storage.FindAccount(nil, appOrg.ID, authTypeID, identifier) + account, err := a.storage.FindAccount(nil, appOrg.ID, code, identifier) if err != nil { return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) } - - accountAuthType, err := a.findAccountAuthType(account, authType, identifier) - if accountAuthType == nil { - return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccountAuthType, nil, err) - } - credential := accountAuthType.Credential - if credential == nil { - return errors.ErrorData(logutils.StatusMissing, model.TypeCredential, logutils.StringArgs("credential reset")) + if account == nil { + return errors.ErrorData(logutils.StatusMissing, model.TypeAccount, &logutils.FieldArgs{"app_org_id": appOrg.ID, "identifier": identifier}) } a.setLogContext(account, l) - //do not allow to reset credential for unverified credentials - err = a.checkCredentialVerified(authImpl, accountAuthType, l) - if err != nil { - return err + accountIdentifier := account.GetAccountIdentifier(code, identifier) + if accountIdentifier == nil { + return errors.ErrorData(logutils.StatusMissing, model.TypeAccountIdentifier, &logutils.FieldArgs{"identifier": identifier}) } - authTypeCreds, err := authImpl.forgotCredential(credential, identifier, appOrg.Application.Name, l) - if err != nil || authTypeCreds == nil { - return errors.WrapErrorAction(logutils.ActionValidate, "forgot password", nil, err) + accountAuthTypes, err := a.findAccountAuthTypesAndCredentials(account, *authType) + if len(accountAuthTypes) == 0 { + return errors.ErrorData(logutils.StatusMissing, model.TypeAccountAuthType, &logutils.FieldArgs{"auth_type": authType.AuthType.Code, "identifier": identifier}) } - - //Update the credential with reset code and expiry - credential.Value = authTypeCreds - if err = a.storage.UpdateCredential(nil, credential); err != nil { - return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeCredential, nil, err) + if len(accountAuthTypes) > 1 { + return errors.ErrorData(logutils.StatusInvalid, model.TypeAccountAuthType, &logutils.FieldArgs{"auth_type": authType.AuthType.Code, "identifier": identifier, "count": len(accountAuthTypes)}) } - return nil -} -// SendVerifyCredential sends the verification code to the identifier -func (a *Auth) SendVerifyCredential(authenticationType string, appTypeIdentifier string, orgID string, apiKey string, identifier string, l *logs.Log) error { - //validate if the provided auth type is supported by the provided application and organization - authType, _, appOrg, err := a.validateAuthType(authenticationType, appTypeIdentifier, orgID) - if err != nil || authType == nil || appOrg == nil { - return errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, nil, err) - } - //validate api key before making db calls - err = a.validateAPIKey(apiKey, appOrg.Application.ID) - if err != nil { - return errors.WrapErrorData(logutils.StatusInvalid, model.TypeAPIKey, nil, err) - } - - if !authType.UseCredentials { - return errors.ErrorData(logutils.StatusInvalid, model.TypeAuthType, logutils.StringArgs("credential verification code")) + credential := accountAuthTypes[0].Credential + if credential == nil { + return errors.ErrorData(logutils.StatusMissing, model.TypeCredential, logutils.StringArgs("credential reset")) } + a.setLogContext(account, l) authImpl, err := a.getAuthTypeImpl(*authType) if err != nil { return errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, nil, err) } - account, err := a.storage.FindAccount(nil, appOrg.ID, authType.ID, identifier) + + err = identifierImpl.checkVerified(accountIdentifier, appOrg.Application.Name) if err != nil { - return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) - } - accountAuthType, err := a.findAccountAuthType(account, authType, identifier) - if accountAuthType == nil { - return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccountAuthType, nil, err) - } - a.setLogContext(account, l) - credential := accountAuthType.Credential - if credential == nil { - return errors.ErrorData(logutils.StatusMissing, model.TypeCredential, logutils.StringArgs("credential verification code")) + return err } - if credential.Verified { - return errors.ErrorData(logutils.StatusInvalid, "credential verification status", &logutils.FieldArgs{"verified": true}) + authTypeCreds, err := authImpl.forgotCredential(identifierImpl, credential, *appOrg) + if err != nil || authTypeCreds == nil { + return errors.WrapErrorAction(logutils.ActionValidate, "forgot password", nil, err) } - err = authImpl.sendVerifyCredential(credential, appOrg.Application.Name, l) - if err != nil { - return errors.WrapErrorAction(logutils.ActionSend, "verification code", nil, err) + //Update the credential with reset code and expiry + credential.Value = authTypeCreds + if err = a.storage.UpdateCredentialValue(credential.ID, credential.Value); err != nil { + return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeCredential, nil, err) } - return nil } @@ -1668,7 +1790,7 @@ func (a *Auth) GetAdminToken(claims tokenauth.Claims, appID string, orgID string return "", errors.ErrorData(logutils.StatusMissing, model.TypeApplicationOrganization, &logutils.FieldArgs{"org_id": orgID, "app_id": appID}) } - adminClaims := a.getStandardClaims(claims.Subject, claims.UID, claims.Name, claims.Email, claims.Phone, claims.Audience, orgID, appID, claims.AuthType, + adminClaims := a.getStandardClaims(claims.Subject, claims.Name, claims.Email, claims.Phone, claims.Username, claims.Audience, orgID, appID, claims.AuthType, claims.ExternalIDs, &claims.ExpiresAt, false, false, true, claims.System, claims.Service, claims.FirstParty, claims.SessionID) return a.buildAccessToken(adminClaims, claims.Permissions, claims.Scope) } @@ -1678,8 +1800,8 @@ func (a *Auth) GetAdminToken(claims tokenauth.Claims, appID string, orgID string // // Input: // accountID (string): ID of the account to link the creds to -// authenticationType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") -// appTypeIdentifier (string): identifier of the app type/client that the user is logging in from +// authenticationType (string): Name of the authentication method for provided creds (eg. "password", "webauthn", "illinois_oidc") +// appTypeIdentifier (string): Identifier of the app type/client that the user is logging in from // creds (string): Credentials/JSON encoded credential structure defined for the specified auth type // params (string): JSON encoded params defined by specified auth type // l (*logs.Log): Log object pointer for request @@ -1687,7 +1809,7 @@ func (a *Auth) GetAdminToken(claims tokenauth.Claims, appID string, orgID string // message (*string): response message // account (*model.Account): account data after the operation func (a *Auth) LinkAccountAuthType(accountID string, authenticationType string, appTypeIdentifier string, creds string, params string, l *logs.Log) (*string, *model.Account, error) { - message := "" + var message *string var newAccountAuthType *model.AccountAuthType account, err := a.storage.FindAccountByID(nil, accountID) @@ -1695,26 +1817,32 @@ func (a *Auth) LinkAccountAuthType(accountID string, authenticationType string, return nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) } if account == nil { - return nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, &logutils.FieldArgs{"id": accountID}) + return nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, &logutils.FieldArgs{"id": accountID}).SetStatus(utils.ErrorStatusNotFound) } //validate if the provided auth type is supported by the provided application and organization - authType, appType, appOrg, err := a.validateAuthType(authenticationType, appTypeIdentifier, account.AppOrg.Organization.ID) + authType, appType, appOrg, err := a.validateAuthType(authenticationType, &appTypeIdentifier, nil, account.AppOrg.Organization.ID) if err != nil || authType == nil || appType == nil || appOrg == nil { - return nil, nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, nil, err) + return nil, nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, nil, err).SetStatus(utils.ErrorStatusNotAllowed) } - if authType.IsAnonymous { + if authType.AuthType.IsAnonymous { return nil, nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAuthType, &logutils.FieldArgs{"anonymous": true}) - } else if authType.IsExternal { - newAccountAuthType, err = a.linkAccountAuthTypeExternal(*account, *authType, *appType, *appOrg, creds, params, l) + } else if authType.AuthType.IsExternal { + // only one account auth type per each external auth type is allowed + externalAats := account.GetAccountAuthTypes(authType.AuthType.Code) + if len(externalAats) > 0 { + return nil, nil, errors.ErrorData(logutils.StatusFound, model.TypeAuthType, &logutils.FieldArgs{"allow_multiple": false, "code": authType.AuthType.Code}) + } + + newAccountAuthType, err = a.linkAccountAuthTypeExternal(account, *authType, *appType, *appOrg, creds, params, l) if err != nil { - return nil, nil, errors.WrapErrorAction("linking", model.TypeCredential, nil, err) + return nil, nil, errors.WrapErrorAction("linking", model.TypeAccountAuthType, nil, err) } } else { - message, newAccountAuthType, err = a.linkAccountAuthType(*account, *authType, *appOrg, creds, params, l) + message, newAccountAuthType, err = a.linkAccountAuthType(account, *authType, *appOrg, appType, creds, params) if err != nil { - return nil, nil, errors.WrapErrorAction("linking", model.TypeCredential, nil, err) + return nil, nil, errors.WrapErrorAction("linking", model.TypeAccountAuthType, nil, err) } } @@ -1722,7 +1850,7 @@ func (a *Auth) LinkAccountAuthType(accountID string, authenticationType string, account.AuthTypes = append(account.AuthTypes, *newAccountAuthType) } - return &message, account, nil + return message, account, nil } // UnlinkAccountAuthType unlinks credentials from an existing account. @@ -1730,14 +1858,56 @@ func (a *Auth) LinkAccountAuthType(accountID string, authenticationType string, // // Input: // accountID (string): ID of the account to unlink creds from -// authenticationType (string): Name of the authentication method of account auth type to unlink -// appTypeIdentifier (string): Identifier of the app type/client that the user is logging in from -// identifier (string): Identifier of account auth type to unlink +// accountAuthTypeID (*string): Account auth type to unlink +// authenticationType (*string): Name of the authentication method of account auth type to unlink +// identifier (*string): Identifier to unlink // l (*logs.Log): Log object pointer for request // Returns: // account (*model.Account): account data after the operation -func (a *Auth) UnlinkAccountAuthType(accountID string, authenticationType string, appTypeIdentifier string, identifier string, l *logs.Log) (*model.Account, error) { - return a.unlinkAccountAuthType(accountID, authenticationType, appTypeIdentifier, identifier, l) +func (a *Auth) UnlinkAccountAuthType(accountID string, accountAuthTypeID *string, authenticationType *string, identifier *string, admin bool, l *logs.Log) (*model.Account, error) { + return a.unlinkAccountAuthType(accountID, accountAuthTypeID, authenticationType, identifier, admin) +} + +// LinkAccountIdentifier links an identifier to an existing account. +func (a *Auth) LinkAccountIdentifier(accountID string, identifierJSON string, admin bool, l *logs.Log) (*string, *model.Account, error) { + identifierImpl := a.getIdentifierTypeImpl(identifierJSON, nil, nil) + if identifierImpl == nil { + return nil, nil, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, nil) + } + + if identifierImpl.getCode() == IdentifierTypeExternal && !admin { + return nil, nil, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, logutils.StringArgs(IdentifierTypeExternal)).SetStatus(utils.ErrorStatusNotAllowed) + } + + account, err := a.storage.FindAccountByID(nil, accountID) + if err != nil { + return nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) + } + if account == nil { + return nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, nil).SetStatus(utils.ErrorStatusNotFound) + } + + message, err := a.linkAccountIdentifier(nil, account, identifierImpl) + if err != nil { + return nil, nil, errors.WrapErrorAction("linking", model.TypeAccountIdentifier, nil, err) + } + + return message, account, nil +} + +// UnlinkAccountIdentifier unlinks an identifier from an existing account. +func (a *Auth) UnlinkAccountIdentifier(accountID string, accountIdentifierID string, admin bool, l *logs.Log) (*model.Account, error) { + account, err := a.storage.FindAccountByID(nil, accountID) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) + } + if account == nil { + return nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, nil) + } + + err = a.unlinkAccountIdentifier(nil, account, &accountIdentifierID, nil, admin) + + return account, nil } // DeleteAccount deletes an account for the given id @@ -1766,23 +1936,30 @@ func (a *Auth) DeleteAccount(id string) error { // InitializeSystemAccount initializes the first system account func (a *Auth) InitializeSystemAccount(context storage.TransactionContext, authType model.AuthType, appOrg model.ApplicationOrganization, allSystemPermission string, email string, password string, clientVersion string, l *logs.Log) (string, error) { - //auth type - authImpl, err := a.getAuthTypeImpl(authType) + now := time.Now().UTC() + profile := model.Profile{ID: uuid.NewString(), DateCreated: now} + privacy := model.Privacy{Public: false} + permissions := []string{allSystemPermission} + + credsMap := passwordCreds{Password: password} + credsBytes, err := json.Marshal(credsMap) if err != nil { - return "", errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, nil, err) + return "", errors.WrapErrorAction(logutils.ActionMarshal, typePasswordCreds, nil, err) } + creds := string(credsBytes) - now := time.Now() - profile := model.Profile{ID: uuid.NewString(), Email: email, DateCreated: now} - privacy := model.Privacy{Public: false} - permissions := []string{allSystemPermission} + code := IdentifierTypeEmail + identifierImpl := a.getIdentifierTypeImpl("", &code, &email) + if identifierImpl == nil { + return "", errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, &logutils.FieldArgs{"code": code, "identifier": email}) + } - _, accountAuthType, err := a.applySignUpAdmin(context, authImpl, nil, authType, appOrg, email, password, profile, privacy, "", permissions, nil, nil, nil, permissions, &clientVersion, l) + _, account, err := a.signUpNewAccount(context, identifierImpl, model.SupportedAuthType{AuthType: authType}, appOrg, nil, creds, "", &clientVersion, profile, privacy, nil, permissions, nil, nil, nil, permissions, l) if err != nil { return "", errors.WrapErrorAction(logutils.ActionRegister, "initial system user", &logutils.FieldArgs{"email": email}, err) } - return accountAuthType.Account.ID, nil + return account.ID, nil } // GrantAccountPermissions grants new permissions to an account after validating the assigner has required permissions diff --git a/core/auth/auth.go b/core/auth/auth.go index 73f19a837..ad7426cfe 100644 --- a/core/auth/auth.go +++ b/core/auth/auth.go @@ -16,6 +16,7 @@ package auth import ( "core-building-block/core/model" + "core-building-block/driven/phoneverifier" "core-building-block/driven/storage" "core-building-block/utils" "encoding/json" @@ -57,13 +58,19 @@ const ( // UpdateScopesPermission is the permission that allows an admin to update account/role scopes UpdateScopesPermission string = "update_auth_scopes" + defaultIllinoisOIDCIdentifier string = "uin" + illinoisOIDCCode string = "illinois_oidc" + typeMail logutils.MessageDataType = "mail" + typeIdentifierType logutils.MessageDataType = "identifier type" typeExternalAuthType logutils.MessageDataType = "external auth type" typeAnonymousAuthType logutils.MessageDataType = "anonymous auth type" typeServiceAuthType logutils.MessageDataType = "service auth type" typeAuth logutils.MessageDataType = "auth" typeAuthRefreshParams logutils.MessageDataType = "auth refresh params" + typeVerificationCode string = "verification code" + refreshTokenLength int = 256 sessionDeletePeriod int = 24 // hours @@ -82,11 +89,13 @@ const ( // Auth represents the auth functionality unit type Auth struct { - storage Storage - emailer Emailer + storage Storage + emailer Emailer + phoneVerifier PhoneVerifier logger *logs.Logger + identifierTypes map[string]identifierType authTypes map[string]authType externalAuthTypes map[string]externalAuthType anonymousAuthTypes map[string]anonymousAuthType @@ -120,9 +129,8 @@ type Auth struct { } // NewAuth creates a new auth instance -func NewAuth(serviceID string, host string, authPrivKey *keys.PrivKey, authService *authservice.AuthService, storage Storage, emailer Emailer, minTokenExp *int64, - maxTokenExp *int64, supportLegacySigs bool, twilioAccountSID string, twilioToken string, twilioServiceSID string, profileBB ProfileBuildingBlock, - smtpHost string, smtpPortNum int, smtpUser string, smtpPassword string, smtpFrom string, logger *logs.Logger, version string) (*Auth, error) { +func NewAuth(serviceID string, host string, authPrivKey *keys.PrivKey, authService *authservice.AuthService, storage Storage, emailer Emailer, phoneVerifier PhoneVerifier, + profileBB ProfileBuildingBlock, minTokenExp *int64, maxTokenExp *int64, supportLegacySigs bool, version string, logger *logs.Logger) (*Auth, error) { if minTokenExp == nil { var minTokenExpVal int64 = 5 minTokenExp = &minTokenExpVal @@ -133,6 +141,7 @@ func NewAuth(serviceID string, host string, authPrivKey *keys.PrivKey, authServi maxTokenExp = &maxTokenExpVal } + identifierTypes := map[string]identifierType{} authTypes := map[string]authType{} externalAuthTypes := map[string]externalAuthType{} anonymousAuthTypes := map[string]anonymousAuthType{} @@ -147,10 +156,11 @@ func NewAuth(serviceID string, host string, authPrivKey *keys.PrivKey, authServi deleteSessionsTimerDone := make(chan bool) - auth := &Auth{storage: storage, emailer: emailer, logger: logger, authTypes: authTypes, externalAuthTypes: externalAuthTypes, anonymousAuthTypes: anonymousAuthTypes, - serviceAuthTypes: serviceAuthTypes, mfaTypes: mfaTypes, authPrivKey: authPrivKey, ServiceRegManager: nil, serviceID: serviceID, host: host, minTokenExp: *minTokenExp, - maxTokenExp: *maxTokenExp, profileBB: profileBB, cachedIdentityProviders: cachedIdentityProviders, identityProvidersLock: identityProvidersLock, - apiKeys: apiKeys, apiKeysLock: apiKeysLock, deleteSessionsTimerDone: deleteSessionsTimerDone, version: version} + auth := &Auth{storage: storage, emailer: emailer, phoneVerifier: phoneVerifier, logger: logger, identifierTypes: identifierTypes, authTypes: authTypes, + externalAuthTypes: externalAuthTypes, anonymousAuthTypes: anonymousAuthTypes, serviceAuthTypes: serviceAuthTypes, mfaTypes: mfaTypes, authPrivKey: authPrivKey, + ServiceRegManager: nil, serviceID: serviceID, host: host, minTokenExp: *minTokenExp, maxTokenExp: *maxTokenExp, profileBB: profileBB, + cachedIdentityProviders: cachedIdentityProviders, identityProvidersLock: identityProvidersLock, apiKeys: apiKeys, apiKeysLock: apiKeysLock, + deleteSessionsTimerDone: deleteSessionsTimerDone, version: version} err := auth.storeCoreRegs() if err != nil { @@ -175,13 +185,19 @@ func NewAuth(serviceID string, host string, authPrivKey *keys.PrivKey, authServi auth.SignatureAuth = signatureAuth + // identifier types + initUsernameIdentifier(auth) + initEmailIdentifier(auth) + initPhoneIdentifier(auth) + initExternalIdentifier(auth) + // auth types - initUsernameAuth(auth) - initEmailAuth(auth) - initPhoneAuth(auth, twilioAccountSID, twilioToken, twilioServiceSID) - initFirebaseAuth(auth) initAnonymousAuth(auth) - initSignatureAuth(auth) + initPasswordAuth(auth) + initCodeAuth(auth) + initWebAuthnAuth(auth) + // initFirebaseAuth(auth) + // initSignatureAuth(auth) // external auth types initOidcAuth(auth) @@ -216,99 +232,102 @@ func (a *Auth) SetIdentityBB(identityBB IdentityBuildingBlock) { a.identityBB = identityBB } -func (a *Auth) applyExternalAuthType(authType model.AuthType, appType model.ApplicationType, appOrg model.ApplicationOrganization, creds string, params string, clientVersion *string, - regProfile model.Profile, privacy model.Privacy, regPreferences map[string]interface{}, username string, admin bool, l *logs.Log) (*model.AccountAuthType, map[string]interface{}, []model.MFAType, map[string]string, error) { - var accountAuthType *model.AccountAuthType +func (a *Auth) applyExternalAuthType(supportedAuthType model.SupportedAuthType, appType model.ApplicationType, appOrg model.ApplicationOrganization, creds string, params string, clientVersion *string, + regProfile model.Profile, privacy model.Privacy, regPreferences map[string]interface{}, admin bool, l *logs.Log) (map[string]interface{}, *model.Account, []model.MFAType, error) { + var newAccount *model.Account var mfaTypes []model.MFAType - var externalIDs map[string]string //external auth type - authImpl, err := a.getExternalAuthTypeImpl(authType) + authImpl, err := a.getExternalAuthTypeImpl(supportedAuthType.AuthType) if err != nil { - return nil, nil, nil, nil, errors.WrapErrorAction(logutils.ActionLoadCache, typeExternalAuthType, nil, err) + return nil, nil, nil, errors.WrapErrorAction(logutils.ActionLoadCache, typeExternalAuthType, nil, err) } //1. get the user from the external system //var externalUser *model.ExternalSystemUser - externalUser, extParams, externalCreds, err := authImpl.externalLogin(authType, appType, appOrg, creds, params, l) + externalUser, extParams, externalCreds, err := authImpl.externalLogin(supportedAuthType.AuthType, appType, appOrg, creds, params, l) if err != nil { - return nil, nil, nil, nil, errors.WrapErrorAction("logging in", "external user", nil, err) + return nil, nil, nil, errors.WrapErrorAction("logging in", "external user", nil, err) } //2. check if the user exists - account, err := a.storage.FindAccount(nil, appOrg.ID, authType.ID, externalUser.Identifier) + // get the correct code for the external identifier from the external IDs map + code := "" + for k, v := range externalUser.ExternalIDs { + if v == externalUser.Identifier { + code = k + } + } + if code == "" && externalUser.Email == externalUser.Identifier { + code = IdentifierTypeEmail + } + + account, err := a.storage.FindAccount(nil, appOrg.ID, code, externalUser.Identifier) if err != nil { - return nil, nil, nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) + return nil, nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) } a.setLogContext(account, l) - canSignIn := a.canSignIn(account, authType.ID, externalUser.Identifier) + canSignIn := a.canSignIn(account, code, externalUser.Identifier) if canSignIn { //account exists - accountAuthType, err = a.applySignInExternal(account, authType, appOrg, *externalUser, externalCreds, l) + newAccount, err = a.applySignInExternal(account, supportedAuthType, appOrg, *externalUser, externalCreds, l) if err != nil { - return nil, nil, nil, nil, errors.WrapErrorAction(logutils.ActionApply, "external sign in", nil, err) + return nil, nil, nil, errors.WrapErrorAction(logutils.ActionApply, "external sign in", nil, err) } mfaTypes = account.GetVerifiedMFATypes() - externalIDs = account.ExternalIDs } else if !admin { //user does not exist, we need to register it - accountAuthType, err = a.applySignUpExternal(nil, authType, appOrg, *externalUser, externalCreds, regProfile, privacy, regPreferences, username, clientVersion, l) + newAccount, err = a.applySignUpExternal(nil, supportedAuthType, appOrg, *externalUser, externalCreds, regProfile, privacy, regPreferences, clientVersion, l) if err != nil { - return nil, nil, nil, nil, errors.WrapErrorAction(logutils.ActionApply, "external sign up", nil, err) + return nil, nil, nil, errors.WrapErrorAction(logutils.ActionApply, "external sign up", nil, err) } - externalIDs = externalUser.ExternalIDs } else { - return nil, nil, nil, nil, errors.ErrorData(logutils.StatusInvalid, "sign up", &logutils.FieldArgs{"identifier": externalUser.Identifier, "auth_type": authType.Code, "app_org_id": appOrg.ID, "admin": true}).SetStatus(utils.ErrorStatusNotAllowed) + return nil, nil, nil, errors.ErrorData(logutils.StatusInvalid, "sign up", &logutils.FieldArgs{"identifier": externalUser.Identifier, "auth_type": supportedAuthType.AuthType.Code, "app_org_id": appOrg.ID, "admin": true}).SetStatus(utils.ErrorStatusNotAllowed) } //TODO: make sure we do not return any refresh tokens in extParams - return accountAuthType, extParams, mfaTypes, externalIDs, nil + return extParams, newAccount, mfaTypes, nil } -func (a *Auth) applySignInExternal(account *model.Account, authType model.AuthType, appOrg model.ApplicationOrganization, - externalUser model.ExternalSystemUser, externalCreds string, l *logs.Log) (*model.AccountAuthType, error) { - var accountAuthType *model.AccountAuthType +func (a *Auth) applySignInExternal(account *model.Account, supportedAuthType model.SupportedAuthType, appOrg model.ApplicationOrganization, + externalUser model.ExternalSystemUser, externalCreds string, l *logs.Log) (*model.Account, error) { + var accountAuthTypes []model.AccountAuthType var err error - //find account auth type - accountAuthType, err = a.findAccountAuthType(account, &authType, externalUser.Identifier) + //find account auth type (there should only be one account auth type with matching auth type code) + accountAuthTypes, err = a.findAccountAuthTypesAndCredentials(account, supportedAuthType) if err != nil { return nil, err } + if len(accountAuthTypes) != 1 { + return nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAccountAuthType, + &logutils.FieldArgs{"count": len(accountAuthTypes), "auth_type_id": supportedAuthType.AuthType.ID, "identifier": externalUser.Identifier}) + } //check if need to update the account data - newAccount, err := a.updateExternalUserIfNeeded(*accountAuthType, externalUser, authType, appOrg, externalCreds, l) + newAccount, err := a.updateExternalUserIfNeeded(accountAuthTypes[0], externalUser, supportedAuthType.AuthType, appOrg, externalCreds, l) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionUpdate, model.TypeExternalSystemUser, nil, err) } if newAccount != nil { - accountAuthType.Account = *newAccount + newAccount.SortAccountAuthTypes(accountAuthTypes[0].ID, "") + newAccount.SortAccountIdentifiers(externalUser.Identifier) } - if accountAuthType.Unverified { - accountAuthType.SetUnverified(false) - err := a.storage.UpdateAccountAuthType(*accountAuthType) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountAuthType, nil, err) - } - } - - return accountAuthType, nil + return newAccount, nil } -func (a *Auth) applySignUpExternal(context storage.TransactionContext, authType model.AuthType, appOrg model.ApplicationOrganization, externalUser model.ExternalSystemUser, - externalCreds string, regProfile model.Profile, privacy model.Privacy, regPreferences map[string]interface{}, username string, clientVersion *string, l *logs.Log) (*model.AccountAuthType, error) { - var accountAuthType *model.AccountAuthType - +func (a *Auth) applySignUpExternal(context storage.TransactionContext, supportedAuthType model.SupportedAuthType, appOrg model.ApplicationOrganization, externalUser model.ExternalSystemUser, + externalCreds string, regProfile model.Profile, privacy model.Privacy, regPreferences map[string]interface{}, clientVersion *string, l *logs.Log) (*model.Account, error) { //1. prepare external admin user data - identifier, aatParams, useSharedProfile, profile, preferences, err := a.prepareExternalUserData(authType, appOrg, externalUser, regProfile, nil, l) + identifiers, aatParams, profile, preferences, err := a.prepareExternalUserData(supportedAuthType.AuthType, appOrg, externalUser, regProfile, nil, l) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionPrepare, "external admin user data", nil, err) } - identityProviderID, ok := authType.Params["identity_provider"].(string) + identityProviderID, ok := supportedAuthType.AuthType.Params["identity_provider"].(string) if !ok { return nil, errors.ErrorData(logutils.StatusMissing, "identity provider id", nil) } @@ -340,89 +359,90 @@ func (a *Auth) applySignUpExternal(context storage.TransactionContext, authType l.WarnError(logutils.MessageActionError(logutils.ActionGet, "external authorization", nil), err) } - //4. check username - if username != "" { - err = a.checkUsername(nil, &appOrg, username) - if err != nil { - return nil, err - } - } - - //5. register the account - accountAuthType, err = a.registerUser(context, authType, identifier, aatParams, appOrg, nil, useSharedProfile, - externalUser.ExternalIDs, *profile, privacy, preferences, username, nil, externalRoles, externalGroups, nil, nil, clientVersion, l) + //4. register the account + //External and anonymous auth is automatically verified, otherwise verified if credential has been verified previously + account, err := a.registerUser(context, identifiers, supportedAuthType.AuthType, true, aatParams, appOrg, nil, externalUser.ExternalIDs, + *profile, privacy, preferences, nil, externalRoles, externalGroups, nil, nil, clientVersion, l) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionRegister, model.TypeAccount, nil, err) } - return accountAuthType, nil + return account, nil } -func (a *Auth) applySignUpAdminExternal(context storage.TransactionContext, authType model.AuthType, appOrg model.ApplicationOrganization, externalUser model.ExternalSystemUser, regProfile model.Profile, - privacy model.Privacy, username string, permissions []string, roleIDs []string, groupIDs []string, scopes []string, creatorPermissions []string, clientVersion *string, l *logs.Log) (*model.AccountAuthType, error) { - var accountAuthType *model.AccountAuthType - +func (a *Auth) applySignUpAdminExternal(context storage.TransactionContext, supportedAuthType model.SupportedAuthType, appOrg model.ApplicationOrganization, externalUser model.ExternalSystemUser, regProfile model.Profile, + privacy model.Privacy, permissions []string, roleIDs []string, groupIDs []string, scopes []string, creatorPermissions []string, clientVersion *string, l *logs.Log) (*model.Account, error) { //1. prepare external admin user data - identifier, aatParams, useSharedProfile, profile, _, err := a.prepareExternalUserData(authType, appOrg, externalUser, regProfile, nil, l) + identifiers, aatParams, profile, _, err := a.prepareExternalUserData(supportedAuthType.AuthType, appOrg, externalUser, regProfile, nil, l) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionPrepare, "external admin user data", nil, err) } - //2. check username - if username != "" { - err = a.checkUsername(nil, &appOrg, username) - if err != nil { - return nil, err - } - } - - //3. register the account - accountAuthType, err = a.registerUser(context, authType, identifier, aatParams, appOrg, nil, useSharedProfile, nil, *profile, privacy, nil, - username, permissions, roleIDs, groupIDs, scopes, creatorPermissions, clientVersion, l) + //2. register the account + //External and anonymous auth is automatically verified, otherwise verified if credential has been verified previously + account, err := a.registerUser(context, identifiers, supportedAuthType.AuthType, false, aatParams, appOrg, nil, nil, *profile, privacy, nil, + permissions, roleIDs, groupIDs, scopes, creatorPermissions, clientVersion, l) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionRegister, "admin account", nil, err) } - return accountAuthType, nil + return account, nil } func (a *Auth) prepareExternalUserData(authType model.AuthType, appOrg model.ApplicationOrganization, externalUser model.ExternalSystemUser, regProfile model.Profile, - regPreferences map[string]interface{}, l *logs.Log) (string, map[string]interface{}, bool, *model.Profile, map[string]interface{}, error) { + regPreferences map[string]interface{}, l *logs.Log) ([]model.AccountIdentifier, map[string]interface{}, *model.Profile, map[string]interface{}, error) { var profile model.Profile var preferences map[string]interface{} - //1. check if needs to use shared profile - useSharedProfile, sharedProfile, _, err := a.applySharedProfile(appOrg.Application, authType.ID, externalUser.Identifier, l) - if err != nil { - return "", nil, false, nil, nil, errors.WrapErrorAction(logutils.ActionApply, "shared profile", nil, err) - } + /* + //1. check if needs to use shared profile + useSharedProfile, sharedProfile, _, err := a.applySharedProfile(appOrg, externalUser.Identifier, l) + if err != nil { + return nil, nil, false, nil, nil, errors.WrapErrorAction(logutils.ActionApply, "shared profile", nil, err) + } - if useSharedProfile { - l.Infof("%s uses a shared profile", externalUser.Identifier) + if useSharedProfile { + l.Infof("%s uses a shared profile", externalUser.Identifier) + + //merge client profile and shared profile + profile = a.mergeProfiles(regProfile, sharedProfile, true) + preferences = regPreferences + } - //merge client profile and shared profile - profile = a.mergeProfiles(regProfile, sharedProfile, true) - preferences = regPreferences - } else { l.Infof("%s does not use a shared profile", externalUser.Identifier) + */ - profile = regProfile - preferences = regPreferences + profile = regProfile + preferences = regPreferences - //prepare profile and preferences - preparedProfile, preparedPreferences, err := a.prepareRegistrationData(authType, externalUser.Identifier, profile, preferences, l) - if err != nil { - return "", nil, false, nil, nil, errors.WrapErrorAction(logutils.ActionPrepare, "user registration data", nil, err) - } - profile = *preparedProfile - preferences = preparedPreferences + //prepare profile and preferences + preparedProfile, preparedPreferences, err := a.prepareRegistrationData(authType, externalUser.Identifier, profile, preferences, l) + if err != nil { + return nil, nil, nil, nil, errors.WrapErrorAction(logutils.ActionPrepare, "user registration data", nil, err) } + profile = *preparedProfile + preferences = preparedPreferences //2. prepare the registration data - params := map[string]interface{}{} - params["user"] = externalUser + params := map[string]interface{}{"user": externalUser} - return externalUser.Identifier, params, useSharedProfile, &profile, preferences, nil + //3. create the account identifiers + now := time.Now().UTC() + accountID := uuid.NewString() + accountIdentifiers := make([]model.AccountIdentifier, 0) + for k, v := range externalUser.ExternalIDs { + primary := (v == externalUser.Identifier) + accountIdentifiers = append(accountIdentifiers, model.AccountIdentifier{ID: uuid.NewString(), Code: k, Identifier: v, Verified: true, + Sensitive: utils.Contains(externalUser.SensitiveExternalIDs, k), Primary: &primary, Account: model.Account{ID: accountID}, DateCreated: now}) + } + if externalUser.Email != "" { + primary := (externalUser.Email == externalUser.Identifier) + accountIdentifiers = append(accountIdentifiers, model.AccountIdentifier{ID: uuid.NewString(), Code: IdentifierTypeEmail, Identifier: externalUser.Email, + Verified: externalUser.IsEmailVerified, Sensitive: true, Primary: &primary, Account: model.Account{ID: accountID}, DateCreated: now}) + } + // AccountAuthTypeID field will be set later + + return accountIdentifiers, params, &profile, preferences, nil } func (a *Auth) applyProfileDataFromExternalUser(profile model.Profile, newExternalUser model.ExternalSystemUser, @@ -441,14 +461,10 @@ func (a *Auth) applyProfileDataFromExternalUser(profile model.Profile, newExtern if len(newExternalUser.LastName) > 0 && (alwaysSync || len(profile.LastName) == 0 || (currentExternalUser != nil && currentExternalUser.LastName != newExternalUser.LastName)) { newProfile.LastName = newExternalUser.LastName } - //email - if len(newExternalUser.Email) > 0 && (alwaysSync || len(profile.Email) == 0 || (currentExternalUser != nil && currentExternalUser.Email != newExternalUser.Email)) { - newProfile.Email = newExternalUser.Email - } changed := !utils.DeepEqual(profile, newProfile) if changed { - now := time.Now() + now := time.Now().UTC() newProfile.DateUpdated = &now return &newProfile, nil } @@ -461,15 +477,9 @@ func (a *Auth) updateExternalUserIfNeeded(accountAuthType model.AccountAuthType, l.Info("updateExternalUserIfNeeded") //get the current external user - currentDataMap := accountAuthType.Params["user"] - currentDataJSON, err := utils.ConvertToJSON(currentDataMap) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, model.TypeExternalSystemUser, nil, err) - } - var currentData *model.ExternalSystemUser - err = json.Unmarshal(currentDataJSON, ¤tData) + currentData, err := utils.JSONConvert[model.ExternalSystemUser, interface{}](accountAuthType.Params["user"]) if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, model.TypeExternalSystemUser, nil, err) + return nil, errors.WrapErrorAction(logutils.ActionParse, model.TypeExternalSystemUser, nil, err) } identityProviderID, ok := authType.Params["identity_provider"].(string) @@ -493,11 +503,13 @@ func (a *Auth) updateExternalUserIfNeeded(accountAuthType model.AccountAuthType, var newAccount *model.Account //there is changes so we need to update it //TODO: Can we do this all in a single storage operation? + updatedActiveStatus := !accountAuthType.Active updatedExternalUser := !currentData.Equals(externalUser) accountAuthType.Params["user"] = externalUser - now := time.Now() + now := time.Now().UTC() accountAuthType.DateUpdated = &now + //TODO: make sure external identifiers get updated in storage and in account memory transaction := func(context storage.TransactionContext) error { //1. first find the account record account, err := a.storage.FindAccountByAuthTypeID(context, accountAuthType.ID) @@ -512,6 +524,7 @@ func (a *Auth) updateExternalUserIfNeeded(accountAuthType model.AccountAuthType, newAccountAuthTypes := make([]model.AccountAuthType, len(account.AuthTypes)) for j, aAuthType := range account.AuthTypes { if aAuthType.ID == accountAuthType.ID { + accountAuthType.Active = true newAccountAuthTypes[j] = accountAuthType } else { newAccountAuthTypes[j] = aAuthType @@ -520,15 +533,7 @@ func (a *Auth) updateExternalUserIfNeeded(accountAuthType model.AccountAuthType, account.AuthTypes = newAccountAuthTypes // 3. update external ids - for k, v := range externalUser.ExternalIDs { - if account.ExternalIDs == nil { - account.ExternalIDs = make(map[string]string) - } - if account.ExternalIDs[k] != v { - updatedExternalUser = true - account.ExternalIDs[k] = v - } - } + updatedIdentifiers := a.updateExternalIdentifiers(account, accountAuthType.ID, &externalUser, false) // 4. update profile profileUpdated := false @@ -557,7 +562,7 @@ func (a *Auth) updateExternalUserIfNeeded(accountAuthType model.AccountAuthType, } // 6. update account if needed - if updatedExternalUser || profileUpdated || rolesUpdated || groupsUpdated { + if updatedActiveStatus || updatedExternalUser || updatedIdentifiers || profileUpdated || rolesUpdated || groupsUpdated { err = a.storage.SaveAccount(context, account) if err != nil { return errors.WrapErrorAction(logutils.ActionSave, model.TypeAccount, nil, err) @@ -598,175 +603,219 @@ func (a *Auth) applyAnonymousAuthType(authType model.AuthType, creds string) (st return anonymousID, account, anonymousParams, nil } -func (a *Auth) applyAuthType(authType model.AuthType, appOrg model.ApplicationOrganization, creds string, params string, clientVersion *string, regProfile model.Profile, - privacy model.Privacy, regPreferences map[string]interface{}, username string, admin bool, l *logs.Log) (string, *model.AccountAuthType, []model.MFAType, map[string]string, error) { +func (a *Auth) applyAuthType(supportedAuthType model.SupportedAuthType, appOrg model.ApplicationOrganization, appType *model.ApplicationType, + creds string, params string, clientVersion *string, regProfile model.Profile, privacy model.Privacy, regPreferences map[string]interface{}, + accountIdentifierID *string, admin bool, l *logs.Log) (map[string]interface{}, *model.Account, []model.MFAType, error) { - //auth type - authImpl, err := a.getAuthTypeImpl(authType) + //identifier type + identifierImpl := a.getIdentifierTypeImpl(creds, nil, nil) + authImpl, err := a.getAuthTypeImpl(supportedAuthType) if err != nil { - return "", nil, nil, nil, errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, nil, err) + return nil, nil, nil, errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, nil, err) } - //check if the user exists check - userIdentifier, err := authImpl.getUserIdentifier(creds) - if err != nil { - return "", nil, nil, nil, errors.WrapErrorAction(logutils.ActionGet, "user identifier", nil, err) - } + var account *model.Account + if identifierImpl == nil { + if accountIdentifierID != nil { + account, err = a.storage.FindAccountByIdentifierID(nil, *accountIdentifierID) + if err != nil { + return nil, nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, &logutils.FieldArgs{"identifier.id": *accountIdentifierID}, err) + } + if account == nil { + return nil, nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, &logutils.FieldArgs{"identifier.id": *accountIdentifierID}) + } - if userIdentifier != "" { - if authType.Code == AuthTypeTwilioPhone && regProfile.Phone == "" { - regProfile.Phone = userIdentifier - } else if authType.Code == AuthTypeEmail && regProfile.Email == "" { - regProfile.Email = userIdentifier - } else if authType.Code == authTypeUsername { - username = userIdentifier + // attempt sign-in after finding the account + retParams, verifiedMFATypes, err := a.applySignIn(nil, authImpl, supportedAuthType, appOrg, account, creds, params, accountIdentifierID, l) + return retParams, account, verifiedMFATypes, err } + + // attempt identifier-less login (only sign in is allowed because sign up is impossible without a user identifier) + message, credID, err := a.checkCredentials(nil, authImpl, nil, nil, creds, params, appOrg) + if err != nil { + return nil, nil, nil, errors.WrapErrorAction(logutils.ActionVerify, model.TypeCredential, nil, err) + } + + if message != nil { + return map[string]interface{}{"message": *message}, nil, nil, nil + } + + account, err := a.storage.FindAccountByCredentialID(nil, credID) + if err != nil { + return nil, nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, &logutils.FieldArgs{"credential_id": credID}, err) + } + + if authImpl.requireIdentifierVerificationForSignIn() && len(account.GetVerifiedAccountIdentifiers()) == 0 { + return nil, nil, nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAccount, &logutils.FieldArgs{"verified": false}) + } + + accountAuthTypes, err := a.findAccountAuthTypesAndCredentials(account, supportedAuthType) + if err != nil { + return nil, nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccountAuthType, nil, err) + } + + _, verifiedMFATypes, err := a.completeSignIn(nil, account, accountAuthTypes, supportedAuthType, credID) + return nil, account, verifiedMFATypes, err } - account, err := a.storage.FindAccount(nil, appOrg.ID, authType.ID, userIdentifier) + code := identifierImpl.getCode() + identifier := identifierImpl.getIdentifier() + account, err = a.storage.FindAccount(nil, appOrg.ID, code, identifier) if err != nil { - return "", nil, nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) //TODO add args.. + return nil, nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, &logutils.FieldArgs{"app_org_id": appOrg.ID, "code": code, "identifier": identifier}, err) } a.setLogContext(account, l) - canSignIn := a.canSignIn(account, authType.ID, userIdentifier) + canSignIn := a.canSignIn(account, code, identifier) //check if it is sign in or sign up isSignUp, err := a.isSignUp(canSignIn, params, l) if err != nil { - return "", nil, nil, nil, errors.WrapErrorAction(logutils.ActionVerify, "is sign up", nil, err) + return nil, nil, nil, errors.WrapErrorAction(logutils.ActionVerify, "is sign up", nil, err) } if isSignUp { if admin { - return "", nil, nil, nil, errors.ErrorData(logutils.StatusInvalid, "sign up", &logutils.FieldArgs{"identifier": userIdentifier, - "auth_type": authType.Code, "app_org_id": appOrg.ID, "admin": true}).SetStatus(utils.ErrorStatusNotAllowed) + return nil, nil, nil, errors.ErrorData(logutils.StatusInvalid, "sign up", &logutils.FieldArgs{"identifier": identifier, + "auth_type": supportedAuthType.AuthType.Code, "app_org_id": appOrg.ID, "admin": true}).SetStatus(utils.ErrorStatusNotAllowed) } - message, accountAuthType, err := a.applySignUp(authImpl, account, authType, appOrg, userIdentifier, creds, params, clientVersion, - regProfile, privacy, regPreferences, username, l) + retParams, account, err := a.applySignUp(identifierImpl, account, supportedAuthType, appOrg, appType, creds, params, clientVersion, regProfile, privacy, regPreferences, l) if err != nil { - return "", nil, nil, nil, err + return nil, nil, nil, err } - return message, accountAuthType, nil, nil, nil + return retParams, account, nil, nil } ///apply sign in - return a.applySignIn(authImpl, authType, account, userIdentifier, creds, l) + retParams, verifiedMFATypes, err := a.applySignIn(identifierImpl, authImpl, supportedAuthType, appOrg, account, creds, params, nil, l) + return retParams, account, verifiedMFATypes, err } -func (a *Auth) applySignIn(authImpl authType, authType model.AuthType, account *model.Account, userIdentifier string, - creds string, l *logs.Log) (string, *model.AccountAuthType, []model.MFAType, map[string]string, error) { +func (a *Auth) applySignIn(identifierImpl identifierType, authImpl authType, supportedAuthType model.SupportedAuthType, appOrg model.ApplicationOrganization, + account *model.Account, creds string, params string, accountIdentifierID *string, l *logs.Log) (map[string]interface{}, []model.MFAType, error) { if account == nil { - return "", nil, nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, nil).SetStatus(utils.ErrorStatusNotFound) + return nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, nil).SetStatus(utils.ErrorStatusNotFound) } - //find account auth type - accountAuthType, err := a.findAccountAuthType(account, &authType, userIdentifier) - if accountAuthType == nil { - return "", nil, nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccountAuthType, nil, err) + //find account identifier + var accountIdentifier *model.AccountIdentifier + identifier := "" + if accountIdentifierID != nil { + accountIdentifier = account.GetAccountIdentifierByID(*accountIdentifierID) + } else if identifierImpl != nil { + identifier = identifierImpl.getIdentifier() + accountIdentifier = account.GetAccountIdentifier(identifierImpl.getCode(), identifier) + } + if accountIdentifier == nil { + return nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccountIdentifier, &logutils.FieldArgs{"identifier": identifier}) + } + if !accountIdentifier.Verified && accountIdentifier.Linked { + return nil, nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAccountIdentifier, &logutils.FieldArgs{"verified": false, "linked": true}) } - if accountAuthType.Unverified && accountAuthType.Linked { - return "", nil, nil, nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAccountAuthType, &logutils.FieldArgs{"verified": false, "linked": true}) + if identifierImpl == nil { + identifierImpl = a.getIdentifierTypeImpl("", &accountIdentifier.Code, &accountIdentifier.Identifier) + if identifierImpl == nil { + return nil, nil, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, &logutils.FieldArgs{"code": accountIdentifier.Code, "identifier": accountIdentifier.Identifier}) + } + } + if identifierImpl.requireVerificationForSignIn() || authImpl.requireIdentifierVerificationForSignIn() { + err := identifierImpl.checkVerified(accountIdentifier, appOrg.Application.Name) + if err != nil { + return nil, nil, errors.WrapErrorData(logutils.StatusInvalid, model.TypeAccountIdentifier, &logutils.FieldArgs{"verified": false}, err) + } } - var message string - message, err = a.checkCredentials(authImpl, authType, accountAuthType, creds, l) + //find account auth type + accountAuthTypes, err := a.findAccountAuthTypesAndCredentials(account, supportedAuthType) if err != nil { - return "", nil, nil, nil, errors.WrapErrorAction(logutils.ActionVerify, model.TypeCredential, nil, err) + return nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccountAuthType, nil, err) } - return message, accountAuthType, account.GetVerifiedMFATypes(), account.ExternalIDs, nil -} - -func (a *Auth) checkCredentialVerified(authImpl authType, accountAuthType *model.AccountAuthType, l *logs.Log) error { - verified, expired, err := authImpl.isCredentialVerified(accountAuthType.Credential, l) + updateIdentifier := !accountIdentifier.Verified + message, credID, err := a.checkCredentials(identifierImpl, authImpl, &account.ID, accountAuthTypes, creds, params, appOrg) if err != nil { - return errors.WrapErrorAction(logutils.ActionVerify, "credential verified", nil, err) + return nil, nil, errors.WrapErrorAction(logutils.ActionVerify, model.TypeCredential, nil, err) } - if !*verified { - //it is unverified - if expired == nil || !*expired { - //not expired, just notify the client that it is "unverified" - return errors.ErrorData("unverified", "credential", nil).SetStatus(utils.ErrorStatusUnverified) - } - //expired, first restart the verification and then notify the client that it is unverified and verification is restarted - - //restart credential verification - err = authImpl.restartCredentialVerification(accountAuthType.Credential, accountAuthType.Account.AppOrg.Application.Name, l) + account.SortAccountIdentifiers(identifierImpl.getIdentifier()) + if updateIdentifier && message == nil { + accountIdentifier.Verified = true + err := a.storage.UpdateAccountIdentifier(nil, *accountIdentifier) if err != nil { - return errors.WrapErrorAction("restarting", "credential verification", nil, err) + return nil, nil, errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountIdentifier, nil, err) } - - //notify the client - return errors.ErrorData("expired", "credential verification", nil).SetStatus(utils.ErrorStatusVerificationExpired) } - return nil + return a.completeSignIn(message, account, accountAuthTypes, supportedAuthType, credID) } -func (a *Auth) checkCredentials(authImpl authType, authType model.AuthType, accountAuthType *model.AccountAuthType, creds string, l *logs.Log) (string, error) { - //check is verified - if authType.UseCredentials { - err := a.checkCredentialVerified(authImpl, accountAuthType, l) - if err != nil { - return "", err +func (a *Auth) completeSignIn(message *string, account *model.Account, accountAuthTypes []model.AccountAuthType, supportedAuthType model.SupportedAuthType, credID string) (map[string]interface{}, []model.MFAType, error) { + //sort by the account auth type used to perform the login + for _, aat := range accountAuthTypes { + if credID == "" || (aat.Credential != nil && aat.Credential.ID == credID) { + account.SortAccountAuthTypes(aat.ID, "") + + // if the account auth type is not already active, mark it as active + if !aat.Active { + aat.Active = true + err := a.storage.UpdateAccountAuthType(nil, aat) + if err != nil { + return nil, nil, errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountAuthType, nil, err) + } + } + + break } } + var retParams map[string]interface{} + if message != nil { + retParams = map[string]interface{}{"message": *message} + } + return retParams, account.GetVerifiedMFATypes(), nil +} + +func (a *Auth) checkCredentials(identifierImpl identifierType, authImpl authType, accountID *string, aats []model.AccountAuthType, creds string, + params string, appOrg model.ApplicationOrganization) (*string, string, error) { //check the credentials - message, err := authImpl.checkCredentials(*accountAuthType, creds, l) + msg, credID, err := authImpl.checkCredentials(identifierImpl, accountID, aats, creds, params, appOrg) if err != nil { - return message, errors.WrapErrorAction(logutils.ActionValidate, model.TypeCredential, nil, err) + return nil, "", errors.WrapErrorAction(logutils.ActionValidate, model.TypeCredential, nil, err) } - //if sign in was completed successfully, set auth type to verified - if message == "" && accountAuthType.Unverified { - accountAuthType.SetUnverified(false) - err := a.storage.UpdateAccountAuthType(*accountAuthType) - if err != nil { - return "", errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountAuthType, nil, err) - } + var message *string + if msg != "" { + message = &msg } - - return message, nil + return message, credID, nil } -func (a *Auth) applySignUp(authImpl authType, account *model.Account, authType model.AuthType, appOrg model.ApplicationOrganization, userIdentifier string, creds string, - params string, clientVersion *string, regProfile model.Profile, privacy model.Privacy, regPreferences map[string]interface{}, username string, l *logs.Log) (string, *model.AccountAuthType, error) { +func (a *Auth) applySignUp(identifierImpl identifierType, account *model.Account, supportedAuthType model.SupportedAuthType, appOrg model.ApplicationOrganization, + appType *model.ApplicationType, creds string, params string, clientVersion *string, regProfile model.Profile, privacy model.Privacy, + regPreferences map[string]interface{}, l *logs.Log) (map[string]interface{}, *model.Account, error) { if account != nil { - err := a.handleAccountAuthTypeConflict(*account, authType.ID, userIdentifier, true) + err := a.handleAccountIdentifierConflict(*account, identifierImpl, true) if err != nil { - return "", nil, err + return nil, nil, err } } - if username != "" { - err := a.checkUsername(nil, &appOrg, username) + if identifierImpl.getCode() == IdentifierTypeUsername { + username := identifierImpl.getIdentifier() + accounts, err := a.storage.FindAccountsByUsername(nil, &appOrg, username) if err != nil { - return "", nil, err + return nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) + } + if len(accounts) > 0 { + return nil, nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAccountUsername, logutils.StringArgs(username+" taken")).SetStatus(utils.ErrorStatusUsernameTaken) } } - retParams, accountAuthType, err := a.signUpNewAccount(nil, authImpl, authType, appOrg, userIdentifier, creds, params, clientVersion, regProfile, privacy, regPreferences, username, nil, nil, nil, nil, nil, l) + retParams, account, err := a.signUpNewAccount(nil, identifierImpl, supportedAuthType, appOrg, appType, creds, params, clientVersion, regProfile, privacy, regPreferences, nil, nil, nil, nil, nil, l) if err != nil { - return "", nil, err + return nil, nil, err } - message, _ := retParams["message"].(string) - return message, accountAuthType, nil -} - -func (a *Auth) applySignUpAdmin(context storage.TransactionContext, authImpl authType, account *model.Account, authType model.AuthType, appOrg model.ApplicationOrganization, identifier string, password string, - regProfile model.Profile, privacy model.Privacy, username string, permissions []string, roles []string, groups []string, scopes []string, creatorPermissions []string, clientVersion *string, l *logs.Log) (map[string]interface{}, *model.AccountAuthType, error) { - - if username != "" { - err := a.checkUsername(nil, &appOrg, username) - if err != nil { - return nil, nil, err - } - } - - return a.signUpNewAccount(context, authImpl, authType, appOrg, identifier, password, "", clientVersion, regProfile, privacy, nil, username, permissions, roles, groups, scopes, creatorPermissions, l) + return retParams, account, nil } func (a *Auth) applyCreateAnonymousAccount(context storage.TransactionContext, appOrg model.ApplicationOrganization, anonymousID string, @@ -775,161 +824,157 @@ func (a *Auth) applyCreateAnonymousAccount(context storage.TransactionContext, a return a.storage.InsertAccount(context, account) } -func (a *Auth) signUpNewAccount(context storage.TransactionContext, authImpl authType, authType model.AuthType, appOrg model.ApplicationOrganization, userIdentifier string, - creds string, params string, clientVersion *string, regProfile model.Profile, privacy model.Privacy, regPreferences map[string]interface{}, username string, permissions []string, - roles []string, groups []string, scopes []string, creatorPermissions []string, l *logs.Log) (map[string]interface{}, *model.AccountAuthType, error) { +func (a *Auth) signUpNewAccount(context storage.TransactionContext, identifierImpl identifierType, supportedAuthType model.SupportedAuthType, appOrg model.ApplicationOrganization, + appType *model.ApplicationType, creds string, params string, clientVersion *string, regProfile model.Profile, privacy model.Privacy, regPreferences map[string]interface{}, + permissions []string, roles []string, groups []string, scopes []string, creatorPermissions []string, l *logs.Log) (map[string]interface{}, *model.Account, error) { var retParams map[string]interface{} + var accountIdentifier *model.AccountIdentifier var credential *model.Credential var profile model.Profile var preferences map[string]interface{} - //check if needs to use shared profile - useSharedProfile, sharedProfile, sharedCredential, err := a.applySharedProfile(appOrg.Application, authType.ID, userIdentifier, l) - if err != nil { - return nil, nil, errors.WrapErrorAction(logutils.ActionApply, "shared profile", nil, err) - } + /* + //check if needs to use shared profile + useSharedProfile, sharedProfile, sharedCredential, err := a.applySharedProfile(appOrg, userIdentifier, l) + if err != nil { + return nil, nil, errors.WrapErrorAction(logutils.ActionApply, "shared profile", nil, err) + } - if useSharedProfile { - l.Infof("%s uses a shared profile", userIdentifier) + if useSharedProfile { + l.Infof("%s uses a shared profile", userIdentifier) - //allow sign up only if the shared credential is verified - if credential != nil && !credential.Verified { - l.Infof("trying to sign up in %s with unverified shared credentials", appOrg.Organization.Name) - return nil, nil, errors.ErrorData("unverified", model.TypeCredential, nil).SetStatus(utils.ErrorStatusSharedCredentialUnverified) - } + //allow sign up only if the shared credential is verified + if credential != nil && !credential.Verified { + l.Infof("trying to sign up in %s with unverified shared credentials", appOrg.Organization.Name) + return nil, nil, errors.ErrorData("unverified", model.TypeCredential, nil).SetStatus(utils.ErrorStatusSharedCredentialUnverified) + } - //merge client profile and shared profile - profile = a.mergeProfiles(regProfile, sharedProfile, true) - preferences = regPreferences + //merge client profile and shared profile + profile = a.mergeProfiles(regProfile, sharedProfile, true) + preferences = regPreferences + + credential = sharedCredential + retParams = map[string]interface{}{"message": "successfully registered"} + } - credential = sharedCredential - retParams = map[string]interface{}{"message": "successfuly registered"} - } else { l.Infof("%s does not use a shared profile", userIdentifier) + */ - profile = regProfile - preferences = regPreferences + profile = regProfile + preferences = regPreferences + + preparedProfile, preparedPreferences, err := a.prepareRegistrationData(supportedAuthType.AuthType, identifierImpl.getIdentifier(), profile, preferences, l) + if err != nil { + return nil, nil, errors.WrapErrorAction(logutils.ActionPrepare, "user registration data", nil, err) + } + profile = *preparedProfile + preferences = preparedPreferences - preparedProfile, preparedPreferences, err := a.prepareRegistrationData(authType, userIdentifier, profile, preferences, l) + //apply sign up + authImpl, err := a.getAuthTypeImpl(supportedAuthType) + if err != nil { + return nil, nil, errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, nil, err) + } + if creatorPermissions == nil { + var message string + message, accountIdentifier, credential, err = authImpl.signUp(identifierImpl, nil, appOrg, creds, params) if err != nil { - return nil, nil, errors.WrapErrorAction(logutils.ActionPrepare, "user registration data", nil, err) + return nil, nil, errors.WrapErrorAction("signing up", "user", nil, err) } - profile = *preparedProfile - preferences = preparedPreferences - - credID := uuid.NewString() - - //apply sign up - var credentialValue map[string]interface{} - if creatorPermissions == nil { - var message string - message, credentialValue, err = authImpl.signUp(authType, appOrg, creds, params, credID, l) - if err != nil { - return nil, nil, errors.WrapErrorAction("signing up", "user", nil, err) - } + if message != "" { retParams = map[string]interface{}{"message": message} - } else { - retParams, credentialValue, err = authImpl.signUpAdmin(authType, appOrg, userIdentifier, creds, credID) - if err != nil { - return nil, nil, errors.WrapErrorAction("signing up", "admin user", nil, err) - } } - - //credential - if credentialValue != nil { - now := time.Now() - credential = &model.Credential{ID: credID, AccountsAuthTypes: nil, Value: credentialValue, Verified: false, - AuthType: authType, DateCreated: now, DateUpdated: &now} + } else { + retParams, accountIdentifier, credential, err = authImpl.signUpAdmin(identifierImpl, appOrg, creds) + if err != nil { + return nil, nil, errors.WrapErrorAction("signing up", "admin user", nil, err) } } + if accountIdentifier == nil { + return nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccountIdentifier, nil) + } - accountAuthType, err := a.registerUser(context, authType, userIdentifier, nil, appOrg, credential, useSharedProfile, - nil, profile, privacy, preferences, username, permissions, roles, groups, scopes, creatorPermissions, clientVersion, l) + if credential != nil { + credential.AuthType.ID = supportedAuthType.AuthType.ID + } + + var accountAuthTypeParams map[string]interface{} + if supportedAuthType.AuthType.Code == AuthTypeWebAuthn && appType != nil { + accountAuthTypeParams = map[string]interface{}{"app_type_identifier": appType.Identifier} + } + account, err := a.registerUser(context, []model.AccountIdentifier{*accountIdentifier}, supportedAuthType.AuthType, retParams == nil, accountAuthTypeParams, + appOrg, credential, nil, profile, privacy, preferences, permissions, roles, groups, scopes, creatorPermissions, clientVersion, l) if err != nil { return nil, nil, errors.WrapErrorAction(logutils.ActionRegister, model.TypeAccount, nil, err) } - return retParams, accountAuthType, nil + return retParams, account, nil } -func (a *Auth) applySharedProfile(app model.Application, authTypeID string, userIdentifier string, l *logs.Log) (bool, *model.Profile, *model.Credential, error) { - //do not share profiles by default - useSharedProfile := false - - var sharedProfile *model.Profile - var sharedCredential *model.Credential - - var err error - - //the application uses shared profiles +/* +func (a *Auth) applySharedProfile(app model.Application, userIdentifier string, l *logs.Log) (bool, *model.Profile, []model.Credential, error) { if app.SharedIdentities { + //the application uses shared profiles l.Infof("%s uses shared identities", app.Name) - hasSharedProfile := false - hasSharedProfile, sharedProfile, sharedCredential, err = a.hasSharedProfile(app, authTypeID, userIdentifier, l) + hasSharedProfile, sharedProfile, sharedCredential, err := a.hasSharedProfile(app, userIdentifier, l) if err != nil { return false, nil, nil, errors.WrapErrorAction(logutils.ActionVerify, "shared profile", nil, err) } if hasSharedProfile { l.Infof("%s already has a profile, so use it", userIdentifier) - useSharedProfile = true } else { l.Infof("%s does not have a profile", userIdentifier) } - } else { - l.Infof("%s does not use shared identities", app.Name) + return hasSharedProfile, sharedProfile, sharedCredential, nil } - return useSharedProfile, sharedProfile, sharedCredential, nil + l.Infof("%s does not use shared identities", app.Name) + return false, nil, nil, nil } -func (a *Auth) hasSharedProfile(app model.Application, authTypeID string, userIdentifier string, l *logs.Log) (bool, *model.Profile, *model.Credential, error) { +func (a *Auth) hasSharedProfile(app model.Application, userIdentifier string, l *logs.Log) (bool, *model.Profile, []model.Credential, error) { l.Info("hasSharedProfile") //find if already there is a profile for the application - profiles, err := a.storage.FindAccountProfiles(app.ID, authTypeID, userIdentifier) + accounts, err := a.storage.FindAccountProfiles(app.ID, userIdentifier) if err != nil { return false, nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeProfile, nil, err) } - if len(profiles) == 0 { + //TODO: which account's profile to use? + //TODO: what if a different individual is using this identifier in another app org (e.g., username)? profile should not be shared + //TODO: may need to get profile ID from client (or entire copy of the shared profile on sign up) + if account == nil { l.Info("there is no profile yet") return false, nil, nil, nil } //find profile - var profile *model.Profile var credential *model.Credential - var credentialID *string - for _, current := range profiles { - for _, account := range current.Accounts { - for _, accAuthType := range account.AuthTypes { - if accAuthType.Identifier == userIdentifier { - //get the profile - profile = ¤t - - if accAuthType.Credential != nil { - credentialID = &accAuthType.Credential.ID //we have only the id loaded in the credential object - } - break - } - } + credentialIDs := make([]string, 0) + for _, accAuthType := range account.AuthTypes { + if accAuthType.Credential != nil { + // we have only the id loaded in the credential object + credentialIDs = append(credentialIDs, accAuthType.Credential.ID) } - } - - if profile == nil { - return false, nil, nil, nil + break } //find the credential - if credentialID != nil { - credential, err = a.storage.FindCredential(nil, *credentialID) + credentials := make([]model.Credential, 0) + for _, credID := range credentialIDs { + credential, err = a.storage.FindCredential(nil, credID) if err != nil { return false, nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeCredential, nil, err) } + + credentials = append(credentials, *credential) } - return true, profile, credential, nil + + return true, &account.Profile, credentials, nil } +*/ // validateAPIKey checks if the given API key is valid for the given app ID func (a *Auth) validateAPIKey(apiKey string, appID string) error { @@ -941,10 +986,10 @@ func (a *Auth) validateAPIKey(apiKey string, appID string) error { return nil } -func (a *Auth) canSignIn(account *model.Account, authTypeID string, userIdentifier string) bool { +func (a *Auth) canSignIn(account *model.Account, code string, identifier string) bool { if account != nil { - aat := account.GetAccountAuthType(authTypeID, userIdentifier) - return aat == nil || !aat.Linked || !aat.Unverified + aat := account.GetAccountIdentifier(code, identifier) + return aat == nil || !aat.Linked || aat.Verified } return false @@ -980,60 +1025,54 @@ func (a *Auth) isSignUp(accountExists bool, params string, l *logs.Log) (bool, e return true, nil } -func (a *Auth) getAccount(authenticationType string, userIdentifier string, apiKey string, appTypeIdentifier string, orgID string) (*model.Account, string, error) { - //validate if the provided auth type is supported by the provided application and organization - authType, _, appOrg, err := a.validateAuthType(authenticationType, appTypeIdentifier, orgID) +func (a *Auth) getAccount(code string, identifier string, apiKey string, appTypeIdentifier string, orgID string) (*model.Account, error) { + //validate if the provided app type is supported by the provided application and organization + _, appOrg, err := a.validateAppOrg(&appTypeIdentifier, nil, orgID) if err != nil { - return nil, "", errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, nil, err) + return nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, nil, err) } //do not allow for admins if appOrg.Application.Admin { - return nil, "", errors.ErrorData(logutils.StatusInvalid, model.TypeApplication, logutils.StringArgs("not allowed for admins")) + return nil, errors.ErrorData(logutils.StatusInvalid, model.TypeApplication, logutils.StringArgs("not allowed for admins")) } //TODO: Ideally we would not make many database calls before validating the API key. Currently needed to get app ID err = a.validateAPIKey(apiKey, appOrg.Application.ID) if err != nil { - return nil, "", errors.WrapErrorData(logutils.StatusInvalid, model.TypeAPIKey, nil, err) + return nil, errors.WrapErrorData(logutils.StatusInvalid, model.TypeAPIKey, nil, err) } //check if the account exists check - account, err := a.storage.FindAccount(nil, appOrg.ID, authType.ID, userIdentifier) + account, err := a.storage.FindAccount(nil, appOrg.ID, code, identifier) if err != nil { - return nil, "", errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) + return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, &logutils.FieldArgs{"app_org_id": appOrg.ID, "code": code, "identifier": identifier}, err) } - return account, authType.ID, nil + return account, nil } -func (a *Auth) findAccountAuthType(account *model.Account, authType *model.AuthType, identifier string) (*model.AccountAuthType, error) { +func (a *Auth) findAccountAuthTypesAndCredentials(account *model.Account, supportedAuthType model.SupportedAuthType) ([]model.AccountAuthType, error) { if account == nil { return nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, nil) } - if authType == nil { - return nil, errors.ErrorData(logutils.StatusMissing, model.TypeAuthType, nil) - } - - accountAuthType := account.GetAccountAuthType(authType.ID, identifier) - if accountAuthType == nil { - return nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccountAuthType, nil) - } - - accountAuthType.AuthType = *authType + accountAuthTypes := account.GetAccountAuthTypes(supportedAuthType.AuthType.Code) + for i, aat := range accountAuthTypes { + accountAuthTypes[i].SupportedAuthType = supportedAuthType - if accountAuthType.Credential != nil { - //populate credentials in accountAuthType - credential, err := a.storage.FindCredential(nil, accountAuthType.Credential.ID) - if err != nil || credential == nil { - return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeCredential, nil, err) + if aat.Credential != nil { + //populate credentials in accountAuthType + credential, err := a.storage.FindCredential(nil, aat.Credential.ID) + if err != nil || credential == nil { + return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeCredential, nil, err) + } + credential.AuthType = supportedAuthType.AuthType + accountAuthTypes[i].Credential = credential } - credential.AuthType = *authType - accountAuthType.Credential = credential } - return accountAuthType, nil + return accountAuthTypes, nil } func (a *Auth) findAccountAuthTypeByID(account *model.Account, accountAuthTypeID string) (*model.AccountAuthType, error) { @@ -1050,12 +1089,13 @@ func (a *Auth) findAccountAuthTypeByID(account *model.Account, accountAuthTypeID return nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccountAuthType, nil) } - authType, err := a.storage.FindAuthType(accountAuthType.AuthType.ID) + authType, err := a.storage.FindAuthType(accountAuthType.SupportedAuthType.AuthType.ID) if err != nil || authType == nil { - return nil, errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, logutils.StringArgs(accountAuthType.AuthType.ID), err) + return nil, errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, logutils.StringArgs(accountAuthType.SupportedAuthType.AuthType.ID), err) } - accountAuthType.AuthType = *authType + //TODO: Handle retrieving supported auth type/params? + accountAuthType.SupportedAuthType = model.SupportedAuthType{AuthType: *authType} if accountAuthType.Credential != nil { //populate credentials in accountAuthType @@ -1069,6 +1109,33 @@ func (a *Auth) findAccountAuthTypeByID(account *model.Account, accountAuthTypeID return accountAuthType, nil } +func (a *Auth) updateAccountIdentifier(context storage.TransactionContext, account *model.Account, accountIdentifier *model.AccountIdentifier) error { + if account == nil { + return errors.ErrorData(logutils.StatusMissing, model.TypeAccount, nil) + } + if accountIdentifier == nil { + return errors.ErrorData(logutils.StatusMissing, model.TypeAccountIdentifier, nil) + } + + newAccountIdentifiers := make([]model.AccountIdentifier, len(account.Identifiers)) + for j, aIdentifier := range account.Identifiers { + if aIdentifier.ID == accountIdentifier.ID { + newAccountIdentifiers[j] = *accountIdentifier + } else { + newAccountIdentifiers[j] = aIdentifier + } + } + account.Identifiers = newAccountIdentifiers + + // update account + err := a.storage.SaveAccount(context, account) + if err != nil { + return errors.WrapErrorAction(logutils.ActionSave, model.TypeAccount, nil, err) + } + + return nil +} + func (a *Auth) clearExpiredSessions(identifier string, l *logs.Log) error { l.Info("clearExpiredSessions") @@ -1105,9 +1172,9 @@ func (a *Auth) clearExpiredSessions(identifier string, l *logs.Log) error { return nil } -func (a *Auth) applyLogin(anonymous bool, sub string, authType model.AuthType, appOrg model.ApplicationOrganization, - accountAuthType *model.AccountAuthType, appType model.ApplicationType, externalIDs map[string]string, ipAddress string, deviceType string, - deviceOS *string, deviceID *string, clientVersion *string, params map[string]interface{}, state string, l *logs.Log) (*model.LoginSession, error) { +func (a *Auth) applyLogin(anonymous bool, sub string, authType model.AuthType, appOrg model.ApplicationOrganization, account *model.Account, + appType model.ApplicationType, ipAddress string, deviceType string, deviceOS *string, deviceID *string, clientVersion *string, params map[string]interface{}, + state string, l *logs.Log) (*model.LoginSession, error) { var err error var loginSession *model.LoginSession @@ -1138,7 +1205,7 @@ func (a *Auth) applyLogin(anonymous bool, sub string, authType model.AuthType, a /// ///create login session entity - loginSession, err = a.createLoginSession(anonymous, sub, authType, appOrg, accountAuthType, appType, externalIDs, ipAddress, params, state, device, l) + loginSession, err = a.createLoginSession(anonymous, sub, authType, appOrg, account, appType, ipAddress, params, state, device, l) if err != nil { return errors.WrapErrorAction(logutils.ActionCreate, model.TypeLoginSession, nil, err) } @@ -1199,48 +1266,50 @@ func (a *Auth) createDevice(accountID string, deviceType string, deviceOS *strin Type: deviceType, OS: *deviceOS, DateCreated: time.Now()}, nil } -func (a *Auth) createLoginSession(anonymous bool, sub string, authType model.AuthType, - appOrg model.ApplicationOrganization, accountAuthType *model.AccountAuthType, appType model.ApplicationType, - externalIDs map[string]string, ipAddress string, params map[string]interface{}, state string, device *model.Device, l *logs.Log) (*model.LoginSession, error) { +func (a *Auth) createLoginSession(anonymous bool, sub string, authType model.AuthType, appOrg model.ApplicationOrganization, account *model.Account, + appType model.ApplicationType, ipAddress string, params map[string]interface{}, state string, device *model.Device, l *logs.Log) (*model.LoginSession, error) { //id - idUUID, _ := uuid.NewUUID() - id := idUUID.String() - - //account auth type - if !anonymous { - //sort account auth types by the one used for login - accountAuthType.Account.SortAccountAuthTypes(accountAuthType.Identifier) - } + id := uuid.NewString() //access token orgID := appOrg.Organization.ID appID := appOrg.Application.ID - uid := "" name := "" email := "" phone := "" + username := "" permissions := []string{} scopes := []string{authorization.ScopeGlobal} + externalIDs := make(map[string]string) if !anonymous { - uid = accountAuthType.Identifier - name = accountAuthType.Account.Profile.GetFullName() - email = accountAuthType.Account.Profile.Email - phone = accountAuthType.Account.Profile.Phone - permissions = accountAuthType.Account.GetPermissionNames() - scopes = append(scopes, accountAuthType.Account.GetScopes()...) - } - claims := a.getStandardClaims(sub, uid, name, email, phone, rokwireTokenAud, orgID, appID, authType.Code, externalIDs, nil, anonymous, true, appOrg.Application.Admin, appOrg.Organization.System, false, true, idUUID.String()) + if account == nil { + return nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, nil) + } + if emailIdentifier := account.GetAccountIdentifier(IdentifierTypeEmail, ""); emailIdentifier != nil { + email = emailIdentifier.Identifier + } + if phoneIdentifier := account.GetAccountIdentifier(IdentifierTypePhone, ""); phoneIdentifier != nil { + phone = phoneIdentifier.Identifier + } + if usernameIdentifier := account.GetAccountIdentifier(IdentifierTypeUsername, ""); usernameIdentifier != nil { + username = usernameIdentifier.Identifier + } + name = account.Profile.GetFullName() + permissions = account.GetPermissionNames() + scopes = append(scopes, account.GetScopes()...) + for _, external := range account.GetExternalAccountIdentifiers() { + externalIDs[external.Code] = external.Identifier + } + } + claims := a.getStandardClaims(sub, name, email, phone, username, rokwireTokenAud, orgID, appID, authType.Code, externalIDs, nil, anonymous, true, appOrg.Application.Admin, appOrg.Organization.System, false, true, id) accessToken, err := a.buildAccessToken(claims, strings.Join(permissions, ","), strings.Join(scopes, " ")) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionCreate, logutils.TypeToken, nil, err) } //refresh token - refreshToken, err := a.buildRefreshToken() - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionCreate, logutils.TypeToken, nil, err) - } + refreshToken := utils.GenerateRandomString(refreshTokenLength) now := time.Now().UTC() var stateExpires *time.Time @@ -1249,9 +1318,8 @@ func (a *Auth) createLoginSession(anonymous bool, sub string, authType model.Aut stateExpires = &stateExpireTime } - loginSession := model.LoginSession{ID: id, AppOrg: appOrg, AuthType: authType, - AppType: appType, Anonymous: anonymous, Identifier: sub, ExternalIDs: externalIDs, AccountAuthType: accountAuthType, - Device: device, IPAddress: ipAddress, AccessToken: accessToken, RefreshTokens: []string{refreshToken}, Params: params, + loginSession := model.LoginSession{ID: id, AppOrg: appOrg, AuthType: authType, AppType: appType, Anonymous: anonymous, Identifier: sub, + Account: account, Device: device, IPAddress: ipAddress, AccessToken: accessToken, RefreshTokens: []string{refreshToken}, Params: params, State: state, StateExpires: stateExpires, DateCreated: now} return &loginSession, nil @@ -1288,8 +1356,7 @@ func (a *Auth) deleteLoginSessions(context storage.TransactionContext, loginSess return nil } -func (a *Auth) prepareRegistrationData(authType model.AuthType, identifier string, - profile model.Profile, preferences map[string]interface{}, l *logs.Log) (*model.Profile, map[string]interface{}, error) { +func (a *Auth) prepareRegistrationData(authType model.AuthType, identifier string, profile model.Profile, preferences map[string]interface{}, l *logs.Log) (*model.Profile, map[string]interface{}, error) { //no need to merge from profile BB for new apps ///profile and preferences @@ -1334,15 +1401,13 @@ func (a *Auth) prepareRegistrationData(authType model.AuthType, identifier strin return &readyProfile, readyPreferences, nil } -func (a *Auth) prepareAccountAuthType(authType model.AuthType, identifier string, accountAuthTypeParams map[string]interface{}, - credential *model.Credential, unverified bool, linked bool) (*model.AccountAuthType, *model.Credential, error) { +func (a *Auth) prepareAccountAuthType(authType model.AuthType, accountAuthTypeActive bool, accountAuthTypeParams map[string]interface{}, credential *model.Credential) (*model.AccountAuthType, error) { now := time.Now() //account auth type - accountAuthTypeID, _ := uuid.NewUUID() - active := true - accountAuthType := &model.AccountAuthType{ID: accountAuthTypeID.String(), AuthType: authType, - Identifier: identifier, Params: accountAuthTypeParams, Credential: credential, Unverified: unverified, Linked: linked, Active: active, DateCreated: now} + accountAuthTypeID := uuid.NewString() + accountAuthType := &model.AccountAuthType{ID: accountAuthTypeID, SupportedAuthType: model.SupportedAuthType{AuthType: authType}, + Params: accountAuthTypeParams, Credential: credential, Active: accountAuthTypeActive, DateCreated: now} //credential if credential != nil { @@ -1350,7 +1415,7 @@ func (a *Auth) prepareAccountAuthType(authType model.AuthType, identifier string credential.AccountsAuthTypes = append(credential.AccountsAuthTypes, *accountAuthType) } - return accountAuthType, credential, nil + return accountAuthType, nil } func (a *Auth) mergeProfiles(dst model.Profile, src *model.Profile, shared bool) model.Profile { @@ -1361,8 +1426,6 @@ func (a *Auth) mergeProfiles(dst model.Profile, src *model.Profile, shared bool) dst.PhotoURL = utils.SetStringIfEmpty(dst.PhotoURL, src.PhotoURL) dst.FirstName = utils.SetStringIfEmpty(dst.FirstName, src.FirstName) dst.LastName = utils.SetStringIfEmpty(dst.LastName, src.LastName) - dst.Email = utils.SetStringIfEmpty(dst.Email, src.Email) - dst.Phone = utils.SetStringIfEmpty(dst.Phone, src.Phone) dst.Address = utils.SetStringIfEmpty(dst.Address, src.Address) dst.ZipCode = utils.SetStringIfEmpty(dst.ZipCode, src.ZipCode) dst.State = utils.SetStringIfEmpty(dst.State, src.State) @@ -1428,46 +1491,45 @@ func (a *Auth) getProfileBBData(authType model.AuthType, identifier string, l *l // creatorPermissions ([]string): an admin user's permissions to validate // l (*logs.Log): Log object pointer for request // Returns: -// Registered account (AccountAuthType): Registered Account object -func (a *Auth) registerUser(context storage.TransactionContext, authType model.AuthType, userIdentifier string, accountAuthTypeParams map[string]interface{}, - appOrg model.ApplicationOrganization, credential *model.Credential, useSharedProfile bool, externalIDs map[string]string, profile model.Profile, privacy model.Privacy, preferences map[string]interface{}, - username string, permissionNames []string, roleIDs []string, groupIDs []string, scopes []string, creatorPermissions []string, clientVersion *string, l *logs.Log) (*model.AccountAuthType, error) { - - //External and anonymous auth is automatically verified, otherwise verified if credential has been verified previously - unverified := true - if creatorPermissions == nil { - if authType.IsExternal || authType.IsAnonymous { - unverified = false - } else if credential != nil { - unverified = !credential.Verified - } +// Registered account (Account): Registered Account object +func (a *Auth) registerUser(context storage.TransactionContext, accountIdentifiers []model.AccountIdentifier, authType model.AuthType, accountAuthTypeActive bool, accountAuthTypeParams map[string]interface{}, + appOrg model.ApplicationOrganization, credential *model.Credential, externalIDs map[string]string, profile model.Profile, privacy model.Privacy, preferences map[string]interface{}, + permissionNames []string, roleIDs []string, groupIDs []string, scopes []string, creatorPermissions []string, clientVersion *string, l *logs.Log) (*model.Account, error) { + if len(accountIdentifiers) == 0 { + return nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccountIdentifier, nil) } - accountAuthType, err := a.constructAccount(context, authType, userIdentifier, accountAuthTypeParams, appOrg, credential, - unverified, externalIDs, profile, privacy, preferences, username, permissionNames, roleIDs, groupIDs, scopes, creatorPermissions, clientVersion, l) + account, err := a.constructAccount(context, accountIdentifiers, authType, accountAuthTypeActive, accountAuthTypeParams, appOrg, credential, + externalIDs, profile, privacy, preferences, permissionNames, roleIDs, groupIDs, scopes, creatorPermissions, clientVersion, l) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionCreate, model.TypeAccount, nil, err) } - err = a.storeNewAccountInfo(context, accountAuthType.Account, credential, useSharedProfile, profile) + err = a.storeNewAccountInfo(context, *account, credential, profile) if err != nil { return nil, errors.WrapErrorAction("storing", "new account information", nil, err) } - return accountAuthType, nil + return account, nil } -func (a *Auth) constructAccount(context storage.TransactionContext, authType model.AuthType, userIdentifier string, accountAuthTypeParams map[string]interface{}, - appOrg model.ApplicationOrganization, credential *model.Credential, unverified bool, externalIDs map[string]string, profile model.Profile, privacy model.Privacy, preferences map[string]interface{}, - username string, permissionNames []string, roleIDs []string, groupIDs []string, scopes []string, assignerPermissions []string, clientVersion *string, l *logs.Log) (*model.AccountAuthType, error) { +func (a *Auth) constructAccount(context storage.TransactionContext, accountIdentifiers []model.AccountIdentifier, authType model.AuthType, accountAuthTypeActive bool, accountAuthTypeParams map[string]interface{}, + appOrg model.ApplicationOrganization, credential *model.Credential, externalIDs map[string]string, profile model.Profile, privacy model.Privacy, preferences map[string]interface{}, + permissionNames []string, roleIDs []string, groupIDs []string, scopes []string, assignerPermissions []string, clientVersion *string, l *logs.Log) (*model.Account, error) { //create account auth type - accountAuthType, _, err := a.prepareAccountAuthType(authType, userIdentifier, accountAuthTypeParams, credential, unverified, false) + accountAuthType, err := a.prepareAccountAuthType(authType, accountAuthTypeActive, accountAuthTypeParams, credential) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionCreate, model.TypeAccountAuthType, nil, err) } + if accountAuthType.Params["user"] != nil { + // external auth type, so set account auth type IDs for identifiers + for i := range accountIdentifiers { + accountIdentifiers[i].AccountAuthTypeID = &accountAuthType.ID + } + } //create account object - accountID, _ := uuid.NewUUID() + accountID := accountIdentifiers[0].Account.ID authTypes := []model.AccountAuthType{*accountAuthType} //assumes admin creator permissions are always non-nil @@ -1529,15 +1591,18 @@ func (a *Auth) constructAccount(context storage.TransactionContext, authType mod scopes = nil } - account := model.Account{ID: accountID.String(), AppOrg: appOrg, Permissions: permissions, - Roles: model.AccountRolesFromAppOrgRoles(roles, true, adminSet), Groups: model.AccountGroupsFromAppOrgGroups(groups, true, adminSet), Scopes: scopes, AuthTypes: authTypes, - ExternalIDs: externalIDs, Preferences: preferences, Profile: profile, Privacy: privacy, Username: username, DateCreated: time.Now(), MostRecentClientVersion: clientVersion} + account := model.Account{ID: accountID, AppOrg: appOrg, AuthTypes: authTypes, Identifiers: accountIdentifiers, Permissions: permissions, + Roles: model.AccountRolesFromAppOrgRoles(roles, true, adminSet), Groups: model.AccountGroupsFromAppOrgGroups(groups, true, adminSet), Scopes: scopes, + Preferences: preferences, Profile: profile, Privacy: privacy, DateCreated: time.Now(), MostRecentClientVersion: clientVersion} - accountAuthType.Account = account - return accountAuthType, nil + // account.AuthTypes[0].Account = account + // for i := range account.Identifiers { + // account.Identifiers[i].Account = account + // } + return &account, nil } -func (a *Auth) storeNewAccountInfo(context storage.TransactionContext, account model.Account, credential *model.Credential, useSharedProfile bool, profile model.Profile) error { +func (a *Auth) storeNewAccountInfo(context storage.TransactionContext, account model.Account, credential *model.Credential, profile model.Profile) error { //insert account object - it includes the account auth type _, err := a.storage.InsertAccount(context, account) if err != nil { @@ -1546,221 +1611,266 @@ func (a *Auth) storeNewAccountInfo(context storage.TransactionContext, account m //insert or update credential if credential != nil { - if useSharedProfile { - //update credential - err = a.storage.UpdateCredential(context, credential) - if err != nil { - return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeCredential, nil, err) - } - } else { - //create credential - err = a.storage.InsertCredential(context, credential) - if err != nil { - return errors.WrapErrorAction(logutils.ActionInsert, model.TypeCredential, nil, err) - } + // if useSharedProfile { + // //update credential + // err = a.storage.UpdateCredential(context, credential) + // if err != nil { + // return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeCredential, nil, err) + // } + // } + + //create credential + err = a.storage.InsertCredential(context, credential) + if err != nil { + return errors.WrapErrorAction(logutils.ActionInsert, model.TypeCredential, nil, err) } } //update profile if shared - if useSharedProfile { - err = a.storage.UpdateAccountProfile(context, profile) - if err != nil { - return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeProfile, nil, err) - } - } + // if useSharedProfile { + // err = a.storage.UpdateAccountProfile(context, profile) + // if err != nil { + // return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeProfile, nil, err) + // } + // } return nil } -func (a *Auth) checkUsername(context storage.TransactionContext, appOrg *model.ApplicationOrganization, username string) error { - accounts, err := a.storage.FindAccountsByUsername(context, appOrg, username) - if err != nil { - return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) - } - if len(accounts) > 0 { - return errors.ErrorData(logutils.StatusInvalid, model.TypeAccountUsername, logutils.StringArgs(username+" taken")).SetStatus(utils.ErrorStatusUsernameTaken) - } +func (a *Auth) linkAccountAuthType(account *model.Account, supportedAuthType model.SupportedAuthType, appOrg model.ApplicationOrganization, appType *model.ApplicationType, + creds string, params string) (*string, *model.AccountAuthType, error) { + var message *string + var aat *model.AccountAuthType + var err error - return nil -} + var accountIdentifier *model.AccountIdentifier + tryIdentifierLink := false + identifierImpl := a.getIdentifierTypeImpl(creds, nil, nil) + if identifierImpl != nil { + accountIdentifier = account.GetAccountIdentifier(identifierImpl.getCode(), identifierImpl.getIdentifier()) -func (a *Auth) linkAccountAuthType(account model.Account, authType model.AuthType, appOrg model.ApplicationOrganization, - creds string, params string, l *logs.Log) (string, *model.AccountAuthType, error) { - authImpl, err := a.getAuthTypeImpl(authType) - if err != nil { - return "", nil, errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, nil, err) + // only try if an identifier was provided and the account does not already have it (conflicts will be handled if attempted) + tryIdentifierLink = (accountIdentifier == nil) } - userIdentifier, err := authImpl.getUserIdentifier(creds) + authImpl, err := a.getAuthTypeImpl(supportedAuthType) if err != nil { - return "", nil, errors.WrapErrorAction(logutils.ActionGet, "user identifier", nil, err) + return nil, nil, errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, nil, err) } - //2. check if the user exists - newCredsAccount, err := a.storage.FindAccount(nil, appOrg.ID, authType.ID, userIdentifier) + aats, err := a.findAccountAuthTypesAndCredentials(account, supportedAuthType) if err != nil { - return "", nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) + return nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccountAuthType, &logutils.FieldArgs{"auth_type_code": supportedAuthType.AuthType.Code}, err) + } + + inactiveAats := make([]model.AccountAuthType, 0) + for _, aat := range aats { + if !aat.Active { + inactiveAats = append(inactiveAats, aat) + } } - if newCredsAccount != nil { - //if account is current account, attempt sign-in. Otherwise, handle conflict - if newCredsAccount.ID == account.ID { - message, aat, err := a.applyLinkVerify(authImpl, authType, &account, userIdentifier, creds, l) + + transaction := func(context storage.TransactionContext) error { + if len(inactiveAats) > 0 { + // there are inactive account auth types (have not been used to sign in yet), so try to verify one of them using creds + var accountAuthType *model.AccountAuthType // do not return this account auth type, so use a new variable + message, accountAuthType, err = a.verifyAuthTypeActive(identifierImpl, accountIdentifier, authImpl, aats, account.ID, creds, params, appOrg) if err != nil { - return "", nil, err - } - if message != "" { - return "", nil, errors.ErrorData("incomplete", "verification", nil).SetStatus(utils.ErrorStatusUnverified) + return err } - if aat != nil { + if accountAuthType != nil { for i, accAuthType := range account.AuthTypes { - if accAuthType.ID == aat.ID { - account.AuthTypes[i] = *aat + if accAuthType.ID == accountAuthType.ID { + account.AuthTypes[i] = *accountAuthType break } } } - return "", nil, nil + + updateIdentifier := accountIdentifier != nil && !accountIdentifier.Verified + if updateIdentifier && message == nil { + accountIdentifier.Verified = true + err := a.storage.UpdateAccountIdentifier(context, *accountIdentifier) + if err != nil { + return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountIdentifier, nil, err) + } + } + + return nil + } + + if !authImpl.allowMultiple() && len(aats) > 0 { + // only one account auth type of this type is allowed, so try linking only the identifier + if tryIdentifierLink { + message, err = a.linkAccountIdentifier(context, account, identifierImpl) + if err != nil { + return errors.WrapErrorAction("linking", model.TypeAccountIdentifier, nil, err) + } + + return nil + } + + return errors.ErrorData(logutils.StatusInvalid, model.TypeAuthType, &logutils.FieldArgs{"allow_multiple": false, "code": supportedAuthType.AuthType.Code}) } - err = a.handleAccountAuthTypeConflict(*newCredsAccount, authType.ID, userIdentifier, false) + //apply sign up + signUpMessage, _, credential, err := authImpl.signUp(identifierImpl, &account.ID, appOrg, creds, params) if err != nil { - return "", nil, err + return errors.WrapErrorAction("signing up", "user", nil, err) + } + if signUpMessage != "" { + message = &signUpMessage } - } - credID := uuid.NewString() + if credential != nil { + credential.AuthType.ID = supportedAuthType.AuthType.ID + } - //apply sign up - message, credentialValue, err := authImpl.signUp(authType, appOrg, creds, params, credID, l) - if err != nil { - return "", nil, errors.WrapErrorAction("signing up", "user", nil, err) - } + var accountAuthTypeParams map[string]interface{} + if supportedAuthType.AuthType.Code == AuthTypeWebAuthn && appType != nil { + accountAuthTypeParams = map[string]interface{}{"app_type_identifier": appType.Identifier} + } + aat, err = a.prepareAccountAuthType(supportedAuthType.AuthType, false, accountAuthTypeParams, credential) + if err != nil { + return errors.WrapErrorAction(logutils.ActionCreate, model.TypeAccountAuthType, nil, err) + } + aat.Account = *account - //credential - var credential *model.Credential - if credentialValue != nil { - now := time.Now() - credential = &model.Credential{ID: credID, AccountsAuthTypes: nil, Value: credentialValue, Verified: false, - AuthType: authType, DateCreated: now, DateUpdated: &now} - } + err = a.registerAccountAuthType(context, *aat, credential, nil, false) + if err != nil { + return errors.WrapErrorAction(logutils.ActionRegister, model.TypeAccountAuthType, nil, err) + } - accountAuthType, credential, err := a.prepareAccountAuthType(authType, userIdentifier, nil, credential, true, true) - if err != nil { - return "", nil, errors.WrapErrorAction(logutils.ActionCreate, model.TypeAccountAuthType, nil, err) + if tryIdentifierLink { + message, err = a.linkAccountIdentifier(context, account, identifierImpl) + if err != nil { + return errors.WrapErrorAction("linking", model.TypeAccountIdentifier, nil, err) + } + } + + return nil } - accountAuthType.Account = account - err = a.registerAccountAuthType(*accountAuthType, credential, nil, l) + err = a.storage.PerformTransaction(transaction) if err != nil { - return "", nil, errors.WrapErrorAction(logutils.ActionRegister, model.TypeAccountAuthType, nil, err) + return nil, nil, err } - return message, accountAuthType, nil + return message, aat, nil } -func (a *Auth) applyLinkVerify(authImpl authType, authType model.AuthType, account *model.Account, - userIdentifier string, creds string, l *logs.Log) (string, *model.AccountAuthType, error) { - //find account auth type - accountAuthType, err := a.findAccountAuthType(account, &authType, userIdentifier) - if accountAuthType == nil { - return "", nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccountAuthType, nil, err) +func (a *Auth) verifyAuthTypeActive(identifierImpl identifierType, accountIdentifier *model.AccountIdentifier, authImpl authType, accountAuthTypes []model.AccountAuthType, accountID string, + creds string, params string, appOrg model.ApplicationOrganization) (*string, *model.AccountAuthType, error) { + if accountIdentifier != nil && identifierImpl.requireVerificationForSignIn() { + err := identifierImpl.checkVerified(accountIdentifier, appOrg.Application.Name) + if err != nil { + return nil, nil, errors.WrapErrorData(logutils.StatusInvalid, model.TypeAccountIdentifier, &logutils.FieldArgs{"verified": false}, err) + } } - if !accountAuthType.Linked { - return "", nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAccountAuthType, &logutils.FieldArgs{"linked": false}) + message, credID, err := a.checkCredentials(identifierImpl, authImpl, &accountID, accountAuthTypes, creds, params, appOrg) + if err != nil { + return nil, nil, errors.WrapErrorAction(logutils.ActionVerify, model.TypeCredential, nil, err) } - if !accountAuthType.Unverified { - return "", nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAccountAuthType, &logutils.FieldArgs{"verified": true}) - } + for _, aat := range accountAuthTypes { + if credID == "" || (aat.Credential != nil && aat.Credential.ID == credID) { + aat.Active = true + err = a.storage.UpdateAccountAuthType(nil, aat) + if err != nil { + return nil, nil, errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountAuthType, nil, err) + } - var message string - message, err = a.checkCredentials(authImpl, authType, accountAuthType, creds, l) - if err != nil { - return "", nil, errors.WrapErrorAction(logutils.ActionVerify, model.TypeCredential, nil, err) + return message, &aat, nil + } } - - return message, accountAuthType, nil + return nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccountAuthType, &logutils.FieldArgs{"credential_id": credID}) } -func (a *Auth) linkAccountAuthTypeExternal(account model.Account, authType model.AuthType, appType model.ApplicationType, appOrg model.ApplicationOrganization, +func (a *Auth) linkAccountAuthTypeExternal(account *model.Account, supportedAuthType model.SupportedAuthType, appType model.ApplicationType, appOrg model.ApplicationOrganization, creds string, params string, l *logs.Log) (*model.AccountAuthType, error) { - authImpl, err := a.getExternalAuthTypeImpl(authType) + authImpl, err := a.getExternalAuthTypeImpl(supportedAuthType.AuthType) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, nil, err) } - externalUser, _, _, err := authImpl.externalLogin(authType, appType, appOrg, creds, params, l) + externalUser, _, _, err := authImpl.externalLogin(supportedAuthType.AuthType, appType, appOrg, creds, params, l) if err != nil { return nil, errors.WrapErrorAction("logging in", "external user", nil, err) } - //2. check if the user exists - newCredsAccount, err := a.storage.FindAccount(nil, appOrg.ID, authType.ID, externalUser.Identifier) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) + // get the correct code for the external identifier from the external IDs map + code := "" + for k, v := range externalUser.ExternalIDs { + if v == externalUser.Identifier { + code = k + } } - //cannot link creds if an account already exists for new creds - if newCredsAccount != nil { - return nil, errors.ErrorData("existing", model.TypeAccount, nil).SetStatus(utils.ErrorStatusAlreadyExists) + if code == "" && externalUser.Email == externalUser.Identifier { + code = IdentifierTypeEmail } - accountAuthTypeParams := map[string]interface{}{} - accountAuthTypeParams["user"] = externalUser - - accountAuthType, credential, err := a.prepareAccountAuthType(authType, externalUser.Identifier, accountAuthTypeParams, nil, false, true) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionCreate, model.TypeAccountAuthType, nil, err) - } - accountAuthType.Account = account + var accountAuthType *model.AccountAuthType + transaction := func(context storage.TransactionContext) error { + newCredsAccount, err := a.storage.FindAccount(context, appOrg.ID, code, externalUser.Identifier) + if err != nil { + return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) + } + //cannot link creds if an account already exists for new creds + if newCredsAccount != nil { + return errors.ErrorData("existing", model.TypeAccount, nil).SetStatus(utils.ErrorStatusAlreadyExists) + } - for k, v := range externalUser.ExternalIDs { - if account.ExternalIDs == nil { - account.ExternalIDs = make(map[string]string) + accountAuthTypeParams := map[string]interface{}{"user": externalUser} + accountAuthType, err = a.prepareAccountAuthType(supportedAuthType.AuthType, true, accountAuthTypeParams, nil) + if err != nil { + return errors.WrapErrorAction(logutils.ActionCreate, model.TypeAccountAuthType, nil, err) } - if account.ExternalIDs[k] == "" { - account.ExternalIDs[k] = v + + updatedIdentifiers := a.updateExternalIdentifiers(account, accountAuthType.ID, externalUser, true) + + accountAuthType.Account = *account + err = a.registerAccountAuthType(context, *accountAuthType, nil, account.Identifiers, updatedIdentifiers) + if err != nil { + return errors.WrapErrorAction(logutils.ActionRegister, model.TypeAccountAuthType, nil, err) } + + return nil } - err = a.registerAccountAuthType(*accountAuthType, credential, account.ExternalIDs, l) + err = a.storage.PerformTransaction(transaction) if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionRegister, model.TypeAccountAuthType, nil, err) + return nil, err } return accountAuthType, nil } -func (a *Auth) registerAccountAuthType(accountAuthType model.AccountAuthType, credential *model.Credential, externalIDs map[string]string, l *logs.Log) error { +func (a *Auth) registerAccountAuthType(context storage.TransactionContext, accountAuthType model.AccountAuthType, credential *model.Credential, accountIdentifiers []model.AccountIdentifier, updatedIdentifiers bool) error { var err error if credential != nil { //TODO - in one transaction - if err = a.storage.InsertCredential(nil, credential); err != nil { + if err = a.storage.InsertCredential(context, credential); err != nil { return errors.WrapErrorAction(logutils.ActionInsert, model.TypeCredential, nil, err) } } - err = a.storage.InsertAccountAuthType(accountAuthType) + err = a.storage.InsertAccountAuthType(context, accountAuthType) if err != nil { - return errors.WrapErrorAction(logutils.ActionInsert, model.TypeAccount, nil, err) + return errors.WrapErrorAction(logutils.ActionInsert, model.TypeAccountAuthType, nil, err) } - if externalIDs != nil { - err = a.storage.UpdateAccountExternalIDs(accountAuthType.Account.ID, externalIDs) - if err != nil { - return errors.WrapErrorAction(logutils.ActionUpdate, "account external IDs", nil, err) - } - - err = a.storage.UpdateLoginSessionExternalIDs(accountAuthType.Account.ID, externalIDs) + if updatedIdentifiers { + err = a.storage.UpdateAccountIdentifiers(context, accountAuthType.Account.ID, accountIdentifiers) if err != nil { - return errors.WrapErrorAction(logutils.ActionUpdate, "login session external IDs", nil, err) + return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountIdentifier, nil, err) } } return nil } -func (a *Auth) unlinkAccountAuthType(accountID string, authenticationType string, appTypeIdentifier string, identifier string, l *logs.Log) (*model.Account, error) { +func (a *Auth) unlinkAccountAuthType(accountID string, accountAuthTypeID *string, authenticationType *string, identifier *string, admin bool) (*model.Account, error) { account, err := a.storage.FindAccountByID(nil, accountID) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) @@ -1768,14 +1878,35 @@ func (a *Auth) unlinkAccountAuthType(accountID string, authenticationType string if account == nil { return nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, &logutils.FieldArgs{"id": accountID}) } + if len(account.AuthTypes) < 2 { + return nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAccount, &logutils.FieldArgs{"auth_types": len(account.AuthTypes)}) + } for i, aat := range account.AuthTypes { // unlink auth type with matching code and identifier - if aat.AuthType.Code == authenticationType && aat.Identifier == identifier { - aat.Account = *account - err := a.removeAccountAuthType(aat) + aatIDMatch := accountAuthTypeID != nil && aat.ID == *accountAuthTypeID + aatCodeMatch := authenticationType != nil && utils.Contains(aat.SupportedAuthType.AuthType.Aliases, *authenticationType) + if aatIDMatch || aatCodeMatch { + transaction := func(context storage.TransactionContext) error { + if aatCodeMatch && identifier != nil && (!aat.SupportedAuthType.AuthType.IsExternal || admin) { + err = a.unlinkAccountIdentifier(context, account, nil, identifier, admin) + if err != nil { + return errors.WrapErrorAction("unlinking", model.TypeAccountIdentifier, &logutils.FieldArgs{"account_id": account.ID, "identifier": *identifier}, err) + } + } + + aat.Account = *account + err = a.removeAccountAuthType(context, aat) + if err != nil { + return errors.WrapErrorAction("unlinking", model.TypeAccountAuthType, nil, err) + } + + return nil + } + + err = a.storage.PerformTransaction(transaction) if err != nil { - return nil, errors.WrapErrorAction("unlinking", model.TypeAccountAuthType, nil, err) + return nil, err } account.AuthTypes = append(account.AuthTypes[:i], account.AuthTypes[i+1:]...) @@ -1786,15 +1917,77 @@ func (a *Auth) unlinkAccountAuthType(accountID string, authenticationType string return account, nil } -func (a *Auth) handleAccountAuthTypeConflict(account model.Account, authTypeID string, userIdentifier string, newAccount bool) error { - aat := account.GetAccountAuthType(authTypeID, userIdentifier) - if aat == nil || !aat.Unverified { +func (a *Auth) linkAccountIdentifier(context storage.TransactionContext, account *model.Account, identifierImpl identifierType) (*string, error) { + identifier := identifierImpl.getIdentifier() + + existingIdentifierAccount, err := a.storage.FindAccount(context, account.AppOrg.ID, identifierImpl.getCode(), identifier) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) + } + if existingIdentifierAccount != nil { + err = a.handleAccountIdentifierConflict(*existingIdentifierAccount, identifierImpl, false) + if err != nil { + return nil, err + } + } + + message, accountIdentifier, err := identifierImpl.buildIdentifier(&account.ID, account.AppOrg.Application.Name) + if err != nil { + return nil, errors.WrapErrorAction("building", model.TypeAccountIdentifier, &logutils.FieldArgs{"account_id": account.ID, "identifier": identifier}, err) + } + accountIdentifier.Linked = true + + account.Identifiers = append(account.Identifiers, *accountIdentifier) + err = a.storage.InsertAccountIdentifier(context, *accountIdentifier) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionCreate, model.TypeAccountIdentifier, &logutils.FieldArgs{"account_id": account.ID, "identifier": identifier}, err) + } + + return &message, nil +} + +func (a *Auth) unlinkAccountIdentifier(context storage.TransactionContext, account *model.Account, accountIdentifierID *string, identifier *string, admin bool) error { + if len(account.Identifiers) < 2 { + return errors.ErrorData(logutils.StatusInvalid, model.TypeAccount, &logutils.FieldArgs{"identifiers": len(account.Identifiers)}) + } + + verifiedIdentifiers := account.GetVerifiedAccountIdentifiers() + if len(verifiedIdentifiers) == 1 { + idMatch := accountIdentifierID != nil && verifiedIdentifiers[0].ID == *accountIdentifierID + identifierMatch := identifier != nil && verifiedIdentifiers[0].Identifier == *identifier + if idMatch || identifierMatch { + return errors.ErrorData(logutils.StatusInvalid, model.TypeAccount, &logutils.FieldArgs{"verified_identifiers": 1}) + } + } + + for i, id := range account.Identifiers { + // unlink identifier with matching identifier value (do not directly unlink identifiers with associated auth type unless admin) + if id.AccountAuthTypeID == nil || admin { + if (identifier != nil && *identifier == id.Identifier) || (accountIdentifierID != nil && *accountIdentifierID == id.ID) { + id.Account = *account + err := a.storage.DeleteAccountIdentifier(context, id) + if err != nil { + return errors.WrapErrorAction(logutils.ActionDelete, model.TypeAccountIdentifier, nil, err) + } + + account.Identifiers = append(account.Identifiers[:i], account.Identifiers[i+1:]...) + break + } + } + } + + return nil +} + +func (a *Auth) handleAccountIdentifierConflict(account model.Account, identifierImpl identifierType, newAccount bool) error { + accountIdentifier := account.GetAccountIdentifier(identifierImpl.getCode(), identifierImpl.getIdentifier()) + if accountIdentifier == nil || accountIdentifier.Verified { //cannot link creds if a verified account already exists for new creds return errors.ErrorData("existing", model.TypeAccount, nil).SetStatus(utils.ErrorStatusAlreadyExists) } //if this is the only auth type (this will only be possible for accounts created through sign up that were never verified/used) - if len(account.AuthTypes) == 1 { + if len(account.Identifiers) == 1 { //if signing up, do not replace previous unverified account created through sign up if newAccount { return errors.ErrorData("existing", model.TypeAccount, nil).SetStatus(utils.ErrorStatusAlreadyExists) @@ -1806,7 +1999,7 @@ func (a *Auth) handleAccountAuthTypeConflict(account model.Account, authTypeID s } } else { //Otherwise unlink auth type from account - err := a.removeAccountAuthType(*aat) + err := a.storage.DeleteAccountIdentifier(nil, *accountIdentifier) if err != nil { return errors.WrapErrorAction(logutils.ActionDelete, model.TypeAccountAuthType, nil, err) } @@ -1815,34 +2008,30 @@ func (a *Auth) handleAccountAuthTypeConflict(account model.Account, authTypeID s return nil } -func (a *Auth) removeAccountAuthType(aat model.AccountAuthType) error { - transaction := func(context storage.TransactionContext) error { - //1. delete account auth type in account - err := a.storage.DeleteAccountAuthType(context, aat) - if err != nil { - return errors.WrapErrorAction(logutils.ActionDelete, model.TypeAccountAuthType, nil, err) - } +func (a *Auth) removeAccountAuthType(context storage.TransactionContext, aat model.AccountAuthType) error { + //1. delete account auth type in account + err := a.storage.DeleteAccountAuthType(context, aat) + if err != nil { + return errors.WrapErrorAction(logutils.ActionDelete, model.TypeAccountAuthType, nil, err) + } - //2. delete credential if it exists - if aat.Credential != nil { - err = a.removeAccountAuthTypeCredential(context, aat) - if err != nil { - return errors.WrapErrorAction(logutils.ActionDelete, model.TypeCredential, nil, err) - } + //2. delete credential if it exists + if aat.Credential != nil { + err = a.removeAccountAuthTypeCredential(context, aat) + if err != nil { + return errors.WrapErrorAction(logutils.ActionDelete, model.TypeCredential, nil, err) } + } - //3. delete login sessions using unlinked account auth type (if unverified no sessions should exist) - if !aat.Unverified { - err = a.storage.DeleteLoginSessionsByAccountAuthTypeID(context, aat.ID) - if err != nil { - return errors.WrapErrorAction(logutils.ActionDelete, model.TypeLoginSession, nil, err) - } + //3. delete identifiers with matching account auth type ID + if aat.Params["user"] != nil { + err = a.storage.DeleteExternalAccountIdentifiers(context, aat) + if err != nil { + return errors.WrapErrorAction(logutils.ActionDelete, model.TypeAccountIdentifier, &logutils.FieldArgs{"external": true, "account_auth_type_id": aat.ID}, err) } - - return nil } - return a.storage.PerformTransaction(transaction) + return nil } func (a *Auth) removeAccountAuthTypeCredential(context storage.TransactionContext, aat model.AccountAuthType) error { @@ -1992,7 +2181,7 @@ func (a *Auth) buildAccessTokenForServiceAccount(account model.ServiceAccount, a aud = strings.Join(services, ",") } - claims := a.getStandardClaims(account.AccountID, "", account.Name, "", "", aud, orgID, appID, authType, nil, nil, false, true, false, false, true, account.FirstParty, "") + claims := a.getStandardClaims(account.AccountID, account.Name, "", "", "", aud, orgID, appID, authType, nil, nil, false, true, false, false, true, account.FirstParty, "") accessToken, err := a.buildAccessToken(claims, strings.Join(permissions, ","), scope) if err != nil { return "", nil, errors.WrapErrorAction(logutils.ActionCreate, logutils.TypeToken, nil, err) @@ -2000,6 +2189,16 @@ func (a *Auth) buildAccessTokenForServiceAccount(account model.ServiceAccount, a return accessToken, &model.AppOrgPair{AppID: appID, OrgID: orgID}, nil } +func (a *Auth) registerIdentifierType(name string, identifier identifierType) error { + if _, ok := a.identifierTypes[name]; ok { + return errors.ErrorData(logutils.StatusFound, typeIdentifierType, &logutils.FieldArgs{"name": name}) + } + + a.identifierTypes[name] = identifier + + return nil +} + func (a *Auth) registerAuthType(name string, auth authType) error { if _, ok := a.authTypes[name]; ok { return errors.ErrorData(logutils.StatusFound, model.TypeAuthType, &logutils.FieldArgs{"name": name}) @@ -2050,77 +2249,124 @@ func (a *Auth) registerMfaType(name string, mfa mfaType) error { return nil } -func (a *Auth) validateAuthType(authenticationType string, appTypeIdentifier string, orgID string) (*model.AuthType, *model.ApplicationType, *model.ApplicationOrganization, error) { +func (a *Auth) validateAuthType(authenticationType string, appTypeIdentifier *string, appID *string, orgID string) (*model.SupportedAuthType, *model.ApplicationType, *model.ApplicationOrganization, error) { //get the auth type authType, err := a.storage.FindAuthType(authenticationType) if err != nil || authType == nil { return nil, nil, nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthType, logutils.StringArgs(authenticationType), err) } - //get the app type - applicationType, err := a.storage.FindApplicationType(appTypeIdentifier) + //get the app type and app org + applicationType, appOrg, err := a.validateAppOrg(appTypeIdentifier, appID, orgID) if err != nil { - return nil, nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeApplicationType, logutils.StringArgs(appTypeIdentifier), err) + return nil, nil, nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeApplicationOrganization, nil, err) + } + + //check if the auth type is supported for this application and organization + if applicationType != nil { + supportedAuthType := appOrg.FindSupportedAuthType(*applicationType, *authType) + if supportedAuthType == nil { + return nil, nil, nil, errors.ErrorAction(logutils.ActionValidate, "not supported auth type for application and organization", &logutils.FieldArgs{"app_type_id": applicationType.ID, "auth_type_id": authType.ID}) + } + return supportedAuthType, applicationType, appOrg, nil } - if applicationType == nil { - return nil, nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeApplicationType, logutils.StringArgs(appTypeIdentifier)) + + for _, appType := range appOrg.Application.Types { + supportedAuthType := appOrg.FindSupportedAuthType(appType, *authType) + if supportedAuthType != nil { + appTypeValue := appType + return supportedAuthType, &appTypeValue, appOrg, nil + } + } + return nil, nil, nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAuthType, &logutils.FieldArgs{"app_org_id": appOrg.ID, "auth_type": authenticationType}) +} + +func (a *Auth) validateAppOrg(appTypeIdentifier *string, appID *string, orgID string) (*model.ApplicationType, *model.ApplicationOrganization, error) { + var applicationID string + var applicationType *model.ApplicationType + var err error + if appID != nil { + applicationID = *appID + } else if appTypeIdentifier != nil { + applicationType, err = a.storage.FindApplicationType(*appTypeIdentifier) + if err != nil { + return nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeApplicationType, logutils.StringArgs(*appTypeIdentifier), err) + + } + if applicationType == nil { + return nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeApplicationType, logutils.StringArgs(*appTypeIdentifier)) + } + applicationID = applicationType.Application.ID } //get the app org - applicationID := applicationType.Application.ID appOrg, err := a.storage.FindApplicationOrganization(applicationID, orgID) if err != nil { - return nil, nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeApplicationOrganization, &logutils.FieldArgs{"app_id": applicationID, "org_id": orgID}, err) + return nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeApplicationOrganization, &logutils.FieldArgs{"app_id": applicationID, "org_id": orgID}, err) } if appOrg == nil { - return nil, nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeApplicationOrganization, &logutils.FieldArgs{"app_id": applicationID, "org_id": orgID}) - } - - //check if the auth type is supported for this application and organization - if !appOrg.IsAuthTypeSupported(*applicationType, *authType) { - return nil, nil, nil, errors.ErrorAction(logutils.ActionValidate, "not supported auth type for application and organization", &logutils.FieldArgs{"app_type_id": applicationType.ID, "auth_type_id": authType.ID}) + return nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeApplicationOrganization, &logutils.FieldArgs{"app_id": applicationID, "org_id": orgID}) } - return authType, applicationType, appOrg, nil + return applicationType, appOrg, nil } -func (a *Auth) validateAuthTypeForAppOrg(authenticationType string, appID string, orgID string) (*model.AuthType, *model.ApplicationOrganization, error) { - authType, err := a.storage.FindAuthType(authenticationType) - if err != nil || authType == nil { - return nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAuthType, logutils.StringArgs(authenticationType), err) - } +func (a *Auth) getIdentifierTypeImpl(identifierJSON string, identifierCode *string, userIdentifier *string) identifierType { + if identifierCode != nil && userIdentifier != nil { + code := *identifierCode + if code == illinoisOIDCCode { + // backwards compatibility: if an OIDC auth type is used, illinois_oidc was provided, so use uin as the identifier code + code = defaultIllinoisOIDCIdentifier + } else if code == string(phoneverifier.TypeTwilio) { + code = IdentifierTypePhone + } - appOrg, err := a.storage.FindApplicationOrganization(appID, orgID) - if err != nil { - return nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeApplicationOrganization, &logutils.FieldArgs{"app_id": appID, "org_id": orgID}, err) - } - if appOrg == nil { - return nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeApplicationOrganization, &logutils.FieldArgs{"app_id": appID, "org_id": orgID}) + identifierMap := map[string]string{code: *userIdentifier} + identifierBytes, err := json.Marshal(identifierMap) + if err != nil { + a.logger.Errorf("error marshalling json for identifierType: %v", err) + return nil + } + identifierJSON = string(identifierBytes) } - for _, appType := range appOrg.Application.Types { - if appOrg.IsAuthTypeSupported(appType, *authType) { - return authType, appOrg, nil + if identifierJSON != "" { + for code, identifierImpl := range a.identifierTypes { + if code == IdentifierTypeExternal { + continue + } + if strings.Contains(identifierJSON, code) { + specificIdentifierImpl, err := identifierImpl.withIdentifier(identifierJSON) + if err == nil && specificIdentifierImpl.getIdentifier() != "" { + return specificIdentifierImpl + } + } + } + + // default to the external identifier type + specificExternalImpl, err := a.identifierTypes[IdentifierTypeExternal].withIdentifier(identifierJSON) + if err == nil && specificExternalImpl.getIdentifier() != "" { + return specificExternalImpl } } - return nil, nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAuthType, &logutils.FieldArgs{"app_org_id": appOrg.ID, "auth_type": authenticationType}) + return nil } -func (a *Auth) getAuthTypeImpl(authType model.AuthType) (authType, error) { - if auth, ok := a.authTypes[authType.Code]; ok { - return auth, nil +func (a *Auth) getAuthTypeImpl(supportedAuthType model.SupportedAuthType) (authType, error) { + if auth, ok := a.authTypes[supportedAuthType.AuthType.Code]; ok { + return auth.withParams(supportedAuthType.Params) } - return nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAuthType, logutils.StringArgs(authType.Code)) + return nil, errors.ErrorData(logutils.StatusInvalid, model.TypeAuthType, logutils.StringArgs(supportedAuthType.AuthType.Code)) } func (a *Auth) getExternalAuthTypeImpl(authType model.AuthType) (externalAuthType, error) { key := authType.Code //illinois_oidc, other_oidc - if strings.HasSuffix(authType.Code, "_oidc") { - key = "oidc" + if strings.HasSuffix(authType.Code, "_"+AuthTypeOidc) { + key = AuthTypeOidc } if auth, ok := a.externalAuthTypes[key]; ok { @@ -2168,15 +2414,6 @@ func (a *Auth) buildCsrfToken(claims tokenauth.Claims) (string, error) { return tokenauth.GenerateSignedToken(&claims, a.authPrivKey) } -func (a *Auth) buildRefreshToken() (string, error) { - newToken, err := utils.GenerateRandomString(refreshTokenLength) - if err != nil { - return "", errors.WrapErrorAction(logutils.ActionCompute, logutils.TypeToken, nil, err) - } - - return newToken, nil -} - // getScopedAccessToken returns a scoped access token with the requested scopes func (a *Auth) getScopedAccessToken(claims tokenauth.Claims, serviceID string, scopes []authorization.Scope) (string, error) { aud, scope := a.tokenDataForScopes(scopes) @@ -2201,8 +2438,8 @@ func (a *Auth) tokenDataForScopes(scopes []authorization.Scope) ([]string, strin return services, strings.Join(scopeStrings, " ") } -func (a *Auth) getStandardClaims(sub string, uid string, name string, email string, phone string, aud string, orgID string, appID string, - authType string, externalIDs map[string]string, exp *int64, anonymous bool, authenticated bool, admin bool, system bool, service bool, firstParty bool, sessionID string) tokenauth.Claims { +func (a *Auth) getStandardClaims(sub string, name string, email string, phone string, username string, aud string, orgID string, appID string, authType string, externalIDs map[string]string, + exp *int64, anonymous bool, authenticated bool, admin bool, system bool, service bool, firstParty bool, sessionID string) tokenauth.Claims { return tokenauth.Claims{ StandardClaims: jwt.StandardClaims{ Audience: aud, @@ -2210,7 +2447,7 @@ func (a *Auth) getStandardClaims(sub string, uid string, name string, email stri ExpiresAt: a.getExp(exp), IssuedAt: time.Now().Unix(), Issuer: a.host, - }, OrgID: orgID, AppID: appID, AuthType: authType, UID: uid, Name: name, Email: email, Phone: phone, + }, OrgID: orgID, AppID: appID, AuthType: authType, Name: name, Email: email, Phone: phone, Username: username, ExternalIDs: externalIDs, Anonymous: anonymous, Authenticated: authenticated, Admin: admin, System: system, Service: service, FirstParty: firstParty, SessionID: sessionID, } @@ -2329,6 +2566,58 @@ func (a *Auth) updateExternalAccountGroups(account *model.Account, newExternalGr return updated, nil } +func (a *Auth) updateExternalIdentifiers(account *model.Account, accountAuthTypeID string, externalUser *model.ExternalSystemUser, linked bool) bool { + updated := false + now := time.Now().UTC() + for k, v := range externalUser.ExternalIDs { + accountIdentifier := account.GetAccountIdentifier(k, "") + if accountIdentifier == nil { + primary := (v == externalUser.Identifier) + newIdentifier := model.AccountIdentifier{ID: uuid.NewString(), Code: k, Identifier: v, Verified: true, Linked: linked, AccountAuthTypeID: &accountAuthTypeID, + Sensitive: utils.Contains(externalUser.SensitiveExternalIDs, k), Primary: &primary, Account: model.Account{ID: account.ID}, DateCreated: now} + account.Identifiers = append(account.Identifiers, newIdentifier) + updated = true + } else if accountIdentifier.Identifier != v { + now := time.Now().UTC() + primary := (v == externalUser.Identifier) + accountIdentifier.Identifier = v + accountIdentifier.Primary = &primary + accountIdentifier.DateUpdated = &now + updated = true + } + } + + if externalUser.Email != "" { + hasExternalEmail := false + for i, identifier := range account.Identifiers { + if identifier.Code == IdentifierTypeEmail { + aatMatch := identifier.AccountAuthTypeID != nil && *identifier.AccountAuthTypeID == accountAuthTypeID // have an external email + identifierMatch := identifier.AccountAuthTypeID == nil && identifier.Identifier == externalUser.Email // have an internal email matching external email field + hasExternalEmail = aatMatch || identifierMatch + if (aatMatch && identifier.Identifier != externalUser.Email) || identifierMatch { + // update if have mismatching external email or internal email matching external email field + primary := (externalUser.Email == externalUser.Identifier) + account.Identifiers[i].Identifier = externalUser.Email + account.Identifiers[i].Primary = &primary + updated = true + } + if hasExternalEmail { + break + } + } + } + if !hasExternalEmail { + primary := (externalUser.Email == externalUser.Identifier) + account.Identifiers = append(account.Identifiers, model.AccountIdentifier{ID: uuid.NewString(), Code: IdentifierTypeEmail, Identifier: externalUser.Email, + Verified: externalUser.IsEmailVerified, Linked: linked, Sensitive: true, AccountAuthTypeID: &accountAuthTypeID, Primary: &primary, + Account: model.Account{ID: account.ID}, DateCreated: now}) + updated = true + } + } + + return updated +} + func (a *Auth) setLogContext(account *model.Account, l *logs.Log) { accountID := "nil" if account != nil { @@ -2365,7 +2654,7 @@ func (a *Auth) storeCoreRegs() error { // storeCoreServiceAccount stores the service account record for the Core BB func (a *Auth) storeCoreServiceAccount() { - coreAccount := model.ServiceAccount{AccountID: a.serviceID, Name: "ROKWIRE Core Building Block", FirstParty: true, DateCreated: time.Now()} + coreAccount := model.ServiceAccount{AccountID: a.serviceID, Name: "ROKWIRE Core Building Block", FirstParty: true, DateCreated: time.Now().UTC()} // Setup core service account if missing a.storage.InsertServiceAccount(&coreAccount) } diff --git a/core/auth/auth_type_code.go b/core/auth/auth_type_code.go new file mode 100644 index 000000000..0977f8726 --- /dev/null +++ b/core/auth/auth_type_code.go @@ -0,0 +1,190 @@ +// Copyright 2023 Board of Trustees of the University of Illinois. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "core-building-block/core/model" + "core-building-block/utils" + "encoding/json" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + "github.com/rokwire/logging-library-go/v2/errors" + "github.com/rokwire/logging-library-go/v2/logutils" + "gopkg.in/go-playground/validator.v9" +) + +const ( + //AuthTypeCode code auth type + AuthTypeCode string = "code" + + typeAuthenticationCode string = "authentication code" + + stateKeyCode string = "code" + + typeCodeCreds logutils.MessageDataType = "code creds" +) + +// codeCreds represents the creds struct for code authentication +type codeCreds struct { + Code *string `json:"code,omitempty"` +} + +// Code implementation of authType +type codeAuthImpl struct { + auth *Auth + authType string +} + +func (a *codeAuthImpl) signUp(identifierImpl identifierType, accountID *string, appOrg model.ApplicationOrganization, creds string, params string) (string, *model.AccountIdentifier, *model.Credential, error) { + identifierChannel, _ := identifierImpl.(authCommunicationChannel) + if identifierChannel == nil { + return "", nil, nil, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, logutils.StringArgs(identifierImpl.getCode())) + } + + if accountID != nil { + return "", nil, nil, nil + } + + // we are not linking a code credential, so use the accountID generated for the identifier + message, accountIdentifier, err := identifierImpl.buildIdentifier(nil, appOrg.Application.Name) + if err != nil { + return "", nil, nil, errors.WrapErrorAction("building", "identifier", logutils.StringArgs(identifierImpl.getCode()), err) + } + + return message, accountIdentifier, nil, nil +} + +func (a *codeAuthImpl) signUpAdmin(identifierImpl identifierType, appOrg model.ApplicationOrganization, creds string) (map[string]interface{}, *model.AccountIdentifier, *model.Credential, error) { + return nil, nil, nil, errors.New(logutils.Unimplemented) +} + +func (a *codeAuthImpl) forgotCredential(identifierImpl identifierType, credential *model.Credential, appOrg model.ApplicationOrganization) (map[string]interface{}, error) { + return nil, errors.New(logutils.Unimplemented) +} + +func (a *codeAuthImpl) resetCredential(credential *model.Credential, resetCode *string, params string) (map[string]interface{}, error) { + return nil, errors.New(logutils.Unimplemented) +} + +func (a *codeAuthImpl) checkCredentials(identifierImpl identifierType, accountID *string, aats []model.AccountAuthType, creds string, params string, appOrg model.ApplicationOrganization) (string, string, error) { + identifierChannel, _ := identifierImpl.(authCommunicationChannel) + if identifierChannel == nil { + return "", "", errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, logutils.StringArgs(identifierImpl.getCode())) + } + + incomingCreds, err := a.parseCreds(creds) + if err != nil { + return "", "", errors.WrapErrorAction(logutils.ActionParse, typeCodeCreds, nil, err) + } + incomingCode := "" + if incomingCreds.Code != nil { + incomingCode = *incomingCreds.Code + } + + if identifierChannel.requiresCodeGeneration() { + if incomingCode == "" { + // generate a new code + code := strconv.Itoa(utils.GenerateRandomInt(1000000)) + padLen := 6 - len(code) + if padLen > 0 { + code = strings.Repeat("0", padLen) + code + } + + // store generated codes in login state collection + state := map[string]interface{}{stateKeyCode: code} + loginState := model.LoginState{ID: uuid.NewString(), AppID: appOrg.Application.ID, OrgID: appOrg.Organization.ID, AccountID: accountID, State: state, DateCreated: time.Now().UTC()} + err := a.auth.storage.InsertLoginState(loginState) + if err != nil { + return "", "", errors.WrapErrorAction(logutils.ActionCreate, model.TypeLoginState, nil, err) + } + } else { + params := map[string]interface{}{ + stateKeyCode: *incomingCreds.Code, + } + loginState, err := a.auth.storage.FindLoginState(appOrg.Application.ID, appOrg.Organization.ID, accountID, params) + if err != nil { + return "", "", errors.WrapErrorAction(logutils.ActionFind, model.TypeLoginState, nil, err) + } + + if loginState == nil { + return "", "", errors.ErrorData(logutils.StatusInvalid, "code", logutils.StringArgs(*incomingCreds.Code)) + } + + return "", "", nil + } + } + + message, err := identifierChannel.sendCode(appOrg.Application.Name, incomingCode, typeAuthenticationCode, "") + if err != nil { + return "", "", err + } + + return message, "", nil +} + +func (a *codeAuthImpl) withParams(params map[string]interface{}) (authType, error) { + return a, nil +} + +func (a *codeAuthImpl) requireIdentifierVerificationForSignIn() bool { + return false +} + +func (a *codeAuthImpl) allowMultiple() bool { + return false +} + +// Helpers + +func (a *codeAuthImpl) parseCreds(creds string) (*codeCreds, error) { + var credential codeCreds + err := json.Unmarshal([]byte(creds), &credential) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeCodeCreds, nil, err) + } + err = validator.New().Struct(credential) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionValidate, typeCodeCreds, nil, err) + } + return &credential, nil +} + +func (a *codeAuthImpl) mapToCreds(credsMap map[string]interface{}) (*codeCreds, error) { + creds, err := utils.JSONConvert[codeCreds, map[string]interface{}](credsMap) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionParse, typeCodeCreds, nil, err) + } + + err = validator.New().Struct(creds) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionValidate, typeCodeCreds, nil, err) + } + return creds, nil +} + +// initCodeAuth initializes and registers a new code auth instance +func initCodeAuth(auth *Auth) (*codeAuthImpl, error) { + code := &codeAuthImpl{auth: auth, authType: AuthTypeCode} + + err := auth.registerAuthType(code.authType, code) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionRegister, model.TypeAuthType, nil, err) + } + + return code, nil +} diff --git a/core/auth/auth_type_email.go b/core/auth/auth_type_email.go deleted file mode 100644 index 2f11c18fc..000000000 --- a/core/auth/auth_type_email.go +++ /dev/null @@ -1,517 +0,0 @@ -// Copyright 2022 Board of Trustees of the University of Illinois. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package auth - -import ( - "core-building-block/core/model" - "core-building-block/utils" - "crypto/subtle" - "encoding/json" - "fmt" - "net/url" - "time" - - "github.com/rokwire/logging-library-go/v2/errors" - "github.com/rokwire/logging-library-go/v2/logs" - "github.com/rokwire/logging-library-go/v2/logutils" - "golang.org/x/crypto/bcrypt" -) - -const ( - //AuthTypeEmail email auth type - AuthTypeEmail string = "email" - - typeTime logutils.MessageDataType = "time.Time" - typeEmailCreds logutils.MessageDataType = "email creds" - typeEmailParams logutils.MessageDataType = "email params" -) - -// enailCreds represents the creds struct for email auth -type emailCreds struct { - Email string `json:"email" bson:"email" validate:"required"` - Password string `json:"password" bson:"password"` - VerificationCode string `json:"verification_code" bson:"verification_code"` - VerificationExpiry time.Time `json:"verification_expiry" bson:"verification_expiry"` - ResetCode string `json:"reset_code" bson:"reset_code"` - ResetExpiry time.Time `json:"reset_expiry" bson:"reset_expiry"` -} - -// Email implementation of authType -type emailAuthImpl struct { - auth *Auth - authType string -} - -func (a *emailAuthImpl) signUp(authType model.AuthType, appOrg model.ApplicationOrganization, creds string, params string, newCredentialID string, l *logs.Log) (string, map[string]interface{}, error) { - type signUpEmailParams struct { - ConfirmPassword string `json:"confirm_password"` - } - - var sEmailCreds emailCreds - err := json.Unmarshal([]byte(creds), &sEmailCreds) - if err != nil { - return "", nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeEmailCreds, nil, err) - } - - var sEmailParams signUpEmailParams - err = json.Unmarshal([]byte(params), &sEmailParams) - if err != nil { - return "", nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeEmailParams, nil, err) - } - - email := sEmailCreds.Email - password := sEmailCreds.Password - confirmPassword := sEmailParams.ConfirmPassword - if len(email) == 0 { - return "", nil, errors.ErrorData(logutils.StatusMissing, typeEmailCreds, logutils.StringArgs("email")) - } - if len(password) == 0 { - return "", nil, errors.ErrorData(logutils.StatusMissing, typeEmailCreds, logutils.StringArgs("password")) - } - if len(confirmPassword) == 0 { - return "", nil, errors.ErrorData(logutils.StatusMissing, typeEmailParams, logutils.StringArgs("confirm_password")) - } - //check if the passwrod matches with the confirm password one - if password != confirmPassword { - return "", nil, errors.ErrorData(logutils.StatusInvalid, "mismatching password fields", nil) - } - - emailCreds, err := a.buildCredentials(authType, appOrg.Application.Name, email, password, newCredentialID) - if err != nil { - return "", nil, errors.WrapErrorAction("building", "email credentials", nil, err) - } - - return "verification code sent successfully", emailCreds, nil -} - -func (a *emailAuthImpl) signUpAdmin(authType model.AuthType, appOrg model.ApplicationOrganization, identifier string, password string, newCredentialID string) (map[string]interface{}, map[string]interface{}, error) { - if password == "" { - password = utils.GenerateRandomPassword(12) - } - - emailCreds, err := a.buildCredentials(authType, appOrg.Application.Name, identifier, password, newCredentialID) - if err != nil { - return nil, nil, errors.WrapErrorAction("building", "email credentials", nil, err) - } - - params := map[string]interface{}{"password": password} - return params, emailCreds, nil -} - -func (a *emailAuthImpl) isCredentialVerified(credential *model.Credential, l *logs.Log) (*bool, *bool, error) { - if credential.Verified { - verified := true - return &verified, nil, nil - } - - //check if email verification is off - verifyEmail := a.getVerifyEmail(credential.AuthType) - if !verifyEmail { - verified := true - return &verified, nil, nil - } - - //it is unverified - verified := false - //check if the verification is expired - storedCreds, err := mapToEmailCreds(credential.Value) - if err != nil { - return nil, nil, errors.WrapErrorAction(logutils.ActionCast, typeEmailCreds, nil, err) - } - expired := false - if storedCreds.VerificationExpiry.Before(time.Now()) { - expired = true - } - return &verified, &expired, nil -} - -func (a *emailAuthImpl) checkCredentials(accountAuthType model.AccountAuthType, creds string, l *logs.Log) (string, error) { - //get stored credential - storedCreds, err := mapToEmailCreds(accountAuthType.Credential.Value) - if err != nil { - return "", errors.WrapErrorAction(logutils.ActionCast, typeEmailCreds, nil, err) - } - - //get request credential - type signInPasswordCred struct { - Password string `json:"password"` - } - var sPasswordParams signInPasswordCred - err = json.Unmarshal([]byte(creds), &sPasswordParams) - if err != nil { - return "", errors.WrapErrorAction(logutils.ActionUnmarshal, "sign in password creds", nil, err) - } - requestPassword := sPasswordParams.Password - - //compare stored and requets ones - err = bcrypt.CompareHashAndPassword([]byte(storedCreds.Password), []byte(requestPassword)) - if err != nil { - return "", errors.WrapErrorAction(logutils.ActionValidate, model.TypeCredential, nil, err).SetStatus(utils.ErrorStatusInvalid) - } - - return "", nil -} - -func (a *emailAuthImpl) buildCredentials(authType model.AuthType, appName string, email string, password string, credID string) (map[string]interface{}, error) { - //password hash - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionGenerate, "password hash", nil, err) - } - - //verification code - code, err := utils.GenerateRandomString(64) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionGenerate, "verification code", nil, err) - } - - verifyEmail := a.getVerifyEmail(authType) - verifyExpiryTime := a.getVerifyExpiry(authType) - - var emailCredValue emailCreds - if verifyEmail { - emailCredValue = emailCreds{Email: email, Password: string(hashedPassword), VerificationCode: code, VerificationExpiry: time.Now().Add(time.Hour * time.Duration(verifyExpiryTime))} - } else { - emailCredValue = emailCreds{Email: email, Password: string(hashedPassword)} - } - - emailCredValueMap, err := emailCredsToMap(&emailCredValue) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionCast, "map from email creds", nil, err) - } - - if verifyEmail { - //send verification code - if err = a.sendVerificationCode(email, appName, code, credID); err != nil { - return nil, errors.WrapErrorAction(logutils.ActionSend, "verification email", nil, err) - } - } - - return emailCredValueMap, nil -} - -func (a *emailAuthImpl) getVerifyEmail(authType model.AuthType) bool { - verifyEmail := true - verifyEmailParam, ok := authType.Params["verify_email"].(bool) - if ok { - verifyEmail = verifyEmailParam - } - return verifyEmail -} - -// Time in seconds to wait before sending another verification email -func (a *emailAuthImpl) getVerifyWaitTime(authType model.AuthType) int { - //Default is 30 seconds - verifyWaitTime := 30 - verifyWaitTimeParam, ok := authType.Params["verify_wait_time"].(int) - if ok { - verifyWaitTime = verifyWaitTimeParam - } - return verifyWaitTime -} - -// Time in hours before verification code expires -func (a *emailAuthImpl) getVerifyExpiry(authType model.AuthType) int { - //Default is 24 hours - verifyExpiry := 24 - verifyExpiryParam, ok := authType.Params["verify_expiry"].(int) - if ok { - verifyExpiry = verifyExpiryParam - } - return verifyExpiry -} - -func (a *emailAuthImpl) sendVerificationCode(email string, appName string, verificationCode string, credentialID string) error { - params := url.Values{} - params.Add("id", credentialID) - params.Add("code", verificationCode) - verificationLink := a.auth.host + fmt.Sprintf("/ui/credential/verify?%s", params.Encode()) - subject := "Verify your email address" - if appName != "" { - subject += " for " + appName - } - body := "Please click the link below to verify your email address:
" + verificationLink + "

If you did not request this verification link, please ignore this message." - return a.auth.emailer.Send(email, subject, body, nil) -} - -func (a *emailAuthImpl) sendPasswordResetEmail(credentialID string, resetCode string, email string, appName string) error { - params := url.Values{} - params.Add("id", credentialID) - params.Add("code", resetCode) - passwordResetLink := a.auth.host + fmt.Sprintf("/ui/credential/reset?%s", params.Encode()) - subject := "Reset your password" - if appName != "" { - subject += " for " + appName - } - body := "Please click the link below to reset your password:
" + passwordResetLink + "

If you did not request a password reset, please ignore this message." - return a.auth.emailer.Send(email, subject, body, nil) -} - -func (a *emailAuthImpl) verifyCredential(credential *model.Credential, verification string, l *logs.Log) (map[string]interface{}, error) { - credBytes, err := json.Marshal(credential.Value) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, typeEmailCreds, nil, err) - } - - var creds *emailCreds - err = json.Unmarshal(credBytes, &creds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeEmailCreds, nil, err) - } - err = a.compareCode(creds.VerificationCode, verification, creds.VerificationExpiry, l) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthCred, &logutils.FieldArgs{"verification_code": verification}, err) - } - - //Update verification data - creds.VerificationCode = "" - creds.VerificationExpiry = time.Time{} - credsMap, err := emailCredsToMap(creds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionCast, typeEmailCreds, nil, err) - } - - return credsMap, nil -} - -func (a *emailAuthImpl) sendVerifyCredential(credential *model.Credential, appName string, l *logs.Log) error { - //Check if verify email is disabled for the given authType - authType := credential.AuthType - verifyEmail := a.getVerifyEmail(authType) - if !verifyEmail { - return errors.ErrorAction(logutils.ActionSend, logutils.TypeString, logutils.StringArgs("verify email is disabled for authType")) - } - verifyWaitTime := a.getVerifyWaitTime(authType) - verifyExpiryTime := a.getVerifyExpiry(authType) - - //Parse credential value to emailCreds - emailCreds, err := mapToEmailCreds(credential.Value) - if err != nil { - return errors.WrapErrorAction(logutils.ActionCast, typeEmailCreds, nil, err) - } - //Check if previous verification email was sent less than 30 seconds ago - now := time.Now() - prevTime := emailCreds.VerificationExpiry.Add(time.Duration(-verifyExpiryTime) * time.Hour) - if now.Sub(prevTime) < time.Duration(verifyWaitTime)*time.Second { - return errors.ErrorAction(logutils.ActionSend, "verify code", logutils.StringArgs("resend requested too soon")) - } - //verification code - code, err := utils.GenerateRandomString(64) - if err != nil { - return errors.WrapErrorAction(logutils.ActionGenerate, "verification code", nil, err) - } - - //send verification email - if err = a.sendVerificationCode(emailCreds.Email, appName, code, credential.ID); err != nil { - return errors.WrapErrorAction(logutils.ActionSend, "verification email", nil, err) - } - - //Update verification data in credential value - emailCreds.VerificationCode = code - emailCreds.VerificationExpiry = time.Now().Add(time.Hour * time.Duration(verifyExpiryTime)) - credsMap, err := emailCredsToMap(emailCreds) - if err != nil { - return errors.WrapErrorAction(logutils.ActionCast, "map from email creds", nil, err) - } - - credential.Value = credsMap - if err = a.auth.storage.UpdateCredential(nil, credential); err != nil { - return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeCredential, nil, err) - } - - return nil -} - -func (a *emailAuthImpl) restartCredentialVerification(credential *model.Credential, appName string, l *logs.Log) error { - storedCreds, err := mapToEmailCreds(credential.Value) - if err != nil { - return errors.WrapErrorAction(logutils.ActionCast, typeEmailCreds, nil, err) - } - //Generate new verification code - newCode, err := utils.GenerateRandomString(64) - if err != nil { - return errors.WrapErrorAction(logutils.ActionGenerate, "verification code", nil, err) - - } - //send new verification code for future - if err = a.sendVerificationCode(storedCreds.Email, appName, newCode, credential.ID); err != nil { - return errors.WrapErrorAction(logutils.ActionSend, "verification email", nil, err) - } - //update new verification data in credential value - storedCreds.VerificationCode = newCode - storedCreds.VerificationExpiry = time.Now().Add(time.Hour * 24) - emailCredValueMap, err := emailCredsToMap(storedCreds) - if err != nil { - return errors.WrapErrorAction(logutils.ActionCast, "map from email creds", nil, err) - } - - err = a.auth.storage.UpdateCredentialValue(credential.ID, emailCredValueMap) - if err != nil { - return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeCredential, nil, err) - } - return nil -} - -func (a *emailAuthImpl) compareCode(credCode string, requestCode string, expiryTime time.Time, l *logs.Log) error { - if expiryTime.Before(time.Now()) { - return errors.ErrorData("expired", "code", nil) - } - - if subtle.ConstantTimeCompare([]byte(credCode), []byte(requestCode)) == 0 { - return errors.ErrorData(logutils.StatusInvalid, "code", nil) - } - return nil -} - -func (a *emailAuthImpl) resetCredential(credential *model.Credential, resetCode *string, params string, l *logs.Log) (map[string]interface{}, error) { - //get the data from params - type Params struct { - NewPassword string `json:"new_password"` - ConfirmPassword string `json:"confirm_password"` - } - - var paramsData Params - err := json.Unmarshal([]byte(params), ¶msData) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeEmailParams, nil, err) - } - newPassword := paramsData.NewPassword - confirmPassword := paramsData.ConfirmPassword - - if len(newPassword) == 0 { - return nil, errors.ErrorData(logutils.StatusMissing, logutils.TypeString, logutils.StringArgs("new_password")) - } - if len(confirmPassword) == 0 { - return nil, errors.ErrorData(logutils.StatusMissing, logutils.TypeString, logutils.StringArgs("confirm_password")) - } - //check if the password matches with the confirm password one - if newPassword != confirmPassword { - return nil, errors.ErrorData(logutils.StatusInvalid, "mismatching password fields", nil) - } - - credBytes, err := json.Marshal(credential.Value) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, typeEmailCreds, nil, err) - } - - var creds *emailCreds - err = json.Unmarshal(credBytes, &creds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeEmailCreds, nil, err) - } - //reset password from link - if resetCode != nil { - if creds.ResetExpiry.Before(time.Now()) { - return nil, errors.ErrorData("expired", "reset expiration time", nil) - } - err = bcrypt.CompareHashAndPassword([]byte(creds.ResetCode), []byte(*resetCode)) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeAuthCred, &logutils.FieldArgs{"reset_code": *resetCode}, err) - } - - //Update verification data - creds.ResetCode = "" - creds.ResetExpiry = time.Time{} - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionGenerate, "password hash", nil, err) - } - - //Update verification data - creds.Password = string(hashedPassword) - credsMap, err := emailCredsToMap(creds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionCast, "map from email creds", nil, err) - } - - return credsMap, nil -} - -func (a *emailAuthImpl) forgotCredential(credential *model.Credential, identifier string, appName string, l *logs.Log) (map[string]interface{}, error) { - emailCreds, err := mapToEmailCreds(credential.Value) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionCast, typeEmailCreds, nil, err) - } - resetCode, err := utils.GenerateRandomString(64) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionGenerate, "reset code", nil, err) - - } - hashedResetCode, err := bcrypt.GenerateFromPassword([]byte(resetCode), bcrypt.DefaultCost) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionGenerate, "reset code hash", nil, err) - } - emailCreds.ResetCode = string(hashedResetCode) - emailCreds.ResetExpiry = time.Now().Add(time.Hour * 24) - err = a.sendPasswordResetEmail(credential.ID, resetCode, identifier, appName) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionSend, "password reset email", nil, err) - } - credsMap, err := emailCredsToMap(emailCreds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionCast, "map from email creds", nil, err) - } - return credsMap, nil -} - -func (a *emailAuthImpl) getUserIdentifier(creds string) (string, error) { - var requestCreds emailCreds - err := json.Unmarshal([]byte(creds), &requestCreds) - if err != nil { - return "", errors.WrapErrorAction(logutils.ActionUnmarshal, typeEmailCreds, nil, err) - } - - return requestCreds.Email, nil -} - -func emailCredsToMap(creds *emailCreds) (map[string]interface{}, error) { - credBytes, err := json.Marshal(creds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, typeEmailCreds, nil, err) - } - var credsMap map[string]interface{} - err = json.Unmarshal(credBytes, &credsMap) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, "map from email creds", nil, err) - } - return credsMap, nil -} - -func mapToEmailCreds(credsMap map[string]interface{}) (*emailCreds, error) { - credBytes, err := json.Marshal(credsMap) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, typeEmailCreds, nil, err) - } - var creds emailCreds - err = json.Unmarshal(credBytes, &creds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeEmailCreds, nil, err) - } - return &creds, nil -} - -// initEmailAuth initializes and registers a new email auth instance -func initEmailAuth(auth *Auth) (*emailAuthImpl, error) { - email := &emailAuthImpl{auth: auth, authType: AuthTypeEmail} - - err := auth.registerAuthType(email.authType, email) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionRegister, model.TypeAuthType, nil, err) - } - - return email, nil -} diff --git a/core/auth/auth_type_firebase.go b/core/auth/auth_type_firebase.go index 524df0ab4..1fd6fd9f3 100644 --- a/core/auth/auth_type_firebase.go +++ b/core/auth/auth_type_firebase.go @@ -18,7 +18,6 @@ import ( "core-building-block/core/model" "github.com/rokwire/logging-library-go/v2/errors" - "github.com/rokwire/logging-library-go/v2/logs" "github.com/rokwire/logging-library-go/v2/logutils" ) @@ -33,44 +32,36 @@ type firebaseAuthImpl struct { authType string } -func (a *firebaseAuthImpl) signUp(authType model.AuthType, appOrg model.ApplicationOrganization, creds string, params string, newCredentialID string, l *logs.Log) (string, map[string]interface{}, error) { - return "", nil, nil +func (a *firebaseAuthImpl) signUp(identifierImpl identifierType, accountID *string, appOrg model.ApplicationOrganization, creds string, params string) (string, *model.AccountIdentifier, *model.Credential, error) { + return "", nil, nil, nil } -func (a *firebaseAuthImpl) signUpAdmin(authType model.AuthType, appOrg model.ApplicationOrganization, identifier string, password string, newCredentialID string) (map[string]interface{}, map[string]interface{}, error) { - return nil, nil, nil +func (a *firebaseAuthImpl) signUpAdmin(identifierImpl identifierType, appOrg model.ApplicationOrganization, creds string) (map[string]interface{}, *model.AccountIdentifier, *model.Credential, error) { + return nil, nil, nil, nil } -func (a *firebaseAuthImpl) getUserIdentifier(creds string) (string, error) { - return "", nil -} - -func (a *firebaseAuthImpl) verifyCredential(credential *model.Credential, verification string, l *logs.Log) (map[string]interface{}, error) { - return nil, errors.New(logutils.Unimplemented) -} - -func (a *firebaseAuthImpl) sendVerifyCredential(credential *model.Credential, appName string, l *logs.Log) error { - return nil +func (a *firebaseAuthImpl) forgotCredential(identifierImpl identifierType, credential *model.Credential, appOrg model.ApplicationOrganization) (map[string]interface{}, error) { + return nil, nil } -func (a *firebaseAuthImpl) restartCredentialVerification(credential *model.Credential, appName string, l *logs.Log) error { - return nil +func (a *firebaseAuthImpl) resetCredential(credential *model.Credential, resetCode *string, params string) (map[string]interface{}, error) { + return nil, nil } -func (a *firebaseAuthImpl) isCredentialVerified(credential *model.Credential, l *logs.Log) (*bool, *bool, error) { - return nil, nil, nil +func (a *firebaseAuthImpl) checkCredentials(identifierImpl identifierType, accountID *string, aats []model.AccountAuthType, creds string, params string, appOrg model.ApplicationOrganization) (string, string, error) { + return "", "", nil } -func (a *firebaseAuthImpl) checkCredentials(accountAuthType model.AccountAuthType, creds string, l *logs.Log) (string, error) { - return "", nil +func (a *firebaseAuthImpl) withParams(params map[string]interface{}) (authType, error) { + return a, nil } -func (a *firebaseAuthImpl) resetCredential(credential *model.Credential, resetCode *string, params string, l *logs.Log) (map[string]interface{}, error) { - return nil, nil +func (a *firebaseAuthImpl) requireIdentifierVerificationForSignIn() bool { + return false } -func (a *firebaseAuthImpl) forgotCredential(credential *model.Credential, identifier string, appName string, l *logs.Log) (map[string]interface{}, error) { - return nil, nil +func (a *firebaseAuthImpl) allowMultiple() bool { + return false } // initFirebaseAuth initializes and registers a new Firebase auth instance diff --git a/core/auth/auth_type_oidc.go b/core/auth/auth_type_oidc.go index d8109b7b1..cd558ec91 100644 --- a/core/auth/auth_type_oidc.go +++ b/core/auth/auth_type_oidc.go @@ -21,12 +21,14 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "net/url" "strconv" "strings" + "golang.org/x/oauth2" "gopkg.in/go-playground/validator.v9" "github.com/coreos/go-oidc" @@ -51,6 +53,7 @@ const ( type oidcAuthImpl struct { auth *Auth authType string + client *http.Client } type oidcAuthConfig struct { @@ -75,10 +78,10 @@ type oidcLoginParams struct { } type oidcToken struct { - IDToken string `json:"id_token" validate:"required"` + IDToken string `json:"id_token"` AccessToken string `json:"access_token" validate:"required"` RefreshToken string `json:"refresh_token"` - TokenType string `json:"token_type" validate:"required"` + TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` } @@ -87,6 +90,16 @@ type oidcRefreshParams struct { RedirectURI string `json:"redirect_uri" bson:"redirect_uri" validate:"required"` } +type transport struct { + t http.RoundTripper + userAgent string +} + +func (adt *transport) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Add("User-Agent", adt.userAgent) + return adt.t.RoundTrip(req) +} + func oidcRefreshParamsFromMap(val map[string]interface{}) (*oidcRefreshParams, error) { oidcToken, ok := val["oidc_token"].(map[string]interface{}) if !ok { @@ -117,7 +130,7 @@ func (a *oidcAuthImpl) externalLogin(authType model.AuthType, appType model.Appl return nil, nil, "", errors.WrapErrorAction(logutils.ActionValidate, typeOidcLoginParams, nil, err) } - oidcConfig, err := a.getOidcAuthConfig(authType, appType) + oidcConfig, err := a.getOidcAuthConfig(authType, appType.ID) if err != nil { return nil, nil, "", errors.WrapErrorAction(logutils.ActionGet, typeOidcAuthConfig, nil, err) } @@ -142,7 +155,7 @@ func (a *oidcAuthImpl) refresh(params map[string]interface{}, authType model.Aut return nil, nil, "", errors.WrapErrorAction(logutils.ActionParse, typeAuthRefreshParams, nil, err) } - oidcConfig, err := a.getOidcAuthConfig(authType, appType) + oidcConfig, err := a.getOidcAuthConfig(authType, appType.ID) if err != nil { return nil, nil, "", errors.WrapErrorAction(logutils.ActionGet, typeOidcAuthConfig, nil, err) } @@ -151,7 +164,7 @@ func (a *oidcAuthImpl) refresh(params map[string]interface{}, authType model.Aut } func (a *oidcAuthImpl) getLoginURL(authType model.AuthType, appType model.ApplicationType, redirectURI string, l *logs.Log) (string, map[string]interface{}, error) { - oidcConfig, err := a.getOidcAuthConfig(authType, appType) + oidcConfig, err := a.getOidcAuthConfig(authType, appType.ID) if err != nil { return "", nil, errors.WrapErrorAction(logutils.ActionGet, typeOidcAuthConfig, nil, err) } @@ -202,7 +215,7 @@ func (a *oidcAuthImpl) getLoginURL(authType model.AuthType, appType model.Applic func (a *oidcAuthImpl) checkToken(idToken string, authType model.AuthType, appType model.ApplicationType, oidcConfig *oidcAuthConfig, l *logs.Log) (string, error) { var err error if oidcConfig == nil { - oidcConfig, err = a.getOidcAuthConfig(authType, appType) + oidcConfig, err = a.getOidcAuthConfig(authType, appType.ID) if err != nil { return "", errors.WrapErrorAction(logutils.ActionGet, typeOidcAuthConfig, nil, err) } @@ -211,8 +224,11 @@ func (a *oidcAuthImpl) checkToken(idToken string, authType model.AuthType, appTy oidcProvider := oidcConfig.Host oidcClientID := oidcConfig.ClientID + parent := oidc.ClientContext(context.Background(), a.client) + ctx := context.WithValue(parent, oauth2.HTTPClient, a.client) + // Validate the token - provider, err := oidc.NewProvider(context.Background(), oidcProvider) + provider, err := oidc.NewProvider(ctx, oidcProvider) if err != nil { return "", errors.WrapErrorAction(logutils.ActionInitialize, "oidc provider", nil, err) } @@ -272,9 +288,13 @@ func (a *oidcAuthImpl) loadOidcTokensAndInfo(bodyData map[string]string, oidcCon return nil, nil, "", errors.WrapErrorAction(logutils.ActionGet, typeOidcToken, nil, err) } - sub, err := a.checkToken(token.IDToken, authType, appType, oidcConfig, l) - if err != nil { - return nil, nil, "", errors.WrapErrorAction(logutils.ActionValidate, typeOidcToken, nil, err) + sub := "" + if token.IDToken != "" { + // we should not check the ID token if it is not provided + sub, err = a.checkToken(token.IDToken, authType, appType, oidcConfig, l) + if err != nil { + return nil, nil, "", errors.WrapErrorAction(logutils.ActionValidate, typeOidcToken, nil, err) + } } userInfoURL := oidcConfig.Host + "/idp/profile/oidc/userinfo" @@ -292,9 +312,12 @@ func (a *oidcAuthImpl) loadOidcTokensAndInfo(bodyData map[string]string, oidcCon return nil, nil, "", errors.WrapErrorAction(logutils.ActionUnmarshal, "user info", nil, err) } - userClaimsSub, _ := userClaims["sub"].(string) - if userClaimsSub != sub { - return nil, nil, "", errors.ErrorData("mismatched", "sub fields", &logutils.FieldArgs{"user info": userClaimsSub, "id token": sub}) + if sub != "" { + // we should only perform this check if we get the ID token + userClaimsSub, _ := userClaims["sub"].(string) + if userClaimsSub != sub { + return nil, nil, "", errors.ErrorData("mismatched", "sub fields", &logutils.FieldArgs{"user info": userClaimsSub, "id token": sub}) + } } identityProviderID, _ := authType.Params["identity_provider"].(string) @@ -347,8 +370,9 @@ func (a *oidcAuthImpl) loadOidcTokensAndInfo(bodyData map[string]string, oidcCon externalIDs[k] = externalID } - externalUser := model.ExternalSystemUser{Identifier: identifier, ExternalIDs: externalIDs, FirstName: firstName, - MiddleName: middleName, LastName: lastName, Email: email, Roles: roles, Groups: groups, SystemSpecific: systemSpecific} + externalUser := model.ExternalSystemUser{Identifier: identifier, ExternalIDs: externalIDs, SensitiveExternalIDs: identityProviderSetting.SensitiveExternalIDs, + IsEmailVerified: identityProviderSetting.IsEmailVerified, FirstName: firstName, MiddleName: middleName, LastName: lastName, Email: email, Roles: roles, + Groups: groups, SystemSpecific: systemSpecific} oidcParams := map[string]interface{}{} oidcParams["id_token"] = token.IDToken @@ -394,15 +418,13 @@ func (a *oidcAuthImpl) loadOidcTokenWithParams(params map[string]string, oidcCon for k, v := range headers { req.Header.Set(k, v) } - - client := &http.Client{} - resp, err := client.Do(req) + resp, err := a.client.Do(req) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionSend, logutils.TypeRequest, nil, err) } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionRead, logutils.TypeRequestBody, nil, err) } @@ -441,8 +463,7 @@ func (a *oidcAuthImpl) loadOidcUserInfo(token *oidcToken, url string) ([]byte, e } req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.TokenType, token.AccessToken)) - client := &http.Client{} - resp, err := client.Do(req) + resp, err := a.client.Do(req) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionSend, logutils.TypeRequest, nil, err) } @@ -462,28 +483,21 @@ func (a *oidcAuthImpl) loadOidcUserInfo(token *oidcToken, url string) ([]byte, e return body, nil } -func (a *oidcAuthImpl) getOidcAuthConfig(authType model.AuthType, appType model.ApplicationType) (*oidcAuthConfig, error) { - errFields := &logutils.FieldArgs{"auth_type_id": authType.ID, "app_type_id": appType.ID} +func (a *oidcAuthImpl) getOidcAuthConfig(authType model.AuthType, appTypeID string) (*oidcAuthConfig, error) { + errFields := &logutils.FieldArgs{"auth_type_id": authType.ID, "app_type_id": appTypeID} identityProviderID, ok := authType.Params["identity_provider"].(string) if !ok { return nil, errors.ErrorData(logutils.StatusInvalid, "identity provider", errFields) } - appTypeID := appType.ID authConfig, err := a.auth.getCachedIdentityProviderConfig(identityProviderID, appTypeID) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeIdentityProviderConfig, errFields, err) } - configBytes, err := json.Marshal(authConfig.Config) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, model.TypeIdentityProviderConfig, errFields, err) - } - - var oidcConfig oidcAuthConfig - err = json.Unmarshal(configBytes, &oidcConfig) + oidcConfig, err := utils.JSONConvert[oidcAuthConfig, map[string]interface{}](authConfig.Config) if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, model.TypeIdentityProviderConfig, errFields, err) + return nil, errors.WrapErrorAction(logutils.ActionParse, model.TypeIdentityProviderConfig, errFields, err) } validate := validator.New() @@ -492,30 +506,29 @@ func (a *oidcAuthImpl) getOidcAuthConfig(authType model.AuthType, appType model. return nil, errors.WrapErrorAction(logutils.ActionValidate, model.TypeIdentityProviderConfig, errFields, err) } - return &oidcConfig, nil + return oidcConfig, nil } // --- Helper functions --- // generatePkceChallenge generates and returns a PKCE code challenge and verifier func generatePkceChallenge() (string, string, error) { - codeVerifier, err := utils.GenerateRandomString(50) - if err != nil { - return "", "", errors.WrapErrorAction(logutils.ActionGenerate, "code verifier", nil, err) - } - + codeVerifier := utils.GenerateRandomString(50) codeChallengeBytes, err := authutils.HashSha256([]byte(codeVerifier)) if err != nil { return "", "", errors.WrapErrorAction(logutils.ActionCompute, "code verifier hash", nil, err) } - codeChallenge := base64.URLEncoding.EncodeToString(codeChallengeBytes) + codeChallenge := base64.RawURLEncoding.EncodeToString(codeChallengeBytes) return codeChallenge, codeVerifier, nil } // initOidcAuth initializes and registers a new OIDC auth instance func initOidcAuth(auth *Auth) (*oidcAuthImpl, error) { - oidc := &oidcAuthImpl{auth: auth, authType: AuthTypeOidc} + client := &http.Client{ + Transport: &transport{t: http.DefaultTransport, userAgent: "RokwireCore/" + auth.version}, + } + oidc := &oidcAuthImpl{auth: auth, authType: AuthTypeOidc, client: client} err := auth.registerExternalAuthType(oidc.authType, oidc) if err != nil { diff --git a/core/auth/auth_type_password.go b/core/auth/auth_type_password.go new file mode 100644 index 000000000..e48eacd20 --- /dev/null +++ b/core/auth/auth_type_password.go @@ -0,0 +1,344 @@ +// Copyright 2023 Board of Trustees of the University of Illinois. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "core-building-block/core/model" + "core-building-block/utils" + "encoding/json" + "time" + + "github.com/google/uuid" + "github.com/rokwire/logging-library-go/v2/errors" + "github.com/rokwire/logging-library-go/v2/logutils" + "golang.org/x/crypto/bcrypt" + "gopkg.in/go-playground/validator.v9" +) + +const ( + //AuthTypePassword password auth type + AuthTypePassword string = "password" + + credentialKeyPassword string = "password" + typePasswordResetCode string = "password reset code" + + typePasswordCreds logutils.MessageDataType = "password creds" + typePasswordParams logutils.MessageDataType = "password params" + typePasswordResetParams logutils.MessageDataType = "password reset params" +) + +// passwordCreds represents the creds struct for password authentication +type passwordCreds struct { + Password string `json:"password" validate:"required"` + + ResetCode *string `json:"reset_code,omitempty"` + ResetExpiry *time.Time `json:"reset_expiry,omitempty"` +} + +func (c *passwordCreds) toMap() (map[string]interface{}, error) { + if c == nil { + return nil, errors.ErrorData(logutils.StatusMissing, typePasswordCreds, nil) + } + + credsMap, err := utils.JSONConvert[map[string]interface{}, passwordCreds](*c) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionParse, typePasswordCreds, nil, err) + } + if credsMap == nil { + return nil, errors.ErrorData(logutils.StatusInvalid, "password creds map", nil) + } + return *credsMap, nil +} + +type passwordParams struct { + ConfirmPassword string `json:"confirm_password" validate:"required"` +} + +type passwordResetParams struct { + NewPassword string `json:"new_password" validate:"required"` + passwordParams +} + +// Password implementation of authType +type passwordAuthImpl struct { + auth *Auth + authType string +} + +func (a *passwordAuthImpl) signUp(identifierImpl identifierType, accountID *string, appOrg model.ApplicationOrganization, creds string, params string) (string, *model.AccountIdentifier, *model.Credential, error) { + credentials, err := a.parseCreds(creds, true) + if err != nil { + return "", nil, nil, errors.WrapErrorAction(logutils.ActionParse, typePasswordCreds, nil, err) + } + + parameters, err := a.parseParams(params) + if err != nil { + return "", nil, nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typePasswordParams, nil, err) + } + + if credentials.Password != parameters.ConfirmPassword { + return "", nil, nil, errors.ErrorData(logutils.StatusInvalid, "mismatching credentials", nil) + } + + message := "" + var accountIdentifier *model.AccountIdentifier + if accountID == nil { + // we are not linking a password credential, so use the accountID generated for the identifier + message, accountIdentifier, err = identifierImpl.buildIdentifier(nil, appOrg.Application.Name) + if err != nil { + return "", nil, nil, errors.WrapErrorAction("building", "identifier", logutils.StringArgs(identifierImpl.getCode()), err) + } + } + + credential, err := a.buildCredential(credentials.Password) + if err != nil { + return "", nil, nil, errors.WrapErrorAction("building", "password credentials", nil, err) + } + + return message, accountIdentifier, credential, nil +} + +func (a *passwordAuthImpl) signUpAdmin(identifierImpl identifierType, appOrg model.ApplicationOrganization, creds string) (map[string]interface{}, *model.AccountIdentifier, *model.Credential, error) { + credentials, err := a.parseCreds(creds, false) + if err != nil { + return nil, nil, nil, errors.WrapErrorAction(logutils.ActionParse, typePasswordCreds, nil, err) + } + + if credentials.Password == "" { + credentials.Password = utils.GenerateRandomPassword(12) + } + + _, accountIdentifier, err := identifierImpl.buildIdentifier(nil, appOrg.Application.Name) + if err != nil { + return nil, nil, nil, errors.WrapErrorAction("building", "identifier", logutils.StringArgs(identifierImpl.getCode()), err) + } + + credential, err := a.buildCredential(credentials.Password) + if err != nil { + return nil, nil, nil, errors.WrapErrorAction("building", "password credentials", nil, err) + } + + params := map[string]interface{}{"password": credentials.Password} + return params, accountIdentifier, credential, nil +} + +func (a *passwordAuthImpl) forgotCredential(identifierImpl identifierType, credential *model.Credential, appOrg model.ApplicationOrganization) (map[string]interface{}, error) { + identifierChannel, _ := identifierImpl.(authCommunicationChannel) + if identifierChannel == nil { + return nil, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, logutils.StringArgs(identifierImpl.getCode())) + } + if credential == nil { + return nil, errors.ErrorData(logutils.StatusMissing, model.TypeCredential, nil) + } + + passwordCreds, err := a.mapToCreds(credential.Value) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionCast, "map to password creds", nil, err) + } + + //TODO: turn length of reset code into a setting + resetCode := utils.GenerateRandomString(64) + hashedResetCode, err := bcrypt.GenerateFromPassword([]byte(resetCode), bcrypt.DefaultCost) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionGenerate, "reset code hash", nil, err) + } + + hashedResetCodeStr := string(hashedResetCode) + resetExpiry := time.Now().UTC().Add(time.Hour * 24) + passwordCreds.ResetCode = &hashedResetCodeStr + passwordCreds.ResetExpiry = &resetExpiry + + _, err = identifierChannel.sendCode(appOrg.Application.Name, resetCode, typePasswordResetCode, credential.ID) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionSend, "password reset code", nil, err) + } + + credsMap, err := passwordCreds.toMap() + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionCast, "map from creds", nil, err) + } + return credsMap, nil +} + +func (a *passwordAuthImpl) resetCredential(credential *model.Credential, resetCode *string, params string) (map[string]interface{}, error) { + if credential == nil { + return nil, errors.ErrorData(logutils.StatusMissing, model.TypeCredential, nil) + } + passwordCreds, err := a.mapToCreds(credential.Value) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionCast, "map to password creds", nil, err) + } + + var resetData passwordResetParams + err = json.Unmarshal([]byte(params), &resetData) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typePasswordResetParams, nil, err) + } + + if len(resetData.NewPassword) == 0 { + return nil, errors.ErrorData(logutils.StatusMissing, logutils.TypeString, logutils.StringArgs("new password")) + } + if len(resetData.ConfirmPassword) == 0 { + return nil, errors.ErrorData(logutils.StatusMissing, logutils.TypeString, logutils.StringArgs("confirm password")) + } + //check if the password matches with the confirm password one + if resetData.NewPassword != resetData.ConfirmPassword { + return nil, errors.ErrorData(logutils.StatusInvalid, "mismatching password reset fields", nil) + } + + //reset password from link + if resetCode != nil { + if passwordCreds.ResetExpiry == nil || passwordCreds.ResetExpiry.Before(time.Now()) { + return nil, errors.ErrorData("expired", "reset expiration time", nil) + } + if passwordCreds.ResetCode == nil { + return nil, errors.ErrorData(logutils.StatusMissing, "stored reset code", nil) + } + err = bcrypt.CompareHashAndPassword([]byte(*passwordCreds.ResetCode), []byte(*resetCode)) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionValidate, "password reset code", nil, err) + } + + //Update reset data + passwordCreds.ResetCode = nil + passwordCreds.ResetExpiry = nil + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(resetData.NewPassword), bcrypt.DefaultCost) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionGenerate, "password hash", nil, err) + } + + //Update password + passwordCreds.Password = string(hashedPassword) + credsMap, err := passwordCreds.toMap() + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionCast, "map from password creds", nil, err) + } + + return credsMap, nil +} + +func (a *passwordAuthImpl) checkCredentials(identifierImpl identifierType, accountID *string, aats []model.AccountAuthType, creds string, params string, appOrg model.ApplicationOrganization) (string, string, error) { + if len(aats) != 1 { + return "", "", errors.ErrorData(logutils.StatusInvalid, "account auth type list", &logutils.FieldArgs{"count": len(aats)}) + } + if aats[0].Credential == nil { + return "", "", errors.ErrorData(logutils.StatusInvalid, model.TypeAccountAuthType, &logutils.FieldArgs{"id": aats[0].ID, "credential": nil}) + } + + storedCreds, err := a.mapToCreds(aats[0].Credential.Value) + if err != nil { + return "", "", errors.WrapErrorAction(logutils.ActionCast, "map to password creds", nil, err) + } + + incomingCreds, err := a.parseCreds(creds, true) + if err != nil { + return "", "", errors.WrapErrorAction(logutils.ActionParse, typePasswordCreds, nil, err) + } + + //compare stored and request passwords + err = bcrypt.CompareHashAndPassword([]byte(storedCreds.Password), []byte(incomingCreds.Password)) + if err != nil { + return "", "", errors.WrapErrorAction(logutils.ActionValidate, model.TypeCredential, nil, err).SetStatus(utils.ErrorStatusInvalid) + } + + return "", aats[0].Credential.ID, nil +} + +func (a *passwordAuthImpl) withParams(params map[string]interface{}) (authType, error) { + return a, nil +} + +func (a *passwordAuthImpl) requireIdentifierVerificationForSignIn() bool { + return true +} + +func (a *passwordAuthImpl) allowMultiple() bool { + return false +} + +// Helpers + +func (a *passwordAuthImpl) buildCredential(password string) (*model.Credential, error) { + //password hash + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionGenerate, "password hash", nil, err) + } + + credValue := &passwordCreds{Password: string(hashedPassword)} + credValueMap, err := credValue.toMap() + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionCast, "map from creds", nil, err) + } + + credential := &model.Credential{ID: uuid.NewString(), Value: credValueMap, AuthType: model.AuthType{Code: a.authType}, DateCreated: time.Now().UTC()} + return credential, nil +} + +func (a *passwordAuthImpl) parseCreds(creds string, validate bool) (*passwordCreds, error) { + var credential passwordCreds + err := json.Unmarshal([]byte(creds), &credential) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typePasswordCreds, nil, err) + } + + if validate { + err = validator.New().Struct(credential) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionValidate, typePasswordCreds, nil, err) + } + } + return &credential, nil +} + +func (a *passwordAuthImpl) parseParams(params string) (*passwordParams, error) { + var parameters passwordParams + err := json.Unmarshal([]byte(params), ¶meters) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typePasswordParams, nil, err) + } + err = validator.New().Struct(parameters) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionValidate, typePasswordParams, nil, err) + } + return ¶meters, nil +} + +func (a *passwordAuthImpl) mapToCreds(credsMap map[string]interface{}) (*passwordCreds, error) { + creds, err := utils.JSONConvert[passwordCreds, map[string]interface{}](credsMap) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionParse, typePasswordCreds, nil, err) + } + + err = validator.New().Struct(creds) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionValidate, typePasswordCreds, nil, err) + } + return creds, nil +} + +// initPasswordAuth initializes and registers a new password auth instance +func initPasswordAuth(auth *Auth) (*passwordAuthImpl, error) { + password := &passwordAuthImpl{auth: auth, authType: AuthTypePassword} + + err := auth.registerAuthType(password.authType, password) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionRegister, model.TypeAuthType, nil, err) + } + + return password, nil +} diff --git a/core/auth/auth_type_phone.go b/core/auth/auth_type_phone.go deleted file mode 100644 index d663b86d6..000000000 --- a/core/auth/auth_type_phone.go +++ /dev/null @@ -1,319 +0,0 @@ -// Copyright 2022 Board of Trustees of the University of Illinois. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package auth - -import ( - "context" - "core-building-block/core/model" - "core-building-block/utils" - "encoding/base64" - "encoding/json" - "io/ioutil" - "net/http" - "net/url" - "regexp" - "strings" - "time" - - "github.com/rokwire/logging-library-go/v2/errors" - "github.com/rokwire/logging-library-go/v2/logs" - "github.com/rokwire/logging-library-go/v2/logutils" - "gopkg.in/go-playground/validator.v9" -) - -const ( - //AuthTypeTwilioPhone phone auth type - AuthTypeTwilioPhone string = "twilio_phone" - - servicesPathPart = "https://verify.twilio.com/v2/Services" - verificationsPathPart = "Verifications" - verificationCheckPart = "VerificationCheck" - typeVerifyServiceID logutils.MessageDataType = "phone verification service id" - typeVerifyServiceToken logutils.MessageDataType = "phone verification service token" - typeVerificationResponse logutils.MessageDataType = "phone verification response" - typeVerificationStatus logutils.MessageDataType = "phone verification staus" - typeVerificationSID logutils.MessageDataType = "phone verification sid" - typePhoneCreds logutils.MessageDataType = "phone creds" - typePhoneNumber logutils.MessageDataType = "E.164 phone number" -) - -// Phone implementation of authType -type twilioPhoneAuthImpl struct { - auth *Auth - authType string - twilioAccountSID string - twilioToken string - twilioServiceSID string -} - -type twilioPhoneCreds struct { - Phone string `json:"phone" validate:"required"` - Code string `json:"code"` - // TODO: Password? -} - -type verifyPhoneResponse struct { - Status string `json:"status"` - Payee interface{} `json:"payee"` - DateUpdated time.Time `json:"date_updated"` - AccountSid string `json:"account_sid"` - To string `json:"to"` - Amount interface{} `json:"amount"` - Valid bool `json:"valid"` - URL string `json:"url"` - Sid string `json:"sid"` - DateCreated time.Time `json:"date_created"` - ServiceSid string `json:"service_sid"` - Channel string `json:"channel"` -} - -type checkStatusResponse struct { - Sid string `json:"sid"` - ServiceSid string `json:"service_sid"` - AccountSid string `json:"account_sid"` - To string `json:"to" validate:"required"` - Channel string `json:"channel"` - Status string `json:"status"` - Amount interface{} `json:"amount"` - Payee interface{} `json:"payee"` - DateCreated time.Time `json:"date_created"` - DateUpdated time.Time `json:"date_updated"` -} - -func (a *twilioPhoneAuthImpl) checkRequestCreds(creds string) (*twilioPhoneCreds, error) { - var requestCreds twilioPhoneCreds - err := json.Unmarshal([]byte(creds), &requestCreds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typePhoneCreds, nil, err) - } - - validate := validator.New() - err = validate.Struct(requestCreds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionValidate, typePhoneCreds, nil, err) - } - - phone := requestCreds.Phone - validPhone := regexp.MustCompile(`^\+[1-9]\d{1,14}$`) - if !validPhone.MatchString(phone) { - return nil, errors.ErrorData(logutils.StatusInvalid, typePhoneNumber, &logutils.FieldArgs{"phone": phone}) - } - - return &requestCreds, nil -} - -func (a *twilioPhoneAuthImpl) signUp(authType model.AuthType, appOrg model.ApplicationOrganization, creds string, params string, newCredentialID string, l *logs.Log) (string, map[string]interface{}, error) { - requestCreds, err := a.checkRequestCreds(creds) - if err != nil { - return "", nil, err - } - - message, err := a.handlePhoneVerify(requestCreds.Phone, *requestCreds, l) - if err != nil { - return "", nil, err - } - - return message, nil, nil -} - -func (a *twilioPhoneAuthImpl) signUpAdmin(authType model.AuthType, appOrg model.ApplicationOrganization, identifier string, password string, newCredentialID string) (map[string]interface{}, map[string]interface{}, error) { - return nil, nil, nil -} - -func (a *twilioPhoneAuthImpl) isCredentialVerified(credential *model.Credential, l *logs.Log) (*bool, *bool, error) { - return nil, nil, nil -} - -func (a *twilioPhoneAuthImpl) checkCredentials(accountAuthType model.AccountAuthType, creds string, l *logs.Log) (string, error) { - requestCreds, err := a.checkRequestCreds(creds) - if err != nil { - return "", err - } - - // existing user - message, err := a.handlePhoneVerify(requestCreds.Phone, *requestCreds, l) - if err != nil { - return "", err - } - - return message, nil -} - -func (a *twilioPhoneAuthImpl) handlePhoneVerify(phone string, verificationCreds twilioPhoneCreds, l *logs.Log) (string, error) { - if a.twilioAccountSID == "" { - return "", errors.ErrorData(logutils.StatusMissing, typeVerifyServiceID, nil) - } - - if a.twilioToken == "" { - return "", errors.ErrorData(logutils.StatusMissing, typeVerifyServiceToken, nil) - } - - data := url.Values{} - data.Add("To", phone) - if verificationCreds.Code != "" { - // check verification - data.Add("Code", verificationCreds.Code) - return "", a.checkVerification(phone, data, l) - } - - // start verification - data.Add("Channel", "sms") - - message := "" - err := a.startVerification(phone, data, l) - if err == nil { - message = "verification code sent successfully" - } - return message, err -} - -func (a *twilioPhoneAuthImpl) startVerification(phone string, data url.Values, l *logs.Log) error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - body, err := makeRequest(ctx, "POST", servicesPathPart+"/"+a.twilioServiceSID+"/"+verificationsPathPart, data, a.twilioAccountSID, a.twilioToken) - if err != nil { - return errors.WrapErrorAction(logutils.ActionSend, logutils.TypeRequest, &logutils.FieldArgs{"verification params": data}, err) - } - - var verifyResult verifyPhoneResponse - err = json.Unmarshal(body, &verifyResult) - if err != nil { - return errors.WrapErrorAction(logutils.ActionUnmarshal, typeVerificationResponse, nil, err) - } - - if verifyResult.To != phone { - return errors.ErrorData(logutils.StatusInvalid, logutils.TypeString, &logutils.FieldArgs{"expected phone": phone, "actual phone": verifyResult.To}) - } - if verifyResult.Status != "pending" { - return errors.ErrorData(logutils.StatusInvalid, typeVerificationStatus, &logutils.FieldArgs{"expected pending, actual:": verifyResult.Status}) - } - if verifyResult.Sid == "" { - return errors.ErrorData(logutils.StatusMissing, typeVerificationSID, nil) - } - - return nil -} - -func (a *twilioPhoneAuthImpl) checkVerification(phone string, data url.Values, l *logs.Log) error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - body, err := makeRequest(ctx, "POST", servicesPathPart+"/"+a.twilioServiceSID+"/"+verificationCheckPart, data, a.twilioAccountSID, a.twilioToken) - if err != nil { - return errors.WrapErrorAction(logutils.ActionSend, logutils.TypeRequest, nil, err) - } - - var checkResponse checkStatusResponse - err = json.Unmarshal(body, &checkResponse) - if err != nil { - return errors.WrapErrorAction(logutils.ActionUnmarshal, typeVerificationResponse, nil, err) - } - - if checkResponse.To != phone { - return errors.ErrorData(logutils.StatusInvalid, logutils.TypeString, &logutils.FieldArgs{"expected phone": phone, "actual phone": checkResponse.To}) - } - if checkResponse.Status != "approved" { - return errors.ErrorData(logutils.StatusInvalid, typeVerificationStatus, &logutils.FieldArgs{"expected approved, actual:": checkResponse.Status}).SetStatus(utils.ErrorStatusInvalid) - } - - return nil -} - -func makeRequest(ctx context.Context, method string, pathPart string, data url.Values, user string, token string) ([]byte, error) { - client := &http.Client{} - rb := new(strings.Reader) - logAction := logutils.ActionSend - - if data != nil && (method == "POST" || method == "PUT") { - rb = strings.NewReader(data.Encode()) - } - if method == "GET" && data != nil { - pathPart = pathPart + "?" + data.Encode() - logAction = logutils.ActionRead - } - - req, err := http.NewRequest(method, pathPart, rb) - if err != nil { - return nil, errors.WrapErrorAction(logAction, logutils.TypeRequest, &logutils.FieldArgs{"path": pathPart}, err) - } - - if token != "" { - req.Header.Add("Authorization", "Basic "+basicAuth(user, token)) - } - req.Header.Add("Accept", "application/json") - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - resp, err := client.Do(req) - if err != nil { - return nil, errors.WrapErrorAction(logAction, logutils.TypeRequest, nil, err) - } - - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionRead, logutils.TypeRequestBody, nil, err) - } - if resp.StatusCode != 200 && resp.StatusCode != 201 { - return nil, errors.ErrorData(logutils.StatusInvalid, logutils.TypeResponse, &logutils.FieldArgs{"status_code": resp.StatusCode, "error": string(body)}) - } - return body, nil -} - -func basicAuth(username, password string) string { - auth := username + ":" + password - return base64.StdEncoding.EncodeToString([]byte(auth)) -} - -func (a *twilioPhoneAuthImpl) getUserIdentifier(creds string) (string, error) { - var requestCreds twilioPhoneCreds - err := json.Unmarshal([]byte(creds), &requestCreds) - if err != nil { - return "", errors.WrapErrorAction(logutils.ActionUnmarshal, typePhoneCreds, nil, err) - } - - return requestCreds.Phone, nil -} - -func (a *twilioPhoneAuthImpl) verifyCredential(credential *model.Credential, verification string, l *logs.Log) (map[string]interface{}, error) { - return nil, errors.New(logutils.Unimplemented) -} - -func (a *twilioPhoneAuthImpl) sendVerifyCredential(credential *model.Credential, appName string, l *logs.Log) error { - return nil -} - -func (a *twilioPhoneAuthImpl) restartCredentialVerification(credential *model.Credential, appName string, l *logs.Log) error { - return nil -} - -func (a *twilioPhoneAuthImpl) resetCredential(credential *model.Credential, resetCode *string, params string, l *logs.Log) (map[string]interface{}, error) { - return nil, nil -} - -func (a *twilioPhoneAuthImpl) forgotCredential(credential *model.Credential, identifier string, appName string, l *logs.Log) (map[string]interface{}, error) { - return nil, nil -} - -// initPhoneAuth initializes and registers a new phone auth instance -func initPhoneAuth(auth *Auth, twilioAccountSID string, twilioToken string, twilioServiceSID string) (*twilioPhoneAuthImpl, error) { - phone := &twilioPhoneAuthImpl{auth: auth, authType: AuthTypeTwilioPhone, twilioAccountSID: twilioAccountSID, twilioToken: twilioToken, twilioServiceSID: twilioServiceSID} - - err := auth.registerAuthType(phone.authType, phone) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionRegister, model.TypeAuthType, nil, err) - } - - return phone, nil -} diff --git a/core/auth/auth_type_signature.go b/core/auth/auth_type_signature.go index b19d72538..2ffe023c1 100644 --- a/core/auth/auth_type_signature.go +++ b/core/auth/auth_type_signature.go @@ -18,7 +18,6 @@ import ( "core-building-block/core/model" "github.com/rokwire/logging-library-go/v2/errors" - "github.com/rokwire/logging-library-go/v2/logs" "github.com/rokwire/logging-library-go/v2/logutils" ) @@ -33,44 +32,36 @@ type signatureAuthImpl struct { authType string } -func (a *signatureAuthImpl) signUp(authType model.AuthType, appOrg model.ApplicationOrganization, creds string, params string, newCredentialID string, l *logs.Log) (string, map[string]interface{}, error) { - return "", nil, nil +func (a *signatureAuthImpl) signUp(identifierImpl identifierType, accountID *string, appOrg model.ApplicationOrganization, creds string, params string) (string, *model.AccountIdentifier, *model.Credential, error) { + return "", nil, nil, nil } -func (a *signatureAuthImpl) signUpAdmin(authType model.AuthType, appOrg model.ApplicationOrganization, identifier string, password string, newCredentialID string) (map[string]interface{}, map[string]interface{}, error) { - return nil, nil, nil +func (a *signatureAuthImpl) signUpAdmin(identifierImpl identifierType, appOrg model.ApplicationOrganization, creds string) (map[string]interface{}, *model.AccountIdentifier, *model.Credential, error) { + return nil, nil, nil, nil } -func (a *signatureAuthImpl) getUserIdentifier(creds string) (string, error) { - return "", nil -} - -func (a *signatureAuthImpl) verifyCredential(credential *model.Credential, verification string, l *logs.Log) (map[string]interface{}, error) { - return nil, errors.New(logutils.Unimplemented) -} - -func (a *signatureAuthImpl) sendVerifyCredential(credential *model.Credential, appName string, l *logs.Log) error { - return nil +func (a *signatureAuthImpl) forgotCredential(identifierImpl identifierType, credential *model.Credential, appOrg model.ApplicationOrganization) (map[string]interface{}, error) { + return nil, nil } -func (a *signatureAuthImpl) restartCredentialVerification(credential *model.Credential, appName string, l *logs.Log) error { - return nil +func (a *signatureAuthImpl) resetCredential(credential *model.Credential, resetCode *string, params string) (map[string]interface{}, error) { + return nil, nil } -func (a *signatureAuthImpl) isCredentialVerified(credential *model.Credential, l *logs.Log) (*bool, *bool, error) { - return nil, nil, nil +func (a *signatureAuthImpl) checkCredentials(identifierImpl identifierType, accountID *string, aats []model.AccountAuthType, creds string, params string, appOrg model.ApplicationOrganization) (string, string, error) { + return "", "", nil } -func (a *signatureAuthImpl) checkCredentials(accountAuthType model.AccountAuthType, creds string, l *logs.Log) (string, error) { - return "", nil +func (a *signatureAuthImpl) withParams(params map[string]interface{}) (authType, error) { + return a, nil } -func (a *signatureAuthImpl) resetCredential(credential *model.Credential, resetCode *string, params string, l *logs.Log) (map[string]interface{}, error) { - return nil, nil +func (a *signatureAuthImpl) requireIdentifierVerificationForSignIn() bool { + return false } -func (a *signatureAuthImpl) forgotCredential(credential *model.Credential, identifier string, appName string, l *logs.Log) (map[string]interface{}, error) { - return nil, nil +func (a *signatureAuthImpl) allowMultiple() bool { + return false } // initSignatureAuth initializes and registers a new signature auth instance diff --git a/core/auth/auth_type_username.go b/core/auth/auth_type_username.go deleted file mode 100644 index 6cbbe6de4..000000000 --- a/core/auth/auth_type_username.go +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright 2022 Board of Trustees of the University of Illinois. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package auth - -import ( - "core-building-block/core/model" - "core-building-block/utils" - "encoding/json" - - "github.com/rokwire/logging-library-go/v2/errors" - "github.com/rokwire/logging-library-go/v2/logs" - "github.com/rokwire/logging-library-go/v2/logutils" - "golang.org/x/crypto/bcrypt" -) - -const ( - authTypeUsername string = "username" - typeUsernameCreds logutils.MessageDataType = "username creds" - typeUsernameParams logutils.MessageDataType = "username params" -) - -// Username implementation of authType -type usernameAuthImpl struct { - auth *Auth - authType string -} - -// userNameCreds represents the creds struct for username auth -type usernameCreds struct { - Username string `json:"username" bson:"username" validate:"required"` - Password string `json:"password" bson:"password"` -} - -func (a *usernameAuthImpl) signUp(authType model.AuthType, appOrg model.ApplicationOrganization, creds string, params string, newCredentialID string, l *logs.Log) (string, map[string]interface{}, error) { - type signUpUsernameParams struct { - ConfirmPassword string `json:"confirm_password"` - } - - var sUsernameCreds usernameCreds - err := json.Unmarshal([]byte(creds), &sUsernameCreds) - if err != nil { - return "", nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeUsernameCreds, nil, err) - } - - var sUsernameParams signUpUsernameParams - err = json.Unmarshal([]byte(params), &sUsernameParams) - if err != nil { - return "", nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeUsernameParams, nil, err) - } - - username := sUsernameCreds.Username - password := sUsernameCreds.Password - confirmPassword := sUsernameParams.ConfirmPassword - if len(username) == 0 { - return "", nil, errors.ErrorData(logutils.StatusMissing, typeUsernameCreds, logutils.StringArgs("username")) - } - if len(password) == 0 { - return "", nil, errors.ErrorData(logutils.StatusMissing, typeUsernameCreds, logutils.StringArgs("password")) - } - - if len(confirmPassword) == 0 { - return "", nil, errors.ErrorData(logutils.StatusMissing, typeUsernameParams, logutils.StringArgs("confirm_password")) - } - //check if the password matches with the confirm password one - if password != confirmPassword { - return "", nil, errors.ErrorData(logutils.StatusInvalid, "mismatching password fields", nil) - } - - usernameCreds, err := a.buildCredentials(authType, appOrg.Application.Name, username, password, newCredentialID) - if err != nil { - return "", nil, errors.WrapErrorAction("building", "username credentials", nil, err) - } - - return "", usernameCreds, nil -} - -func (a *usernameAuthImpl) signUpAdmin(authType model.AuthType, appOrg model.ApplicationOrganization, identifier string, password string, newCredentialID string) (map[string]interface{}, map[string]interface{}, error) { - if password == "" { - password = utils.GenerateRandomPassword(12) - } - - usernameCreds, err := a.buildCredentials(authType, appOrg.Application.Name, identifier, password, newCredentialID) - if err != nil { - return nil, nil, errors.WrapErrorAction("building", "username credentials", nil, err) - } - - params := map[string]interface{}{"password": password} - return params, usernameCreds, nil -} - -func (a *usernameAuthImpl) buildCredentials(authType model.AuthType, appName string, username string, password string, credID string) (map[string]interface{}, error) { - - //password hash - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionGenerate, "password hash", nil, err) - } - - usernameCredValue := usernameCreds{Username: username, Password: string(hashedPassword)} - - usernameCredValueMap, err := usernameCredsToMap(&usernameCredValue) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionCast, "map from username creds", nil, err) - } - - return usernameCredValueMap, nil -} - -func usernameCredsToMap(creds *usernameCreds) (map[string]interface{}, error) { - credBytes, err := json.Marshal(creds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, typeUsernameCreds, nil, err) - } - var credsMap map[string]interface{} - err = json.Unmarshal(credBytes, &credsMap) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, "map from username creds", nil, err) - } - return credsMap, nil -} - -func (a *usernameAuthImpl) getUserIdentifier(creds string) (string, error) { - var requestCreds usernameCreds - err := json.Unmarshal([]byte(creds), &requestCreds) - if err != nil { - return "", errors.WrapErrorAction(logutils.ActionUnmarshal, typeUsernameCreds, nil, err) - } - - return requestCreds.Username, nil -} - -func (a *usernameAuthImpl) verifyCredential(credential *model.Credential, verification string, l *logs.Log) (map[string]interface{}, error) { - return nil, errors.New(logutils.Unimplemented) -} - -func (a *usernameAuthImpl) sendVerifyCredential(credential *model.Credential, appName string, l *logs.Log) error { - return nil -} - -func (a *usernameAuthImpl) restartCredentialVerification(credential *model.Credential, appName string, l *logs.Log) error { - return nil -} - -func (a *usernameAuthImpl) isCredentialVerified(credential *model.Credential, l *logs.Log) (*bool, *bool, error) { - //TODO verification process for usernames - verified := true - expired := false - return &verified, &expired, nil -} - -func (a *usernameAuthImpl) checkCredentials(accountAuthType model.AccountAuthType, creds string, l *logs.Log) (string, error) { - //get stored credential - storedCreds, err := mapToUsernameCreds(accountAuthType.Credential.Value) - if err != nil { - return "", errors.WrapErrorAction(logutils.ActionCast, typeUsernameCreds, nil, err) - } - - //get request credential - type signInPasswordCred struct { - Password string `json:"password"` - } - var sPasswordParams signInPasswordCred - err = json.Unmarshal([]byte(creds), &sPasswordParams) - if err != nil { - return "", errors.WrapErrorAction(logutils.ActionUnmarshal, "sign in password creds", nil, err) - } - requestPassword := sPasswordParams.Password - - //compare stored and requests ones - err = bcrypt.CompareHashAndPassword([]byte(storedCreds.Password), []byte(requestPassword)) - if err != nil { - return "", errors.WrapErrorAction(logutils.ActionValidate, model.TypeCredential, nil, err).SetStatus(utils.ErrorStatusInvalid) - } - - return "", nil -} - -func mapToUsernameCreds(credsMap map[string]interface{}) (*usernameCreds, error) { - credBytes, err := json.Marshal(credsMap) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, typeUsernameCreds, nil, err) - } - var creds usernameCreds - err = json.Unmarshal(credBytes, &creds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeUsernameCreds, nil, err) - } - return &creds, nil -} - -func (a *usernameAuthImpl) resetCredential(credential *model.Credential, resetCode *string, params string, l *logs.Log) (map[string]interface{}, error) { - //get the data from params - type Params struct { - NewPassword string `json:"new_password"` - ConfirmPassword string `json:"confirm_password"` - } - - var paramsData Params - err := json.Unmarshal([]byte(params), ¶msData) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeUsernameParams, nil, err) - } - newPassword := paramsData.NewPassword - confirmPassword := paramsData.ConfirmPassword - - if len(newPassword) == 0 { - return nil, errors.ErrorData(logutils.StatusMissing, logutils.TypeString, logutils.StringArgs("new_password")) - } - if len(confirmPassword) == 0 { - return nil, errors.ErrorData(logutils.StatusMissing, logutils.TypeString, logutils.StringArgs("confirm_password")) - } - //check if the password matches with the confirm password one - if newPassword != confirmPassword { - return nil, errors.ErrorData(logutils.StatusInvalid, "mismatching password fields", nil) - } - - credBytes, err := json.Marshal(credential.Value) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, typeUsernameCreds, nil, err) - } - - var creds *usernameCreds - err = json.Unmarshal(credBytes, &creds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeUsernameCreds, nil, err) - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionGenerate, "password hash", nil, err) - } - - //Update verification data - creds.Password = string(hashedPassword) - credsMap, err := usernameCredsToMap(creds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionCast, "map from username creds", nil, err) - } - - return credsMap, nil -} - -func (a *usernameAuthImpl) forgotCredential(credential *model.Credential, identifier string, appName string, l *logs.Log) (map[string]interface{}, error) { - return nil, nil -} - -// initUsernameAuth initializes and registers a new username auth instance -func initUsernameAuth(auth *Auth) (*usernameAuthImpl, error) { - username := &usernameAuthImpl{auth: auth, authType: authTypeUsername} - - err := auth.registerAuthType(username.authType, username) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionRegister, model.TypeAuthType, nil, err) - } - - return username, nil -} diff --git a/core/auth/auth_type_webauthn.go b/core/auth/auth_type_webauthn.go new file mode 100644 index 000000000..b65155d3d --- /dev/null +++ b/core/auth/auth_type_webauthn.go @@ -0,0 +1,716 @@ +// Copyright 2022 Board of Trustees of the University of Illinois. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "bytes" + "core-building-block/core/model" + "core-building-block/driven/storage" + "core-building-block/utils" + "encoding/json" + "strings" + "time" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/google/uuid" + "gopkg.in/go-playground/validator.v9" + + "github.com/rokwire/logging-library-go/v2/errors" + "github.com/rokwire/logging-library-go/v2/logutils" +) + +const ( + //AuthTypeWebAuthn webauthn auth type + AuthTypeWebAuthn string = "webauthn" + + credentialKeyResponse string = "response" + credentialKeyCredential string = "credential" + + stateKeyChallenge string = "challenge" + stateKeySession string = "session" + + typeWebAuthnCreds logutils.MessageDataType = "webauthn creds" + typeWebAuthnParams logutils.MessageDataType = "webauthn params" + + rpDisplayNameKey string = "rp_display_name" +) + +type webAuthnUser struct { + ID string + Name string + DisplayName string + Credentials []webauthn.Credential +} + +// WebAuthnID unique user ID +func (u webAuthnUser) WebAuthnID() []byte { + return []byte(u.ID) +} + +// WebAuthnName unique human-readable identifier +func (u webAuthnUser) WebAuthnName() string { + return u.Name +} + +// WebAuthnDisplayName user display name (display purposes only) +func (u webAuthnUser) WebAuthnDisplayName() string { + return u.DisplayName +} + +// WebAuthnCredentials +func (u webAuthnUser) WebAuthnCredentials() []webauthn.Credential { + return u.Credentials +} + +// WebAuthnIcon deprecated +func (u webAuthnUser) WebAuthnIcon() string { + return "" +} + +// webauthnCreds represents the creds struct for webauthn authentication +type webauthnCreds struct { + Credential *string `json:"credential,omitempty"` + Response *string `json:"response,omitempty"` +} + +func (c *webauthnCreds) toMap() (map[string]interface{}, error) { + if c == nil { + return nil, errors.ErrorData(logutils.StatusMissing, typeWebAuthnCreds, nil) + } + + credsMap, err := utils.JSONConvert[map[string]interface{}, webauthnCreds](*c) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionParse, typeWebAuthnCreds, nil, err) + } + if credsMap == nil { + return nil, errors.ErrorData(logutils.StatusInvalid, "webauthn creds map", nil) + } + return *credsMap, nil +} + +type webauthnParams struct { + DisplayName *string `json:"display_name"` +} + +// WebAuthn implementation of authType +type webAuthnAuthImpl struct { + auth *Auth + authType string + + config *webauthn.WebAuthn +} + +func (a *webAuthnAuthImpl) signUp(identifierImpl identifierType, accountID *string, appOrg model.ApplicationOrganization, creds string, params string) (string, *model.AccountIdentifier, *model.Credential, error) { + parameters, err := a.parseParams(params) + if err != nil { + return "", nil, nil, errors.WrapErrorAction(logutils.ActionParse, typeWebAuthnParams, nil, err) + } + + var user webAuthnUser + if identifierImpl != nil { + user.Name = identifierImpl.getIdentifier() + } else if parameters.DisplayName != nil { + user.Name = *parameters.DisplayName + } + + var accountIdentifier *model.AccountIdentifier + if accountID != nil { + // we are linking a webauthn credential, so use the existing accountID + user.ID = *accountID + } else { + _, accountIdentifier, err = identifierImpl.buildIdentifier(nil, appOrg.Application.Name) + if err != nil { + return "", nil, nil, errors.WrapErrorAction("building", "identifier", logutils.StringArgs(identifierImpl.getCode()), err) + } + + accountIdentifier.Verified = false + user.ID = accountIdentifier.Account.ID + } + + if parameters.DisplayName != nil { + user.DisplayName = *parameters.DisplayName + } else if accountIdentifier != nil { + user.DisplayName = accountIdentifier.Identifier + } + + message, err := a.beginRegistration(user, appOrg) + if err != nil { + return "", nil, nil, errors.WrapErrorAction(logutils.ActionStart, "webauthn registration", nil, err) + } + + return message, accountIdentifier, nil, nil +} + +func (a *webAuthnAuthImpl) signUpAdmin(identifierImpl identifierType, appOrg model.ApplicationOrganization, creds string) (map[string]interface{}, *model.AccountIdentifier, *model.Credential, error) { + return nil, nil, nil, errors.New(logutils.Unimplemented) +} + +func (a *webAuthnAuthImpl) forgotCredential(identifierImpl identifierType, credential *model.Credential, appOrg model.ApplicationOrganization) (map[string]interface{}, error) { + //TODO: implement + return nil, errors.New(logutils.Unimplemented) +} + +func (a *webAuthnAuthImpl) resetCredential(credential *model.Credential, resetCode *string, params string) (map[string]interface{}, error) { + //TODO: implement + return nil, errors.New(logutils.Unimplemented) +} + +func (a *webAuthnAuthImpl) checkCredentials(identifierImpl identifierType, accountID *string, aats []model.AccountAuthType, creds string, params string, appOrg model.ApplicationOrganization) (string, string, error) { + incomingCreds, err := a.parseCreds(creds) + if err != nil { + return "", "", errors.WrapErrorAction(logutils.ActionParse, typeWebAuthnCreds, nil, err) + } + + var user webauthn.User + if incomingCreds.Response == nil { + user, err = a.buildUser(accountID, aats) + if err != nil { + return "", "", errors.WrapErrorAction("building", "webauthn user", nil, err) + } + + var optionData string + if user != nil && len(user.WebAuthnCredentials()) == 0 { + // attempting login with identifier and no credentials - need to restart registration instead + parameters, err := a.parseParams(params) + if err != nil { + return "", "", errors.WrapErrorAction(logutils.ActionParse, typeWebAuthnParams, nil, err) + } + + newUser, ok := user.(webAuthnUser) + if !ok { + return "", "", errors.ErrorData(logutils.StatusInvalid, "webauthn user", nil) + } + + if identifierImpl != nil { + newUser.Name = identifierImpl.getIdentifier() + } else if parameters.DisplayName != nil { + newUser.Name = *parameters.DisplayName + } + + if parameters.DisplayName != nil { + newUser.DisplayName = *parameters.DisplayName + } else if identifierImpl != nil { + newUser.DisplayName = identifierImpl.getIdentifier() + } + + optionData, err = a.beginRegistration(newUser, appOrg) + if err != nil { + return "", "", errors.WrapErrorAction(logutils.ActionStart, "webauthn registration", nil, err) + } + } else { + optionData, err = a.beginLogin(user, appOrg) + if err != nil { + return "", "", errors.WrapErrorAction("beginning", "webauthn login", nil, err) + } + } + + return optionData, "", nil + } + + // accountID will not be nil if linking or if account identifier has been verified during sign up + if accountID != nil { + user, err = a.buildUser(accountID, aats) + if err != nil { + return "", "", errors.WrapErrorAction("building", "webauthn user", nil, err) + } + + // complete registration + if response, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(*incomingCreds.Response)); err == nil { + credID, err := a.completeRegistration(response, user, aats, appOrg) + if err != nil { + return "", "", err + } + return "", credID, nil + } + } + + // either complete login with identifier or complete discoverable login without identifier + if response, err := protocol.ParseCredentialRequestResponseBody(strings.NewReader(*incomingCreds.Response)); err == nil { + if user != nil { + if len(response.Response.UserHandle) > 0 { + for _, aat := range aats { + // backwards compatibility: user handles (user IDs) used to be credential IDs + // check if the user handle matches any of the user's webauthn credential IDs + // if so, set the user handle equal to the user ID (now the account ID) + if aat.Credential != nil && bytes.Equal(response.Response.UserHandle, []byte(aat.Credential.ID)) { + response.Response.UserHandle = user.WebAuthnID() + break + } + } + } + } + + credID, err := a.completeLogin(response, user, aats, appOrg) + return "", credID, err + } + + // cannot parse response, so it is invalid + return "", "", errors.ErrorData(logutils.StatusInvalid, logutils.MessageDataType(credentialKeyResponse), nil) +} + +func (a *webAuthnAuthImpl) withParams(params map[string]interface{}) (authType, error) { + rpDisplayName, _ := params["rp_display_name"].(string) + if rpDisplayName == "" { + rpDisplayName = rpDisplayNameKey + } + rpID, ok := params["rp_id"].(string) + if !ok { + return nil, errors.ErrorData(logutils.StatusInvalid, "supported auth type param", &logutils.FieldArgs{"param": "rp_id"}) + } + rpOrigins, ok := params["rp_origins"].(string) + if !ok { + return nil, errors.ErrorData(logutils.StatusInvalid, "supported auth type param", &logutils.FieldArgs{"param": "rp_origins"}) + } + + requireResidentKey, _ := params["require_resident_key"].(bool) + residentKey, ok := params["resident_key"].(string) + if !ok { + residentKey = string(protocol.ResidentKeyRequirementRequired) + } + userVerification, ok := params["user_verification"].(string) + if !ok { + userVerification = string(protocol.VerificationRequired) + } + attestationPreference, ok := params["attestation_preference"].(string) + if !ok { + attestationPreference = string(protocol.PreferNoAttestation) + } + authenticatorAttachment, ok := params["authenticator_attachment"].(string) + if !ok { + authenticatorAttachment = string(protocol.Platform) + } + + wconfig := &webauthn.Config{ + RPDisplayName: rpDisplayName, + RPID: rpID, // Generally the FQDN for your site + RPOrigins: strings.Split(rpOrigins, ","), // The origin URLs allowed for WebAuthn requests + AttestationPreference: protocol.ConveyancePreference(attestationPreference), + AuthenticatorSelection: protocol.AuthenticatorSelection{ + AuthenticatorAttachment: protocol.AuthenticatorAttachment(authenticatorAttachment), + RequireResidentKey: &requireResidentKey, + ResidentKey: protocol.ResidentKeyRequirement(residentKey), + UserVerification: protocol.UserVerificationRequirement(userVerification), + }, + } + + config, err := webauthn.New(wconfig) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionInitialize, model.TypeAuthType, nil, err) + } + + return &webAuthnAuthImpl{auth: a.auth, authType: a.authType, config: config}, nil +} + +func (a *webAuthnAuthImpl) requireIdentifierVerificationForSignIn() bool { + return true +} + +func (a *webAuthnAuthImpl) allowMultiple() bool { + return true +} + +// Helpers + +func (a *webAuthnAuthImpl) beginRegistration(user webauthn.User, appOrg model.ApplicationOrganization) (string, error) { + if a.config.Config.RPDisplayName == rpDisplayNameKey { + a.config.Config.RPDisplayName = appOrg.Application.Name + } + options, session, err := a.config.BeginRegistration(user) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionRegister, model.TypeAccount, nil, err) + } + + sessionData, err := json.Marshal(session) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionMarshal, "session", nil, err) + } + + state := map[string]interface{}{stateKeyChallenge: session.Challenge, stateKeySession: string(sessionData)} + accountID := string(user.WebAuthnID()) + loginState := model.LoginState{ID: uuid.NewString(), AppID: appOrg.Application.ID, OrgID: appOrg.Organization.ID, AccountID: &accountID, State: state, DateCreated: time.Now().UTC()} + err = a.auth.storage.InsertLoginState(loginState) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionCreate, model.TypeLoginState, nil, err) + } + + optionData, err := json.Marshal(options) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionMarshal, "creation options", nil, err) + } + + return string(optionData), nil +} + +func (a *webAuthnAuthImpl) completeRegistration(response *protocol.ParsedCredentialCreationData, user webauthn.User, aats []model.AccountAuthType, appOrg model.ApplicationOrganization) (string, error) { + if user == nil { + return "", errors.ErrorData(logutils.StatusMissing, model.TypeAccount, nil) + } + + accountIDVal := string(user.WebAuthnID()) + accountID := &accountIDVal + + params := map[string]interface{}{ + stateKeyChallenge: response.Response.CollectedClientData.Challenge, + } + loginState, err := a.auth.storage.FindLoginState(appOrg.Application.ID, appOrg.Organization.ID, accountID, params) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionFind, model.TypeLoginState, nil, err) + } + + session, err := a.parseWebAuthnSession(loginState.State) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionParse, "session", nil, err) + } + if session == nil { + return "", errors.ErrorData(logutils.StatusMissing, "session", nil) + } + + if a.config.Config.RPDisplayName == rpDisplayNameKey { + a.config.Config.RPDisplayName = appOrg.Application.Name + } + credential, err := a.config.CreateCredential(user, *session, response) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionCreate, model.TypeCredential, nil, err) + } + + credentialData, err := json.Marshal(credential) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionMarshal, "credential", nil, err) + } + + credentialStr := string(credentialData) + credValue := &webauthnCreds{Credential: &credentialStr} + credData, err := credValue.toMap() + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionCast, "map from webauthn creds", nil, err) + } + + var accountAuthType *model.AccountAuthType + for i, aat := range aats { + if aat.Credential == nil { + accountAuthType = &aats[i] + break + } + } + if accountAuthType == nil { + return "", errors.ErrorData(logutils.StatusMissing, "account auth type without credential", &logutils.FieldArgs{"auth_type_code": a.authType, "account_id": accountIDVal}) + } + + credID := uuid.NewString() + transaction := func(context storage.TransactionContext) error { + //1. insert new credential + storeCred := &model.Credential{ID: credID, Value: credData, AccountsAuthTypes: []model.AccountAuthType{*accountAuthType}, + AuthType: model.AuthType{Code: a.authType}, DateCreated: time.Now().UTC()} + err = a.auth.storage.InsertCredential(context, storeCred) + if err != nil { + return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeCredential, nil, err) + } + + //2. update the credential of the existing account auth type + accountAuthType.Credential = &model.Credential{ID: credID} + err = a.auth.storage.UpdateAccountAuthType(context, *accountAuthType) + if err != nil { + return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountAuthType, &logutils.FieldArgs{"id": accountAuthType.ID, "account_id": accountIDVal}, err) + } + + return nil + } + + err = a.auth.storage.PerformTransaction(transaction) + if err != nil { + return "", err + } + + return credID, nil +} + +func (a *webAuthnAuthImpl) beginLogin(user webauthn.User, appOrg model.ApplicationOrganization) (string, error) { + var options *protocol.CredentialAssertion + var session *webauthn.SessionData + var err error + var accountID *string + if a.config.Config.RPDisplayName == rpDisplayNameKey { + a.config.Config.RPDisplayName = appOrg.Application.Name + } + if user != nil { + options, session, err = a.config.BeginLogin(user) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionStart, "login", nil, err) + } + accountIDVal := string(user.WebAuthnID()) + accountID = &accountIDVal + } else { + // if no user, we can start a discoverable login + options, session, err = a.config.BeginDiscoverableLogin() + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionStart, "discoverable login", nil, err) + } + } + + sessionData, err := json.Marshal(session) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionMarshal, "session", nil, err) + } + + state := map[string]interface{}{stateKeyChallenge: session.Challenge, stateKeySession: string(sessionData)} + loginState := model.LoginState{ID: uuid.NewString(), AppID: appOrg.Application.ID, OrgID: appOrg.Organization.ID, AccountID: accountID, State: state, DateCreated: time.Now().UTC()} + err = a.auth.storage.InsertLoginState(loginState) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionUpdate, model.TypeCredential, nil, err) + } + + optionData, err := json.Marshal(options) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionMarshal, "creation options", nil, err) + } + + return string(optionData), nil +} + +func (a *webAuthnAuthImpl) completeLogin(response *protocol.ParsedCredentialAssertionData, user webauthn.User, aats []model.AccountAuthType, appOrg model.ApplicationOrganization) (string, error) { + if a.config.Config.RPDisplayName == rpDisplayNameKey { + a.config.Config.RPDisplayName = appOrg.Application.Name + } + + var accountID *string + if user != nil { + accountIDVal := string(user.WebAuthnID()) + accountID = &accountIDVal + } + + params := map[string]interface{}{ + stateKeyChallenge: response.Response.CollectedClientData.Challenge, + } + loginState, err := a.auth.storage.FindLoginState(appOrg.Application.ID, appOrg.Organization.ID, accountID, params) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionFind, model.TypeLoginState, nil, err) + } + + session, err := a.parseWebAuthnSession(loginState.State) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionParse, "session", nil, err) + } + if session == nil { + return "", errors.ErrorData(logutils.StatusMissing, "session", nil) + } + + var credential *model.Credential + var updatedCred *webauthn.Credential + if user != nil { + updatedCred, err = a.config.ValidateLogin(user, *session, response) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionValidate, "login", nil, err) + } + + // find matching credential in provided list + for _, aat := range aats { + if aat.Credential != nil { + webAuthnCred, err := a.parseWebAuthnCredential(aat.Credential.Value) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionParse, "webauthn credential", nil, err) + } + + if webAuthnCred != nil && bytes.Equal(updatedCred.ID, webAuthnCred.ID) { + credential = aat.Credential + break + } + } + } + } else { + // if no user, we can validate a discoverable login + userDiscoverer := func(rawID, userHandle []byte) (webauthn.User, error) { + legacyUserHandle := false + + // find account by userHandle (should match an account ID) + account, err := a.auth.storage.FindAccountByID(nil, string(userHandle)) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, &logutils.FieldArgs{"userHandle": string(userHandle)}, err) + } + if account == nil { + // backwards compatibility: user handles (user IDs) used to be credential IDs + // check if the user handle matches any of the user's webauthn credential IDs + account, err = a.auth.storage.FindAccountByCredentialID(nil, string(userHandle)) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, &logutils.FieldArgs{"userHandle": string(userHandle), "legacy": true}, err) + } + + if account == nil { + return nil, errors.ErrorData(logutils.StatusMissing, model.TypeAccount, &logutils.FieldArgs{"userHandle": string(userHandle)}) + } + legacyUserHandle = true + } + + // find matching credential by rawId (should match a credential ID) + aats, err := a.auth.findAccountAuthTypesAndCredentials(account, model.SupportedAuthType{AuthType: model.AuthType{Code: a.authType}}) + for _, aat := range aats { + if aat.Credential != nil { + webAuthnCred, err := a.parseWebAuthnCredential(aat.Credential.Value) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionParse, "webauthn credential", nil, err) + } + + if webAuthnCred != nil && bytes.Equal(rawID, webAuthnCred.ID) { + credential = aat.Credential + userID := account.ID + if legacyUserHandle { + userID = credential.ID + } + return webAuthnUser{ID: userID, Credentials: []webauthn.Credential{*webAuthnCred}}, nil + } + } + } + + return nil, errors.ErrorData(logutils.StatusMissing, "user", &logutils.FieldArgs{"userHandle": string(userHandle), "rawID": string(rawID)}) + } + + updatedCred, err = a.config.ValidateDiscoverableLogin(userDiscoverer, *session, response) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionValidate, "discoverable login", nil, err) + } + } + + credID := "" + if credential != nil { + credID = credential.ID + credentialData, err := json.Marshal(updatedCred) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionMarshal, "credential", nil, err) + } + + credential.Value[credentialKeyCredential] = string(credentialData) + err = a.auth.storage.UpdateCredentialValue(credID, credential.Value) + if err != nil { + return "", errors.WrapErrorAction(logutils.ActionUpdate, model.TypeCredential, nil, err) + } + } + + return credID, nil +} + +func (a *webAuthnAuthImpl) buildUser(accountID *string, aats []model.AccountAuthType) (webauthn.User, error) { + var user webauthn.User + + userCredentials := make([]webauthn.Credential, 0) + for _, aat := range aats { + if aat.Credential != nil { + webAuthnCred, err := a.parseWebAuthnCredential(aat.Credential.Value) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionParse, "webauthn credential", nil, err) + } + + // have at least one valid webauthn credential, so initiate login + if webAuthnCred != nil { + userCredentials = append(userCredentials, *webAuthnCred) + } + } + } + + if accountID != nil { // otherwise identifier-less login + user = webAuthnUser{ID: *accountID, Credentials: userCredentials} + } + + return user, nil +} + +func (a *webAuthnAuthImpl) parseCreds(creds string) (*webauthnCreds, error) { + var credential webauthnCreds + err := json.Unmarshal([]byte(creds), &credential) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeWebAuthnCreds, nil, err) + } + err = validator.New().Struct(credential) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionValidate, typeWebAuthnCreds, nil, err) + } + return &credential, nil +} + +func (a *webAuthnAuthImpl) parseParams(params string) (*webauthnParams, error) { + var parameters webauthnParams + err := json.Unmarshal([]byte(params), ¶meters) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeWebAuthnParams, nil, err) + } + err = validator.New().Struct(parameters) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionValidate, typeWebAuthnParams, nil, err) + } + return ¶meters, nil +} + +func (a *webAuthnAuthImpl) mapToCreds(credsMap map[string]interface{}) (*webauthnCreds, error) { + creds, err := utils.JSONConvert[webauthnCreds, map[string]interface{}](credsMap) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionParse, typeWebAuthnCreds, nil, err) + } + + err = validator.New().Struct(creds) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionValidate, typeWebAuthnCreds, nil, err) + } + return creds, nil +} + +func (a *webAuthnAuthImpl) parseWebAuthnSession(credValue map[string]interface{}) (*webauthn.SessionData, error) { + if credValue[stateKeySession] == nil { + return nil, nil + } + + sessionJSON, ok := credValue[stateKeySession].(string) + if !ok { + return nil, errors.ErrorData(logutils.StatusInvalid, "session", nil) + } + + var session webauthn.SessionData + err := json.Unmarshal([]byte(sessionJSON), &session) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, "session", nil, err) + } + + return &session, nil +} + +func (a *webAuthnAuthImpl) parseWebAuthnCredential(credValue map[string]interface{}) (*webauthn.Credential, error) { + if credValue[credentialKeyCredential] == nil { + return nil, nil + } + + credentialJSON, ok := credValue[credentialKeyCredential].(string) + if !ok { + return nil, errors.ErrorData(logutils.StatusInvalid, "credential", nil) + } + + var credentialVal webauthn.Credential + err := json.Unmarshal([]byte(credentialJSON), &credentialVal) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, "credential", nil, err) + } + + return &credentialVal, nil + // user.Credentials = []webauthn.Credential{credentialVal} +} + +// initWebAuthnAuth initializes and registers a new webauthn auth instance +func initWebAuthnAuth(auth *Auth) (*webAuthnAuthImpl, error) { + webauthn := &webAuthnAuthImpl{auth: auth, authType: AuthTypeWebAuthn} + + err := auth.registerAuthType(webauthn.authType, webauthn) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionRegister, model.TypeAuthType, logutils.StringArgs(AuthTypeWebAuthn), err) + } + + return webauthn, nil +} diff --git a/core/auth/identifier_type_email.go b/core/auth/identifier_type_email.go new file mode 100644 index 000000000..02c090d08 --- /dev/null +++ b/core/auth/identifier_type_email.go @@ -0,0 +1,329 @@ +// Copyright 2022 Board of Trustees of the University of Illinois. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "core-building-block/core/model" + "core-building-block/utils" + "crypto/subtle" + "encoding/json" + "fmt" + "net/url" + "regexp" + "strings" + "time" + + "github.com/google/uuid" + "github.com/rokwire/core-auth-library-go/v3/authutils" + "github.com/rokwire/logging-library-go/v2/errors" + "github.com/rokwire/logging-library-go/v2/logutils" + "gopkg.in/go-playground/validator.v9" +) + +const ( + //IdentifierTypeEmail email identifier type + IdentifierTypeEmail string = "email" + + typeEmailIdentifier logutils.MessageDataType = "email identifier" +) + +type emailIdentifier struct { + Email string `json:"email" validate:"required"` +} + +// Email implementation of identifierType +type emailIdentifierImpl struct { + auth *Auth + code string + + identifier string +} + +func (a *emailIdentifierImpl) getCode() string { + return a.code +} + +func (a *emailIdentifierImpl) getIdentifier() string { + return a.identifier +} + +func (a *emailIdentifierImpl) withIdentifier(creds string) (identifierType, error) { + var requestCreds emailIdentifier + err := json.Unmarshal([]byte(creds), &requestCreds) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeEmailIdentifier, nil, err) + } + + err = validator.New().Struct(requestCreds) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionValidate, typeEmailIdentifier, nil, err) + } + + email := strings.TrimSpace(requestCreds.Email) + validEmail := regexp.MustCompile(`^[a-zA-Z0-9.!#\$%&'*+/=?^_{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?(?:.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?)*$`) + if !validEmail.MatchString(email) { + return nil, errors.ErrorData(logutils.StatusInvalid, typeEmailIdentifier, &logutils.FieldArgs{"email": email}) + } + + return &emailIdentifierImpl{auth: a.auth, code: a.code, identifier: email}, nil +} + +func (a *emailIdentifierImpl) buildIdentifier(accountID *string, appName string) (string, *model.AccountIdentifier, error) { + if a.identifier == "" { + return "", nil, errors.ErrorData(logutils.StatusMissing, "email identifier", nil) + } + + accountIDStr := "" + if accountID != nil { + accountIDStr = *accountID + } else { + accountIDStr = uuid.NewString() + } + + message := "" + accountIdentifier := model.AccountIdentifier{ID: uuid.NewString(), Code: a.code, Identifier: a.identifier, Verified: false, + Sensitive: true, Account: model.Account{ID: accountIDStr}, DateCreated: time.Now().UTC()} + sent, err := a.sendVerifyIdentifier(&accountIdentifier, appName) + if err != nil { + return "", nil, errors.WrapErrorAction(logutils.ActionSend, "identifier verification", nil, err) + } + if sent { + message = "verification code sent successfully" + } + + return message, &accountIdentifier, nil +} + +func (a *emailIdentifierImpl) maskIdentifier() (string, error) { + emailParts := strings.Split(a.identifier, "@") + if len(emailParts) != 2 { + return "", errors.ErrorData(logutils.StatusInvalid, typeEmailIdentifier, &logutils.FieldArgs{"identifier": a.identifier}) + } + + emailParts[0] = utils.GetLogValue(emailParts[0], 3) // mask all but the last 3 characters of the email prefix + return strings.Join(emailParts, "@"), nil +} + +func (a *emailIdentifierImpl) requireVerificationForSignIn() bool { + return true +} + +func (a *emailIdentifierImpl) checkVerified(accountIdentifier *model.AccountIdentifier, appName string) error { + verified := accountIdentifier.Verified + expired := accountIdentifier.VerificationExpiry == nil || accountIdentifier.VerificationExpiry.Before(time.Now()) + + if !verified { + //it is unverified + if !expired { + //not expired, just notify the client that it is "unverified" + return errors.ErrorData("unverified", model.TypeAccountIdentifier, nil).SetStatus(utils.ErrorStatusUnverified) + } + + //restart identifier verification + err := a.restartIdentifierVerification(accountIdentifier, appName) + if err != nil { + return errors.WrapErrorAction("restarting", "identifier verification", nil, err) + } + + err = a.auth.storage.UpdateAccountIdentifier(nil, *accountIdentifier) + if err != nil { + return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountIdentifier, nil, err) + } + + //notify the client that it is unverified and verification is restarted + return errors.ErrorData("expired", "identifier verification", nil).SetStatus(utils.ErrorStatusVerificationExpired) + } + + return nil +} + +func (a *emailIdentifierImpl) allowMultiple() bool { + return true +} + +// authCommunicationChannel interface + +func (a *emailIdentifierImpl) verifyIdentifier(accountIdentifier *model.AccountIdentifier, verification string) error { + if accountIdentifier == nil { + return errors.ErrorData(logutils.StatusMissing, model.TypeAccountIdentifier, nil) + } + + if accountIdentifier.VerificationExpiry == nil || accountIdentifier.VerificationExpiry.Before(time.Now()) { + return errors.ErrorData("expired", "email verification code", nil) + } + if accountIdentifier.VerificationCode == nil { + return errors.ErrorData(logutils.StatusMissing, "email verification code", nil) + } + if subtle.ConstantTimeCompare([]byte(*accountIdentifier.VerificationCode), []byte(verification)) == 0 { + return errors.ErrorData(logutils.StatusInvalid, "email verification code", nil) + } + + //Update verification data + now := time.Now().UTC() + accountIdentifier.Verified = true + accountIdentifier.VerificationCode = nil + accountIdentifier.VerificationExpiry = nil + accountIdentifier.DateUpdated = &now + return nil +} + +func (a *emailIdentifierImpl) sendVerifyIdentifier(accountIdentifier *model.AccountIdentifier, appName string) (bool, error) { + if accountIdentifier == nil { + return false, errors.ErrorData(logutils.StatusMissing, model.TypeAccountIdentifier, nil) + } + + //verification settings + verifyWaitTime, verifyExpiryTime, err := a.getVerificationSettings() + if err != nil { + return false, errors.WrapErrorAction(logutils.ActionGet, "email verification settings", nil, err) + } + if verifyWaitTime == nil || verifyExpiryTime == nil { + return false, nil + } + + //Check if previous verification email was sent within the wait time if one was already sent + if accountIdentifier.VerificationExpiry != nil { + prevTime := accountIdentifier.VerificationExpiry.Add(time.Duration(-*verifyExpiryTime) * time.Hour) + if time.Now().UTC().Sub(prevTime) < time.Duration(*verifyWaitTime)*time.Second { + return false, errors.ErrorAction(logutils.ActionSend, "verification email", logutils.StringArgs("resend requested too soon")) + } + } + + //verification code + //TODO: turn length of reset code into a setting + code := utils.GenerateRandomString(64) + + //send verification email + if _, err = a.sendCode(appName, code, typeVerificationCode, accountIdentifier.ID); err != nil { + return false, errors.WrapErrorAction(logutils.ActionSend, "verification email", nil, err) + } + + //Update verification data in credential value + now := time.Now().UTC() + newExpiry := now.Add(time.Hour * time.Duration(*verifyExpiryTime)) + accountIdentifier.VerificationCode = &code + accountIdentifier.VerificationExpiry = &newExpiry + accountIdentifier.DateUpdated = &now + return true, nil +} + +func (a *emailIdentifierImpl) restartIdentifierVerification(accountIdentifier *model.AccountIdentifier, appName string) error { + if accountIdentifier == nil { + return errors.ErrorData(logutils.StatusMissing, model.TypeAccountIdentifier, nil) + } + + //Generate new verification code + newCode := utils.GenerateRandomString(64) + + //send new verification code for future + if _, err := a.sendCode(appName, newCode, typeVerificationCode, accountIdentifier.ID); err != nil { + return errors.WrapErrorAction(logutils.ActionSend, "verification email", nil, err) + } + //update new verification data in credential value + expiry := time.Now().UTC().Add(time.Hour * 24) + accountIdentifier.VerificationCode = &newCode + accountIdentifier.VerificationExpiry = &expiry + return nil +} + +func (a *emailIdentifierImpl) sendCode(appName string, code string, codeType string, itemID string) (string, error) { + if a.identifier == "" { + return "", errors.ErrorData(logutils.StatusMissing, typeEmailIdentifier, nil) + } + + params := url.Values{} + params.Add("id", itemID) + params.Add("code", code) + switch codeType { + case typePasswordResetCode: + passwordResetLink := a.auth.host + fmt.Sprintf("/ui/credential/reset?%s", params.Encode()) + subject := "Reset your password" + if appName != "" { + subject += " for " + appName + } + body := "Please click the link below to reset your password:
" + passwordResetLink + "

If you did not request a password reset, please ignore this message." + return "", a.auth.emailer.Send(a.identifier, subject, body, nil) + case typeVerificationCode: + verificationLink := a.auth.host + fmt.Sprintf("/ui/identifier/verify?%s", params.Encode()) + subject := "Verify your email address" + if appName != "" { + subject += " for " + appName + } + body := "Please click the link below to verify your email address:
" + verificationLink + "

If you did not request this verification link, please ignore this message." + return "", a.auth.emailer.Send(a.identifier, subject, body, nil) + case typeAuthenticationCode: + subject := "Your authentication code" + body := "Please use the code " + code + " to login" + if appName != "" { + subject += " for " + appName + body += " to " + appName + } + body += ". If you did not request this authentication code, please ignore this message." + return "", a.auth.emailer.Send(a.identifier, subject, body, nil) + default: + return "", errors.ErrorData(logutils.StatusInvalid, "code type", logutils.StringArgs(codeType)) + } +} + +func (a *emailIdentifierImpl) requiresCodeGeneration() bool { + return true +} + +// Helpers + +func (a *emailIdentifierImpl) getVerificationSettings() (*int, *int, error) { + // Time in seconds to wait before sending another auth code (default is 30) + verifyWaitTime := 30 + // Time in hours before auth code expires (default is 24) + verifyExpiry := 24 + + config, err := a.auth.storage.FindConfig(model.ConfigTypeAuth, authutils.AllApps, authutils.AllOrgs) + if err != nil { + return nil, nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeConfig, &logutils.FieldArgs{"type": model.ConfigTypeAuth, "app_id": authutils.AllApps, "org_id": authutils.AllOrgs}, err) + } + if config != nil { + authConfigData, err := model.GetConfigData[model.AuthConfigData](*config) + if err != nil { + return nil, nil, errors.WrapErrorAction(logutils.ActionParse, model.TypeAuthConfigData, nil, err) + } + + // Should email addresses be verified (default is true) + if authConfigData.EmailShouldVerify != nil && !*authConfigData.EmailShouldVerify { + return nil, nil, nil + } + + if authConfigData.EmailVerifyWaitTime != nil { + verifyWaitTime = *authConfigData.EmailVerifyWaitTime + } + + if authConfigData.EmailVerifyExpiry != nil { + verifyExpiry = *authConfigData.EmailVerifyExpiry + } + } + + return &verifyWaitTime, &verifyExpiry, nil +} + +// initEmailIdentifier initializes and registers a new email identifier instance +func initEmailIdentifier(auth *Auth) (*emailIdentifierImpl, error) { + email := &emailIdentifierImpl{auth: auth, code: IdentifierTypeEmail} + + err := auth.registerIdentifierType(email.code, email) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionRegister, typeIdentifierType, nil, err) + } + + return email, nil +} diff --git a/core/auth/identifier_type_external.go b/core/auth/identifier_type_external.go new file mode 100644 index 000000000..0a6ac4788 --- /dev/null +++ b/core/auth/identifier_type_external.go @@ -0,0 +1,98 @@ +// Copyright 2022 Board of Trustees of the University of Illinois. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "core-building-block/core/model" + "core-building-block/utils" + "encoding/json" + "strings" + + "github.com/rokwire/logging-library-go/v2/errors" + "github.com/rokwire/logging-library-go/v2/logutils" +) + +const ( + //IdentifierTypeExternal external identifier type + IdentifierTypeExternal string = "external" + + typeExternalIdentifier logutils.MessageDataType = "external identifier" +) + +// External implementation of identifierType +type externalIdentifierImpl struct { + auth *Auth + code string + + identifier string +} + +func (a *externalIdentifierImpl) getCode() string { + return a.code +} + +func (a *externalIdentifierImpl) getIdentifier() string { + return a.identifier +} + +func (a *externalIdentifierImpl) withIdentifier(creds string) (identifierType, error) { + var requestCreds map[string]string + err := json.Unmarshal([]byte(creds), &requestCreds) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeExternalIdentifier, nil, err) + } + + // these are keys that should NOT be treated as external identifier codes + excludeKeys := []string{IdentifierTypeEmail, IdentifierTypePhone, IdentifierTypeUsername, AuthTypePassword, AuthTypeCode, credentialKeyResponse} + for k, id := range requestCreds { + if !utils.Contains(excludeKeys, k) { + return &externalIdentifierImpl{auth: a.auth, code: k, identifier: strings.TrimSpace(id)}, nil + } + } + + return nil, errors.ErrorData(logutils.StatusMissing, "external identifier", nil) +} + +func (a *externalIdentifierImpl) buildIdentifier(accountID *string, appName string) (string, *model.AccountIdentifier, error) { + return "", nil, errors.ErrorData(logutils.StatusInvalid, typeIdentifierType, nil) +} + +func (a *externalIdentifierImpl) maskIdentifier() (string, error) { + return a.identifier, nil +} + +func (a *externalIdentifierImpl) requireVerificationForSignIn() bool { + return true +} + +func (a *externalIdentifierImpl) checkVerified(accountIdentifier *model.AccountIdentifier, appName string) error { + return nil +} + +func (a *externalIdentifierImpl) allowMultiple() bool { + return true +} + +// initExternalIdentifier initializes and registers a new external identifier instance +func initExternalIdentifier(auth *Auth) (*externalIdentifierImpl, error) { + external := &externalIdentifierImpl{auth: auth, code: IdentifierTypeExternal} + + err := auth.registerIdentifierType(external.code, external) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionRegister, typeIdentifierType, nil, err) + } + + return external, nil +} diff --git a/core/auth/identifier_type_phone.go b/core/auth/identifier_type_phone.go new file mode 100644 index 000000000..9eca87e40 --- /dev/null +++ b/core/auth/identifier_type_phone.go @@ -0,0 +1,219 @@ +// Copyright 2022 Board of Trustees of the University of Illinois. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "core-building-block/core/model" + "core-building-block/utils" + "encoding/json" + "net/url" + "regexp" + "time" + + "github.com/google/uuid" + "github.com/rokwire/logging-library-go/v2/errors" + "github.com/rokwire/logging-library-go/v2/logutils" + "gopkg.in/go-playground/validator.v9" +) + +const ( + //IdentifierTypePhone phone identifier type + IdentifierTypePhone string = "phone" + + typePhoneIdentifier logutils.MessageDataType = "phone identifier" + typePhoneNumber logutils.MessageDataType = "E.164 phone number" +) + +type phoneIdentifier struct { + Phone string `json:"phone" validate:"required"` +} + +// Phone implementation of identifierType +type phoneIdentifierImpl struct { + auth *Auth + code string + + identifier string +} + +func (a *phoneIdentifierImpl) getCode() string { + return a.code +} + +func (a *phoneIdentifierImpl) getIdentifier() string { + return a.identifier +} + +func (a *phoneIdentifierImpl) withIdentifier(creds string) (identifierType, error) { + var requestCreds phoneIdentifier + err := json.Unmarshal([]byte(creds), &requestCreds) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typePhoneIdentifier, nil, err) + } + + err = validator.New().Struct(requestCreds) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionValidate, typePhoneIdentifier, nil, err) + } + + validPhone := regexp.MustCompile(`^\+[1-9]\d{1,14}$`) + if !validPhone.MatchString(requestCreds.Phone) { + return nil, errors.ErrorData(logutils.StatusInvalid, typePhoneNumber, &logutils.FieldArgs{"phone": requestCreds.Phone}) + } + + return &phoneIdentifierImpl{auth: a.auth, code: a.code, identifier: requestCreds.Phone}, nil +} + +func (a *phoneIdentifierImpl) buildIdentifier(accountID *string, appName string) (string, *model.AccountIdentifier, error) { + if a.identifier == "" { + return "", nil, errors.ErrorData(logutils.StatusMissing, "phone identifier", nil) + } + + accountIDStr := "" + if accountID != nil { + accountIDStr = *accountID + } else { + accountIDStr = uuid.NewString() + } + + message := "" + accountIdentifier := model.AccountIdentifier{ID: uuid.NewString(), Code: a.code, Identifier: a.identifier, Verified: false, + Sensitive: true, Account: model.Account{ID: accountIDStr}, DateCreated: time.Now().UTC()} + sent, err := a.sendVerifyIdentifier(&accountIdentifier, appName) + if err != nil { + return "", nil, errors.WrapErrorAction(logutils.ActionSend, "phone verification", nil, err) + } + if sent { + message = "verification code sent successfully" + } + + return message, &accountIdentifier, nil +} + +func (a *phoneIdentifierImpl) maskIdentifier() (string, error) { + return utils.GetLogValue(a.identifier, 4), nil // mask all but the last 4 phone digits +} + +func (a *phoneIdentifierImpl) requireVerificationForSignIn() bool { + return false +} + +func (a *phoneIdentifierImpl) checkVerified(accountIdentifier *model.AccountIdentifier, appName string) error { + verified := accountIdentifier.Verified + expired := accountIdentifier.VerificationExpiry == nil || accountIdentifier.VerificationExpiry.Before(time.Now()) + + if !verified { + //it is unverified + if !expired { + //not expired, just notify the client that it is "unverified" + return errors.ErrorData("unverified", model.TypeAccountIdentifier, nil).SetStatus(utils.ErrorStatusUnverified) + } + + //restart identifier verification + err := a.restartIdentifierVerification(accountIdentifier, appName) + if err != nil { + return errors.WrapErrorAction("restarting", "identifier verification", nil, err) + } + + //notify the client that it is unverified and verification is restarted + return errors.ErrorData("expired", "identifier verification", nil).SetStatus(utils.ErrorStatusVerificationExpired) + } + + return nil +} + +func (a *phoneIdentifierImpl) allowMultiple() bool { + return true +} + +// authCommunicationChannel interface + +func (a *phoneIdentifierImpl) verifyIdentifier(accountIdentifier *model.AccountIdentifier, verification string) error { + _, err := a.sendCode("", verification, "", "") + if err != nil { + return errors.WrapErrorAction(logutils.ActionVerify, "verification code", nil, err) + } + + //TODO: do twilio/other phone verifiers have verification timeouts? + accountIdentifier.Verified = true + return nil +} + +func (a *phoneIdentifierImpl) sendVerifyIdentifier(accountIdentifier *model.AccountIdentifier, appName string) (bool, error) { + if accountIdentifier == nil { + return false, errors.ErrorData(logutils.StatusMissing, model.TypeAccountIdentifier, nil) + } + + //send verification code + if _, err := a.sendCode(appName, "", typeVerificationCode, accountIdentifier.ID); err != nil { + return false, errors.WrapErrorAction(logutils.ActionSend, "verification phone", nil, err) + } + + //TODO: do twilio/other phone verifiers have verification timeouts? + return true, nil +} + +func (a *phoneIdentifierImpl) restartIdentifierVerification(accountIdentifier *model.AccountIdentifier, appName string) error { + if accountIdentifier == nil { + return errors.ErrorData(logutils.StatusMissing, model.TypeAccountIdentifier, nil) + } + + //send new verification code for future + if _, err := a.sendCode(appName, "", typeVerificationCode, accountIdentifier.ID); err != nil { + return errors.WrapErrorAction(logutils.ActionSend, "verification text", nil, err) + } + + return nil +} + +func (a *phoneIdentifierImpl) sendCode(appName string, code string, codeType string, itemID string) (string, error) { + if a.identifier == "" { + return "", errors.ErrorData(logutils.StatusMissing, typeEmailIdentifier, nil) + } + + data := url.Values{} + data.Add("To", a.identifier) + if code != "" { + // check verification + data.Add("Code", code) + return "", a.auth.phoneVerifier.CheckVerification(a.identifier, data) + } + + // start verification + data.Add("Channel", "sms") + + message := "" + err := a.auth.phoneVerifier.StartVerification(a.identifier, data) + if err == nil { + message = "verification code sent successfully" + } + return message, err +} + +func (a *phoneIdentifierImpl) requiresCodeGeneration() bool { + return false +} + +// initPhoneIdentifier initializes and registers a new phone identifier instance +func initPhoneIdentifier(auth *Auth) (*phoneIdentifierImpl, error) { + phone := &phoneIdentifierImpl{auth: auth, code: IdentifierTypePhone} + + err := auth.registerIdentifierType(phone.code, phone) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionRegister, typeIdentifierType, nil, err) + } + + return phone, nil +} diff --git a/core/auth/identifier_type_username.go b/core/auth/identifier_type_username.go new file mode 100644 index 000000000..e508d5079 --- /dev/null +++ b/core/auth/identifier_type_username.go @@ -0,0 +1,124 @@ +// Copyright 2022 Board of Trustees of the University of Illinois. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "core-building-block/core/model" + "core-building-block/utils" + "encoding/json" + "strings" + "time" + + "github.com/google/uuid" + "github.com/rokwire/logging-library-go/v2/errors" + "github.com/rokwire/logging-library-go/v2/logutils" + "gopkg.in/go-playground/validator.v9" +) + +const ( + //IdentifierTypeUsername username identifier type + IdentifierTypeUsername string = "username" + + typeUsernameIdentifier logutils.MessageDataType = "username identifier" +) + +type usernameIdentifier struct { + Username string `json:"username" validate:"required"` +} + +// Username implementation of identifierType +type usernameIdentifierImpl struct { + auth *Auth + code string + + identifier string +} + +func (a *usernameIdentifierImpl) getCode() string { + return a.code +} + +func (a *usernameIdentifierImpl) getIdentifier() string { + return a.identifier +} + +func (a *usernameIdentifierImpl) withIdentifier(creds string) (identifierType, error) { + var requestCreds usernameIdentifier + err := json.Unmarshal([]byte(creds), &requestCreds) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, typeUsernameIdentifier, nil, err) + } + + err = validator.New().Struct(requestCreds) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionValidate, typeUsernameIdentifier, nil, err) + } + + username := strings.TrimSpace(strings.ToLower(requestCreds.Username)) + + // some applications may append - to usernames to support cross-platform passkeys - we just want the raw username + platforms := []string{"android", "ios", "web"} + if usernameParts := strings.Split(username, "-"); len(usernameParts) > 1 && utils.Contains(platforms, usernameParts[len(usernameParts)-1]) { + username = strings.Join(usernameParts[:len(usernameParts)-1], "-") + } + + return &usernameIdentifierImpl{auth: a.auth, code: a.code, identifier: username}, nil +} + +func (a *usernameIdentifierImpl) buildIdentifier(accountID *string, appName string) (string, *model.AccountIdentifier, error) { + if a.identifier == "" { + return "", nil, errors.ErrorData(logutils.StatusMissing, "username identifier", nil) + } + + accountIDStr := "" + if accountID != nil { + accountIDStr = *accountID + } else { + accountIDStr = uuid.NewString() + } + + accountIdentifier := model.AccountIdentifier{ID: uuid.NewString(), Code: a.code, Identifier: a.identifier, Verified: true, + Account: model.Account{ID: accountIDStr}, DateCreated: time.Now().UTC()} + + return "", &accountIdentifier, nil +} + +func (a *usernameIdentifierImpl) maskIdentifier() (string, error) { + return a.identifier, nil +} + +func (a *usernameIdentifierImpl) requireVerificationForSignIn() bool { + return true +} + +func (a *usernameIdentifierImpl) checkVerified(accountIdentifier *model.AccountIdentifier, appName string) error { + return nil // return nil because username verification is not possible for now +} + +func (a *usernameIdentifierImpl) allowMultiple() bool { + return false +} + +// initUsernameIdentifier initializes and registers a new username identifier instance +func initUsernameIdentifier(auth *Auth) (*usernameIdentifierImpl, error) { + username := &usernameIdentifierImpl{auth: auth, code: IdentifierTypeUsername} + + err := auth.registerIdentifierType(username.code, username) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionRegister, typeIdentifierType, nil, err) + } + + return username, nil +} diff --git a/core/auth/interfaces.go b/core/auth/interfaces.go index 229dc44e8..ca913dd13 100644 --- a/core/auth/interfaces.go +++ b/core/auth/interfaces.go @@ -17,6 +17,7 @@ package auth import ( "core-building-block/core/model" "core-building-block/driven/storage" + "net/url" "time" "github.com/lestrrat-go/jwx/jwk" @@ -26,52 +27,109 @@ import ( "github.com/rokwire/logging-library-go/v2/logs" ) +// identifierType is the interface for auth identifiers that are not external to the system +type identifierType interface { + //getType returns the identifier code + // Returns: + // identifierCode (string): identifier code + getCode() string + + //getIdentifier returns the user identifier + // Returns: + // userIdentifier (string): User identifier + getIdentifier() string + + //withIdentifier parses the credentials and copies the calling identifierType while caching the identifier + // Returns: + // identifierImpl (identifierType): Copy of calling identifierType with cached identifier + withIdentifier(creds string) (identifierType, error) + + //buildIdentifier creates a new account identifier + // Returns: + // message (string): response message + // accountIdentifier (*model.AccountIdentifier): the new account identifier + buildIdentifier(accountID *string, appName string) (string, *model.AccountIdentifier, error) + + // masks the cached identifier + maskIdentifier() (string, error) + + // gives whether the identifier must be verified before sign-in is allowed + requireVerificationForSignIn() bool + + // checks whether the given account identifier is verified, restarts verification if necessary and possible + checkVerified(accountIdentifier *model.AccountIdentifier, appName string) error + + //allowMultiple says whether an account may have multiple identifiers of this type + // Returns: + // allowed (bool): whether mulitple identifier types are allowed + allowMultiple() bool +} + +type authCommunicationChannel interface { + //verifies identifier (e.g., checks the verification code generated on email signup for email auth type) + verifyIdentifier(accountIdentifier *model.AccountIdentifier, verification string) error + + //sends the verification code to the identifier + // Returns: + // sentCode (bool): whether the verification code was sent successfully + sendVerifyIdentifier(accountIdentifier *model.AccountIdentifier, appName string) (bool, error) + + //restarts the identifier verification + restartIdentifierVerification(accountIdentifier *model.AccountIdentifier, appName string) error + + //sendCode sends a verification code using the channel + // Returns: + // message (string): response message + sendCode(appName string, code string, codeType string, itemID string) (string, error) + + //requiresCodeGeneration says whether a code needs to be generated by this service to send on the channel + // Returns: + // required (bool): whether codes need to be generated by the service + requiresCodeGeneration() bool +} + // authType is the interface for authentication for auth types which are not external for the system(the users do not come from external system) type authType interface { //signUp applies sign up operation // Returns: // message (string): Success message if verification is required. If verification is not required, return "" - // credentialValue (map): Credential value - signUp(authType model.AuthType, appOrg model.ApplicationOrganization, creds string, params string, newCredentialID string, l *logs.Log) (string, map[string]interface{}, error) + // accountIdentifier (*model.AccountIdentifier): new account identifier + // credential (*model.Credential): new credential + signUp(identifierImpl identifierType, accountID *string, appOrg model.ApplicationOrganization, creds string, params string) (string, *model.AccountIdentifier, *model.Credential, error) //signUpAdmin signs up a new admin user // Returns: - // password (string): newly generated password - // credentialValue (map): Credential value - signUpAdmin(authType model.AuthType, appOrg model.ApplicationOrganization, identifier string, password string, newCredentialID string) (map[string]interface{}, map[string]interface{}, error) - - //verifies credential (checks the verification code generated on email signup for email auth type) - // Returns: - // authTypeCreds (map[string]interface{}): Updated Credential.Value - verifyCredential(credential *model.Credential, verification string, l *logs.Log) (map[string]interface{}, error) + // credentialParams (map): newly generated credential parameters + // accountIdentifier (*model.AccountIdentifier): new account identifier + // credential (*model.Credential): new credential + signUpAdmin(identifierImpl identifierType, appOrg model.ApplicationOrganization, creds string) (map[string]interface{}, *model.AccountIdentifier, *model.Credential, error) - //sends the verification code to the identifier - sendVerifyCredential(credential *model.Credential, appName string, l *logs.Log) error - - //restarts the credential verification - restartCredentialVerification(credential *model.Credential, appName string, l *logs.Log) error + //apply forgot credential for the auth type (generates a reset password link with code and expiry and sends it to given identifier for email auth type) + forgotCredential(identifierImpl identifierType, credential *model.Credential, appOrg model.ApplicationOrganization) (map[string]interface{}, error) //updates the value of the credential object with new value // Returns: // authTypeCreds (map[string]interface{}): Updated Credential.Value - resetCredential(credential *model.Credential, resetCode *string, params string, l *logs.Log) (map[string]interface{}, error) + resetCredential(credential *model.Credential, resetCode *string, params string) (map[string]interface{}, error) - //apply forgot credential for the auth type (generates a reset password link with code and expiry and sends it to given identifier for email auth type) - forgotCredential(credential *model.Credential, identifier string, appName string, l *logs.Log) (map[string]interface{}, error) - - //getUserIdentifier parses the credentials and returns the user identifier + //checkCredential checks if the incoming credentials are valid for the stored credentials // Returns: - // userIdentifier (string): User identifier - getUserIdentifier(creds string) (string, error) + // message (string): information required to complete login, if applicable + // credentialID (string): the ID of the credential used to validate the login + checkCredentials(identifierImpl identifierType, accountID *string, aats []model.AccountAuthType, creds string, params string, appOrg model.ApplicationOrganization) (string, string, error) - //isCredentialVerified says if the credential is verified + //withParams parses the params and copies the calling authType while caching the params // Returns: - // verified (bool): is credential verified - // expired (bool): is credential verification expired - isCredentialVerified(credential *model.Credential, l *logs.Log) (*bool, *bool, error) + // authImpl (authType): Copy of calling authType with cached params + withParams(params map[string]interface{}) (authType, error) + + // gives whether the identifier used with this auth type must be verified before sign-in is allowed + requireIdentifierVerificationForSignIn() bool - //checkCredentials checks if the account credentials are valid for the account auth type - checkCredentials(accountAuthType model.AccountAuthType, creds string, l *logs.Log) (string, error) + //allowMultiple says whether an account may have multiple auth types of this type + // Returns: + // allowed (bool): whether mulitple auth types are allowed + allowMultiple() bool } // externalAuthType is the interface for authentication for auth types which are external for the system(the users comes from external system). @@ -123,7 +181,7 @@ type APIs interface { // deviceType (string): "mobile" or "web" or "desktop" etc // deviceOS (*string): Device OS // deviceID (*string): Device ID - // authenticationType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") + // authenticationType (string): Name of the authentication method for provided creds (eg. "password", "code", "illinois_oidc") // creds (string): Credentials/JSON encoded credential structure defined for the specified auth type // apiKey (string): API key to validate the specified app // appTypeIdentifier (string): identifier of the app type/client that the user is logging in from @@ -132,10 +190,11 @@ type APIs interface { // clientVersion(*string): Most recent client version // profile (Profile): Account profile // preferences (map): Account preferences + // accountIdentifierID (*string): UUID of account identifier, meant to be used after using SignInOptions // admin (bool): Is this an admin login? // l (*logs.Log): Log object pointer for request // Returns: - // Message (*string): message + // Response parameters (map): any messages or parameters to send in response when requiring identifier verification and/or NOT logging in the user // Login session (*LoginSession): Signed ROKWIRE access token to be used to authorize future requests // Access token (string): Signed ROKWIRE access token to be used to authorize future requests // Refresh Token (string): Refresh token that can be sent to refresh the access token once it expires @@ -145,7 +204,7 @@ type APIs interface { // MFA types ([]model.MFAType): list of MFA types account is enrolled in Login(ipAddress string, deviceType string, deviceOS *string, deviceID *string, authenticationType string, creds string, apiKey string, appTypeIdentifier string, orgID string, params string, clientVersion *string, profile model.Profile, privacy model.Privacy, preferences map[string]interface{}, - username string, admin bool, l *logs.Log) (*string, *model.LoginSession, []model.MFAType, error) + accountIdentifierID *string, admin bool, l *logs.Log) (map[string]interface{}, *model.LoginSession, []model.MFAType, error) //Logout logouts an account from app/org // Input: @@ -153,40 +212,45 @@ type APIs interface { Logout(appID string, orgID string, currentAccountID string, sessionID string, allSessions bool, l *logs.Log) error //AccountExists checks if a user is already registered - //The authentication method must be one of the supported for the application. // Input: - // authenticationType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") // userIdentifier (string): User identifier for the specified auth type // apiKey (string): API key to validate the specified app // appTypeIdentifier (string): identifier of the app type/client that the user is logging in from // orgID (string): ID of the organization that the user is logging in // Returns: // accountExisted (bool): valid when error is nil - AccountExists(authenticationType string, userIdentifier string, apiKey string, appTypeIdentifier string, orgID string) (bool, error) + AccountExists(identifierJSON string, apiKey string, appTypeIdentifier string, orgID string, authenticationType *string, userIdentifier *string) (bool, error) //CanSignIn checks if a user can sign in - //The authentication method must be one of the supported for the application. // Input: - // authenticationType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") // userIdentifier (string): User identifier for the specified auth type // apiKey (string): API key to validate the specified app // appTypeIdentifier (string): identifier of the app type/client being used // orgID (string): ID of the organization being used // Returns: // canSignIn (bool): valid when error is nil - CanSignIn(authenticationType string, userIdentifier string, apiKey string, appTypeIdentifier string, orgID string) (bool, error) + CanSignIn(identifierJSON string, apiKey string, appTypeIdentifier string, orgID string, authenticationType *string, userIdentifier *string) (bool, error) //CanLink checks if a user can link a new auth type - //The authentication method must be one of the supported for the application. // Input: - // authenticationType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") // userIdentifier (string): User identifier for the specified auth type // apiKey (string): API key to validate the specified app // appTypeIdentifier (string): identifier of the app type/client being used // orgID (string): ID of the organization being used // Returns: // canLink (bool): valid when error is nil - CanLink(authenticationType string, userIdentifier string, apiKey string, appTypeIdentifier string, orgID string) (bool, error) + CanLink(identifierJSON string, apiKey string, appTypeIdentifier string, orgID string, authenticationType *string, userIdentifier *string) (bool, error) + + //SignInOptions returns the identifiers and auth types that may be used to sign in to an account + // Input: + // userIdentifier (string): User identifier for the specified auth type + // apiKey (string): API key to validate the specified app + // appTypeIdentifier (string): identifier of the app type/client being used + // orgID (string): ID of the organization being used + // Returns: + // identifiers ([]model.AccountIdentifier): account identifiers that may be used for sign-in + // authTypes ([]model.AccountAuthType): account auth types that may be used for sign-in + SignInOptions(identifierJSON string, apiKey string, appTypeIdentifier string, orgID string, authenticationType *string, userIdentifier *string, l *logs.Log) ([]model.AccountIdentifier, []model.AccountAuthType, error) //Refresh refreshes an access token using a refresh token // Input: @@ -203,7 +267,7 @@ type APIs interface { //GetLoginURL returns a pre-formatted login url for SSO providers // Input: - // authType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") + // authType (string): Name of the authentication method for provided creds (eg. "illinois_oidc") // appTypeIdentifier (string): Identifier of the app type/client that the user is logging in from // orgID (string): ID of the organization that the user is logging in // redirectURI (string): Registered redirect URI where client will receive response @@ -234,22 +298,22 @@ type APIs interface { LoginMFA(apiKey string, accountID string, sessionID string, identifier string, mfaType string, mfaCode string, state string, l *logs.Log) (*string, *model.LoginSession, error) //CreateAdminAccount creates an account for a new admin user - CreateAdminAccount(authenticationType string, appID string, orgID string, identifier string, profile model.Profile, privacy model.Privacy, username string, permissions []string, + CreateAdminAccount(authenticationType string, appID string, orgID string, identifierJSON string, profile model.Profile, privacy model.Privacy, permissions []string, roleIDs []string, groupIDs []string, scopes []string, creatorPermissions []string, clientVersion *string, l *logs.Log) (*model.Account, map[string]interface{}, error) //UpdateAdminAccount updates an existing user's account with new permissions, roles, and groups - UpdateAdminAccount(authenticationType string, appID string, orgID string, identifier string, permissions []string, roleIDs []string, + UpdateAdminAccount(authenticationType string, appID string, orgID string, identifierJSON string, permissions []string, roleIDs []string, groupIDs []string, scopes []string, updaterPermissions []string, l *logs.Log) (*model.Account, map[string]interface{}, error) //CreateAnonymousAccount creates a new anonymous account CreateAnonymousAccount(context storage.TransactionContext, appID string, orgID string, anonymousID string, preferences map[string]interface{}, systemConfigs map[string]interface{}, skipExistsCheck bool, l *logs.Log) (*model.Account, error) - //VerifyCredential verifies credential (checks the verification code in the credentials collection) - VerifyCredential(id string, verification string, l *logs.Log) error + //VerifyIdentifier verifies credential (checks the verification code in the credentials collection) + VerifyIdentifier(id string, verification string, l *logs.Log) (*model.AccountIdentifier, error) - //SendVerifyCredential sends verification code to identifier - SendVerifyCredential(authenticationType string, appTypeIdentifier string, orgID string, apiKey string, identifier string, l *logs.Log) error + //SendVerifyIdentifier sends verification code to identifier + SendVerifyIdentifier(appTypeIdentifier string, orgID string, apiKey string, identifierJSON string, l *logs.Log) error //UpdateCredential updates the credential object with the new value // Input: @@ -262,14 +326,14 @@ type APIs interface { //ForgotCredential initiate forgot credential process (generates a reset link and sends to the given identifier for email auth type) // Input: - // authenticationType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") - // identifier: identifier of the account auth type + // authenticationType (string): Name of the authentication method for provided creds (eg. "password") + // identifierJSON (string): JSON string of the user's identifier and the identifier code // appTypeIdentifier (string): Identifier of the app type/client that the user is logging in from // orgID (string): ID of the organization that the user is logging in // apiKey (string): API key to validate the specified app // Returns: // error: if any - ForgotCredential(authenticationType string, appTypeIdentifier string, orgID string, apiKey string, identifier string, l *logs.Log) error + ForgotCredential(authenticationType string, identifierJSON string, appTypeIdentifier string, orgID string, apiKey string, l *logs.Log) error //ResetForgotCredential resets forgot credential // Input: @@ -367,8 +431,8 @@ type APIs interface { //The authentication method must be one of the supported for the application. // Input: // accountID (string): ID of the account to link the creds to - // authenticationType (string): Name of the authentication method for provided creds (eg. "email", "username", "illinois_oidc") - // appTypeIdentifier (string): identifier of the app type/client that the user is logging in from + // authenticationType (string): Name of the authentication method for provided creds (eg. "password", "webauthn", "illinois_oidc") + // appTypeIdentifier (string): Identifier of the app type/client that the user is logging in from // creds (string): Credentials/JSON encoded credential structure defined for the specified auth type // params (string): JSON encoded params defined by specified auth type // l (*logs.Log): Log object pointer for request @@ -381,13 +445,17 @@ type APIs interface { //The authentication method must be one of the supported for the application. // Input: // accountID (string): ID of the account to unlink creds from - // authenticationType (string): Name of the authentication method of account auth type to unlink - // appTypeIdentifier (string): Identifier of the app type/client that the user is logging in from - // identifier (string): Identifier of account auth type to unlink + // accountAuthTypeID (*string): Account auth type to unlink + // authenticationType (*string): Name of the authentication method of account auth type to unlink + // identifier (*string): Identifier to unlink // l (*logs.Log): Log object pointer for request // Returns: // account (*model.Account): account data after the operation - UnlinkAccountAuthType(accountID string, authenticationType string, appTypeIdentifier string, identifier string, l *logs.Log) (*model.Account, error) + UnlinkAccountAuthType(accountID string, accountAuthTypeID *string, authenticationType *string, identifier *string, admin bool, l *logs.Log) (*model.Account, error) + + LinkAccountIdentifier(accountID string, identifierJSON string, admin bool, l *logs.Log) (*string, *model.Account, error) + + UnlinkAccountIdentifier(accountID string, accountIdentifierID string, admin bool, l *logs.Log) (*model.Account, error) //InitializeSystemAccount initializes the first system account InitializeSystemAccount(context storage.TransactionContext, authType model.AuthType, appOrg model.ApplicationOrganization, allSystemPermission string, email string, password string, clientVersion string, l *logs.Log) (string, error) @@ -456,6 +524,9 @@ type Storage interface { PerformTransaction(func(context storage.TransactionContext) error) error + //Configs + FindConfig(configType string, appID string, orgID string) (*model.Config, error) + //AuthTypes FindAuthType(codeOrID string) (*model.AuthType, error) @@ -467,7 +538,6 @@ type Storage interface { UpdateLoginSession(context storage.TransactionContext, loginSession model.LoginSession) error DeleteLoginSession(context storage.TransactionContext, id string) error DeleteLoginSessionsByIDs(context storage.TransactionContext, ids []string) error - DeleteLoginSessionsByAccountAuthTypeID(context storage.TransactionContext, id string) error DeleteLoginSessionsByIdentifier(context storage.TransactionContext, identifier string) error //LoginsSessions - predefined queries for manage deletion logic @@ -475,8 +545,12 @@ type Storage interface { FindSessionsLazy(appID string, orgID string) ([]model.LoginSession, error) /// + //LoginStates + FindLoginState(appID string, orgID string, accountID *string, stateParams map[string]interface{}) (*model.LoginState, error) + InsertLoginState(loginState model.LoginState) error + //Accounts - FindAccount(context storage.TransactionContext, appOrgID string, authTypeID string, accountAuthTypeIdentifier string) (*model.Account, error) + FindAccount(context storage.TransactionContext, appOrgID string, code string, identifier string) (*model.Account, error) FindAccountByID(context storage.TransactionContext, id string) (*model.Account, error) FindAccountsByUsername(context storage.TransactionContext, appOrg *model.ApplicationOrganization, username string) ([]model.Account, error) InsertAccount(context storage.TransactionContext, account model.Account) (*model.Account, error) @@ -486,7 +560,7 @@ type Storage interface { //Profiles UpdateAccountProfile(context storage.TransactionContext, profile model.Profile) error - FindAccountProfiles(appID string, authTypeID string, accountAuthTypeIdentifier string) ([]model.Profile, error) + FindAccountProfiles(appID string, accountIdentifier string) ([]model.Profile, error) //ServiceAccounts FindServiceAccount(context storage.TransactionContext, accountID string, appID string, orgID string) (*model.ServiceAccount, error) @@ -502,13 +576,18 @@ type Storage interface { //AccountAuthTypes FindAccountByAuthTypeID(context storage.TransactionContext, id string) (*model.Account, error) - InsertAccountAuthType(item model.AccountAuthType) error - UpdateAccountAuthType(item model.AccountAuthType) error + FindAccountByCredentialID(context storage.TransactionContext, id string) (*model.Account, error) + InsertAccountAuthType(context storage.TransactionContext, item model.AccountAuthType) error + UpdateAccountAuthType(context storage.TransactionContext, item model.AccountAuthType) error DeleteAccountAuthType(context storage.TransactionContext, item model.AccountAuthType) error - //ExternalIDs - UpdateAccountExternalIDs(accountID string, externalIDs map[string]string) error - UpdateLoginSessionExternalIDs(accountID string, externalIDs map[string]string) error + //AccountIdentifiers + FindAccountByIdentifierID(context storage.TransactionContext, id string) (*model.Account, error) + InsertAccountIdentifier(context storage.TransactionContext, item model.AccountIdentifier) error + UpdateAccountIdentifier(context storage.TransactionContext, item model.AccountIdentifier) error + UpdateAccountIdentifiers(context storage.TransactionContext, accountID string, items []model.AccountIdentifier) error + DeleteAccountIdentifier(context storage.TransactionContext, item model.AccountIdentifier) error + DeleteExternalAccountIdentifiers(context storage.TransactionContext, aat model.AccountAuthType) error //Applications FindApplication(context storage.TransactionContext, ID string) (*model.Application, error) @@ -519,6 +598,7 @@ type Storage interface { //Credentials InsertCredential(context storage.TransactionContext, creds *model.Credential) error FindCredential(context storage.TransactionContext, ID string) (*model.Credential, error) + FindCredentials(context storage.TransactionContext, ids []string) ([]model.Credential, error) UpdateCredential(context storage.TransactionContext, creds *model.Credential) error UpdateCredentialValue(ID string, value map[string]interface{}) error DeleteCredential(context storage.TransactionContext, ID string) error @@ -601,3 +681,9 @@ type IdentityBuildingBlock interface { type Emailer interface { Send(toEmail string, subject string, body string, attachmentFilename *string) error } + +// PhoneVerifier is used by core to verify phone numbers +type PhoneVerifier interface { + StartVerification(phone string, data url.Values) error + CheckVerification(phone string, data url.Values) error +} diff --git a/core/auth/mfa_recovery.go b/core/auth/mfa_recovery.go index e00cd9c45..2daac5c49 100644 --- a/core/auth/mfa_recovery.go +++ b/core/auth/mfa_recovery.go @@ -18,7 +18,6 @@ import ( "core-building-block/core/model" "core-building-block/driven/storage" "core-building-block/utils" - "encoding/json" "time" "github.com/google/uuid" @@ -45,24 +44,23 @@ func (m *recoveryMfaImpl) verify(context storage.TransactionContext, mfa *model. return nil, errors.ErrorData(logutils.StatusMissing, "mfa params", nil) } - var codes []string - data, err := json.Marshal(mfa.Params["codes"]) + codes, err := utils.JSONConvert[[]string, interface{}](mfa.Params["codes"]) if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, "stored recovery codes", nil, err) + return nil, errors.WrapErrorAction(logutils.ActionParse, "stored recovery codes", nil, err) } - err = json.Unmarshal(data, &codes) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, "stored recovery codes", nil, err) + if codes == nil { + return nil, errors.ErrorData(logutils.StatusInvalid, "stored recovery codes", nil) } + recoveryCodes := *codes - if len(codes) == 0 { + if len(recoveryCodes) == 0 { message := "no valid codes" return &message, errors.ErrorData(logutils.StatusMissing, "recovery codes", nil) } - for i, rc := range codes { + for i, rc := range recoveryCodes { if code == rc { - mfa.Params["codes"] = append(codes[:i], codes[i+1:]...) + mfa.Params["codes"] = append(recoveryCodes[:i], recoveryCodes[i+1:]...) now := time.Now().UTC() mfa.DateUpdated = &now @@ -81,11 +79,7 @@ func (m *recoveryMfaImpl) verify(context storage.TransactionContext, mfa *model. func (m *recoveryMfaImpl) enroll(identifier string) (*model.MFAType, error) { codes := make([]string, numCodes) for i := 0; i < numCodes; i++ { - newCode, err := utils.GenerateRandomString(codeLength) - if err != nil { - return nil, errors.WrapErrorAction("generating", "recovery code", nil, err) - } - codes[i] = string(newCode) + codes[i] = string(utils.GenerateRandomString(codeLength)) } params := map[string]interface{}{ diff --git a/core/auth/service_static_token.go b/core/auth/service_static_token.go index 0c998effb..6fa529918 100644 --- a/core/auth/service_static_token.go +++ b/core/auth/service_static_token.go @@ -18,7 +18,6 @@ import ( "core-building-block/core/model" "core-building-block/utils" "encoding/base64" - "encoding/json" "time" "github.com/google/uuid" @@ -47,15 +46,9 @@ type staticTokenServiceAuthImpl struct { } func (s *staticTokenServiceAuthImpl) checkCredentials(_ *sigauth.Request, creds interface{}, params map[string]interface{}) ([]model.ServiceAccount, error) { - credsData, err := json.Marshal(creds) + tokenCreds, err := utils.JSONConvert[staticTokenCreds, interface{}](creds) if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, TypeStaticTokenCreds, nil, err) - } - - var tokenCreds staticTokenCreds - err = json.Unmarshal([]byte(credsData), &tokenCreds) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, TypeStaticTokenCreds, nil, err) + return nil, errors.WrapErrorAction(logutils.ActionParse, TypeStaticTokenCreds, nil, err) } validate := validator.New() @@ -95,10 +88,7 @@ func (s *staticTokenServiceAuthImpl) addCredentials(creds *model.ServiceAccountC return nil, errors.ErrorData(logutils.StatusMissing, model.TypeServiceAccountCredential, nil) } - token, err := s.auth.buildRefreshToken() - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionCreate, logutils.TypeToken, nil, err) - } + token := utils.GenerateRandomString(refreshTokenLength) creds.ID = uuid.NewString() creds.Secrets = map[string]interface{}{ diff --git a/core/interfaces.go b/core/interfaces.go index af4741977..078415ba6 100644 --- a/core/interfaces.go +++ b/core/interfaces.go @@ -167,7 +167,7 @@ type Storage interface { FindAccountByID(context storage.TransactionContext, id string) (*model.Account, error) FindAccounts(context storage.TransactionContext, limit *int, offset *int, appID string, orgID string, accountID *string, firstName *string, lastName *string, authType *string, - authTypeIdentifier *string, anonymous *bool, hasPermissions *bool, permissions []string, roleIDs []string, groupIDs []string) ([]model.Account, error) + identifier *string, anonymous *bool, hasPermissions *bool, permissions []string, roleIDs []string, groupIDs []string) ([]model.Account, error) FindPublicAccounts(context storage.TransactionContext, appID string, orgID string, limit *int, offset *int, search *string, firstName *string, lastName *string, username *string, followingID *string, followerID *string, userID string) ([]model.PublicAccount, error) FindAccountsByParams(searchParams map[string]interface{}, appID string, orgID string, limit int, offset int, allAccess bool, approvedKeys []string) ([]map[string]interface{}, error) @@ -202,8 +202,8 @@ type Storage interface { FindConfig(configType string, appID string, orgID string) (*model.Config, error) FindConfigByID(id string) (*model.Config, error) FindConfigs(configType *string) ([]model.Config, error) - InsertConfig(config model.Config) error - UpdateConfig(config model.Config) error + InsertConfig(context storage.TransactionContext, config model.Config) error + UpdateConfig(context storage.TransactionContext, config model.Config) error DeleteConfig(id string) error FindPermissionsByName(context storage.TransactionContext, names []string) ([]model.Permission, error) diff --git a/core/mocks/Storage.go b/core/mocks/Storage.go index 82ee9850f..dbbd9fefd 100644 --- a/core/mocks/Storage.go +++ b/core/mocks/Storage.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery v2.33.2. DO NOT EDIT. package mocks @@ -297,17 +297,17 @@ func (_m *Storage) FindAccountByID(context storage.TransactionContext, id string return r0, r1 } -// FindAccounts provides a mock function with given fields: context, limit, offset, appID, orgID, accountID, firstName, lastName, authType, authTypeIdentifier, anonymous, hasPermissions, permissions, roleIDs, groupIDs -func (_m *Storage) FindAccounts(context storage.TransactionContext, limit *int, offset *int, appID string, orgID string, accountID *string, firstName *string, lastName *string, authType *string, authTypeIdentifier *string, anonymous *bool, hasPermissions *bool, permissions []string, roleIDs []string, groupIDs []string) ([]model.Account, error) { - ret := _m.Called(context, limit, offset, appID, orgID, accountID, firstName, lastName, authType, authTypeIdentifier, anonymous, hasPermissions, permissions, roleIDs, groupIDs) +// FindAccounts provides a mock function with given fields: context, limit, offset, appID, orgID, accountID, firstName, lastName, authType, identifier, anonymous, hasPermissions, permissions, roleIDs, groupIDs +func (_m *Storage) FindAccounts(context storage.TransactionContext, limit *int, offset *int, appID string, orgID string, accountID *string, firstName *string, lastName *string, authType *string, identifier *string, anonymous *bool, hasPermissions *bool, permissions []string, roleIDs []string, groupIDs []string) ([]model.Account, error) { + ret := _m.Called(context, limit, offset, appID, orgID, accountID, firstName, lastName, authType, identifier, anonymous, hasPermissions, permissions, roleIDs, groupIDs) var r0 []model.Account var r1 error if rf, ok := ret.Get(0).(func(storage.TransactionContext, *int, *int, string, string, *string, *string, *string, *string, *string, *bool, *bool, []string, []string, []string) ([]model.Account, error)); ok { - return rf(context, limit, offset, appID, orgID, accountID, firstName, lastName, authType, authTypeIdentifier, anonymous, hasPermissions, permissions, roleIDs, groupIDs) + return rf(context, limit, offset, appID, orgID, accountID, firstName, lastName, authType, identifier, anonymous, hasPermissions, permissions, roleIDs, groupIDs) } if rf, ok := ret.Get(0).(func(storage.TransactionContext, *int, *int, string, string, *string, *string, *string, *string, *string, *bool, *bool, []string, []string, []string) []model.Account); ok { - r0 = rf(context, limit, offset, appID, orgID, accountID, firstName, lastName, authType, authTypeIdentifier, anonymous, hasPermissions, permissions, roleIDs, groupIDs) + r0 = rf(context, limit, offset, appID, orgID, accountID, firstName, lastName, authType, identifier, anonymous, hasPermissions, permissions, roleIDs, groupIDs) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]model.Account) @@ -315,7 +315,7 @@ func (_m *Storage) FindAccounts(context storage.TransactionContext, limit *int, } if rf, ok := ret.Get(1).(func(storage.TransactionContext, *int, *int, string, string, *string, *string, *string, *string, *string, *bool, *bool, []string, []string, []string) error); ok { - r1 = rf(context, limit, offset, appID, orgID, accountID, firstName, lastName, authType, authTypeIdentifier, anonymous, hasPermissions, permissions, roleIDs, groupIDs) + r1 = rf(context, limit, offset, appID, orgID, accountID, firstName, lastName, authType, identifier, anonymous, hasPermissions, permissions, roleIDs, groupIDs) } else { r1 = ret.Error(1) } @@ -1343,13 +1343,13 @@ func (_m *Storage) InsertAuthType(context storage.TransactionContext, authType m return r0, r1 } -// InsertConfig provides a mock function with given fields: config -func (_m *Storage) InsertConfig(config model.Config) error { - ret := _m.Called(config) +// InsertConfig provides a mock function with given fields: context, config +func (_m *Storage) InsertConfig(context storage.TransactionContext, config model.Config) error { + ret := _m.Called(context, config) var r0 error - if rf, ok := ret.Get(0).(func(model.Config) error); ok { - r0 = rf(config) + if rf, ok := ret.Get(0).(func(storage.TransactionContext, model.Config) error); ok { + r0 = rf(context, config) } else { r0 = ret.Error(0) } @@ -1640,13 +1640,13 @@ func (_m *Storage) UpdateAuthTypes(ID string, code string, description string, i return r0 } -// UpdateConfig provides a mock function with given fields: config -func (_m *Storage) UpdateConfig(config model.Config) error { - ret := _m.Called(config) +// UpdateConfig provides a mock function with given fields: context, config +func (_m *Storage) UpdateConfig(context storage.TransactionContext, config model.Config) error { + ret := _m.Called(context, config) var r0 error - if rf, ok := ret.Get(0).(func(model.Config) error); ok { - r0 = rf(config) + if rf, ok := ret.Get(0).(func(storage.TransactionContext, model.Config) error); ok { + r0 = rf(context, config) } else { r0 = ret.Error(0) } @@ -1682,13 +1682,12 @@ func (_m *Storage) UpdatePermission(item model.Permission) error { return r0 } -type mockConstructorTestingTNewStorage interface { +// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewStorage(t interface { mock.TestingT Cleanup(func()) -} - -// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewStorage(t mockConstructorTestingTNewStorage) *Storage { +}) *Storage { mock := &Storage{} mock.Mock.Test(t) diff --git a/core/model/application.go b/core/model/application.go index 1c10ff912..6b3549985 100644 --- a/core/model/application.go +++ b/core/model/application.go @@ -349,18 +349,19 @@ func (ao ApplicationOrganization) FindIdentityProviderSetting(identityProviderID return nil } -// IsAuthTypeSupported checks if an auth type is supported for application type -func (ao ApplicationOrganization) IsAuthTypeSupported(appType ApplicationType, authType AuthType) bool { +// FindSupportedAuthType finds a supported auth type for application type +func (ao ApplicationOrganization) FindSupportedAuthType(appType ApplicationType, authType AuthType) *SupportedAuthType { for _, sat := range ao.SupportedAuthTypes { if sat.AppTypeID == appType.ID { for _, at := range sat.SupportedAuthTypes { if at.AuthTypeID == authType.ID { - return true + at.AuthType = authType + return &at } } } } - return false + return nil } // IdentityProviderSetting represents identity provider setting for an organization in an application @@ -376,8 +377,10 @@ func (ao ApplicationOrganization) IsAuthTypeSupported(appType ApplicationType, a type IdentityProviderSetting struct { IdentityProviderID string `bson:"identity_provider_id"` - UserIdentifierField string `bson:"user_identifier_field"` - ExternalIDFields map[string]string `bson:"external_id_fields"` + UserIdentifierField string `bson:"user_identifier_field"` + ExternalIDFields map[string]string `bson:"external_id_fields"` + SensitiveExternalIDs []string `bson:"sensitive_external_ids"` + IsEmailVerified bool `bson:"is_email_verified"` FirstNameField string `bson:"first_name_field"` MiddleNameField string `bson:"middle_name_field"` @@ -457,7 +460,8 @@ type AuthTypesSupport struct { // SupportedAuthType represents a supported auth type type SupportedAuthType struct { AuthTypeID string `bson:"auth_type_id"` - Params map[string]interface{} `bson:"params"` + Params map[string]interface{} `bson:"params,omitempty"` + AuthType AuthType `bson:"-"` } // ApplicationConfig represents app configs diff --git a/core/model/auth.go b/core/model/auth.go index 03f59d03c..ba1505561 100644 --- a/core/model/auth.go +++ b/core/model/auth.go @@ -27,6 +27,8 @@ import ( const ( //TypeLoginSession login session type TypeLoginSession logutils.MessageDataType = "login session" + //TypeLoginState login state type + TypeLoginState logutils.MessageDataType = "login state" //TypeAuthType auth type TypeAuthType logutils.MessageDataType = "auth type" //TypeIdentityProvider identity provider type @@ -90,9 +92,8 @@ type LoginSession struct { Anonymous bool - Identifier string //it is the account id(anonymous id for anonymous logins) - ExternalIDs map[string]string - AccountAuthType *AccountAuthType //it may be nil for anonymous logins + Identifier string //it is the account id(anonymous id for anonymous logins) + Account *Account //it may be nil for anonymous logins Device *Device @@ -220,6 +221,18 @@ func (ls LoginSession) LogInfo() string { ls.StateExpires, ls.MfaAttempts, ls.DateRefreshed, ls.DateUpdated, ls.DateCreated) } +// LoginState represents a state variable generated during a login request and used to complete that request (by generating a LoginSession) +type LoginState struct { + ID string `bson:"_id"` + AppID string `bson:"app_id"` + OrgID string `bson:"org_id"` + + AccountID *string `bson:"account_id"` + + State map[string]interface{} `bson:"state"` + DateCreated time.Time `bson:"date_created"` +} + // APIKey represents an API key entity type APIKey struct { ID string `json:"id" bson:"_id"` @@ -239,6 +252,7 @@ type AuthType struct { UseCredentials bool `bson:"use_credentials"` //says if the auth type uses credentials IgnoreMFA bool `bson:"ignore_mfa"` //says if login using this auth type may bypass account MFA Params map[string]interface{} `bson:"params"` + Aliases []string `bson:"aliases,omitempty"` } // IdentityProvider represents identity provider entity diff --git a/core/model/config.go b/core/model/config.go index b6041236b..770ff630d 100644 --- a/core/model/config.go +++ b/core/model/config.go @@ -29,11 +29,15 @@ const ( TypeConfigData logutils.MessageDataType = "config data" // TypeEnvConfigData env configs type TypeEnvConfigData logutils.MessageDataType = "env config data" + // TypeAuthConfigData auth configs type + TypeAuthConfigData logutils.MessageDataType = "auth config data" //TypeOrganizationConfig ... TypeOrganizationConfig logutils.MessageDataType = "org config" // ConfigTypeEnv is the Config type for EnvConfigData ConfigTypeEnv string = "env" + // ConfigTypeAuth is the Config type for AuthConfigData + ConfigTypeAuth string = "auth" ) // Config contains generic configs @@ -54,6 +58,13 @@ type EnvConfigData struct { CORSAllowedHeaders []string `json:"cors_allowed_headers" bson:"cors_allowed_headers"` } +// AuthConfigData contains auth configs for this service +type AuthConfigData struct { + EmailShouldVerify *bool `json:"email_should_verify" bson:"email_should_verify"` + EmailVerifyWaitTime *int `json:"email_verify_wait_time" bson:"email_verify_wait_time"` + EmailVerifyExpiry *int `json:"email_verify_expiry" bson:"email_verify_expiry"` +} + // GetConfigData returns a pointer to the given config's Data as the given type T func GetConfigData[T ConfigData](c Config) (*T, error) { if data, ok := c.Data.(T); ok { @@ -64,7 +75,7 @@ func GetConfigData[T ConfigData](c Config) (*T, error) { // ConfigData represents any set of data that may be stored in a config type ConfigData interface { - EnvConfigData | map[string]interface{} + EnvConfigData | AuthConfigData | map[string]interface{} } // OrganizationConfig represents configuration for an organization diff --git a/core/model/user.go b/core/model/user.go index e96973c1d..a38598441 100644 --- a/core/model/user.go +++ b/core/model/user.go @@ -33,6 +33,8 @@ const ( TypeAccountSystemConfigs logutils.MessageDataType = "account system configs" //TypeAccountAuthType account auth type TypeAccountAuthType logutils.MessageDataType = "account auth type" + //TypeAccountIdentifier account identifier + TypeAccountIdentifier logutils.MessageDataType = "account identifier" //TypeAccountPermissions account permissions TypeAccountPermissions logutils.MessageDataType = "account permissions" //TypeAccountRoles account roles @@ -76,12 +78,11 @@ type Account struct { Groups []AccountGroup Scopes []string - AuthTypes []AccountAuthType + Identifiers []AccountIdentifier + AuthTypes []AccountAuthType MFATypes []MFAType - Username string - ExternalIDs map[string]string Preferences map[string]interface{} SystemConfigs map[string]interface{} Profile Profile //one account has one profile, one profile can be shared between many accounts @@ -111,21 +112,81 @@ func (a Account) GetAccountAuthTypeByID(ID string) *AccountAuthType { return nil } -// GetAccountAuthType finds account auth type -func (a Account) GetAccountAuthType(authTypeID string, identifier string) *AccountAuthType { +// GetAccountAuthTypes finds account auth types +func (a Account) GetAccountAuthTypes(authTypeIDorCode string) []AccountAuthType { + authTypes := make([]AccountAuthType, 0) for _, aat := range a.AuthTypes { - if aat.AuthType.ID == authTypeID && aat.Identifier == identifier { + if aat.SupportedAuthType.AuthType.ID == authTypeIDorCode || aat.SupportedAuthType.AuthType.Code == authTypeIDorCode { aat.Account = a - return &aat + authTypes = append(authTypes, aat) } } - return nil + return authTypes } -// SortAccountAuthTypes sorts account auth types by matching the given uid -func (a Account) SortAccountAuthTypes(uid string) { +// SortAccountAuthTypes sorts account auth types by matching the given id +func (a Account) SortAccountAuthTypes(id string, authType string) { sort.Slice(a.AuthTypes, func(i, _ int) bool { - return a.AuthTypes[i].Identifier == uid + return (id != "" && a.AuthTypes[i].ID == id) || (authType != "" && a.AuthTypes[i].SupportedAuthType.AuthType.Code == authType) + }) +} + +// GetAccountIdentifier finds account identifier +func (a Account) GetAccountIdentifier(code string, identifier string) *AccountIdentifier { + for _, id := range a.Identifiers { + if code != "" && id.Code == code && identifier != "" && id.Identifier == identifier { + id.Account = a + return &id + } + if code != "" && id.Code == code { + id.Account = a + return &id + } + if identifier != "" && id.Identifier == identifier { + id.Account = a + return &id + } + } + return nil +} + +// GetAccountIdentifierByID finds account identifier by its ID +func (a Account) GetAccountIdentifierByID(id string) *AccountIdentifier { + for _, ai := range a.Identifiers { + if ai.ID == id { + ai.Account = a + return &ai + } + } + return nil +} + +// GetVerifiedAccountIdentifiers returns a list of only verified identifiers for this account +func (a Account) GetVerifiedAccountIdentifiers() []AccountIdentifier { + identifiers := make([]AccountIdentifier, 0) + for _, id := range a.Identifiers { + if id.Verified { + identifiers = append(identifiers, id) + } + } + return identifiers +} + +// GetExternalAccountIdentifiers returns a list of only external identifiers for this account +func (a Account) GetExternalAccountIdentifiers() []AccountIdentifier { + identifiers := make([]AccountIdentifier, 0) + for _, id := range a.Identifiers { + if id.AccountAuthTypeID != nil { + identifiers = append(identifiers, id) + } + } + return identifiers +} + +// SortAccountIdentifiers sorts account identifiers by matching the given identifier +func (a Account) SortAccountIdentifiers(identifier string) { + sort.Slice(a.Identifiers, func(i, _ int) bool { + return a.Identifiers[i].Identifier == identifier }) } @@ -334,56 +395,30 @@ func AccountGroupsFromAppOrgGroups(items []AppOrgGroup, active bool, adminSet bo type AccountAuthType struct { ID string - AuthType AuthType //one of the supported auth type - Account Account + SupportedAuthType SupportedAuthType //one of the supported auth type + Account Account - Identifier string - Params map[string]interface{} + Params map[string]interface{} Credential *Credential //this can be nil as the external auth types authenticates the users outside the system - Active bool - Unverified bool - Linked bool + Active bool DateCreated time.Time DateUpdated *time.Time } -// SetUnverified sets the Unverified flag to value in the account auth type itself and the appropriate account auth type within the account member -func (aat *AccountAuthType) SetUnverified(value bool) { - if aat == nil { - return - } - - aat.Unverified = false - for i := 0; i < len(aat.Account.AuthTypes); i++ { - if aat.Account.AuthTypes[i].ID == aat.ID { - aat.Account.AuthTypes[i].Unverified = false - } - } -} - // Equals checks if two account auth types are equal func (aat *AccountAuthType) Equals(other AccountAuthType) bool { - if aat.Identifier != other.Identifier { - return false - } if aat.Account.ID != other.Account.ID { return false } - if aat.AuthType.Code != other.AuthType.Code { + if aat.SupportedAuthType.AuthType.Code != other.SupportedAuthType.AuthType.Code { return false } if aat.Active != other.Active { return false } - if aat.Unverified != other.Unverified { - return false - } - if aat.Linked != other.Linked { - return false - } if !utils.DeepEqual(aat.Params, other.Params) { return false } @@ -399,13 +434,66 @@ func (aat *AccountAuthType) Equals(other AccountAuthType) bool { return true } +// AccountIdentifier represents account identifiers +type AccountIdentifier struct { + ID string + Code string + Identifier string + + Verified bool + Linked bool + Sensitive bool + + AccountAuthTypeID *string + Primary *bool + + Account Account + + VerificationCode *string + VerificationExpiry *time.Time + + DateCreated time.Time + DateUpdated *time.Time +} + +// SetVerified sets the Verified flag to value in the account auth type itself and the appropriate account auth type within the account member +func (ai *AccountIdentifier) SetVerified(value bool) { + if ai == nil { + return + } + + ai.Verified = value + for i := 0; i < len(ai.Account.Identifiers); i++ { + if ai.Account.Identifiers[i].Identifier == ai.Identifier { + ai.Account.Identifiers[i].Verified = value + } + } +} + +// Equals checks if two account identifiers are equal +func (ai *AccountIdentifier) Equals(other AccountIdentifier) bool { + if ai.Identifier != other.Identifier { + return false + } + if ai.Account.ID != other.Account.ID { + return false + } + if ai.Verified != other.Verified { + return false + } + if ai.Linked != other.Linked { + return false + } + + return true +} + // Credential represents a credential for account auth type/s type Credential struct { ID string AuthType AuthType - AccountsAuthTypes []AccountAuthType //one credential can be used for more than one account auth type - Verified bool + AccountsAuthTypes []AccountAuthType //one credential can be used for more than one account auth type Value map[string]interface{} //credential value DateCreated time.Time @@ -435,8 +523,6 @@ type Profile struct { PhotoURL string FirstName string LastName string - Email string - Phone string BirthYear int16 Address string ZipCode string @@ -469,12 +555,6 @@ func (p Profile) Merge(src Profile) Profile { if src.LastName != "" { p.LastName = src.LastName } - if src.Email != "" { - p.Email = src.Email - } - if src.Phone != "" { - p.Phone = src.Phone - } if src.Address != "" { p.Address = src.Address } @@ -518,14 +598,6 @@ func ProfileFromMap(profileMap map[string]interface{}) Profile { if typeVal, ok := val.(string); ok { profile.LastName = typeVal } - } else if key == "email" { - if typeVal, ok := val.(string); ok { - profile.Email = typeVal - } - } else if key == "phone" { - if typeVal, ok := val.(string); ok { - profile.Phone = typeVal - } } else if key == "birth_year" { if typeVal, ok := val.(int16); ok { profile.BirthYear = typeVal @@ -548,7 +620,7 @@ func ProfileFromMap(profileMap map[string]interface{}) Profile { } } else if key == "photo_url" { if typeVal, ok := val.(string); ok { - profile.Phone = typeVal + profile.PhotoURL = typeVal } } else { profile.UnstructuredProperties[key] = val @@ -573,8 +645,10 @@ type Device struct { // ExternalSystemUser represents external system user type ExternalSystemUser struct { - Identifier string `json:"identifier" bson:"identifier"` //this is the identifier used in our system to map the user - ExternalIDs map[string]string `json:"external_ids" bson:"external_ids"` + Identifier string `json:"identifier" bson:"identifier"` //this is the identifier used in our system to map the user + ExternalIDs map[string]string `json:"external_ids" bson:"external_ids"` + SensitiveExternalIDs []string `json:"sensitive_external_ids" bson:"sensitive_external_ids"` + IsEmailVerified bool `json:"is_email_verified" bson:"is_email_verified"` //these are common fields which should be popuated by the external system FirstName string `json:"first_name" bson:"first_name"` diff --git a/driven/phoneverifier/twilio_adapter.go b/driven/phoneverifier/twilio_adapter.go new file mode 100644 index 000000000..532f8f0d5 --- /dev/null +++ b/driven/phoneverifier/twilio_adapter.go @@ -0,0 +1,191 @@ +// Copyright 2023 Board of Trustees of the University of Illinois. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package phoneverifier + +import ( + "context" + "core-building-block/utils" + "encoding/base64" + "encoding/json" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/rokwire/logging-library-go/v2/errors" + "github.com/rokwire/logging-library-go/v2/logutils" +) + +const ( + // TypeTwilio refers to the twilio specific phone identifier type + TypeTwilio logutils.MessageDataType = "twilio_phone" + + servicesPathPart = "https://verify.twilio.com/v2/Services" + verificationsPathPart = "Verifications" + verificationCheckPart = "VerificationCheck" + typeVerifyServiceID logutils.MessageDataType = "phone verification service id" + typeVerifyServiceToken logutils.MessageDataType = "phone verification service token" + typeVerificationResponse logutils.MessageDataType = "phone verification response" + typeVerificationStatus logutils.MessageDataType = "phone verification staus" + typeVerificationSID logutils.MessageDataType = "phone verification sid" +) + +// TwilioAdapter implements the Emailer interface +type TwilioAdapter struct { + accountSID string + token string + serviceSID string + httpClient *http.Client +} + +type verifyPhoneResponse struct { + Status string `json:"status"` + Payee interface{} `json:"payee"` + DateUpdated time.Time `json:"date_updated"` + AccountSid string `json:"account_sid"` + To string `json:"to"` + Amount interface{} `json:"amount"` + Valid bool `json:"valid"` + URL string `json:"url"` + Sid string `json:"sid"` + DateCreated time.Time `json:"date_created"` + ServiceSid string `json:"service_sid"` + Channel string `json:"channel"` +} + +type checkStatusResponse struct { + Sid string `json:"sid"` + ServiceSid string `json:"service_sid"` + AccountSid string `json:"account_sid"` + To string `json:"to" validate:"required"` + Channel string `json:"channel"` + Status string `json:"status"` + Amount interface{} `json:"amount"` + Payee interface{} `json:"payee"` + DateCreated time.Time `json:"date_created"` + DateUpdated time.Time `json:"date_updated"` +} + +// StartVerification begins the phone verification process +func (a *TwilioAdapter) StartVerification(phone string, data url.Values) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + body, err := a.makeRequest(ctx, "POST", servicesPathPart+"/"+a.serviceSID+"/"+verificationsPathPart, data, a.accountSID, a.token) + if err != nil { + return errors.WrapErrorAction(logutils.ActionSend, logutils.TypeRequest, &logutils.FieldArgs{"verification params": data}, err) + } + + var verifyResult verifyPhoneResponse + err = json.Unmarshal(body, &verifyResult) + if err != nil { + return errors.WrapErrorAction(logutils.ActionUnmarshal, typeVerificationResponse, nil, err) + } + + if verifyResult.To != phone { + return errors.ErrorData(logutils.StatusInvalid, logutils.TypeString, &logutils.FieldArgs{"expected phone": phone, "actual phone": verifyResult.To}) + } + if verifyResult.Status != "pending" { + return errors.ErrorData(logutils.StatusInvalid, typeVerificationStatus, &logutils.FieldArgs{"expected pending, actual:": verifyResult.Status}) + } + if verifyResult.Sid == "" { + return errors.ErrorData(logutils.StatusMissing, typeVerificationSID, nil) + } + + return nil +} + +// CheckVerification verifies the code sent to a user's phone to finish verification +func (a *TwilioAdapter) CheckVerification(phone string, data url.Values) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + body, err := a.makeRequest(ctx, "POST", servicesPathPart+"/"+a.serviceSID+"/"+verificationCheckPart, data, a.accountSID, a.token) + if err != nil { + return errors.WrapErrorAction(logutils.ActionSend, logutils.TypeRequest, nil, err) + } + + var checkResponse checkStatusResponse + err = json.Unmarshal(body, &checkResponse) + if err != nil { + return errors.WrapErrorAction(logutils.ActionUnmarshal, typeVerificationResponse, nil, err) + } + + if checkResponse.To != phone { + return errors.ErrorData(logutils.StatusInvalid, logutils.TypeString, &logutils.FieldArgs{"expected phone": phone, "actual phone": checkResponse.To}) + } + if checkResponse.Status != "approved" { + return errors.ErrorData(logutils.StatusInvalid, typeVerificationStatus, &logutils.FieldArgs{"expected approved, actual:": checkResponse.Status}).SetStatus(utils.ErrorStatusInvalid) + } + + return nil +} + +func (a *TwilioAdapter) makeRequest(ctx context.Context, method string, pathPart string, data url.Values, user string, token string) ([]byte, error) { + rb := new(strings.Reader) + logAction := logutils.ActionSend + + if data != nil && (method == "POST" || method == "PUT") { + rb = strings.NewReader(data.Encode()) + } + if method == "GET" && data != nil { + pathPart = pathPart + "?" + data.Encode() + logAction = logutils.ActionRead + } + + req, err := http.NewRequest(method, pathPart, rb) + if err != nil { + return nil, errors.WrapErrorAction(logAction, logutils.TypeRequest, &logutils.FieldArgs{"path": pathPart}, err) + } + + if token != "" { + req.Header.Add("Authorization", "Basic "+a.basicAuth(user, token)) + } + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + resp, err := a.httpClient.Do(req) + if err != nil { + return nil, errors.WrapErrorAction(logAction, logutils.TypeRequest, nil, err) + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionRead, logutils.TypeRequestBody, nil, err) + } + if resp.StatusCode != 200 && resp.StatusCode != 201 { + return nil, errors.ErrorData(logutils.StatusInvalid, logutils.TypeResponse, &logutils.FieldArgs{"status_code": resp.StatusCode, "error": string(body)}) + } + return body, nil +} + +func (a *TwilioAdapter) basicAuth(username, password string) string { + auth := username + ":" + password + return base64.StdEncoding.EncodeToString([]byte(auth)) +} + +// NewTwilioAdapter creates a new twilio phone verifier adapter instance +func NewTwilioAdapter(accountSID string, token string, serviceSID string) (*TwilioAdapter, error) { + if accountSID == "" { + return nil, errors.ErrorData(logutils.StatusMissing, typeVerifyServiceID, nil) + } + if token == "" { + return nil, errors.ErrorData(logutils.StatusMissing, typeVerifyServiceToken, nil) + } + + client := &http.Client{} + return &TwilioAdapter{accountSID: accountSID, token: token, serviceSID: serviceSID, httpClient: client}, nil +} diff --git a/driven/profilebb/adapter.go b/driven/profilebb/adapter.go index e06399d3e..58c684039 100644 --- a/driven/profilebb/adapter.go +++ b/driven/profilebb/adapter.go @@ -173,10 +173,9 @@ func (a *Adapter) GetProfileBBData(queryParams map[string]string, l *logs.Log) ( l.WarnError(logutils.MessageActionError(logutils.ActionParse, "date created", nil), err) dateCreated = &now } - existingProfile := model.Profile{FirstName: profileData.PII.FirstName, LastName: profileData.PII.LastName, - Email: profileData.PII.Email, Phone: profileData.PII.Phone, BirthYear: profileData.PII.BirthYear, - Address: profileData.PII.Address, ZipCode: profileData.PII.ZipCode, State: profileData.PII.State, - Country: profileData.PII.Country, DateCreated: *dateCreated, DateUpdated: &now} + existingProfile := model.Profile{FirstName: profileData.PII.FirstName, LastName: profileData.PII.LastName, BirthYear: profileData.PII.BirthYear, + Address: profileData.PII.Address, ZipCode: profileData.PII.ZipCode, State: profileData.PII.State, Country: profileData.PII.Country, + DateCreated: *dateCreated, DateUpdated: &now} preferences := a.reformatPreferences(profileData.NonPII, l) diff --git a/driven/storage/adapter.go b/driven/storage/adapter.go index 2088075ad..6e4b6037b 100644 --- a/driven/storage/adapter.go +++ b/driven/storage/adapter.go @@ -121,6 +121,11 @@ func (sa *Adapter) Start() error { return errors.WrapErrorAction(logutils.ActionCache, model.TypeConfig, nil, err) } + err = sa.migrateAuthTypes() + if err != nil { + return errors.WrapErrorAction("migrating", model.TypeAuthType, nil, err) + } + return err } @@ -454,6 +459,9 @@ func (sa *Adapter) setCachedAuthTypes(authProviders []model.AuthType) { func (sa *Adapter) setCachedAuthType(authType model.AuthType) { sa.cachedAuthTypes.Store(authType.ID, authType) sa.cachedAuthTypes.Store(authType.Code, authType) + for _, alias := range authType.Aliases { + sa.cachedAuthTypes.Store(alias, authType) + } } func (sa *Adapter) getCachedAuthType(key string) (*model.AuthType, error) { @@ -736,6 +744,8 @@ func (sa *Adapter) setCachedConfigs(configs []model.Config) { switch config.Type { case model.ConfigTypeEnv: err = parseConfigsData[model.EnvConfigData](&config) + case model.ConfigTypeAuth: + err = parseConfigsData[model.AuthConfigData](&config) default: err = parseConfigsData[map[string]interface{}](&config) } @@ -1034,7 +1044,7 @@ func (sa *Adapter) buildLoginSession(context TransactionContext, ls *loginSessio //account - from storage var account *model.Account var err error - if ls.AccountAuthTypeID != nil { + if !ls.Anonymous { account, err = sa.FindAccountByID(context, ls.Identifier) if err != nil { return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, &logutils.FieldArgs{"_id": ls.Identifier}, err) @@ -1121,11 +1131,6 @@ func (sa *Adapter) DeleteLoginSessionByID(context TransactionContext, id string) return sa.deleteLoginSessions(context, "_id", id, true) } -// DeleteLoginSessionsByAccountAuthTypeID deletes login sessions by account auth type ID -func (sa *Adapter) DeleteLoginSessionsByAccountAuthTypeID(context TransactionContext, id string) error { - return sa.deleteLoginSessions(context, "account_auth_type_id", id, false) -} - func (sa *Adapter) deleteLoginSessions(context TransactionContext, key string, value string, checkDeletedCount bool) error { filter := bson.M{key: value} @@ -1208,11 +1213,49 @@ func (sa *Adapter) FindSessionsLazy(appID string, orgID string) ([]model.LoginSe return sessions, nil } -// FindAccount finds an account for app, org, auth type and account auth type identifier -func (sa *Adapter) FindAccount(context TransactionContext, appOrgID string, authTypeID string, accountAuthTypeIdentifier string) (*model.Account, error) { - filter := bson.D{primitive.E{Key: "app_org_id", Value: appOrgID}, - primitive.E{Key: "auth_types.auth_type_id", Value: authTypeID}, - primitive.E{Key: "auth_types.identifier", Value: accountAuthTypeIdentifier}} +// FindLoginState finds a saved login state +func (sa *Adapter) FindLoginState(appID string, orgID string, accountID *string, stateParams map[string]interface{}) (*model.LoginState, error) { + filter := bson.M{"app_id": appID, "org_id": orgID} + + if accountID != nil { + filter["account_id"] = *accountID + } + for k, v := range stateParams { + filter["state."+k] = v + } + + var states []model.LoginState + err := sa.db.loginStates.Find(filter, &states, nil) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeLoginState, nil, err) + } + if len(states) == 0 { + //not found + return nil, nil + } + + loginState := states[0] + return &loginState, nil +} + +// InsertLoginState inserts a new login state +func (sa *Adapter) InsertLoginState(loginState model.LoginState) error { + _, err := sa.db.loginStates.InsertOne(loginState) + if err != nil { + return errors.WrapErrorAction(logutils.ActionInsert, model.TypeLoginState, nil, err) + } + + return nil +} + +// FindAccount finds an account for app, org, auth type and identifier +func (sa *Adapter) FindAccount(context TransactionContext, appOrgID string, code string, identifier string) (*model.Account, error) { + filter := bson.M{"app_org_id": appOrgID, "identifiers": bson.M{ + "$elemMatch": bson.M{ + "code": code, + "identifier": identifier, + }, + }} var accounts []account err := sa.db.accounts.FindWithContext(context, filter, &accounts, nil) if err != nil { @@ -1233,13 +1276,13 @@ func (sa *Adapter) FindAccount(context TransactionContext, appOrgID string, auth return nil, errors.ErrorData(logutils.StatusMissing, model.TypeApplicationOrganization, nil) } - modelAccount := accountFromStorage(account, *appOrg) + modelAccount := accountFromStorage(account, *appOrg, sa) return &modelAccount, nil } // FindAccounts finds accounts func (sa *Adapter) FindAccounts(context TransactionContext, limit *int, offset *int, appID string, orgID string, accountID *string, firstName *string, lastName *string, authType *string, - authTypeIdentifier *string, anonymous *bool, hasPermissions *bool, permissions []string, roleIDs []string, groupIDs []string) ([]model.Account, error) { + identifier *string, anonymous *bool, hasPermissions *bool, permissions []string, roleIDs []string, groupIDs []string) ([]model.Account, error) { //find app org id appOrg, err := sa.getCachedApplicationOrganization(appID, orgID) if err != nil { @@ -1273,8 +1316,8 @@ func (sa *Adapter) FindAccounts(context TransactionContext, limit *int, offset * } filter = append(filter, primitive.E{Key: "auth_types.auth_type_id", Value: cachedAuthType.ID}) } - if authTypeIdentifier != nil { - filter = append(filter, primitive.E{Key: "auth_types.identifier", Value: *authTypeIdentifier}) + if identifier != nil { + filter = append(filter, primitive.E{Key: "identifiers.identifier", Value: *identifier}) } if anonymous != nil { filter = append(filter, primitive.E{Key: "anonymous", Value: *anonymous}) @@ -1326,7 +1369,7 @@ func (sa *Adapter) FindAccounts(context TransactionContext, limit *int, offset * return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, nil, err) } - accounts := accountsFromStorage(list, *appOrg) + accounts := accountsFromStorage(list, *appOrg, sa) return accounts, nil } @@ -1367,7 +1410,7 @@ func (sa *Adapter) FindPublicAccounts(context TransactionContext, appID string, } regexFilter := bson.M{ "$or": []bson.M{ - {"username": primitive.Regex{Pattern: searchStr, Options: "i"}}, + {"identifiers": bson.M{"$elemMatch": bson.M{"code": "username", "identifier": primitive.Regex{Pattern: searchStr, Options: "i"}}}}, {"profile.first_name": primitive.Regex{Pattern: searchStr, Options: "i"}}, {"profile.last_name": primitive.Regex{Pattern: searchStr, Options: "i"}}, }, @@ -1399,7 +1442,7 @@ func (sa *Adapter) FindPublicAccounts(context TransactionContext, appID string, } if username != nil { usernameStr = *username - pipeline = append(pipeline, bson.M{"$match": bson.M{"username": *username}}) + pipeline = append(pipeline, bson.M{"$match": bson.M{"identifiers": bson.M{"$elemMatch": bson.M{"code": "username", "identifier": *username}}}}) } if followingID != nil { @@ -1431,9 +1474,17 @@ func (sa *Adapter) FindPublicAccounts(context TransactionContext, appID string, var publicAccounts []model.PublicAccount for _, account := range accounts { + username := "" + for _, id := range account.Identifiers { + if id.Code == "username" { + username = id.Identifier + break + } + } + publicAccounts = append(publicAccounts, model.PublicAccount{ ID: account.ID, - Username: account.Username, + Username: username, FirstName: account.Profile.FirstName, LastName: account.Profile.LastName, Verified: account.Verified, @@ -1527,17 +1578,17 @@ func (sa *Adapter) FindAccountsByAccountID(context TransactionContext, appID str if err != nil { return nil, err } - accounts := accountsFromStorage(accountResult, *appOrg) + accounts := accountsFromStorage(accountResult, *appOrg, sa) return accounts, nil } -// FindAccountsByUsername finds accounts with a username for a given appOrg +// FindAccountsByUsername finds accounts by username for a given appOrg func (sa *Adapter) FindAccountsByUsername(context TransactionContext, appOrg *model.ApplicationOrganization, username string) ([]model.Account, error) { if appOrg == nil { return nil, errors.ErrorData(logutils.StatusMissing, model.TypeApplicationOrganization, nil) } - filter := bson.D{primitive.E{Key: "app_org_id", Value: appOrg.ID}, primitive.E{Key: "username", Value: username}} + filter := bson.D{primitive.E{Key: "app_org_id", Value: appOrg.ID}, primitive.E{Key: "identifiers", Value: bson.M{"$elemMatch": bson.M{"code": "username", "identifier": username}}}} var accountResult []account err := sa.db.accounts.Find(filter, &accountResult, nil) @@ -1548,7 +1599,7 @@ func (sa *Adapter) FindAccountsByUsername(context TransactionContext, appOrg *mo sa.logger.WarnWithFields("duplicate username", logutils.Fields{"number": len(accountResult), "app_org_id": appOrg.ID, "username": username}) } - accounts := accountsFromStorage(accountResult, *appOrg) + accounts := accountsFromStorage(accountResult, *appOrg, sa) return accounts, nil } @@ -1562,6 +1613,16 @@ func (sa *Adapter) FindAccountByAuthTypeID(context TransactionContext, id string return sa.findAccount(context, "auth_types.id", id) } +// FindAccountByCredentialID finds an account by auth type id +func (sa *Adapter) FindAccountByCredentialID(context TransactionContext, id string) (*model.Account, error) { + return sa.findAccount(context, "auth_types.credential_id", id) +} + +// FindAccountByIdentifierID finds an account by identifier id +func (sa *Adapter) FindAccountByIdentifierID(context TransactionContext, id string) (*model.Account, error) { + return sa.findAccount(context, "identifiers.id", id) +} + func (sa *Adapter) findAccount(context TransactionContext, key string, id string) (*model.Account, error) { account, err := sa.findStorageAccount(context, key, id) if err != nil { @@ -1581,7 +1642,7 @@ func (sa *Adapter) findAccount(context TransactionContext, key string, id string return nil, errors.ErrorData(logutils.StatusMissing, model.TypeApplicationOrganization, nil) } - modelAccount := accountFromStorage(*account, *appOrg) + modelAccount := accountFromStorage(*account, *appOrg, sa) return &modelAccount, nil } @@ -1976,12 +2037,15 @@ func (sa *Adapter) UpdateAccountUsername(context TransactionContext, accountID s filter := bson.D{primitive.E{Key: "_id", Value: accountID}} update := bson.D{ primitive.E{Key: "$set", Value: bson.D{ - primitive.E{Key: "username", Value: username}, + primitive.E{Key: "identifiers.$[id].identifier", Value: username}, primitive.E{Key: "date_updated", Value: time.Now().UTC()}, }}, } - res, err := sa.db.accounts.UpdateOneWithContext(context, filter, update, nil) + opts := options.UpdateOptions{} + arrayFilters := []interface{}{bson.M{"id.code": "username"}} + opts.SetArrayFilters(options.ArrayFilters{Filters: arrayFilters}) + res, err := sa.db.accounts.UpdateOneWithContext(context, filter, update, &opts) if err != nil { return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccount, &logutils.FieldArgs{"id": accountID}, err) } @@ -1992,7 +2056,7 @@ func (sa *Adapter) UpdateAccountUsername(context TransactionContext, accountID s return nil } -// UpdateAccountVerified updates an account's username +// UpdateAccountVerified updates an account's verified status func (sa *Adapter) UpdateAccountVerified(context TransactionContext, accountID string, appID string, orgID string, verified bool) error { appOrg, err := sa.FindApplicationOrganization(appID, orgID) if err != nil || appOrg == nil { @@ -2219,7 +2283,7 @@ func (sa *Adapter) UpdateAccountScopes(context TransactionContext, accountID str } // InsertAccountAuthType inserts am account auth type -func (sa *Adapter) InsertAccountAuthType(item model.AccountAuthType) error { +func (sa *Adapter) InsertAccountAuthType(context TransactionContext, item model.AccountAuthType) error { storageItem := accountAuthTypeToStorage(item) //3. first find the account record @@ -2228,80 +2292,42 @@ func (sa *Adapter) InsertAccountAuthType(item model.AccountAuthType) error { primitive.E{Key: "$push", Value: bson.D{ primitive.E{Key: "auth_types", Value: storageItem}, }}, + primitive.E{Key: "$set", Value: bson.D{ + primitive.E{Key: "date_updated", Value: time.Now().UTC()}, + }}, } - res, err := sa.db.accounts.UpdateOne(filter, update, nil) + res, err := sa.db.accounts.UpdateOneWithContext(context, filter, update, nil) if err != nil { return errors.WrapErrorAction(logutils.ActionInsert, model.TypeAccountAuthType, nil, err) } if res.ModifiedCount != 1 { - return errors.ErrorAction(logutils.ActionUpdate, model.TypeAccountAuthType, &logutils.FieldArgs{"unexpected modified count": res.ModifiedCount}) + return errors.ErrorAction(logutils.ActionUpdate, model.TypeAccount, &logutils.FieldArgs{"modified": res.ModifiedCount, "expected": 1}) } return nil } -// UpdateAccountAuthType updates account auth type -func (sa *Adapter) UpdateAccountAuthType(item model.AccountAuthType) error { - // transaction - err := sa.db.dbClient.UseSession(context.Background(), func(sessionContext mongo.SessionContext) error { - err := sessionContext.StartTransaction() - if err != nil { - sa.abortTransaction(sessionContext) - return errors.WrapErrorAction(logutils.ActionStart, logutils.TypeTransaction, nil, err) - } - - //1. set time updated to the item - now := time.Now() - item.DateUpdated = &now - - //2 convert to storage item - storageItem := accountAuthTypeToStorage(item) - - //3. first find the account record - findFilter := bson.M{"auth_types.id": item.ID} - var accounts []account - err = sa.db.accounts.FindWithContext(sessionContext, findFilter, &accounts, nil) - if err != nil { - sa.abortTransaction(sessionContext) - return errors.WrapErrorAction(logutils.ActionFind, model.TypeUserAuth, &logutils.FieldArgs{"account auth type id": item.ID}, err) - } - if len(accounts) == 0 { - sa.abortTransaction(sessionContext) - return errors.ErrorAction(logutils.ActionFind, "for some reasons account is nil for account auth type", &logutils.FieldArgs{"acccount auth type id": item.ID}) - } - account := accounts[0] - - //4. update the account auth type in the account record - accountAuthTypes := account.AuthTypes - newAccountAuthTypes := make([]accountAuthType, len(accountAuthTypes)) - for j, aAuthType := range accountAuthTypes { - if aAuthType.ID == storageItem.ID { - newAccountAuthTypes[j] = storageItem - } else { - newAccountAuthTypes[j] = aAuthType - } - } - account.AuthTypes = newAccountAuthTypes +// UpdateAccountAuthType updates an account with the provided account auth type +func (sa *Adapter) UpdateAccountAuthType(context TransactionContext, item model.AccountAuthType) error { + storageItem := accountAuthTypeToStorage(item) + now := time.Now().UTC() + storageItem.DateUpdated = &now - //4. update the account record - replaceFilter := bson.M{"_id": account.ID} - err = sa.db.accounts.ReplaceOneWithContext(sessionContext, replaceFilter, account, nil) - if err != nil { - sa.abortTransaction(sessionContext) - return errors.WrapErrorAction(logutils.ActionReplace, model.TypeAccount, nil, err) - } + filter := bson.M{"_id": item.Account.ID, "auth_types.id": item.ID} + update := bson.D{ + primitive.E{Key: "$set", Value: bson.D{ + primitive.E{Key: "auth_types.$", Value: storageItem}, + primitive.E{Key: "date_updated", Value: now}, + }}, + } - //commit the transaction - err = sessionContext.CommitTransaction(sessionContext) - if err != nil { - sa.abortTransaction(sessionContext) - return errors.WrapErrorAction(logutils.ActionCommit, logutils.TypeTransaction, nil, err) - } - return nil - }) + res, err := sa.db.accounts.UpdateOneWithContext(context, filter, update, nil) if err != nil { - return err + return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountAuthType, nil, err) + } + if res.ModifiedCount != 1 { + return errors.ErrorAction(logutils.ActionUpdate, model.TypeAccount, &logutils.FieldArgs{"modified": res.ModifiedCount, "expected": 1}) } return nil @@ -2312,7 +2338,10 @@ func (sa *Adapter) DeleteAccountAuthType(context TransactionContext, item model. filter := bson.M{"_id": item.Account.ID} update := bson.D{ primitive.E{Key: "$pull", Value: bson.D{ - primitive.E{Key: "auth_types", Value: bson.M{"auth_type_code": item.AuthType.Code, "identifier": item.Identifier}}, + primitive.E{Key: "auth_types", Value: bson.M{"id": item.ID, "auth_type_code": item.SupportedAuthType.AuthType.Code}}, + }}, + primitive.E{Key: "$set", Value: bson.D{ + primitive.E{Key: "date_updated", Value: time.Now().UTC()}, }}, } @@ -2327,42 +2356,125 @@ func (sa *Adapter) DeleteAccountAuthType(context TransactionContext, item model. return nil } -// UpdateAccountExternalIDs updates account external IDs -func (sa *Adapter) UpdateAccountExternalIDs(accountID string, externalIDs map[string]string) error { - filter := bson.D{primitive.E{Key: "_id", Value: accountID}} - now := time.Now().UTC() +// InsertAccountIdentifier inserts am account auth type +func (sa *Adapter) InsertAccountIdentifier(context TransactionContext, item model.AccountIdentifier) error { + storageItem := accountIdentifierToStorage(item) + + //3. first find the account record + filter := bson.M{"_id": item.Account.ID} update := bson.D{ + primitive.E{Key: "$push", Value: bson.D{ + primitive.E{Key: "identifiers", Value: storageItem}, + }}, primitive.E{Key: "$set", Value: bson.D{ - primitive.E{Key: "external_ids", Value: externalIDs}, - primitive.E{Key: "date_updated", Value: &now}, + primitive.E{Key: "date_updated", Value: time.Now().UTC()}, }}, } - res, err := sa.db.accounts.UpdateOne(filter, update, nil) + res, err := sa.db.accounts.UpdateOneWithContext(context, filter, update, nil) if err != nil { - return errors.WrapErrorAction(logutils.ActionUpdate, "account external IDs", &logutils.FieldArgs{"_id": accountID}, err) + return errors.WrapErrorAction(logutils.ActionInsert, model.TypeAccountIdentifier, nil, err) } if res.ModifiedCount != 1 { - return errors.ErrorAction(logutils.ActionUpdate, "account external IDs", &logutils.FieldArgs{"_id": accountID, "unexpected modified count": res.ModifiedCount}) + return errors.ErrorAction(logutils.ActionUpdate, model.TypeAccount, &logutils.FieldArgs{"unexpected modified count": res.ModifiedCount}) } return nil } -// UpdateLoginSessionExternalIDs updates login session external IDs -func (sa *Adapter) UpdateLoginSessionExternalIDs(accountID string, externalIDs map[string]string) error { - filter := bson.D{primitive.E{Key: "identifier", Value: accountID}} +// UpdateAccountIdentifier updates an account with the given account identifier +func (sa *Adapter) UpdateAccountIdentifier(context TransactionContext, item model.AccountIdentifier) error { + storageItem := accountIdentifierToStorage(item) now := time.Now().UTC() + storageItem.DateUpdated = &now + + filter := bson.M{"_id": item.Account.ID, "identifiers.id": item.ID} update := bson.D{ primitive.E{Key: "$set", Value: bson.D{ - primitive.E{Key: "external_ids", Value: externalIDs}, - primitive.E{Key: "date_updated", Value: &now}, + primitive.E{Key: "identifiers.$", Value: storageItem}, + primitive.E{Key: "date_updated", Value: now}, + }}, + } + + res, err := sa.db.accounts.UpdateOneWithContext(context, filter, update, nil) + if err != nil { + return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountIdentifier, nil, err) + } + if res.ModifiedCount != 1 { + return errors.ErrorAction(logutils.ActionUpdate, model.TypeAccount, &logutils.FieldArgs{"modified": res.ModifiedCount, "expected": 1}) + } + + return nil +} + +// UpdateAccountIdentifiers updates an account with the given list of account identifiers +func (sa *Adapter) UpdateAccountIdentifiers(context TransactionContext, accountID string, items []model.AccountIdentifier) error { + if len(items) == 0 { + return nil + } + + storageItems := accountIdentifiersToStorage(items) + + filter := bson.M{"_id": accountID} + update := bson.D{ + primitive.E{Key: "$set", Value: bson.D{ + primitive.E{Key: "identifiers", Value: storageItems}, + primitive.E{Key: "date_updated", Value: time.Now().UTC()}, + }}, + } + + res, err := sa.db.accounts.UpdateOneWithContext(context, filter, update, nil) + if err != nil { + return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeAccountIdentifier, nil, err) + } + if res.ModifiedCount != 1 { + return errors.ErrorAction(logutils.ActionUpdate, model.TypeAccount, &logutils.FieldArgs{"modified": res.ModifiedCount, "expected": 1}) + } + + return nil +} + +// DeleteAccountIdentifier deletes the given account identifier from an account +func (sa *Adapter) DeleteAccountIdentifier(context TransactionContext, item model.AccountIdentifier) error { + filter := bson.M{"_id": item.Account.ID} + update := bson.D{ + primitive.E{Key: "$pull", Value: bson.D{ + primitive.E{Key: "identifiers", Value: bson.M{"id": item.ID, "code": item.Code}}, + }}, + primitive.E{Key: "$set", Value: bson.D{ + primitive.E{Key: "date_updated", Value: time.Now().UTC()}, }}, } - _, err := sa.db.loginsSessions.UpdateMany(filter, update, nil) + res, err := sa.db.accounts.UpdateOneWithContext(context, filter, update, nil) + if err != nil { + return errors.WrapErrorAction(logutils.ActionDelete, model.TypeAccountIdentifier, nil, err) + } + if res.ModifiedCount != 1 { + return errors.ErrorAction(logutils.ActionUpdate, model.TypeAccount, &logutils.FieldArgs{"modified": res.ModifiedCount, "expected": 1}) + } + + return nil +} + +// DeleteExternalAccountIdentifiers deletes account identifiers with an account auth type ID matching the given account auth type +func (sa *Adapter) DeleteExternalAccountIdentifiers(context TransactionContext, aat model.AccountAuthType) error { + filter := bson.M{"_id": aat.Account.ID} + update := bson.D{ + primitive.E{Key: "$pull", Value: bson.D{ + primitive.E{Key: "identifiers", Value: bson.M{"account_auth_type_id": aat.ID}}, + }}, + primitive.E{Key: "$set", Value: bson.D{ + primitive.E{Key: "date_updated", Value: time.Now().UTC()}, + }}, + } + + res, err := sa.db.accounts.UpdateOne(filter, update, nil) if err != nil { - return errors.WrapErrorAction(logutils.ActionUpdate, "login session external IDs", &logutils.FieldArgs{"identifier": accountID}, err) + return errors.WrapErrorAction(logutils.ActionDelete, model.TypeAccountIdentifier, nil, err) + } + if res.ModifiedCount != 1 { + return errors.ErrorAction(logutils.ActionUpdate, model.TypeAccount, &logutils.FieldArgs{"modified": res.ModifiedCount, "expected": 1}) } return nil @@ -2407,14 +2519,37 @@ func (sa *Adapter) FindCredential(context TransactionContext, ID string) (*model return &modelCreds, nil } +// FindCredentials finds a list of credentials by a list of IDs +func (sa *Adapter) FindCredentials(context TransactionContext, ids []string) ([]model.Credential, error) { + filter := bson.D{primitive.E{Key: "_id", Value: bson.M{"$in": ids}}} + + var creds []credential + err := sa.db.credentials.FindWithContext(context, filter, &creds, nil) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeCredential, &logutils.FieldArgs{"ids": ids}, err) + } + + return credentialsFromStorage(creds), nil +} + // InsertCredential inserts a set of credential func (sa *Adapter) InsertCredential(context TransactionContext, creds *model.Credential) error { - storageCreds := credentialToStorage(creds) - - if storageCreds == nil { + if creds == nil { return errors.ErrorData(logutils.StatusInvalid, logutils.TypeArg, logutils.StringArgs(model.TypeCredential)) } + if creds.AuthType.ID == "" { + authType, err := sa.getCachedAuthType(creds.AuthType.Code) + if err != nil { + return errors.WrapErrorAction(logutils.ActionLoadCache, model.TypeAuthType, &logutils.FieldArgs{"code": creds.AuthType.Code}, err) + } + if authType == nil { + return errors.ErrorData(logutils.StatusMissing, model.TypeAuthType, &logutils.FieldArgs{"code": creds.AuthType.Code}) + } + creds.AuthType = *authType + } + storageCreds := credentialToStorage(*creds) + _, err := sa.db.credentials.InsertOneWithContext(context, storageCreds) if err != nil { return errors.WrapErrorAction(logutils.ActionInsert, model.TypeCredential, nil, err) @@ -2425,12 +2560,12 @@ func (sa *Adapter) InsertCredential(context TransactionContext, creds *model.Cre // UpdateCredential updates a set of credentials func (sa *Adapter) UpdateCredential(context TransactionContext, creds *model.Credential) error { - storageCreds := credentialToStorage(creds) - - if storageCreds == nil { + if creds == nil { return errors.ErrorData(logutils.StatusInvalid, logutils.TypeArg, logutils.StringArgs(model.TypeCredential)) } + storageCreds := credentialToStorage(*creds) + filter := bson.D{primitive.E{Key: "_id", Value: storageCreds.ID}} err := sa.db.credentials.ReplaceOneWithContext(context, filter, storageCreds, nil) if err != nil { @@ -2446,6 +2581,7 @@ func (sa *Adapter) UpdateCredentialValue(ID string, value map[string]interface{} update := bson.D{ primitive.E{Key: "$set", Value: bson.D{ primitive.E{Key: "value", Value: value}, + primitive.E{Key: "date_updated", Value: time.Now().UTC()}, }}, } @@ -3184,8 +3320,6 @@ func (sa *Adapter) UpdateAccountProfile(context TransactionContext, profile mode primitive.E{Key: "profile.photo_url", Value: profile.PhotoURL}, primitive.E{Key: "profile.first_name", Value: profile.FirstName}, primitive.E{Key: "profile.last_name", Value: profile.LastName}, - primitive.E{Key: "profile.email", Value: profile.Email}, - primitive.E{Key: "profile.phone", Value: profile.Phone}, primitive.E{Key: "profile.birth_year", Value: profile.BirthYear}, primitive.E{Key: "profile.address", Value: profile.Address}, primitive.E{Key: "profile.zip_code", Value: profile.ZipCode}, @@ -3224,10 +3358,10 @@ func (sa *Adapter) UpdateAccountPrivacy(context TransactionContext, accountID st return nil } -// FindAccountProfiles finds profiles by app id, authtype id and account auth type identifier -func (sa *Adapter) FindAccountProfiles(appID string, authTypeID string, accountAuthTypeIdentifier string) ([]model.Profile, error) { +// FindAccountProfiles finds profiles by app id, authtype id and account identifier +func (sa *Adapter) FindAccountProfiles(appID string, accountIdentifier string) ([]model.Profile, error) { pipeline := []bson.M{ - {"$match": bson.M{"auth_types.auth_type_id": authTypeID, "auth_types.identifier": accountAuthTypeIdentifier}}, + {"$match": bson.M{"identifiers.identifier": accountIdentifier}}, {"$lookup": bson.M{ "from": "applications_organizations", "localField": "app_org_id", @@ -3239,14 +3373,14 @@ func (sa *Adapter) FindAccountProfiles(appID string, authTypeID string, accountA var accounts []account err := sa.db.accounts.Aggregate(pipeline, &accounts, nil) if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, &logutils.FieldArgs{"app_id": appID, "auth_types.id": authTypeID, "auth_types.identifier": accountAuthTypeIdentifier}, err) + return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, &logutils.FieldArgs{"app_id": appID, "identifiers.identifier": accountIdentifier}, err) } if len(accounts) == 0 { //not found return nil, nil } - result := profilesFromStorage(accounts, *sa) + result := profilesFromStorage(accounts, sa) return result, nil } @@ -3279,8 +3413,8 @@ func (sa *Adapter) FindConfigs(configType *string) ([]model.Config, error) { } // InsertConfig inserts a new config -func (sa *Adapter) InsertConfig(config model.Config) error { - _, err := sa.db.configs.InsertOne(config) +func (sa *Adapter) InsertConfig(context TransactionContext, config model.Config) error { + _, err := sa.db.configs.InsertOneWithContext(context, config) if err != nil { return errors.WrapErrorAction(logutils.ActionInsert, model.TypeConfig, nil, err) } @@ -3289,7 +3423,7 @@ func (sa *Adapter) InsertConfig(config model.Config) error { } // UpdateConfig updates an existing config -func (sa *Adapter) UpdateConfig(config model.Config) error { +func (sa *Adapter) UpdateConfig(context TransactionContext, config model.Config) error { filter := bson.M{"_id": config.ID} update := bson.D{ primitive.E{Key: "$set", Value: bson.D{ @@ -3298,10 +3432,10 @@ func (sa *Adapter) UpdateConfig(config model.Config) error { primitive.E{Key: "org_id", Value: config.OrgID}, primitive.E{Key: "system", Value: config.System}, primitive.E{Key: "data", Value: config.Data}, - primitive.E{Key: "date_updated", Value: config.DateUpdated}, + primitive.E{Key: "date_updated", Value: time.Now().UTC()}, }}, } - _, err := sa.db.configs.UpdateOne(filter, update, nil) + _, err := sa.db.configs.UpdateOneWithContext(context, filter, update, nil) if err != nil { return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeConfig, &logutils.FieldArgs{"id": config.ID}, err) } @@ -3361,7 +3495,7 @@ func (sa *Adapter) InsertOrganization(context TransactionContext, organization m // UpdateOrganization updates an organization func (sa *Adapter) UpdateOrganization(ID string, name string, requestType string, organizationDomains []string) error { - now := time.Now() + now := time.Now().UTC() //TODO - use pointers and update only what not nil updatOrganizationFilter := bson.D{primitive.E{Key: "_id", Value: ID}} updateOrganization := bson.D{ @@ -3538,7 +3672,7 @@ func (sa *Adapter) InsertAppConfig(item model.ApplicationConfig) (*model.Applica // UpdateAppConfig updates an appconfig func (sa *Adapter) UpdateAppConfig(ID string, appType model.ApplicationType, appOrg *model.ApplicationOrganization, version model.Version, data map[string]interface{}) error { - now := time.Now() + now := time.Now().UTC() //TODO - use pointers and update only what not nil updatAppConfigFilter := bson.D{primitive.E{Key: "_id", Value: ID}} updateItem := bson.D{primitive.E{Key: "date_updated", Value: now}, primitive.E{Key: "app_type_id", Value: appType.ID}, primitive.E{Key: "version", Value: versionToStorage(version)}} @@ -3714,7 +3848,7 @@ func (sa *Adapter) InsertApplicationOrganization(context TransactionContext, app // UpdateApplicationOrganization updates an application organization func (sa *Adapter) UpdateApplicationOrganization(context TransactionContext, applicationOrganization model.ApplicationOrganization) error { appOrg := applicationOrganizationToStorage(applicationOrganization) - now := time.Now() + now := time.Now().UTC() filter := bson.M{"_id": applicationOrganization.ID} update := bson.D{primitive.E{Key: "date_updated", Value: now}, @@ -3806,7 +3940,7 @@ func (sa *Adapter) InsertAuthType(context TransactionContext, authType model.Aut func (sa *Adapter) UpdateAuthTypes(ID string, code string, description string, isExternal bool, isAnonymous bool, useCredentials bool, ignoreMFA bool, params map[string]interface{}) error { - now := time.Now() + now := time.Now().UTC() updateAuthTypeFilter := bson.D{primitive.E{Key: "_id", Value: ID}} updateAuthType := bson.D{ primitive.E{Key: "$set", Value: bson.D{ diff --git a/driven/storage/conversions_auth.go b/driven/storage/conversions_auth.go index 55c789887..ecebed7ed 100644 --- a/driven/storage/conversions_auth.go +++ b/driven/storage/conversions_auth.go @@ -31,11 +31,6 @@ func loginSessionFromStorage(item loginSession, authType model.AuthType, account anonymous := item.Anonymous identifier := item.Identifier - externalIDs := item.ExternalIDs - var accountAuthType *model.AccountAuthType - if item.AccountAuthTypeID != nil && account != nil { - accountAuthType = account.GetAccountAuthTypeByID(*item.AccountAuthTypeID) - } var deviceID string if item.DeviceID != nil { deviceID = *item.DeviceID @@ -61,10 +56,9 @@ func loginSessionFromStorage(item loginSession, authType model.AuthType, account dateUpdated := item.DateUpdated dateCreated := item.DateCreated - return model.LoginSession{ID: id, AppOrg: appOrg, AuthType: authType, AppType: appType, - Anonymous: anonymous, Identifier: identifier, ExternalIDs: externalIDs, AccountAuthType: accountAuthType, - Device: device, IPAddress: idAddress, AccessToken: accessToken, RefreshTokens: refreshTokens, Params: params, - State: state, StateExpires: stateExpires, MfaAttempts: mfaAttempts, + return model.LoginSession{ID: id, AppOrg: appOrg, AuthType: authType, AppType: appType, Anonymous: anonymous, + Identifier: identifier, Account: account, Device: device, IPAddress: idAddress, AccessToken: accessToken, + RefreshTokens: refreshTokens, Params: params, State: state, StateExpires: stateExpires, MfaAttempts: mfaAttempts, DateRefreshed: dateRefreshed, DateUpdated: dateUpdated, DateCreated: dateCreated} } @@ -81,13 +75,6 @@ func loginSessionToStorage(item model.LoginSession) *loginSession { anonymous := item.Anonymous identifier := item.Identifier - externalIDs := item.ExternalIDs - var accountAuthTypeID *string - var accountAuthTypeIdentifier *string - if item.AccountAuthType != nil && len(item.AccountAuthType.ID) != 0 { - accountAuthTypeID = &item.AccountAuthType.ID - accountAuthTypeIdentifier = &item.AccountAuthType.Identifier - } var deviceID *string if item.Device != nil { deviceID = &item.Device.ID @@ -112,12 +99,10 @@ func loginSessionToStorage(item model.LoginSession) *loginSession { dateUpdated := item.DateUpdated dateCreated := item.DateCreated - return &loginSession{ID: id, AppID: appID, OrgID: orgID, AuthTypeCode: authTypeCode, - AppTypeID: appTypeID, AppTypeIdentifier: appTypeIdentifier, Anonymous: anonymous, - Identifier: identifier, ExternalIDs: externalIDs, AccountAuthTypeID: accountAuthTypeID, AccountAuthTypeIdentifier: accountAuthTypeIdentifier, - DeviceID: deviceID, IPAddress: ipAddress, AccessToken: accessToken, RefreshTokens: refreshTokens, - Params: params, State: state, StateExpires: stateExpires, MfaAttempts: mfaAttempts, - DateRefreshed: dateRefreshed, DateUpdated: dateUpdated, DateCreated: dateCreated} + return &loginSession{ID: id, AppID: appID, OrgID: orgID, AuthTypeCode: authTypeCode, AppTypeID: appTypeID, AppTypeIdentifier: appTypeIdentifier, + Anonymous: anonymous, Identifier: identifier, DeviceID: deviceID, IPAddress: ipAddress, AccessToken: accessToken, RefreshTokens: refreshTokens, + Params: params, State: state, StateExpires: stateExpires, MfaAttempts: mfaAttempts, DateRefreshed: dateRefreshed, + DateUpdated: dateUpdated, DateCreated: dateCreated} } // ServiceAccount diff --git a/driven/storage/conversions_user.go b/driven/storage/conversions_user.go index e05fedc62..10090fe66 100644 --- a/driven/storage/conversions_user.go +++ b/driven/storage/conversions_user.go @@ -19,27 +19,28 @@ import ( ) // Account -func accountFromStorage(item account, appOrg model.ApplicationOrganization) model.Account { +func accountFromStorage(item account, appOrg model.ApplicationOrganization, sa *Adapter) model.Account { roles := accountRolesFromStorage(item.Roles, appOrg) groups := accountGroupsFromStorage(item.Groups, appOrg) - authTypes := accountAuthTypesFromStorage(item.AuthTypes) + identifiers := accountIdentifiersFromStorage(item.Identifiers) + authTypes := accountAuthTypesFromStorage(item.AuthTypes, sa) mfaTypes := mfaTypesFromStorage(item.MFATypes) profile := profileFromStorage(item.Profile) devices := accountDevicesFromStorage(item) - return model.Account{ID: item.ID, AppOrg: appOrg, Anonymous: item.Anonymous, Permissions: item.Permissions, Roles: roles, Groups: groups, Scopes: item.Scopes, AuthTypes: authTypes, - MFATypes: mfaTypes, Username: item.Username, ExternalIDs: item.ExternalIDs, Preferences: item.Preferences, Profile: profile, SystemConfigs: item.SystemConfigs, - Privacy: item.Privacy, Verified: item.Verified, Devices: devices, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated, LastLoginDate: item.LastLoginDate, - LastAccessTokenDate: item.LastAccessTokenDate, MostRecentClientVersion: item.MostRecentClientVersion} + return model.Account{ID: item.ID, AppOrg: appOrg, Anonymous: item.Anonymous, Permissions: item.Permissions, Roles: roles, Groups: groups, Scopes: item.Scopes, + Identifiers: identifiers, AuthTypes: authTypes, MFATypes: mfaTypes, Preferences: item.Preferences, Profile: profile, SystemConfigs: item.SystemConfigs, + Privacy: item.Privacy, Verified: item.Verified, Devices: devices, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated, + LastLoginDate: item.LastLoginDate, LastAccessTokenDate: item.LastAccessTokenDate, MostRecentClientVersion: item.MostRecentClientVersion} } -func accountsFromStorage(items []account, appOrg model.ApplicationOrganization) []model.Account { +func accountsFromStorage(items []account, appOrg model.ApplicationOrganization, sa *Adapter) []model.Account { if len(items) == 0 { return make([]model.Account, 0) } res := make([]model.Account, len(items)) for i, item := range items { - res[i] = accountFromStorage(item, appOrg) + res[i] = accountFromStorage(item, appOrg, sa) } return res } @@ -50,6 +51,7 @@ func accountToStorage(item *model.Account) *account { permissions := item.Permissions roles := accountRolesToStorage(item.Roles) groups := accountGroupsToStorage(item.Groups) + identifiers := accountIdentifiersToStorage(item.Identifiers) authTypes := accountAuthTypesToStorage(item.AuthTypes) mfaTypes := mfaTypesToStorage(item.MFATypes) profile := profileToStorage(item.Profile) @@ -60,9 +62,10 @@ func accountToStorage(item *model.Account) *account { lastAccessTokenDate := item.LastAccessTokenDate mostRecentClientVersion := item.MostRecentClientVersion - return &account{ID: id, AppOrgID: appOrgID, Anonymous: item.Anonymous, Permissions: permissions, Roles: roles, Groups: groups, Scopes: item.Scopes, AuthTypes: authTypes, MFATypes: mfaTypes, - Privacy: item.Privacy, Verified: item.Verified, Username: item.Username, ExternalIDs: item.ExternalIDs, Preferences: item.Preferences, Profile: profile, SystemConfigs: item.SystemConfigs, Devices: devices, - DateCreated: dateCreated, DateUpdated: dateUpdated, LastLoginDate: lastLoginDate, LastAccessTokenDate: lastAccessTokenDate, MostRecentClientVersion: mostRecentClientVersion} + return &account{ID: id, AppOrgID: appOrgID, Anonymous: item.Anonymous, Permissions: permissions, Roles: roles, Groups: groups, Scopes: item.Scopes, + Identifiers: identifiers, AuthTypes: authTypes, MFATypes: mfaTypes, Privacy: item.Privacy, Verified: item.Verified, Preferences: item.Preferences, + Profile: profile, SystemConfigs: item.SystemConfigs, Devices: devices, DateCreated: dateCreated, DateUpdated: dateUpdated, + LastLoginDate: lastLoginDate, LastAccessTokenDate: lastAccessTokenDate, MostRecentClientVersion: mostRecentClientVersion} } func accountDevicesFromStorage(item account) []model.Device { @@ -94,28 +97,24 @@ func accountDeviceToStorage(item model.Device) userDevice { } // AccountAuthType -func accountAuthTypeFromStorage(item accountAuthType) model.AccountAuthType { +func accountAuthTypeFromStorage(item accountAuthType, sa *Adapter) model.AccountAuthType { id := item.ID - authType := model.AuthType{ID: item.AuthTypeID, Code: item.AuthTypeCode} - identifier := item.Identifier + + authType, _ := sa.FindAuthType(item.AuthTypeID) params := item.Params var credential *model.Credential if item.CredentialID != nil { credential = &model.Credential{ID: *item.CredentialID} } active := item.Active - return model.AccountAuthType{ID: id, AuthType: authType, Identifier: identifier, Params: params, Credential: credential, - Active: active, Unverified: item.Unverified, Linked: item.Linked, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated} + return model.AccountAuthType{ID: id, SupportedAuthType: model.SupportedAuthType{AuthTypeID: item.AuthTypeID, AuthType: *authType}, Params: params, Credential: credential, + Active: active, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated} } -func accountAuthTypesFromStorage(items []accountAuthType) []model.AccountAuthType { - if len(items) == 0 { - return make([]model.AccountAuthType, 0) - } - +func accountAuthTypesFromStorage(items []accountAuthType, sa *Adapter) []model.AccountAuthType { res := make([]model.AccountAuthType, len(items)) for i, aat := range items { - res[i] = accountAuthTypeFromStorage(aat) + res[i] = accountAuthTypeFromStorage(aat, sa) } return res } @@ -125,15 +124,11 @@ func accountAuthTypeToStorage(item model.AccountAuthType) accountAuthType { if item.Credential != nil { credentialID = &item.Credential.ID } - return accountAuthType{ID: item.ID, AuthTypeID: item.AuthType.ID, AuthTypeCode: item.AuthType.Code, Identifier: item.Identifier, - Params: item.Params, CredentialID: credentialID, Active: item.Active, Unverified: item.Unverified, Linked: item.Linked, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated} + return accountAuthType{ID: item.ID, AuthTypeID: item.SupportedAuthType.AuthType.ID, AuthTypeCode: item.SupportedAuthType.AuthType.Code, + Params: item.Params, CredentialID: credentialID, Active: item.Active, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated} } func accountAuthTypesToStorage(items []model.AccountAuthType) []accountAuthType { - if len(items) == 0 { - return make([]accountAuthType, 0) - } - res := make([]accountAuthType, len(items)) for i, aat := range items { res[i] = accountAuthTypeToStorage(aat) @@ -141,6 +136,35 @@ func accountAuthTypesToStorage(items []model.AccountAuthType) []accountAuthType return res } +// AccountIdentifier +func accountIdentifierFromStorage(item accountIdentifier) model.AccountIdentifier { + return model.AccountIdentifier{ID: item.ID, Code: item.Code, Identifier: item.Identifier, Verified: item.Verified, Linked: item.Linked, + Sensitive: item.Sensitive, AccountAuthTypeID: item.AccountAuthTypeID, Primary: item.Primary, VerificationCode: item.VerificationCode, + VerificationExpiry: item.VerificationExpiry, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated} +} + +func accountIdentifiersFromStorage(items []accountIdentifier) []model.AccountIdentifier { + res := make([]model.AccountIdentifier, len(items)) + for i, aat := range items { + res[i] = accountIdentifierFromStorage(aat) + } + return res +} + +func accountIdentifierToStorage(item model.AccountIdentifier) accountIdentifier { + return accountIdentifier{ID: item.ID, Code: item.Code, Identifier: item.Identifier, Verified: item.Verified, Linked: item.Linked, + Sensitive: item.Sensitive, AccountAuthTypeID: item.AccountAuthTypeID, Primary: item.Primary, VerificationCode: item.VerificationCode, + VerificationExpiry: item.VerificationExpiry, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated} +} + +func accountIdentifiersToStorage(items []model.AccountIdentifier) []accountIdentifier { + res := make([]accountIdentifier, len(items)) + for i, aat := range items { + res[i] = accountIdentifierToStorage(aat) + } + return res +} + // AccountRole func accountRoleFromStorage(item *accountRole, appOrg model.ApplicationOrganization) model.AccountRole { if item == nil { @@ -222,12 +246,12 @@ func accountGroupsToStorage(items []model.AccountGroup) []accountGroup { // Profile func profileFromStorage(item profile) model.Profile { return model.Profile{ID: item.ID, PhotoURL: item.PhotoURL, FirstName: item.FirstName, LastName: item.LastName, - Email: item.Email, Phone: item.Phone, BirthYear: item.BirthYear, Address: item.Address, ZipCode: item.ZipCode, - State: item.State, Country: item.Country, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated, + BirthYear: item.BirthYear, Address: item.Address, ZipCode: item.ZipCode, State: item.State, + Country: item.Country, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated, UnstructuredProperties: item.UnstructuredProperties} } -func profilesFromStorage(items []account, sa Adapter) []model.Profile { +func profilesFromStorage(items []account, sa *Adapter) []model.Profile { if len(items) == 0 { return make([]model.Profile, 0) } @@ -236,7 +260,7 @@ func profilesFromStorage(items []account, sa Adapter) []model.Profile { accounts := make(map[string][]model.Account, len(items)) for _, account := range items { appOrg, _ := sa.getCachedApplicationOrganizationByKey(account.AppOrgID) - rAccount := accountFromStorage(account, *appOrg) + rAccount := accountFromStorage(account, *appOrg, sa) //add account to the map profileAccounts := accounts[rAccount.Profile.ID] @@ -261,8 +285,8 @@ func profilesFromStorage(items []account, sa Adapter) []model.Profile { func profileToStorage(item model.Profile) profile { return profile{ID: item.ID, PhotoURL: item.PhotoURL, FirstName: item.FirstName, LastName: item.LastName, - Email: item.Email, Phone: item.Phone, BirthYear: item.BirthYear, Address: item.Address, ZipCode: item.ZipCode, - State: item.State, Country: item.Country, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated, + BirthYear: item.BirthYear, Address: item.Address, ZipCode: item.ZipCode, State: item.State, + Country: item.Country, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated, UnstructuredProperties: item.UnstructuredProperties} } @@ -287,23 +311,35 @@ func credentialFromStorage(item credential) model.Credential { accountAuthTypes[i] = model.AccountAuthType{ID: id} } authType := model.AuthType{ID: item.AuthTypeID} - return model.Credential{ID: item.ID, AuthType: authType, AccountsAuthTypes: accountAuthTypes, Verified: item.Verified, + return model.Credential{ID: item.ID, AuthType: authType, AccountsAuthTypes: accountAuthTypes, Value: item.Value, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated} } -func credentialToStorage(item *model.Credential) *credential { - if item == nil { - return nil +func credentialsFromStorage(items []credential) []model.Credential { + res := make([]model.Credential, len(items)) + for i, cred := range items { + res[i] = credentialFromStorage(cred) } + return res +} +func credentialToStorage(item model.Credential) credential { accountAuthTypes := make([]string, len(item.AccountsAuthTypes)) for i, aat := range item.AccountsAuthTypes { accountAuthTypes[i] = aat.ID } - return &credential{ID: item.ID, AuthTypeID: item.AuthType.ID, AccountsAuthTypes: accountAuthTypes, Verified: item.Verified, + return credential{ID: item.ID, AuthTypeID: item.AuthType.ID, AccountsAuthTypes: accountAuthTypes, Value: item.Value, DateCreated: item.DateCreated, DateUpdated: item.DateUpdated} } +func credentialsToStorage(items []model.Credential) []credential { + res := make([]credential, len(items)) + for i, cred := range items { + res[i] = credentialToStorage(cred) + } + return res +} + // MFA func mfaTypesFromStorage(items []mfaType) []model.MFAType { res := make([]model.MFAType, len(items)) diff --git a/driven/storage/database.go b/driven/storage/database.go index e2054a4f3..1cff2055b 100644 --- a/driven/storage/database.go +++ b/driven/storage/database.go @@ -42,6 +42,7 @@ type database struct { devices *collectionWrapper credentials *collectionWrapper loginsSessions *collectionWrapper + loginStates *collectionWrapper configs *collectionWrapper serviceRegs *collectionWrapper serviceRegistrations *collectionWrapper @@ -136,6 +137,12 @@ func (m *database) start() error { return err } + loginStates := &collectionWrapper{database: m, coll: db.Collection("login_states")} + err = m.applyLoginStatesChecks(loginStates) + if err != nil { + return err + } + serviceAuthorizations := &collectionWrapper{database: m, coll: db.Collection("service_authorizations")} err = m.applyServiceAuthorizationsChecks(serviceAuthorizations) if err != nil { @@ -212,6 +219,7 @@ func (m *database) start() error { m.devices = devices m.credentials = credentials m.loginsSessions = loginsSessions + m.loginStates = loginStates m.configs = configs m.apiKeys = apiKeys m.serviceRegs = serviceRegs @@ -245,6 +253,11 @@ func (m *database) start() error { func (m *database) applyAuthTypesChecks(authenticationTypes *collectionWrapper) error { m.logger.Info("apply auth types checks.....") + err := authenticationTypes.AddIndex(bson.D{primitive.E{Key: "code", Value: 1}}, true) + if err != nil { + return err + } + m.logger.Info("auth types check passed") return nil } @@ -259,9 +272,13 @@ func (m *database) applyIdentityProvidersChecks(identityProviders *collectionWra func (m *database) applyAccountsChecks(accounts *collectionWrapper) error { m.logger.Info("apply accounts checks.....") - //add compound index - auth_type identifier + auth_type_id + // remove old indexes + accounts.DropIndex("auth_types.identifier_1_auth_types.auth_type_id_1_app_org_id_1") + accounts.DropIndex("auth_types.identifier_1_auth_types.auth_type_code_1_app_org_id_1") + + //add compound index - identifier identifier + identifier code // Can't be unique because of anonymous accounts - err := accounts.AddIndex(bson.D{primitive.E{Key: "auth_types.identifier", Value: 1}, primitive.E{Key: "auth_types.auth_type_id", Value: 1}, primitive.E{Key: "app_org_id", Value: 1}}, false) + err := accounts.AddIndex(bson.D{primitive.E{Key: "identifiers.identifier", Value: 1}, primitive.E{Key: "identifiers.code", Value: 1}, primitive.E{Key: "app_org_id", Value: 1}}, false) if err != nil { return err } @@ -284,6 +301,12 @@ func (m *database) applyAccountsChecks(accounts *collectionWrapper) error { return err } + //add identifiers index + err = accounts.AddIndex(bson.D{primitive.E{Key: "identifiers.id", Value: 1}}, false) + if err != nil { + return err + } + // err = accounts.AddIndex(bson.D{primitive.E{Key: "username", Value: "text"}, primitive.E{Key: "profile.first_name", Value: "text"}, primitive.E{Key: "profile.last_name", Value: "text"}}, false) // if err != nil { // return err @@ -309,25 +332,22 @@ func (m *database) applyDevicesChecks(devices *collectionWrapper) error { func (m *database) applyCredentialChecks(credentials *collectionWrapper) error { m.logger.Info("apply credentials checks.....") - // Add user_auth_type_id index - err := credentials.AddIndex(bson.D{primitive.E{Key: "user_auth_type_id", Value: 1}}, false) - if err != nil { - return err - } + // remove unused index + credentials.DropIndex("user_auth_type_id_1") m.logger.Info("credentials check passed") return nil } -func (m *database) applyLoginsSessionsChecks(refreshTokens *collectionWrapper) error { +func (m *database) applyLoginsSessionsChecks(loginSessions *collectionWrapper) error { m.logger.Info("apply logins sessions checks.....") - err := refreshTokens.AddIndex(bson.D{primitive.E{Key: "refresh_token", Value: 1}}, false) + err := loginSessions.AddIndex(bson.D{primitive.E{Key: "refresh_token", Value: 1}}, false) if err != nil { return err } - err = refreshTokens.AddIndex(bson.D{primitive.E{Key: "expires", Value: 1}}, false) + err = loginSessions.AddIndex(bson.D{primitive.E{Key: "expires", Value: 1}}, false) if err != nil { return err } @@ -336,6 +356,31 @@ func (m *database) applyLoginsSessionsChecks(refreshTokens *collectionWrapper) e return nil } +func (m *database) applyLoginStatesChecks(loginStates *collectionWrapper) error { + m.logger.Info("apply logins states checks.....") + + err := loginStates.AddIndex(bson.D{primitive.E{Key: "app_id", Value: 1}, primitive.E{Key: "org_id", Value: 1}}, false) + if err != nil { + return err + } + + err = loginStates.AddIndex(bson.D{primitive.E{Key: "account_id", Value: 1}}, false) + if err != nil { + return err + } + + // create TTL index which auto-deletes login state documents after 5 minutes + opts := options.IndexOptions{} + opts.SetExpireAfterSeconds(5 * 60) + err = loginStates.AddIndexWithOptions(bson.D{primitive.E{Key: "date_created", Value: 1}}, &opts) + if err != nil { + return err + } + + m.logger.Info("logins states check passed") + return nil +} + func (m *database) applyAPIKeysChecks(apiKeys *collectionWrapper) error { m.logger.Info("apply api keys checks.....") @@ -427,8 +472,6 @@ func (m *database) applyOrganizationsChecks(organizations *collectionWrapper) er return err } - //TODO - //add applications index err = organizations.AddIndex(bson.D{primitive.E{Key: "applications", Value: 1}}, false) if err != nil { diff --git a/driven/storage/migrations.go b/driven/storage/migrations.go new file mode 100644 index 000000000..540e7bf94 --- /dev/null +++ b/driven/storage/migrations.go @@ -0,0 +1,473 @@ +// Copyright 2022 Board of Trustees of the University of Illinois. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package storage + +import ( + "core-building-block/core/model" + "core-building-block/utils" + "strings" + "time" + + "github.com/google/uuid" + "github.com/rokwire/logging-library-go/v2/errors" + "github.com/rokwire/logging-library-go/v2/logutils" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Database Migration functions + +func (sa *Adapter) migrateAuthTypes() error { + transaction := func(context TransactionContext) error { + //1. insert new auth types + newAuthTypes := map[string]model.AuthType{ + "password": {ID: uuid.NewString(), Code: "password", Description: "Authentication type relying on password", UseCredentials: true, Aliases: []string{"email", "username"}}, + "code": {ID: uuid.NewString(), Code: "code", Description: "Authentication type relying on codes sent over a communication channel", Aliases: []string{"phone", "twilio_phone"}}, + "webauthn": {ID: uuid.NewString(), Code: "webauthn", Description: "Authentication type relying on WebAuthn", UseCredentials: true}, + } + inserted := false + for code, authType := range newAuthTypes { + existing, err := sa.FindAuthType(code) + if err != nil { + return errors.WrapErrorAction(logutils.ActionFind, model.TypeAuthType, logutils.StringArgs(code), err) + } + if existing == nil { + _, err = sa.InsertAuthType(context, authType) + if err != nil { + return errors.WrapErrorAction(logutils.ActionInsert, model.TypeAuthType, logutils.StringArgs(code), err) + } + + inserted = true + } + } + // if all already exist, migration is done so return no error + if !inserted { + return nil + } + + //2. remove old auth types if they exist + removedAuthTypeIDs := make(map[string]model.AuthType) + removedAuthTypeCodes := map[string]model.AuthType{ + "email": newAuthTypes["password"], + "username": newAuthTypes["password"], + "phone": newAuthTypes["code"], + "twilio_phone": newAuthTypes["code"], + } + for old, new := range removedAuthTypeCodes { + // need to load auth type directly from DB so that we do not get one of the new auth types by alias + var authTypes []model.AuthType + err := sa.db.authTypes.FindWithContext(context, bson.M{"code": old}, &authTypes, nil) + if err != nil { + return errors.WrapErrorAction(logutils.ActionFind, model.TypeAuthType, logutils.StringArgs(old), err) + } + if len(authTypes) == 0 { + continue + } + + removedAuthTypeIDs[authTypes[0].ID] = new + + // remove the unwanted auth type, which also updates the cache + _, err = sa.db.authTypes.DeleteOneWithContext(context, bson.M{"code": old}, nil) + if err != nil { + return errors.WrapErrorAction(logutils.ActionDelete, model.TypeAuthType, logutils.StringArgs(old), err) + } + } + + //3. migrate credentials + removedCredentials, err := sa.migrateCredentials(context, removedAuthTypeIDs) + if err != nil { + return errors.WrapErrorAction("migrating", model.TypeCredential, nil, err) + } + + //4. migrate app orgs + err = sa.migrateAppOrgs(context, removedAuthTypeIDs, removedCredentials) + if err != nil { + return errors.WrapErrorAction("migrating", model.TypeApplicationOrganization, nil, err) + } + + //5. migrate login sessions + err = sa.migrateLoginSessions(context, removedAuthTypeCodes) + if err != nil { + return errors.WrapErrorAction("migrating", model.TypeLoginSession, nil, err) + } + + return nil + } + + return sa.PerformTransaction(transaction) +} + +func (sa *Adapter) migrateCredentials(context TransactionContext, removedAuthTypes map[string]model.AuthType) ([]string, error) { + var allCredentials []credential + err := sa.db.credentials.FindWithContext(context, bson.M{}, &allCredentials, nil) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionFind, model.TypeCredential, nil, err) + } + + type passwordCreds struct { + Password string `json:"password"` + + ResetCode *string `json:"reset_code,omitempty"` + ResetExpiry *time.Time `json:"reset_expiry,omitempty"` + } + + type webauthnCreds struct { + Credential *string `json:"credential,omitempty"` + Session *string `json:"session,omitempty"` + } + + migratedCredentials := make([]interface{}, 0) + removedCredentials := make([]string, 0) + for _, cred := range allCredentials { + var migrated credential + if newAuthType, exists := removedAuthTypes[cred.AuthTypeID]; exists && newAuthType.Code == "password" { + // found a password credential, migrate it + passwordValue, err := utils.JSONConvert[passwordCreds, map[string]interface{}](cred.Value) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionParse, "password credential value map", &logutils.FieldArgs{"id": cred.ID}, err) + } + if passwordValue == nil || passwordValue.Password == "" { + removedCredentials = append(removedCredentials, cred.ID) + continue + } + if passwordValue.ResetCode != nil && *passwordValue.ResetCode == "" { + passwordValue.ResetCode = nil + } + if passwordValue.ResetExpiry != nil && passwordValue.ResetExpiry.IsZero() { + passwordValue.ResetExpiry = nil + } + + passwordValueMap, err := utils.JSONConvert[map[string]interface{}, passwordCreds](*passwordValue) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionParse, "password credential value", &logutils.FieldArgs{"id": cred.ID}, err) + } + if passwordValueMap == nil { + removedCredentials = append(removedCredentials, cred.ID) + continue + } + + migrated = credential{ID: cred.ID, AuthTypeID: newAuthType.ID, AccountsAuthTypes: cred.AccountsAuthTypes, Value: *passwordValueMap, + DateCreated: cred.DateCreated, DateUpdated: cred.DateUpdated} + } else { + // found something other than a password credential, try to migrate it as a webauthn credential + webauthnValue, err := utils.JSONConvert[webauthnCreds, map[string]interface{}](cred.Value) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionParse, "webauthn credential value map", &logutils.FieldArgs{"id": cred.ID}, err) + } + + // credential value is not for webauthn or it is in a hanging state or is missing its credential + if webauthnValue == nil || webauthnValue.Session != nil || webauthnValue.Credential == nil { + removedCredentials = append(removedCredentials, cred.ID) + continue + } + + webauthnValueMap, err := utils.JSONConvert[map[string]interface{}, webauthnCreds](*webauthnValue) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionParse, "webauthn credential value", &logutils.FieldArgs{"id": cred.ID}, err) + } + if webauthnValueMap == nil { + removedCredentials = append(removedCredentials, cred.ID) + continue + } + + migrated = credential{ID: cred.ID, AuthTypeID: cred.AuthTypeID, AccountsAuthTypes: cred.AccountsAuthTypes, Value: *webauthnValueMap, + DateCreated: cred.DateCreated, DateUpdated: cred.DateUpdated} + } + + if migrated.ID != "" { + migratedCredentials = append(migratedCredentials, migrated) + } else { + removedCredentials = append(removedCredentials, cred.ID) + } + } + + _, err = sa.db.credentials.DeleteManyWithContext(context, bson.M{}, nil) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionDelete, model.TypeCredential, nil, err) + } + + _, err = sa.db.credentials.InsertManyWithContext(context, migratedCredentials, nil) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionInsert, model.TypeCredential, nil, err) + } + + return removedCredentials, nil +} + +func (sa *Adapter) migrateAppOrgs(context TransactionContext, removedAuthTypes map[string]model.AuthType, removedCredentials []string) error { + appOrgs, err := sa.FindApplicationsOrganizations() + if err != nil { + return errors.WrapErrorAction(logutils.ActionFind, model.TypeApplicationOrganization, nil, err) + } + + for _, appOrg := range appOrgs { + updated := false + for i, appType := range appOrg.SupportedAuthTypes { + updatedIDs := make([]string, 0) + for j, authType := range appType.SupportedAuthTypes { + if newAuthType, exists := removedAuthTypes[authType.AuthTypeID]; exists { + if !utils.Contains(updatedIDs, newAuthType.ID) { + appType.SupportedAuthTypes[j] = model.SupportedAuthType{AuthTypeID: newAuthType.ID} + updatedIDs = append(updatedIDs, newAuthType.ID) + } else { + // remove the obsolete supported auth type if the newID is already included in the list + appType.SupportedAuthTypes = append(appType.SupportedAuthTypes[:j], appType.SupportedAuthTypes[j+1:]...) + } + } + } + + if len(updatedIDs) > 0 { + appOrg.SupportedAuthTypes[i] = appType + updated = true + } + } + + if updated { + err = sa.UpdateApplicationOrganization(context, appOrg) + if err != nil { + return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeApplicationOrganization, &logutils.FieldArgs{"id": appOrg.ID}, err) + } + } + + err = sa.migrateAccounts(context, appOrg, removedAuthTypes, removedCredentials) + if err != nil { + return errors.WrapErrorAction("migrating", model.TypeAccount, &logutils.FieldArgs{"app_org_id": appOrg.ID}, err) + } + } + + return nil +} + +func (sa *Adapter) migrateAccounts(context TransactionContext, appOrg model.ApplicationOrganization, removedAuthTypes map[string]model.AuthType, removedCredentials []string) error { + filter := bson.M{"app_org_id": appOrg.ID} + var accounts []account + + err := sa.db.accounts.Find(filter, &accounts, nil) + if err != nil { + return errors.WrapErrorAction(logutils.ActionFind, model.TypeAccount, &logutils.FieldArgs{"app_org_id": appOrg.ID}, err) + } + if len(accounts) == 0 { + return nil + } + + migratedAccounts := make([]interface{}, len(accounts)) + for i, acct := range accounts { + migrated := acct + identifiers := make([]accountIdentifier, 0) + authTypes := make([]accountAuthType, 0) + addedIdentifiers := make([]string, 0) + for _, aat := range acct.AuthTypes { + newAat := aat + isExternal := (aat.Params["user"] != nil) + newAuthType, exists := removedAuthTypes[aat.AuthTypeID] + if aat.Identifier != nil && !isExternal { + identifier := *aat.Identifier + identifierCode, _ := strings.CutPrefix(aat.AuthTypeCode, "twilio_") + if !exists { + if strings.Contains(identifier, "@") { + identifierCode = "email" + } else if strings.Contains(identifier, "+") { + identifierCode = "phone" + } else { + identifierCode = "username" + } + + if strings.Contains(identifier, "-") { + identifierParts := strings.Split(identifier, "-") + identifier = identifierParts[0] + } + } + + if !utils.Contains(addedIdentifiers, identifier) { + verified := true + if aat.Unverified != nil { + verified = !*aat.Unverified + } + linked := false + if aat.Linked != nil { + linked = *aat.Linked + } + + newIdentifier := accountIdentifier{ID: uuid.NewString(), Code: identifierCode, Identifier: identifier, Verified: verified, Linked: linked, + Sensitive: identifierCode == "email" || identifierCode == "phone", DateCreated: aat.DateCreated, DateUpdated: aat.DateUpdated} + identifiers = append(identifiers, newIdentifier) + addedIdentifiers = append(addedIdentifiers, identifier) + } + } + + if exists { + // update the auth type ID and code if the current auth type was removed + newAat.AuthTypeID = newAuthType.ID + newAat.AuthTypeCode = newAuthType.Code + } else if isExternal { + // parse the external user from params + externalUser, err := utils.JSONConvert[model.ExternalSystemUser, interface{}](aat.Params["user"]) + if err != nil { + return errors.WrapErrorAction(logutils.ActionParse, model.TypeExternalSystemUser, &logutils.FieldArgs{"auth_types.id": aat.ID}, err) + } + if externalUser != nil { + externalAatID := aat.ID + linked := false + if aat.Linked != nil { + linked = *aat.Linked + } + var dateUpdated *time.Time + if aat.DateUpdated != nil { + dateUpdatedVal := *aat.DateUpdated + dateUpdated = &dateUpdatedVal + } + + // add the primary external identifier + code := "" + primary := true + for k, v := range appOrg.IdentityProvidersSettings[0].ExternalIDFields { + if v == appOrg.IdentityProvidersSettings[0].UserIdentifierField { + code = k + break + } + } + primaryIdentifier := accountIdentifier{ID: uuid.NewString(), Code: code, Identifier: externalUser.Identifier, Verified: true, Linked: linked, + AccountAuthTypeID: &externalAatID, Primary: &primary, DateCreated: aat.DateCreated, DateUpdated: dateUpdated} + identifiers = append(identifiers, primaryIdentifier) + + // add the other external identifiers from external IDs + for code, id := range externalUser.ExternalIDs { + if code != primaryIdentifier.Code { + primary := false + newIdentifier := accountIdentifier{ID: uuid.NewString(), Code: code, Identifier: id, Verified: true, Linked: linked, + AccountAuthTypeID: &externalAatID, Primary: &primary, DateCreated: aat.DateCreated, DateUpdated: dateUpdated} + identifiers = append(identifiers, newIdentifier) + } + } + + // add the external email if there is one + if externalUser.Email != "" && externalUser.Email != externalUser.Identifier { + primary := false + externalEmail := accountIdentifier{ID: uuid.NewString(), Code: "email", Identifier: externalUser.Email, Verified: true, Linked: linked, + Sensitive: true, Primary: &primary, AccountAuthTypeID: &externalAatID, DateCreated: aat.DateCreated, DateUpdated: dateUpdated} + identifiers = append(identifiers, externalEmail) + } + } + } + + // do not keep the account auth type if its associated credential was removed + if newAat.CredentialID != nil && utils.Contains(removedCredentials, *newAat.CredentialID) { + continue + } + + newAat.Identifier = nil + newAat.Unverified = nil + newAat.Linked = nil + authTypes = append(authTypes, newAat) + } + + now := time.Now().UTC() + // add profile email to identifiers if not already there + if acct.Profile.Email != nil && *acct.Profile.Email != "" { + foundEmail := false + for _, identifier := range identifiers { + if identifier.Code == "email" && identifier.Identifier == *acct.Profile.Email { + foundEmail = true + break + } + } + if !foundEmail { + emailIdentifier := accountIdentifier{ID: uuid.NewString(), Code: "email", Identifier: *acct.Profile.Email, Sensitive: true, DateCreated: now} + identifiers = append(identifiers, emailIdentifier) + } + } + // add profile phone to identifiers if not already there + if acct.Profile.Phone != nil && *acct.Profile.Phone != "" { + foundPhone := false + for _, identifier := range identifiers { + if identifier.Code == "phone" && identifier.Identifier == *acct.Profile.Phone { + foundPhone = true + break + } + } + if !foundPhone { + identifiers = append(identifiers, accountIdentifier{ID: uuid.NewString(), Code: "phone", Identifier: *acct.Profile.Phone, Sensitive: true, DateCreated: now}) + } + } + // add account username to identifiers if not already there + if acct.Username != nil && *acct.Username != "" { + foundUsername := false + for _, identifier := range identifiers { + if identifier.Code == "username" { + foundUsername = true + break + } + } + if !foundUsername { + identifiers = append(identifiers, accountIdentifier{ID: uuid.NewString(), Code: "username", Identifier: *acct.Username, Verified: true, DateCreated: now}) + } + } + + migrated.AuthTypes = authTypes + migrated.Identifiers = identifiers + migrated.ExternalIDs = nil + migrated.Profile.Email = nil + migrated.Profile.Phone = nil + migrated.Username = nil + migratedAccounts[i] = migrated + } + + _, err = sa.db.accounts.DeleteManyWithContext(context, filter, nil) + if err != nil { + return errors.WrapErrorAction(logutils.ActionDelete, model.TypeAccount, nil, err) + } + + _, err = sa.db.accounts.InsertManyWithContext(context, migratedAccounts, nil) + if err != nil { + return errors.WrapErrorAction(logutils.ActionInsert, model.TypeAccount, nil, err) + } + + return nil +} + +func (sa *Adapter) migrateLoginSessions(context TransactionContext, removedAuthTypes map[string]model.AuthType) error { + // remove the following fields from all login sessions + update := bson.D{primitive.E{Key: "$unset", Value: bson.D{ + primitive.E{Key: "account_auth_type_id", Value: 1}, + primitive.E{Key: "account_auth_type_identifier", Value: 1}, + primitive.E{Key: "external_ids", Value: 1}, + }}} + res, err := sa.db.loginsSessions.UpdateManyWithContext(context, bson.M{}, update, nil) + if err != nil { + return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeLoginSession, nil, err) + } + if res.ModifiedCount != res.MatchedCount { + return errors.ErrorAction(logutils.ActionUpdate, model.TypeLoginSession, &logutils.FieldArgs{"matched": res.MatchedCount, "modified": res.ModifiedCount}) + } + + for oldCode, authType := range removedAuthTypes { + // update the auth_type_code field for all removed auth types + update := bson.D{ + primitive.E{Key: "$set", Value: bson.D{ + primitive.E{Key: "auth_type_code", Value: authType.Code}, + }}, + } + + res, err := sa.db.loginsSessions.UpdateManyWithContext(context, bson.M{"auth_type_code": oldCode}, update, nil) + if err != nil { + return errors.WrapErrorAction(logutils.ActionUpdate, model.TypeLoginSession, &logutils.FieldArgs{"auth_type_code": oldCode}, err) + } + if res.ModifiedCount != res.MatchedCount { + return errors.ErrorAction(logutils.ActionUpdate, model.TypeLoginSession, &logutils.FieldArgs{"auth_type_code": oldCode, "matched": res.MatchedCount, "modified": res.ModifiedCount}) + } + } + + return nil +} diff --git a/driven/storage/model_auth.go b/driven/storage/model_auth.go index ee12f63c9..ef9b4851f 100644 --- a/driven/storage/model_auth.go +++ b/driven/storage/model_auth.go @@ -34,11 +34,7 @@ type loginSession struct { Anonymous bool `bson:"anonymous"` - Identifier string `bson:"identifier"` - ExternalIDs map[string]string `bson:"external_ids"` - - AccountAuthTypeID *string `bson:"account_auth_type_id"` - AccountAuthTypeIdentifier *string `bson:"account_auth_type_identifier"` + Identifier string `bson:"identifier"` DeviceID *string `bson:"device_id"` diff --git a/driven/storage/model_user.go b/driven/storage/model_user.go index 05000b739..7c5e8b43c 100644 --- a/driven/storage/model_user.go +++ b/driven/storage/model_user.go @@ -30,12 +30,11 @@ type account struct { Groups []accountGroup `bson:"groups,omitempty"` Scopes []string `bson:"scopes,omitempty"` - AuthTypes []accountAuthType `bson:"auth_types,omitempty"` + Identifiers []accountIdentifier `bson:"identifiers,omitempty"` + AuthTypes []accountAuthType `bson:"auth_types,omitempty"` MFATypes []mfaType `bson:"mfa_types,omitempty"` - Username string `bson:"username"` - ExternalIDs map[string]string `bson:"external_ids"` Preferences map[string]interface{} `bson:"preferences"` SystemConfigs map[string]interface{} `bson:"system_configs"` Profile profile `bson:"profile"` @@ -54,6 +53,11 @@ type account struct { LastLoginDate *time.Time `bson:"last_login_date"` LastAccessTokenDate *time.Time `bson:"last_access_token_date"` MostRecentClientVersion *string `bson:"most_recent_client_version"` + + // Deprecated: + Username *string `bson:"username,omitempty"` + // Deprecated: + ExternalIDs map[string]string `bson:"external_ids,omitempty"` } type accountRole struct { @@ -72,12 +76,35 @@ type accountAuthType struct { ID string `bson:"id"` AuthTypeID string `bson:"auth_type_id"` AuthTypeCode string `bson:"auth_type_code"` - Identifier string `bson:"identifier"` Params map[string]interface{} `bson:"params"` CredentialID *string `bson:"credential_id"` Active bool `bson:"active"` - Unverified bool `bson:"unverified"` - Linked bool `bson:"linked"` + + // Deprecated: + Identifier *string `bson:"identifier,omitempty"` + // Deprecated: + Unverified *bool `bson:"unverified,omitempty"` + // Deprecated: + Linked *bool `bson:"linked,omitempty"` + + DateCreated time.Time `bson:"date_created"` + DateUpdated *time.Time `bson:"date_updated"` +} + +type accountIdentifier struct { + ID string `bson:"id"` + Code string `bson:"code"` + Identifier string `bson:"identifier"` + + Verified bool `bson:"verified"` + Linked bool `bson:"linked"` + Sensitive bool `bson:"sensitive"` + + AccountAuthTypeID *string `bson:"account_auth_type_id"` + Primary *bool `bson:"primary,omitempty"` + + VerificationCode *string `bson:"verification_code,omitempty"` + VerificationExpiry *time.Time `bson:"verification_expiry,omitempty"` DateCreated time.Time `bson:"date_created"` DateUpdated *time.Time `bson:"date_updated"` @@ -89,8 +116,6 @@ type profile struct { PhotoURL string `bson:"photo_url"` FirstName string `bson:"first_name"` LastName string `bson:"last_name"` - Email string `bson:"email"` - Phone string `bson:"phone"` BirthYear int16 `bson:"birth_year"` Address string `bson:"address"` ZipCode string `bson:"zip_code"` @@ -101,6 +126,11 @@ type profile struct { DateUpdated *time.Time `bson:"date_updated"` UnstructuredProperties map[string]interface{} `bson:"unstructured_properties"` + + // Deprecated: + Email *string `bson:"email,omitempty"` + // Deprecated: + Phone *string `bson:"phone,omitempty"` } type userDevice struct { @@ -133,7 +163,6 @@ type credential struct { AuthTypeID string `bson:"auth_type_id"` AccountsAuthTypes []string `bson:"account_auth_types"` - Verified bool `bson:"verified"` Value map[string]interface{} `bson:"value"` DateCreated time.Time `bson:"date_created"` diff --git a/driver/web/adapter.go b/driver/web/adapter.go index f970bf0cf..306a03991 100644 --- a/driver/web/adapter.go +++ b/driver/web/adapter.go @@ -93,7 +93,9 @@ func (we Adapter) Start() { //ui subRouter.HandleFunc("/ui/credential/reset", we.serveResetCredential) //Public - subRouter.HandleFunc("/ui/credential/verify", we.uiWrapFunc(we.servicesApisHandler.verifyCredential, nil)).Methods("GET") //Public (validates code) + subRouter.HandleFunc("/ui/webauthn-test", we.serveWebAuthnTest) //Public + subRouter.HandleFunc("/ui/identifier/verify", we.uiWrapFunc(we.servicesApisHandler.verifyIdentifier, nil)).Methods("GET") //Public (validates code) + // subRouter.HandleFunc("/ui/webauthn-test", we.serveWebAuthnTest) //Public ///default /// subRouter.HandleFunc("/version", we.wrapFunc(we.defaultApisHandler.getVersion, nil)).Methods("GET") //Public @@ -107,13 +109,16 @@ func (we Adapter) Start() { servicesSubRouter.HandleFunc("/auth/login-url", we.wrapFunc(we.servicesApisHandler.loginURL, nil)).Methods("POST") //Requires API key in request servicesSubRouter.HandleFunc("/auth/refresh", we.wrapFunc(we.servicesApisHandler.refresh, nil)).Methods("POST") //Requires API key in request servicesSubRouter.HandleFunc("/auth/logout", we.wrapFunc(we.servicesApisHandler.logout, we.auth.services.User)).Methods("POST") - servicesSubRouter.HandleFunc("/auth/account/exists", we.wrapFunc(we.servicesApisHandler.accountExists, nil)).Methods("POST") //Requires API key in request - servicesSubRouter.HandleFunc("/auth/account/can-sign-in", we.wrapFunc(we.servicesApisHandler.canSignIn, nil)).Methods("POST") //Requires API key in request - servicesSubRouter.HandleFunc("/auth/account/can-link", we.wrapFunc(we.servicesApisHandler.canLink, nil)).Methods("POST") //Requires API key in request + servicesSubRouter.HandleFunc("/auth/account/exists", we.wrapFunc(we.servicesApisHandler.accountExists, nil)).Methods("POST") //Requires API key in request + servicesSubRouter.HandleFunc("/auth/account/can-sign-in", we.wrapFunc(we.servicesApisHandler.canSignIn, nil)).Methods("POST") //Requires API key in request + servicesSubRouter.HandleFunc("/auth/account/can-link", we.wrapFunc(we.servicesApisHandler.canLink, nil)).Methods("POST") //Requires API key in request + servicesSubRouter.HandleFunc("/auth/account/sign-in-options", we.wrapFunc(we.servicesApisHandler.signInOptions, nil)).Methods("POST") //Requires API key in request + servicesSubRouter.HandleFunc("/auth/account/identifier/link", we.wrapFunc(we.servicesApisHandler.linkAccountIdentifier, we.auth.services.Authenticated)).Methods("POST") + servicesSubRouter.HandleFunc("/auth/account/identifier/link", we.wrapFunc(we.servicesApisHandler.unlinkAccountIdentifier, we.auth.services.Authenticated)).Methods("DELETE") servicesSubRouter.HandleFunc("/auth/account/auth-type/link", we.wrapFunc(we.servicesApisHandler.linkAccountAuthType, we.auth.services.Authenticated)).Methods("POST") servicesSubRouter.HandleFunc("/auth/account/auth-type/link", we.wrapFunc(we.servicesApisHandler.unlinkAccountAuthType, we.auth.services.Authenticated)).Methods("DELETE") - servicesSubRouter.HandleFunc("/auth/credential/verify", we.wrapFunc(we.servicesApisHandler.verifyCredential, nil)).Methods("GET") //Public (validates code) - servicesSubRouter.HandleFunc("/auth/credential/send-verify", we.wrapFunc(we.servicesApisHandler.sendVerifyCredential, nil)).Methods("POST") //Requires API key in request + servicesSubRouter.HandleFunc("/auth/identifier/verify", we.wrapFunc(we.servicesApisHandler.verifyIdentifier, nil)).Methods("GET") //Public (validates code) + servicesSubRouter.HandleFunc("/auth/identifier/send-verify", we.wrapFunc(we.servicesApisHandler.sendVerifyIdentifier, nil)).Methods("POST") //Requires API key in request servicesSubRouter.HandleFunc("/auth/credential/forgot/initiate", we.wrapFunc(we.servicesApisHandler.forgotCredentialInitiate, nil)).Methods("POST") //Requires API key in request servicesSubRouter.HandleFunc("/auth/credential/forgot/complete", we.wrapFunc(we.servicesApisHandler.forgotCredentialComplete, nil)).Methods("POST") //Public servicesSubRouter.HandleFunc("/auth/credential/update", we.wrapFunc(we.servicesApisHandler.updateCredential, we.auth.services.Authenticated)).Methods("POST") @@ -142,8 +147,9 @@ func (we Adapter) Start() { servicesSubRouter.HandleFunc("/app-configs", we.wrapFunc(we.servicesApisHandler.getApplicationConfigs, nil)).Methods("POST") //Requires API key in request servicesSubRouter.HandleFunc("/app-configs/organization", we.wrapFunc(we.servicesApisHandler.getApplicationOrgConfigs, we.auth.services.Standard)).Methods("POST") - // DEPRECATED - servicesSubRouter.HandleFunc("/application/configs", we.wrapFunc(we.servicesApisHandler.getApplicationConfigs, nil)).Methods("POST") //Requires API key in request + // Deprecated: + servicesSubRouter.HandleFunc("/auth/credential/send-verify", we.wrapFunc(we.servicesApisHandler.sendVerifyIdentifier, nil)).Methods("POST") //Requires API key in request + servicesSubRouter.HandleFunc("/application/configs", we.wrapFunc(we.servicesApisHandler.getApplicationConfigs, nil)).Methods("POST") //Requires API key in request servicesSubRouter.HandleFunc("/application/organization/configs", we.wrapFunc(we.servicesApisHandler.getApplicationOrgConfigs, we.auth.services.Standard)).Methods("POST") /// @@ -313,6 +319,11 @@ func (we Adapter) serveResetCredential(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./driver/web/ui/reset-credential.html") } +func (we Adapter) serveWebAuthnTest(w http.ResponseWriter, r *http.Request) { + w.Header().Add("access-control-allow-origin", "*") + http.ServeFile(w, r, "./driver/web/ui/webauthn-test.html") +} + func (we Adapter) serveDoc(w http.ResponseWriter, r *http.Request) { w.Header().Add("access-control-allow-origin", "*") diff --git a/driver/web/apis_admin.go b/driver/web/apis_admin.go index 36201448a..2078a856d 100644 --- a/driver/web/apis_admin.go +++ b/driver/web/apis_admin.go @@ -101,16 +101,12 @@ func (h AdminApisHandler) login(l *logs.Log, r *http.Request, claims *tokenauth. //privacy requestPrivacy := privacyFromDefNullable(requestData.Privacy) - username := "" - if requestData.Username != nil { - username = *requestData.Username - } - //device requestDevice := requestData.Device - message, loginSession, mfaTypes, err := h.coreAPIs.Auth.Login(ip, string(requestDevice.Type), requestDevice.Os, requestDevice.DeviceId, string(requestData.AuthType), - requestCreds, requestData.ApiKey, requestData.AppTypeIdentifier, requestData.OrgId, requestParams, &clientVersion, requestProfile, requestPrivacy, requestPreferences, username, true, l) + noLoginParams, loginSession, mfaTypes, err := h.coreAPIs.Auth.Login(ip, string(requestDevice.Type), requestDevice.Os, requestDevice.DeviceId, string(requestData.AuthType), + requestCreds, requestData.ApiKey, requestData.AppTypeIdentifier, requestData.OrgId, requestParams, &clientVersion, requestProfile, requestPrivacy, requestPreferences, + requestData.AccountIdentifierId, true, l) if err != nil { loggingErr, ok := err.(*errors.Error) if ok && loggingErr.Status() != "" { @@ -121,9 +117,25 @@ func (h AdminApisHandler) login(l *logs.Log, r *http.Request, claims *tokenauth. ///prepare response - //message - if message != nil { - responseData := &Def.SharedResLogin{Message: message} + //noLoginParams + if noLoginParams != nil { + var message *string + if messageVal, _ := noLoginParams["message"].(string); messageVal != "" { + message = &messageVal + } + + var paramsRes Def.SharedResLogin_Params + paramsBytes, err := json.Marshal(noLoginParams) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, logutils.MessageDataType("no login response params"), nil, err, http.StatusInternalServerError, false) + } + + err = json.Unmarshal(paramsBytes, ¶msRes) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("no login response params"), nil, err, http.StatusInternalServerError, false) + } + + responseData := &Def.SharedResLogin{Message: message, Params: ¶msRes} respData, err := json.Marshal(responseData) if err != nil { return l.HTTPResponseErrorAction(logutils.ActionMarshal, logutils.MessageDataType("auth login response"), nil, err, http.StatusInternalServerError, false) @@ -132,7 +144,7 @@ func (h AdminApisHandler) login(l *logs.Log, r *http.Request, claims *tokenauth. } if loginSession.State != "" { - paramsRes, err := convert[Def.SharedResLoginMfa_Params](loginSession.Params) + paramsRes, err := utils.JSONConvert[Def.SharedResLoginMfa_Params](loginSession.Params) if err != nil { return l.HTTPResponseErrorAction("converting", logutils.MessageDataType("auth login response params"), nil, err, http.StatusInternalServerError, false) } @@ -226,7 +238,7 @@ func (h AdminApisHandler) refresh(l *logs.Log, r *http.Request, claims *tokenaut accessToken := loginSession.AccessToken refreshToken := loginSession.CurrentRefreshToken() - paramsRes, err := convert[Def.SharedResRefresh_Params](loginSession.Params) + paramsRes, err := utils.JSONConvert[Def.SharedResRefresh_Params](loginSession.Params) if err != nil { return l.HTTPResponseErrorAction("converting", logutils.MessageDataType("auth refresh response params"), nil, err, http.StatusInternalServerError, false) } @@ -897,7 +909,7 @@ func (h AdminApisHandler) getAccount(l *logs.Log, r *http.Request, claims *token var accountData *Def.Account if account != nil { - account.SortAccountAuthTypes(claims.UID) + account.SortAccountAuthTypes("", claims.AuthType) accountData = accountToDef(*account) } @@ -942,14 +954,15 @@ func (h AdminApisHandler) createAdminAccount(l *logs.Log, r *http.Request, claim profile := profileFromDefNullable(requestData.Profile) privacy := privacyFromDefNullable(requestData.Privacy) - username := "" - if requestData.Username != nil { - username = *requestData.Username + //identifier + requestIdentifier, err := interfaceToJSON(requestData.Identifier) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, model.TypeCreds, nil, err, http.StatusBadRequest, true) } creatorPermissions := strings.Split(claims.Permissions, ",") account, params, err := h.coreAPIs.Auth.CreateAdminAccount(string(requestData.AuthType), claims.AppID, claims.OrgID, - requestData.Identifier, profile, privacy, username, permissions, roleIDs, groupIDs, scopes, creatorPermissions, &clientVersion, l) + requestIdentifier, profile, privacy, permissions, roleIDs, groupIDs, scopes, creatorPermissions, &clientVersion, l) if err != nil || account == nil { return l.HTTPResponseErrorAction(logutils.ActionCreate, model.TypeAccount, nil, err, http.StatusInternalServerError, true) } @@ -992,8 +1005,15 @@ func (h AdminApisHandler) updateAdminAccount(l *logs.Log, r *http.Request, claim if requestData.Scopes != nil { scopes = *requestData.Scopes } + + //identifier + requestIdentifier, err := interfaceToJSON(requestData.Identifier) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, model.TypeCreds, nil, err, http.StatusBadRequest, true) + } + updaterPermissions := strings.Split(claims.Permissions, ",") - account, params, err := h.coreAPIs.Auth.UpdateAdminAccount(string(requestData.AuthType), claims.AppID, claims.OrgID, requestData.Identifier, + account, params, err := h.coreAPIs.Auth.UpdateAdminAccount(string(requestData.AuthType), claims.AppID, claims.OrgID, requestIdentifier, permissions, roleIDs, groupIDs, scopes, updaterPermissions, l) if err != nil || account == nil { return l.HTTPResponseErrorAction(logutils.ActionUpdate, model.TypeAccount, nil, err, http.StatusInternalServerError, true) diff --git a/driver/web/apis_services.go b/driver/web/apis_services.go index 5442287a5..925c95b95 100644 --- a/driver/web/apis_services.go +++ b/driver/web/apis_services.go @@ -20,6 +20,7 @@ import ( Def "core-building-block/driver/web/docs/gen" "core-building-block/utils" "encoding/json" + "fmt" "io/ioutil" "net/http" "strconv" @@ -31,6 +32,8 @@ import ( "github.com/rokwire/logging-library-go/v2/errors" "github.com/rokwire/logging-library-go/v2/logs" "github.com/rokwire/logging-library-go/v2/logutils" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) // ServicesApisHandler handles the rest APIs implementation @@ -81,16 +84,12 @@ func (h ServicesApisHandler) login(l *logs.Log, r *http.Request, claims *tokenau // privacy requestPrivacy := privacyFromDefNullable(requestData.Privacy) - username := "" - if requestData.Username != nil { - username = *requestData.Username - } - //device requestDevice := requestData.Device - message, loginSession, mfaTypes, err := h.coreAPIs.Auth.Login(ip, string(requestDevice.Type), requestDevice.Os, requestDevice.DeviceId, string(requestData.AuthType), - requestCreds, requestData.ApiKey, requestData.AppTypeIdentifier, requestData.OrgId, requestParams, &clientVersion, requestProfile, requestPrivacy, requestPreferences, username, false, l) + noLoginParams, loginSession, mfaTypes, err := h.coreAPIs.Auth.Login(ip, string(requestDevice.Type), requestDevice.Os, requestDevice.DeviceId, string(requestData.AuthType), + requestCreds, requestData.ApiKey, requestData.AppTypeIdentifier, requestData.OrgId, requestParams, &clientVersion, requestProfile, requestPrivacy, requestPreferences, + requestData.AccountIdentifierId, false, l) if err != nil { loggingErr, ok := err.(*errors.Error) if ok && loggingErr.Status() != "" { @@ -101,9 +100,25 @@ func (h ServicesApisHandler) login(l *logs.Log, r *http.Request, claims *tokenau ///prepare response - //message - if message != nil { - responseData := &Def.SharedResLogin{Message: message} + //noLoginParams + if noLoginParams != nil { + var message *string + if messageVal, _ := noLoginParams["message"].(string); messageVal != "" { + message = &messageVal + } + + var paramsRes Def.SharedResLogin_Params + paramsBytes, err := json.Marshal(noLoginParams) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, logutils.MessageDataType("no login response params"), nil, err, http.StatusInternalServerError, false) + } + + err = json.Unmarshal(paramsBytes, ¶msRes) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("no login response params"), nil, err, http.StatusInternalServerError, false) + } + + responseData := &Def.SharedResLogin{Message: message, Params: ¶msRes} respData, err := json.Marshal(responseData) if err != nil { return l.HTTPResponseErrorAction(logutils.ActionMarshal, logutils.MessageDataType("auth login response"), nil, err, http.StatusInternalServerError, false) @@ -112,7 +127,7 @@ func (h ServicesApisHandler) login(l *logs.Log, r *http.Request, claims *tokenau } if loginSession.State != "" { - paramsRes, err := convert[Def.SharedResLoginMfa_Params](loginSession.Params) + paramsRes, err := utils.JSONConvert[Def.SharedResLoginMfa_Params](loginSession.Params) if err != nil { return l.HTTPResponseErrorAction("converting", logutils.MessageDataType("auth login response params"), nil, err, http.StatusInternalServerError, false) } @@ -180,7 +195,7 @@ func (h ServicesApisHandler) refresh(l *logs.Log, r *http.Request, claims *token accessToken := loginSession.AccessToken refreshToken := loginSession.CurrentRefreshToken() - paramsRes, err := convert[Def.SharedResRefresh_Params](loginSession.Params) + paramsRes, err := utils.JSONConvert[Def.SharedResRefresh_Params](loginSession.Params) if err != nil { return l.HTTPResponseErrorAction("converting", logutils.MessageDataType("auth refresh response params"), nil, err, http.StatusInternalServerError, false) } @@ -234,7 +249,20 @@ func (h ServicesApisHandler) accountExists(l *logs.Log, r *http.Request, claims return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.TypeRequest, nil, err, http.StatusBadRequest, true) } - accountExists, err := h.coreAPIs.Auth.AccountExists(string(requestData.AuthType), requestData.UserIdentifier, requestData.ApiKey, requestData.AppTypeIdentifier, requestData.OrgId) + //identifier + requestIdentifier, err := interfaceToJSON(requestData.Identifier) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, model.TypeCreds, nil, err, http.StatusBadRequest, true) + } + + //auth type + var authType *string + if requestData.AuthType != nil { + authTypeStr := string(*requestData.AuthType) + authType = &authTypeStr + } + + accountExists, err := h.coreAPIs.Auth.AccountExists(requestIdentifier, requestData.ApiKey, requestData.AppTypeIdentifier, requestData.OrgId, authType, requestData.UserIdentifier) if err != nil { return l.HTTPResponseErrorAction(logutils.ActionGet, logutils.MessageDataType("account exists"), nil, err, http.StatusInternalServerError, false) } @@ -259,7 +287,20 @@ func (h ServicesApisHandler) canSignIn(l *logs.Log, r *http.Request, claims *tok return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.TypeRequest, nil, err, http.StatusBadRequest, true) } - canSignIn, err := h.coreAPIs.Auth.CanSignIn(string(requestData.AuthType), requestData.UserIdentifier, requestData.ApiKey, requestData.AppTypeIdentifier, requestData.OrgId) + //identifier + requestIdentifier, err := interfaceToJSON(requestData.Identifier) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, model.TypeCreds, nil, err, http.StatusBadRequest, true) + } + + //auth type + var authType *string + if requestData.AuthType != nil { + authTypeStr := string(*requestData.AuthType) + authType = &authTypeStr + } + + canSignIn, err := h.coreAPIs.Auth.CanSignIn(requestIdentifier, requestData.ApiKey, requestData.AppTypeIdentifier, requestData.OrgId, authType, requestData.UserIdentifier) if err != nil { return l.HTTPResponseErrorAction(logutils.ActionGet, logutils.MessageDataType("can sign in"), nil, err, http.StatusInternalServerError, false) } @@ -284,7 +325,20 @@ func (h ServicesApisHandler) canLink(l *logs.Log, r *http.Request, claims *token return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.TypeRequest, nil, err, http.StatusBadRequest, true) } - canLink, err := h.coreAPIs.Auth.CanLink(string(requestData.AuthType), requestData.UserIdentifier, requestData.ApiKey, requestData.AppTypeIdentifier, requestData.OrgId) + //identifier + requestIdentifier, err := interfaceToJSON(requestData.Identifier) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, model.TypeCreds, nil, err, http.StatusBadRequest, true) + } + + //auth type + var authType *string + if requestData.AuthType != nil { + authTypeStr := string(*requestData.AuthType) + authType = &authTypeStr + } + + canLink, err := h.coreAPIs.Auth.CanLink(requestIdentifier, requestData.ApiKey, requestData.AppTypeIdentifier, requestData.OrgId, authType, requestData.UserIdentifier) if err != nil { return l.HTTPResponseErrorAction(logutils.ActionGet, logutils.MessageDataType("can link"), nil, err, http.StatusInternalServerError, false) } @@ -297,14 +351,46 @@ func (h ServicesApisHandler) canLink(l *logs.Log, r *http.Request, claims *token return l.HTTPResponseSuccessJSON(respData) } -func (h ServicesApisHandler) linkAccountAuthType(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { - data, err := ioutil.ReadAll(r.Body) +func (h ServicesApisHandler) signInOptions(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { + var requestData Def.SharedReqAccountCheck + err := json.NewDecoder(r.Body).Decode(&requestData) if err != nil { - return l.HTTPResponseErrorAction(logutils.ActionRead, logutils.TypeRequestBody, nil, err, http.StatusBadRequest, false) + return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.TypeRequest, nil, err, http.StatusBadRequest, true) + } + + //identifier + requestIdentifier, err := interfaceToJSON(requestData.Identifier) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, model.TypeCreds, nil, err, http.StatusBadRequest, true) + } + + //auth type + var authType *string + if requestData.AuthType != nil { + authTypeStr := string(*requestData.AuthType) + authType = &authTypeStr + } + + identifiers, authTypes, err := h.coreAPIs.Auth.SignInOptions(requestIdentifier, requestData.ApiKey, requestData.AppTypeIdentifier, requestData.OrgId, authType, requestData.UserIdentifier, l) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionGet, logutils.MessageDataType("sign-in options"), nil, err, http.StatusInternalServerError, false) } + respIdentifiers := accountIdentifiersToDef(identifiers) + respAuthTypes := accountAuthTypesToDef(authTypes) + resp := Def.SharedResSignInOptions{Identifiers: respIdentifiers, AuthTypes: respAuthTypes} + + respData, err := json.Marshal(resp) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, logutils.TypeResponse, nil, err, http.StatusInternalServerError, false) + } + + return l.HTTPResponseSuccessJSON(respData) +} + +func (h ServicesApisHandler) linkAccountAuthType(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { var requestData Def.ServicesReqAccountAuthTypeLink - err = json.Unmarshal(data, &requestData) + err := json.NewDecoder(r.Body).Decode(&requestData) if err != nil { return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("account auth type link request"), nil, err, http.StatusBadRequest, true) } @@ -326,13 +412,15 @@ func (h ServicesApisHandler) linkAccountAuthType(l *logs.Log, r *http.Request, c return l.HTTPResponseError("Error linking account auth type", err, http.StatusInternalServerError, true) } + identifiers := make([]Def.AccountIdentifier, 0) authTypes := make([]Def.AccountAuthType, 0) if account != nil { - account.SortAccountAuthTypes(claims.UID) - authTypes = accountAuthTypesToDef(account.AuthTypes) + account.SortAccountAuthTypes("", claims.AuthType) + identifiers = accountIdentifiersToDef(account.Identifiers) + authTypes = accountAuthTypesToDefLegacy(account) } - responseData := &Def.ServicesResAccountAuthTypeLink{AuthTypes: authTypes, Message: message} + responseData := &Def.ServicesResAccountAuthTypeLink{Identifiers: &identifiers, AuthTypes: authTypes, Message: message} respData, err := json.Marshal(responseData) if err != nil { return l.HTTPResponseErrorAction(logutils.ActionMarshal, "link account auth type response", nil, err, http.StatusInternalServerError, false) @@ -342,33 +430,31 @@ func (h ServicesApisHandler) linkAccountAuthType(l *logs.Log, r *http.Request, c } func (h ServicesApisHandler) unlinkAccountAuthType(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { - data, err := ioutil.ReadAll(r.Body) - if err != nil { - return l.HTTPResponseErrorAction(logutils.ActionRead, logutils.TypeRequestBody, nil, err, http.StatusBadRequest, false) - } - var requestData Def.ServicesReqAccountAuthTypeUnlink - err = json.Unmarshal(data, &requestData) + err := json.NewDecoder(r.Body).Decode(&requestData) if err != nil { return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("account auth type unlink request"), nil, err, http.StatusBadRequest, true) } - if string(requestData.AuthType) == claims.AuthType && requestData.Identifier == claims.UID { - return l.HTTPResponseError("May not unlink account auth type currently in use", nil, http.StatusBadRequest, false) + var authType *string + if requestData.AuthType != nil { + authTypeStr := string(*requestData.AuthType) + authType = &authTypeStr } - - account, err := h.coreAPIs.Auth.UnlinkAccountAuthType(claims.Subject, string(requestData.AuthType), requestData.AppTypeIdentifier, requestData.Identifier, l) + account, err := h.coreAPIs.Auth.UnlinkAccountAuthType(claims.Subject, requestData.Id, authType, requestData.Identifier, false, l) if err != nil { return l.HTTPResponseError("Error unlinking account auth type", err, http.StatusInternalServerError, true) } + identifiers := make([]Def.AccountIdentifier, 0) authTypes := make([]Def.AccountAuthType, 0) if account != nil { - account.SortAccountAuthTypes(claims.UID) - authTypes = accountAuthTypesToDef(account.AuthTypes) + account.SortAccountAuthTypes("", claims.AuthType) + identifiers = accountIdentifiersToDef(account.Identifiers) + authTypes = accountAuthTypesToDefLegacy(account) } - responseData := &Def.ServicesResAccountAuthTypeLink{AuthTypes: authTypes} + responseData := &Def.ServicesResAccountAuthTypeLink{Identifiers: &identifiers, AuthTypes: authTypes} respData, err := json.Marshal(responseData) if err != nil { return l.HTTPResponseErrorAction(logutils.ActionMarshal, "unlink account auth type response", nil, err, http.StatusInternalServerError, false) @@ -377,6 +463,64 @@ func (h ServicesApisHandler) unlinkAccountAuthType(l *logs.Log, r *http.Request, return l.HTTPResponseSuccessJSON(respData) } +func (h ServicesApisHandler) linkAccountIdentifier(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { + var requestData Def.ServicesReqAccountIdentifierLink + err := json.NewDecoder(r.Body).Decode(&requestData) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("account identifier link request"), nil, err, http.StatusBadRequest, true) + } + + //identifier + requestIdentifier, err := interfaceToJSON(requestData.Identifier) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, model.TypeCreds, nil, err, http.StatusBadRequest, true) + } + + message, account, err := h.coreAPIs.Auth.LinkAccountIdentifier(claims.Subject, requestIdentifier, false, l) + if err != nil { + return l.HTTPResponseError("Error linking account identifier", err, http.StatusInternalServerError, true) + } + + identifiers := make([]Def.AccountIdentifier, 0) + if account != nil { + identifiers = accountIdentifiersToDef(account.Identifiers) + } + + responseData := &Def.ServicesResAccountIdentifierLink{Identifiers: identifiers, Message: message} + respData, err := json.Marshal(responseData) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, "link account identifier response", nil, err, http.StatusInternalServerError, false) + } + + return l.HTTPResponseSuccessJSON(respData) +} + +func (h ServicesApisHandler) unlinkAccountIdentifier(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { + var requestData Def.ServicesReqAccountIdentifierUnlink + err := json.NewDecoder(r.Body).Decode(&requestData) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("account identifier unlink request"), nil, err, http.StatusBadRequest, true) + } + + account, err := h.coreAPIs.Auth.UnlinkAccountIdentifier(claims.Subject, requestData.Id, false, l) + if err != nil { + return l.HTTPResponseError("Error unlinking account identifier", err, http.StatusInternalServerError, true) + } + + identifiers := make([]Def.AccountIdentifier, 0) + if account != nil { + identifiers = accountIdentifiersToDef(account.Identifiers) + } + + responseData := &Def.ServicesResAccountIdentifierLink{Identifiers: identifiers} + respData, err := json.Marshal(responseData) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, "unlink account identifier response", nil, err, http.StatusInternalServerError, false) + } + + return l.HTTPResponseSuccessJSON(respData) +} + func (h ServicesApisHandler) authorizeService(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { data, err := ioutil.ReadAll(r.Body) if err != nil { @@ -451,7 +595,7 @@ func (h ServicesApisHandler) getAccount(l *logs.Log, r *http.Request, claims *to var accountData *Def.Account if account != nil { - account.SortAccountAuthTypes(claims.UID) + account.SortAccountAuthTypes("", claims.AuthType) accountData = accountToDef(*account) } @@ -496,14 +640,15 @@ func (h ServicesApisHandler) createAdminAccount(l *logs.Log, r *http.Request, cl profile := profileFromDefNullable(requestData.Profile) privacy := privacyFromDefNullable(requestData.Privacy) - username := "" - if requestData.Username != nil { - username = *requestData.Username + //identifier + requestIdentifier, err := interfaceToJSON(requestData.Identifier) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, model.TypeCreds, nil, err, http.StatusBadRequest, true) } creatorPermissions := strings.Split(claims.Permissions, ",") account, params, err := h.coreAPIs.Auth.CreateAdminAccount(string(requestData.AuthType), claims.AppID, claims.OrgID, - requestData.Identifier, profile, privacy, username, permissions, roleIDs, groupIDs, scopes, creatorPermissions, &clientVersion, l) + requestIdentifier, profile, privacy, permissions, roleIDs, groupIDs, scopes, creatorPermissions, &clientVersion, l) if err != nil || account == nil { return l.HTTPResponseErrorAction(logutils.ActionCreate, model.TypeAccount, nil, err, http.StatusInternalServerError, true) } @@ -546,8 +691,15 @@ func (h ServicesApisHandler) updateAdminAccount(l *logs.Log, r *http.Request, cl if requestData.Scopes != nil { scopes = *requestData.Scopes } + + //identifier + requestIdentifier, err := interfaceToJSON(requestData.Identifier) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionMarshal, model.TypeCreds, nil, err, http.StatusBadRequest, true) + } + updaterPermissions := strings.Split(claims.Permissions, ",") - account, params, err := h.coreAPIs.Auth.UpdateAdminAccount(string(requestData.AuthType), claims.AppID, claims.OrgID, requestData.Identifier, + account, params, err := h.coreAPIs.Auth.UpdateAdminAccount(string(requestData.AuthType), claims.AppID, claims.OrgID, requestIdentifier, permissions, roleIDs, groupIDs, scopes, updaterPermissions, l) if err != nil || account == nil { return l.HTTPResponseErrorAction(logutils.ActionUpdate, model.TypeAccount, nil, err, http.StatusInternalServerError, true) @@ -634,6 +786,16 @@ func (h ServicesApisHandler) getProfile(l *logs.Log, r *http.Request, claims *to profileResp := profileToDef(profile) + // maintain backwards compatibility + if len(profile.Accounts) == 1 { + if emailIdentifier := profile.Accounts[0].GetAccountIdentifier("email", ""); emailIdentifier != nil { + profileResp.Email = &emailIdentifier.Identifier + } + if phoneIdentifier := profile.Accounts[0].GetAccountIdentifier("phone", ""); phoneIdentifier != nil { + profileResp.Phone = &phoneIdentifier.Identifier + } + } + data, err := json.Marshal(profileResp) if err != nil { return l.HTTPResponseErrorAction(logutils.ActionMarshal, model.TypeProfile, nil, err, http.StatusInternalServerError, false) @@ -1006,25 +1168,6 @@ func (h ServicesApisHandler) getTest(l *logs.Log, r *http.Request, claims *token return l.HTTPResponseSuccessMessage(res) } -// Handler for verify endpoint -func (h ServicesApisHandler) verifyCredential(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { - id := r.URL.Query().Get("id") - if id == "" { - return l.HTTPResponseErrorData(logutils.StatusMissing, logutils.TypeQueryParam, logutils.StringArgs("id"), nil, http.StatusBadRequest, false) - } - - code := r.URL.Query().Get("code") - if code == "" { - return l.HTTPResponseErrorData(logutils.StatusMissing, logutils.TypeQueryParam, logutils.StringArgs("code"), nil, http.StatusBadRequest, false) - } - - if err := h.coreAPIs.Auth.VerifyCredential(id, code, l); err != nil { - return l.HTTPResponseErrorAction(logutils.ActionValidate, "code", nil, err, http.StatusInternalServerError, false) - } - - return l.HTTPResponseSuccessMessage("Code verified successfully!") -} - func (h ServicesApisHandler) getApplicationConfigs(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { data, err := ioutil.ReadAll(r.Body) if err != nil { @@ -1089,18 +1232,12 @@ func (h ServicesApisHandler) getApplicationOrgConfigs(l *logs.Log, r *http.Reque return l.HTTPResponseSuccessJSON(response) } -// Handler for reset password endpoint from client application +// Handler for reset credential endpoint from client application func (h ServicesApisHandler) updateCredential(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { - accountID := claims.Subject - data, err := ioutil.ReadAll(r.Body) - if err != nil { - return l.HTTPResponseErrorAction(logutils.ActionRead, logutils.TypeRequestBody, nil, err, http.StatusBadRequest, false) - } - var requestData Def.ServicesReqCredentialUpdate - err = json.Unmarshal(data, &requestData) + err := json.NewDecoder(r.Body).Decode(&requestData) if err != nil { - return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("auth reset password client request"), nil, err, http.StatusBadRequest, true) + return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("auth reset credential client request"), nil, err, http.StatusBadRequest, true) } //params @@ -1109,24 +1246,19 @@ func (h ServicesApisHandler) updateCredential(l *logs.Log, r *http.Request, clai return l.HTTPResponseErrorAction(logutils.ActionMarshal, "params", nil, err, http.StatusBadRequest, true) } - if err := h.coreAPIs.Auth.UpdateCredential(accountID, requestData.AccountAuthTypeId, requestParams, l); err != nil { - return l.HTTPResponseErrorAction(logutils.ActionUpdate, "password", nil, err, http.StatusInternalServerError, false) + if err := h.coreAPIs.Auth.UpdateCredential(claims.Subject, requestData.AccountAuthTypeId, requestParams, l); err != nil { + return l.HTTPResponseErrorAction(logutils.ActionUpdate, "credential", nil, err, http.StatusInternalServerError, false) } - return l.HTTPResponseSuccessMessage("Reset Password from client successfully") + return l.HTTPResponseSuccessMessage("Reset credential from client successfully") } -// Handler for reset password endpoint from reset link +// Handler for reset credential endpoint from reset link func (h ServicesApisHandler) forgotCredentialComplete(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { - data, err := ioutil.ReadAll(r.Body) - if err != nil { - return l.HTTPResponseErrorAction(logutils.ActionRead, logutils.TypeRequestBody, nil, err, http.StatusBadRequest, false) - } - var requestData Def.ServicesReqCredentialForgotComplete - err = json.Unmarshal(data, &requestData) + err := json.NewDecoder(r.Body).Decode(&requestData) if err != nil { - return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("auth reset password link request"), nil, err, http.StatusBadRequest, true) + return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("auth reset credential link request"), nil, err, http.StatusBadRequest, true) } //params @@ -1136,47 +1268,91 @@ func (h ServicesApisHandler) forgotCredentialComplete(l *logs.Log, r *http.Reque } if err := h.coreAPIs.Auth.ResetForgotCredential(requestData.CredentialId, requestData.ResetCode, requestParams, l); err != nil { - return l.HTTPResponseErrorAction(logutils.ActionUpdate, "password", nil, err, http.StatusInternalServerError, false) + return l.HTTPResponseErrorAction(logutils.ActionUpdate, "credential", nil, err, http.StatusInternalServerError, false) } - return l.HTTPResponseSuccessMessage("Reset Password from link successfully") + return l.HTTPResponseSuccessMessage("Reset credential from link successfully") } // Handler for forgot credential endpoint func (h ServicesApisHandler) forgotCredentialInitiate(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { - data, err := ioutil.ReadAll(r.Body) + var requestData Def.ServicesReqCredentialForgotInitiate + err := json.NewDecoder(r.Body).Decode(&requestData) if err != nil { - return l.HTTPResponseErrorAction(logutils.ActionRead, logutils.TypeRequestBody, nil, err, http.StatusBadRequest, false) + return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("auth reset credential request"), nil, err, http.StatusBadRequest, true) } - var requestData Def.ServicesReqCredentialForgotInitiate - err = json.Unmarshal(data, &requestData) + var requestIdentifier interface{} + if identifier, err := requestData.Identifier.AsSharedReqIdentifierString(); err == nil { + requestIdentifier = map[string]string{string(requestData.AuthType): identifier} + } else if identifier, err := requestData.Identifier.AsSharedReqIdentifiers(); err == nil { + requestIdentifier = identifier + } else { + return l.HTTPResponseErrorData(logutils.StatusInvalid, logutils.MessageDataType("auth reset credential identifier"), nil, err, http.StatusBadRequest, true) + } + + //identifier + identifierJSON, err := interfaceToJSON(requestIdentifier) if err != nil { - return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("auth reset password request"), nil, err, http.StatusBadRequest, true) + return l.HTTPResponseErrorAction(logutils.ActionMarshal, model.TypeCreds, nil, err, http.StatusBadRequest, true) } - if err := h.coreAPIs.Auth.ForgotCredential(string(requestData.AuthType), requestData.AppTypeIdentifier, - requestData.OrgId, requestData.ApiKey, requestData.Identifier, l); err != nil { - return l.HTTPResponseErrorAction(logutils.ActionSend, "forgot password link", nil, err, http.StatusInternalServerError, false) + if err := h.coreAPIs.Auth.ForgotCredential(string(requestData.AuthType), identifierJSON, requestData.AppTypeIdentifier, requestData.OrgId, requestData.ApiKey, l); err != nil { + return l.HTTPResponseErrorAction(logutils.ActionSend, "forgot credential link", nil, err, http.StatusInternalServerError, false) } - return l.HTTPResponseSuccessMessage("Sent forgot password link successfully") + return l.HTTPResponseSuccessMessage("Sent forgot credential link successfully") +} + +// Handler for verify endpoint +func (h ServicesApisHandler) verifyIdentifier(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { + id := r.URL.Query().Get("id") + if id == "" { + return l.HTTPResponseErrorData(logutils.StatusMissing, logutils.TypeQueryParam, logutils.StringArgs("id"), nil, http.StatusBadRequest, false) + } + + code := r.URL.Query().Get("code") + if code == "" { + return l.HTTPResponseErrorData(logutils.StatusMissing, logutils.TypeQueryParam, logutils.StringArgs("code"), nil, http.StatusBadRequest, false) + } + + accountIdentifier, err := h.coreAPIs.Auth.VerifyIdentifier(id, code, l) + if err != nil { + return l.HTTPResponseErrorAction(logutils.ActionValidate, model.TypeAccountIdentifier, nil, err, http.StatusInternalServerError, false) + } + + identifierStr := "Account identifier" + if accountIdentifier != nil && accountIdentifier.Code != "" { + identifierStr = cases.Title(language.English).String(accountIdentifier.Code) + } + return l.HTTPResponseSuccessMessage(fmt.Sprintf("%s verified successfully!", identifierStr)) } // Handler for resending verify code -func (h ServicesApisHandler) sendVerifyCredential(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { - data, err := ioutil.ReadAll(r.Body) +func (h ServicesApisHandler) sendVerifyIdentifier(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse { + var requestData Def.ServicesReqIdentifierSendVerify + err := json.NewDecoder(r.Body).Decode(&requestData) if err != nil { - return l.HTTPResponseErrorAction(logutils.ActionRead, logutils.TypeRequestBody, nil, err, http.StatusBadRequest, false) + return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("auth resend verification request"), nil, err, http.StatusBadRequest, true) } - var requestData Def.ServicesReqCredentialSendVerify - err = json.Unmarshal(data, &requestData) + var requestIdentifier interface{} + if identifier, err := requestData.Identifier.AsSharedReqIdentifierString(); err == nil && requestData.AuthType != nil { + authType := string(*requestData.AuthType) + requestIdentifier = map[string]string{authType: identifier} + } else if identifier, err := requestData.Identifier.AsSharedReqIdentifiers(); err == nil { + requestIdentifier = identifier + } else { + return l.HTTPResponseErrorData(logutils.StatusInvalid, logutils.MessageDataType("auth resend verification identifier"), nil, err, http.StatusBadRequest, true) + } + + //identifier + identifierJSON, err := interfaceToJSON(requestIdentifier) if err != nil { - return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("auth resend verify code request"), nil, err, http.StatusBadRequest, true) + return l.HTTPResponseErrorAction(logutils.ActionMarshal, model.TypeCreds, nil, err, http.StatusBadRequest, true) } - if err := h.coreAPIs.Auth.SendVerifyCredential(string(requestData.AuthType), requestData.AppTypeIdentifier, requestData.OrgId, requestData.ApiKey, requestData.Identifier, l); err != nil { + if err := h.coreAPIs.Auth.SendVerifyIdentifier(requestData.AppTypeIdentifier, requestData.OrgId, requestData.ApiKey, identifierJSON, l); err != nil { return l.HTTPResponseErrorAction(logutils.ActionSend, "code", nil, err, http.StatusInternalServerError, false) } diff --git a/driver/web/conversions.go b/driver/web/conversions.go index 654a5268c..6dbf07806 100644 --- a/driver/web/conversions.go +++ b/driver/web/conversions.go @@ -16,43 +16,12 @@ package web import ( "encoding/json" - "reflect" "time" "github.com/rokwire/logging-library-go/v2/errors" "github.com/rokwire/logging-library-go/v2/logutils" ) -func convert[T any, F any](val F) (*T, error) { - if isNil(val) { - return nil, nil - } - - bytes, err := json.Marshal(val) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, "value", nil, err) - } - - var out T - err = json.Unmarshal(bytes, &out) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, "value", nil, err) - } - - return &out, nil -} - -func isNil(i interface{}) bool { - if i == nil { - return true - } - switch reflect.TypeOf(i).Kind() { - case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: - return reflect.ValueOf(i).IsNil() - } - return false -} - func defString(pointer *string) string { if pointer == nil { return "" diff --git a/driver/web/conversions_application.go b/driver/web/conversions_application.go index d484eb091..3c21c92af 100644 --- a/driver/web/conversions_application.go +++ b/driver/web/conversions_application.go @@ -245,8 +245,12 @@ func supportedAuthTypeFromDef(item *Def.SupportedAuthTypes) *model.AuthTypesSupp supportedAuthTypes := []model.SupportedAuthType{} if item.SupportedAuthTypes != nil { for _, authType := range *item.SupportedAuthTypes { - if authType.AuthTypeId != nil && authType.Params != nil { - supportedAuthTypes = append(supportedAuthTypes, model.SupportedAuthType{AuthTypeID: *authType.AuthTypeId, Params: *authType.Params}) + var params map[string]interface{} + if authType.Params != nil { + params = *authType.Params + } + if authType.AuthTypeId != nil { + supportedAuthTypes = append(supportedAuthTypes, model.SupportedAuthType{AuthTypeID: *authType.AuthTypeId, Params: params}) } } } @@ -339,6 +343,14 @@ func identityProviderSettingFromDef(item *Def.IdentityProviderSettings) *model.I if item.ExternalIdFields != nil { externalIDFields = *item.ExternalIdFields } + var sensitiveExternalIDs []string + if item.SensitiveExternalIds != nil { + sensitiveExternalIDs = *item.SensitiveExternalIds + } + var isEmailVerified bool + if item.IsEmailVerified != nil { + isEmailVerified = *item.IsEmailVerified + } var roles map[string]string if item.Roles != nil { roles = *item.Roles @@ -357,9 +369,10 @@ func identityProviderSettingFromDef(item *Def.IdentityProviderSettings) *model.I } return &model.IdentityProviderSetting{IdentityProviderID: item.IdentityProviderId, UserIdentifierField: item.UserIdentifierField, - ExternalIDFields: externalIDFields, FirstNameField: firstNameField, MiddleNameField: middleNameField, - LastNameField: lastNameField, EmailField: emailField, RolesField: rolesField, GroupsField: groupsField, - UserSpecificFields: userSpecificFields, Roles: roles, Groups: groups, AlwaysSyncProfile: alwaysSyncProfile, IdentityBBBaseURL: identityBBBaseURL} + ExternalIDFields: externalIDFields, SensitiveExternalIDs: sensitiveExternalIDs, IsEmailVerified: isEmailVerified, + FirstNameField: firstNameField, MiddleNameField: middleNameField, LastNameField: lastNameField, EmailField: emailField, RolesField: rolesField, + GroupsField: groupsField, UserSpecificFields: userSpecificFields, Roles: roles, Groups: groups, AlwaysSyncProfile: alwaysSyncProfile, + IdentityBBBaseURL: identityBBBaseURL} } func identityProviderSettingsToDef(items []model.IdentityProviderSetting) []Def.IdentityProviderSettings { @@ -382,6 +395,8 @@ func identityProviderSettingToDef(item *model.IdentityProviderSetting) *Def.Iden } externalIDs := item.ExternalIDFields + sensitiveExternalIDs := item.SensitiveExternalIDs + isEmailVerified := item.IsEmailVerified roles := item.Roles groups := item.Groups @@ -395,9 +410,10 @@ func identityProviderSettingToDef(item *model.IdentityProviderSetting) *Def.Iden alwaysSyncProfile := item.AlwaysSyncProfile identityBBBaseURL := item.IdentityBBBaseURL return &Def.IdentityProviderSettings{IdentityProviderId: item.IdentityProviderID, UserIdentifierField: item.UserIdentifierField, - ExternalIdFields: &externalIDs, FirstNameField: &firstNameField, MiddleNameField: &middleNameField, - LastNameField: &lastNameField, EmailField: &emailField, RolesField: &rolesField, GroupsField: &groupsField, - UserSpecificFields: &userSpecificFields, Roles: &roles, Groups: &groups, AlwaysSyncProfile: &alwaysSyncProfile, IdentityBbBaseUrl: &identityBBBaseURL} + ExternalIdFields: &externalIDs, SensitiveExternalIds: &sensitiveExternalIDs, IsEmailVerified: &isEmailVerified, + FirstNameField: &firstNameField, MiddleNameField: &middleNameField, LastNameField: &lastNameField, EmailField: &emailField, RolesField: &rolesField, + GroupsField: &groupsField, UserSpecificFields: &userSpecificFields, Roles: &roles, Groups: &groups, AlwaysSyncProfile: &alwaysSyncProfile, + IdentityBbBaseUrl: &identityBBBaseURL} } // AppOrgRole diff --git a/driver/web/conversions_auth.go b/driver/web/conversions_auth.go index 0f5436a6e..16a9b9c97 100644 --- a/driver/web/conversions_auth.go +++ b/driver/web/conversions_auth.go @@ -32,12 +32,6 @@ import ( // LoginSession func loginSessionToDef(item model.LoginSession) Def.LoginSession { - var accountAuthTypeID *string - var accountAuthTypeIdentifier *string - if item.AccountAuthType != nil { - accountAuthTypeID = &item.AccountAuthType.ID - accountAuthTypeIdentifier = &item.AccountAuthType.Identifier - } var deviceID *string if item.Device != nil { deviceID = &item.Device.ID @@ -52,8 +46,7 @@ func loginSessionToDef(item model.LoginSession) Def.LoginSession { dateRefreshed := utils.FormatTime(item.DateRefreshed) dateUpdated := utils.FormatTime(item.DateUpdated) dateCreated := utils.FormatTime(&item.DateCreated) - return Def.LoginSession{Id: &item.ID, Anonymous: &item.Anonymous, AuthTypeCode: &authTypeCode, AppOrgId: &appOrgID, - AccountAuthTypeId: accountAuthTypeID, AccountAuthTypeIdentifier: accountAuthTypeIdentifier, AppTypeId: &appTypeID, + return Def.LoginSession{Id: &item.ID, Anonymous: &item.Anonymous, AuthTypeCode: &authTypeCode, AppOrgId: &appOrgID, AppTypeId: &appTypeID, AppTypeIdentifier: &appTypeIdentifier, DeviceId: deviceID, Identifier: &item.Identifier, IpAddress: &item.IPAddress, RefreshTokensCount: &refreshTokensCount, State: &item.State, MfaAttempts: &item.MfaAttempts, StateExpires: &stateExpires, DateRefreshed: &dateRefreshed, DateUpdated: &dateUpdated, DateCreated: &dateCreated, diff --git a/driver/web/conversions_config.go b/driver/web/conversions_config.go index d4476a9d0..c0438d1e8 100644 --- a/driver/web/conversions_config.go +++ b/driver/web/conversions_config.go @@ -75,15 +75,12 @@ func configFromDef(item Def.AdminReqCreateUpdateConfig, claims *tokenauth.Claims orgID = authutils.AllOrgs } - var configData interface{} - configBytes, err := json.Marshal(item.Data) + configData, err := utils.JSONConvert[interface{}, Def.AdminReqCreateUpdateConfig_Data](item.Data) if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, model.TypeConfig, nil, err) + return nil, errors.WrapErrorAction(logutils.ActionParse, model.TypeConfig, nil, err) } - - err = json.Unmarshal(configBytes, &configData) - if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, model.TypeConfig, nil, err) + if configData == nil { + return nil, errors.ErrorData(logutils.StatusInvalid, model.TypeConfigData, nil) } return &model.Config{Type: item.Type, AppID: appID, OrgID: orgID, System: item.System, Data: configData}, nil } diff --git a/driver/web/conversions_user.go b/driver/web/conversions_user.go index f976c913d..b64c8da3b 100644 --- a/driver/web/conversions_user.go +++ b/driver/web/conversions_user.go @@ -37,17 +37,9 @@ func accountToDef(item model.Account) *Def.Account { //groups groups := accountGroupsToDef(item.GetActiveGroups()) //account auth types - authTypes := accountAuthTypesToDef(item.AuthTypes) - //external ids - externalIds := map[string]interface{}{} - for k, v := range item.ExternalIDs { - externalIds[k] = v - } - //username - var username *string - if item.Username != "" { - username = &item.Username - } + authTypes := accountAuthTypesToDefLegacy(&item) + //account identifiers + identifiers := accountIdentifiersToDef(item.Identifiers) //account usage information lastLoginDate := utils.FormatTime(item.LastLoginDate) lastAccessTokenDate := utils.FormatTime(item.LastAccessTokenDate) @@ -57,9 +49,21 @@ func accountToDef(item model.Account) *Def.Account { scopes = []string{} } + // maintain backwards compatibility + var username *string + if usernameIdentifier := item.GetAccountIdentifier("username", ""); usernameIdentifier != nil { + username = &usernameIdentifier.Identifier + } + if emailIdentifier := item.GetAccountIdentifier("email", ""); emailIdentifier != nil { + profile.Email = &emailIdentifier.Identifier + } + if phoneIdentifier := item.GetAccountIdentifier("phone", ""); phoneIdentifier != nil { + profile.Phone = &phoneIdentifier.Identifier + } + return &Def.Account{Id: &item.ID, Anonymous: &item.Anonymous, System: &item.AppOrg.Organization.System, Permissions: &permissions, Roles: &roles, Groups: &groups, - Privacy: privacy, Verified: &item.Verified, Scopes: &scopes, AuthTypes: &authTypes, Username: username, Profile: profile, Preferences: preferences, SystemConfigs: systemConfigs, - LastLoginDate: &lastLoginDate, LastAccessTokenDate: &lastAccessTokenDate, MostRecentClientVersion: item.MostRecentClientVersion, ExternalIds: &externalIds} + Privacy: privacy, Verified: &item.Verified, Scopes: &scopes, Identifiers: &identifiers, AuthTypes: &authTypes, Profile: profile, Preferences: preferences, + SystemConfigs: systemConfigs, LastLoginDate: &lastLoginDate, LastAccessTokenDate: &lastAccessTokenDate, MostRecentClientVersion: item.MostRecentClientVersion, Username: username} } func accountsToDef(items []model.Account) []Def.Account { @@ -85,8 +89,10 @@ func partialAccountToDef(item model.Account, params map[string]interface{}) *Def //systemConfigs systemConfigs := &item.SystemConfigs + //account identifiers + identifiers := accountIdentifiersToDef(item.Identifiers) //account auth types - authTypes := accountAuthTypesToDef(item.AuthTypes) + authTypes := accountAuthTypesToDefLegacy(&item) for i := 0; i < len(authTypes); i++ { authTypes[i].Params = nil } @@ -97,11 +103,6 @@ func partialAccountToDef(item model.Account, params map[string]interface{}) *Def formatted := utils.FormatTime(item.DateUpdated) dateUpdated = &formatted } - //username - var username *string - if item.Username != "" { - username = &item.Username - } //params var paramsData *map[string]interface{} @@ -109,18 +110,18 @@ func partialAccountToDef(item model.Account, params map[string]interface{}) *Def paramsData = ¶ms } - //external ids - externalIds := map[string]interface{}{} - for k, v := range item.ExternalIDs { - externalIds[k] = v - } - privacy := privacyToDef(&item.Privacy) + // maintain backwards compatibility + var username *string + if usernameIdentifier := item.GetAccountIdentifier("username", ""); usernameIdentifier != nil { + username = &usernameIdentifier.Identifier + } + return &Def.PartialAccount{Id: &item.ID, Anonymous: item.Anonymous, AppId: item.AppOrg.Application.ID, OrgId: item.AppOrg.Organization.ID, FirstName: item.Profile.FirstName, - LastName: item.Profile.LastName, Username: username, System: &item.AppOrg.Organization.System, Permissions: permissions, Roles: roles, Groups: groups, - Privacy: privacy, Verified: &item.Verified, Scopes: &scopes, SystemConfigs: systemConfigs, AuthTypes: authTypes, DateCreated: &dateCreated, - DateUpdated: dateUpdated, Params: paramsData, ExternalIds: &externalIds} + LastName: item.Profile.LastName, System: &item.AppOrg.Organization.System, Permissions: permissions, Roles: roles, Groups: groups, + Privacy: privacy, Verified: &item.Verified, Scopes: &scopes, SystemConfigs: systemConfigs, Identifiers: identifiers, AuthTypes: authTypes, + DateCreated: &dateCreated, DateUpdated: dateUpdated, Params: paramsData, Username: username} } func partialAccountsToDef(items []model.Account) []Def.PartialAccount { @@ -135,7 +136,8 @@ func partialAccountsToDef(items []model.Account) []Def.PartialAccount { func accountAuthTypeToDef(item model.AccountAuthType) Def.AccountAuthType { params := item.Params - return Def.AccountAuthType{Id: item.ID, Code: item.AuthType.Code, Identifier: item.Identifier, Active: &item.Active, Unverified: &item.Unverified, Params: ¶ms} + code := item.SupportedAuthType.AuthType.Code + return Def.AccountAuthType{Id: item.ID, AuthTypeCode: code, Active: &item.Active, Params: ¶ms, Code: &code} } func accountAuthTypesToDef(items []model.AccountAuthType) []Def.AccountAuthType { @@ -146,6 +148,60 @@ func accountAuthTypesToDef(items []model.AccountAuthType) []Def.AccountAuthType return result } +func accountAuthTypesToDefLegacy(account *model.Account) []Def.AccountAuthType { + if account == nil { + return nil + } + + aats := make([]Def.AccountAuthType, 0) + for _, aat := range account.AuthTypes { + resAat := accountAuthTypeToDef(aat) + addedLegacy := false + for _, id := range account.Identifiers { + // create the account auth type and set the identifier if the account has an identifier code matching an alias + code := id.Code + identifier := id.Identifier + legacyAat := resAat + if id.AccountAuthTypeID != nil && *id.AccountAuthTypeID == aat.ID && id.Primary != nil && *id.Primary { + // only use the primary identifierfor old account auth types + legacyAat.Identifier = &identifier + + aats = append(aats, legacyAat) + addedLegacy = true + } else if id.AccountAuthTypeID == nil && utils.Contains(aat.SupportedAuthType.AuthType.Aliases, id.Code) { + if code == "phone" { + code = "twilio_" + code + } + legacyAat.Code = &code + legacyAat.Identifier = &identifier + + aats = append(aats, legacyAat) + addedLegacy = true + } + } + + if !addedLegacy { + aats = append(aats, resAat) + } + } + + return aats +} + +// AccountIdentifier +func accountIdentifierToDef(item model.AccountIdentifier) Def.AccountIdentifier { + return Def.AccountIdentifier{Id: item.ID, Code: item.Code, Identifier: item.Identifier, Linked: item.Linked, Verified: item.Verified, + Sensitive: item.Sensitive, AccountAuthTypeId: item.AccountAuthTypeID} +} + +func accountIdentifiersToDef(items []model.AccountIdentifier) []Def.AccountIdentifier { + result := make([]Def.AccountIdentifier, len(items)) + for i, item := range items { + result[i] = accountIdentifierToDef(item) + } + return result +} + // AccountRole func accountRoleToDef(item model.AccountRole) Def.AppOrgRole { permissions := applicationPermissionsToDef(item.Role.Permissions) @@ -216,14 +272,6 @@ func profileFromDef(item *Def.Profile) model.Profile { if item.LastName != nil { lastName = *item.LastName } - var email string - if item.Email != nil { - email = *item.Email - } - var phone string - if item.Phone != nil { - phone = *item.Phone - } var birthYear int if item.BirthYear != nil { birthYear = *item.BirthYear @@ -251,7 +299,7 @@ func profileFromDef(item *Def.Profile) model.Profile { } return model.Profile{PhotoURL: photoURL, FirstName: firstName, LastName: lastName, - Email: email, Phone: phone, BirthYear: int16(birthYear), Address: address, ZipCode: zipCode, + BirthYear: int16(birthYear), Address: address, ZipCode: zipCode, State: state, Country: country, UnstructuredProperties: unstructuredProperties} } @@ -263,8 +311,8 @@ func profileToDef(item *model.Profile) *Def.Profile { itemVal := *item birthYear := int(itemVal.BirthYear) return &Def.Profile{Id: &itemVal.ID, PhotoUrl: &itemVal.PhotoURL, FirstName: &itemVal.FirstName, LastName: &itemVal.LastName, - Email: &itemVal.Email, Phone: &itemVal.Phone, BirthYear: &birthYear, Address: &itemVal.Address, ZipCode: &itemVal.ZipCode, - State: &itemVal.State, Country: &itemVal.Country, UnstructuredProperties: &itemVal.UnstructuredProperties} + BirthYear: &birthYear, Address: &itemVal.Address, ZipCode: &itemVal.ZipCode, State: &itemVal.State, + Country: &itemVal.Country, UnstructuredProperties: &itemVal.UnstructuredProperties} } func profileFromDefNullable(item *Def.ProfileNullable) model.Profile { @@ -284,14 +332,6 @@ func profileFromDefNullable(item *Def.ProfileNullable) model.Profile { if item.LastName != nil { lastName = *item.LastName } - var email string - if item.Email != nil { - email = *item.Email - } - var phone string - if item.Phone != nil { - phone = *item.Phone - } var birthYear int if item.BirthYear != nil { birthYear = *item.BirthYear @@ -319,7 +359,7 @@ func profileFromDefNullable(item *Def.ProfileNullable) model.Profile { } return model.Profile{PhotoURL: photoURL, FirstName: firstName, LastName: lastName, - Email: email, Phone: phone, BirthYear: int16(birthYear), Address: address, ZipCode: zipCode, + BirthYear: int16(birthYear), Address: address, ZipCode: zipCode, State: state, Country: country, UnstructuredProperties: unstructuredProperties} } diff --git a/driver/web/docs/gen/def.yaml b/driver/web/docs/gen/def.yaml index e535e8aa8..4f7ae3575 100644 --- a/driver/web/docs/gen/def.yaml +++ b/driver/web/docs/gen/def.yaml @@ -211,6 +211,35 @@ paths: type: mobile device_id: '5555' os: Android + webauthn-sign_up: + summary: WebAuthn - sign up + value: + auth_type: webauthn + app_type_identifier: edu.illinois.rokwire + org_id: 0a2eff20-e2cd-11eb-af68-60f81db5ecc0 + api_key: 95a463e3-2ce8-450b-ba75-d8506b874738 + params: + sign_up: true + name: test + display_name: John Doe + preferences: + key1: value1 + key2: value2 + profile: + address: address + birth_year: 1990 + country: county + email: email + first_name: first name + last_name: last name + phone: '+000000000000' + photo_url: photo url + state: state + zip_code: zip code + device: + type: mobile + device_id: '5555' + os: Android required: true responses: '200': @@ -377,17 +406,17 @@ paths: description: Unauthorized '500': description: Internal error - /services/auth/credential/verify: + /services/auth/identifier/verify: get: tags: - Services summary: Validate verification code description: | - Validates verification code to verify account ownership + Validates verification code to verify account identifier ownership parameters: - name: id in: query - description: Credential ID + description: Account identifier ID required: true style: form explode: false @@ -415,7 +444,7 @@ paths: description: Unauthorized '500': description: Internal error - /services/auth/credential/send-verify: + /services/auth/identifier/send-verify: post: tags: - Services @@ -428,7 +457,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/_services_req_credential_send-verify' + $ref: '#/components/schemas/_services_req_identifier_send-verify' required: true responses: '200': @@ -657,6 +686,136 @@ paths: description: Unauthorized '500': description: Internal error + /services/auth/account/sign-in-options: + post: + tags: + - Services + summary: Get account sign-in options + description: | + Get the sign-in options for the account with the provided parameters + requestBody: + description: | + Account information to be checked + content: + application/json: + schema: + $ref: '#/components/schemas/_shared_req_AccountCheck' + required: true + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/_shared_res_SignInOptions' + '400': + description: Bad request + '401': + description: Unauthorized + '500': + description: Internal error + /services/auth/account/identifier/link: + post: + tags: + - Services + summary: Link identifier + description: | + Link identifier to an existing account + + **Auth:** Requires "authenticated" auth token + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/_services_req_account_identifier-link' + examples: + email: + summary: Email + value: + identifier: + email: test@example.com + phone: + summary: Phone + value: + identifier: + phone: '+12223334444' + username: + summary: Username + value: + identifier: + username: username + required: true + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/_services_res_account_identifier-link' + '400': + description: Bad request + '401': + description: Unauthorized + '500': + description: Internal error + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - invalid + - unverified + - verification-expired + - already-exists + - not-found + - not-allowed + - internal-server-error + description: | + - `invalid`: Invalid identifier + - `unverified`: Unverified identifier + - `verification-expired`: Identifier verification expired. The verification is restarted + - `already-exists`: Auth type identifier already exists + - `not-found`: Account could not be found when `sign-up=false` + - `not-allowed`: Invalid operation + - `internal-server-error`: An undefined error occurred + message: + type: string + delete: + tags: + - Services + summary: Unlink identifier + description: | + Unlink identifier from an existing account + + **Auth:** Requires "authenticated" auth token + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/_services_req_account_identifier-unlink' + example: + id: + required: true + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/_services_res_account_identifier-link' + '400': + description: Bad request + '401': + description: Unauthorized + '500': + description: Internal error /services/auth/account/auth-type/link: post: tags: @@ -674,34 +833,43 @@ paths: schema: $ref: '#/components/schemas/_services_req_account_auth-type-link' examples: - email-sign_up: - summary: Email + password: + summary: Password value: - auth_type: email + auth_type: password app_type_identifier: edu.illinois.rokwire - org_id: 0a2eff20-e2cd-11eb-af68-60f81db5ecc0 - api_key: 95a463e3-2ce8-450b-ba75-d8506b874738 creds: - email: test@example.com password: test12345 params: confirm_password: test12345 - phone: - summary: Phone + code: + summary: Code value: - auth_type: twilio_phone + auth_type: code app_type_identifier: edu.illinois.rokwire - org_id: 0a2eff20-e2cd-11eb-af68-60f81db5ecc0 - api_key: 95a463e3-2ce8-450b-ba75-d8506b874738 creds: phone: '+12223334444' + webauthn-begin_registration: + summary: Webauthn begin registration + value: + auth_type: webauthn + app_type_identifier: edu.illinois.rokwire + params: + display_name: Name + webauthn-complete_registration: + summary: Webauthn complete registration + value: + auth_type: webauthn + app_type_identifier: edu.illinois.rokwire + creds: + response: + params: + display_name: Name illinois_oidc: summary: Illinois OIDC value: auth_type: illinois_oidc app_type_identifier: edu.illinois.rokwire - org_id: 0a2eff20-e2cd-11eb-af68-60f81db5ecc0 - api_key: 95a463e3-2ce8-450b-ba75-d8506b874738 creds: 'https://redirect.example.com?code=ai324uith8gSEefesEguorgwsf43' params: redirect_uri: 'https://redirect.example.com' @@ -733,13 +901,15 @@ paths: - verification-expired - already-exists - not-found + - not-allowed - internal-server-error description: | - `invalid`: Invalid credentials - `unverified`: Unverified credentials - - `verification-expired`: Credentials verification expired. The verification is restarted - - `already-exists`: Auth type identifier already exists + - `verification-expired`: Identifier verification expired. The verification is restarted + - `already-exists`: Auth type already exists - `not-found`: Account could not be found when `sign-up=false` + - `not-allowed`: Invalid operation - `internal-server-error`: An undefined error occurred message: type: string @@ -758,25 +928,8 @@ paths: application/json: schema: $ref: '#/components/schemas/_services_req_account_auth-type-unlink' - examples: - email: - summary: Email - value: - auth_type: email - app_type_identifier: edu.illinois.rokwire - identifier: test@example.com - phone: - summary: Phone - value: - auth_type: twilio_phone - app_type_identifier: edu.illinois.rokwire - identifier: '+12223334444' - illinois_oidc: - summary: Illinois OIDC - value: - auth_type: illinois_oidc - app_type_identifier: edu.illinois.rokwire - identifier: '123456789' + example: + id: required: true responses: '200': @@ -1628,6 +1781,36 @@ paths: description: AppConfig not found '500': description: Internal error + /services/auth/credential/send-verify: + post: + tags: + - Services + summary: Send verification code to identifier + description: | + Sends verification code to identifier to verify account ownership + deprecated: true + requestBody: + description: | + Account information to be checked + content: + application/json: + schema: + $ref: '#/components/schemas/_services_req_identifier_send-verify' + required: true + responses: + '200': + description: Successful operation + content: + text/plain: + schema: + type: string + example: Successfully sent verification code + '400': + description: Bad request + '401': + description: Unauthorized + '500': + description: Internal error /services/application/configs: post: tags: @@ -5801,7 +5984,7 @@ paths: description: Unauthorized '500': description: Internal error - /ui/credential/verify: + /ui/identifier/verify: get: tags: - UI @@ -5811,7 +5994,7 @@ paths: parameters: - name: id in: query - description: Credential ID + description: Identifier ID required: true style: form explode: false @@ -5890,6 +6073,7 @@ components: data: anyOf: - $ref: '#/components/schemas/EnvConfigData' + - $ref: '#/components/schemas/AuthConfigData' date_created: readOnly: true type: string @@ -5910,6 +6094,18 @@ components: items: type: string nullable: true + AuthConfigData: + type: object + properties: + email_should_verify: + type: boolean + nullable: true + email_verify_wait_time: + type: integer + nullable: true + email_verify_expiry: + type: integer + nullable: true Application: required: - id @@ -6136,6 +6332,13 @@ components: additionalProperties: type: string nullable: true + sensitive_external_ids: + type: array + items: + type: string + nullable: true + is_email_verified: + type: boolean first_name_field: type: string middle_name_field: @@ -6252,10 +6455,6 @@ components: type: string app_type_identifier: type: string - account_auth_type_id: - type: string - account_auth_type_identifier: - type: string device_id: type: string nullable: true @@ -6294,7 +6493,7 @@ components: type: string code: type: string - description: username or email or phone or illinois_oidc etc + description: passowrd or code or webauthn or illinois_oidc etc description: type: string is_external: @@ -6571,8 +6770,6 @@ components: type: string app_org: $ref: '#/components/schemas/ApplicationOrganization' - username: - type: string profile: $ref: '#/components/schemas/Profile' privacy: @@ -6589,9 +6786,10 @@ components: type: boolean system: type: boolean - external_ids: - type: object - nullable: true + identifiers: + type: array + items: + $ref: '#/components/schemas/AccountIdentifier' auth_types: type: array items: @@ -6622,6 +6820,10 @@ components: type: string most_recent_client_version: type: string + username: + type: string + nullable: true + deprecated: true PublicAccount: required: - id @@ -6650,6 +6852,7 @@ components: - roles - groups - anonymous + - identifiers - auth_types - date_created type: object @@ -6667,8 +6870,6 @@ components: type: string system: type: boolean - username: - type: string permissions: type: array items: @@ -6685,6 +6886,10 @@ components: type: array items: type: string + identifiers: + type: array + items: + $ref: '#/components/schemas/AccountIdentifier' auth_types: type: array items: @@ -6707,9 +6912,10 @@ components: date_updated: type: string nullable: true - external_ids: - type: object + username: + type: string nullable: true + deprecated: true Profile: required: - id @@ -6727,9 +6933,11 @@ components: email: type: string nullable: true + deprecated: true phone: type: string nullable: true + deprecated: true birth_year: type: integer nullable: true @@ -6764,9 +6972,11 @@ components: email: type: string nullable: true + deprecated: true phone: type: string nullable: true + deprecated: true birth_year: type: integer nullable: true @@ -6807,24 +7017,50 @@ components: AccountAuthType: required: - id - - code - - identifier + - auth_type_code type: object properties: id: type: string + auth_type_code: + type: string code: type: string + deprecated: true identifier: type: string + deprecated: true params: type: object additionalProperties: true nullable: true active: type: boolean - unverified: + AccountIdentifier: + required: + - id + - code + - identifier + - verified + - linked + - sensitive + type: object + properties: + id: + type: string + code: + type: string + identifier: + type: string + verified: + type: boolean + linked: type: boolean + sensitive: + type: boolean + account_auth_type_id: + type: string + nullable: true Device: required: - id @@ -6874,10 +7110,15 @@ components: type: string enum: - email + - phone - twilio_phone - illinois_oidc + - conde_oidc - anonymous - username + - password + - webauthn + - code app_type_identifier: type: string org_id: @@ -6886,18 +7127,19 @@ components: type: string creds: anyOf: - - $ref: '#/components/schemas/_shared_req_CredsEmail' - - $ref: '#/components/schemas/_shared_req_CredsTwilioPhone' + - $ref: '#/components/schemas/_shared_req_CredsAnonymous' + - $ref: '#/components/schemas/_shared_req_CredsCode' - $ref: '#/components/schemas/_shared_req_CredsOIDC' - - $ref: '#/components/schemas/_shared_req_CredsAPIKey' - - $ref: '#/components/schemas/_shared_req_CredsUsername' + - $ref: '#/components/schemas/_shared_req_CredsPassword' + - $ref: '#/components/schemas/_shared_req_CredsWebAuthn' + - $ref: '#/components/schemas/_shared_req_CredsNone' params: type: object anyOf: - - $ref: '#/components/schemas/_shared_req_ParamsEmail' - $ref: '#/components/schemas/_shared_req_ParamsOIDC' + - $ref: '#/components/schemas/_shared_req_ParamsPassword' + - $ref: '#/components/schemas/_shared_req_ParamsWebAuthn' - $ref: '#/components/schemas/_shared_req_ParamsNone' - - $ref: '#/components/schemas/_shared_req_ParamsUsername' device: $ref: '#/components/schemas/Device' profile: @@ -6907,7 +7149,7 @@ components: preferences: type: object nullable: true - username: + account_identifier_id: type: string nullable: true _shared_req_Login_Mfa: @@ -6953,6 +7195,7 @@ components: type: string enum: - illinois_oidc + - conde_oidc app_type_identifier: type: string org_id: @@ -6996,10 +7239,10 @@ components: auth_type: type: string enum: - - email + - password - illinois_oidc identifier: - type: string + $ref: '#/components/schemas/_shared_req_Identifiers' permissions: type: array items: @@ -7020,9 +7263,6 @@ components: $ref: '#/components/schemas/ProfileNullable' privacy: $ref: '#/components/schemas/PrivacyNullable' - username: - type: string - nullable: true _shared_req_UpdateAccount: required: - auth_type @@ -7032,10 +7272,10 @@ components: auth_type: type: string enum: - - email + - password - illinois_oidc identifier: - type: string + $ref: '#/components/schemas/_shared_req_Identifiers' permissions: type: array items: @@ -7054,81 +7294,93 @@ components: type: string _shared_req_AccountCheck: required: - - auth_type - app_type_identifier - org_id - api_key - - user_identifier type: object properties: + app_type_identifier: + type: string + org_id: + type: string + api_key: + type: string + identifier: + $ref: '#/components/schemas/_shared_req_Identifiers' auth_type: type: string enum: - username - email + - phone + - anonymous - twilio_phone - illinois_oidc - - anonymous - app_type_identifier: - type: string - org_id: - type: string - api_key: - type: string + - conde_oidc + deprecated: true user_identifier: type: string - _shared_req_CredsEmail: - required: - - email - - password + deprecated: true + _shared_req_CredsNone: type: object - description: Auth login creds for auth_type="email" - properties: - email: - type: string - password: - type: string - _shared_req_CredsTwilioPhone: + description: Auth login request creds for unlisted auth_types (None) + nullable: true + _shared_req_CredsAnonymous: type: object - description: Auth login creds for auth_type="twilio_phone" - required: - - phone + description: Auth login creds for auth_type="anonymous" properties: - phone: - type: string - code: + anonymous_id: type: string + _shared_req_CredsCode: + type: object + description: Auth login creds for auth_type="code" + allOf: + - $ref: '#/components/schemas/_shared_req_Identifiers' + - properties: + code: + type: string _shared_req_CredsOIDC: type: string description: | Auth login creds for auth_type="oidc" (or variants) - full redirect URI received from OIDC provider - _shared_req_CredsUsername: - required: - - username - - password + _shared_req_CredsPassword: + type: object + description: Auth login creds for auth_type="password" + allOf: + - $ref: '#/components/schemas/_shared_req_Identifiers' + - required: + - password + properties: + password: + type: string + _shared_req_CredsWebAuthn: + type: object + description: Auth login creds for auth_type="webauthn" + allOf: + - $ref: '#/components/schemas/_shared_req_Identifiers' + - properties: + response: + type: string + _shared_req_Identifiers: type: object - description: Auth login creds for auth_type="username" + description: Allowed identifier types properties: username: type: string - password: + email: type: string - _shared_req_CredsAPIKey: - type: object - description: Auth login creds for auth_type="anonymous" - properties: - anonymous_id: + phone: type: string - _shared_req_ParamsEmail: + additionalProperties: + type: string + _shared_req_IdentifierString: + type: string + description: User identifier string + _shared_req_ParamsNone: type: object - description: Auth login params for auth_type="email" - properties: - confirm_password: - type: string - description: This should match the `creds` password field when sign_up=true. This should be verified on the client side as well to reduce invalid requests. - sign_up: - type: boolean + description: Auth login request params for unlisted auth_types (None) + nullable: true _shared_req_ParamsOIDC: type: object description: Auth login params for auth_type="oidc" (or variants) @@ -7137,20 +7389,16 @@ components: type: string pkce_verifier: type: string - _shared_req_ParamsUsername: + _shared_req_ParamsPassword: type: object - description: Auth login params for auth_type="username" + description: Auth login params for auth_type="email" properties: confirm_password: type: string description: This should match the `creds` password field when sign_up=true. This should be verified on the client side as well to reduce invalid requests. sign_up: type: boolean - _shared_req_ParamsNone: - type: object - description: Auth login request params for unlisted auth_types (None) - nullable: true - _shared_req_ParamsSetEmailCredential: + _shared_req_ParamsResetPassword: required: - new_password - confirm_password @@ -7160,6 +7408,15 @@ components: type: string confirm_password: type: string + _shared_req_ParamsWebAuthn: + type: object + description: Auth login params for auth_type="webauthn" + properties: + display_name: + type: string + description: User's account name for display purposes + sign_up: + type: boolean _shared_req_app-configs: required: - app_type_identifier @@ -7197,7 +7454,7 @@ components: nullable: true anyOf: - $ref: '#/components/schemas/_shared_res_ParamsOIDC' - - $ref: '#/components/schemas/_shared_res_ParamsAPIKey' + - $ref: '#/components/schemas/_shared_res_ParamsAnonymous' - $ref: '#/components/schemas/_shared_res_ParamsNone' message: type: string @@ -7224,7 +7481,7 @@ components: nullable: true anyOf: - $ref: '#/components/schemas/_shared_res_ParamsOIDC' - - $ref: '#/components/schemas/_shared_res_ParamsAPIKey' + - $ref: '#/components/schemas/_shared_res_ParamsAnonymous' - $ref: '#/components/schemas/_shared_res_ParamsNone' _shared_res_LoginUrl: required: @@ -7246,7 +7503,7 @@ components: nullable: true anyOf: - $ref: '#/components/schemas/_shared_res_ParamsOIDC' - - $ref: '#/components/schemas/_shared_res_ParamsAPIKey' + - $ref: '#/components/schemas/_shared_res_ParamsAnonymous' - $ref: '#/components/schemas/_shared_res_ParamsNone' _shared_res_Mfa: type: object @@ -7259,12 +7516,16 @@ components: type: object _shared_res_AccountCheck: type: boolean - _shared_res_ParamsAPIKey: + _shared_res_ParamsAnonymous: type: object description: Auth login response params for auth_type="anonymous" properties: anonymous_id: type: string + _shared_res_ParamsNone: + type: object + description: Auth login response params for unlisted auth_types (None) + nullable: true _shared_res_ParamsOIDC: type: object description: Auth login response params for auth_type="oidc" (or variants) @@ -7282,10 +7543,6 @@ components: type: string redirect_uri: type: string - _shared_res_ParamsNone: - type: object - description: Auth login response params for unlisted auth_types (None) - nullable: true _shared_res_RokwireToken: type: object properties: @@ -7300,49 +7557,71 @@ components: type: string enum: - Bearer + _shared_res_SignInOptions: + required: + - identifiers + - auth_types + type: object + properties: + identifiers: + type: array + items: + $ref: '#/components/schemas/AccountIdentifier' + auth_types: + type: array + items: + $ref: '#/components/schemas/AccountAuthType' _services_req_account_auth-type-link: required: - auth_type - app_type_identifier - - creds type: object properties: auth_type: type: string enum: + - password + - webauthn + - code + - illinois_oidc + - conde_oidc - email + - phone - twilio_phone - - illinois_oidc - username app_type_identifier: type: string creds: anyOf: - - $ref: '#/components/schemas/_shared_req_CredsEmail' - - $ref: '#/components/schemas/_shared_req_CredsTwilioPhone' + - $ref: '#/components/schemas/_shared_req_CredsCode' - $ref: '#/components/schemas/_shared_req_CredsOIDC' + - $ref: '#/components/schemas/_shared_req_CredsPassword' + - $ref: '#/components/schemas/_shared_req_CredsWebAuthn' + - $ref: '#/components/schemas/_shared_req_CredsNone' params: type: object anyOf: - - $ref: '#/components/schemas/_shared_req_ParamsEmail' - $ref: '#/components/schemas/_shared_req_ParamsOIDC' + - $ref: '#/components/schemas/_shared_req_ParamsPassword' + - $ref: '#/components/schemas/_shared_req_ParamsWebAuthn' - $ref: '#/components/schemas/_shared_req_ParamsNone' _services_req_account_auth-type-unlink: - required: - - auth_type - - app_type_identifier - - identifier type: object properties: + id: + type: string auth_type: type: string enum: + - password + - webauthn + - code + - illinois_oidc + - conde_oidc - email + - phone - twilio_phone - - illinois_oidc - username - app_type_identifier: - type: string identifier: type: string _services_res_account_auth-type-link: @@ -7353,24 +7632,42 @@ components: message: type: string nullable: true + identifiers: + type: array + items: + $ref: '#/components/schemas/AccountIdentifier' auth_types: type: array items: $ref: '#/components/schemas/AccountAuthType' - _services_req_credential_update: + _services_req_account_identifier-link: required: - - account_auth_type_id + - identifier type: object properties: - account_auth_type_id: + identifier: + $ref: '#/components/schemas/_shared_req_Identifiers' + _services_req_account_identifier-unlink: + required: + - id + type: object + properties: + id: type: string - params: - type: object - anyOf: - - $ref: '#/components/schemas/_shared_req_ParamsSetEmailCredential' - _services_req_credential_send-verify: + _services_res_account_identifier-link: + required: + - identifiers + type: object + properties: + message: + type: string + nullable: true + identifiers: + type: array + items: + $ref: '#/components/schemas/AccountIdentifier' + _services_req_identifier_send-verify: required: - - auth_type - app_type_identifier - org_id - api_key @@ -7378,7 +7675,9 @@ components: type: object properties: identifier: - type: string + oneOf: + - $ref: '#/components/schemas/_shared_req_Identifiers' + - $ref: '#/components/schemas/_shared_req_IdentifierString' org_id: type: string api_key: @@ -7389,27 +7688,41 @@ components: type: string enum: - email + _services_req_credential_update: + required: + - account_auth_type_id + type: object + properties: + account_auth_type_id: + type: string + params: + type: object + anyOf: + - $ref: '#/components/schemas/_shared_req_ParamsResetPassword' _services_req_credential_forgot_initiate: required: - auth_type + - identifier - app_type_identifier - org_id - api_key - - identifier type: object properties: auth_type: type: string enum: + - password - email + identifier: + oneOf: + - $ref: '#/components/schemas/_shared_req_Identifiers' + - $ref: '#/components/schemas/_shared_req_IdentifierString' app_type_identifier: type: string org_id: type: string api_key: type: string - identifier: - type: string _services_req_credential_forgot_complete: required: - credential_id @@ -7423,7 +7736,7 @@ components: params: type: object anyOf: - - $ref: '#/components/schemas/_shared_req_ParamsSetEmailCredential' + - $ref: '#/components/schemas/_shared_req_ParamsResetPassword' _services_req_authorize-service: required: - service_id @@ -7657,6 +7970,7 @@ components: data: anyOf: - $ref: '#/components/schemas/EnvConfigData' + - $ref: '#/components/schemas/AuthConfigData' _system_req_update_service-account: type: object properties: diff --git a/driver/web/docs/gen/gen_types.go b/driver/web/docs/gen/gen_types.go index e24af0ba0..d085da503 100644 --- a/driver/web/docs/gen/gen_types.go +++ b/driver/web/docs/gen/gen_types.go @@ -5,6 +5,7 @@ package Def import ( "encoding/json" + "fmt" "github.com/oapi-codegen/runtime" ) @@ -64,28 +65,39 @@ const ( // Defines values for ServicesReqAccountAuthTypeLinkAuthType. const ( + ServicesReqAccountAuthTypeLinkAuthTypeCode ServicesReqAccountAuthTypeLinkAuthType = "code" + ServicesReqAccountAuthTypeLinkAuthTypeCondeOidc ServicesReqAccountAuthTypeLinkAuthType = "conde_oidc" ServicesReqAccountAuthTypeLinkAuthTypeEmail ServicesReqAccountAuthTypeLinkAuthType = "email" ServicesReqAccountAuthTypeLinkAuthTypeIllinoisOidc ServicesReqAccountAuthTypeLinkAuthType = "illinois_oidc" + ServicesReqAccountAuthTypeLinkAuthTypePassword ServicesReqAccountAuthTypeLinkAuthType = "password" + ServicesReqAccountAuthTypeLinkAuthTypePhone ServicesReqAccountAuthTypeLinkAuthType = "phone" ServicesReqAccountAuthTypeLinkAuthTypeTwilioPhone ServicesReqAccountAuthTypeLinkAuthType = "twilio_phone" ServicesReqAccountAuthTypeLinkAuthTypeUsername ServicesReqAccountAuthTypeLinkAuthType = "username" + ServicesReqAccountAuthTypeLinkAuthTypeWebauthn ServicesReqAccountAuthTypeLinkAuthType = "webauthn" ) // Defines values for ServicesReqAccountAuthTypeUnlinkAuthType. const ( + ServicesReqAccountAuthTypeUnlinkAuthTypeCode ServicesReqAccountAuthTypeUnlinkAuthType = "code" + ServicesReqAccountAuthTypeUnlinkAuthTypeCondeOidc ServicesReqAccountAuthTypeUnlinkAuthType = "conde_oidc" ServicesReqAccountAuthTypeUnlinkAuthTypeEmail ServicesReqAccountAuthTypeUnlinkAuthType = "email" ServicesReqAccountAuthTypeUnlinkAuthTypeIllinoisOidc ServicesReqAccountAuthTypeUnlinkAuthType = "illinois_oidc" + ServicesReqAccountAuthTypeUnlinkAuthTypePassword ServicesReqAccountAuthTypeUnlinkAuthType = "password" + ServicesReqAccountAuthTypeUnlinkAuthTypePhone ServicesReqAccountAuthTypeUnlinkAuthType = "phone" ServicesReqAccountAuthTypeUnlinkAuthTypeTwilioPhone ServicesReqAccountAuthTypeUnlinkAuthType = "twilio_phone" ServicesReqAccountAuthTypeUnlinkAuthTypeUsername ServicesReqAccountAuthTypeUnlinkAuthType = "username" + ServicesReqAccountAuthTypeUnlinkAuthTypeWebauthn ServicesReqAccountAuthTypeUnlinkAuthType = "webauthn" ) // Defines values for ServicesReqCredentialForgotInitiateAuthType. const ( - ServicesReqCredentialForgotInitiateAuthTypeEmail ServicesReqCredentialForgotInitiateAuthType = "email" + ServicesReqCredentialForgotInitiateAuthTypeEmail ServicesReqCredentialForgotInitiateAuthType = "email" + ServicesReqCredentialForgotInitiateAuthTypePassword ServicesReqCredentialForgotInitiateAuthType = "password" ) -// Defines values for ServicesReqCredentialSendVerifyAuthType. +// Defines values for ServicesReqIdentifierSendVerifyAuthType. const ( - ServicesReqCredentialSendVerifyAuthTypeEmail ServicesReqCredentialSendVerifyAuthType = "email" + ServicesReqIdentifierSendVerifyAuthTypeEmail ServicesReqIdentifierSendVerifyAuthType = "email" ) // Defines values for ServicesReqServiceAccountsAccessTokenAuthType. @@ -114,29 +126,37 @@ const ( // Defines values for SharedReqAccountCheckAuthType. const ( SharedReqAccountCheckAuthTypeAnonymous SharedReqAccountCheckAuthType = "anonymous" + SharedReqAccountCheckAuthTypeCondeOidc SharedReqAccountCheckAuthType = "conde_oidc" SharedReqAccountCheckAuthTypeEmail SharedReqAccountCheckAuthType = "email" SharedReqAccountCheckAuthTypeIllinoisOidc SharedReqAccountCheckAuthType = "illinois_oidc" + SharedReqAccountCheckAuthTypePhone SharedReqAccountCheckAuthType = "phone" SharedReqAccountCheckAuthTypeTwilioPhone SharedReqAccountCheckAuthType = "twilio_phone" SharedReqAccountCheckAuthTypeUsername SharedReqAccountCheckAuthType = "username" ) // Defines values for SharedReqCreateAccountAuthType. const ( - SharedReqCreateAccountAuthTypeEmail SharedReqCreateAccountAuthType = "email" SharedReqCreateAccountAuthTypeIllinoisOidc SharedReqCreateAccountAuthType = "illinois_oidc" + SharedReqCreateAccountAuthTypePassword SharedReqCreateAccountAuthType = "password" ) // Defines values for SharedReqLoginAuthType. const ( SharedReqLoginAuthTypeAnonymous SharedReqLoginAuthType = "anonymous" + SharedReqLoginAuthTypeCode SharedReqLoginAuthType = "code" + SharedReqLoginAuthTypeCondeOidc SharedReqLoginAuthType = "conde_oidc" SharedReqLoginAuthTypeEmail SharedReqLoginAuthType = "email" SharedReqLoginAuthTypeIllinoisOidc SharedReqLoginAuthType = "illinois_oidc" + SharedReqLoginAuthTypePassword SharedReqLoginAuthType = "password" + SharedReqLoginAuthTypePhone SharedReqLoginAuthType = "phone" SharedReqLoginAuthTypeTwilioPhone SharedReqLoginAuthType = "twilio_phone" SharedReqLoginAuthTypeUsername SharedReqLoginAuthType = "username" + SharedReqLoginAuthTypeWebauthn SharedReqLoginAuthType = "webauthn" ) // Defines values for SharedReqLoginUrlAuthType. const ( + SharedReqLoginUrlAuthTypeCondeOidc SharedReqLoginUrlAuthType = "conde_oidc" SharedReqLoginUrlAuthTypeIllinoisOidc SharedReqLoginUrlAuthType = "illinois_oidc" ) @@ -157,8 +177,8 @@ const ( // Defines values for SharedReqUpdateAccountAuthType. const ( - Email SharedReqUpdateAccountAuthType = "email" IllinoisOidc SharedReqUpdateAccountAuthType = "illinois_oidc" + Password SharedReqUpdateAccountAuthType = "password" ) // Defines values for SharedResRokwireTokenTokenType. @@ -179,9 +199,9 @@ type Account struct { AppOrg *ApplicationOrganization `json:"app_org"` AuthTypes *[]AccountAuthType `json:"auth_types,omitempty"` Devices *[]Device `json:"devices,omitempty"` - ExternalIds *map[string]interface{} `json:"external_ids"` Groups *[]AppOrgGroup `json:"groups,omitempty"` Id *string `json:"id,omitempty"` + Identifiers *[]AccountIdentifier `json:"identifiers,omitempty"` LastAccessTokenDate *string `json:"last_access_token_date,omitempty"` LastLoginDate *string `json:"last_login_date,omitempty"` MostRecentClientVersion *string `json:"most_recent_client_version,omitempty"` @@ -193,18 +213,32 @@ type Account struct { Scopes *[]string `json:"scopes,omitempty"` System *bool `json:"system,omitempty"` SystemConfigs *map[string]interface{} `json:"system_configs"` - Username *string `json:"username,omitempty"` - Verified *bool `json:"verified,omitempty"` + // Deprecated: + Username *string `json:"username"` + Verified *bool `json:"verified,omitempty"` } // AccountAuthType defines model for AccountAuthType. type AccountAuthType struct { - Active *bool `json:"active,omitempty"` - Code string `json:"code"` - Id string `json:"id"` - Identifier string `json:"identifier"` + Active *bool `json:"active,omitempty"` + AuthTypeCode string `json:"auth_type_code"` + // Deprecated: + Code *string `json:"code,omitempty"` + Id string `json:"id"` + // Deprecated: + Identifier *string `json:"identifier,omitempty"` Params *map[string]interface{} `json:"params"` - Unverified *bool `json:"unverified,omitempty"` +} + +// AccountIdentifier defines model for AccountIdentifier. +type AccountIdentifier struct { + AccountAuthTypeId *string `json:"account_auth_type_id"` + Code string `json:"code"` + Id string `json:"id"` + Identifier string `json:"identifier"` + Linked bool `json:"linked"` + Sensitive bool `json:"sensitive"` + Verified bool `json:"verified"` } // AdminToken defines model for AdminToken. @@ -284,6 +318,13 @@ type ApplicationType struct { Versions *[]string `json:"versions,omitempty"` } +// AuthConfigData defines model for AuthConfigData. +type AuthConfigData struct { + EmailShouldVerify *bool `json:"email_should_verify"` + EmailVerifyExpiry *int `json:"email_verify_expiry"` + EmailVerifyWaitTime *int `json:"email_verify_wait_time"` +} + // AuthServiceReg Service registration record used for auth type AuthServiceReg struct { Host string `json:"host"` @@ -294,7 +335,7 @@ type AuthServiceReg struct { // AuthType defines model for AuthType. type AuthType struct { - // Code username or email or phone or illinois_oidc etc + // Code passowrd or code or webauthn or illinois_oidc etc Code string `json:"code"` Description string `json:"description"` Id *string `json:"id,omitempty"` @@ -356,20 +397,22 @@ type Follow struct { // IdentityProviderSettings defines model for IdentityProviderSettings. type IdentityProviderSettings struct { - AlwaysSyncProfile *bool `json:"always_sync_profile,omitempty"` - EmailField *string `json:"email_field,omitempty"` - ExternalIdFields *map[string]string `json:"external_id_fields"` - FirstNameField *string `json:"first_name_field,omitempty"` - Groups *map[string]string `json:"groups"` - GroupsField *string `json:"groups_field,omitempty"` - IdentityBbBaseUrl *string `json:"identity_bb_base_url,omitempty"` - IdentityProviderId string `json:"identity_provider_id"` - LastNameField *string `json:"last_name_field,omitempty"` - MiddleNameField *string `json:"middle_name_field,omitempty"` - Roles *map[string]string `json:"roles"` - RolesField *string `json:"roles_field,omitempty"` - UserIdentifierField string `json:"user_identifier_field"` - UserSpecificFields *[]string `json:"user_specific_fields"` + AlwaysSyncProfile *bool `json:"always_sync_profile,omitempty"` + EmailField *string `json:"email_field,omitempty"` + ExternalIdFields *map[string]string `json:"external_id_fields"` + FirstNameField *string `json:"first_name_field,omitempty"` + Groups *map[string]string `json:"groups"` + GroupsField *string `json:"groups_field,omitempty"` + IdentityBbBaseUrl *string `json:"identity_bb_base_url,omitempty"` + IdentityProviderId string `json:"identity_provider_id"` + IsEmailVerified *bool `json:"is_email_verified,omitempty"` + LastNameField *string `json:"last_name_field,omitempty"` + MiddleNameField *string `json:"middle_name_field,omitempty"` + Roles *map[string]string `json:"roles"` + RolesField *string `json:"roles_field,omitempty"` + SensitiveExternalIds *[]string `json:"sensitive_external_ids"` + UserIdentifierField string `json:"user_identifier_field"` + UserSpecificFields *[]string `json:"user_specific_fields"` } // InactiveExpirePolicy defines model for InactiveExpirePolicy. @@ -454,24 +497,22 @@ type JWKS struct { // LoginSession defines model for LoginSession. type LoginSession struct { - AccountAuthTypeId *string `json:"account_auth_type_id,omitempty"` - AccountAuthTypeIdentifier *string `json:"account_auth_type_identifier,omitempty"` - Anonymous *bool `json:"anonymous,omitempty"` - AppOrgId *string `json:"app_org_id,omitempty"` - AppTypeId *string `json:"app_type_id,omitempty"` - AppTypeIdentifier *string `json:"app_type_identifier,omitempty"` - AuthTypeCode *string `json:"auth_type_code,omitempty"` - DateCreated *string `json:"date_created,omitempty"` - DateRefreshed *string `json:"date_refreshed"` - DateUpdated *string `json:"date_updated"` - DeviceId *string `json:"device_id"` - Id *string `json:"id,omitempty"` - Identifier *string `json:"identifier,omitempty"` - IpAddress *string `json:"ip_address,omitempty"` - MfaAttempts *int `json:"mfa_attempts,omitempty"` - RefreshTokensCount *int `json:"refresh_tokens_count,omitempty"` - State *string `json:"state,omitempty"` - StateExpires *string `json:"state_expires"` + Anonymous *bool `json:"anonymous,omitempty"` + AppOrgId *string `json:"app_org_id,omitempty"` + AppTypeId *string `json:"app_type_id,omitempty"` + AppTypeIdentifier *string `json:"app_type_identifier,omitempty"` + AuthTypeCode *string `json:"auth_type_code,omitempty"` + DateCreated *string `json:"date_created,omitempty"` + DateRefreshed *string `json:"date_refreshed"` + DateUpdated *string `json:"date_updated"` + DeviceId *string `json:"device_id"` + Id *string `json:"id,omitempty"` + Identifier *string `json:"identifier,omitempty"` + IpAddress *string `json:"ip_address,omitempty"` + MfaAttempts *int `json:"mfa_attempts,omitempty"` + RefreshTokensCount *int `json:"refresh_tokens_count,omitempty"` + State *string `json:"state,omitempty"` + StateExpires *string `json:"state_expires"` } // LoginSessionSettings defines model for LoginSessionSettings. @@ -515,10 +556,10 @@ type PartialAccount struct { AuthTypes []AccountAuthType `json:"auth_types"` DateCreated *string `json:"date_created,omitempty"` DateUpdated *string `json:"date_updated"` - ExternalIds *map[string]interface{} `json:"external_ids"` FirstName string `json:"first_name"` Groups []AppOrgGroup `json:"groups"` Id *string `json:"id,omitempty"` + Identifiers []AccountIdentifier `json:"identifiers"` LastName string `json:"last_name"` OrgId string `json:"org_id"` Params *map[string]interface{} `json:"params"` @@ -528,8 +569,9 @@ type PartialAccount struct { Scopes *[]string `json:"scopes,omitempty"` System *bool `json:"system,omitempty"` SystemConfigs *map[string]interface{} `json:"system_configs"` - Username *string `json:"username,omitempty"` - Verified *bool `json:"verified,omitempty"` + // Deprecated: + Username *string `json:"username"` + Verified *bool `json:"verified,omitempty"` } // Permission defines model for Permission. @@ -555,13 +597,15 @@ type PrivacyNullable struct { // Profile defines model for Profile. type Profile struct { - Address *string `json:"address"` - BirthYear *int `json:"birth_year"` - Country *string `json:"country"` - Email *string `json:"email"` - FirstName *string `json:"first_name,omitempty"` - Id *string `json:"id,omitempty"` - LastName *string `json:"last_name,omitempty"` + Address *string `json:"address"` + BirthYear *int `json:"birth_year"` + Country *string `json:"country"` + // Deprecated: + Email *string `json:"email"` + FirstName *string `json:"first_name,omitempty"` + Id *string `json:"id,omitempty"` + LastName *string `json:"last_name,omitempty"` + // Deprecated: Phone *string `json:"phone"` PhotoUrl *string `json:"photo_url,omitempty"` State *string `json:"state"` @@ -571,12 +615,14 @@ type Profile struct { // ProfileNullable defines model for ProfileNullable. type ProfileNullable struct { - Address *string `json:"address"` - BirthYear *int `json:"birth_year"` - Country *string `json:"country"` - Email *string `json:"email"` - FirstName *string `json:"first_name"` - LastName *string `json:"last_name"` + Address *string `json:"address"` + BirthYear *int `json:"birth_year"` + Country *string `json:"country"` + // Deprecated: + Email *string `json:"email"` + FirstName *string `json:"first_name"` + LastName *string `json:"last_name"` + // Deprecated: Phone *string `json:"phone"` PhotoUrl *string `json:"photo_url"` State *string `json:"state"` @@ -755,7 +801,7 @@ type AdminReqVerified struct { type ServicesReqAccountAuthTypeLink struct { AppTypeIdentifier string `json:"app_type_identifier"` AuthType ServicesReqAccountAuthTypeLinkAuthType `json:"auth_type"` - Creds ServicesReqAccountAuthTypeLink_Creds `json:"creds"` + Creds *ServicesReqAccountAuthTypeLink_Creds `json:"creds,omitempty"` Params *ServicesReqAccountAuthTypeLink_Params `json:"params,omitempty"` } @@ -774,14 +820,25 @@ type ServicesReqAccountAuthTypeLink_Params struct { // ServicesReqAccountAuthTypeUnlink defines model for _services_req_account_auth-type-unlink. type ServicesReqAccountAuthTypeUnlink struct { - AppTypeIdentifier string `json:"app_type_identifier"` - AuthType ServicesReqAccountAuthTypeUnlinkAuthType `json:"auth_type"` - Identifier string `json:"identifier"` + AuthType *ServicesReqAccountAuthTypeUnlinkAuthType `json:"auth_type,omitempty"` + Id *string `json:"id,omitempty"` + Identifier *string `json:"identifier,omitempty"` } // ServicesReqAccountAuthTypeUnlinkAuthType defines model for ServicesReqAccountAuthTypeUnlink.AuthType. type ServicesReqAccountAuthTypeUnlinkAuthType string +// ServicesReqAccountIdentifierLink defines model for _services_req_account_identifier-link. +type ServicesReqAccountIdentifierLink struct { + // Identifier Allowed identifier types + Identifier SharedReqIdentifiers `json:"identifier"` +} + +// ServicesReqAccountIdentifierUnlink defines model for _services_req_account_identifier-unlink. +type ServicesReqAccountIdentifierUnlink struct { + Id string `json:"id"` +} + // ServicesReqAuthorizeService defines model for _services_req_authorize-service. type ServicesReqAuthorizeService struct { // ApprovedScopes Scopes to be granted to this service in this and future tokens. Replaces existing scopes if present. @@ -803,28 +860,21 @@ type ServicesReqCredentialForgotComplete_Params struct { // ServicesReqCredentialForgotInitiate defines model for _services_req_credential_forgot_initiate. type ServicesReqCredentialForgotInitiate struct { - ApiKey string `json:"api_key"` - AppTypeIdentifier string `json:"app_type_identifier"` - AuthType ServicesReqCredentialForgotInitiateAuthType `json:"auth_type"` - Identifier string `json:"identifier"` - OrgId string `json:"org_id"` + ApiKey string `json:"api_key"` + AppTypeIdentifier string `json:"app_type_identifier"` + AuthType ServicesReqCredentialForgotInitiateAuthType `json:"auth_type"` + Identifier ServicesReqCredentialForgotInitiate_Identifier `json:"identifier"` + OrgId string `json:"org_id"` } // ServicesReqCredentialForgotInitiateAuthType defines model for ServicesReqCredentialForgotInitiate.AuthType. type ServicesReqCredentialForgotInitiateAuthType string -// ServicesReqCredentialSendVerify defines model for _services_req_credential_send-verify. -type ServicesReqCredentialSendVerify struct { - ApiKey string `json:"api_key"` - AppTypeIdentifier string `json:"app_type_identifier"` - AuthType ServicesReqCredentialSendVerifyAuthType `json:"auth_type"` - Identifier string `json:"identifier"` - OrgId string `json:"org_id"` +// ServicesReqCredentialForgotInitiate_Identifier defines model for ServicesReqCredentialForgotInitiate.Identifier. +type ServicesReqCredentialForgotInitiate_Identifier struct { + union json.RawMessage } -// ServicesReqCredentialSendVerifyAuthType defines model for ServicesReqCredentialSendVerify.AuthType. -type ServicesReqCredentialSendVerifyAuthType string - // ServicesReqCredentialUpdate defines model for _services_req_credential_update. type ServicesReqCredentialUpdate struct { AccountAuthTypeId string `json:"account_auth_type_id"` @@ -836,6 +886,23 @@ type ServicesReqCredentialUpdate_Params struct { union json.RawMessage } +// ServicesReqIdentifierSendVerify defines model for _services_req_identifier_send-verify. +type ServicesReqIdentifierSendVerify struct { + ApiKey string `json:"api_key"` + AppTypeIdentifier string `json:"app_type_identifier"` + AuthType *ServicesReqIdentifierSendVerifyAuthType `json:"auth_type,omitempty"` + Identifier ServicesReqIdentifierSendVerify_Identifier `json:"identifier"` + OrgId string `json:"org_id"` +} + +// ServicesReqIdentifierSendVerifyAuthType defines model for ServicesReqIdentifierSendVerify.AuthType. +type ServicesReqIdentifierSendVerifyAuthType string + +// ServicesReqIdentifierSendVerify_Identifier defines model for ServicesReqIdentifierSendVerify.Identifier. +type ServicesReqIdentifierSendVerify_Identifier struct { + union json.RawMessage +} + // ServicesReqServiceAccountsAccessToken defines model for _services_req_service-accounts_access-token. type ServicesReqServiceAccountsAccessToken struct { AccountId string `json:"account_id"` @@ -875,8 +942,15 @@ type ServicesReqServiceAccountsParamsAuthType string // ServicesResAccountAuthTypeLink defines model for _services_res_account_auth-type-link. type ServicesResAccountAuthTypeLink struct { - AuthTypes []AccountAuthType `json:"auth_types"` - Message *string `json:"message"` + AuthTypes []AccountAuthType `json:"auth_types"` + Identifiers *[]AccountIdentifier `json:"identifiers,omitempty"` + Message *string `json:"message"` +} + +// ServicesResAccountIdentifierLink defines model for _services_res_account_identifier-link. +type ServicesResAccountIdentifierLink struct { + Identifiers []AccountIdentifier `json:"identifiers"` + Message *string `json:"message"` } // ServicesResAuthorizeService defines model for _services_res_authorize-service. @@ -908,11 +982,16 @@ type ServicesServiceAccountsCredsStaticToken struct { // SharedReqAccountCheck defines model for _shared_req_AccountCheck. type SharedReqAccountCheck struct { - ApiKey string `json:"api_key"` - AppTypeIdentifier string `json:"app_type_identifier"` - AuthType SharedReqAccountCheckAuthType `json:"auth_type"` - OrgId string `json:"org_id"` - UserIdentifier string `json:"user_identifier"` + ApiKey string `json:"api_key"` + AppTypeIdentifier string `json:"app_type_identifier"` + // Deprecated: + AuthType *SharedReqAccountCheckAuthType `json:"auth_type,omitempty"` + + // Identifier Allowed identifier types + Identifier *SharedReqIdentifiers `json:"identifier,omitempty"` + OrgId string `json:"org_id"` + // Deprecated: + UserIdentifier *string `json:"user_identifier,omitempty"` } // SharedReqAccountCheckAuthType defines model for SharedReqAccountCheck.AuthType. @@ -920,60 +999,84 @@ type SharedReqAccountCheckAuthType string // SharedReqCreateAccount defines model for _shared_req_CreateAccount. type SharedReqCreateAccount struct { - AuthType SharedReqCreateAccountAuthType `json:"auth_type"` - GroupIds *[]string `json:"group_ids,omitempty"` - Identifier string `json:"identifier"` - Permissions *[]string `json:"permissions,omitempty"` - Privacy *PrivacyNullable `json:"privacy"` - Profile *ProfileNullable `json:"profile"` - RoleIds *[]string `json:"role_ids,omitempty"` - Scopes *[]string `json:"scopes,omitempty"` - Username *string `json:"username"` + AuthType SharedReqCreateAccountAuthType `json:"auth_type"` + GroupIds *[]string `json:"group_ids,omitempty"` + + // Identifier Allowed identifier types + Identifier SharedReqIdentifiers `json:"identifier"` + Permissions *[]string `json:"permissions,omitempty"` + Privacy *PrivacyNullable `json:"privacy"` + Profile *ProfileNullable `json:"profile"` + RoleIds *[]string `json:"role_ids,omitempty"` + Scopes *[]string `json:"scopes,omitempty"` } // SharedReqCreateAccountAuthType defines model for SharedReqCreateAccount.AuthType. type SharedReqCreateAccountAuthType string -// SharedReqCredsAPIKey Auth login creds for auth_type="anonymous" -type SharedReqCredsAPIKey struct { +// SharedReqCredsAnonymous Auth login creds for auth_type="anonymous" +type SharedReqCredsAnonymous struct { AnonymousId *string `json:"anonymous_id,omitempty"` } -// SharedReqCredsEmail Auth login creds for auth_type="email" -type SharedReqCredsEmail struct { - Email string `json:"email"` - Password string `json:"password"` +// SharedReqCredsCode defines model for _shared_req_CredsCode. +type SharedReqCredsCode struct { + Code *string `json:"code,omitempty"` + Email *string `json:"email,omitempty"` + Phone *string `json:"phone,omitempty"` + Username *string `json:"username,omitempty"` + AdditionalProperties map[string]string `json:"-"` } +// SharedReqCredsNone Auth login request creds for unlisted auth_types (None) +type SharedReqCredsNone = map[string]interface{} + // SharedReqCredsOIDC Auth login creds for auth_type="oidc" (or variants) // - full redirect URI received from OIDC provider type SharedReqCredsOIDC = string -// SharedReqCredsTwilioPhone Auth login creds for auth_type="twilio_phone" -type SharedReqCredsTwilioPhone struct { - Code *string `json:"code,omitempty"` - Phone string `json:"phone"` +// SharedReqCredsPassword defines model for _shared_req_CredsPassword. +type SharedReqCredsPassword struct { + Email *string `json:"email,omitempty"` + Password string `json:"password"` + Phone *string `json:"phone,omitempty"` + Username *string `json:"username,omitempty"` + AdditionalProperties map[string]string `json:"-"` } -// SharedReqCredsUsername Auth login creds for auth_type="username" -type SharedReqCredsUsername struct { - Password string `json:"password"` - Username string `json:"username"` +// SharedReqCredsWebAuthn defines model for _shared_req_CredsWebAuthn. +type SharedReqCredsWebAuthn struct { + Email *string `json:"email,omitempty"` + Phone *string `json:"phone,omitempty"` + Response *string `json:"response,omitempty"` + Username *string `json:"username,omitempty"` + AdditionalProperties map[string]string `json:"-"` +} + +// SharedReqIdentifierString User identifier string +type SharedReqIdentifierString = string + +// SharedReqIdentifiers Allowed identifier types +type SharedReqIdentifiers struct { + Email *string `json:"email,omitempty"` + Phone *string `json:"phone,omitempty"` + Username *string `json:"username,omitempty"` + AdditionalProperties map[string]string `json:"-"` } // SharedReqLogin defines model for _shared_req_Login. type SharedReqLogin struct { - ApiKey string `json:"api_key"` - AppTypeIdentifier string `json:"app_type_identifier"` - AuthType SharedReqLoginAuthType `json:"auth_type"` - Creds *SharedReqLogin_Creds `json:"creds,omitempty"` - Device Device `json:"device"` - OrgId string `json:"org_id"` - Params *SharedReqLogin_Params `json:"params,omitempty"` - Preferences *map[string]interface{} `json:"preferences"` - Privacy *PrivacyNullable `json:"privacy"` - Profile *ProfileNullable `json:"profile"` - Username *string `json:"username"` + AccountIdentifierId *string `json:"account_identifier_id"` + ApiKey string `json:"api_key"` + AppTypeIdentifier string `json:"app_type_identifier"` + AuthType SharedReqLoginAuthType `json:"auth_type"` + Creds *SharedReqLogin_Creds `json:"creds,omitempty"` + Device Device `json:"device"` + OrgId string `json:"org_id"` + Params *SharedReqLogin_Params `json:"params,omitempty"` + Preferences *map[string]interface{} `json:"preferences"` + Privacy *PrivacyNullable `json:"privacy"` + Profile *ProfileNullable `json:"profile"` } // SharedReqLoginAuthType defines model for SharedReqLogin.AuthType. @@ -1025,13 +1128,6 @@ type SharedReqMfa struct { // SharedReqMfaType defines model for SharedReqMfa.Type. type SharedReqMfaType string -// SharedReqParamsEmail Auth login params for auth_type="email" -type SharedReqParamsEmail struct { - // ConfirmPassword This should match the `creds` password field when sign_up=true. This should be verified on the client side as well to reduce invalid requests. - ConfirmPassword *string `json:"confirm_password,omitempty"` - SignUp *bool `json:"sign_up,omitempty"` -} - // SharedReqParamsNone Auth login request params for unlisted auth_types (None) type SharedReqParamsNone = map[string]interface{} @@ -1041,17 +1137,24 @@ type SharedReqParamsOIDC struct { RedirectUri *string `json:"redirect_uri,omitempty"` } -// SharedReqParamsSetEmailCredential defines model for _shared_req_ParamsSetEmailCredential. -type SharedReqParamsSetEmailCredential struct { +// SharedReqParamsPassword Auth login params for auth_type="email" +type SharedReqParamsPassword struct { + // ConfirmPassword This should match the `creds` password field when sign_up=true. This should be verified on the client side as well to reduce invalid requests. + ConfirmPassword *string `json:"confirm_password,omitempty"` + SignUp *bool `json:"sign_up,omitempty"` +} + +// SharedReqParamsResetPassword defines model for _shared_req_ParamsResetPassword. +type SharedReqParamsResetPassword struct { ConfirmPassword string `json:"confirm_password"` NewPassword string `json:"new_password"` } -// SharedReqParamsUsername Auth login params for auth_type="username" -type SharedReqParamsUsername struct { - // ConfirmPassword This should match the `creds` password field when sign_up=true. This should be verified on the client side as well to reduce invalid requests. - ConfirmPassword *string `json:"confirm_password,omitempty"` - SignUp *bool `json:"sign_up,omitempty"` +// SharedReqParamsWebAuthn Auth login params for auth_type="webauthn" +type SharedReqParamsWebAuthn struct { + // DisplayName User's account name for display purposes + DisplayName *string `json:"display_name,omitempty"` + SignUp *bool `json:"sign_up,omitempty"` } // SharedReqRefresh defines model for _shared_req_Refresh. @@ -1062,12 +1165,14 @@ type SharedReqRefresh struct { // SharedReqUpdateAccount defines model for _shared_req_UpdateAccount. type SharedReqUpdateAccount struct { - AuthType SharedReqUpdateAccountAuthType `json:"auth_type"` - GroupIds *[]string `json:"group_ids,omitempty"` - Identifier string `json:"identifier"` - Permissions *[]string `json:"permissions,omitempty"` - RoleIds *[]string `json:"role_ids,omitempty"` - Scopes *[]string `json:"scopes,omitempty"` + AuthType SharedReqUpdateAccountAuthType `json:"auth_type"` + GroupIds *[]string `json:"group_ids,omitempty"` + + // Identifier Allowed identifier types + Identifier SharedReqIdentifiers `json:"identifier"` + Permissions *[]string `json:"permissions,omitempty"` + RoleIds *[]string `json:"role_ids,omitempty"` + Scopes *[]string `json:"scopes,omitempty"` } // SharedReqUpdateAccountAuthType defines model for SharedReqUpdateAccount.AuthType. @@ -1135,8 +1240,8 @@ type SharedResMfa struct { Verified *bool `json:"verified,omitempty"` } -// SharedResParamsAPIKey Auth login response params for auth_type="anonymous" -type SharedResParamsAPIKey struct { +// SharedResParamsAnonymous Auth login response params for auth_type="anonymous" +type SharedResParamsAnonymous struct { AnonymousId *string `json:"anonymous_id,omitempty"` } @@ -1180,6 +1285,12 @@ type SharedResRokwireToken struct { // SharedResRokwireTokenTokenType The type of the provided tokens to be specified when they are sent in the "Authorization" header type SharedResRokwireTokenTokenType string +// SharedResSignInOptions defines model for _shared_res_SignInOptions. +type SharedResSignInOptions struct { + AuthTypes []AccountAuthType `json:"auth_types"` + Identifiers []AccountIdentifier `json:"identifiers"` +} + // SystemReqUpdateServiceAccount defines model for _system_req_update_service-account. type SystemReqUpdateServiceAccount struct { Name *string `json:"name,omitempty"` @@ -1410,9 +1521,9 @@ type GetServicesAccountsPublicParams struct { FollowerId *string `form:"follower-id,omitempty" json:"follower-id,omitempty"` } -// GetServicesAuthCredentialVerifyParams defines parameters for GetServicesAuthCredentialVerify. -type GetServicesAuthCredentialVerifyParams struct { - // Id Credential ID +// GetServicesAuthIdentifierVerifyParams defines parameters for GetServicesAuthIdentifierVerify. +type GetServicesAuthIdentifierVerifyParams struct { + // Id Account identifier ID Id string `form:"id" json:"id"` // Code Verification code @@ -1595,9 +1706,9 @@ type GetUiCredentialResetParams struct { Code string `form:"code" json:"code"` } -// GetUiCredentialVerifyParams defines parameters for GetUiCredentialVerify. -type GetUiCredentialVerifyParams struct { - // Id Credential ID +// GetUiIdentifierVerifyParams defines parameters for GetUiIdentifierVerify. +type GetUiIdentifierVerifyParams struct { + // Id Identifier ID Id string `form:"id" json:"id"` // Code Verification code @@ -1760,6 +1871,15 @@ type PostServicesAuthAccountCanSignInJSONRequestBody = SharedReqAccountCheck // PostServicesAuthAccountExistsJSONRequestBody defines body for PostServicesAuthAccountExists for application/json ContentType. type PostServicesAuthAccountExistsJSONRequestBody = SharedReqAccountCheck +// DeleteServicesAuthAccountIdentifierLinkJSONRequestBody defines body for DeleteServicesAuthAccountIdentifierLink for application/json ContentType. +type DeleteServicesAuthAccountIdentifierLinkJSONRequestBody = ServicesReqAccountIdentifierUnlink + +// PostServicesAuthAccountIdentifierLinkJSONRequestBody defines body for PostServicesAuthAccountIdentifierLink for application/json ContentType. +type PostServicesAuthAccountIdentifierLinkJSONRequestBody = ServicesReqAccountIdentifierLink + +// PostServicesAuthAccountSignInOptionsJSONRequestBody defines body for PostServicesAuthAccountSignInOptions for application/json ContentType. +type PostServicesAuthAccountSignInOptionsJSONRequestBody = SharedReqAccountCheck + // PostServicesAuthAuthorizeServiceJSONRequestBody defines body for PostServicesAuthAuthorizeService for application/json ContentType. type PostServicesAuthAuthorizeServiceJSONRequestBody = ServicesReqAuthorizeService @@ -1770,11 +1890,14 @@ type PostServicesAuthCredentialForgotCompleteJSONRequestBody = ServicesReqCreden type PostServicesAuthCredentialForgotInitiateJSONRequestBody = ServicesReqCredentialForgotInitiate // PostServicesAuthCredentialSendVerifyJSONRequestBody defines body for PostServicesAuthCredentialSendVerify for application/json ContentType. -type PostServicesAuthCredentialSendVerifyJSONRequestBody = ServicesReqCredentialSendVerify +type PostServicesAuthCredentialSendVerifyJSONRequestBody = ServicesReqIdentifierSendVerify // PostServicesAuthCredentialUpdateJSONRequestBody defines body for PostServicesAuthCredentialUpdate for application/json ContentType. type PostServicesAuthCredentialUpdateJSONRequestBody = ServicesReqCredentialUpdate +// PostServicesAuthIdentifierSendVerifyJSONRequestBody defines body for PostServicesAuthIdentifierSendVerify for application/json ContentType. +type PostServicesAuthIdentifierSendVerifyJSONRequestBody = ServicesReqIdentifierSendVerify + // PostServicesAuthLoginJSONRequestBody defines body for PostServicesAuthLogin for application/json ContentType. type PostServicesAuthLoginJSONRequestBody = SharedReqLogin @@ -1859,6 +1982,441 @@ type PostTpsAccountsCountJSONRequestBody = PostTpsAccountsCountJSONBody // PostTpsServiceAccountIdJSONRequestBody defines body for PostTpsServiceAccountId for application/json ContentType. type PostTpsServiceAccountIdJSONRequestBody = ServicesReqServiceAccountsParams +// Getter for additional properties for SharedReqCredsCode. Returns the specified +// element and whether it was found +func (a SharedReqCredsCode) Get(fieldName string) (value string, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for SharedReqCredsCode +func (a *SharedReqCredsCode) Set(fieldName string, value string) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]string) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for SharedReqCredsCode to handle AdditionalProperties +func (a *SharedReqCredsCode) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["code"]; found { + err = json.Unmarshal(raw, &a.Code) + if err != nil { + return fmt.Errorf("error reading 'code': %w", err) + } + delete(object, "code") + } + + if raw, found := object["email"]; found { + err = json.Unmarshal(raw, &a.Email) + if err != nil { + return fmt.Errorf("error reading 'email': %w", err) + } + delete(object, "email") + } + + if raw, found := object["phone"]; found { + err = json.Unmarshal(raw, &a.Phone) + if err != nil { + return fmt.Errorf("error reading 'phone': %w", err) + } + delete(object, "phone") + } + + if raw, found := object["username"]; found { + err = json.Unmarshal(raw, &a.Username) + if err != nil { + return fmt.Errorf("error reading 'username': %w", err) + } + delete(object, "username") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]string) + for fieldName, fieldBuf := range object { + var fieldVal string + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for SharedReqCredsCode to handle AdditionalProperties +func (a SharedReqCredsCode) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Code != nil { + object["code"], err = json.Marshal(a.Code) + if err != nil { + return nil, fmt.Errorf("error marshaling 'code': %w", err) + } + } + + if a.Email != nil { + object["email"], err = json.Marshal(a.Email) + if err != nil { + return nil, fmt.Errorf("error marshaling 'email': %w", err) + } + } + + if a.Phone != nil { + object["phone"], err = json.Marshal(a.Phone) + if err != nil { + return nil, fmt.Errorf("error marshaling 'phone': %w", err) + } + } + + if a.Username != nil { + object["username"], err = json.Marshal(a.Username) + if err != nil { + return nil, fmt.Errorf("error marshaling 'username': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for SharedReqCredsPassword. Returns the specified +// element and whether it was found +func (a SharedReqCredsPassword) Get(fieldName string) (value string, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for SharedReqCredsPassword +func (a *SharedReqCredsPassword) Set(fieldName string, value string) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]string) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for SharedReqCredsPassword to handle AdditionalProperties +func (a *SharedReqCredsPassword) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["email"]; found { + err = json.Unmarshal(raw, &a.Email) + if err != nil { + return fmt.Errorf("error reading 'email': %w", err) + } + delete(object, "email") + } + + if raw, found := object["password"]; found { + err = json.Unmarshal(raw, &a.Password) + if err != nil { + return fmt.Errorf("error reading 'password': %w", err) + } + delete(object, "password") + } + + if raw, found := object["phone"]; found { + err = json.Unmarshal(raw, &a.Phone) + if err != nil { + return fmt.Errorf("error reading 'phone': %w", err) + } + delete(object, "phone") + } + + if raw, found := object["username"]; found { + err = json.Unmarshal(raw, &a.Username) + if err != nil { + return fmt.Errorf("error reading 'username': %w", err) + } + delete(object, "username") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]string) + for fieldName, fieldBuf := range object { + var fieldVal string + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for SharedReqCredsPassword to handle AdditionalProperties +func (a SharedReqCredsPassword) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Email != nil { + object["email"], err = json.Marshal(a.Email) + if err != nil { + return nil, fmt.Errorf("error marshaling 'email': %w", err) + } + } + + object["password"], err = json.Marshal(a.Password) + if err != nil { + return nil, fmt.Errorf("error marshaling 'password': %w", err) + } + + if a.Phone != nil { + object["phone"], err = json.Marshal(a.Phone) + if err != nil { + return nil, fmt.Errorf("error marshaling 'phone': %w", err) + } + } + + if a.Username != nil { + object["username"], err = json.Marshal(a.Username) + if err != nil { + return nil, fmt.Errorf("error marshaling 'username': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for SharedReqCredsWebAuthn. Returns the specified +// element and whether it was found +func (a SharedReqCredsWebAuthn) Get(fieldName string) (value string, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for SharedReqCredsWebAuthn +func (a *SharedReqCredsWebAuthn) Set(fieldName string, value string) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]string) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for SharedReqCredsWebAuthn to handle AdditionalProperties +func (a *SharedReqCredsWebAuthn) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["email"]; found { + err = json.Unmarshal(raw, &a.Email) + if err != nil { + return fmt.Errorf("error reading 'email': %w", err) + } + delete(object, "email") + } + + if raw, found := object["phone"]; found { + err = json.Unmarshal(raw, &a.Phone) + if err != nil { + return fmt.Errorf("error reading 'phone': %w", err) + } + delete(object, "phone") + } + + if raw, found := object["response"]; found { + err = json.Unmarshal(raw, &a.Response) + if err != nil { + return fmt.Errorf("error reading 'response': %w", err) + } + delete(object, "response") + } + + if raw, found := object["username"]; found { + err = json.Unmarshal(raw, &a.Username) + if err != nil { + return fmt.Errorf("error reading 'username': %w", err) + } + delete(object, "username") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]string) + for fieldName, fieldBuf := range object { + var fieldVal string + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for SharedReqCredsWebAuthn to handle AdditionalProperties +func (a SharedReqCredsWebAuthn) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Email != nil { + object["email"], err = json.Marshal(a.Email) + if err != nil { + return nil, fmt.Errorf("error marshaling 'email': %w", err) + } + } + + if a.Phone != nil { + object["phone"], err = json.Marshal(a.Phone) + if err != nil { + return nil, fmt.Errorf("error marshaling 'phone': %w", err) + } + } + + if a.Response != nil { + object["response"], err = json.Marshal(a.Response) + if err != nil { + return nil, fmt.Errorf("error marshaling 'response': %w", err) + } + } + + if a.Username != nil { + object["username"], err = json.Marshal(a.Username) + if err != nil { + return nil, fmt.Errorf("error marshaling 'username': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for SharedReqIdentifiers. Returns the specified +// element and whether it was found +func (a SharedReqIdentifiers) Get(fieldName string) (value string, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for SharedReqIdentifiers +func (a *SharedReqIdentifiers) Set(fieldName string, value string) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]string) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for SharedReqIdentifiers to handle AdditionalProperties +func (a *SharedReqIdentifiers) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["email"]; found { + err = json.Unmarshal(raw, &a.Email) + if err != nil { + return fmt.Errorf("error reading 'email': %w", err) + } + delete(object, "email") + } + + if raw, found := object["phone"]; found { + err = json.Unmarshal(raw, &a.Phone) + if err != nil { + return fmt.Errorf("error reading 'phone': %w", err) + } + delete(object, "phone") + } + + if raw, found := object["username"]; found { + err = json.Unmarshal(raw, &a.Username) + if err != nil { + return fmt.Errorf("error reading 'username': %w", err) + } + delete(object, "username") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]string) + for fieldName, fieldBuf := range object { + var fieldVal string + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for SharedReqIdentifiers to handle AdditionalProperties +func (a SharedReqIdentifiers) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Email != nil { + object["email"], err = json.Marshal(a.Email) + if err != nil { + return nil, fmt.Errorf("error marshaling 'email': %w", err) + } + } + + if a.Phone != nil { + object["phone"], err = json.Marshal(a.Phone) + if err != nil { + return nil, fmt.Errorf("error marshaling 'phone': %w", err) + } + } + + if a.Username != nil { + object["username"], err = json.Marshal(a.Username) + if err != nil { + return nil, fmt.Errorf("error marshaling 'username': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + // AsEnvConfigData returns the union data inside the Config_Data as a EnvConfigData func (t Config_Data) AsEnvConfigData() (EnvConfigData, error) { var body EnvConfigData @@ -1885,6 +2443,32 @@ func (t *Config_Data) MergeEnvConfigData(v EnvConfigData) error { return err } +// AsAuthConfigData returns the union data inside the Config_Data as a AuthConfigData +func (t Config_Data) AsAuthConfigData() (AuthConfigData, error) { + var body AuthConfigData + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromAuthConfigData overwrites any union data inside the Config_Data as the provided AuthConfigData +func (t *Config_Data) FromAuthConfigData(v AuthConfigData) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeAuthConfigData performs a merge with any union data inside the Config_Data, using the provided AuthConfigData +func (t *Config_Data) MergeAuthConfigData(v AuthConfigData) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(t.union, b) + t.union = merged + return err +} + func (t Config_Data) MarshalJSON() ([]byte, error) { b, err := t.union.MarshalJSON() return b, err @@ -1921,6 +2505,32 @@ func (t *AdminReqCreateUpdateConfig_Data) MergeEnvConfigData(v EnvConfigData) er return err } +// AsAuthConfigData returns the union data inside the AdminReqCreateUpdateConfig_Data as a AuthConfigData +func (t AdminReqCreateUpdateConfig_Data) AsAuthConfigData() (AuthConfigData, error) { + var body AuthConfigData + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromAuthConfigData overwrites any union data inside the AdminReqCreateUpdateConfig_Data as the provided AuthConfigData +func (t *AdminReqCreateUpdateConfig_Data) FromAuthConfigData(v AuthConfigData) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeAuthConfigData performs a merge with any union data inside the AdminReqCreateUpdateConfig_Data, using the provided AuthConfigData +func (t *AdminReqCreateUpdateConfig_Data) MergeAuthConfigData(v AuthConfigData) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(t.union, b) + t.union = merged + return err +} + func (t AdminReqCreateUpdateConfig_Data) MarshalJSON() ([]byte, error) { b, err := t.union.MarshalJSON() return b, err @@ -1931,22 +2541,22 @@ func (t *AdminReqCreateUpdateConfig_Data) UnmarshalJSON(b []byte) error { return err } -// AsSharedReqCredsEmail returns the union data inside the ServicesReqAccountAuthTypeLink_Creds as a SharedReqCredsEmail -func (t ServicesReqAccountAuthTypeLink_Creds) AsSharedReqCredsEmail() (SharedReqCredsEmail, error) { - var body SharedReqCredsEmail +// AsSharedReqCredsCode returns the union data inside the ServicesReqAccountAuthTypeLink_Creds as a SharedReqCredsCode +func (t ServicesReqAccountAuthTypeLink_Creds) AsSharedReqCredsCode() (SharedReqCredsCode, error) { + var body SharedReqCredsCode err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqCredsEmail overwrites any union data inside the ServicesReqAccountAuthTypeLink_Creds as the provided SharedReqCredsEmail -func (t *ServicesReqAccountAuthTypeLink_Creds) FromSharedReqCredsEmail(v SharedReqCredsEmail) error { +// FromSharedReqCredsCode overwrites any union data inside the ServicesReqAccountAuthTypeLink_Creds as the provided SharedReqCredsCode +func (t *ServicesReqAccountAuthTypeLink_Creds) FromSharedReqCredsCode(v SharedReqCredsCode) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqCredsEmail performs a merge with any union data inside the ServicesReqAccountAuthTypeLink_Creds, using the provided SharedReqCredsEmail -func (t *ServicesReqAccountAuthTypeLink_Creds) MergeSharedReqCredsEmail(v SharedReqCredsEmail) error { +// MergeSharedReqCredsCode performs a merge with any union data inside the ServicesReqAccountAuthTypeLink_Creds, using the provided SharedReqCredsCode +func (t *ServicesReqAccountAuthTypeLink_Creds) MergeSharedReqCredsCode(v SharedReqCredsCode) error { b, err := json.Marshal(v) if err != nil { return err @@ -1957,22 +2567,22 @@ func (t *ServicesReqAccountAuthTypeLink_Creds) MergeSharedReqCredsEmail(v Shared return err } -// AsSharedReqCredsTwilioPhone returns the union data inside the ServicesReqAccountAuthTypeLink_Creds as a SharedReqCredsTwilioPhone -func (t ServicesReqAccountAuthTypeLink_Creds) AsSharedReqCredsTwilioPhone() (SharedReqCredsTwilioPhone, error) { - var body SharedReqCredsTwilioPhone +// AsSharedReqCredsOIDC returns the union data inside the ServicesReqAccountAuthTypeLink_Creds as a SharedReqCredsOIDC +func (t ServicesReqAccountAuthTypeLink_Creds) AsSharedReqCredsOIDC() (SharedReqCredsOIDC, error) { + var body SharedReqCredsOIDC err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqCredsTwilioPhone overwrites any union data inside the ServicesReqAccountAuthTypeLink_Creds as the provided SharedReqCredsTwilioPhone -func (t *ServicesReqAccountAuthTypeLink_Creds) FromSharedReqCredsTwilioPhone(v SharedReqCredsTwilioPhone) error { +// FromSharedReqCredsOIDC overwrites any union data inside the ServicesReqAccountAuthTypeLink_Creds as the provided SharedReqCredsOIDC +func (t *ServicesReqAccountAuthTypeLink_Creds) FromSharedReqCredsOIDC(v SharedReqCredsOIDC) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqCredsTwilioPhone performs a merge with any union data inside the ServicesReqAccountAuthTypeLink_Creds, using the provided SharedReqCredsTwilioPhone -func (t *ServicesReqAccountAuthTypeLink_Creds) MergeSharedReqCredsTwilioPhone(v SharedReqCredsTwilioPhone) error { +// MergeSharedReqCredsOIDC performs a merge with any union data inside the ServicesReqAccountAuthTypeLink_Creds, using the provided SharedReqCredsOIDC +func (t *ServicesReqAccountAuthTypeLink_Creds) MergeSharedReqCredsOIDC(v SharedReqCredsOIDC) error { b, err := json.Marshal(v) if err != nil { return err @@ -1983,22 +2593,22 @@ func (t *ServicesReqAccountAuthTypeLink_Creds) MergeSharedReqCredsTwilioPhone(v return err } -// AsSharedReqCredsOIDC returns the union data inside the ServicesReqAccountAuthTypeLink_Creds as a SharedReqCredsOIDC -func (t ServicesReqAccountAuthTypeLink_Creds) AsSharedReqCredsOIDC() (SharedReqCredsOIDC, error) { - var body SharedReqCredsOIDC +// AsSharedReqCredsPassword returns the union data inside the ServicesReqAccountAuthTypeLink_Creds as a SharedReqCredsPassword +func (t ServicesReqAccountAuthTypeLink_Creds) AsSharedReqCredsPassword() (SharedReqCredsPassword, error) { + var body SharedReqCredsPassword err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqCredsOIDC overwrites any union data inside the ServicesReqAccountAuthTypeLink_Creds as the provided SharedReqCredsOIDC -func (t *ServicesReqAccountAuthTypeLink_Creds) FromSharedReqCredsOIDC(v SharedReqCredsOIDC) error { +// FromSharedReqCredsPassword overwrites any union data inside the ServicesReqAccountAuthTypeLink_Creds as the provided SharedReqCredsPassword +func (t *ServicesReqAccountAuthTypeLink_Creds) FromSharedReqCredsPassword(v SharedReqCredsPassword) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqCredsOIDC performs a merge with any union data inside the ServicesReqAccountAuthTypeLink_Creds, using the provided SharedReqCredsOIDC -func (t *ServicesReqAccountAuthTypeLink_Creds) MergeSharedReqCredsOIDC(v SharedReqCredsOIDC) error { +// MergeSharedReqCredsPassword performs a merge with any union data inside the ServicesReqAccountAuthTypeLink_Creds, using the provided SharedReqCredsPassword +func (t *ServicesReqAccountAuthTypeLink_Creds) MergeSharedReqCredsPassword(v SharedReqCredsPassword) error { b, err := json.Marshal(v) if err != nil { return err @@ -2009,32 +2619,48 @@ func (t *ServicesReqAccountAuthTypeLink_Creds) MergeSharedReqCredsOIDC(v SharedR return err } -func (t ServicesReqAccountAuthTypeLink_Creds) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err +// AsSharedReqCredsWebAuthn returns the union data inside the ServicesReqAccountAuthTypeLink_Creds as a SharedReqCredsWebAuthn +func (t ServicesReqAccountAuthTypeLink_Creds) AsSharedReqCredsWebAuthn() (SharedReqCredsWebAuthn, error) { + var body SharedReqCredsWebAuthn + err := json.Unmarshal(t.union, &body) + return body, err } -func (t *ServicesReqAccountAuthTypeLink_Creds) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) +// FromSharedReqCredsWebAuthn overwrites any union data inside the ServicesReqAccountAuthTypeLink_Creds as the provided SharedReqCredsWebAuthn +func (t *ServicesReqAccountAuthTypeLink_Creds) FromSharedReqCredsWebAuthn(v SharedReqCredsWebAuthn) error { + b, err := json.Marshal(v) + t.union = b return err } -// AsSharedReqParamsEmail returns the union data inside the ServicesReqAccountAuthTypeLink_Params as a SharedReqParamsEmail -func (t ServicesReqAccountAuthTypeLink_Params) AsSharedReqParamsEmail() (SharedReqParamsEmail, error) { - var body SharedReqParamsEmail +// MergeSharedReqCredsWebAuthn performs a merge with any union data inside the ServicesReqAccountAuthTypeLink_Creds, using the provided SharedReqCredsWebAuthn +func (t *ServicesReqAccountAuthTypeLink_Creds) MergeSharedReqCredsWebAuthn(v SharedReqCredsWebAuthn) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(t.union, b) + t.union = merged + return err +} + +// AsSharedReqCredsNone returns the union data inside the ServicesReqAccountAuthTypeLink_Creds as a SharedReqCredsNone +func (t ServicesReqAccountAuthTypeLink_Creds) AsSharedReqCredsNone() (SharedReqCredsNone, error) { + var body SharedReqCredsNone err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqParamsEmail overwrites any union data inside the ServicesReqAccountAuthTypeLink_Params as the provided SharedReqParamsEmail -func (t *ServicesReqAccountAuthTypeLink_Params) FromSharedReqParamsEmail(v SharedReqParamsEmail) error { +// FromSharedReqCredsNone overwrites any union data inside the ServicesReqAccountAuthTypeLink_Creds as the provided SharedReqCredsNone +func (t *ServicesReqAccountAuthTypeLink_Creds) FromSharedReqCredsNone(v SharedReqCredsNone) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqParamsEmail performs a merge with any union data inside the ServicesReqAccountAuthTypeLink_Params, using the provided SharedReqParamsEmail -func (t *ServicesReqAccountAuthTypeLink_Params) MergeSharedReqParamsEmail(v SharedReqParamsEmail) error { +// MergeSharedReqCredsNone performs a merge with any union data inside the ServicesReqAccountAuthTypeLink_Creds, using the provided SharedReqCredsNone +func (t *ServicesReqAccountAuthTypeLink_Creds) MergeSharedReqCredsNone(v SharedReqCredsNone) error { b, err := json.Marshal(v) if err != nil { return err @@ -2045,6 +2671,16 @@ func (t *ServicesReqAccountAuthTypeLink_Params) MergeSharedReqParamsEmail(v Shar return err } +func (t ServicesReqAccountAuthTypeLink_Creds) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *ServicesReqAccountAuthTypeLink_Creds) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + // AsSharedReqParamsOIDC returns the union data inside the ServicesReqAccountAuthTypeLink_Params as a SharedReqParamsOIDC func (t ServicesReqAccountAuthTypeLink_Params) AsSharedReqParamsOIDC() (SharedReqParamsOIDC, error) { var body SharedReqParamsOIDC @@ -2071,6 +2707,58 @@ func (t *ServicesReqAccountAuthTypeLink_Params) MergeSharedReqParamsOIDC(v Share return err } +// AsSharedReqParamsPassword returns the union data inside the ServicesReqAccountAuthTypeLink_Params as a SharedReqParamsPassword +func (t ServicesReqAccountAuthTypeLink_Params) AsSharedReqParamsPassword() (SharedReqParamsPassword, error) { + var body SharedReqParamsPassword + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromSharedReqParamsPassword overwrites any union data inside the ServicesReqAccountAuthTypeLink_Params as the provided SharedReqParamsPassword +func (t *ServicesReqAccountAuthTypeLink_Params) FromSharedReqParamsPassword(v SharedReqParamsPassword) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeSharedReqParamsPassword performs a merge with any union data inside the ServicesReqAccountAuthTypeLink_Params, using the provided SharedReqParamsPassword +func (t *ServicesReqAccountAuthTypeLink_Params) MergeSharedReqParamsPassword(v SharedReqParamsPassword) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(t.union, b) + t.union = merged + return err +} + +// AsSharedReqParamsWebAuthn returns the union data inside the ServicesReqAccountAuthTypeLink_Params as a SharedReqParamsWebAuthn +func (t ServicesReqAccountAuthTypeLink_Params) AsSharedReqParamsWebAuthn() (SharedReqParamsWebAuthn, error) { + var body SharedReqParamsWebAuthn + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromSharedReqParamsWebAuthn overwrites any union data inside the ServicesReqAccountAuthTypeLink_Params as the provided SharedReqParamsWebAuthn +func (t *ServicesReqAccountAuthTypeLink_Params) FromSharedReqParamsWebAuthn(v SharedReqParamsWebAuthn) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeSharedReqParamsWebAuthn performs a merge with any union data inside the ServicesReqAccountAuthTypeLink_Params, using the provided SharedReqParamsWebAuthn +func (t *ServicesReqAccountAuthTypeLink_Params) MergeSharedReqParamsWebAuthn(v SharedReqParamsWebAuthn) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(t.union, b) + t.union = merged + return err +} + // AsSharedReqParamsNone returns the union data inside the ServicesReqAccountAuthTypeLink_Params as a SharedReqParamsNone func (t ServicesReqAccountAuthTypeLink_Params) AsSharedReqParamsNone() (SharedReqParamsNone, error) { var body SharedReqParamsNone @@ -2107,22 +2795,22 @@ func (t *ServicesReqAccountAuthTypeLink_Params) UnmarshalJSON(b []byte) error { return err } -// AsSharedReqParamsSetEmailCredential returns the union data inside the ServicesReqCredentialForgotComplete_Params as a SharedReqParamsSetEmailCredential -func (t ServicesReqCredentialForgotComplete_Params) AsSharedReqParamsSetEmailCredential() (SharedReqParamsSetEmailCredential, error) { - var body SharedReqParamsSetEmailCredential +// AsSharedReqParamsResetPassword returns the union data inside the ServicesReqCredentialForgotComplete_Params as a SharedReqParamsResetPassword +func (t ServicesReqCredentialForgotComplete_Params) AsSharedReqParamsResetPassword() (SharedReqParamsResetPassword, error) { + var body SharedReqParamsResetPassword err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqParamsSetEmailCredential overwrites any union data inside the ServicesReqCredentialForgotComplete_Params as the provided SharedReqParamsSetEmailCredential -func (t *ServicesReqCredentialForgotComplete_Params) FromSharedReqParamsSetEmailCredential(v SharedReqParamsSetEmailCredential) error { +// FromSharedReqParamsResetPassword overwrites any union data inside the ServicesReqCredentialForgotComplete_Params as the provided SharedReqParamsResetPassword +func (t *ServicesReqCredentialForgotComplete_Params) FromSharedReqParamsResetPassword(v SharedReqParamsResetPassword) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqParamsSetEmailCredential performs a merge with any union data inside the ServicesReqCredentialForgotComplete_Params, using the provided SharedReqParamsSetEmailCredential -func (t *ServicesReqCredentialForgotComplete_Params) MergeSharedReqParamsSetEmailCredential(v SharedReqParamsSetEmailCredential) error { +// MergeSharedReqParamsResetPassword performs a merge with any union data inside the ServicesReqCredentialForgotComplete_Params, using the provided SharedReqParamsResetPassword +func (t *ServicesReqCredentialForgotComplete_Params) MergeSharedReqParamsResetPassword(v SharedReqParamsResetPassword) error { b, err := json.Marshal(v) if err != nil { return err @@ -2143,22 +2831,84 @@ func (t *ServicesReqCredentialForgotComplete_Params) UnmarshalJSON(b []byte) err return err } -// AsSharedReqParamsSetEmailCredential returns the union data inside the ServicesReqCredentialUpdate_Params as a SharedReqParamsSetEmailCredential -func (t ServicesReqCredentialUpdate_Params) AsSharedReqParamsSetEmailCredential() (SharedReqParamsSetEmailCredential, error) { - var body SharedReqParamsSetEmailCredential +// AsSharedReqIdentifiers returns the union data inside the ServicesReqCredentialForgotInitiate_Identifier as a SharedReqIdentifiers +func (t ServicesReqCredentialForgotInitiate_Identifier) AsSharedReqIdentifiers() (SharedReqIdentifiers, error) { + var body SharedReqIdentifiers + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromSharedReqIdentifiers overwrites any union data inside the ServicesReqCredentialForgotInitiate_Identifier as the provided SharedReqIdentifiers +func (t *ServicesReqCredentialForgotInitiate_Identifier) FromSharedReqIdentifiers(v SharedReqIdentifiers) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeSharedReqIdentifiers performs a merge with any union data inside the ServicesReqCredentialForgotInitiate_Identifier, using the provided SharedReqIdentifiers +func (t *ServicesReqCredentialForgotInitiate_Identifier) MergeSharedReqIdentifiers(v SharedReqIdentifiers) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(t.union, b) + t.union = merged + return err +} + +// AsSharedReqIdentifierString returns the union data inside the ServicesReqCredentialForgotInitiate_Identifier as a SharedReqIdentifierString +func (t ServicesReqCredentialForgotInitiate_Identifier) AsSharedReqIdentifierString() (SharedReqIdentifierString, error) { + var body SharedReqIdentifierString + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromSharedReqIdentifierString overwrites any union data inside the ServicesReqCredentialForgotInitiate_Identifier as the provided SharedReqIdentifierString +func (t *ServicesReqCredentialForgotInitiate_Identifier) FromSharedReqIdentifierString(v SharedReqIdentifierString) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeSharedReqIdentifierString performs a merge with any union data inside the ServicesReqCredentialForgotInitiate_Identifier, using the provided SharedReqIdentifierString +func (t *ServicesReqCredentialForgotInitiate_Identifier) MergeSharedReqIdentifierString(v SharedReqIdentifierString) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(t.union, b) + t.union = merged + return err +} + +func (t ServicesReqCredentialForgotInitiate_Identifier) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *ServicesReqCredentialForgotInitiate_Identifier) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + +// AsSharedReqParamsResetPassword returns the union data inside the ServicesReqCredentialUpdate_Params as a SharedReqParamsResetPassword +func (t ServicesReqCredentialUpdate_Params) AsSharedReqParamsResetPassword() (SharedReqParamsResetPassword, error) { + var body SharedReqParamsResetPassword err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqParamsSetEmailCredential overwrites any union data inside the ServicesReqCredentialUpdate_Params as the provided SharedReqParamsSetEmailCredential -func (t *ServicesReqCredentialUpdate_Params) FromSharedReqParamsSetEmailCredential(v SharedReqParamsSetEmailCredential) error { +// FromSharedReqParamsResetPassword overwrites any union data inside the ServicesReqCredentialUpdate_Params as the provided SharedReqParamsResetPassword +func (t *ServicesReqCredentialUpdate_Params) FromSharedReqParamsResetPassword(v SharedReqParamsResetPassword) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqParamsSetEmailCredential performs a merge with any union data inside the ServicesReqCredentialUpdate_Params, using the provided SharedReqParamsSetEmailCredential -func (t *ServicesReqCredentialUpdate_Params) MergeSharedReqParamsSetEmailCredential(v SharedReqParamsSetEmailCredential) error { +// MergeSharedReqParamsResetPassword performs a merge with any union data inside the ServicesReqCredentialUpdate_Params, using the provided SharedReqParamsResetPassword +func (t *ServicesReqCredentialUpdate_Params) MergeSharedReqParamsResetPassword(v SharedReqParamsResetPassword) error { b, err := json.Marshal(v) if err != nil { return err @@ -2179,22 +2929,22 @@ func (t *ServicesReqCredentialUpdate_Params) UnmarshalJSON(b []byte) error { return err } -// AsSharedReqCredsEmail returns the union data inside the SharedReqLogin_Creds as a SharedReqCredsEmail -func (t SharedReqLogin_Creds) AsSharedReqCredsEmail() (SharedReqCredsEmail, error) { - var body SharedReqCredsEmail +// AsSharedReqIdentifiers returns the union data inside the ServicesReqIdentifierSendVerify_Identifier as a SharedReqIdentifiers +func (t ServicesReqIdentifierSendVerify_Identifier) AsSharedReqIdentifiers() (SharedReqIdentifiers, error) { + var body SharedReqIdentifiers err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqCredsEmail overwrites any union data inside the SharedReqLogin_Creds as the provided SharedReqCredsEmail -func (t *SharedReqLogin_Creds) FromSharedReqCredsEmail(v SharedReqCredsEmail) error { +// FromSharedReqIdentifiers overwrites any union data inside the ServicesReqIdentifierSendVerify_Identifier as the provided SharedReqIdentifiers +func (t *ServicesReqIdentifierSendVerify_Identifier) FromSharedReqIdentifiers(v SharedReqIdentifiers) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqCredsEmail performs a merge with any union data inside the SharedReqLogin_Creds, using the provided SharedReqCredsEmail -func (t *SharedReqLogin_Creds) MergeSharedReqCredsEmail(v SharedReqCredsEmail) error { +// MergeSharedReqIdentifiers performs a merge with any union data inside the ServicesReqIdentifierSendVerify_Identifier, using the provided SharedReqIdentifiers +func (t *ServicesReqIdentifierSendVerify_Identifier) MergeSharedReqIdentifiers(v SharedReqIdentifiers) error { b, err := json.Marshal(v) if err != nil { return err @@ -2205,22 +2955,84 @@ func (t *SharedReqLogin_Creds) MergeSharedReqCredsEmail(v SharedReqCredsEmail) e return err } -// AsSharedReqCredsTwilioPhone returns the union data inside the SharedReqLogin_Creds as a SharedReqCredsTwilioPhone -func (t SharedReqLogin_Creds) AsSharedReqCredsTwilioPhone() (SharedReqCredsTwilioPhone, error) { - var body SharedReqCredsTwilioPhone +// AsSharedReqIdentifierString returns the union data inside the ServicesReqIdentifierSendVerify_Identifier as a SharedReqIdentifierString +func (t ServicesReqIdentifierSendVerify_Identifier) AsSharedReqIdentifierString() (SharedReqIdentifierString, error) { + var body SharedReqIdentifierString err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqCredsTwilioPhone overwrites any union data inside the SharedReqLogin_Creds as the provided SharedReqCredsTwilioPhone -func (t *SharedReqLogin_Creds) FromSharedReqCredsTwilioPhone(v SharedReqCredsTwilioPhone) error { +// FromSharedReqIdentifierString overwrites any union data inside the ServicesReqIdentifierSendVerify_Identifier as the provided SharedReqIdentifierString +func (t *ServicesReqIdentifierSendVerify_Identifier) FromSharedReqIdentifierString(v SharedReqIdentifierString) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqCredsTwilioPhone performs a merge with any union data inside the SharedReqLogin_Creds, using the provided SharedReqCredsTwilioPhone -func (t *SharedReqLogin_Creds) MergeSharedReqCredsTwilioPhone(v SharedReqCredsTwilioPhone) error { +// MergeSharedReqIdentifierString performs a merge with any union data inside the ServicesReqIdentifierSendVerify_Identifier, using the provided SharedReqIdentifierString +func (t *ServicesReqIdentifierSendVerify_Identifier) MergeSharedReqIdentifierString(v SharedReqIdentifierString) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(t.union, b) + t.union = merged + return err +} + +func (t ServicesReqIdentifierSendVerify_Identifier) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *ServicesReqIdentifierSendVerify_Identifier) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + +// AsSharedReqCredsAnonymous returns the union data inside the SharedReqLogin_Creds as a SharedReqCredsAnonymous +func (t SharedReqLogin_Creds) AsSharedReqCredsAnonymous() (SharedReqCredsAnonymous, error) { + var body SharedReqCredsAnonymous + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromSharedReqCredsAnonymous overwrites any union data inside the SharedReqLogin_Creds as the provided SharedReqCredsAnonymous +func (t *SharedReqLogin_Creds) FromSharedReqCredsAnonymous(v SharedReqCredsAnonymous) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeSharedReqCredsAnonymous performs a merge with any union data inside the SharedReqLogin_Creds, using the provided SharedReqCredsAnonymous +func (t *SharedReqLogin_Creds) MergeSharedReqCredsAnonymous(v SharedReqCredsAnonymous) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(t.union, b) + t.union = merged + return err +} + +// AsSharedReqCredsCode returns the union data inside the SharedReqLogin_Creds as a SharedReqCredsCode +func (t SharedReqLogin_Creds) AsSharedReqCredsCode() (SharedReqCredsCode, error) { + var body SharedReqCredsCode + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromSharedReqCredsCode overwrites any union data inside the SharedReqLogin_Creds as the provided SharedReqCredsCode +func (t *SharedReqLogin_Creds) FromSharedReqCredsCode(v SharedReqCredsCode) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeSharedReqCredsCode performs a merge with any union data inside the SharedReqLogin_Creds, using the provided SharedReqCredsCode +func (t *SharedReqLogin_Creds) MergeSharedReqCredsCode(v SharedReqCredsCode) error { b, err := json.Marshal(v) if err != nil { return err @@ -2257,22 +3069,22 @@ func (t *SharedReqLogin_Creds) MergeSharedReqCredsOIDC(v SharedReqCredsOIDC) err return err } -// AsSharedReqCredsAPIKey returns the union data inside the SharedReqLogin_Creds as a SharedReqCredsAPIKey -func (t SharedReqLogin_Creds) AsSharedReqCredsAPIKey() (SharedReqCredsAPIKey, error) { - var body SharedReqCredsAPIKey +// AsSharedReqCredsPassword returns the union data inside the SharedReqLogin_Creds as a SharedReqCredsPassword +func (t SharedReqLogin_Creds) AsSharedReqCredsPassword() (SharedReqCredsPassword, error) { + var body SharedReqCredsPassword err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqCredsAPIKey overwrites any union data inside the SharedReqLogin_Creds as the provided SharedReqCredsAPIKey -func (t *SharedReqLogin_Creds) FromSharedReqCredsAPIKey(v SharedReqCredsAPIKey) error { +// FromSharedReqCredsPassword overwrites any union data inside the SharedReqLogin_Creds as the provided SharedReqCredsPassword +func (t *SharedReqLogin_Creds) FromSharedReqCredsPassword(v SharedReqCredsPassword) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqCredsAPIKey performs a merge with any union data inside the SharedReqLogin_Creds, using the provided SharedReqCredsAPIKey -func (t *SharedReqLogin_Creds) MergeSharedReqCredsAPIKey(v SharedReqCredsAPIKey) error { +// MergeSharedReqCredsPassword performs a merge with any union data inside the SharedReqLogin_Creds, using the provided SharedReqCredsPassword +func (t *SharedReqLogin_Creds) MergeSharedReqCredsPassword(v SharedReqCredsPassword) error { b, err := json.Marshal(v) if err != nil { return err @@ -2283,22 +3095,48 @@ func (t *SharedReqLogin_Creds) MergeSharedReqCredsAPIKey(v SharedReqCredsAPIKey) return err } -// AsSharedReqCredsUsername returns the union data inside the SharedReqLogin_Creds as a SharedReqCredsUsername -func (t SharedReqLogin_Creds) AsSharedReqCredsUsername() (SharedReqCredsUsername, error) { - var body SharedReqCredsUsername +// AsSharedReqCredsWebAuthn returns the union data inside the SharedReqLogin_Creds as a SharedReqCredsWebAuthn +func (t SharedReqLogin_Creds) AsSharedReqCredsWebAuthn() (SharedReqCredsWebAuthn, error) { + var body SharedReqCredsWebAuthn err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqCredsUsername overwrites any union data inside the SharedReqLogin_Creds as the provided SharedReqCredsUsername -func (t *SharedReqLogin_Creds) FromSharedReqCredsUsername(v SharedReqCredsUsername) error { +// FromSharedReqCredsWebAuthn overwrites any union data inside the SharedReqLogin_Creds as the provided SharedReqCredsWebAuthn +func (t *SharedReqLogin_Creds) FromSharedReqCredsWebAuthn(v SharedReqCredsWebAuthn) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqCredsUsername performs a merge with any union data inside the SharedReqLogin_Creds, using the provided SharedReqCredsUsername -func (t *SharedReqLogin_Creds) MergeSharedReqCredsUsername(v SharedReqCredsUsername) error { +// MergeSharedReqCredsWebAuthn performs a merge with any union data inside the SharedReqLogin_Creds, using the provided SharedReqCredsWebAuthn +func (t *SharedReqLogin_Creds) MergeSharedReqCredsWebAuthn(v SharedReqCredsWebAuthn) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(t.union, b) + t.union = merged + return err +} + +// AsSharedReqCredsNone returns the union data inside the SharedReqLogin_Creds as a SharedReqCredsNone +func (t SharedReqLogin_Creds) AsSharedReqCredsNone() (SharedReqCredsNone, error) { + var body SharedReqCredsNone + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromSharedReqCredsNone overwrites any union data inside the SharedReqLogin_Creds as the provided SharedReqCredsNone +func (t *SharedReqLogin_Creds) FromSharedReqCredsNone(v SharedReqCredsNone) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeSharedReqCredsNone performs a merge with any union data inside the SharedReqLogin_Creds, using the provided SharedReqCredsNone +func (t *SharedReqLogin_Creds) MergeSharedReqCredsNone(v SharedReqCredsNone) error { b, err := json.Marshal(v) if err != nil { return err @@ -2319,22 +3157,22 @@ func (t *SharedReqLogin_Creds) UnmarshalJSON(b []byte) error { return err } -// AsSharedReqParamsEmail returns the union data inside the SharedReqLogin_Params as a SharedReqParamsEmail -func (t SharedReqLogin_Params) AsSharedReqParamsEmail() (SharedReqParamsEmail, error) { - var body SharedReqParamsEmail +// AsSharedReqParamsOIDC returns the union data inside the SharedReqLogin_Params as a SharedReqParamsOIDC +func (t SharedReqLogin_Params) AsSharedReqParamsOIDC() (SharedReqParamsOIDC, error) { + var body SharedReqParamsOIDC err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqParamsEmail overwrites any union data inside the SharedReqLogin_Params as the provided SharedReqParamsEmail -func (t *SharedReqLogin_Params) FromSharedReqParamsEmail(v SharedReqParamsEmail) error { +// FromSharedReqParamsOIDC overwrites any union data inside the SharedReqLogin_Params as the provided SharedReqParamsOIDC +func (t *SharedReqLogin_Params) FromSharedReqParamsOIDC(v SharedReqParamsOIDC) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqParamsEmail performs a merge with any union data inside the SharedReqLogin_Params, using the provided SharedReqParamsEmail -func (t *SharedReqLogin_Params) MergeSharedReqParamsEmail(v SharedReqParamsEmail) error { +// MergeSharedReqParamsOIDC performs a merge with any union data inside the SharedReqLogin_Params, using the provided SharedReqParamsOIDC +func (t *SharedReqLogin_Params) MergeSharedReqParamsOIDC(v SharedReqParamsOIDC) error { b, err := json.Marshal(v) if err != nil { return err @@ -2345,22 +3183,22 @@ func (t *SharedReqLogin_Params) MergeSharedReqParamsEmail(v SharedReqParamsEmail return err } -// AsSharedReqParamsOIDC returns the union data inside the SharedReqLogin_Params as a SharedReqParamsOIDC -func (t SharedReqLogin_Params) AsSharedReqParamsOIDC() (SharedReqParamsOIDC, error) { - var body SharedReqParamsOIDC +// AsSharedReqParamsPassword returns the union data inside the SharedReqLogin_Params as a SharedReqParamsPassword +func (t SharedReqLogin_Params) AsSharedReqParamsPassword() (SharedReqParamsPassword, error) { + var body SharedReqParamsPassword err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqParamsOIDC overwrites any union data inside the SharedReqLogin_Params as the provided SharedReqParamsOIDC -func (t *SharedReqLogin_Params) FromSharedReqParamsOIDC(v SharedReqParamsOIDC) error { +// FromSharedReqParamsPassword overwrites any union data inside the SharedReqLogin_Params as the provided SharedReqParamsPassword +func (t *SharedReqLogin_Params) FromSharedReqParamsPassword(v SharedReqParamsPassword) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqParamsOIDC performs a merge with any union data inside the SharedReqLogin_Params, using the provided SharedReqParamsOIDC -func (t *SharedReqLogin_Params) MergeSharedReqParamsOIDC(v SharedReqParamsOIDC) error { +// MergeSharedReqParamsPassword performs a merge with any union data inside the SharedReqLogin_Params, using the provided SharedReqParamsPassword +func (t *SharedReqLogin_Params) MergeSharedReqParamsPassword(v SharedReqParamsPassword) error { b, err := json.Marshal(v) if err != nil { return err @@ -2371,22 +3209,22 @@ func (t *SharedReqLogin_Params) MergeSharedReqParamsOIDC(v SharedReqParamsOIDC) return err } -// AsSharedReqParamsNone returns the union data inside the SharedReqLogin_Params as a SharedReqParamsNone -func (t SharedReqLogin_Params) AsSharedReqParamsNone() (SharedReqParamsNone, error) { - var body SharedReqParamsNone +// AsSharedReqParamsWebAuthn returns the union data inside the SharedReqLogin_Params as a SharedReqParamsWebAuthn +func (t SharedReqLogin_Params) AsSharedReqParamsWebAuthn() (SharedReqParamsWebAuthn, error) { + var body SharedReqParamsWebAuthn err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqParamsNone overwrites any union data inside the SharedReqLogin_Params as the provided SharedReqParamsNone -func (t *SharedReqLogin_Params) FromSharedReqParamsNone(v SharedReqParamsNone) error { +// FromSharedReqParamsWebAuthn overwrites any union data inside the SharedReqLogin_Params as the provided SharedReqParamsWebAuthn +func (t *SharedReqLogin_Params) FromSharedReqParamsWebAuthn(v SharedReqParamsWebAuthn) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqParamsNone performs a merge with any union data inside the SharedReqLogin_Params, using the provided SharedReqParamsNone -func (t *SharedReqLogin_Params) MergeSharedReqParamsNone(v SharedReqParamsNone) error { +// MergeSharedReqParamsWebAuthn performs a merge with any union data inside the SharedReqLogin_Params, using the provided SharedReqParamsWebAuthn +func (t *SharedReqLogin_Params) MergeSharedReqParamsWebAuthn(v SharedReqParamsWebAuthn) error { b, err := json.Marshal(v) if err != nil { return err @@ -2397,22 +3235,22 @@ func (t *SharedReqLogin_Params) MergeSharedReqParamsNone(v SharedReqParamsNone) return err } -// AsSharedReqParamsUsername returns the union data inside the SharedReqLogin_Params as a SharedReqParamsUsername -func (t SharedReqLogin_Params) AsSharedReqParamsUsername() (SharedReqParamsUsername, error) { - var body SharedReqParamsUsername +// AsSharedReqParamsNone returns the union data inside the SharedReqLogin_Params as a SharedReqParamsNone +func (t SharedReqLogin_Params) AsSharedReqParamsNone() (SharedReqParamsNone, error) { + var body SharedReqParamsNone err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedReqParamsUsername overwrites any union data inside the SharedReqLogin_Params as the provided SharedReqParamsUsername -func (t *SharedReqLogin_Params) FromSharedReqParamsUsername(v SharedReqParamsUsername) error { +// FromSharedReqParamsNone overwrites any union data inside the SharedReqLogin_Params as the provided SharedReqParamsNone +func (t *SharedReqLogin_Params) FromSharedReqParamsNone(v SharedReqParamsNone) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedReqParamsUsername performs a merge with any union data inside the SharedReqLogin_Params, using the provided SharedReqParamsUsername -func (t *SharedReqLogin_Params) MergeSharedReqParamsUsername(v SharedReqParamsUsername) error { +// MergeSharedReqParamsNone performs a merge with any union data inside the SharedReqLogin_Params, using the provided SharedReqParamsNone +func (t *SharedReqLogin_Params) MergeSharedReqParamsNone(v SharedReqParamsNone) error { b, err := json.Marshal(v) if err != nil { return err @@ -2459,22 +3297,22 @@ func (t *SharedResLogin_Params) MergeSharedResParamsOIDC(v SharedResParamsOIDC) return err } -// AsSharedResParamsAPIKey returns the union data inside the SharedResLogin_Params as a SharedResParamsAPIKey -func (t SharedResLogin_Params) AsSharedResParamsAPIKey() (SharedResParamsAPIKey, error) { - var body SharedResParamsAPIKey +// AsSharedResParamsAnonymous returns the union data inside the SharedResLogin_Params as a SharedResParamsAnonymous +func (t SharedResLogin_Params) AsSharedResParamsAnonymous() (SharedResParamsAnonymous, error) { + var body SharedResParamsAnonymous err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedResParamsAPIKey overwrites any union data inside the SharedResLogin_Params as the provided SharedResParamsAPIKey -func (t *SharedResLogin_Params) FromSharedResParamsAPIKey(v SharedResParamsAPIKey) error { +// FromSharedResParamsAnonymous overwrites any union data inside the SharedResLogin_Params as the provided SharedResParamsAnonymous +func (t *SharedResLogin_Params) FromSharedResParamsAnonymous(v SharedResParamsAnonymous) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedResParamsAPIKey performs a merge with any union data inside the SharedResLogin_Params, using the provided SharedResParamsAPIKey -func (t *SharedResLogin_Params) MergeSharedResParamsAPIKey(v SharedResParamsAPIKey) error { +// MergeSharedResParamsAnonymous performs a merge with any union data inside the SharedResLogin_Params, using the provided SharedResParamsAnonymous +func (t *SharedResLogin_Params) MergeSharedResParamsAnonymous(v SharedResParamsAnonymous) error { b, err := json.Marshal(v) if err != nil { return err @@ -2547,22 +3385,22 @@ func (t *SharedResLoginMfa_Params) MergeSharedResParamsOIDC(v SharedResParamsOID return err } -// AsSharedResParamsAPIKey returns the union data inside the SharedResLoginMfa_Params as a SharedResParamsAPIKey -func (t SharedResLoginMfa_Params) AsSharedResParamsAPIKey() (SharedResParamsAPIKey, error) { - var body SharedResParamsAPIKey +// AsSharedResParamsAnonymous returns the union data inside the SharedResLoginMfa_Params as a SharedResParamsAnonymous +func (t SharedResLoginMfa_Params) AsSharedResParamsAnonymous() (SharedResParamsAnonymous, error) { + var body SharedResParamsAnonymous err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedResParamsAPIKey overwrites any union data inside the SharedResLoginMfa_Params as the provided SharedResParamsAPIKey -func (t *SharedResLoginMfa_Params) FromSharedResParamsAPIKey(v SharedResParamsAPIKey) error { +// FromSharedResParamsAnonymous overwrites any union data inside the SharedResLoginMfa_Params as the provided SharedResParamsAnonymous +func (t *SharedResLoginMfa_Params) FromSharedResParamsAnonymous(v SharedResParamsAnonymous) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedResParamsAPIKey performs a merge with any union data inside the SharedResLoginMfa_Params, using the provided SharedResParamsAPIKey -func (t *SharedResLoginMfa_Params) MergeSharedResParamsAPIKey(v SharedResParamsAPIKey) error { +// MergeSharedResParamsAnonymous performs a merge with any union data inside the SharedResLoginMfa_Params, using the provided SharedResParamsAnonymous +func (t *SharedResLoginMfa_Params) MergeSharedResParamsAnonymous(v SharedResParamsAnonymous) error { b, err := json.Marshal(v) if err != nil { return err @@ -2635,22 +3473,22 @@ func (t *SharedResRefresh_Params) MergeSharedResParamsOIDC(v SharedResParamsOIDC return err } -// AsSharedResParamsAPIKey returns the union data inside the SharedResRefresh_Params as a SharedResParamsAPIKey -func (t SharedResRefresh_Params) AsSharedResParamsAPIKey() (SharedResParamsAPIKey, error) { - var body SharedResParamsAPIKey +// AsSharedResParamsAnonymous returns the union data inside the SharedResRefresh_Params as a SharedResParamsAnonymous +func (t SharedResRefresh_Params) AsSharedResParamsAnonymous() (SharedResParamsAnonymous, error) { + var body SharedResParamsAnonymous err := json.Unmarshal(t.union, &body) return body, err } -// FromSharedResParamsAPIKey overwrites any union data inside the SharedResRefresh_Params as the provided SharedResParamsAPIKey -func (t *SharedResRefresh_Params) FromSharedResParamsAPIKey(v SharedResParamsAPIKey) error { +// FromSharedResParamsAnonymous overwrites any union data inside the SharedResRefresh_Params as the provided SharedResParamsAnonymous +func (t *SharedResRefresh_Params) FromSharedResParamsAnonymous(v SharedResParamsAnonymous) error { b, err := json.Marshal(v) t.union = b return err } -// MergeSharedResParamsAPIKey performs a merge with any union data inside the SharedResRefresh_Params, using the provided SharedResParamsAPIKey -func (t *SharedResRefresh_Params) MergeSharedResParamsAPIKey(v SharedResParamsAPIKey) error { +// MergeSharedResParamsAnonymous performs a merge with any union data inside the SharedResRefresh_Params, using the provided SharedResParamsAnonymous +func (t *SharedResRefresh_Params) MergeSharedResParamsAnonymous(v SharedResParamsAnonymous) error { b, err := json.Marshal(v) if err != nil { return err diff --git a/driver/web/docs/index.yaml b/driver/web/docs/index.yaml index 161129a5b..946de8ca6 100644 --- a/driver/web/docs/index.yaml +++ b/driver/web/docs/index.yaml @@ -39,10 +39,10 @@ paths: $ref: "./resources/services/auth/login-url.yaml" /services/auth/logout: $ref: "./resources/services/auth/logout.yaml" - /services/auth/credential/verify: - $ref: "./resources/services/auth/credential/verify.yaml" - /services/auth/credential/send-verify: - $ref: "./resources/services/auth/credential/send-verify.yaml" + /services/auth/identifier/verify: + $ref: "./resources/services/auth/identifier/verify.yaml" + /services/auth/identifier/send-verify: + $ref: "./resources/services/auth/identifier/send-verify.yaml" /services/auth/credential/forgot/initiate: $ref: "./resources/services/auth/credential/forgot/initiate.yaml" /services/auth/credential/forgot/complete: @@ -57,6 +57,10 @@ paths: $ref: "./resources/services/auth/account/can-sign-in.yaml" /services/auth/account/can-link: $ref: "./resources/services/auth/account/can-link.yaml" + /services/auth/account/sign-in-options: + $ref: "./resources/services/auth/account/sign-in-options.yaml" + /services/auth/account/identifier/link: + $ref: "./resources/services/auth/account/identifier/link.yaml" /services/auth/account/auth-type/link: $ref: "./resources/services/auth/account/auth-type/link.yaml" /services/auth/authorize-service: @@ -91,6 +95,10 @@ paths: $ref: "./resources/services/app-configs/configs.yaml" /services/app-configs/organization: $ref: "./resources/services/app-configs/organization/configs.yaml" + + # DEPRECATED + /services/auth/credential/send-verify: + $ref: "./resources/services/auth/credential/send-verify.yaml" /services/application/configs: $ref: "./resources/services/application/configs.yaml" /services/application/organization/configs: @@ -235,8 +243,8 @@ paths: #ui /ui/credential/reset: $ref: "./resources/ui/credential/reset.yaml" - /ui/credential/verify: - $ref: "./resources/ui/credential/verify.yaml" + /ui/identifier/verify: + $ref: "./resources/ui/identifier/verify.yaml" #default /version: diff --git a/driver/web/docs/resources/services/auth/account/auth-type/link.yaml b/driver/web/docs/resources/services/auth/account/auth-type/link.yaml index 89b6ed52a..f8bb2294f 100644 --- a/driver/web/docs/resources/services/auth/account/auth-type/link.yaml +++ b/driver/web/docs/resources/services/auth/account/auth-type/link.yaml @@ -14,34 +14,43 @@ post: schema: $ref: "../../../../../schemas/apis/services/account/auth-type/link/request/Link.yaml" examples: - email-sign_up: - summary: Email + password: + summary: Password value: - auth_type: email + auth_type: password app_type_identifier: edu.illinois.rokwire - org_id: 0a2eff20-e2cd-11eb-af68-60f81db5ecc0 - api_key: 95a463e3-2ce8-450b-ba75-d8506b874738 creds: - email: test@example.com password: test12345 params: confirm_password: test12345 - phone: - summary: Phone + code: + summary: Code value: - auth_type: twilio_phone + auth_type: code app_type_identifier: edu.illinois.rokwire - org_id: 0a2eff20-e2cd-11eb-af68-60f81db5ecc0 - api_key: 95a463e3-2ce8-450b-ba75-d8506b874738 creds: phone: "+12223334444" + webauthn-begin_registration: + summary: Webauthn begin registration + value: + auth_type: webauthn + app_type_identifier: edu.illinois.rokwire + params: + display_name: Name + webauthn-complete_registration: + summary: Webauthn complete registration + value: + auth_type: webauthn + app_type_identifier: edu.illinois.rokwire + creds: + response: + params: + display_name: Name illinois_oidc: summary: Illinois OIDC value: auth_type: illinois_oidc app_type_identifier: edu.illinois.rokwire - org_id: 0a2eff20-e2cd-11eb-af68-60f81db5ecc0 - api_key: 95a463e3-2ce8-450b-ba75-d8506b874738 creds: https://redirect.example.com?code=ai324uith8gSEefesEguorgwsf43 params: redirect_uri: https://redirect.example.com @@ -73,13 +82,15 @@ post: - verification-expired - already-exists - not-found + - not-allowed - internal-server-error description: | - `invalid`: Invalid credentials - `unverified`: Unverified credentials - - `verification-expired`: Credentials verification expired. The verification is restarted - - `already-exists`: Auth type identifier already exists + - `verification-expired`: Identifier verification expired. The verification is restarted + - `already-exists`: Auth type already exists - `not-found`: Account could not be found when `sign-up=false` + - `not-allowed`: Invalid operation - `internal-server-error`: An undefined error occurred message: type: string @@ -98,25 +109,8 @@ delete: application/json: schema: $ref: "../../../../../schemas/apis/services/account/auth-type/link/request/Unlink.yaml" - examples: - email: - summary: Email - value: - auth_type: email - app_type_identifier: edu.illinois.rokwire - identifier: test@example.com - phone: - summary: Phone - value: - auth_type: twilio_phone - app_type_identifier: edu.illinois.rokwire - identifier: "+12223334444" - illinois_oidc: - summary: Illinois OIDC - value: - auth_type: illinois_oidc - app_type_identifier: edu.illinois.rokwire - identifier: "123456789" + example: + id: required: true responses: 200: diff --git a/driver/web/docs/resources/services/auth/account/identifier/link.yaml b/driver/web/docs/resources/services/auth/account/identifier/link.yaml new file mode 100644 index 000000000..9d86222fe --- /dev/null +++ b/driver/web/docs/resources/services/auth/account/identifier/link.yaml @@ -0,0 +1,101 @@ +post: + tags: + - Services + summary: Link identifier + description: | + Link identifier to an existing account + + **Auth:** Requires "authenticated" auth token + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: "../../../../../schemas/apis/services/account/identifier/link/request/Link.yaml" + examples: + email: + summary: Email + value: + identifier: + email: test@example.com + phone: + summary: Phone + value: + identifier: + phone: "+12223334444" + username: + summary: Username + value: + identifier: + username: username + required: true + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "../../../../../schemas/apis/services/account/identifier/link/response/Response.yaml" + 400: + description: Bad request + 401: + description: Unauthorized + 500: + description: Internal error + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - invalid + - unverified + - verification-expired + - already-exists + - not-found + - not-allowed + - internal-server-error + description: | + - `invalid`: Invalid identifier + - `unverified`: Unverified identifier + - `verification-expired`: Identifier verification expired. The verification is restarted + - `already-exists`: Auth type identifier already exists + - `not-found`: Account could not be found when `sign-up=false` + - `not-allowed`: Invalid operation + - `internal-server-error`: An undefined error occurred + message: + type: string +delete: + tags: + - Services + summary: Unlink identifier + description: | + Unlink identifier from an existing account + + **Auth:** Requires "authenticated" auth token + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: "../../../../../schemas/apis/services/account/identifier/link/request/Unlink.yaml" + example: + id: + required: true + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "../../../../../schemas/apis/services/account/identifier/link/response/Response.yaml" + 400: + description: Bad request + 401: + description: Unauthorized + 500: + description: Internal error \ No newline at end of file diff --git a/driver/web/docs/resources/services/auth/account/sign-in-options.yaml b/driver/web/docs/resources/services/auth/account/sign-in-options.yaml new file mode 100644 index 000000000..4b8f6d390 --- /dev/null +++ b/driver/web/docs/resources/services/auth/account/sign-in-options.yaml @@ -0,0 +1,27 @@ +post: + tags: + - Services + summary: Get account sign-in options + description: | + Get the sign-in options for the account with the provided parameters + requestBody: + description: | + Account information to be checked + content: + application/json: + schema: + $ref: "../../../../schemas/apis/shared/requests/AccountCheck.yaml" + required: true + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "../../../../schemas/apis/shared/responses/SignInOptions.yaml" + 400: + description: Bad request + 401: + description: Unauthorized + 500: + description: Internal error \ No newline at end of file diff --git a/driver/web/docs/resources/services/auth/credential/send-verify.yaml b/driver/web/docs/resources/services/auth/credential/send-verify.yaml index b66d223b1..b4ff76da3 100644 --- a/driver/web/docs/resources/services/auth/credential/send-verify.yaml +++ b/driver/web/docs/resources/services/auth/credential/send-verify.yaml @@ -4,13 +4,14 @@ post: summary: Send verification code to identifier description: | Sends verification code to identifier to verify account ownership + deprecated: true requestBody: description: | Account information to be checked content: application/json: schema: - $ref: "../../../../schemas/apis/services/credential/send-verify/request/Request.yaml" + $ref: "../../../../schemas/apis/services/identifier/send-verify/request/Request.yaml" required: true responses: 200: diff --git a/driver/web/docs/resources/services/auth/identifier/send-verify.yaml b/driver/web/docs/resources/services/auth/identifier/send-verify.yaml new file mode 100644 index 000000000..d5196cd1d --- /dev/null +++ b/driver/web/docs/resources/services/auth/identifier/send-verify.yaml @@ -0,0 +1,28 @@ +post: + tags: + - Services + summary: Send verification code to identifier + description: | + Sends verification code to identifier to verify account ownership + requestBody: + description: | + Account information to be checked + content: + application/json: + schema: + $ref: "../../../../schemas/apis/services/identifier/send-verify/request/Request.yaml" + required: true + responses: + 200: + description: Successful operation + content: + text/plain: + schema: + type: string + example: Successfully sent verification code + 400: + description: Bad request + 401: + description: Unauthorized + 500: + description: Internal error diff --git a/driver/web/docs/resources/services/auth/credential/verify.yaml b/driver/web/docs/resources/services/auth/identifier/verify.yaml similarity index 86% rename from driver/web/docs/resources/services/auth/credential/verify.yaml rename to driver/web/docs/resources/services/auth/identifier/verify.yaml index 5e8c57ae8..e983059de 100644 --- a/driver/web/docs/resources/services/auth/credential/verify.yaml +++ b/driver/web/docs/resources/services/auth/identifier/verify.yaml @@ -3,11 +3,11 @@ get: - Services summary: Validate verification code description: | - Validates verification code to verify account ownership + Validates verification code to verify account identifier ownership parameters: - name: id in: query - description: Credential ID + description: Account identifier ID required: true style: form explode: false diff --git a/driver/web/docs/resources/services/auth/login.yaml b/driver/web/docs/resources/services/auth/login.yaml index 89ac0551a..e3fcb1249 100644 --- a/driver/web/docs/resources/services/auth/login.yaml +++ b/driver/web/docs/resources/services/auth/login.yaml @@ -180,6 +180,35 @@ post: type: mobile device_id: "5555" os: Android + webauthn-sign_up: + summary: WebAuthn - sign up + value: + auth_type: webauthn + app_type_identifier: edu.illinois.rokwire + org_id: 0a2eff20-e2cd-11eb-af68-60f81db5ecc0 + api_key: 95a463e3-2ce8-450b-ba75-d8506b874738 + params: + sign_up: true + name: test + display_name: John Doe + preferences: + key1: value1 + key2: value2 + profile: + address: address + birth_year: 1990 + country: county + email: email + first_name: first name + last_name: last name + phone: "+000000000000" + photo_url: photo url + state: state + zip_code: zip code + device: + type: mobile + device_id: "5555" + os: Android required: true responses: 200: diff --git a/driver/web/docs/resources/ui/credential/verify.yaml b/driver/web/docs/resources/ui/identifier/verify.yaml similarity index 95% rename from driver/web/docs/resources/ui/credential/verify.yaml rename to driver/web/docs/resources/ui/identifier/verify.yaml index f1ed9369b..555739309 100644 --- a/driver/web/docs/resources/ui/credential/verify.yaml +++ b/driver/web/docs/resources/ui/identifier/verify.yaml @@ -7,7 +7,7 @@ get: parameters: - name: id in: query - description: Credential ID + description: Identifier ID required: true style: form explode: false diff --git a/driver/web/docs/schemas/apis/admin/configs/request/Request.yaml b/driver/web/docs/schemas/apis/admin/configs/request/Request.yaml index b356a62f5..d66f4c2f1 100644 --- a/driver/web/docs/schemas/apis/admin/configs/request/Request.yaml +++ b/driver/web/docs/schemas/apis/admin/configs/request/Request.yaml @@ -16,4 +16,5 @@ properties: type: boolean data: anyOf: - - $ref: "../../../../config/EnvConfigData.yaml" \ No newline at end of file + - $ref: "../../../../config/EnvConfigData.yaml" + - $ref: "../../../../config/AuthConfigData.yaml" \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/services/account/auth-type/link/request/Link.yaml b/driver/web/docs/schemas/apis/services/account/auth-type/link/request/Link.yaml index 5f5223022..9acd94d09 100644 --- a/driver/web/docs/schemas/apis/services/account/auth-type/link/request/Link.yaml +++ b/driver/web/docs/schemas/apis/services/account/auth-type/link/request/Link.yaml @@ -1,26 +1,33 @@ required: - auth_type - app_type_identifier - - creds type: object properties: auth_type: type: string enum: + - password + - webauthn + - code + - illinois_oidc + - conde_oidc - email + - phone - twilio_phone - - illinois_oidc - username app_type_identifier: type: string creds: anyOf: - - $ref: "../../../../../shared/requests/CredsEmail.yaml" - - $ref: "../../../../../shared/requests/CredsTwilioPhone.yaml" + - $ref: "../../../../../shared/requests/CredsCode.yaml" - $ref: "../../../../../shared/requests/CredsOIDC.yaml" + - $ref: "../../../../../shared/requests/CredsPassword.yaml" + - $ref: "../../../../../shared/requests/CredsWebAuthn.yaml" + - $ref: "../../../../../shared/requests/CredsNone.yaml" params: type: object anyOf: - - $ref: "../../../../../shared/requests/ParamsEmail.yaml" - $ref: "../../../../../shared/requests/ParamsOIDC.yaml" + - $ref: "../../../../../shared/requests/ParamsPassword.yaml" + - $ref: "../../../../../shared/requests/ParamsWebAuthn.yaml" - $ref: "../../../../../shared/requests/ParamsNone.yaml" \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/services/account/auth-type/link/request/Unlink.yaml b/driver/web/docs/schemas/apis/services/account/auth-type/link/request/Unlink.yaml index bf5d3f672..285d4307f 100644 --- a/driver/web/docs/schemas/apis/services/account/auth-type/link/request/Unlink.yaml +++ b/driver/web/docs/schemas/apis/services/account/auth-type/link/request/Unlink.yaml @@ -1,17 +1,18 @@ -required: - - auth_type - - app_type_identifier - - identifier type: object properties: + id: + type: string auth_type: type: string enum: + - password + - webauthn + - code + - illinois_oidc + - conde_oidc - email + - phone - twilio_phone - - illinois_oidc - username - app_type_identifier: - type: string identifier: type: string \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/services/account/auth-type/link/response/Response.yaml b/driver/web/docs/schemas/apis/services/account/auth-type/link/response/Response.yaml index 15da85bde..780a36278 100644 --- a/driver/web/docs/schemas/apis/services/account/auth-type/link/response/Response.yaml +++ b/driver/web/docs/schemas/apis/services/account/auth-type/link/response/Response.yaml @@ -5,6 +5,10 @@ properties: message: type: string nullable: true + identifiers: + type: array + items: + $ref: "../../../../../../user/AccountIdentifier.yaml" auth_types: type: array items: diff --git a/driver/web/docs/schemas/apis/services/account/identifier/link/request/Link.yaml b/driver/web/docs/schemas/apis/services/account/identifier/link/request/Link.yaml new file mode 100644 index 000000000..20f23b97c --- /dev/null +++ b/driver/web/docs/schemas/apis/services/account/identifier/link/request/Link.yaml @@ -0,0 +1,6 @@ +required: + - identifier +type: object +properties: + identifier: + $ref: "../../../../../shared/requests/Identifiers.yaml" \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/services/account/identifier/link/request/Unlink.yaml b/driver/web/docs/schemas/apis/services/account/identifier/link/request/Unlink.yaml new file mode 100644 index 000000000..cf30202d3 --- /dev/null +++ b/driver/web/docs/schemas/apis/services/account/identifier/link/request/Unlink.yaml @@ -0,0 +1,6 @@ +required: + - id +type: object +properties: + id: + type: string \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/services/account/identifier/link/response/Response.yaml b/driver/web/docs/schemas/apis/services/account/identifier/link/response/Response.yaml new file mode 100644 index 000000000..e6028878d --- /dev/null +++ b/driver/web/docs/schemas/apis/services/account/identifier/link/response/Response.yaml @@ -0,0 +1,11 @@ +required: + - identifiers +type: object +properties: + message: + type: string + nullable: true + identifiers: + type: array + items: + $ref: "../../../../../../user/AccountIdentifier.yaml" \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/services/credential/forgot/complete/request/Request.yaml b/driver/web/docs/schemas/apis/services/credential/forgot/complete/request/Request.yaml index 6adb19c92..58abfef70 100644 --- a/driver/web/docs/schemas/apis/services/credential/forgot/complete/request/Request.yaml +++ b/driver/web/docs/schemas/apis/services/credential/forgot/complete/request/Request.yaml @@ -10,4 +10,4 @@ properties: params: type: object anyOf: - - $ref: "../../../../../shared/requests/ParamsSetEmailCredential.yaml" \ No newline at end of file + - $ref: "../../../../../shared/requests/ParamsResetPassword.yaml" \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/services/credential/forgot/initiate/request/Request.yaml b/driver/web/docs/schemas/apis/services/credential/forgot/initiate/request/Request.yaml index d91f8df2e..d50c15154 100644 --- a/driver/web/docs/schemas/apis/services/credential/forgot/initiate/request/Request.yaml +++ b/driver/web/docs/schemas/apis/services/credential/forgot/initiate/request/Request.yaml @@ -1,20 +1,23 @@ required: - auth_type + - identifier - app_type_identifier - org_id - api_key - - identifier type: object properties: auth_type: type: string enum: + - password - email + identifier: + oneOf: + - $ref: "../../../../../shared/requests/Identifiers.yaml" + - $ref: "../../../../../shared/requests/IdentifierString.yaml" app_type_identifier: type: string org_id: type: string api_key: - type: string - identifier: type: string \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/services/credential/update/request/Request.yaml b/driver/web/docs/schemas/apis/services/credential/update/request/Request.yaml index 3fa77dc87..4bd3f17fe 100644 --- a/driver/web/docs/schemas/apis/services/credential/update/request/Request.yaml +++ b/driver/web/docs/schemas/apis/services/credential/update/request/Request.yaml @@ -7,4 +7,4 @@ properties: params: type: object anyOf: - - $ref: "../../../../shared/requests/ParamsSetEmailCredential.yaml" \ No newline at end of file + - $ref: "../../../../shared/requests/ParamsResetPassword.yaml" \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/services/credential/send-verify/request/Request.yaml b/driver/web/docs/schemas/apis/services/identifier/send-verify/request/Request.yaml similarity index 65% rename from driver/web/docs/schemas/apis/services/credential/send-verify/request/Request.yaml rename to driver/web/docs/schemas/apis/services/identifier/send-verify/request/Request.yaml index 11e76f7c5..3b55b2c6b 100644 --- a/driver/web/docs/schemas/apis/services/credential/send-verify/request/Request.yaml +++ b/driver/web/docs/schemas/apis/services/identifier/send-verify/request/Request.yaml @@ -1,5 +1,4 @@ required: - - auth_type - app_type_identifier - org_id - api_key @@ -7,7 +6,9 @@ required: type: object properties: identifier: - type: string + oneOf: + - $ref: "../../../../shared/requests/Identifiers.yaml" + - $ref: "../../../../shared/requests/IdentifierString.yaml" org_id: type: string api_key: diff --git a/driver/web/docs/schemas/apis/shared/requests/AccountCheck.yaml b/driver/web/docs/schemas/apis/shared/requests/AccountCheck.yaml index 7e6f15c8d..0c9ceb024 100644 --- a/driver/web/docs/schemas/apis/shared/requests/AccountCheck.yaml +++ b/driver/web/docs/schemas/apis/shared/requests/AccountCheck.yaml @@ -1,24 +1,29 @@ required: - - auth_type - app_type_identifier - org_id - api_key - - user_identifier type: object properties: + app_type_identifier: + type: string + org_id: + type: string + api_key: + type: string + identifier: + $ref: "./Identifiers.yaml" auth_type: type: string enum: - username - email + - phone + - anonymous - twilio_phone - illinois_oidc - - anonymous - app_type_identifier: - type: string - org_id: - type: string - api_key: - type: string + - conde_oidc + deprecated: true user_identifier: - type: string \ No newline at end of file + type: string + deprecated: true + \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/CredsAPIKey.yaml b/driver/web/docs/schemas/apis/shared/requests/CredsAnonymous.yaml similarity index 100% rename from driver/web/docs/schemas/apis/shared/requests/CredsAPIKey.yaml rename to driver/web/docs/schemas/apis/shared/requests/CredsAnonymous.yaml diff --git a/driver/web/docs/schemas/apis/shared/requests/CredsCode.yaml b/driver/web/docs/schemas/apis/shared/requests/CredsCode.yaml new file mode 100644 index 000000000..b66b9f5ed --- /dev/null +++ b/driver/web/docs/schemas/apis/shared/requests/CredsCode.yaml @@ -0,0 +1,7 @@ +type: object +description: Auth login creds for auth_type="code" +allOf: + - $ref: "./Identifiers.yaml" + - properties: + code: + type: string \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/CredsEmail.yaml b/driver/web/docs/schemas/apis/shared/requests/CredsEmail.yaml deleted file mode 100644 index a33facbd3..000000000 --- a/driver/web/docs/schemas/apis/shared/requests/CredsEmail.yaml +++ /dev/null @@ -1,10 +0,0 @@ -required: - - email - - password -type: object -description: Auth login creds for auth_type="email" -properties: - email: - type: string - password: - type: string \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/CredsNone.yaml b/driver/web/docs/schemas/apis/shared/requests/CredsNone.yaml new file mode 100644 index 000000000..2ceb741a6 --- /dev/null +++ b/driver/web/docs/schemas/apis/shared/requests/CredsNone.yaml @@ -0,0 +1,3 @@ +type: object +description: Auth login request creds for unlisted auth_types (None) +nullable: true \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/CredsPassword.yaml b/driver/web/docs/schemas/apis/shared/requests/CredsPassword.yaml new file mode 100644 index 000000000..edd5b7f2f --- /dev/null +++ b/driver/web/docs/schemas/apis/shared/requests/CredsPassword.yaml @@ -0,0 +1,9 @@ +type: object +description: Auth login creds for auth_type="password" +allOf: + - $ref: "./Identifiers.yaml" + - required: + - password + properties: + password: + type: string \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/CredsTwilioPhone.yaml b/driver/web/docs/schemas/apis/shared/requests/CredsTwilioPhone.yaml deleted file mode 100644 index 091da0bb7..000000000 --- a/driver/web/docs/schemas/apis/shared/requests/CredsTwilioPhone.yaml +++ /dev/null @@ -1,9 +0,0 @@ -type: object -description: Auth login creds for auth_type="twilio_phone" -required: - - phone -properties: - phone: - type: string - code: - type: string \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/CredsUsername.yaml b/driver/web/docs/schemas/apis/shared/requests/CredsUsername.yaml deleted file mode 100644 index 8188a37e4..000000000 --- a/driver/web/docs/schemas/apis/shared/requests/CredsUsername.yaml +++ /dev/null @@ -1,10 +0,0 @@ -required: - - username - - password -type: object -description: Auth login creds for auth_type="username" -properties: - username: - type: string - password: - type: string \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/CredsWebAuthn.yaml b/driver/web/docs/schemas/apis/shared/requests/CredsWebAuthn.yaml new file mode 100644 index 000000000..9a66d8ac6 --- /dev/null +++ b/driver/web/docs/schemas/apis/shared/requests/CredsWebAuthn.yaml @@ -0,0 +1,7 @@ +type: object +description: Auth login creds for auth_type="webauthn" +allOf: + - $ref: "./Identifiers.yaml" + - properties: + response: + type: string \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/IdentifierString.yaml b/driver/web/docs/schemas/apis/shared/requests/IdentifierString.yaml new file mode 100644 index 000000000..23d3801f8 --- /dev/null +++ b/driver/web/docs/schemas/apis/shared/requests/IdentifierString.yaml @@ -0,0 +1,2 @@ +type: string +description: User identifier string \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/Identifiers.yaml b/driver/web/docs/schemas/apis/shared/requests/Identifiers.yaml new file mode 100644 index 000000000..65f7f322d --- /dev/null +++ b/driver/web/docs/schemas/apis/shared/requests/Identifiers.yaml @@ -0,0 +1,11 @@ +type: object +description: Allowed identifier types +properties: + username: + type: string + email: + type: string + phone: + type: string +additionalProperties: + type: string \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/ParamsEmail.yaml b/driver/web/docs/schemas/apis/shared/requests/ParamsPassword.yaml similarity index 100% rename from driver/web/docs/schemas/apis/shared/requests/ParamsEmail.yaml rename to driver/web/docs/schemas/apis/shared/requests/ParamsPassword.yaml diff --git a/driver/web/docs/schemas/apis/shared/requests/ParamsSetEmailCredential.yaml b/driver/web/docs/schemas/apis/shared/requests/ParamsResetPassword.yaml similarity index 100% rename from driver/web/docs/schemas/apis/shared/requests/ParamsSetEmailCredential.yaml rename to driver/web/docs/schemas/apis/shared/requests/ParamsResetPassword.yaml diff --git a/driver/web/docs/schemas/apis/shared/requests/ParamsUsername.yaml b/driver/web/docs/schemas/apis/shared/requests/ParamsUsername.yaml deleted file mode 100644 index 9f070b9b4..000000000 --- a/driver/web/docs/schemas/apis/shared/requests/ParamsUsername.yaml +++ /dev/null @@ -1,8 +0,0 @@ -type: object -description: Auth login params for auth_type="username" -properties: - confirm_password: - type: string - description: This should match the `creds` password field when sign_up=true. This should be verified on the client side as well to reduce invalid requests. - sign_up: - type: boolean \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/ParamsWebAuthn.yaml b/driver/web/docs/schemas/apis/shared/requests/ParamsWebAuthn.yaml new file mode 100644 index 000000000..00de65a04 --- /dev/null +++ b/driver/web/docs/schemas/apis/shared/requests/ParamsWebAuthn.yaml @@ -0,0 +1,8 @@ +type: object +description: Auth login params for auth_type="webauthn" +properties: + display_name: + type: string + description: User's account name for display purposes + sign_up: + type: boolean \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/create-account/Request.yaml b/driver/web/docs/schemas/apis/shared/requests/create-account/Request.yaml index 81025edbb..113ec3b00 100644 --- a/driver/web/docs/schemas/apis/shared/requests/create-account/Request.yaml +++ b/driver/web/docs/schemas/apis/shared/requests/create-account/Request.yaml @@ -6,10 +6,10 @@ properties: auth_type: type: string enum: - - email + - password - illinois_oidc identifier: - type: string + $ref: "../Identifiers.yaml" permissions: type: array items: @@ -29,7 +29,4 @@ properties: profile: $ref: "../../../../user/ProfileNullable.yaml" privacy: - $ref: "../../../../user/PrivacyNullable.yaml" - username: - type: string - nullable: true \ No newline at end of file + $ref: "../../../../user/PrivacyNullable.yaml" \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/login-url/Request.yaml b/driver/web/docs/schemas/apis/shared/requests/login-url/Request.yaml index 3c2c5a819..760f9959e 100644 --- a/driver/web/docs/schemas/apis/shared/requests/login-url/Request.yaml +++ b/driver/web/docs/schemas/apis/shared/requests/login-url/Request.yaml @@ -10,6 +10,7 @@ properties: type: string enum: - illinois_oidc + - conde_oidc app_type_identifier: type: string org_id: diff --git a/driver/web/docs/schemas/apis/shared/requests/login/Request.yaml b/driver/web/docs/schemas/apis/shared/requests/login/Request.yaml index ff2dc606e..634d8816f 100644 --- a/driver/web/docs/schemas/apis/shared/requests/login/Request.yaml +++ b/driver/web/docs/schemas/apis/shared/requests/login/Request.yaml @@ -10,10 +10,15 @@ properties: type: string enum: - email + - phone - twilio_phone - illinois_oidc + - conde_oidc - anonymous - username + - password + - webauthn + - code app_type_identifier: type: string org_id: @@ -22,18 +27,19 @@ properties: type: string creds: anyOf: - - $ref: "../CredsEmail.yaml" - - $ref: "../CredsTwilioPhone.yaml" + - $ref: "../CredsAnonymous.yaml" + - $ref: "../CredsCode.yaml" - $ref: "../CredsOIDC.yaml" - - $ref: "../CredsAPIKey.yaml" - - $ref: "../CredsUsername.yaml" + - $ref: "../CredsPassword.yaml" + - $ref: "../CredsWebAuthn.yaml" + - $ref: "../CredsNone.yaml" params: type: object anyOf: - - $ref: "../ParamsEmail.yaml" - $ref: "../ParamsOIDC.yaml" + - $ref: "../ParamsPassword.yaml" + - $ref: "../ParamsWebAuthn.yaml" - $ref: "../ParamsNone.yaml" - - $ref: "../ParamsUsername.yaml" device: $ref: "../../../../user/Device.yaml" profile: @@ -43,6 +49,6 @@ properties: preferences: type: object nullable: true - username: + account_identifier_id: type: string - nullable: true + nullable: true \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/requests/update-account/Request.yaml b/driver/web/docs/schemas/apis/shared/requests/update-account/Request.yaml index 746bdf38f..3e7f15bbf 100644 --- a/driver/web/docs/schemas/apis/shared/requests/update-account/Request.yaml +++ b/driver/web/docs/schemas/apis/shared/requests/update-account/Request.yaml @@ -6,10 +6,10 @@ properties: auth_type: type: string enum: - - email + - password - illinois_oidc identifier: - type: string + $ref: "../Identifiers.yaml" permissions: type: array items: diff --git a/driver/web/docs/schemas/apis/shared/responses/ParamsAPIKey.yaml b/driver/web/docs/schemas/apis/shared/responses/ParamsAnonymous.yaml similarity index 100% rename from driver/web/docs/schemas/apis/shared/responses/ParamsAPIKey.yaml rename to driver/web/docs/schemas/apis/shared/responses/ParamsAnonymous.yaml diff --git a/driver/web/docs/schemas/apis/shared/responses/SignInOptions.yaml b/driver/web/docs/schemas/apis/shared/responses/SignInOptions.yaml new file mode 100644 index 000000000..e333e7c01 --- /dev/null +++ b/driver/web/docs/schemas/apis/shared/responses/SignInOptions.yaml @@ -0,0 +1,13 @@ +required: + - identifiers + - auth_types +type: object +properties: + identifiers: + type: array + items: + $ref: "../../../user/AccountIdentifier.yaml" + auth_types: + type: array + items: + $ref: "../../../user/AccountAuthType.yaml" diff --git a/driver/web/docs/schemas/apis/shared/responses/login/MfaResponse.yaml b/driver/web/docs/schemas/apis/shared/responses/login/MfaResponse.yaml index 1f154e255..0e5928c64 100644 --- a/driver/web/docs/schemas/apis/shared/responses/login/MfaResponse.yaml +++ b/driver/web/docs/schemas/apis/shared/responses/login/MfaResponse.yaml @@ -20,5 +20,5 @@ properties: nullable: true anyOf: - $ref: "../ParamsOIDC.yaml" - - $ref: "../ParamsAPIKey.yaml" + - $ref: "../ParamsAnonymous.yaml" - $ref: "../ParamsNone.yaml" \ No newline at end of file diff --git a/driver/web/docs/schemas/apis/shared/responses/login/Response.yaml b/driver/web/docs/schemas/apis/shared/responses/login/Response.yaml index f2e9189be..3823b635e 100644 --- a/driver/web/docs/schemas/apis/shared/responses/login/Response.yaml +++ b/driver/web/docs/schemas/apis/shared/responses/login/Response.yaml @@ -9,7 +9,7 @@ properties: nullable: true anyOf: - $ref: "../ParamsOIDC.yaml" - - $ref: "../ParamsAPIKey.yaml" + - $ref: "../ParamsAnonymous.yaml" - $ref: "../ParamsNone.yaml" message: type: string diff --git a/driver/web/docs/schemas/apis/shared/responses/refresh/Response.yaml b/driver/web/docs/schemas/apis/shared/responses/refresh/Response.yaml index ce27aaf49..39bdb9871 100644 --- a/driver/web/docs/schemas/apis/shared/responses/refresh/Response.yaml +++ b/driver/web/docs/schemas/apis/shared/responses/refresh/Response.yaml @@ -7,5 +7,5 @@ properties: nullable: true anyOf: - $ref: "../ParamsOIDC.yaml" - - $ref: "../ParamsAPIKey.yaml" + - $ref: "../ParamsAnonymous.yaml" - $ref: "../ParamsNone.yaml" diff --git a/driver/web/docs/schemas/application/IdentityProviderSettings.yaml b/driver/web/docs/schemas/application/IdentityProviderSettings.yaml index 1943b5a47..e1f404d8d 100644 --- a/driver/web/docs/schemas/application/IdentityProviderSettings.yaml +++ b/driver/web/docs/schemas/application/IdentityProviderSettings.yaml @@ -7,11 +7,18 @@ properties: type: string user_identifier_field: type: string - external_id_fields: # map + external_id_fields: type: object additionalProperties: type: string nullable: true + sensitive_external_ids: + type: array + items: + type: string + nullable: true + is_email_verified: + type: boolean first_name_field: type: string middle_name_field: diff --git a/driver/web/docs/schemas/auth/AuthType.yaml b/driver/web/docs/schemas/auth/AuthType.yaml index b20f18c51..bc0ea8112 100644 --- a/driver/web/docs/schemas/auth/AuthType.yaml +++ b/driver/web/docs/schemas/auth/AuthType.yaml @@ -13,7 +13,7 @@ properties: type: string code: type: string - description: "username or email or phone or illinois_oidc etc" + description: "passowrd or code or webauthn or illinois_oidc etc" description: type: string is_external: diff --git a/driver/web/docs/schemas/auth/LoginSession.yaml b/driver/web/docs/schemas/auth/LoginSession.yaml index 980b86aa2..b8def46c5 100644 --- a/driver/web/docs/schemas/auth/LoginSession.yaml +++ b/driver/web/docs/schemas/auth/LoginSession.yaml @@ -14,10 +14,6 @@ properties: type: string app_type_identifier: type: string - account_auth_type_id: - type: string - account_auth_type_identifier: - type: string device_id: type: string nullable: true diff --git a/driver/web/docs/schemas/config/AuthConfigData.yaml b/driver/web/docs/schemas/config/AuthConfigData.yaml new file mode 100644 index 000000000..fc746a8b5 --- /dev/null +++ b/driver/web/docs/schemas/config/AuthConfigData.yaml @@ -0,0 +1,11 @@ +type: object +properties: + email_should_verify: + type: boolean + nullable: true + email_verify_wait_time: + type: integer + nullable: true + email_verify_expiry: + type: integer + nullable: true \ No newline at end of file diff --git a/driver/web/docs/schemas/config/Config.yaml b/driver/web/docs/schemas/config/Config.yaml index e69bb7109..a0a853850 100644 --- a/driver/web/docs/schemas/config/Config.yaml +++ b/driver/web/docs/schemas/config/Config.yaml @@ -25,6 +25,7 @@ properties: data: anyOf: - $ref: "./EnvConfigData.yaml" + - $ref: "./AuthConfigData.yaml" date_created: readOnly: true type: string diff --git a/driver/web/docs/schemas/index.yaml b/driver/web/docs/schemas/index.yaml index 3d15d0165..7fca1adf6 100644 --- a/driver/web/docs/schemas/index.yaml +++ b/driver/web/docs/schemas/index.yaml @@ -3,6 +3,8 @@ Config: $ref: "./config/Config.yaml" EnvConfigData: $ref: "./config/EnvConfigData.yaml" +AuthConfigData: + $ref: "./config/AuthConfigData.yaml" # application Application: @@ -94,6 +96,8 @@ Username: $ref: "./user/Username.yaml" AccountAuthType: $ref: "./user/AccountAuthType.yaml" +AccountIdentifier: + $ref: "./user/AccountIdentifier.yaml" Device: $ref: "./user/Device.yaml" Follow: @@ -120,26 +124,32 @@ _shared_req_UpdateAccount: $ref: "./apis/shared/requests/update-account/Request.yaml" _shared_req_AccountCheck: $ref: "./apis/shared/requests/AccountCheck.yaml" -_shared_req_CredsEmail: - $ref: "./apis/shared/requests/CredsEmail.yaml" -_shared_req_CredsTwilioPhone: - $ref: "./apis/shared/requests/CredsTwilioPhone.yaml" +_shared_req_CredsNone: + $ref: "./apis/shared/requests/CredsNone.yaml" +_shared_req_CredsAnonymous: + $ref: "./apis/shared/requests/CredsAnonymous.yaml" +_shared_req_CredsCode: + $ref: "./apis/shared/requests/CredsCode.yaml" _shared_req_CredsOIDC: $ref: "./apis/shared/requests/CredsOIDC.yaml" -_shared_req_CredsUsername: - $ref: "./apis/shared/requests/CredsUsername.yaml" -_shared_req_CredsAPIKey: - $ref: "./apis/shared/requests/CredsAPIKey.yaml" -_shared_req_ParamsEmail: - $ref: "./apis/shared/requests/ParamsEmail.yaml" -_shared_req_ParamsOIDC: - $ref: "./apis/shared/requests/ParamsOIDC.yaml" -_shared_req_ParamsUsername: - $ref: "./apis/shared/requests/ParamsUsername.yaml" +_shared_req_CredsPassword: + $ref: "./apis/shared/requests/CredsPassword.yaml" +_shared_req_CredsWebAuthn: + $ref: "./apis/shared/requests/CredsWebAuthn.yaml" +_shared_req_Identifiers: + $ref: "./apis/shared/requests/Identifiers.yaml" +_shared_req_IdentifierString: + $ref: "./apis/shared/requests/IdentifierString.yaml" _shared_req_ParamsNone: $ref: "./apis/shared/requests/ParamsNone.yaml" -_shared_req_ParamsSetEmailCredential: - $ref: "./apis/shared/requests/ParamsSetEmailCredential.yaml" +_shared_req_ParamsOIDC: + $ref: "./apis/shared/requests/ParamsOIDC.yaml" +_shared_req_ParamsPassword: + $ref: "./apis/shared/requests/ParamsPassword.yaml" +_shared_req_ParamsResetPassword: + $ref: "./apis/shared/requests/ParamsResetPassword.yaml" +_shared_req_ParamsWebAuthn: + $ref: "./apis/shared/requests/ParamsWebAuthn.yaml" _shared_req_app-configs: $ref: "./apis/shared/requests/app-configs/Request.yaml" _shared_req_app-configs-org: @@ -158,14 +168,16 @@ _shared_res_Mfa: $ref: "./apis/shared/responses/mfa/Response.yaml" _shared_res_AccountCheck: $ref: "./apis/shared/responses/AccountCheck.yaml" -_shared_res_ParamsAPIKey: - $ref: "./apis/shared/responses/ParamsAPIKey.yaml" -_shared_res_ParamsOIDC: - $ref: "./apis/shared/responses/ParamsOIDC.yaml" +_shared_res_ParamsAnonymous: + $ref: "./apis/shared/responses/ParamsAnonymous.yaml" _shared_res_ParamsNone: $ref: "./apis/shared/responses/ParamsNone.yaml" +_shared_res_ParamsOIDC: + $ref: "./apis/shared/responses/ParamsOIDC.yaml" _shared_res_RokwireToken: $ref: "./apis/shared/responses/RokwireToken.yaml" +_shared_res_SignInOptions: + $ref: "./apis/shared/responses/SignInOptions.yaml" ## end SHARED requests and responses ## SERVICES section @@ -178,14 +190,22 @@ _services_req_account_auth-type-unlink: _services_res_account_auth-type-link: $ref: "./apis/services/account/auth-type/link/response/Response.yaml" +### account identifier link API +_services_req_account_identifier-link: + $ref: "./apis/services/account/identifier/link/request/Link.yaml" +_services_req_account_identifier-unlink: + $ref: "./apis/services/account/identifier/link/request/Unlink.yaml" +_services_res_account_identifier-link: + $ref: "./apis/services/account/identifier/link/response/Response.yaml" + +### identifier_send-verify API +_services_req_identifier_send-verify: + $ref: "./apis/services/identifier/send-verify/request/Request.yaml" + ### credential_update API _services_req_credential_update: $ref: "./apis/services/credential/update/request/Request.yaml" -### credential_send-verify API -_services_req_credential_send-verify: - $ref: "./apis/services/credential/send-verify/request/Request.yaml" - ### credential_forgot_initiate API _services_req_credential_forgot_initiate: $ref: "./apis/services/credential/forgot/initiate/request/Request.yaml" diff --git a/driver/web/docs/schemas/user/Account.yaml b/driver/web/docs/schemas/user/Account.yaml index fe6966468..35f732e74 100644 --- a/driver/web/docs/schemas/user/Account.yaml +++ b/driver/web/docs/schemas/user/Account.yaml @@ -7,8 +7,6 @@ properties: type: string app_org: $ref: "../application/ApplicationOrganization.yaml" - username: - type: string profile: $ref: "./Profile.yaml" privacy: @@ -25,9 +23,10 @@ properties: type: boolean system: type: boolean - external_ids: - type: object - nullable: true + identifiers: + type: array + items: + $ref: "./AccountIdentifier.yaml" auth_types: type: array items: @@ -57,4 +56,8 @@ properties: last_access_token_date: type: string most_recent_client_version: - type: string \ No newline at end of file + type: string + username: + type: string + nullable: true + deprecated: true \ No newline at end of file diff --git a/driver/web/docs/schemas/user/AccountAuthType.yaml b/driver/web/docs/schemas/user/AccountAuthType.yaml index 410ed119b..14ccf00cb 100644 --- a/driver/web/docs/schemas/user/AccountAuthType.yaml +++ b/driver/web/docs/schemas/user/AccountAuthType.yaml @@ -1,20 +1,21 @@ required: - id - - code - - identifier + - auth_type_code type: object properties: id: type: string + auth_type_code: + type: string code: type: string + deprecated: true identifier: type: string + deprecated: true params: type: object additionalProperties: true nullable: true active: type: boolean - unverified: - type: boolean diff --git a/driver/web/docs/schemas/user/AccountIdentifier.yaml b/driver/web/docs/schemas/user/AccountIdentifier.yaml new file mode 100644 index 000000000..8edab60da --- /dev/null +++ b/driver/web/docs/schemas/user/AccountIdentifier.yaml @@ -0,0 +1,24 @@ +required: + - id + - code + - identifier + - verified + - linked + - sensitive +type: object +properties: + id: + type: string + code: + type: string + identifier: + type: string + verified: + type: boolean + linked: + type: boolean + sensitive: + type: boolean + account_auth_type_id: + type: string + nullable: true \ No newline at end of file diff --git a/driver/web/docs/schemas/user/PartialAccount.yaml b/driver/web/docs/schemas/user/PartialAccount.yaml index a0d411601..a97c5f0fb 100644 --- a/driver/web/docs/schemas/user/PartialAccount.yaml +++ b/driver/web/docs/schemas/user/PartialAccount.yaml @@ -8,6 +8,7 @@ required: - roles - groups - anonymous + - identifiers - auth_types - date_created type: object @@ -25,8 +26,6 @@ properties: type: string system: type: boolean - username: - type: string permissions: type: array items: @@ -43,10 +42,14 @@ properties: type: array items: type: string + identifiers: + type: array + items: + $ref: "./AccountIdentifier.yaml" auth_types: type: array items: - $ref: "../user/AccountAuthType.yaml" + $ref: "./AccountAuthType.yaml" system_configs: type: object nullable: true @@ -65,6 +68,7 @@ properties: date_updated: type: string nullable: true - external_ids: - type: object - nullable: true \ No newline at end of file + username: + type: string + nullable: true + deprecated: true \ No newline at end of file diff --git a/driver/web/docs/schemas/user/Profile.yaml b/driver/web/docs/schemas/user/Profile.yaml index 08e1f80ce..78cb4027b 100644 --- a/driver/web/docs/schemas/user/Profile.yaml +++ b/driver/web/docs/schemas/user/Profile.yaml @@ -14,9 +14,11 @@ properties: email: type: string nullable: true + deprecated: true phone: type: string nullable: true + deprecated: true birth_year: type: integer nullable: true diff --git a/driver/web/docs/schemas/user/ProfileNullable.yaml b/driver/web/docs/schemas/user/ProfileNullable.yaml index 9177a56e3..5b264fcfe 100644 --- a/driver/web/docs/schemas/user/ProfileNullable.yaml +++ b/driver/web/docs/schemas/user/ProfileNullable.yaml @@ -13,9 +13,11 @@ properties: email: type: string nullable: true + deprecated: true phone: type: string nullable: true + deprecated: true birth_year: type: integer nullable: true diff --git a/driver/web/ui/webauthn-test.html b/driver/web/ui/webauthn-test.html new file mode 100644 index 000000000..57c309aa5 --- /dev/null +++ b/driver/web/ui/webauthn-test.html @@ -0,0 +1,248 @@ + + + + + + WebAuthn Demo + + + + + + Username: +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/driver/web/utils.go b/driver/web/utils.go index 5f93a7f6e..6667248c2 100644 --- a/driver/web/utils.go +++ b/driver/web/utils.go @@ -17,6 +17,7 @@ package web import ( "core-building-block/core/model" Def "core-building-block/driver/web/docs/gen" + "core-building-block/utils" "encoding/json" "net/http" @@ -35,26 +36,21 @@ func authBuildLoginResponse(l *logs.Log, loginSession *model.LoginSession) logs. //account var accountData *Def.Account - if loginSession.AccountAuthType != nil { - account := loginSession.AccountAuthType.Account - accountData = accountToDef(account) + if loginSession.Account != nil { + accountData = accountToDef(*loginSession.Account) } //params - var paramsRes Def.SharedResLogin_Params + var paramsRes *Def.SharedResLogin_Params + var err error if loginSession.Params != nil { - paramsBytes, err := json.Marshal(loginSession.Params) + paramsRes, err = utils.JSONConvert[Def.SharedResLogin_Params, map[string]interface{}](loginSession.Params) if err != nil { - return l.HTTPResponseErrorAction(logutils.ActionMarshal, logutils.MessageDataType("auth login response params"), nil, err, http.StatusInternalServerError, false) - } - - err = json.Unmarshal(paramsBytes, ¶msRes) - if err != nil { - return l.HTTPResponseErrorAction(logutils.ActionUnmarshal, logutils.MessageDataType("auth login response params"), nil, err, http.StatusInternalServerError, false) + return l.HTTPResponseErrorAction(logutils.ActionParse, logutils.MessageDataType("auth login response params"), nil, err, http.StatusInternalServerError, false) } } - responseData := &Def.SharedResLogin{Token: &rokwireToken, Account: accountData, Params: ¶msRes} + responseData := &Def.SharedResLogin{Token: &rokwireToken, Account: accountData, Params: paramsRes} respData, err := json.Marshal(responseData) if err != nil { return l.HTTPResponseErrorAction(logutils.ActionMarshal, logutils.MessageDataType("auth login response"), nil, err, http.StatusInternalServerError, false) diff --git a/go.mod b/go.mod index 49d3cf5bc..e0c9f98fb 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,11 @@ module core-building-block -go 1.20 +go 1.21 require ( github.com/coreos/go-oidc v2.2.1+incompatible github.com/getkin/kin-openapi v0.120.0 + github.com/go-webauthn/webauthn v0.8.6 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.3.1 github.com/gorilla/mux v1.8.0 @@ -17,7 +18,9 @@ require ( github.com/swaggo/http-swagger v1.3.4 go.mongodb.org/mongo-driver v1.12.1 golang.org/x/crypto v0.14.0 + golang.org/x/oauth2 v0.13.0 golang.org/x/sync v0.4.0 + golang.org/x/text v0.13.0 gopkg.in/go-playground/validator.v9 v9.31.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v2 v2.4.0 @@ -28,15 +31,18 @@ require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/google/go-tpm v0.9.0 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/x448/float16 v0.8.4 // indirect ) require ( @@ -46,12 +52,15 @@ require ( github.com/boombuler/barcode v1.0.1 // indirect github.com/casbin/casbin/v2 v2.77.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fxamacker/cbor/v2 v2.5.0 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/swag v0.22.4 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-webauthn/x v0.1.4 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.5.9 // indirect @@ -69,7 +78,7 @@ require ( github.com/stretchr/objx v0.5.1 // indirect github.com/swaggo/files v1.0.1 // indirect github.com/swaggo/swag v1.16.2 // indirect - github.com/tidwall/gjson v1.16.0 // indirect + github.com/tidwall/gjson v1.17.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect @@ -77,9 +86,7 @@ require ( github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect golang.org/x/net v0.16.0 // indirect - golang.org/x/oauth2 v0.13.0 // indirect golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.14.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.31.0 // indirect diff --git a/go.sum b/go.sum index 964c10f74..f5e3487a7 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= +github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg= github.com/getkin/kin-openapi v0.120.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -44,10 +46,17 @@ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/Nu github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-webauthn/webauthn v0.8.6 h1:bKMtL1qzd2WTFkf1mFTVbreYrwn7dsYmEPjTq6QN90E= +github.com/go-webauthn/webauthn v0.8.6/go.mod h1:emwVLMCI5yx9evTTvr0r+aOZCdWJqMfbRhF0MufyUog= +github.com/go-webauthn/x v0.1.4 h1:sGmIFhcY70l6k7JIDfnjVBiAAFEssga5lXIUXe0GtAs= +github.com/go-webauthn/x v0.1.4/go.mod h1:75Ug0oK6KYpANh5hDOanfDI+dvPWHk788naJVG/37H8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -61,6 +70,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= +github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -80,6 +91,7 @@ github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQs github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -105,6 +117,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= @@ -155,14 +169,17 @@ github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4 github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg= -github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -185,6 +202,7 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -255,6 +273,7 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= diff --git a/main.go b/main.go index c2b531f21..5384883cd 100644 --- a/main.go +++ b/main.go @@ -20,10 +20,10 @@ import ( "core-building-block/core/model" "core-building-block/driven/emailer" "core-building-block/driven/identitybb" + "core-building-block/driven/phoneverifier" "core-building-block/driven/profilebb" "core-building-block/driven/storage" "core-building-block/driver/web" - "core-building-block/utils" "os" "strconv" "strings" @@ -64,16 +64,11 @@ func main() { logger.Infof("Version: %s", Version) - err := utils.SetRandomSeed() - if err != nil { - logger.Error(err.Error()) - } - env := envLoader.GetAndLogEnvVar("ROKWIRE_CORE_ENVIRONMENT", true, false) //local, dev, staging, prod port := envLoader.GetAndLogEnvVar("ROKWIRE_CORE_PORT", false, false) //Default port of 80 if port == "" { - port = "80" + port = "5000" } host := envLoader.GetAndLogEnvVar("ROKWIRE_CORE_HOST", true, false) @@ -88,7 +83,7 @@ func main() { mongoDBName := envLoader.GetAndLogEnvVar("ROKWIRE_CORE_MONGO_DATABASE", true, false) mongoTimeout := envLoader.GetAndLogEnvVar("ROKWIRE_CORE_MONGO_TIMEOUT", false, false) storageAdapter := storage.NewStorageAdapter(host, mongoDBAuth, mongoDBName, mongoTimeout, logger) - err = storageAdapter.Start() + err := storageAdapter.Start() if err != nil { logger.Fatalf("Cannot start the mongoDB adapter: %v", err) } @@ -98,6 +93,11 @@ func main() { twilioToken := envLoader.GetAndLogEnvVar("ROKWIRE_CORE_AUTH_TWILIO_TOKEN", false, true) twilioServiceSID := envLoader.GetAndLogEnvVar("ROKWIRE_CORE_AUTH_TWILIO_SERVICE_SID", false, true) + twilioPhoneVerifier, err := phoneverifier.NewTwilioAdapter(twilioAccountSID, twilioToken, twilioServiceSID) + if err != nil { + logger.Warnf("Cannot start the twilio phone verifier: %v", err) + } + smtpHost := envLoader.GetAndLogEnvVar("ROKWIRE_CORE_SMTP_HOST", false, false) smtpPort := envLoader.GetAndLogEnvVar("ROKWIRE_CORE_SMTP_PORT", false, false) smtpUser := envLoader.GetAndLogEnvVar("ROKWIRE_CORE_SMTP_USER", false, true) @@ -111,6 +111,18 @@ func main() { logger.Infof("Error parsing ROKWIRE_CORE_VERIFY_EMAIL, applying defaults: %v", err) verifyEmail = true } + verifyWaitTimeRaw := envLoader.GetAndLogEnvVar("ROKWIRE_CORE_VERIFY_WAIT_TIME", false, false) + verifyWaitTime, err := strconv.Atoi(verifyWaitTimeRaw) + if err != nil { + logger.Infof("Error parsing ROKWIRE_CORE_VERIFY_WAIT_TIME, applying defaults: %v", err) + verifyWaitTime = 30 // minutes + } + verifyExpiryRaw := envLoader.GetAndLogEnvVar("ROKWIRE_CORE_VERIFY_EXPIRY", false, false) + verifyExpiry, err := strconv.Atoi(verifyExpiryRaw) + if err != nil { + logger.Infof("Error parsing ROKWIRE_CORE_VERIFY_EXPIRY, applying defaults: %v", err) + verifyExpiry = 24 // hours + } emailer := emailer.NewEmailerAdapter(smtpHost, smtpPortNum, smtpUser, smtpPassword, smtpFrom) @@ -180,8 +192,8 @@ func main() { FirstParty: true, } - authImpl, err := auth.NewAuth(serviceID, host, authPrivKey, authService, storageAdapter, emailer, minTokenExp, maxTokenExp, supportLegacySigs, - twilioAccountSID, twilioToken, twilioServiceSID, profileBBAdapter, smtpHost, smtpPortNum, smtpUser, smtpPassword, smtpFrom, logger, Version) + authImpl, err := auth.NewAuth(serviceID, host, authPrivKey, authService, storageAdapter, emailer, twilioPhoneVerifier, profileBBAdapter, + minTokenExp, maxTokenExp, supportLegacySigs, Version, logger) if err != nil { logger.Fatalf("Error initializing auth: %v", err) } @@ -204,7 +216,7 @@ func main() { } //core - coreAPIs := core.NewCoreAPIs(env, Version, Build, serviceID, storageAdapter, authImpl, systemInitSettings, verifyEmail, logger) + coreAPIs := core.NewCoreAPIs(env, Version, Build, serviceID, storageAdapter, authImpl, systemInitSettings, verifyEmail, verifyWaitTime, verifyExpiry, logger) coreAPIs.Start() // read CORS parameters from stored env config diff --git a/utils/utils.go b/utils/utils.go index 9cd6d00af..528059243 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -17,7 +17,6 @@ package utils import ( crand "crypto/rand" "crypto/sha256" - "encoding/binary" "encoding/json" "fmt" "math/rand" @@ -57,22 +56,10 @@ const ( special string = "!@#$%^&*()" ) -// SetRandomSeed sets the seed for random number generation -func SetRandomSeed() error { - seed := make([]byte, 8) - _, err := crand.Read(seed) - if err != nil { - return errors.WrapErrorAction(logutils.ActionGenerate, "math/rand seed", nil, err) - } - - rand.Seed(int64(binary.LittleEndian.Uint64(seed))) - return nil -} - // GenerateRandomBytes returns securely generated random bytes func GenerateRandomBytes(n int) ([]byte, error) { b := make([]byte, n) - _, err := rand.Read(b) + _, err := crand.Read(b) if err != nil { return nil, err } @@ -81,13 +68,13 @@ func GenerateRandomBytes(n int) ([]byte, error) { } // GenerateRandomString returns a URL-safe, base64 encoded securely generated random string -func GenerateRandomString(s int) (string, error) { +func GenerateRandomString(s int) string { chars := []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") b := make([]rune, s) for i := range b { b[i] = chars[rand.Intn(len(chars))] } - return string(b), nil + return string(b) } // GenerateRandomInt returns a random integer between 0 and max @@ -108,13 +95,36 @@ func GenerateRandomPassword(s int) string { return string(password) } -// ConvertToJSON converts to json -func ConvertToJSON(data interface{}) ([]byte, error) { - dataJSON, err := json.Marshal(data) +// JSONConvert json marshals and unmarshals data into result (result should be passed as a pointer) +func JSONConvert[T any, F any](val F) (*T, error) { + if IsNil(val) { + return nil, nil + } + + bytes, err := json.Marshal(val) + if err != nil { + return nil, errors.WrapErrorAction(logutils.ActionMarshal, "value", nil, err) + } + + var out T + err = json.Unmarshal(bytes, &out) if err != nil { - return nil, errors.WrapErrorAction(logutils.ActionMarshal, "map to json", nil, err) + return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, "value", nil, err) + } + + return &out, nil +} + +// IsNil determines whether the given interface has a nil value +func IsNil(i interface{}) bool { + if i == nil { + return true } - return dataJSON, nil + switch reflect.TypeOf(i).Kind() { + case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: + return reflect.ValueOf(i).IsNil() + } + return false } // DeepEqual checks whether a and b are “deeply equal,”