feat: add totp UI & recovery code (#429)
This commit is contained in:
parent
d7da81d308
commit
cac67b7915
22
app/package-lock.json
generated
22
app/package-lock.json
generated
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -56,7 +56,7 @@ 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)
|
||||||
|
@ -64,7 +64,17 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
|
||||||
}
|
}
|
||||||
if !status {
|
if !status {
|
||||||
log.Debug("Failed to verify otp request: Incorrect value")
|
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 {
|
} else {
|
||||||
var otp *models.OTP
|
var otp *models.OTP
|
||||||
|
|
|
@ -99,9 +99,9 @@ func totpLoginTest(t *testing.T, s TestSetup) {
|
||||||
cookie = strings.TrimSuffix(cookie, ";")
|
cookie = strings.TrimSuffix(cookie, ";")
|
||||||
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
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -147,9 +147,9 @@ func totpLoginTest(t *testing.T, s TestSetup) {
|
||||||
cookie = strings.TrimSuffix(cookie, ";")
|
cookie = strings.TrimSuffix(cookie, ";")
|
||||||
req.Header.Set("Cookie", cookie)
|
req.Header.Set("Cookie", cookie)
|
||||||
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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user