diff --git a/app/package-lock.json b/app/package-lock.json index cf0f682..f8d8f63 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "^1.1.13", + "@authorizerdev/authorizer-react": "^1.1.15", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", @@ -27,9 +27,9 @@ } }, "node_modules/@authorizerdev/authorizer-js": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.6.tgz", - "integrity": "sha512-9+9phHUMF+AeDM0y+XQvIRDoerOXnQ1vfTfYN6KxWN1apdrkAd9nzS1zUsA2uJSnX3fFZOErn83GjbYYCYF1BA==", + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.17.tgz", + "integrity": "sha512-aF/lu9wZR7TBRaRMAes/hy1q8cZzz5Zo60QLU9Iu09sqnhliHJCp5wSkjsVH+V4ER9i7bmJ2HNABTmOdluxj3A==", "dependencies": { "cross-fetch": "^3.1.5" }, @@ -41,11 +41,11 @@ } }, "node_modules/@authorizerdev/authorizer-react": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.13.tgz", - "integrity": "sha512-LmpzyfR0+nEn+bjUrb/QU9b3kiVoYzMBIvcQ1nV4TNvrvVSqbLPKk+GmoIPkiBEtfy/QSM6XFLkiGNGD9BRP+g==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.15.tgz", + "integrity": "sha512-Y71qC4GUAHL0QCNj5mVv0Jwv1cIg4Y0yXRiOeYV21C1NMleyLRXgw4qzJ/Vk8rmXsxqSHmr8SGrwOLcSKA2oMA==", "dependencies": { - "@authorizerdev/authorizer-js": "^1.2.6" + "@authorizerdev/authorizer-js": "^1.2.17" }, "engines": { "node": ">=10" @@ -607,9 +607,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, diff --git a/app/package.json b/app/package.json index 2406108..5221603 100644 --- a/app/package.json +++ b/app/package.json @@ -12,7 +12,7 @@ "author": "Lakhan Samani", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "^1.1.13", + "@authorizerdev/authorizer-react": "^1.1.15", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", diff --git a/app/src/pages/login.tsx b/app/src/pages/login.tsx index 0b713de..3e8e4a1 100644 --- a/app/src/pages/login.tsx +++ b/app/src/pages/login.tsx @@ -37,8 +37,8 @@ export default function Login({ urlProps }: { urlProps: Record }) { {view === VIEW_TYPES.LOGIN && (

Login

-
+
{config.is_basic_authentication_enabled && !config.is_magic_link_login_enabled && ( diff --git a/app/yarn.lock b/app/yarn.lock index 2938142..5e2c385 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -2,19 +2,19 @@ # yarn lockfile v1 -"@authorizerdev/authorizer-js@^1.2.6": - version "1.2.6" - resolved "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.6.tgz" - integrity sha512-9+9phHUMF+AeDM0y+XQvIRDoerOXnQ1vfTfYN6KxWN1apdrkAd9nzS1zUsA2uJSnX3fFZOErn83GjbYYCYF1BA== +"@authorizerdev/authorizer-js@^1.2.17": + version "1.2.17" + resolved "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.17.tgz" + integrity sha512-aF/lu9wZR7TBRaRMAes/hy1q8cZzz5Zo60QLU9Iu09sqnhliHJCp5wSkjsVH+V4ER9i7bmJ2HNABTmOdluxj3A== dependencies: cross-fetch "^3.1.5" -"@authorizerdev/authorizer-react@^1.1.13": - version "1.1.13" - resolved "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.13.tgz" - integrity sha512-LmpzyfR0+nEn+bjUrb/QU9b3kiVoYzMBIvcQ1nV4TNvrvVSqbLPKk+GmoIPkiBEtfy/QSM6XFLkiGNGD9BRP+g== +"@authorizerdev/authorizer-react@^1.1.15": + version "1.1.15" + resolved "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.15.tgz" + integrity sha512-Y71qC4GUAHL0QCNj5mVv0Jwv1cIg4Y0yXRiOeYV21C1NMleyLRXgw4qzJ/Vk8rmXsxqSHmr8SGrwOLcSKA2oMA== dependencies: - "@authorizerdev/authorizer-js" "^1.2.6" + "@authorizerdev/authorizer-js" "^1.2.17" "@babel/code-frame@^7.22.13": version "7.22.13" @@ -420,9 +420,9 @@ ms@2.1.2: integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== node-fetch@^2.6.12: - version "2.6.12" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz" - integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== + version "2.7.0" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" diff --git a/server/authenticators/providers/providers.go b/server/authenticators/providers/providers.go index 60c0f79..7f43ef5 100644 --- a/server/authenticators/providers/providers.go +++ b/server/authenticators/providers/providers.go @@ -19,7 +19,7 @@ type Provider interface { // Generate totp: to generate totp, store secret into db and returns base64 of QR code image Generate(ctx context.Context, id string) (*AuthenticatorConfig, error) // Validate totp: user passcode with secret stored in our db - Validate(ctx context.Context, passcode string, id string) (bool, error) - // RecoveryCode totp: gives a recovery code for first time user - RecoveryCode(ctx context.Context, id string) (*string, error) + Validate(ctx context.Context, passcode string, userID string) (bool, error) + // ValidateRecoveryCode totp: allows user to validate using recovery code incase if they lost their device + ValidateRecoveryCode(ctx context.Context, recoveryCode, userID string) (bool, error) } diff --git a/server/authenticators/providers/totp/totp.go b/server/authenticators/providers/totp/totp.go index 1a28f87..b02fe29 100644 --- a/server/authenticators/providers/totp/totp.go +++ b/server/authenticators/providers/totp/totp.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "image/png" "time" @@ -113,24 +114,38 @@ func (p *provider) Validate(ctx context.Context, passcode string, userID string) return status, nil } -// RecoveryCode generates a recovery code for a user's TOTP authentication, if not already verified. -func (p *provider) RecoveryCode(ctx context.Context, id string) (*string, error) { +// ValidateRecoveryCode validates a Time-Based One-Time Password (TOTP) recovery code against the stored TOTP recovery code for a user. +func (p *provider) ValidateRecoveryCode(ctx context.Context, recoveryCode, userID string) (bool, error) { // get totp details - // totpModel, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, id, constants.EnvKeyTOTPAuthenticator) - // if err != nil { - // return nil, fmt.Errorf("error while getting totp details from authenticators") - // } - // //TODO *totpModel.RecoveryCode == "null" used to just verify couchbase recoveryCode value to be nil - // // have to find another way round - // if totpModel.RecoveryCode == nil || *totpModel.RecoveryCode == "null" { - // recoveryCode := utils.GenerateTOTPRecoveryCode() - // totpModel.RecoveryCode = &recoveryCode - - // _, err = db.Provider.UpdateAuthenticator(ctx, totpModel) - // if err != nil { - // return nil, fmt.Errorf("error while updaing authenticator table for totp") - // } - // return &recoveryCode, nil - // } - return nil, nil + totpModel, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, userID, constants.EnvKeyTOTPAuthenticator) + if err != nil { + return false, err + } + // convert recoveryCodes to map + recoveryCodesMap := map[string]bool{} + err = json.Unmarshal([]byte(refs.StringValue(totpModel.RecoveryCodes)), &recoveryCodesMap) + if err != nil { + return false, err + } + // check if recovery code is valid + if val, ok := recoveryCodesMap[recoveryCode]; !ok { + return false, fmt.Errorf("invalid recovery code") + } else if val { + return false, fmt.Errorf("recovery code already used") + } + // update recovery code map + recoveryCodesMap[recoveryCode] = true + // convert recoveryCodesMap to string + jsonData, err := json.Marshal(recoveryCodesMap) + if err != nil { + return false, err + } + recoveryCodesString := string(jsonData) + totpModel.RecoveryCodes = refs.NewStringRef(recoveryCodesString) + // update recovery code map in db + _, err = db.Provider.UpdateAuthenticator(ctx, totpModel) + if err != nil { + return false, err + } + return true, nil } diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 594b5b9..df7829a 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -2899,7 +2899,7 @@ input VerifyOTPRequest { email: String phone_number: String otp: String! - totp: Boolean + is_totp: Boolean # state is used for authorization code grant flow # it is used to get code for an on-going auth process during login # and use that code for setting ` + "`" + `c_hash` + "`" + ` in id_token @@ -18898,7 +18898,7 @@ func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context, asMap[k] = v } - fieldsInOrder := [...]string{"email", "phone_number", "otp", "totp", "state"} + fieldsInOrder := [...]string{"email", "phone_number", "otp", "is_totp", "state"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -18932,15 +18932,15 @@ func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context, return it, err } it.Otp = data - case "totp": + case "is_totp": var err error - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("totp")) + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("is_totp")) data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) if err != nil { return it, err } - it.Totp = data + it.IsTotp = data case "state": var err error diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 69ece42..06a93ee 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -515,7 +515,7 @@ type VerifyOTPRequest struct { Email *string `json:"email,omitempty"` PhoneNumber *string `json:"phone_number,omitempty"` Otp string `json:"otp"` - Totp *bool `json:"totp,omitempty"` + IsTotp *bool `json:"is_totp,omitempty"` State *string `json:"state,omitempty"` } diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 900eb7b..35e6459 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -573,7 +573,7 @@ input VerifyOTPRequest { email: String phone_number: String otp: String! - totp: Boolean + is_totp: Boolean # state is used for authorization code grant flow # it is used to get code for an on-going auth process during login # and use that code for setting `c_hash` in id_token diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index a0eeb13..e056dee 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -56,7 +56,7 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod return res, err } // Verify OTP based on TOPT or OTP - if refs.BoolValue(params.Totp) { + if refs.BoolValue(params.IsTotp) { status, err := authenticators.Provider.Validate(ctx, params.Otp, user.ID) if err != nil { log.Debug("Failed to validate totp: ", err) @@ -64,7 +64,17 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod } if !status { log.Debug("Failed to verify otp request: Incorrect value") - return res, fmt.Errorf(`invalid otp`) + log.Info("Checking if otp is recovery code") + // Check if otp is recovery code + isValidRecoveryCode, err := authenticators.Provider.ValidateRecoveryCode(ctx, params.Otp, user.ID) + if err != nil { + log.Debug("Failed to validate recovery code: ", err) + return nil, fmt.Errorf("error while validating recovery code") + } + if !isValidRecoveryCode { + log.Debug("Failed to verify otp request: Incorrect value") + return res, fmt.Errorf(`invalid otp`) + } } } else { var otp *models.OTP diff --git a/server/test/totp_login_test.go b/server/test/totp_login_test.go index 44d7c3a..11b992f 100644 --- a/server/test/totp_login_test.go +++ b/server/test/totp_login_test.go @@ -99,9 +99,9 @@ func totpLoginTest(t *testing.T, s TestSetup) { cookie = strings.TrimSuffix(cookie, ";") req.Header.Set("Cookie", cookie) valid, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ - Email: &email, - Totp: refs.NewBoolRef(true), - Otp: code, + Email: &email, + IsTotp: refs.NewBoolRef(true), + Otp: code, }) accessToken := valid.AccessToken assert.NoError(t, err) @@ -147,9 +147,9 @@ func totpLoginTest(t *testing.T, s TestSetup) { cookie = strings.TrimSuffix(cookie, ";") req.Header.Set("Cookie", cookie) valid, err = resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ - Otp: code, - Email: &email, - Totp: refs.NewBoolRef(true), + Otp: code, + Email: &email, + IsTotp: refs.NewBoolRef(true), }) assert.NoError(t, err) assert.NotNil(t, *valid.AccessToken)