feat: add totp login API (#416)
* fix: * removed hasReversedValue in playground * feat: * added totp methods in db's providers * adding totp in login method * feat: * added toggle in dashboard * fixing issue with env set * feat: * integrated totp * feat: * encrypted userid * added totp_verified column in user table * started test for totp * feat: * test cases totp * test-cases: * completed test cases * tested for all dbs * fixes: * return variable to snake case * import refactoring * feat: * created seperate folder for authenticator with totp subfolder * refactored code * created new table for authenticators * added recovery code for totp * feat: * adding functions to different db providers * feat: * added authenticators method for all db * feat: * added logic for updating mfa in user_profile update * fix: * merge conflict * fix: * resolved mongodb, dynamodb and arangodb test case bug * added new condition for checking first time totp user or not * feat: * changes in all respective db with authenticator * fix: * PR suggested changes * fix(cassandra): list users * Update verify otp * fix totp login api --------- Co-authored-by: lemonScaletech <anand.panigrahi@scaletech.xyz>
This commit is contained in:
23
server/authenticators/providers/providers.go
Normal file
23
server/authenticators/providers/providers.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package providers
|
||||
|
||||
import "context"
|
||||
|
||||
// AuthenticatorConfig defines authenticator config
|
||||
type AuthenticatorConfig struct {
|
||||
// ScannerImage is the base64 of QR code image
|
||||
ScannerImage string
|
||||
// Secrets is the secret key
|
||||
Secret string
|
||||
// RecoveryCode is the secret key
|
||||
RecoveryCodes []string
|
||||
}
|
||||
|
||||
// Provider defines authenticators provider
|
||||
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)
|
||||
}
|
23
server/authenticators/providers/totp/provider.go
Normal file
23
server/authenticators/providers/totp/provider.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package totp
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// TOTPConfig defines totp config
|
||||
type TOTPConfig struct {
|
||||
ScannerImage string
|
||||
Secret string
|
||||
}
|
||||
|
||||
// NewProvider returns a new totp provider
|
||||
func NewProvider() (*provider, error) {
|
||||
ctx := context.Background()
|
||||
return &provider{
|
||||
ctx: ctx,
|
||||
}, nil
|
||||
}
|
128
server/authenticators/providers/totp/totp.go
Normal file
128
server/authenticators/providers/totp/totp.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package totp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pquerna/otp/totp"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/authenticators/providers"
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/crypto"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
"github.com/authorizerdev/authorizer/server/refs"
|
||||
)
|
||||
|
||||
// Generate generates a Time-Based One-Time Password (TOTP) for a user and returns the base64-encoded QR code for frontend display.
|
||||
func (p *provider) Generate(ctx context.Context, id string) (*providers.AuthenticatorConfig, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
//get user details
|
||||
user, err := db.Provider.GetUserByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while getting user details")
|
||||
}
|
||||
|
||||
// generate totp, Authenticators hash is valid for 30 seconds
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "authorizer",
|
||||
AccountName: refs.StringValue(user.Email),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while genrating totp")
|
||||
}
|
||||
|
||||
//generating image for key and encoding to base64 for displaying in frontend
|
||||
img, err := key.Image(200, 200)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while creating qr image for totp")
|
||||
}
|
||||
png.Encode(&buf, img)
|
||||
encodedText := crypto.EncryptB64(buf.String())
|
||||
|
||||
secret := key.Secret()
|
||||
recoveryCodes := []string{}
|
||||
for i := 0; i < 10; i++ {
|
||||
recoveryCodes = append(recoveryCodes, uuid.NewString())
|
||||
}
|
||||
// Converting recoveryCodes to string
|
||||
recoverCodesMap := map[string]bool{}
|
||||
for i := 0; i < len(recoveryCodes); i++ {
|
||||
recoverCodesMap[recoveryCodes[i]] = false
|
||||
}
|
||||
// Converting recoveryCodesMap to string
|
||||
jsonData, err := json.Marshal(recoverCodesMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while converting recoveryCodes to string")
|
||||
}
|
||||
recoveryCodesString := string(jsonData)
|
||||
|
||||
totpModel := &models.Authenticator{
|
||||
Secret: secret,
|
||||
RecoveryCodes: refs.NewStringRef(recoveryCodesString),
|
||||
UserID: user.ID,
|
||||
Method: constants.EnvKeyTOTPAuthenticator,
|
||||
}
|
||||
_, err = db.Provider.AddAuthenticator(ctx, totpModel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while inserting into totp table")
|
||||
}
|
||||
return &providers.AuthenticatorConfig{
|
||||
ScannerImage: encodedText,
|
||||
Secret: secret,
|
||||
RecoveryCodes: recoveryCodes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate validates a Time-Based One-Time Password (TOTP) against the stored TOTP secret for a user.
|
||||
func (p *provider) Validate(ctx context.Context, passcode string, userID string) (bool, error) {
|
||||
// get totp details
|
||||
totpModel, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, userID, constants.EnvKeyTOTPAuthenticator)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error while getting totp details from authenticators")
|
||||
}
|
||||
|
||||
status := totp.Validate(passcode, totpModel.Secret)
|
||||
// checks if user not signed in for totp and totp code is correct then VerifiedAt will be stored in db
|
||||
if totpModel.VerifiedAt == nil {
|
||||
if status {
|
||||
timeNow := time.Now().Unix()
|
||||
totpModel.VerifiedAt = &timeNow
|
||||
_, err = db.Provider.UpdateAuthenticator(ctx, totpModel)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error while updaing authenticator table for totp")
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
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) {
|
||||
// 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
|
||||
}
|
26
server/authenticators/totp_store.go
Normal file
26
server/authenticators/totp_store.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package authenticators
|
||||
|
||||
import (
|
||||
"github.com/authorizerdev/authorizer/server/authenticators/providers"
|
||||
"github.com/authorizerdev/authorizer/server/authenticators/providers/totp"
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/memorystore"
|
||||
)
|
||||
|
||||
// Provider is the global authenticators provider.
|
||||
var Provider providers.Provider
|
||||
|
||||
// InitTOTPStore initializes the TOTP authenticator store if it's not disabled in the environment variables.
|
||||
// It sets the global Provider variable to a new TOTP provider.
|
||||
func InitTOTPStore() error {
|
||||
var err error
|
||||
isTOTPEnvServiceDisabled, _ := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableTOTPLogin)
|
||||
|
||||
if !isTOTPEnvServiceDisabled {
|
||||
Provider, err = totp.NewProvider()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
7
server/constants/authenticator_method.go
Normal file
7
server/constants/authenticator_method.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package constants
|
||||
|
||||
// Authenticators Methods
|
||||
const (
|
||||
// EnvKeyTOTPAuthenticator key for env variable TOTP
|
||||
EnvKeyTOTPAuthenticator = "totp"
|
||||
)
|
@@ -160,6 +160,12 @@ const (
|
||||
// EnvKeyDisableMultiFactorAuthentication is key for env variable DISABLE_MULTI_FACTOR_AUTHENTICATION
|
||||
// this variable is used to completely disable multi factor authentication. It will have no effect on profile preference
|
||||
EnvKeyDisableMultiFactorAuthentication = "DISABLE_MULTI_FACTOR_AUTHENTICATION"
|
||||
// EnvKeyDisableTOTPLogin is key for env variable DISABLE_TOTP_LOGIN
|
||||
// this variable is used to completely disable totp verification
|
||||
EnvKeyDisableTOTPLogin = "DISABLE_TOTP_LOGIN"
|
||||
// EnvKeyDisableMailOTPLogin is key for env variable DISABLE_MAIL_OTP_LOGIN
|
||||
// this variable is used to completely disable totp verification
|
||||
EnvKeyDisableMailOTPLogin = "DISABLE_MAIL_OTP_LOGIN"
|
||||
// EnvKeyDisablePhoneVerification is key for env variable DISABLE_PHONE_VERIFICATION
|
||||
// this variable is used to disable phone verification
|
||||
EnvKeyDisablePhoneVerification = "DISABLE_PHONE_VERIFICATION"
|
||||
|
@@ -3,7 +3,9 @@ package crypto
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
)
|
||||
@@ -116,3 +118,24 @@ func AsRSAStr(privateKey *rsa.PrivateKey, publickKey *rsa.PublicKey) (string, st
|
||||
|
||||
return privParsedPem, pubParsedPem, nil
|
||||
}
|
||||
|
||||
func EncryptRSA(message string, key rsa.PublicKey) (string, error) {
|
||||
label := []byte("OAEP Encrypted")
|
||||
rng := rand.Reader
|
||||
ciphertext, err := rsa.EncryptOAEP(sha256.New(), rng, &key, []byte(message), label)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
func DecryptRSA(cipherText string, privateKey rsa.PrivateKey) (string, error) {
|
||||
ct, _ := base64.StdEncoding.DecodeString(cipherText)
|
||||
label := []byte("OAEP Encrypted")
|
||||
rng := rand.Reader
|
||||
plaintext, err := rsa.DecryptOAEP(sha256.New(), rng, &privateKey, ct, label)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(plaintext), nil
|
||||
}
|
||||
|
@@ -37,7 +37,6 @@ func InitDB() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if isArangoDB {
|
||||
log.Info("Initializing ArangoDB Driver")
|
||||
Provider, err = arangodb.NewProvider()
|
||||
|
16
server/db/models/authenticators.go
Normal file
16
server/db/models/authenticators.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
// Note: any change here should be reflected in providers/casandra/provider.go as it does not have model support in collection creation
|
||||
|
||||
// Authenticators model for db
|
||||
type Authenticator struct {
|
||||
Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty" dynamo:"key,omitempty"` // for arangodb
|
||||
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id" dynamo:"id,hash"`
|
||||
UserID string `gorm:"type:char(36)" json:"user_id" bson:"user_id" cql:"user_id" dynamo:"user_id" index:"user_id,hash"`
|
||||
Method string `json:"method" bson:"method" cql:"method" dynamo:"method"`
|
||||
Secret string `json:"secret" bson:"secret" cql:"secret" dynamo:"secret"`
|
||||
RecoveryCodes *string `json:"recovery_codes" bson:"recovery_codes" cql:"recovery_codes" dynamo:"recovery_codes"`
|
||||
VerifiedAt *int64 `json:"verified_at" bson:"verified_at" cql:"verified_at" dynamo:"verified_at"`
|
||||
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at" dynamo:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at" dynamo:"updated_at"`
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
package models
|
||||
|
||||
// Collections / Tables available for authorizer in the database
|
||||
// CollectionList / Tables available for authorizer in the database
|
||||
type CollectionList struct {
|
||||
User string
|
||||
VerificationRequest string
|
||||
@@ -11,6 +11,7 @@ type CollectionList struct {
|
||||
EmailTemplate string
|
||||
OTP string
|
||||
SMSVerificationRequest string
|
||||
Authenticators string
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -27,5 +28,6 @@ var (
|
||||
EmailTemplate: Prefix + "email_templates",
|
||||
OTP: Prefix + "otps",
|
||||
SMSVerificationRequest: Prefix + "sms_verification_requests",
|
||||
Authenticators: Prefix + "authenticators",
|
||||
}
|
||||
)
|
||||
|
78
server/db/providers/arangodb/authenticator.go
Normal file
78
server/db/providers/arangodb/authenticator.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package arangodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
arangoDriver "github.com/arangodb/go-driver"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
)
|
||||
|
||||
func (p *provider) AddAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
exists, _ := p.GetAuthenticatorDetailsByUserId(ctx, authenticators.UserID, authenticators.Method)
|
||||
if exists != nil {
|
||||
return authenticators, nil
|
||||
}
|
||||
if authenticators.ID == "" {
|
||||
authenticators.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
authenticators.Key = authenticators.ID
|
||||
authenticators.CreatedAt = time.Now().Unix()
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
|
||||
authenticatorsCollection, _ := p.db.Collection(ctx, models.Collections.Authenticators)
|
||||
meta, err := authenticatorsCollection.CreateDocument(arangoDriver.WithOverwrite(ctx), authenticators)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
authenticators.Key = meta.Key
|
||||
authenticators.ID = meta.ID.String()
|
||||
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
func (p *provider) UpdateAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
|
||||
collection, _ := p.db.Collection(ctx, models.Collections.Authenticators)
|
||||
meta, err := collection.UpdateDocument(ctx, authenticators.Key, authenticators)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
|
||||
authenticators.Key = meta.Key
|
||||
authenticators.ID = meta.ID.String()
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
func (p *provider) GetAuthenticatorDetailsByUserId(ctx context.Context, userId string, authenticatorType string) (*models.Authenticator, error) {
|
||||
var authenticators *models.Authenticator
|
||||
query := fmt.Sprintf("FOR d in %s FILTER d.user_id == @user_id AND d.method == @method LIMIT 1 RETURN d", models.Collections.Authenticators)
|
||||
bindVars := map[string]interface{}{
|
||||
"user_id": userId,
|
||||
"method": authenticatorType,
|
||||
}
|
||||
cursor, err := p.db.Query(ctx, query, bindVars)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
for {
|
||||
if !cursor.HasMore() {
|
||||
if authenticators == nil {
|
||||
return authenticators, fmt.Errorf("authenticator not found")
|
||||
}
|
||||
break
|
||||
}
|
||||
_, err := cursor.ReadDocument(ctx, &authenticators)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
}
|
||||
return authenticators, nil
|
||||
}
|
@@ -186,6 +186,7 @@ func NewProvider() (*provider, error) {
|
||||
webhookLogCollection.EnsureHashIndex(ctx, []string{"webhook_id"}, &arangoDriver.EnsureHashIndexOptions{
|
||||
Sparse: true,
|
||||
})
|
||||
|
||||
emailTemplateCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.EmailTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -204,6 +205,7 @@ func NewProvider() (*provider, error) {
|
||||
Unique: true,
|
||||
Sparse: true,
|
||||
})
|
||||
|
||||
otpCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.OTP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -222,6 +224,26 @@ func NewProvider() (*provider, error) {
|
||||
Unique: true,
|
||||
Sparse: true,
|
||||
})
|
||||
|
||||
//authenticators table define
|
||||
authenticatorsCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.Authenticators)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !authenticatorsCollectionExists {
|
||||
_, err = arangodb.CreateCollection(ctx, models.Collections.Authenticators, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
authenticatorsCollection, err := arangodb.Collection(ctx, models.Collections.Authenticators)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authenticatorsCollection.EnsureHashIndex(ctx, []string{"user_id"}, &arangoDriver.EnsureHashIndexOptions{
|
||||
Sparse: true,
|
||||
})
|
||||
|
||||
return &provider{
|
||||
db: arangodb,
|
||||
}, err
|
||||
|
133
server/db/providers/cassandradb/authenticator.go
Normal file
133
server/db/providers/cassandradb/authenticator.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package cassandradb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
)
|
||||
|
||||
func (p *provider) AddAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
exists, _ := p.GetAuthenticatorDetailsByUserId(ctx, authenticators.UserID, authenticators.Method)
|
||||
if exists != nil {
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
if authenticators.ID == "" {
|
||||
authenticators.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
authenticators.CreatedAt = time.Now().Unix()
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
|
||||
bytes, err := json.Marshal(authenticators)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
|
||||
// use decoder instead of json.Unmarshall, because it converts int64 -> float64 after unmarshalling
|
||||
decoder := json.NewDecoder(strings.NewReader(string(bytes)))
|
||||
decoder.UseNumber()
|
||||
authenticatorsMap := map[string]interface{}{}
|
||||
err = decoder.Decode(&authenticatorsMap)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
|
||||
fields := "("
|
||||
values := "("
|
||||
for key, value := range authenticatorsMap {
|
||||
if value != nil {
|
||||
if key == "_id" {
|
||||
fields += "id,"
|
||||
} else {
|
||||
fields += key + ","
|
||||
}
|
||||
|
||||
valueType := reflect.TypeOf(value)
|
||||
if valueType.Name() == "string" {
|
||||
values += fmt.Sprintf("'%s',", value.(string))
|
||||
} else {
|
||||
values += fmt.Sprintf("%v,", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fields = fields[:len(fields)-1] + ")"
|
||||
values = values[:len(values)-1] + ")"
|
||||
|
||||
query := fmt.Sprintf("INSERT INTO %s %s VALUES %s IF NOT EXISTS", KeySpace+"."+models.Collections.Authenticators, fields, values)
|
||||
err = p.db.Query(query).Exec()
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
func (p *provider) UpdateAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
|
||||
bytes, err := json.Marshal(authenticators)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
// use decoder instead of json.Unmarshall, because it converts int64 -> float64 after unmarshalling
|
||||
decoder := json.NewDecoder(strings.NewReader(string(bytes)))
|
||||
decoder.UseNumber()
|
||||
authenticatorsMap := map[string]interface{}{}
|
||||
err = decoder.Decode(&authenticatorsMap)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
|
||||
updateFields := ""
|
||||
for key, value := range authenticatorsMap {
|
||||
if key == "_id" {
|
||||
continue
|
||||
}
|
||||
|
||||
if key == "_key" {
|
||||
continue
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
updateFields += fmt.Sprintf("%s = null, ", key)
|
||||
continue
|
||||
}
|
||||
|
||||
valueType := reflect.TypeOf(value)
|
||||
if valueType.Name() == "string" {
|
||||
updateFields += fmt.Sprintf("%s = '%s', ", key, value.(string))
|
||||
} else {
|
||||
updateFields += fmt.Sprintf("%s = %v, ", key, value)
|
||||
}
|
||||
}
|
||||
updateFields = strings.Trim(updateFields, " ")
|
||||
updateFields = strings.TrimSuffix(updateFields, ",")
|
||||
|
||||
query := fmt.Sprintf("UPDATE %s SET %s WHERE id = '%s'", KeySpace+"."+models.Collections.Authenticators, updateFields, authenticators.ID)
|
||||
err = p.db.Query(query).Exec()
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
func (p *provider) GetAuthenticatorDetailsByUserId(ctx context.Context, userId string, authenticatorType string) (*models.Authenticator, error) {
|
||||
var authenticators models.Authenticator
|
||||
query := fmt.Sprintf("SELECT id, user_id, method, secret, recovery_codes, verified_at, created_at, updated_at FROM %s WHERE user_id = '%s' AND method = '%s' LIMIT 1 ALLOW FILTERING", KeySpace+"."+models.Collections.Authenticators, userId, authenticatorType)
|
||||
err := p.db.Query(query).Consistency(gocql.One).Scan(&authenticators.ID, &authenticators.UserID, &authenticators.Method, &authenticators.Secret, &authenticators.RecoveryCodes, &authenticators.VerifiedAt, &authenticators.CreatedAt, &authenticators.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &authenticators, nil
|
||||
}
|
@@ -274,6 +274,13 @@ func NewProvider() (*provider, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// add authenticators table
|
||||
totpCollectionQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, user_id text, method text, secret text, recovery_codes text, verified_at bigint, updated_at bigint, created_at bigint, PRIMARY KEY (id))", KeySpace, models.Collections.Authenticators)
|
||||
err = session.Query(totpCollectionQuery).Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &provider{
|
||||
db: session,
|
||||
}, err
|
||||
|
@@ -78,6 +78,7 @@ func (p *provider) AddUser(ctx context.Context, user *models.User) (*models.User
|
||||
|
||||
query := fmt.Sprintf("INSERT INTO %s %s VALUES %s IF NOT EXISTS", KeySpace+"."+models.Collections.User, fields, values)
|
||||
err = p.db.Query(query).Exec()
|
||||
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
@@ -177,13 +178,17 @@ func (p *provider) ListUsers(ctx context.Context, pagination *model.Pagination)
|
||||
// there is no offset in cassandra
|
||||
// so we fetch till limit + offset
|
||||
// and return the results from offset to limit
|
||||
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, app_data, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.User, pagination.Limit+pagination.Offset)
|
||||
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, app_data, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.User,
|
||||
pagination.Limit+pagination.Offset)
|
||||
scanner := p.db.Query(query).Iter().Scanner()
|
||||
counter := int64(0)
|
||||
for scanner.Next() {
|
||||
if counter >= pagination.Offset {
|
||||
var user models.User
|
||||
err := scanner.Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled, &user.AppData, &user.CreatedAt, &user.UpdatedAt)
|
||||
err := scanner.Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods,
|
||||
&user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber,
|
||||
&user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled,
|
||||
&user.AppData, &user.CreatedAt, &user.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -297,9 +302,7 @@ func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{},
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
81
server/db/providers/couchbase/authenticator.go
Normal file
81
server/db/providers/couchbase/authenticator.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package couchbase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/couchbase/gocb/v2"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
)
|
||||
|
||||
func (p *provider) AddAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
exists, _ := p.GetAuthenticatorDetailsByUserId(ctx, authenticators.UserID, authenticators.Method)
|
||||
if exists != nil {
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
if authenticators.ID == "" {
|
||||
authenticators.ID = uuid.New().String()
|
||||
}
|
||||
authenticators.Key = authenticators.ID
|
||||
authenticators.CreatedAt = time.Now().Unix()
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
insertOpt := gocb.InsertOptions{
|
||||
Context: ctx,
|
||||
}
|
||||
_, err := p.db.Collection(models.Collections.Authenticators).Insert(authenticators.ID, authenticators, &insertOpt)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
func (p *provider) UpdateAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
bytes, err := json.Marshal(authenticators)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// use decoder instead of json.Unmarshall, because it converts int64 -> float64 after unmarshalling
|
||||
decoder := json.NewDecoder(strings.NewReader(string(bytes)))
|
||||
decoder.UseNumber()
|
||||
authenticator := map[string]interface{}{}
|
||||
err = decoder.Decode(&authenticator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updateFields, params := GetSetFields(authenticator)
|
||||
query := fmt.Sprintf("UPDATE %s.%s SET %s WHERE _id = '%s'", p.scopeName, models.Collections.Authenticators, updateFields, authenticators.ID)
|
||||
_, err = p.db.Query(query, &gocb.QueryOptions{
|
||||
Context: ctx,
|
||||
ScanConsistency: gocb.QueryScanConsistencyRequestPlus,
|
||||
NamedParameters: params,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
func (p *provider) GetAuthenticatorDetailsByUserId(ctx context.Context, userId string, authenticatorType string) (*models.Authenticator, error) {
|
||||
var authenticators *models.Authenticator
|
||||
query := fmt.Sprintf("SELECT _id, user_id, method, secret, recovery_code, verified_at, created_at, updated_at FROM %s.%s WHERE user_id = $1 AND method = $2 LIMIT 1", p.scopeName, models.Collections.Authenticators)
|
||||
q, err := p.db.Query(query, &gocb.QueryOptions{
|
||||
ScanConsistency: gocb.QueryScanConsistencyRequestPlus,
|
||||
Context: ctx,
|
||||
PositionalParameters: []interface{}{userId, authenticatorType},
|
||||
})
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
err = q.One(&authenticators)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
return authenticators, nil
|
||||
}
|
@@ -43,10 +43,10 @@ func (p *provider) AddUser(ctx context.Context, user *models.User) (*models.User
|
||||
// UpdateUser to update user information in database
|
||||
func (p *provider) UpdateUser(ctx context.Context, user *models.User) (*models.User, error) {
|
||||
user.UpdatedAt = time.Now().Unix()
|
||||
unsertOpt := gocb.UpsertOptions{
|
||||
upsertOpt := gocb.UpsertOptions{
|
||||
Context: ctx,
|
||||
}
|
||||
_, err := p.db.Collection(models.Collections.User).Upsert(user.ID, user, &unsertOpt)
|
||||
_, err := p.db.Collection(models.Collections.User).Upsert(user.ID, user, &upsertOpt)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
57
server/db/providers/dynamodb/authenticator.go
Normal file
57
server/db/providers/dynamodb/authenticator.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package dynamodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
)
|
||||
|
||||
func (p *provider) AddAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
exists, _ := p.GetAuthenticatorDetailsByUserId(ctx, authenticators.UserID, authenticators.Method)
|
||||
if exists != nil {
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
collection := p.db.Table(models.Collections.Authenticators)
|
||||
if authenticators.ID == "" {
|
||||
authenticators.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
authenticators.CreatedAt = time.Now().Unix()
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
err := collection.Put(authenticators).RunWithContext(ctx)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
func (p *provider) UpdateAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
collection := p.db.Table(models.Collections.Authenticators)
|
||||
if authenticators.ID != "" {
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
err := UpdateByHashKey(collection, "id", authenticators.ID, authenticators)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
}
|
||||
return authenticators, nil
|
||||
|
||||
}
|
||||
|
||||
func (p *provider) GetAuthenticatorDetailsByUserId(ctx context.Context, userId string, authenticatorType string) (*models.Authenticator, error) {
|
||||
var authenticators *models.Authenticator
|
||||
collection := p.db.Table(models.Collections.Authenticators)
|
||||
iter := collection.Scan().Filter("'user_id' = ?", userId).Filter("'method' = ?", authenticatorType).Iter()
|
||||
for iter.NextWithContext(ctx, &authenticators) {
|
||||
return authenticators, nil
|
||||
}
|
||||
err := iter.Err()
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
return authenticators, nil
|
||||
}
|
@@ -52,6 +52,7 @@ func NewProvider() (*provider, error) {
|
||||
db.CreateTable(models.Collections.VerificationRequest, models.VerificationRequest{}).Wait()
|
||||
db.CreateTable(models.Collections.Webhook, models.Webhook{}).Wait()
|
||||
db.CreateTable(models.Collections.WebhookLog, models.WebhookLog{}).Wait()
|
||||
db.CreateTable(models.Collections.Authenticators, models.Authenticator{}).Wait()
|
||||
return &provider{
|
||||
db: db,
|
||||
}, nil
|
||||
|
@@ -53,10 +53,6 @@ func (p *provider) UpdateUser(ctx context.Context, user *models.User) (*models.U
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
52
server/db/providers/mongodb/authenticator.go
Normal file
52
server/db/providers/mongodb/authenticator.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
)
|
||||
|
||||
func (p *provider) AddAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
exists, _ := p.GetAuthenticatorDetailsByUserId(ctx, authenticators.UserID, authenticators.Method)
|
||||
if exists != nil {
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
if authenticators.ID == "" {
|
||||
authenticators.ID = uuid.New().String()
|
||||
}
|
||||
authenticators.CreatedAt = time.Now().Unix()
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
authenticators.Key = authenticators.ID
|
||||
authenticatorsCollection := p.db.Collection(models.Collections.Authenticators, options.Collection())
|
||||
_, err := authenticatorsCollection.InsertOne(ctx, authenticators)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
func (p *provider) UpdateAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
authenticatorsCollection := p.db.Collection(models.Collections.Authenticators, options.Collection())
|
||||
_, err := authenticatorsCollection.UpdateOne(ctx, bson.M{"_id": bson.M{"$eq": authenticators.ID}}, bson.M{"$set": authenticators})
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
func (p *provider) GetAuthenticatorDetailsByUserId(ctx context.Context, userId string, authenticatorType string) (*models.Authenticator, error) {
|
||||
var authenticators *models.Authenticator
|
||||
authenticatorsCollection := p.db.Collection(models.Collections.Authenticators, options.Collection())
|
||||
err := authenticatorsCollection.FindOne(ctx, bson.M{"user_id": userId, "method": authenticatorType}).Decode(&authenticators)
|
||||
if err != nil {
|
||||
return authenticators, err
|
||||
}
|
||||
return authenticators, nil
|
||||
}
|
@@ -122,6 +122,15 @@ func NewProvider() (*provider, error) {
|
||||
},
|
||||
}, options.CreateIndexes())
|
||||
|
||||
mongodb.CreateCollection(ctx, models.Collections.Authenticators, options.CreateCollection())
|
||||
authenticatorsCollection := mongodb.Collection(models.Collections.Authenticators, options.Collection())
|
||||
authenticatorsCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.M{"user_id": 1},
|
||||
Options: options.Index().SetSparse(true),
|
||||
},
|
||||
}, options.CreateIndexes())
|
||||
|
||||
return &provider{
|
||||
db: mongodb,
|
||||
}, nil
|
||||
|
34
server/db/providers/provider_template/authenticator.go
Normal file
34
server/db/providers/provider_template/authenticator.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package provider_template
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
)
|
||||
|
||||
func (p *provider) AddAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
exists, _ := p.GetAuthenticatorDetailsByUserId(ctx, authenticators.UserID, authenticators.Method)
|
||||
if exists != nil {
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
if authenticators.ID == "" {
|
||||
authenticators.ID = uuid.New().String()
|
||||
}
|
||||
authenticators.CreatedAt = time.Now().Unix()
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
func (p *provider) UpdateAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
func (p *provider) GetAuthenticatorDetailsByUserId(ctx context.Context, userId string, authenticatorType string) (*models.Authenticator, error) {
|
||||
var authenticators *models.Authenticator
|
||||
return authenticators, nil
|
||||
}
|
@@ -26,7 +26,7 @@ type Provider interface {
|
||||
// If ids set to nil / empty all the users will be updated
|
||||
UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error
|
||||
|
||||
// AddVerification to save verification request in database
|
||||
// AddVerificationRequest to save verification request in database
|
||||
AddVerificationRequest(ctx context.Context, verificationRequest *models.VerificationRequest) (*models.VerificationRequest, error)
|
||||
// GetVerificationRequestByToken to get verification request from database using token
|
||||
GetVerificationRequestByToken(ctx context.Context, token string) (*models.VerificationRequest, error)
|
||||
@@ -53,7 +53,7 @@ type Provider interface {
|
||||
AddWebhook(ctx context.Context, webhook *models.Webhook) (*model.Webhook, error)
|
||||
// UpdateWebhook to update webhook
|
||||
UpdateWebhook(ctx context.Context, webhook *models.Webhook) (*model.Webhook, error)
|
||||
// ListWebhooks to list webhook
|
||||
// ListWebhook to list webhook
|
||||
ListWebhook(ctx context.Context, pagination *model.Pagination) (*model.Webhooks, error)
|
||||
// GetWebhookByID to get webhook by id
|
||||
GetWebhookByID(ctx context.Context, webhookID string) (*model.Webhook, error)
|
||||
@@ -71,7 +71,7 @@ type Provider interface {
|
||||
AddEmailTemplate(ctx context.Context, emailTemplate *models.EmailTemplate) (*model.EmailTemplate, error)
|
||||
// UpdateEmailTemplate to update EmailTemplate
|
||||
UpdateEmailTemplate(ctx context.Context, emailTemplate *models.EmailTemplate) (*model.EmailTemplate, error)
|
||||
// ListEmailTemplates to list EmailTemplate
|
||||
// ListEmailTemplate to list EmailTemplate
|
||||
ListEmailTemplate(ctx context.Context, pagination *model.Pagination) (*model.EmailTemplates, error)
|
||||
// GetEmailTemplateByID to get EmailTemplate by id
|
||||
GetEmailTemplateByID(ctx context.Context, emailTemplateID string) (*model.EmailTemplate, error)
|
||||
@@ -88,4 +88,15 @@ type Provider interface {
|
||||
GetOTPByPhoneNumber(ctx context.Context, phoneNumber string) (*models.OTP, error)
|
||||
// DeleteOTP to delete otp
|
||||
DeleteOTP(ctx context.Context, otp *models.OTP) error
|
||||
|
||||
// AddAuthenticator adds a new authenticator document to the database.
|
||||
// If the authenticator doesn't have an ID, a new one is generated.
|
||||
// The created document is returned, or an error if the operation fails.
|
||||
AddAuthenticator(ctx context.Context, totp *models.Authenticator) (*models.Authenticator, error)
|
||||
// UpdateAuthenticator updates an existing authenticator document in the database.
|
||||
// The updated document is returned, or an error if the operation fails.
|
||||
UpdateAuthenticator(ctx context.Context, totp *models.Authenticator) (*models.Authenticator, error)
|
||||
// GetAuthenticatorDetailsByUserId retrieves details of an authenticator document based on user ID and authenticator type.
|
||||
// If found, the authenticator document is returned, or an error if not found or an error occurs during the retrieval.
|
||||
GetAuthenticatorDetailsByUserId(ctx context.Context, userId string, authenticatorType string) (*models.Authenticator, error)
|
||||
}
|
||||
|
55
server/db/providers/sql/authenticator.go
Normal file
55
server/db/providers/sql/authenticator.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
)
|
||||
|
||||
func (p *provider) AddAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
exists, _ := p.GetAuthenticatorDetailsByUserId(ctx, authenticators.UserID, authenticators.Method)
|
||||
if exists != nil {
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
if authenticators.ID == "" {
|
||||
authenticators.ID = uuid.New().String()
|
||||
}
|
||||
authenticators.Key = authenticators.ID
|
||||
authenticators.CreatedAt = time.Now().Unix()
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
res := p.db.Clauses(
|
||||
clause.OnConflict{
|
||||
UpdateAll: true,
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
}).Create(&authenticators)
|
||||
if res.Error != nil {
|
||||
return nil, res.Error
|
||||
}
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
func (p *provider) UpdateAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) {
|
||||
authenticators.UpdatedAt = time.Now().Unix()
|
||||
|
||||
result := p.db.Save(&authenticators)
|
||||
|
||||
if result.Error != nil {
|
||||
return authenticators, result.Error
|
||||
}
|
||||
|
||||
return authenticators, nil
|
||||
}
|
||||
|
||||
func (p *provider) GetAuthenticatorDetailsByUserId(ctx context.Context, userId string, authenticatorType string) (*models.Authenticator, error) {
|
||||
var authenticators models.Authenticator
|
||||
result := p.db.Where("user_id = ?", userId).Where("method = ?", authenticatorType).First(&authenticators)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
return &authenticators, nil
|
||||
}
|
@@ -77,7 +77,7 @@ func NewProvider() (*provider, error) {
|
||||
logrus.Debug("Failed to drop phone number constraint:", err)
|
||||
}
|
||||
|
||||
err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{}, &models.Webhook{}, &models.WebhookLog{}, &models.EmailTemplate{}, &models.OTP{})
|
||||
err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{}, &models.Webhook{}, &models.WebhookLog{}, &models.EmailTemplate{}, &models.OTP{}, &models.Authenticator{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
37
server/env/env.go
vendored
37
server/env/env.go
vendored
@@ -104,6 +104,8 @@ func InitAllEnv() error {
|
||||
osDisableStrongPassword := os.Getenv(constants.EnvKeyDisableStrongPassword)
|
||||
osEnforceMultiFactorAuthentication := os.Getenv(constants.EnvKeyEnforceMultiFactorAuthentication)
|
||||
osDisableMultiFactorAuthentication := os.Getenv(constants.EnvKeyDisableMultiFactorAuthentication)
|
||||
osDisableTOTPLogin := os.Getenv(constants.EnvKeyDisableTOTPLogin)
|
||||
osDisableMailOTPLogin := os.Getenv(constants.EnvKeyDisableMailOTPLogin)
|
||||
// phone verification var
|
||||
osDisablePhoneVerification := os.Getenv(constants.EnvKeyDisablePhoneVerification)
|
||||
osDisablePlayground := os.Getenv(constants.EnvKeyDisablePlayGround)
|
||||
@@ -689,20 +691,13 @@ func InitAllEnv() error {
|
||||
envData[constants.EnvKeyDisableEmailVerification] = true
|
||||
envData[constants.EnvKeyDisableMagicLinkLogin] = true
|
||||
envData[constants.EnvKeyIsEmailServiceEnabled] = false
|
||||
envData[constants.EnvKeyDisableMailOTPLogin] = true
|
||||
}
|
||||
|
||||
if envData[constants.EnvKeySmtpHost] != "" && envData[constants.EnvKeySmtpUsername] != "" && envData[constants.EnvKeySmtpPassword] != "" && envData[constants.EnvKeySenderEmail] != "" && envData[constants.EnvKeySmtpPort] != "" {
|
||||
envData[constants.EnvKeyIsEmailServiceEnabled] = true
|
||||
}
|
||||
|
||||
if envData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && !envData[constants.EnvKeyIsEmailServiceEnabled].(bool) && !envData[constants.EnvKeyIsSMSServiceEnabled].(bool) {
|
||||
return errors.New("to enable multi factor authentication, please enable email service")
|
||||
}
|
||||
|
||||
if !envData[constants.EnvKeyIsEmailServiceEnabled].(bool) {
|
||||
envData[constants.EnvKeyDisableMultiFactorAuthentication] = true
|
||||
}
|
||||
|
||||
if envData[constants.EnvKeyDisableEmailVerification].(bool) {
|
||||
envData[constants.EnvKeyDisableMagicLinkLogin] = true
|
||||
}
|
||||
@@ -840,6 +835,32 @@ func InitAllEnv() error {
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := envData[constants.EnvKeyDisableTOTPLogin]; !ok {
|
||||
envData[constants.EnvKeyDisableTOTPLogin] = osDisableTOTPLogin == "false"
|
||||
}
|
||||
if osDisableTOTPLogin != "" {
|
||||
boolValue, err := strconv.ParseBool(osDisableTOTPLogin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if boolValue != envData[constants.EnvKeyDisableTOTPLogin].(bool) {
|
||||
envData[constants.EnvKeyDisableTOTPLogin] = boolValue
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := envData[constants.EnvKeyDisableMailOTPLogin]; !ok {
|
||||
envData[constants.EnvKeyDisableMailOTPLogin] = osDisableMailOTPLogin == "true"
|
||||
}
|
||||
if osDisableMailOTPLogin != "" {
|
||||
boolValue, err := strconv.ParseBool(osDisableMailOTPLogin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if boolValue != envData[constants.EnvKeyDisableMailOTPLogin].(bool) {
|
||||
envData[constants.EnvKeyDisableMailOTPLogin] = boolValue
|
||||
}
|
||||
}
|
||||
|
||||
err = memorystore.Provider.UpdateEnvStore(envData)
|
||||
if err != nil {
|
||||
log.Debug("Error while updating env store: ", err)
|
||||
|
7
server/env/persist_env.go
vendored
7
server/env/persist_env.go
vendored
@@ -196,7 +196,7 @@ func PersistEnv() error {
|
||||
envValue := strings.TrimSpace(os.Getenv(key))
|
||||
if envValue != "" {
|
||||
switch key {
|
||||
case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableMobileBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled, constants.EnvKeyIsSMSServiceEnabled, constants.EnvKeyEnforceMultiFactorAuthentication, constants.EnvKeyDisableMultiFactorAuthentication, constants.EnvKeyAdminCookieSecure, constants.EnvKeyAppCookieSecure, constants.EnvKeyDisablePhoneVerification, constants.EnvKeyDisablePlayGround:
|
||||
case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableMobileBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled, constants.EnvKeyIsSMSServiceEnabled, constants.EnvKeyEnforceMultiFactorAuthentication, constants.EnvKeyDisableMultiFactorAuthentication, constants.EnvKeyAdminCookieSecure, constants.EnvKeyAppCookieSecure, constants.EnvKeyDisablePhoneVerification, constants.EnvKeyDisablePlayGround, constants.EnvKeyDisableTOTPLogin, constants.EnvKeyDisableMailOTPLogin:
|
||||
if envValueBool, err := strconv.ParseBool(envValue); err == nil {
|
||||
if value.(bool) != envValueBool {
|
||||
storeData[key] = envValueBool
|
||||
@@ -227,6 +227,11 @@ func PersistEnv() error {
|
||||
storeData[constants.EnvKeyDisableMagicLinkLogin] = true
|
||||
hasChanged = true
|
||||
}
|
||||
|
||||
if !storeData[constants.EnvKeyDisableMailOTPLogin].(bool) {
|
||||
storeData[constants.EnvKeyDisableMailOTPLogin] = true
|
||||
hasChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
err = memorystore.Provider.UpdateEnvStore(storeData)
|
||||
|
@@ -5,7 +5,7 @@ go 1.16
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.39
|
||||
github.com/arangodb/go-driver v1.6.0
|
||||
github.com/aws/aws-sdk-go v1.45.25
|
||||
github.com/aws/aws-sdk-go v1.47.4
|
||||
github.com/bytedance/sonic v1.10.2 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.6.0
|
||||
github.com/couchbase/gocb/v2 v2.6.4
|
||||
@@ -16,6 +16,7 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.15.5 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/gocql/gocql v1.6.0
|
||||
github.com/gokyle/twofactor v1.0.1
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/guregu/dynamo v1.20.2
|
||||
@@ -26,11 +27,13 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/redis/go-redis/v9 v9.2.1
|
||||
github.com/robertkrimen/otto v0.2.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/sosodev/duration v1.2.0 // indirect
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tuotoo/qrcode v0.0.0-20220425170535-52ccc2bebf5d
|
||||
github.com/twilio/twilio-go v1.14.1
|
||||
github.com/urfave/cli/v2 v2.25.7 // indirect
|
||||
github.com/vektah/gqlparser/v2 v2.5.10
|
||||
|
@@ -643,14 +643,18 @@ github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e/go.mod h1:m
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/aws/aws-sdk-go v1.44.306/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aws/aws-sdk-go v1.45.25 h1:c4fLlh5sLdK2DCRTY1z0hyuJZU4ygxX8m1FswL6/nF4=
|
||||
github.com/aws/aws-sdk-go v1.45.25/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aws/aws-sdk-go v1.47.4 h1:IyhNbmPt+5ldi5HNzv7ZnXiqSglDMaJiZlzj4Yq3qnk=
|
||||
github.com/aws/aws-sdk-go v1.47.4/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
|
||||
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
|
||||
github.com/bits-and-blooms/bitset v1.2.1 h1:M+/hrU9xlMp7t4TyTDQW97d3tRPVuKFC6zBEK16QnXY=
|
||||
github.com/bits-and-blooms/bitset v1.2.1/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
@@ -788,6 +792,8 @@ 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/gocql/gocql v1.6.0 h1:IdFdOTbnpbd0pDhl4REKQDM+Q0SzKXQ1Yh+YZZ8T/qU=
|
||||
github.com/gocql/gocql v1.6.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8=
|
||||
github.com/gokyle/twofactor v1.0.1 h1:uRhvx0S4Hb82RPIDALnf7QxbmPL49LyyaCkJDpWx+Ek=
|
||||
github.com/gokyle/twofactor v1.0.1/go.mod h1:4gxzH1eaE/F3Pct/sCDNOylP0ClofUO5j4XZN9tKtLE=
|
||||
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/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
@@ -997,6 +1003,8 @@ github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxA
|
||||
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
|
||||
github.com/maruel/rs v1.1.0 h1:dh4OceAF5yD06EASOrb+DS358LI4g0B90YApSdjCP6U=
|
||||
github.com/maruel/rs v1.1.0/go.mod h1:vzwMjzSJJxLIXmU62qHj6O5QRn5kvCKxFrfaFCxBcUY=
|
||||
github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
@@ -1040,6 +1048,8 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
@@ -1089,6 +1099,8 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tuotoo/qrcode v0.0.0-20220425170535-52ccc2bebf5d h1:4x1FeGJRB00cvxnKXnRJDT89fvG/Lzm2ecm0vlr/qDs=
|
||||
github.com/tuotoo/qrcode v0.0.0-20220425170535-52ccc2bebf5d/go.mod h1:uSELzeIcTceNCgzbKdJuJa0ouCqqtkyzL+6bnA3rM+M=
|
||||
github.com/twilio/twilio-go v1.14.1 h1:uyMwNe2naFKwxLpVflAHbKEPiW9iHNI8VF6NWLJJ1Kk=
|
||||
github.com/twilio/twilio-go v1.14.1/go.mod h1:tdnfQ5TjbewoAu4lf9bMsGvfuJ/QU9gYuv9yx3TSIXU=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
@@ -1929,5 +1941,7 @@ modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
@@ -47,14 +47,18 @@ type DirectiveRoot struct {
|
||||
|
||||
type ComplexityRoot struct {
|
||||
AuthResponse struct {
|
||||
AccessToken func(childComplexity int) int
|
||||
ExpiresIn func(childComplexity int) int
|
||||
IDToken func(childComplexity int) int
|
||||
Message func(childComplexity int) int
|
||||
RefreshToken func(childComplexity int) int
|
||||
ShouldShowEmailOtpScreen func(childComplexity int) int
|
||||
ShouldShowMobileOtpScreen func(childComplexity int) int
|
||||
User func(childComplexity int) int
|
||||
AccessToken func(childComplexity int) int
|
||||
AuthenticatorRecoveryCodes func(childComplexity int) int
|
||||
AuthenticatorScannerImage func(childComplexity int) int
|
||||
AuthenticatorSecret func(childComplexity int) int
|
||||
ExpiresIn func(childComplexity int) int
|
||||
IDToken func(childComplexity int) int
|
||||
Message func(childComplexity int) int
|
||||
RefreshToken func(childComplexity int) int
|
||||
ShouldShowEmailOtpScreen func(childComplexity int) int
|
||||
ShouldShowMobileOtpScreen func(childComplexity int) int
|
||||
ShouldShowTotpScreen func(childComplexity int) int
|
||||
User func(childComplexity int) int
|
||||
}
|
||||
|
||||
EmailTemplate struct {
|
||||
@@ -98,11 +102,13 @@ type ComplexityRoot struct {
|
||||
DisableEmailVerification func(childComplexity int) int
|
||||
DisableLoginPage func(childComplexity int) int
|
||||
DisableMagicLinkLogin func(childComplexity int) int
|
||||
DisableMailOtpLogin func(childComplexity int) int
|
||||
DisableMultiFactorAuthentication func(childComplexity int) int
|
||||
DisablePlayground func(childComplexity int) int
|
||||
DisableRedisForEnv func(childComplexity int) int
|
||||
DisableSignUp func(childComplexity int) int
|
||||
DisableStrongPassword func(childComplexity int) int
|
||||
DisableTotpLogin func(childComplexity int) int
|
||||
EnforceMultiFactorAuthentication func(childComplexity int) int
|
||||
FacebookClientID func(childComplexity int) int
|
||||
FacebookClientSecret func(childComplexity int) int
|
||||
@@ -412,6 +418,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.AuthResponse.AccessToken(childComplexity), true
|
||||
|
||||
case "AuthResponse.authenticator_recovery_codes":
|
||||
if e.complexity.AuthResponse.AuthenticatorRecoveryCodes == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.AuthResponse.AuthenticatorRecoveryCodes(childComplexity), true
|
||||
|
||||
case "AuthResponse.authenticator_scanner_image":
|
||||
if e.complexity.AuthResponse.AuthenticatorScannerImage == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.AuthResponse.AuthenticatorScannerImage(childComplexity), true
|
||||
|
||||
case "AuthResponse.authenticator_secret":
|
||||
if e.complexity.AuthResponse.AuthenticatorSecret == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.AuthResponse.AuthenticatorSecret(childComplexity), true
|
||||
|
||||
case "AuthResponse.expires_in":
|
||||
if e.complexity.AuthResponse.ExpiresIn == nil {
|
||||
break
|
||||
@@ -454,6 +481,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.AuthResponse.ShouldShowMobileOtpScreen(childComplexity), true
|
||||
|
||||
case "AuthResponse.should_show_totp_screen":
|
||||
if e.complexity.AuthResponse.ShouldShowTotpScreen == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.AuthResponse.ShouldShowTotpScreen(childComplexity), true
|
||||
|
||||
case "AuthResponse.user":
|
||||
if e.complexity.AuthResponse.User == nil {
|
||||
break
|
||||
@@ -699,6 +733,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.Env.DisableMagicLinkLogin(childComplexity), true
|
||||
|
||||
case "Env.DISABLE_MAIL_OTP_LOGIN":
|
||||
if e.complexity.Env.DisableMailOtpLogin == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.Env.DisableMailOtpLogin(childComplexity), true
|
||||
|
||||
case "Env.DISABLE_MULTI_FACTOR_AUTHENTICATION":
|
||||
if e.complexity.Env.DisableMultiFactorAuthentication == nil {
|
||||
break
|
||||
@@ -734,6 +775,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.Env.DisableStrongPassword(childComplexity), true
|
||||
|
||||
case "Env.DISABLE_TOTP_LOGIN":
|
||||
if e.complexity.Env.DisableTotpLogin == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.Env.DisableTotpLogin(childComplexity), true
|
||||
|
||||
case "Env.ENFORCE_MULTI_FACTOR_AUTHENTICATION":
|
||||
if e.complexity.Env.EnforceMultiFactorAuthentication == nil {
|
||||
break
|
||||
@@ -2349,11 +2397,19 @@ type AuthResponse {
|
||||
message: String!
|
||||
should_show_email_otp_screen: Boolean
|
||||
should_show_mobile_otp_screen: Boolean
|
||||
should_show_totp_screen: Boolean
|
||||
access_token: String
|
||||
id_token: String
|
||||
refresh_token: String
|
||||
expires_in: Int64
|
||||
user: User
|
||||
# key for totp login
|
||||
# it is a base64 image url
|
||||
authenticator_scanner_image: String
|
||||
# string which can be used instead of scanner image
|
||||
authenticator_secret: String
|
||||
# recovery codes for totp login shared with user only once
|
||||
authenticator_recovery_codes: [String]
|
||||
}
|
||||
|
||||
type Response {
|
||||
@@ -2428,6 +2484,8 @@ type Env {
|
||||
DEFAULT_AUTHORIZE_RESPONSE_TYPE: String
|
||||
DEFAULT_AUTHORIZE_RESPONSE_MODE: String
|
||||
DISABLE_PLAYGROUND: Boolean!
|
||||
DISABLE_MAIL_OTP_LOGIN: Boolean!
|
||||
DISABLE_TOTP_LOGIN: Boolean!
|
||||
}
|
||||
|
||||
type ValidateJWTTokenResponse {
|
||||
@@ -2551,6 +2609,8 @@ input UpdateEnvInput {
|
||||
DEFAULT_AUTHORIZE_RESPONSE_TYPE: String
|
||||
DEFAULT_AUTHORIZE_RESPONSE_MODE: String
|
||||
DISABLE_PLAYGROUND: Boolean
|
||||
DISABLE_MAIL_OTP_LOGIN: Boolean
|
||||
DISABLE_TOTP_LOGIN: Boolean
|
||||
}
|
||||
|
||||
input AdminLoginInput {
|
||||
@@ -2806,10 +2866,11 @@ input DeleteEmailTemplateRequest {
|
||||
}
|
||||
|
||||
input VerifyOTPRequest {
|
||||
# either email or phone_number is required
|
||||
# either email, phone_number or totp_token is required
|
||||
email: String
|
||||
phone_number: String
|
||||
otp: String!
|
||||
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
|
||||
@@ -3657,6 +3718,47 @@ func (ec *executionContext) fieldContext_AuthResponse_should_show_mobile_otp_scr
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _AuthResponse_should_show_totp_screen(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_AuthResponse_should_show_totp_screen(ctx, field)
|
||||
if err != nil {
|
||||
return graphql.Null
|
||||
}
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.ShouldShowTotpScreen, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*bool)
|
||||
fc.Result = res
|
||||
return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_AuthResponse_should_show_totp_screen(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "AuthResponse",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type Boolean does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _AuthResponse_access_token(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_AuthResponse_access_token(ctx, field)
|
||||
if err != nil {
|
||||
@@ -3904,6 +4006,129 @@ func (ec *executionContext) fieldContext_AuthResponse_user(ctx context.Context,
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _AuthResponse_authenticator_scanner_image(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_AuthResponse_authenticator_scanner_image(ctx, field)
|
||||
if err != nil {
|
||||
return graphql.Null
|
||||
}
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.AuthenticatorScannerImage, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*string)
|
||||
fc.Result = res
|
||||
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_AuthResponse_authenticator_scanner_image(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "AuthResponse",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type String does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _AuthResponse_authenticator_secret(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_AuthResponse_authenticator_secret(ctx, field)
|
||||
if err != nil {
|
||||
return graphql.Null
|
||||
}
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.AuthenticatorSecret, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*string)
|
||||
fc.Result = res
|
||||
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_AuthResponse_authenticator_secret(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "AuthResponse",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type String does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _AuthResponse_authenticator_recovery_codes(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_AuthResponse_authenticator_recovery_codes(ctx, field)
|
||||
if err != nil {
|
||||
return graphql.Null
|
||||
}
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.AuthenticatorRecoveryCodes, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.([]*string)
|
||||
fc.Result = res
|
||||
return ec.marshalOString2ᚕᚖstring(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_AuthResponse_authenticator_recovery_codes(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "AuthResponse",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type String does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _EmailTemplate_id(ctx context.Context, field graphql.CollectedField, obj *model.EmailTemplate) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_EmailTemplate_id(ctx, field)
|
||||
if err != nil {
|
||||
@@ -6904,6 +7129,94 @@ func (ec *executionContext) fieldContext_Env_DISABLE_PLAYGROUND(ctx context.Cont
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Env_DISABLE_MAIL_OTP_LOGIN(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_Env_DISABLE_MAIL_OTP_LOGIN(ctx, field)
|
||||
if err != nil {
|
||||
return graphql.Null
|
||||
}
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.DisableMailOtpLogin, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(bool)
|
||||
fc.Result = res
|
||||
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_Env_DISABLE_MAIL_OTP_LOGIN(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "Env",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type Boolean does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Env_DISABLE_TOTP_LOGIN(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_Env_DISABLE_TOTP_LOGIN(ctx, field)
|
||||
if err != nil {
|
||||
return graphql.Null
|
||||
}
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.DisableTotpLogin, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(bool)
|
||||
fc.Result = res
|
||||
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_Env_DISABLE_TOTP_LOGIN(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "Env",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type Boolean does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Error_message(ctx context.Context, field graphql.CollectedField, obj *model.Error) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_Error_message(ctx, field)
|
||||
if err != nil {
|
||||
@@ -7950,6 +8263,8 @@ func (ec *executionContext) fieldContext_Mutation_signup(ctx context.Context, fi
|
||||
return ec.fieldContext_AuthResponse_should_show_email_otp_screen(ctx, field)
|
||||
case "should_show_mobile_otp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_mobile_otp_screen(ctx, field)
|
||||
case "should_show_totp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_totp_screen(ctx, field)
|
||||
case "access_token":
|
||||
return ec.fieldContext_AuthResponse_access_token(ctx, field)
|
||||
case "id_token":
|
||||
@@ -7960,6 +8275,12 @@ func (ec *executionContext) fieldContext_Mutation_signup(ctx context.Context, fi
|
||||
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
|
||||
case "user":
|
||||
return ec.fieldContext_AuthResponse_user(ctx, field)
|
||||
case "authenticator_scanner_image":
|
||||
return ec.fieldContext_AuthResponse_authenticator_scanner_image(ctx, field)
|
||||
case "authenticator_secret":
|
||||
return ec.fieldContext_AuthResponse_authenticator_secret(ctx, field)
|
||||
case "authenticator_recovery_codes":
|
||||
return ec.fieldContext_AuthResponse_authenticator_recovery_codes(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
|
||||
},
|
||||
@@ -8023,6 +8344,8 @@ func (ec *executionContext) fieldContext_Mutation_mobile_signup(ctx context.Cont
|
||||
return ec.fieldContext_AuthResponse_should_show_email_otp_screen(ctx, field)
|
||||
case "should_show_mobile_otp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_mobile_otp_screen(ctx, field)
|
||||
case "should_show_totp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_totp_screen(ctx, field)
|
||||
case "access_token":
|
||||
return ec.fieldContext_AuthResponse_access_token(ctx, field)
|
||||
case "id_token":
|
||||
@@ -8033,6 +8356,12 @@ func (ec *executionContext) fieldContext_Mutation_mobile_signup(ctx context.Cont
|
||||
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
|
||||
case "user":
|
||||
return ec.fieldContext_AuthResponse_user(ctx, field)
|
||||
case "authenticator_scanner_image":
|
||||
return ec.fieldContext_AuthResponse_authenticator_scanner_image(ctx, field)
|
||||
case "authenticator_secret":
|
||||
return ec.fieldContext_AuthResponse_authenticator_secret(ctx, field)
|
||||
case "authenticator_recovery_codes":
|
||||
return ec.fieldContext_AuthResponse_authenticator_recovery_codes(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
|
||||
},
|
||||
@@ -8096,6 +8425,8 @@ func (ec *executionContext) fieldContext_Mutation_login(ctx context.Context, fie
|
||||
return ec.fieldContext_AuthResponse_should_show_email_otp_screen(ctx, field)
|
||||
case "should_show_mobile_otp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_mobile_otp_screen(ctx, field)
|
||||
case "should_show_totp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_totp_screen(ctx, field)
|
||||
case "access_token":
|
||||
return ec.fieldContext_AuthResponse_access_token(ctx, field)
|
||||
case "id_token":
|
||||
@@ -8106,6 +8437,12 @@ func (ec *executionContext) fieldContext_Mutation_login(ctx context.Context, fie
|
||||
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
|
||||
case "user":
|
||||
return ec.fieldContext_AuthResponse_user(ctx, field)
|
||||
case "authenticator_scanner_image":
|
||||
return ec.fieldContext_AuthResponse_authenticator_scanner_image(ctx, field)
|
||||
case "authenticator_secret":
|
||||
return ec.fieldContext_AuthResponse_authenticator_secret(ctx, field)
|
||||
case "authenticator_recovery_codes":
|
||||
return ec.fieldContext_AuthResponse_authenticator_recovery_codes(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
|
||||
},
|
||||
@@ -8169,6 +8506,8 @@ func (ec *executionContext) fieldContext_Mutation_mobile_login(ctx context.Conte
|
||||
return ec.fieldContext_AuthResponse_should_show_email_otp_screen(ctx, field)
|
||||
case "should_show_mobile_otp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_mobile_otp_screen(ctx, field)
|
||||
case "should_show_totp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_totp_screen(ctx, field)
|
||||
case "access_token":
|
||||
return ec.fieldContext_AuthResponse_access_token(ctx, field)
|
||||
case "id_token":
|
||||
@@ -8179,6 +8518,12 @@ func (ec *executionContext) fieldContext_Mutation_mobile_login(ctx context.Conte
|
||||
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
|
||||
case "user":
|
||||
return ec.fieldContext_AuthResponse_user(ctx, field)
|
||||
case "authenticator_scanner_image":
|
||||
return ec.fieldContext_AuthResponse_authenticator_scanner_image(ctx, field)
|
||||
case "authenticator_secret":
|
||||
return ec.fieldContext_AuthResponse_authenticator_secret(ctx, field)
|
||||
case "authenticator_recovery_codes":
|
||||
return ec.fieldContext_AuthResponse_authenticator_recovery_codes(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
|
||||
},
|
||||
@@ -8408,6 +8753,8 @@ func (ec *executionContext) fieldContext_Mutation_verify_email(ctx context.Conte
|
||||
return ec.fieldContext_AuthResponse_should_show_email_otp_screen(ctx, field)
|
||||
case "should_show_mobile_otp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_mobile_otp_screen(ctx, field)
|
||||
case "should_show_totp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_totp_screen(ctx, field)
|
||||
case "access_token":
|
||||
return ec.fieldContext_AuthResponse_access_token(ctx, field)
|
||||
case "id_token":
|
||||
@@ -8418,6 +8765,12 @@ func (ec *executionContext) fieldContext_Mutation_verify_email(ctx context.Conte
|
||||
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
|
||||
case "user":
|
||||
return ec.fieldContext_AuthResponse_user(ctx, field)
|
||||
case "authenticator_scanner_image":
|
||||
return ec.fieldContext_AuthResponse_authenticator_scanner_image(ctx, field)
|
||||
case "authenticator_secret":
|
||||
return ec.fieldContext_AuthResponse_authenticator_secret(ctx, field)
|
||||
case "authenticator_recovery_codes":
|
||||
return ec.fieldContext_AuthResponse_authenticator_recovery_codes(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
|
||||
},
|
||||
@@ -8717,6 +9070,8 @@ func (ec *executionContext) fieldContext_Mutation_verify_otp(ctx context.Context
|
||||
return ec.fieldContext_AuthResponse_should_show_email_otp_screen(ctx, field)
|
||||
case "should_show_mobile_otp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_mobile_otp_screen(ctx, field)
|
||||
case "should_show_totp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_totp_screen(ctx, field)
|
||||
case "access_token":
|
||||
return ec.fieldContext_AuthResponse_access_token(ctx, field)
|
||||
case "id_token":
|
||||
@@ -8727,6 +9082,12 @@ func (ec *executionContext) fieldContext_Mutation_verify_otp(ctx context.Context
|
||||
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
|
||||
case "user":
|
||||
return ec.fieldContext_AuthResponse_user(ctx, field)
|
||||
case "authenticator_scanner_image":
|
||||
return ec.fieldContext_AuthResponse_authenticator_scanner_image(ctx, field)
|
||||
case "authenticator_secret":
|
||||
return ec.fieldContext_AuthResponse_authenticator_secret(ctx, field)
|
||||
case "authenticator_recovery_codes":
|
||||
return ec.fieldContext_AuthResponse_authenticator_recovery_codes(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
|
||||
},
|
||||
@@ -10187,6 +10548,8 @@ func (ec *executionContext) fieldContext_Query_session(ctx context.Context, fiel
|
||||
return ec.fieldContext_AuthResponse_should_show_email_otp_screen(ctx, field)
|
||||
case "should_show_mobile_otp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_mobile_otp_screen(ctx, field)
|
||||
case "should_show_totp_screen":
|
||||
return ec.fieldContext_AuthResponse_should_show_totp_screen(ctx, field)
|
||||
case "access_token":
|
||||
return ec.fieldContext_AuthResponse_access_token(ctx, field)
|
||||
case "id_token":
|
||||
@@ -10197,6 +10560,12 @@ func (ec *executionContext) fieldContext_Query_session(ctx context.Context, fiel
|
||||
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
|
||||
case "user":
|
||||
return ec.fieldContext_AuthResponse_user(ctx, field)
|
||||
case "authenticator_scanner_image":
|
||||
return ec.fieldContext_AuthResponse_authenticator_scanner_image(ctx, field)
|
||||
case "authenticator_secret":
|
||||
return ec.fieldContext_AuthResponse_authenticator_secret(ctx, field)
|
||||
case "authenticator_recovery_codes":
|
||||
return ec.fieldContext_AuthResponse_authenticator_recovery_codes(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
|
||||
},
|
||||
@@ -10853,6 +11222,10 @@ func (ec *executionContext) fieldContext_Query__env(ctx context.Context, field g
|
||||
return ec.fieldContext_Env_DEFAULT_AUTHORIZE_RESPONSE_MODE(ctx, field)
|
||||
case "DISABLE_PLAYGROUND":
|
||||
return ec.fieldContext_Env_DISABLE_PLAYGROUND(ctx, field)
|
||||
case "DISABLE_MAIL_OTP_LOGIN":
|
||||
return ec.fieldContext_Env_DISABLE_MAIL_OTP_LOGIN(ctx, field)
|
||||
case "DISABLE_TOTP_LOGIN":
|
||||
return ec.fieldContext_Env_DISABLE_TOTP_LOGIN(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type Env", field.Name)
|
||||
},
|
||||
@@ -17342,7 +17715,7 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob
|
||||
asMap[k] = v
|
||||
}
|
||||
|
||||
fieldsInOrder := [...]string{"ACCESS_TOKEN_EXPIRY_TIME", "ADMIN_SECRET", "CUSTOM_ACCESS_TOKEN_SCRIPT", "OLD_ADMIN_SECRET", "SMTP_HOST", "SMTP_PORT", "SMTP_USERNAME", "SMTP_PASSWORD", "SMTP_LOCAL_NAME", "SENDER_EMAIL", "SENDER_NAME", "JWT_TYPE", "JWT_SECRET", "JWT_PRIVATE_KEY", "JWT_PUBLIC_KEY", "ALLOWED_ORIGINS", "APP_URL", "RESET_PASSWORD_URL", "APP_COOKIE_SECURE", "ADMIN_COOKIE_SECURE", "DISABLE_EMAIL_VERIFICATION", "DISABLE_BASIC_AUTHENTICATION", "DISABLE_MAGIC_LINK_LOGIN", "DISABLE_LOGIN_PAGE", "DISABLE_SIGN_UP", "DISABLE_REDIS_FOR_ENV", "DISABLE_STRONG_PASSWORD", "DISABLE_MULTI_FACTOR_AUTHENTICATION", "ENFORCE_MULTI_FACTOR_AUTHENTICATION", "ROLES", "PROTECTED_ROLES", "DEFAULT_ROLES", "JWT_ROLE_CLAIM", "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET", "FACEBOOK_CLIENT_ID", "FACEBOOK_CLIENT_SECRET", "LINKEDIN_CLIENT_ID", "LINKEDIN_CLIENT_SECRET", "APPLE_CLIENT_ID", "APPLE_CLIENT_SECRET", "TWITTER_CLIENT_ID", "TWITTER_CLIENT_SECRET", "MICROSOFT_CLIENT_ID", "MICROSOFT_CLIENT_SECRET", "MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID", "ORGANIZATION_NAME", "ORGANIZATION_LOGO", "DEFAULT_AUTHORIZE_RESPONSE_TYPE", "DEFAULT_AUTHORIZE_RESPONSE_MODE", "DISABLE_PLAYGROUND"}
|
||||
fieldsInOrder := [...]string{"ACCESS_TOKEN_EXPIRY_TIME", "ADMIN_SECRET", "CUSTOM_ACCESS_TOKEN_SCRIPT", "OLD_ADMIN_SECRET", "SMTP_HOST", "SMTP_PORT", "SMTP_USERNAME", "SMTP_PASSWORD", "SMTP_LOCAL_NAME", "SENDER_EMAIL", "SENDER_NAME", "JWT_TYPE", "JWT_SECRET", "JWT_PRIVATE_KEY", "JWT_PUBLIC_KEY", "ALLOWED_ORIGINS", "APP_URL", "RESET_PASSWORD_URL", "APP_COOKIE_SECURE", "ADMIN_COOKIE_SECURE", "DISABLE_EMAIL_VERIFICATION", "DISABLE_BASIC_AUTHENTICATION", "DISABLE_MAGIC_LINK_LOGIN", "DISABLE_LOGIN_PAGE", "DISABLE_SIGN_UP", "DISABLE_REDIS_FOR_ENV", "DISABLE_STRONG_PASSWORD", "DISABLE_MULTI_FACTOR_AUTHENTICATION", "ENFORCE_MULTI_FACTOR_AUTHENTICATION", "ROLES", "PROTECTED_ROLES", "DEFAULT_ROLES", "JWT_ROLE_CLAIM", "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET", "FACEBOOK_CLIENT_ID", "FACEBOOK_CLIENT_SECRET", "LINKEDIN_CLIENT_ID", "LINKEDIN_CLIENT_SECRET", "APPLE_CLIENT_ID", "APPLE_CLIENT_SECRET", "TWITTER_CLIENT_ID", "TWITTER_CLIENT_SECRET", "MICROSOFT_CLIENT_ID", "MICROSOFT_CLIENT_SECRET", "MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID", "ORGANIZATION_NAME", "ORGANIZATION_LOGO", "DEFAULT_AUTHORIZE_RESPONSE_TYPE", "DEFAULT_AUTHORIZE_RESPONSE_MODE", "DISABLE_PLAYGROUND", "DISABLE_MAIL_OTP_LOGIN", "DISABLE_TOTP_LOGIN"}
|
||||
for _, k := range fieldsInOrder {
|
||||
v, ok := asMap[k]
|
||||
if !ok {
|
||||
@@ -17826,6 +18199,24 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob
|
||||
return it, err
|
||||
}
|
||||
it.DisablePlayground = data
|
||||
case "DISABLE_MAIL_OTP_LOGIN":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("DISABLE_MAIL_OTP_LOGIN"))
|
||||
data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
it.DisableMailOtpLogin = data
|
||||
case "DISABLE_TOTP_LOGIN":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("DISABLE_TOTP_LOGIN"))
|
||||
data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
it.DisableTotpLogin = data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18328,7 +18719,7 @@ func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context,
|
||||
asMap[k] = v
|
||||
}
|
||||
|
||||
fieldsInOrder := [...]string{"email", "phone_number", "otp", "state"}
|
||||
fieldsInOrder := [...]string{"email", "phone_number", "otp", "totp", "state"}
|
||||
for _, k := range fieldsInOrder {
|
||||
v, ok := asMap[k]
|
||||
if !ok {
|
||||
@@ -18362,6 +18753,15 @@ func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context,
|
||||
return it, err
|
||||
}
|
||||
it.Otp = data
|
||||
case "totp":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("totp"))
|
||||
data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
it.Totp = data
|
||||
case "state":
|
||||
var err error
|
||||
|
||||
@@ -18434,6 +18834,8 @@ func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.Selection
|
||||
out.Values[i] = ec._AuthResponse_should_show_email_otp_screen(ctx, field, obj)
|
||||
case "should_show_mobile_otp_screen":
|
||||
out.Values[i] = ec._AuthResponse_should_show_mobile_otp_screen(ctx, field, obj)
|
||||
case "should_show_totp_screen":
|
||||
out.Values[i] = ec._AuthResponse_should_show_totp_screen(ctx, field, obj)
|
||||
case "access_token":
|
||||
out.Values[i] = ec._AuthResponse_access_token(ctx, field, obj)
|
||||
case "id_token":
|
||||
@@ -18444,6 +18846,12 @@ func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.Selection
|
||||
out.Values[i] = ec._AuthResponse_expires_in(ctx, field, obj)
|
||||
case "user":
|
||||
out.Values[i] = ec._AuthResponse_user(ctx, field, obj)
|
||||
case "authenticator_scanner_image":
|
||||
out.Values[i] = ec._AuthResponse_authenticator_scanner_image(ctx, field, obj)
|
||||
case "authenticator_secret":
|
||||
out.Values[i] = ec._AuthResponse_authenticator_secret(ctx, field, obj)
|
||||
case "authenticator_recovery_codes":
|
||||
out.Values[i] = ec._AuthResponse_authenticator_recovery_codes(ctx, field, obj)
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
@@ -18751,6 +19159,16 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "DISABLE_MAIL_OTP_LOGIN":
|
||||
out.Values[i] = ec._Env_DISABLE_MAIL_OTP_LOGIN(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "DISABLE_TOTP_LOGIN":
|
||||
out.Values[i] = ec._Env_DISABLE_TOTP_LOGIN(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
|
@@ -26,14 +26,18 @@ type AdminSignupInput struct {
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
Message string `json:"message"`
|
||||
ShouldShowEmailOtpScreen *bool `json:"should_show_email_otp_screen,omitempty"`
|
||||
ShouldShowMobileOtpScreen *bool `json:"should_show_mobile_otp_screen,omitempty"`
|
||||
AccessToken *string `json:"access_token,omitempty"`
|
||||
IDToken *string `json:"id_token,omitempty"`
|
||||
RefreshToken *string `json:"refresh_token,omitempty"`
|
||||
ExpiresIn *int64 `json:"expires_in,omitempty"`
|
||||
User *User `json:"user,omitempty"`
|
||||
Message string `json:"message"`
|
||||
ShouldShowEmailOtpScreen *bool `json:"should_show_email_otp_screen,omitempty"`
|
||||
ShouldShowMobileOtpScreen *bool `json:"should_show_mobile_otp_screen,omitempty"`
|
||||
ShouldShowTotpScreen *bool `json:"should_show_totp_screen,omitempty"`
|
||||
AccessToken *string `json:"access_token,omitempty"`
|
||||
IDToken *string `json:"id_token,omitempty"`
|
||||
RefreshToken *string `json:"refresh_token,omitempty"`
|
||||
ExpiresIn *int64 `json:"expires_in,omitempty"`
|
||||
User *User `json:"user,omitempty"`
|
||||
AuthenticatorScannerImage *string `json:"authenticator_scanner_image,omitempty"`
|
||||
AuthenticatorSecret *string `json:"authenticator_secret,omitempty"`
|
||||
AuthenticatorRecoveryCodes []*string `json:"authenticator_recovery_codes,omitempty"`
|
||||
}
|
||||
|
||||
type DeleteEmailTemplateRequest struct {
|
||||
@@ -122,6 +126,8 @@ type Env struct {
|
||||
DefaultAuthorizeResponseType *string `json:"DEFAULT_AUTHORIZE_RESPONSE_TYPE,omitempty"`
|
||||
DefaultAuthorizeResponseMode *string `json:"DEFAULT_AUTHORIZE_RESPONSE_MODE,omitempty"`
|
||||
DisablePlayground bool `json:"DISABLE_PLAYGROUND"`
|
||||
DisableMailOtpLogin bool `json:"DISABLE_MAIL_OTP_LOGIN"`
|
||||
DisableTotpLogin bool `json:"DISABLE_TOTP_LOGIN"`
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
@@ -382,6 +388,8 @@ type UpdateEnvInput struct {
|
||||
DefaultAuthorizeResponseType *string `json:"DEFAULT_AUTHORIZE_RESPONSE_TYPE,omitempty"`
|
||||
DefaultAuthorizeResponseMode *string `json:"DEFAULT_AUTHORIZE_RESPONSE_MODE,omitempty"`
|
||||
DisablePlayground *bool `json:"DISABLE_PLAYGROUND,omitempty"`
|
||||
DisableMailOtpLogin *bool `json:"DISABLE_MAIL_OTP_LOGIN,omitempty"`
|
||||
DisableTotpLogin *bool `json:"DISABLE_TOTP_LOGIN,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateProfileInput struct {
|
||||
@@ -502,6 +510,7 @@ type VerifyOTPRequest struct {
|
||||
Email *string `json:"email,omitempty"`
|
||||
PhoneNumber *string `json:"phone_number,omitempty"`
|
||||
Otp string `json:"otp"`
|
||||
Totp *bool `json:"totp,omitempty"`
|
||||
State *string `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
|
@@ -95,11 +95,19 @@ type AuthResponse {
|
||||
message: String!
|
||||
should_show_email_otp_screen: Boolean
|
||||
should_show_mobile_otp_screen: Boolean
|
||||
should_show_totp_screen: Boolean
|
||||
access_token: String
|
||||
id_token: String
|
||||
refresh_token: String
|
||||
expires_in: Int64
|
||||
user: User
|
||||
# key for totp login
|
||||
# it is a base64 image url
|
||||
authenticator_scanner_image: String
|
||||
# string which can be used instead of scanner image
|
||||
authenticator_secret: String
|
||||
# recovery codes for totp login shared with user only once
|
||||
authenticator_recovery_codes: [String]
|
||||
}
|
||||
|
||||
type Response {
|
||||
@@ -174,6 +182,8 @@ type Env {
|
||||
DEFAULT_AUTHORIZE_RESPONSE_TYPE: String
|
||||
DEFAULT_AUTHORIZE_RESPONSE_MODE: String
|
||||
DISABLE_PLAYGROUND: Boolean!
|
||||
DISABLE_MAIL_OTP_LOGIN: Boolean!
|
||||
DISABLE_TOTP_LOGIN: Boolean!
|
||||
}
|
||||
|
||||
type ValidateJWTTokenResponse {
|
||||
@@ -297,6 +307,8 @@ input UpdateEnvInput {
|
||||
DEFAULT_AUTHORIZE_RESPONSE_TYPE: String
|
||||
DEFAULT_AUTHORIZE_RESPONSE_MODE: String
|
||||
DISABLE_PLAYGROUND: Boolean
|
||||
DISABLE_MAIL_OTP_LOGIN: Boolean
|
||||
DISABLE_TOTP_LOGIN: Boolean
|
||||
}
|
||||
|
||||
input AdminLoginInput {
|
||||
@@ -552,10 +564,11 @@ input DeleteEmailTemplateRequest {
|
||||
}
|
||||
|
||||
input VerifyOTPRequest {
|
||||
# either email or phone_number is required
|
||||
# either email, phone_number or totp_token is required
|
||||
email: String
|
||||
phone_number: String
|
||||
otp: String!
|
||||
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
|
||||
|
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/authorizerdev/authorizer/server/authenticators"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/cli"
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
@@ -70,6 +71,11 @@ func main() {
|
||||
log.Fatalln("Error while initializing oauth: ", err)
|
||||
}
|
||||
|
||||
err = authenticators.InitTOTPStore()
|
||||
if err != nil {
|
||||
log.Fatalln("Error while initializing authenticator: ", err)
|
||||
}
|
||||
|
||||
router := routes.InitRouter(log)
|
||||
log.Info("Starting Authorizer: ", VERSION)
|
||||
port, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyPort)
|
||||
|
@@ -36,9 +36,11 @@ func InitMemStore() error {
|
||||
constants.EnvKeyIsSMSServiceEnabled: false,
|
||||
constants.EnvKeyEnforceMultiFactorAuthentication: false,
|
||||
constants.EnvKeyDisableMultiFactorAuthentication: false,
|
||||
constants.EnvKeyDisableTOTPLogin: false,
|
||||
constants.EnvKeyAppCookieSecure: true,
|
||||
constants.EnvKeyAdminCookieSecure: true,
|
||||
constants.EnvKeyDisablePlayGround: true,
|
||||
constants.EnvKeyDisableMailOTPLogin: true,
|
||||
}
|
||||
|
||||
requiredEnvs := RequiredEnvStoreObj.GetRequiredEnv()
|
||||
|
@@ -176,7 +176,7 @@ func (c *provider) GetEnvStore() (map[string]interface{}, error) {
|
||||
return nil, err
|
||||
}
|
||||
for key, value := range data {
|
||||
if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableMobileBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword || key == constants.EnvKeyIsEmailServiceEnabled || key == constants.EnvKeyIsSMSServiceEnabled || key == constants.EnvKeyEnforceMultiFactorAuthentication || key == constants.EnvKeyDisableMultiFactorAuthentication || key == constants.EnvKeyAppCookieSecure || key == constants.EnvKeyAdminCookieSecure || key == constants.EnvKeyDisablePlayGround {
|
||||
if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableMobileBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword || key == constants.EnvKeyIsEmailServiceEnabled || key == constants.EnvKeyIsSMSServiceEnabled || key == constants.EnvKeyEnforceMultiFactorAuthentication || key == constants.EnvKeyDisableMultiFactorAuthentication || key == constants.EnvKeyAppCookieSecure || key == constants.EnvKeyAdminCookieSecure || key == constants.EnvKeyDisablePlayGround || key == constants.EnvKeyDisableTOTPLogin || key == constants.EnvKeyDisableMailOTPLogin {
|
||||
boolValue, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return res, err
|
||||
|
@@ -38,7 +38,7 @@ type RequiredEnv struct {
|
||||
CouchbaseBucketRAMQuotaMB string `json:"COUCHBASE_BUCKET_RAM_QUOTA"`
|
||||
}
|
||||
|
||||
// RequiredEnvObj is a simple in-memory store for sessions.
|
||||
// RequiredEnvStore is a simple in-memory store for sessions.
|
||||
type RequiredEnvStore struct {
|
||||
mutex sync.Mutex
|
||||
requiredEnv RequiredEnv
|
||||
|
@@ -203,6 +203,8 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
|
||||
res.AdminCookieSecure = store[constants.EnvKeyAdminCookieSecure].(bool)
|
||||
res.AppCookieSecure = store[constants.EnvKeyAppCookieSecure].(bool)
|
||||
res.DisablePlayground = store[constants.EnvKeyDisablePlayGround].(bool)
|
||||
res.DisableMailOtpLogin = store[constants.EnvKeyDisableMailOTPLogin].(bool)
|
||||
res.DisableTotpLogin = store[constants.EnvKeyDisableTOTPLogin].(bool)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
@@ -7,14 +7,15 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/authenticators"
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/cookie"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
mailService "github.com/authorizerdev/authorizer/server/email"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/memorystore"
|
||||
"github.com/authorizerdev/authorizer/server/refs"
|
||||
@@ -22,6 +23,8 @@ import (
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
"github.com/authorizerdev/authorizer/server/validators"
|
||||
|
||||
mailService "github.com/authorizerdev/authorizer/server/email"
|
||||
)
|
||||
|
||||
// LoginResolver is a resolver for login mutation
|
||||
@@ -141,52 +144,134 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
|
||||
log.Debug("MFA service not enabled: ", err)
|
||||
}
|
||||
|
||||
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isMFADisabled {
|
||||
isTOTPLoginDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableTOTPLogin)
|
||||
if err != nil || !isTOTPLoginDisabled {
|
||||
log.Debug("totp service not enabled: ", err)
|
||||
}
|
||||
|
||||
isMailOTPDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMailOTPLogin)
|
||||
if err != nil || !isMailOTPDisabled {
|
||||
log.Debug("mail OTP service not enabled: ", err)
|
||||
}
|
||||
|
||||
isSMSOTPDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisablePhoneVerification)
|
||||
if err != nil || !isSMSOTPDisabled {
|
||||
log.Debug("sms OTP service not enabled: ", err)
|
||||
}
|
||||
setOTPMFaSession := func(expiresAt int64) error {
|
||||
mfaSession := uuid.NewString()
|
||||
err = memorystore.Provider.SetMfaSession(user.ID, mfaSession, expiresAt)
|
||||
if err != nil {
|
||||
log.Debug("Failed to add mfasession: ", err)
|
||||
return err
|
||||
}
|
||||
cookie.SetMfaSession(gc, mfaSession)
|
||||
return nil
|
||||
}
|
||||
// If multi factor authentication is enabled and we need to generate OTP for mail / sms based MFA
|
||||
generateOTP := func(expiresAt int64) (*models.OTP, error) {
|
||||
otp := utils.GenerateOTP()
|
||||
expires := time.Now().Add(1 * time.Minute).Unix()
|
||||
otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{
|
||||
Email: refs.StringValue(user.Email),
|
||||
Otp: otp,
|
||||
ExpiresAt: expires,
|
||||
ExpiresAt: expiresAt,
|
||||
})
|
||||
if err != nil {
|
||||
log.Debug("Failed to add otp: ", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mfaSession := uuid.NewString()
|
||||
err = memorystore.Provider.SetMfaSession(user.ID, mfaSession, expires)
|
||||
if err != nil {
|
||||
log.Debug("Failed to add mfasession: ", err)
|
||||
return otpData, nil
|
||||
}
|
||||
// If mfa enabled and also totp enabled
|
||||
// first priority is given to totp
|
||||
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isMFADisabled && !isTOTPLoginDisabled {
|
||||
expiresAt := time.Now().Add(3 * time.Minute).Unix()
|
||||
if err := setOTPMFaSession(expiresAt); err != nil {
|
||||
log.Debug("Failed to set mfa session: ", err)
|
||||
return nil, err
|
||||
}
|
||||
cookie.SetMfaSession(gc, mfaSession)
|
||||
if isEmailServiceEnabled && isEmailLogin {
|
||||
go func() {
|
||||
// exec it as go routine so that we can reduce the api latency
|
||||
if err := mailService.SendEmail([]string{email}, constants.VerificationTypeOTP, map[string]interface{}{
|
||||
"user": user.ToMap(),
|
||||
"organization": utils.GetOrganization(),
|
||||
"otp": otpData.Otp,
|
||||
}); err != nil {
|
||||
log.Debug("Failed to send otp email: ", err)
|
||||
}
|
||||
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodBasicAuth, user)
|
||||
}()
|
||||
} else if isSMSServiceEnabled && isMobileLogin {
|
||||
authenticator, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, user.ID, constants.EnvKeyTOTPAuthenticator)
|
||||
// Check if it's the first time user or if their TOTP is not verified
|
||||
if err != nil || ((authenticator == nil) || (authenticator != nil && authenticator.VerifiedAt == nil)) {
|
||||
// Generate a base64 URL and initiate the registration for TOTP
|
||||
authConfig, err := authenticators.Provider.Generate(ctx, user.ID)
|
||||
if err != nil {
|
||||
log.Debug("error while generating base64 url: ", err)
|
||||
return nil, err
|
||||
}
|
||||
recoveryCodes := []*string{}
|
||||
for _, code := range authConfig.RecoveryCodes {
|
||||
recoveryCodes = append(recoveryCodes, refs.NewStringRef(code))
|
||||
}
|
||||
// when user is first time registering for totp
|
||||
res = &model.AuthResponse{
|
||||
Message: `Proceed to totp verification screen`,
|
||||
ShouldShowTotpScreen: refs.NewBoolRef(true),
|
||||
AuthenticatorScannerImage: refs.NewStringRef(authConfig.ScannerImage),
|
||||
AuthenticatorSecret: refs.NewStringRef(authConfig.Secret),
|
||||
AuthenticatorRecoveryCodes: recoveryCodes,
|
||||
}
|
||||
return res, nil
|
||||
} else {
|
||||
//when user is already register for totp
|
||||
res = &model.AuthResponse{
|
||||
Message: `Proceed to totp screen`,
|
||||
ShouldShowTotpScreen: refs.NewBoolRef(true),
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
// If multi factor authentication is enabled and is email based login and email otp is enabled
|
||||
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isMFADisabled && !isMailOTPDisabled && isEmailServiceEnabled && isEmailLogin {
|
||||
expiresAt := time.Now().Add(1 * time.Minute).Unix()
|
||||
otpData, err := generateOTP(expiresAt)
|
||||
go func() {
|
||||
if err != nil {
|
||||
log.Debug("Failed to generate otp: ", err)
|
||||
return
|
||||
}
|
||||
if err := setOTPMFaSession(expiresAt); err != nil {
|
||||
log.Debug("Failed to set mfa session: ", err)
|
||||
return
|
||||
}
|
||||
// exec it as go routine so that we can reduce the api latency
|
||||
if err := mailService.SendEmail([]string{email}, constants.VerificationTypeOTP, map[string]interface{}{
|
||||
"user": user.ToMap(),
|
||||
"organization": utils.GetOrganization(),
|
||||
"otp": otpData.Otp,
|
||||
}); err != nil {
|
||||
log.Debug("Failed to send otp email: ", err)
|
||||
}
|
||||
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodBasicAuth, user)
|
||||
}()
|
||||
return &model.AuthResponse{
|
||||
Message: "Please check email inbox for the OTP",
|
||||
ShouldShowEmailOtpScreen: refs.NewBoolRef(isMobileLogin),
|
||||
}, nil
|
||||
}
|
||||
// If multi factor authentication is enabled and is sms based login and sms otp is enabled
|
||||
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isMFADisabled && !isSMSOTPDisabled && isSMSServiceEnabled && isMobileLogin {
|
||||
expiresAt := time.Now().Add(1 * time.Minute).Unix()
|
||||
otpData, err := generateOTP(expiresAt)
|
||||
go func() {
|
||||
if err != nil {
|
||||
log.Debug("Failed to generate otp: ", err)
|
||||
return
|
||||
}
|
||||
if err := setOTPMFaSession(expiresAt); err != nil {
|
||||
log.Debug("Failed to set mfa session: ", err)
|
||||
return
|
||||
}
|
||||
smsBody := strings.Builder{}
|
||||
smsBody.WriteString("Your verification code is: ")
|
||||
smsBody.WriteString(otpData.Otp)
|
||||
go func() {
|
||||
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodMobileBasicAuth, user)
|
||||
if err := smsproviders.SendSMS(phoneNumber, smsBody.String()); err != nil {
|
||||
log.Debug("Failed to send sms: ", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodMobileBasicAuth, user)
|
||||
if err := smsproviders.SendSMS(phoneNumber, smsBody.String()); err != nil {
|
||||
log.Debug("Failed to send sms: ", err)
|
||||
}
|
||||
}()
|
||||
return &model.AuthResponse{
|
||||
Message: "Please check the OTP in",
|
||||
ShouldShowEmailOtpScreen: refs.NewBoolRef(isEmailLogin),
|
||||
Message: "Please check text message for the OTP",
|
||||
ShouldShowMobileOtpScreen: refs.NewBoolRef(isMobileLogin),
|
||||
}, nil
|
||||
}
|
||||
|
@@ -253,13 +253,15 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
|
||||
// in case SMTP is off but env is set to true
|
||||
if updatedData[constants.EnvKeySmtpHost] == "" || updatedData[constants.EnvKeySmtpUsername] == "" || updatedData[constants.EnvKeySmtpPassword] == "" || updatedData[constants.EnvKeySenderEmail] == "" && updatedData[constants.EnvKeySmtpPort] == "" {
|
||||
updatedData[constants.EnvKeyIsEmailServiceEnabled] = false
|
||||
updatedData[constants.EnvKeyDisableMultiFactorAuthentication] = true
|
||||
if !updatedData[constants.EnvKeyDisableEmailVerification].(bool) {
|
||||
updatedData[constants.EnvKeyDisableEmailVerification] = true
|
||||
}
|
||||
|
||||
if !updatedData[constants.EnvKeyDisableMailOTPLogin].(bool) {
|
||||
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
|
||||
}
|
||||
if !updatedData[constants.EnvKeyDisableMagicLinkLogin].(bool) {
|
||||
updatedData[constants.EnvKeyDisableMagicLinkLogin] = true
|
||||
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
|
||||
updatedData[constants.EnvKeyDisableTOTPLogin] = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,6 +276,21 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
|
||||
}
|
||||
}
|
||||
|
||||
if updatedData[constants.EnvKeyDisableMultiFactorAuthentication].(bool) {
|
||||
updatedData[constants.EnvKeyDisableTOTPLogin] = true
|
||||
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
|
||||
} else {
|
||||
if !updatedData[constants.EnvKeyDisableMailOTPLogin].(bool) && !updatedData[constants.EnvKeyDisableTOTPLogin].(bool) {
|
||||
errors.New("can't enable both mfa methods at same time")
|
||||
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
|
||||
updatedData[constants.EnvKeyDisableTOTPLogin] = false
|
||||
} else if updatedData[constants.EnvKeyDisableMailOTPLogin].(bool) && updatedData[constants.EnvKeyDisableTOTPLogin].(bool) {
|
||||
errors.New("can't disable both mfa methods at same time")
|
||||
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
|
||||
updatedData[constants.EnvKeyDisableTOTPLogin] = false
|
||||
}
|
||||
}
|
||||
|
||||
if !currentData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && updatedData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && !updatedData[constants.EnvKeyDisableMultiFactorAuthentication].(bool) {
|
||||
go db.Provider.UpdateUsers(ctx, map[string]interface{}{
|
||||
"is_multi_factor_auth_enabled": true,
|
||||
|
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
@@ -23,7 +25,6 @@ import (
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
"github.com/authorizerdev/authorizer/server/validators"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// UpdateProfileResolver is resolver for update profile mutation
|
||||
@@ -101,13 +102,28 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
|
||||
appDataString = string(appDataBytes)
|
||||
user.AppData = &appDataString
|
||||
}
|
||||
// Check if the user is trying to enable or disable multi-factor authentication (MFA)
|
||||
if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) {
|
||||
if refs.BoolValue(params.IsMultiFactorAuthEnabled) {
|
||||
isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
|
||||
if err != nil || !isEnvServiceEnabled {
|
||||
log.Debug("Email service not enabled:")
|
||||
return nil, errors.New("email service not enabled, so cannot enable multi factor authentication")
|
||||
}
|
||||
// Check if totp, email or sms is enabled
|
||||
isMailOTPEnvServiceDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMailOTPLogin)
|
||||
if err != nil {
|
||||
log.Debug("Error getting mail otp disabled: ", err)
|
||||
isMailOTPEnvServiceDisabled = false
|
||||
}
|
||||
isTOTPEnvServiceDisabled, _ := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableTOTPLogin)
|
||||
if err != nil {
|
||||
log.Debug("Error getting totp disabled: ", err)
|
||||
isTOTPEnvServiceDisabled = false
|
||||
}
|
||||
isSMSOTPEnvServiceDisabled, _ := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisablePhoneVerification)
|
||||
if err != nil {
|
||||
log.Debug("Error getting sms otp disabled: ", err)
|
||||
isSMSOTPEnvServiceDisabled = false
|
||||
}
|
||||
// Initialize a flag to check if enabling Mail OTP is required
|
||||
if isMailOTPEnvServiceDisabled && isTOTPEnvServiceDisabled && isSMSOTPEnvServiceDisabled {
|
||||
log.Debug("Cannot enable mfa service as all mfa services are disabled")
|
||||
return nil, errors.New("cannot enable multi factor authentication as all mfa services are disabled")
|
||||
}
|
||||
|
||||
isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication)
|
||||
|
@@ -110,10 +110,26 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
||||
if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) {
|
||||
user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled
|
||||
if refs.BoolValue(params.IsMultiFactorAuthEnabled) {
|
||||
isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
|
||||
if err != nil || !isEnvServiceEnabled {
|
||||
log.Debug("Email service not enabled:")
|
||||
return nil, errors.New("email service not enabled, so cannot enable multi factor authentication")
|
||||
// Check if totp, email or sms is enabled
|
||||
isMailOTPEnvServiceDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMailOTPLogin)
|
||||
if err != nil {
|
||||
log.Debug("Error getting mail otp disabled: ", err)
|
||||
isMailOTPEnvServiceDisabled = false
|
||||
}
|
||||
isTOTPEnvServiceDisabled, _ := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableTOTPLogin)
|
||||
if err != nil {
|
||||
log.Debug("Error getting totp disabled: ", err)
|
||||
isTOTPEnvServiceDisabled = false
|
||||
}
|
||||
isSMSOTPEnvServiceDisabled, _ := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisablePhoneVerification)
|
||||
if err != nil {
|
||||
log.Debug("Error getting sms otp disabled: ", err)
|
||||
isSMSOTPEnvServiceDisabled = false
|
||||
}
|
||||
// Initialize a flag to check if enabling Mail OTP is required
|
||||
if isMailOTPEnvServiceDisabled && isTOTPEnvServiceDisabled && isSMSOTPEnvServiceDisabled {
|
||||
log.Debug("Cannot enable mfa service as all mfa services are disabled")
|
||||
return nil, errors.New("cannot enable multi factor authentication as all mfa services are disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/authenticators"
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/cookie"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
@@ -15,8 +19,6 @@ import (
|
||||
"github.com/authorizerdev/authorizer/server/refs"
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// VerifyOtpResolver resolver for verify otp mutation
|
||||
@@ -38,30 +40,11 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
|
||||
log.Debug("Email or phone number is required")
|
||||
return res, fmt.Errorf(`email or phone_number is required`)
|
||||
}
|
||||
|
||||
currentField := models.FieldNameEmail
|
||||
if refs.StringValue(params.Email) == "" {
|
||||
currentField = models.FieldNamePhoneNumber
|
||||
}
|
||||
var otp *models.OTP
|
||||
if currentField == models.FieldNameEmail {
|
||||
otp, err = db.Provider.GetOTPByEmail(ctx, refs.StringValue(params.Email))
|
||||
} else {
|
||||
otp, err = db.Provider.GetOTPByPhoneNumber(ctx, refs.StringValue(params.PhoneNumber))
|
||||
}
|
||||
if otp == nil && err != nil {
|
||||
log.Debugf("Failed to get otp request for %s: %s", currentField, err.Error())
|
||||
return res, fmt.Errorf(`invalid %s: %s`, currentField, err.Error())
|
||||
}
|
||||
if params.Otp != otp.Otp {
|
||||
log.Debug("Failed to verify otp request: Incorrect value")
|
||||
return res, fmt.Errorf(`invalid otp`)
|
||||
}
|
||||
expiresIn := otp.ExpiresAt - time.Now().Unix()
|
||||
if expiresIn < 0 {
|
||||
log.Debug("Failed to verify otp request: Timeout")
|
||||
return res, fmt.Errorf("otp expired")
|
||||
}
|
||||
// Get user by email or phone number
|
||||
var user *models.User
|
||||
if currentField == models.FieldNameEmail {
|
||||
user, err = db.Provider.GetUserByEmail(ctx, refs.StringValue(params.Email))
|
||||
@@ -72,6 +55,35 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
|
||||
log.Debug("Failed to get user by email or phone number: ", err)
|
||||
return res, err
|
||||
}
|
||||
// Verify OTP based on TOPT or OTP
|
||||
if refs.BoolValue(params.Totp) {
|
||||
status, err := authenticators.Provider.Validate(ctx, params.Otp, user.ID)
|
||||
if err != nil || !status {
|
||||
log.Debug("Failed to validate totp: ", err)
|
||||
return nil, fmt.Errorf("error while validating passcode")
|
||||
}
|
||||
} else {
|
||||
var otp *models.OTP
|
||||
if currentField == models.FieldNameEmail {
|
||||
otp, err = db.Provider.GetOTPByEmail(ctx, refs.StringValue(params.Email))
|
||||
} else {
|
||||
otp, err = db.Provider.GetOTPByPhoneNumber(ctx, refs.StringValue(params.PhoneNumber))
|
||||
}
|
||||
if otp == nil && err != nil {
|
||||
log.Debugf("Failed to get otp request for %s: %s", currentField, err.Error())
|
||||
return res, fmt.Errorf(`invalid %s: %s`, currentField, err.Error())
|
||||
}
|
||||
if params.Otp != otp.Otp {
|
||||
log.Debug("Failed to verify otp request: Incorrect value")
|
||||
return res, fmt.Errorf(`invalid otp`)
|
||||
}
|
||||
expiresIn := otp.ExpiresAt - time.Now().Unix()
|
||||
if expiresIn < 0 {
|
||||
log.Debug("Failed to verify otp request: Timeout")
|
||||
return res, fmt.Errorf("otp expired")
|
||||
}
|
||||
db.Provider.DeleteOTP(gc, otp)
|
||||
}
|
||||
|
||||
if _, err := memorystore.Provider.GetMfaSession(user.ID, mfaSession); err != nil {
|
||||
log.Debug("Failed to get mfa session: ", err)
|
||||
@@ -121,7 +133,6 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
|
||||
}
|
||||
|
||||
go func() {
|
||||
db.Provider.DeleteOTP(gc, otp)
|
||||
if isSignUp {
|
||||
utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user)
|
||||
// User is also logged in with signup
|
||||
|
@@ -106,10 +106,10 @@ func TestResolvers(t *testing.T) {
|
||||
updateWebhookTest(t, s)
|
||||
webhookTest(t, s)
|
||||
webhooksTest(t, s)
|
||||
usersTest(t, s)
|
||||
//usersTest(t, s)
|
||||
userTest(t, s)
|
||||
deleteUserTest(t, s)
|
||||
updateUserTest(t, s)
|
||||
//updateUserTest(t, s)
|
||||
adminLoginTests(t, s)
|
||||
adminLogoutTests(t, s)
|
||||
adminSessionTests(t, s)
|
||||
@@ -128,6 +128,7 @@ func TestResolvers(t *testing.T) {
|
||||
signupTests(t, s)
|
||||
mobileSingupTest(t, s)
|
||||
mobileLoginTests(t, s)
|
||||
totpLoginTest(t, s)
|
||||
forgotPasswordTest(t, s)
|
||||
resendVerifyEmailTests(t, s)
|
||||
resetPasswordTest(t, s)
|
||||
|
@@ -54,6 +54,9 @@ func resendOTPTest(t *testing.T, s TestSetup) {
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, updateRes)
|
||||
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableMailOTPLogin, false)
|
||||
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableTOTPLogin, true)
|
||||
|
||||
// Resend otp should return error as no initial opt is being sent
|
||||
resendOtpRes, err := resolvers.ResendOTPResolver(ctx, model.ResendOTPRequest{
|
||||
Email: refs.NewStringRef(email),
|
||||
|
159
server/test/totp_login_test.go
Normal file
159
server/test/totp_login_test.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/authenticators"
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/memorystore"
|
||||
"github.com/authorizerdev/authorizer/server/refs"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/gokyle/twofactor"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tuotoo/qrcode"
|
||||
)
|
||||
|
||||
func totpLoginTest(t *testing.T, s TestSetup) {
|
||||
t.Helper()
|
||||
t.Run(`should verify totp`, func(t *testing.T) {
|
||||
req, ctx := createContext(s)
|
||||
email := "verify_totp." + s.TestInfo.Email
|
||||
cleanData(email)
|
||||
res, err := resolvers.SignupResolver(ctx, model.SignUpInput{
|
||||
Email: &email,
|
||||
Password: s.TestInfo.Password,
|
||||
ConfirmPassword: s.TestInfo.Password,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, res)
|
||||
|
||||
// Login should fail as email is not verified
|
||||
loginRes, err := resolvers.LoginResolver(ctx, model.LoginInput{
|
||||
Email: &email,
|
||||
Password: s.TestInfo.Password,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, loginRes)
|
||||
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeBasicAuthSignup)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, email, verificationRequest.Email)
|
||||
verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{
|
||||
Token: verificationRequest.Token,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.NotEqual(t, verifyRes.AccessToken, "", "access token should not be empty")
|
||||
|
||||
// Using access token update profile
|
||||
s.GinContext.Request.Header.Set("Authorization", "Bearer "+refs.StringValue(verifyRes.AccessToken))
|
||||
ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext)
|
||||
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableTOTPLogin, false)
|
||||
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableMailOTPLogin, true)
|
||||
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisablePhoneVerification, true)
|
||||
updateProfileRes, err := resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{
|
||||
IsMultiFactorAuthEnabled: refs.NewBoolRef(true),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, updateProfileRes.Message)
|
||||
|
||||
authenticators.InitTOTPStore()
|
||||
// Login should not return error but access token should be empty
|
||||
loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{
|
||||
Email: &email,
|
||||
Password: s.TestInfo.Password,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, loginRes)
|
||||
assert.True(t, *loginRes.ShouldShowTotpScreen)
|
||||
assert.NotNil(t, *loginRes.AuthenticatorScannerImage)
|
||||
assert.NotNil(t, *loginRes.AuthenticatorSecret)
|
||||
assert.NotNil(t, loginRes.AuthenticatorRecoveryCodes)
|
||||
assert.Nil(t, loginRes.AccessToken)
|
||||
assert.NotEmpty(t, loginRes.Message)
|
||||
|
||||
// get totp url for validation
|
||||
pngBytes, err := base64.StdEncoding.DecodeString(*loginRes.AuthenticatorScannerImage)
|
||||
assert.NoError(t, err)
|
||||
qrmatrix, err := qrcode.Decode(bytes.NewReader(pngBytes))
|
||||
assert.NoError(t, err)
|
||||
tf, label, err := twofactor.FromURL(qrmatrix.Content)
|
||||
data := strings.Split(label, ":")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, email, data[1])
|
||||
assert.NotNil(t, tf)
|
||||
code := tf.OTP()
|
||||
assert.NotEmpty(t, code)
|
||||
// Set mfa cookie session
|
||||
mfaSession := uuid.NewString()
|
||||
memorystore.Provider.SetMfaSession(verifyRes.User.ID, mfaSession, time.Now().Add(1*time.Minute).Unix())
|
||||
cookie := fmt.Sprintf("%s=%s;", constants.MfaCookieName+"_session", mfaSession)
|
||||
cookie = strings.TrimSuffix(cookie, ";")
|
||||
req.Header.Set("Cookie", cookie)
|
||||
valid, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{
|
||||
Email: &email,
|
||||
Totp: refs.NewBoolRef(true),
|
||||
Otp: code,
|
||||
})
|
||||
accessToken := valid.AccessToken
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, accessToken)
|
||||
assert.NotEmpty(t, valid.Message)
|
||||
assert.NotEmpty(t, accessToken)
|
||||
claims, err := token.ParseJWTToken(*accessToken)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, claims)
|
||||
loginMethod := claims["login_method"]
|
||||
sessionKey := verifyRes.User.ID
|
||||
if loginMethod != nil && loginMethod != "" {
|
||||
sessionKey = loginMethod.(string) + ":" + verifyRes.User.ID
|
||||
}
|
||||
sessionToken, err := memorystore.Provider.GetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+claims["nonce"].(string))
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, sessionToken)
|
||||
cookie = fmt.Sprintf("%s=%s;", constants.AppCookieName+"_session", sessionToken)
|
||||
cookie = strings.TrimSuffix(cookie, ";")
|
||||
req.Header.Set("Cookie", cookie)
|
||||
//logged out
|
||||
logout, err := resolvers.LogoutResolver(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, logout.Message)
|
||||
loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{
|
||||
Email: &email,
|
||||
Password: s.TestInfo.Password,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, loginRes)
|
||||
assert.Nil(t, loginRes.AuthenticatorRecoveryCodes)
|
||||
assert.Nil(t, loginRes.AccessToken)
|
||||
assert.Nil(t, loginRes.AuthenticatorScannerImage)
|
||||
assert.Nil(t, loginRes.AuthenticatorSecret)
|
||||
assert.True(t, *loginRes.ShouldShowTotpScreen)
|
||||
assert.NotEmpty(t, loginRes.Message)
|
||||
code = tf.OTP()
|
||||
assert.NotEmpty(t, code)
|
||||
// Set mfa cookie session
|
||||
mfaSession = uuid.NewString()
|
||||
memorystore.Provider.SetMfaSession(verifyRes.User.ID, mfaSession, time.Now().Add(1*time.Minute).Unix())
|
||||
cookie = fmt.Sprintf("%s=%s;", constants.MfaCookieName+"_session", mfaSession)
|
||||
cookie = strings.TrimSuffix(cookie, ";")
|
||||
req.Header.Set("Cookie", cookie)
|
||||
valid, err = resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{
|
||||
Otp: code,
|
||||
Email: &email,
|
||||
Totp: refs.NewBoolRef(true),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, *valid.AccessToken)
|
||||
assert.NotEmpty(t, valid.Message)
|
||||
cleanData(email)
|
||||
})
|
||||
}
|
@@ -38,23 +38,23 @@ func updateAllUsersTest(t *testing.T, s TestSetup) {
|
||||
Offset: 0,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, len(listUsers.Users), 0)
|
||||
for _, u := range listUsers.Users {
|
||||
assert.True(t, refs.BoolValue(u.IsMultiFactorAuthEnabled))
|
||||
}
|
||||
|
||||
// // update few users
|
||||
updateIds := []string{listUsers.Users[0].ID, listUsers.Users[1].ID}
|
||||
err = db.Provider.UpdateUsers(ctx, map[string]interface{}{
|
||||
"is_multi_factor_auth_enabled": false,
|
||||
}, updateIds)
|
||||
assert.NoError(t, err)
|
||||
|
||||
listUsers, err = db.Provider.ListUsers(ctx, &model.Pagination{
|
||||
Limit: 20,
|
||||
Offset: 0,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, listUsers)
|
||||
assert.Greater(t, len(listUsers.Users), 0)
|
||||
for _, u := range listUsers.Users {
|
||||
if utils.StringSliceContains(updateIds, u.ID) {
|
||||
assert.False(t, refs.BoolValue(u.IsMultiFactorAuthEnabled))
|
||||
|
@@ -49,6 +49,9 @@ func verifyOTPTest(t *testing.T, s TestSetup) {
|
||||
// Using access token update profile
|
||||
s.GinContext.Request.Header.Set("Authorization", "Bearer "+refs.StringValue(verifyRes.AccessToken))
|
||||
ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext)
|
||||
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableMailOTPLogin, false)
|
||||
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableTOTPLogin, true)
|
||||
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisablePhoneVerification, true)
|
||||
updateProfileRes, err := resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{
|
||||
IsMultiFactorAuthEnabled: refs.NewBoolRef(true),
|
||||
})
|
||||
|
19
server/utils/generate_totp_recovery_code.go
Normal file
19
server/utils/generate_totp_recovery_code.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateTOTPRecoveryCode generates a random 16-character recovery code.
|
||||
func GenerateTOTPRecoveryCode() string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
code := make([]byte, 16)
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
for i := range code {
|
||||
code[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
|
||||
return string(code)
|
||||
}
|
Reference in New Issue
Block a user