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:
Lakhan Samani
2023-11-16 18:30:54 +05:30
committed by GitHub
parent d8cd965004
commit fe4c693324
57 changed files with 4321 additions and 1111 deletions

View File

@@ -37,7 +37,6 @@ func InitDB() error {
return err
}
}
if isArangoDB {
log.Info("Initializing ArangoDB Driver")
Provider, err = arangodb.NewProvider()

View 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"`
}

View File

@@ -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",
}
)

View 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
}

View File

@@ -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

View 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
}

View File

@@ -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

View File

@@ -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
}

View 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
}

View File

@@ -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
}

View 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
}

View File

@@ -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

View File

@@ -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
}

View 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
}

View File

@@ -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

View 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
}

View File

@@ -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)
}

View 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
}

View File

@@ -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
}