feat: add totp UI & recovery code (#429)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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"`
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user