feat: add totp UI & recovery code (#429)

This commit is contained in:
Lakhan Samani 2023-12-03 09:03:22 +05:30 committed by GitHub
parent d7da81d308
commit cac67b7915
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 87 additions and 62 deletions

22
app/package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^1.1.13", "@authorizerdev/authorizer-react": "^1.1.15",
"@types/react": "^17.0.15", "@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17", "esbuild": "^0.12.17",
@ -27,9 +27,9 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-js": { "node_modules/@authorizerdev/authorizer-js": {
"version": "1.2.6", "version": "1.2.17",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.6.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.17.tgz",
"integrity": "sha512-9+9phHUMF+AeDM0y+XQvIRDoerOXnQ1vfTfYN6KxWN1apdrkAd9nzS1zUsA2uJSnX3fFZOErn83GjbYYCYF1BA==", "integrity": "sha512-aF/lu9wZR7TBRaRMAes/hy1q8cZzz5Zo60QLU9Iu09sqnhliHJCp5wSkjsVH+V4ER9i7bmJ2HNABTmOdluxj3A==",
"dependencies": { "dependencies": {
"cross-fetch": "^3.1.5" "cross-fetch": "^3.1.5"
}, },
@ -41,11 +41,11 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-react": { "node_modules/@authorizerdev/authorizer-react": {
"version": "1.1.13", "version": "1.1.15",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.13.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.15.tgz",
"integrity": "sha512-LmpzyfR0+nEn+bjUrb/QU9b3kiVoYzMBIvcQ1nV4TNvrvVSqbLPKk+GmoIPkiBEtfy/QSM6XFLkiGNGD9BRP+g==", "integrity": "sha512-Y71qC4GUAHL0QCNj5mVv0Jwv1cIg4Y0yXRiOeYV21C1NMleyLRXgw4qzJ/Vk8rmXsxqSHmr8SGrwOLcSKA2oMA==",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": "^1.2.6" "@authorizerdev/authorizer-js": "^1.2.17"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -607,9 +607,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/node-fetch": { "node_modules/node-fetch": {
"version": "2.6.12", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": { "dependencies": {
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
}, },

View File

@ -12,7 +12,7 @@
"author": "Lakhan Samani", "author": "Lakhan Samani",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^1.1.13", "@authorizerdev/authorizer-react": "^1.1.15",
"@types/react": "^17.0.15", "@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17", "esbuild": "^0.12.17",

View File

@ -37,8 +37,8 @@ export default function Login({ urlProps }: { urlProps: Record<string, any> }) {
{view === VIEW_TYPES.LOGIN && ( {view === VIEW_TYPES.LOGIN && (
<Fragment> <Fragment>
<h1 style={{ textAlign: 'center' }}>Login</h1> <h1 style={{ textAlign: 'center' }}>Login</h1>
<br />
<AuthorizerSocialLogin urlProps={urlProps} /> <AuthorizerSocialLogin urlProps={urlProps} />
<br />
{config.is_basic_authentication_enabled && {config.is_basic_authentication_enabled &&
!config.is_magic_link_login_enabled && ( !config.is_magic_link_login_enabled && (
<AuthorizerBasicAuthLogin urlProps={urlProps} /> <AuthorizerBasicAuthLogin urlProps={urlProps} />

View File

@ -2,19 +2,19 @@
# yarn lockfile v1 # yarn lockfile v1
"@authorizerdev/authorizer-js@^1.2.6": "@authorizerdev/authorizer-js@^1.2.17":
version "1.2.6" version "1.2.17"
resolved "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.6.tgz" resolved "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.17.tgz"
integrity sha512-9+9phHUMF+AeDM0y+XQvIRDoerOXnQ1vfTfYN6KxWN1apdrkAd9nzS1zUsA2uJSnX3fFZOErn83GjbYYCYF1BA== integrity sha512-aF/lu9wZR7TBRaRMAes/hy1q8cZzz5Zo60QLU9Iu09sqnhliHJCp5wSkjsVH+V4ER9i7bmJ2HNABTmOdluxj3A==
dependencies: dependencies:
cross-fetch "^3.1.5" cross-fetch "^3.1.5"
"@authorizerdev/authorizer-react@^1.1.13": "@authorizerdev/authorizer-react@^1.1.15":
version "1.1.13" version "1.1.15"
resolved "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.13.tgz" resolved "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.15.tgz"
integrity sha512-LmpzyfR0+nEn+bjUrb/QU9b3kiVoYzMBIvcQ1nV4TNvrvVSqbLPKk+GmoIPkiBEtfy/QSM6XFLkiGNGD9BRP+g== integrity sha512-Y71qC4GUAHL0QCNj5mVv0Jwv1cIg4Y0yXRiOeYV21C1NMleyLRXgw4qzJ/Vk8rmXsxqSHmr8SGrwOLcSKA2oMA==
dependencies: dependencies:
"@authorizerdev/authorizer-js" "^1.2.6" "@authorizerdev/authorizer-js" "^1.2.17"
"@babel/code-frame@^7.22.13": "@babel/code-frame@^7.22.13":
version "7.22.13" version "7.22.13"
@ -420,9 +420,9 @@ ms@2.1.2:
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
node-fetch@^2.6.12: node-fetch@^2.6.12:
version "2.6.12" version "2.7.0"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz"
integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
dependencies: dependencies:
whatwg-url "^5.0.0" whatwg-url "^5.0.0"

View File

@ -19,7 +19,7 @@ type Provider interface {
// Generate totp: to generate totp, store secret into db and returns base64 of QR code image // Generate totp: to generate totp, store secret into db and returns base64 of QR code image
Generate(ctx context.Context, id string) (*AuthenticatorConfig, error) Generate(ctx context.Context, id string) (*AuthenticatorConfig, error)
// Validate totp: user passcode with secret stored in our db // Validate totp: user passcode with secret stored in our db
Validate(ctx context.Context, passcode string, id string) (bool, error) Validate(ctx context.Context, passcode string, userID string) (bool, error)
// RecoveryCode totp: gives a recovery code for first time user // ValidateRecoveryCode totp: allows user to validate using recovery code incase if they lost their device
RecoveryCode(ctx context.Context, id string) (*string, error) ValidateRecoveryCode(ctx context.Context, recoveryCode, userID string) (bool, error)
} }

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"image/png" "image/png"
"time" "time"
@ -113,24 +114,38 @@ func (p *provider) Validate(ctx context.Context, passcode string, userID string)
return status, nil return status, nil
} }
// RecoveryCode generates a recovery code for a user's TOTP authentication, if not already verified. // ValidateRecoveryCode validates a Time-Based One-Time Password (TOTP) recovery code against the stored TOTP recovery code for a user.
func (p *provider) RecoveryCode(ctx context.Context, id string) (*string, error) { func (p *provider) ValidateRecoveryCode(ctx context.Context, recoveryCode, userID string) (bool, error) {
// get totp details // get totp details
// totpModel, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, id, constants.EnvKeyTOTPAuthenticator) totpModel, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, userID, constants.EnvKeyTOTPAuthenticator)
// if err != nil { if err != nil {
// return nil, fmt.Errorf("error while getting totp details from authenticators") return false, err
// } }
// //TODO *totpModel.RecoveryCode == "null" used to just verify couchbase recoveryCode value to be nil // convert recoveryCodes to map
// // have to find another way round recoveryCodesMap := map[string]bool{}
// if totpModel.RecoveryCode == nil || *totpModel.RecoveryCode == "null" { err = json.Unmarshal([]byte(refs.StringValue(totpModel.RecoveryCodes)), &recoveryCodesMap)
// recoveryCode := utils.GenerateTOTPRecoveryCode() if err != nil {
// totpModel.RecoveryCode = &recoveryCode return false, err
}
// _, err = db.Provider.UpdateAuthenticator(ctx, totpModel) // check if recovery code is valid
// if err != nil { if val, ok := recoveryCodesMap[recoveryCode]; !ok {
// return nil, fmt.Errorf("error while updaing authenticator table for totp") return false, fmt.Errorf("invalid recovery code")
// } } else if val {
// return &recoveryCode, nil return false, fmt.Errorf("recovery code already used")
// } }
return nil, nil // 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
} }

View File

@ -2899,7 +2899,7 @@ input VerifyOTPRequest {
email: String email: String
phone_number: String phone_number: String
otp: String! otp: String!
totp: Boolean is_totp: Boolean
# state is used for authorization code grant flow # state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login # 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 # 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 asMap[k] = v
} }
fieldsInOrder := [...]string{"email", "phone_number", "otp", "totp", "state"} fieldsInOrder := [...]string{"email", "phone_number", "otp", "is_totp", "state"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@ -18932,15 +18932,15 @@ func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context,
return it, err return it, err
} }
it.Otp = data it.Otp = data
case "totp": case "is_totp":
var err error 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) data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v)
if err != nil { if err != nil {
return it, err return it, err
} }
it.Totp = data it.IsTotp = data
case "state": case "state":
var err error var err error

View File

@ -515,7 +515,7 @@ type VerifyOTPRequest struct {
Email *string `json:"email,omitempty"` Email *string `json:"email,omitempty"`
PhoneNumber *string `json:"phone_number,omitempty"` PhoneNumber *string `json:"phone_number,omitempty"`
Otp string `json:"otp"` Otp string `json:"otp"`
Totp *bool `json:"totp,omitempty"` IsTotp *bool `json:"is_totp,omitempty"`
State *string `json:"state,omitempty"` State *string `json:"state,omitempty"`
} }

View File

@ -573,7 +573,7 @@ input VerifyOTPRequest {
email: String email: String
phone_number: String phone_number: String
otp: String! otp: String!
totp: Boolean is_totp: Boolean
# state is used for authorization code grant flow # state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login # 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 # and use that code for setting `c_hash` in id_token

View File

@ -56,16 +56,26 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
return res, err return res, err
} }
// Verify OTP based on TOPT or OTP // 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) status, err := authenticators.Provider.Validate(ctx, params.Otp, user.ID)
if err != nil { if err != nil {
log.Debug("Failed to validate totp: ", err) log.Debug("Failed to validate totp: ", err)
return nil, fmt.Errorf("error while validating passcode") return nil, fmt.Errorf("error while validating passcode")
} }
if !status { if !status {
log.Debug("Failed to verify otp request: Incorrect value")
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") log.Debug("Failed to verify otp request: Incorrect value")
return res, fmt.Errorf(`invalid otp`) return res, fmt.Errorf(`invalid otp`)
} }
}
} else { } else {
var otp *models.OTP var otp *models.OTP
if currentField == models.FieldNameEmail { if currentField == models.FieldNameEmail {

View File

@ -100,7 +100,7 @@ func totpLoginTest(t *testing.T, s TestSetup) {
req.Header.Set("Cookie", cookie) req.Header.Set("Cookie", cookie)
valid, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ valid, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{
Email: &email, Email: &email,
Totp: refs.NewBoolRef(true), IsTotp: refs.NewBoolRef(true),
Otp: code, Otp: code,
}) })
accessToken := valid.AccessToken accessToken := valid.AccessToken
@ -149,7 +149,7 @@ func totpLoginTest(t *testing.T, s TestSetup) {
valid, err = resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ valid, err = resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{
Otp: code, Otp: code,
Email: &email, Email: &email,
Totp: refs.NewBoolRef(true), IsTotp: refs.NewBoolRef(true),
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, *valid.AccessToken) assert.NotNil(t, *valid.AccessToken)