From 9ff1fa6f8431e9295780302e078076782761a2c5 Mon Sep 17 00:00:00 2001 From: lemonScaletech Date: Tue, 29 Aug 2023 12:48:44 +0530 Subject: [PATCH 1/8] fix: * removed hasReversedValue in playground --- dashboard/src/components/EnvComponents/Features.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/dashboard/src/components/EnvComponents/Features.tsx b/dashboard/src/components/EnvComponents/Features.tsx index fda3149..929da5b 100644 --- a/dashboard/src/components/EnvComponents/Features.tsx +++ b/dashboard/src/components/EnvComponents/Features.tsx @@ -133,7 +133,6 @@ const Features = ({ variables, setVariables }: any) => { variables={variables} setVariables={setVariables} inputType={SwitchInputType.DISABLE_PLAYGROUND} - hasReversedValue /> From 3f5283bf7f4d30595ea2dce6c14691ccda75f5fe Mon Sep 17 00:00:00 2001 From: lemonScaletech Date: Thu, 31 Aug 2023 19:50:44 +0530 Subject: [PATCH 2/8] feat: * added totp methods in db's providers * adding totp in login method --- server/constants/env.go | 3 + server/db/models/user.go | 1 + server/db/providers/arangodb/totp.go | 68 +++++++++++++++++++ server/db/providers/cassandradb/totp.go | 68 +++++++++++++++++++ server/db/providers/couchbase/totp.go | 68 +++++++++++++++++++ server/db/providers/dynamodb/totp.go | 68 +++++++++++++++++++ server/db/providers/mongodb/totp.go | 68 +++++++++++++++++++ server/db/providers/provider_template/totp.go | 68 +++++++++++++++++++ server/db/providers/providers.go | 6 +- server/db/providers/sql/totp.go | 68 +++++++++++++++++++ server/go.mod | 1 + server/go.sum | 4 ++ server/graph/generated/generated.go | 68 +++++++++++++++++++ server/graph/model/models_gen.go | 1 + server/graph/schema.graphqls | 1 + server/resolvers/login.go | 10 +++ 16 files changed, 570 insertions(+), 1 deletion(-) create mode 100644 server/db/providers/arangodb/totp.go create mode 100644 server/db/providers/cassandradb/totp.go create mode 100644 server/db/providers/couchbase/totp.go create mode 100644 server/db/providers/dynamodb/totp.go create mode 100644 server/db/providers/mongodb/totp.go create mode 100644 server/db/providers/provider_template/totp.go create mode 100644 server/db/providers/sql/totp.go diff --git a/server/constants/env.go b/server/constants/env.go index 828d4b8..649603c 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -160,6 +160,9 @@ 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" + // EnvKeyDisableTotpAuthentication is key for env variable DISABLE_TOTP_AUTHENTICATION + // this variable is used to completely disable totp verification + EnvKeyDisableTotpAuthentication = "DISABLE_TOTP_AUTHENTICATION" // EnvKeyDisablePhoneVerification is key for env variable DISABLE_PHONE_VERIFICATION // this variable is used to disable phone verification EnvKeyDisablePhoneVerification = "DISABLE_PHONE_VERIFICATION" diff --git a/server/db/models/user.go b/server/db/models/user.go index a262823..c077356 100644 --- a/server/db/models/user.go +++ b/server/db/models/user.go @@ -34,6 +34,7 @@ type User struct { UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at" dynamo:"updated_at"` CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at" dynamo:"created_at"` AppData *string `json:"app_data" bson:"app_data" cql:"app_data" dynamo:"app_data"` + TotpSecret *string `json:"totp_secret" bson:"totp_secret" cql:"totp_secret" dynamo:"totp_secret"` } func (user *User) AsAPIUser() *model.User { diff --git a/server/db/providers/arangodb/totp.go b/server/db/providers/arangodb/totp.go new file mode 100644 index 0000000..7bdae0b --- /dev/null +++ b/server/db/providers/arangodb/totp.go @@ -0,0 +1,68 @@ +package arangodb + +import ( + "bytes" + "context" + "fmt" + "image/png" + "time" + + "github.com/pquerna/otp/totp" + + "github.com/authorizerdev/authorizer/server/crypto" +) + +func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) { + var buf bytes.Buffer + //get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("error while getting user details") + } + + // generate totp, TOTP hash is valid for 30 seconds + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: "authorizer", + AccountName: user.Email, + }) + if err != nil { + return nil, fmt.Errorf("error while genrating totp") + } + + // get secret for user + secret := key.Secret() + + //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()) + + // update user totp secret in db + user.UpdatedAt = time.Now().Unix() + user.TotpSecret = &secret + _, err = p.UpdateUser(ctx, user) + if err != nil { + return nil, fmt.Errorf("error while updating user's totp secret") + } + + return &encodedText, nil +} + +func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) { + // get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return false, fmt.Errorf("error while getting user details") + } + + // validate passcode inputted by user + for { + status := totp.Validate(passcode, *user.TotpSecret) + if status { + return status, nil + } + } +} diff --git a/server/db/providers/cassandradb/totp.go b/server/db/providers/cassandradb/totp.go new file mode 100644 index 0000000..7732d04 --- /dev/null +++ b/server/db/providers/cassandradb/totp.go @@ -0,0 +1,68 @@ +package cassandradb + +import ( + "bytes" + "context" + "fmt" + "image/png" + "time" + + "github.com/pquerna/otp/totp" + + "github.com/authorizerdev/authorizer/server/crypto" +) + +func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) { + var buf bytes.Buffer + //get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("error while getting user details") + } + + // generate totp, TOTP hash is valid for 30 seconds + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: "authorizer", + AccountName: user.Email, + }) + if err != nil { + return nil, fmt.Errorf("error while genrating totp") + } + + // get secret for user + secret := key.Secret() + + //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()) + + // update user totp secret in db + user.UpdatedAt = time.Now().Unix() + user.TotpSecret = &secret + _, err = p.UpdateUser(ctx, user) + if err != nil { + return nil, fmt.Errorf("error while updating user's totp secret") + } + + return &encodedText, nil +} + +func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) { + // get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return false, fmt.Errorf("error while getting user details") + } + + // validate passcode inputted by user + for { + status := totp.Validate(passcode, *user.TotpSecret) + if status { + return status, nil + } + } +} diff --git a/server/db/providers/couchbase/totp.go b/server/db/providers/couchbase/totp.go new file mode 100644 index 0000000..a7e25fc --- /dev/null +++ b/server/db/providers/couchbase/totp.go @@ -0,0 +1,68 @@ +package couchbase + +import ( + "bytes" + "context" + "fmt" + "image/png" + "time" + + "github.com/pquerna/otp/totp" + + "github.com/authorizerdev/authorizer/server/crypto" +) + +func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) { + var buf bytes.Buffer + //get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("error while getting user details") + } + + // generate totp, TOTP hash is valid for 30 seconds + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: "authorizer", + AccountName: user.Email, + }) + if err != nil { + return nil, fmt.Errorf("error while genrating totp") + } + + // get secret for user + secret := key.Secret() + + //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()) + + // update user totp secret in db + user.UpdatedAt = time.Now().Unix() + user.TotpSecret = &secret + _, err = p.UpdateUser(ctx, user) + if err != nil { + return nil, fmt.Errorf("error while updating user's totp secret") + } + + return &encodedText, nil +} + +func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) { + // get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return false, fmt.Errorf("error while getting user details") + } + + // validate passcode inputted by user + for { + status := totp.Validate(passcode, *user.TotpSecret) + if status { + return status, nil + } + } +} diff --git a/server/db/providers/dynamodb/totp.go b/server/db/providers/dynamodb/totp.go new file mode 100644 index 0000000..5207709 --- /dev/null +++ b/server/db/providers/dynamodb/totp.go @@ -0,0 +1,68 @@ +package dynamodb + +import ( + "bytes" + "context" + "fmt" + "image/png" + "time" + + "github.com/pquerna/otp/totp" + + "github.com/authorizerdev/authorizer/server/crypto" +) + +func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) { + var buf bytes.Buffer + //get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("error while getting user details") + } + + // generate totp, TOTP hash is valid for 30 seconds + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: "authorizer", + AccountName: user.Email, + }) + if err != nil { + return nil, fmt.Errorf("error while genrating totp") + } + + // get secret for user + secret := key.Secret() + + //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()) + + // update user totp secret in db + user.UpdatedAt = time.Now().Unix() + user.TotpSecret = &secret + _, err = p.UpdateUser(ctx, user) + if err != nil { + return nil, fmt.Errorf("error while updating user's totp secret") + } + + return &encodedText, nil +} + +func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) { + // get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return false, fmt.Errorf("error while getting user details") + } + + // validate passcode inputted by user + for { + status := totp.Validate(passcode, *user.TotpSecret) + if status { + return status, nil + } + } +} diff --git a/server/db/providers/mongodb/totp.go b/server/db/providers/mongodb/totp.go new file mode 100644 index 0000000..031d732 --- /dev/null +++ b/server/db/providers/mongodb/totp.go @@ -0,0 +1,68 @@ +package mongodb + +import ( + "bytes" + "context" + "fmt" + "image/png" + "time" + + "github.com/pquerna/otp/totp" + + "github.com/authorizerdev/authorizer/server/crypto" +) + +func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) { + var buf bytes.Buffer + //get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("error while getting user details") + } + + // generate totp, TOTP hash is valid for 30 seconds + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: "authorizer", + AccountName: user.Email, + }) + if err != nil { + return nil, fmt.Errorf("error while genrating totp") + } + + // get secret for user + secret := key.Secret() + + //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()) + + // update user totp secret in db + user.UpdatedAt = time.Now().Unix() + user.TotpSecret = &secret + _, err = p.UpdateUser(ctx, user) + if err != nil { + return nil, fmt.Errorf("error while updating user's totp secret") + } + + return &encodedText, nil +} + +func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) { + // get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return false, fmt.Errorf("error while getting user details") + } + + // validate passcode inputted by user + for { + status := totp.Validate(passcode, *user.TotpSecret) + if status { + return status, nil + } + } +} diff --git a/server/db/providers/provider_template/totp.go b/server/db/providers/provider_template/totp.go new file mode 100644 index 0000000..1e2fda6 --- /dev/null +++ b/server/db/providers/provider_template/totp.go @@ -0,0 +1,68 @@ +package provider_template + +import ( + "bytes" + "context" + "fmt" + "image/png" + "time" + + "github.com/pquerna/otp/totp" + + "github.com/authorizerdev/authorizer/server/crypto" +) + +func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) { + var buf bytes.Buffer + //get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("error while getting user details") + } + + // generate totp, TOTP hash is valid for 30 seconds + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: "authorizer", + AccountName: user.Email, + }) + if err != nil { + return nil, fmt.Errorf("error while genrating totp") + } + + // get secret for user + secret := key.Secret() + + //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()) + + // update user totp secret in db + user.UpdatedAt = time.Now().Unix() + user.TotpSecret = &secret + _, err = p.UpdateUser(ctx, user) + if err != nil { + return nil, fmt.Errorf("error while updating user's totp secret") + } + + return &encodedText, nil +} + +func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) { + // get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return false, fmt.Errorf("error while getting user details") + } + + // validate passcode inputted by user + for { + status := totp.Validate(passcode, *user.TotpSecret) + if status { + return status, nil + } + } +} diff --git a/server/db/providers/providers.go b/server/db/providers/providers.go index 65b9010..c34c34e 100644 --- a/server/db/providers/providers.go +++ b/server/db/providers/providers.go @@ -2,7 +2,6 @@ package providers import ( "context" - "github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/graph/model" ) @@ -88,4 +87,9 @@ type Provider interface { GetOTPByPhoneNumber(ctx context.Context, phoneNumber string) (*models.OTP, error) // DeleteOTP to delete otp DeleteOTP(ctx context.Context, otp *models.OTP) error + + // GenerateTotp to generate totp, store secret into db and returns base64 of QR code image + GenerateTotp(ctx context.Context, id string) (*string, error) + // ValidatePasscode validate user passcode with secret stored in our db + ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) } diff --git a/server/db/providers/sql/totp.go b/server/db/providers/sql/totp.go new file mode 100644 index 0000000..cb4957b --- /dev/null +++ b/server/db/providers/sql/totp.go @@ -0,0 +1,68 @@ +package sql + +import ( + "bytes" + "context" + "fmt" + "image/png" + "time" + + "github.com/pquerna/otp/totp" + + "github.com/authorizerdev/authorizer/server/crypto" +) + +func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) { + var buf bytes.Buffer + //get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("error while getting user details") + } + + // generate totp, TOTP hash is valid for 30 seconds + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: "authorizer", + AccountName: user.Email, + }) + if err != nil { + return nil, fmt.Errorf("error while genrating totp") + } + + // get secret for user + secret := key.Secret() + + //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()) + + // update user totp secret in db + user.UpdatedAt = time.Now().Unix() + user.TotpSecret = &secret + _, err = p.UpdateUser(ctx, user) + if err != nil { + return nil, fmt.Errorf("error while updating user's totp secret") + } + + return &encodedText, nil +} + +func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) { + // get user details + user, err := p.GetUserByID(ctx, id) + if err != nil { + return false, fmt.Errorf("error while getting user details") + } + + // validate passcode inputted by user + for { + status := totp.Validate(passcode, *user.TotpSecret) + if status { + return status, nil + } + } +} diff --git a/server/go.mod b/server/go.mod index d1747e2..8404573 100644 --- a/server/go.mod +++ b/server/go.mod @@ -21,6 +21,7 @@ require ( github.com/joho/godotenv v1.3.0 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pquerna/otp v1.4.0 github.com/redis/go-redis/v9 v9.0.3 github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f github.com/sirupsen/logrus v1.8.1 diff --git a/server/go.sum b/server/go.sum index 4a2c928..4b61a81 100644 --- a/server/go.sum +++ b/server/go.sum @@ -58,6 +58,8 @@ github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYE github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= 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.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= @@ -285,6 +287,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k= github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index f323e42..84523cc 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -52,6 +52,7 @@ type ComplexityRoot struct { RefreshToken func(childComplexity int) int ShouldShowEmailOtpScreen func(childComplexity int) int ShouldShowMobileOtpScreen func(childComplexity int) int + TotpBase64url func(childComplexity int) int User func(childComplexity int) int } @@ -446,6 +447,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AuthResponse.ShouldShowMobileOtpScreen(childComplexity), true + case "AuthResponse.totpBase64URL": + if e.complexity.AuthResponse.TotpBase64url == nil { + break + } + + return e.complexity.AuthResponse.TotpBase64url(childComplexity), true + case "AuthResponse.user": if e.complexity.AuthResponse.User == nil { break @@ -2301,6 +2309,7 @@ type AuthResponse { refresh_token: String expires_in: Int64 user: User + totpBase64URL: String } type Response { @@ -3845,6 +3854,47 @@ func (ec *executionContext) fieldContext_AuthResponse_user(ctx context.Context, return fc, nil } +func (ec *executionContext) _AuthResponse_totpBase64URL(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AuthResponse_totpBase64URL(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.TotpBase64url, 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_totpBase64URL(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 { @@ -7901,6 +7951,8 @@ 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 "totpBase64URL": + return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -7974,6 +8026,8 @@ 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 "totpBase64URL": + return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8047,6 +8101,8 @@ 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 "totpBase64URL": + return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8120,6 +8176,8 @@ 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 "totpBase64URL": + return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8359,6 +8417,8 @@ 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 "totpBase64URL": + return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8668,6 +8728,8 @@ 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 "totpBase64URL": + return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -10090,6 +10152,8 @@ 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 "totpBase64URL": + return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -18150,6 +18214,10 @@ func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.Selection out.Values[i] = ec._AuthResponse_user(ctx, field, obj) + case "totpBase64URL": + + out.Values[i] = ec._AuthResponse_totpBase64URL(ctx, field, obj) + default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 4cdd8d0..49adb71 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -34,6 +34,7 @@ type AuthResponse struct { RefreshToken *string `json:"refresh_token"` ExpiresIn *int64 `json:"expires_in"` User *User `json:"user"` + TotpBase64url *string `json:"totpBase64URL"` } type DeleteEmailTemplateRequest struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 1c7de22..44a90ba 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -99,6 +99,7 @@ type AuthResponse { refresh_token: String expires_in: Int64 user: User + totpBase64URL: String } type Response { diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 6b16c3e..bc1f3d9 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -150,6 +150,16 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes }, nil } + if !isMFADisabled && refs.BoolValue(user.IsMultiFactorAuthEnabled) { + if user.TotpSecret == nil { + base64URL, err := db.Provider.GenerateTotp(ctx, user.ID) + if err != nil { + log.Debug("error while generating base64 url: ", err) + } + res.TotpBase64url = base64URL + } + } + code := "" codeChallenge := "" nonce := "" From 9fda8c01f58de46ee5c6fe7cd87ffb9c66e10307 Mon Sep 17 00:00:00 2001 From: lemonScaletech Date: Fri, 1 Sep 2023 19:36:47 +0530 Subject: [PATCH 3/8] feat: * added toggle in dashboard * fixing issue with env set --- .../src/components/EnvComponents/Features.tsx | 44 ++++++ dashboard/src/constants.ts | 4 + dashboard/src/graphql/queries/index.ts | 2 + dashboard/src/pages/Environment.tsx | 2 + server/constants/env.go | 7 +- server/env/env.go | 45 ++++++ server/env/persist_env.go | 7 +- server/graph/generated/generated.go | 144 +++++++++++++++++- server/graph/model/models_gen.go | 4 + server/graph/schema.graphqls | 4 + server/memorystore/memory_store.go | 2 + server/memorystore/providers/redis/store.go | 2 +- server/resolvers/env.go | 2 + server/resolvers/login.go | 16 +- server/resolvers/update_env.go | 11 ++ 15 files changed, 289 insertions(+), 7 deletions(-) diff --git a/dashboard/src/components/EnvComponents/Features.tsx b/dashboard/src/components/EnvComponents/Features.tsx index 929da5b..9f0c445 100644 --- a/dashboard/src/components/EnvComponents/Features.tsx +++ b/dashboard/src/components/EnvComponents/Features.tsx @@ -4,6 +4,7 @@ import InputField from '../InputField'; import { SwitchInputType } from '../../constants'; const Features = ({ variables, setVariables }: any) => { + // window.alert(variables) return (
{' '} @@ -24,6 +25,8 @@ const Features = ({ variables, setVariables }: any) => { /> + + Email Verification: @@ -97,6 +100,7 @@ const Features = ({ variables, setVariables }: any) => { also ignore the user MFA setting. + { /> + + { + !variables.DISABLE_MULTI_FACTOR_AUTHENTICATION && + + + TOTP: + + Note: to enable totp mfa + + + + + + + + } + {!variables.DISABLE_MULTI_FACTOR_AUTHENTICATION && + + + EMAIL OTP: + + Note: to enable email otp mfa + + + + + + + } + diff --git a/dashboard/src/constants.ts b/dashboard/src/constants.ts index 2dbf412..7a12131 100644 --- a/dashboard/src/constants.ts +++ b/dashboard/src/constants.ts @@ -85,6 +85,8 @@ export const SwitchInputType = { DISABLE_MULTI_FACTOR_AUTHENTICATION: 'DISABLE_MULTI_FACTOR_AUTHENTICATION', ENFORCE_MULTI_FACTOR_AUTHENTICATION: 'ENFORCE_MULTI_FACTOR_AUTHENTICATION', DISABLE_PLAYGROUND: 'DISABLE_PLAYGROUND', + DISABLE_TOTP_LOGIN: 'DISABLE_TOTP_LOGIN', + DISABLE_MAIL_OTP_LOGIN: 'DISABLE_MAIL_OTP_LOGIN', }; export const DateInputType = { @@ -169,6 +171,8 @@ export interface envVarTypes { DEFAULT_AUTHORIZE_RESPONSE_TYPE: string; DEFAULT_AUTHORIZE_RESPONSE_MODE: string; DISABLE_PLAYGROUND: boolean; + DISABLE_TOTP_LOGIN: boolean; + DISABLE_MAIL_OTP_LOGIN: boolean; } export const envSubViews = { diff --git a/dashboard/src/graphql/queries/index.ts b/dashboard/src/graphql/queries/index.ts index c5152f5..ffa8cd9 100644 --- a/dashboard/src/graphql/queries/index.ts +++ b/dashboard/src/graphql/queries/index.ts @@ -74,6 +74,8 @@ export const EnvVariablesQuery = ` DEFAULT_AUTHORIZE_RESPONSE_TYPE DEFAULT_AUTHORIZE_RESPONSE_MODE DISABLE_PLAYGROUND + DISABLE_TOTP_LOGIN + DISABLE_MAIL_OTP_LOGIN } } `; diff --git a/dashboard/src/pages/Environment.tsx b/dashboard/src/pages/Environment.tsx index c8d405c..8871f4a 100644 --- a/dashboard/src/pages/Environment.tsx +++ b/dashboard/src/pages/Environment.tsx @@ -94,6 +94,8 @@ const Environment = () => { DEFAULT_AUTHORIZE_RESPONSE_TYPE: '', DEFAULT_AUTHORIZE_RESPONSE_MODE: '', DISABLE_PLAYGROUND: false, + DISABLE_TOTP_LOGIN: false, + DISABLE_MAIL_OTP_LOGIN: true, }); const [fieldVisibility, setFieldVisibility] = React.useState< diff --git a/server/constants/env.go b/server/constants/env.go index 649603c..e89984b 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -160,9 +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" - // EnvKeyDisableTotpAuthentication is key for env variable DISABLE_TOTP_AUTHENTICATION + // EnvKeyDisableTOTPLogin is key for env variable DISABLE_TOTP_LOGIN // this variable is used to completely disable totp verification - EnvKeyDisableTotpAuthentication = "DISABLE_TOTP_AUTHENTICATION" + 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" diff --git a/server/env/env.go b/server/env/env.go index 3f65cf2..46ae2f4 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -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,6 +691,7 @@ 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] != "" { @@ -705,6 +708,7 @@ func InitAllEnv() error { if envData[constants.EnvKeyDisableEmailVerification].(bool) { envData[constants.EnvKeyDisableMagicLinkLogin] = true + envData[constants.EnvKeyDisableMailOTPLogin] = true } if val, ok := envData[constants.EnvKeyAllowedOrigins]; !ok || val == "" { @@ -840,6 +844,47 @@ 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 + } + } + + if envData[constants.EnvKeyDisableTOTPLogin] == false && envData[constants.EnvKeyDisableMailOTPLogin].(bool) == false { + errors.New("can't enable both mfa") + } + + if envData[constants.EnvKeyDisableMultiFactorAuthentication].(bool) { + envData[constants.EnvKeyDisableTOTPLogin] = true + envData[constants.EnvKeyDisableMailOTPLogin] = true + } else { + if !envData[constants.EnvKeyDisableMailOTPLogin].(bool) && !envData[constants.EnvKeyDisableTOTPLogin].(bool) { + errors.New("can't enable both mfa methods at same time") + envData[constants.EnvKeyDisableMailOTPLogin] = false + envData[constants.EnvKeyDisableTOTPLogin] = true + } + } + err = memorystore.Provider.UpdateEnvStore(envData) if err != nil { log.Debug("Error while updating env store: ", err) diff --git a/server/env/persist_env.go b/server/env/persist_env.go index eb0b64f..56142c5 100644 --- a/server/env/persist_env.go +++ b/server/env/persist_env.go @@ -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) diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 84523cc..2694bc5 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -97,11 +97,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 @@ -699,6 +701,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 +743,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 @@ -2384,6 +2400,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 { @@ -2507,6 +2525,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 { @@ -6895,6 +6915,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 { @@ -10810,6 +10918,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) }, @@ -17196,7 +17308,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 { @@ -17627,6 +17739,22 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob if err != nil { return it, err } + case "DISABLE_MAIL_OTP_LOGIN": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("DISABLE_MAIL_OTP_LOGIN")) + it.DisableMailOtpLogin, err = ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + case "DISABLE_TOTP_LOGIN": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("DISABLE_TOTP_LOGIN")) + it.DisableTotpLogin, err = ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } } } @@ -18625,6 +18753,20 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = ec._Env_DISABLE_PLAYGROUND(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "DISABLE_MAIL_OTP_LOGIN": + + out.Values[i] = ec._Env_DISABLE_MAIL_OTP_LOGIN(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "DISABLE_TOTP_LOGIN": + + out.Values[i] = ec._Env_DISABLE_TOTP_LOGIN(ctx, field, obj) + if out.Values[i] == graphql.Null { invalids++ } diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 49adb71..7430472 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -123,6 +123,8 @@ type Env struct { DefaultAuthorizeResponseType *string `json:"DEFAULT_AUTHORIZE_RESPONSE_TYPE"` DefaultAuthorizeResponseMode *string `json:"DEFAULT_AUTHORIZE_RESPONSE_MODE"` DisablePlayground bool `json:"DISABLE_PLAYGROUND"` + DisableMailOtpLogin bool `json:"DISABLE_MAIL_OTP_LOGIN"` + DisableTotpLogin bool `json:"DISABLE_TOTP_LOGIN"` } type Error struct { @@ -382,6 +384,8 @@ type UpdateEnvInput struct { DefaultAuthorizeResponseType *string `json:"DEFAULT_AUTHORIZE_RESPONSE_TYPE"` DefaultAuthorizeResponseMode *string `json:"DEFAULT_AUTHORIZE_RESPONSE_MODE"` DisablePlayground *bool `json:"DISABLE_PLAYGROUND"` + DisableMailOtpLogin *bool `json:"DISABLE_MAIL_OTP_LOGIN"` + DisableTotpLogin *bool `json:"DISABLE_TOTP_LOGIN"` } type UpdateProfileInput struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 44a90ba..433144a 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -174,6 +174,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 +299,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 { diff --git a/server/memorystore/memory_store.go b/server/memorystore/memory_store.go index 4004d68..a143d04 100644 --- a/server/memorystore/memory_store.go +++ b/server/memorystore/memory_store.go @@ -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() diff --git a/server/memorystore/providers/redis/store.go b/server/memorystore/providers/redis/store.go index d761ce1..a1b21db 100644 --- a/server/memorystore/providers/redis/store.go +++ b/server/memorystore/providers/redis/store.go @@ -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 diff --git a/server/resolvers/env.go b/server/resolvers/env.go index b7a949a..5eb86bd 100644 --- a/server/resolvers/env.go +++ b/server/resolvers/env.go @@ -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 } diff --git a/server/resolvers/login.go b/server/resolvers/login.go index bc1f3d9..1472697 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -110,8 +110,18 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes log.Debug("MFA service not enabled: ", err) } + 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) + } + // If email service is not enabled continue the process in any way - if refs.BoolValue(user.IsMultiFactorAuthEnabled) && isEmailServiceEnabled && !isMFADisabled { + if refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isMailOTPDisabled && !isMFADisabled { otp := utils.GenerateOTP() expires := time.Now().Add(1 * time.Minute).Unix() otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{ @@ -150,14 +160,16 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes }, nil } - if !isMFADisabled && refs.BoolValue(user.IsMultiFactorAuthEnabled) { + if !isMFADisabled && refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isTOTPLoginDisabled { if user.TotpSecret == nil { base64URL, err := db.Provider.GenerateTotp(ctx, user.ID) if err != nil { log.Debug("error while generating base64 url: ", err) } res.TotpBase64url = base64URL + } + } code := "" diff --git a/server/resolvers/update_env.go b/server/resolvers/update_env.go index 96388aa..6b3fe0e 100644 --- a/server/resolvers/update_env.go +++ b/server/resolvers/update_env.go @@ -263,6 +263,17 @@ 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 + } + } + if updatedData[constants.EnvKeySmtpHost] != "" || updatedData[constants.EnvKeySmtpUsername] != "" || updatedData[constants.EnvKeySmtpPassword] != "" || updatedData[constants.EnvKeySenderEmail] != "" && updatedData[constants.EnvKeySmtpPort] != "" { updatedData[constants.EnvKeyIsEmailServiceEnabled] = true } From bbb1cf63019bfdf4414f34e89d79eb414e1580e8 Mon Sep 17 00:00:00 2001 From: lemonScaletech Date: Wed, 6 Sep 2023 11:26:22 +0530 Subject: [PATCH 4/8] feat: * integrated totp --- .../src/components/EnvComponents/Features.tsx | 1 + server/crypto/rsa.go | 47 +++ server/db/providers/arangodb/totp.go | 37 ++ server/db/providers/cassandradb/totp.go | 37 ++ server/db/providers/couchbase/totp.go | 37 ++ server/db/providers/dynamodb/totp.go | 37 ++ server/db/providers/mongodb/totp.go | 37 ++ server/db/providers/provider_template/totp.go | 37 ++ server/db/providers/sql/totp.go | 23 +- server/env/env.go | 24 -- server/graph/generated/generated.go | 398 +++++++++++++++++- server/graph/model/models_gen.go | 27 +- server/graph/schema.graphqls | 10 + server/graph/schema.resolvers.go | 5 + server/resolvers/login.go | 15 +- server/resolvers/update_env.go | 30 +- server/resolvers/verify_totp.go | 119 ++++++ 17 files changed, 858 insertions(+), 63 deletions(-) create mode 100644 server/resolvers/verify_totp.go diff --git a/dashboard/src/components/EnvComponents/Features.tsx b/dashboard/src/components/EnvComponents/Features.tsx index 9f0c445..c41f493 100644 --- a/dashboard/src/components/EnvComponents/Features.tsx +++ b/dashboard/src/components/EnvComponents/Features.tsx @@ -177,6 +177,7 @@ const Features = ({ variables, setVariables }: any) => { variables={variables} setVariables={setVariables} inputType={SwitchInputType.DISABLE_PLAYGROUND} + hasReversedValue /> diff --git a/server/crypto/rsa.go b/server/crypto/rsa.go index 35bebd3..45be9ad 100644 --- a/server/crypto/rsa.go +++ b/server/crypto/rsa.go @@ -3,9 +3,12 @@ package crypto import ( "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" + "encoding/base64" "encoding/pem" "errors" + "fmt" ) // NewRSAKey to generate new RSA Key if env is not set @@ -116,3 +119,47 @@ 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 + } + fmt.Println("Plaintext:", string(plaintext)) + return string(plaintext), nil +} + +func ParseRSAPublicKey(key string) (*rsa.PublicKey, error) { + // Decode the PEM-encoded public key data. + block, _ := pem.Decode([]byte(key)) + if block == nil { + return nil, fmt.Errorf("failed to parse PEM block containing public key") + } + + // Parse the DER-encoded public key data. + pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + // Type-assert the parsed public key to an rsa.PublicKey. + rsaPublicKey, ok := pubKey.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("parsed public key is not an RSA public key") + } + + return rsaPublicKey, nil +} diff --git a/server/db/providers/arangodb/totp.go b/server/db/providers/arangodb/totp.go index 7bdae0b..d64d821 100644 --- a/server/db/providers/arangodb/totp.go +++ b/server/db/providers/arangodb/totp.go @@ -3,8 +3,13 @@ package arangodb import ( "bytes" "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "fmt" "image/png" + "os" "time" "github.com/pquerna/otp/totp" @@ -66,3 +71,35 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str } } } + +func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { + key := os.Getenv("TOTP_PRIVATE_KEY") + var privateKey *rsa.PrivateKey + if key == "" { + privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, err + } + + privateKeyPEM := encodePrivateKeyToPEM(privateKey) + os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) + } + publicKey := privateKey.PublicKey + return &publicKey, nil +} + +func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { + // Marshal the private key to DER format. + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + + // Create a PEM block for the private key. + privateKeyPEMBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + } + + // Encode the PEM block to PEM format. + privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) + + return privateKeyPEM +} diff --git a/server/db/providers/cassandradb/totp.go b/server/db/providers/cassandradb/totp.go index 7732d04..b34a6c3 100644 --- a/server/db/providers/cassandradb/totp.go +++ b/server/db/providers/cassandradb/totp.go @@ -3,8 +3,13 @@ package cassandradb import ( "bytes" "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "fmt" "image/png" + "os" "time" "github.com/pquerna/otp/totp" @@ -66,3 +71,35 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str } } } + +func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { + key := os.Getenv("TOTP_PRIVATE_KEY") + var privateKey *rsa.PrivateKey + if key == "" { + privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, err + } + + privateKeyPEM := encodePrivateKeyToPEM(privateKey) + os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) + } + publicKey := privateKey.PublicKey + return &publicKey, nil +} + +func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { + // Marshal the private key to DER format. + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + + // Create a PEM block for the private key. + privateKeyPEMBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + } + + // Encode the PEM block to PEM format. + privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) + + return privateKeyPEM +} diff --git a/server/db/providers/couchbase/totp.go b/server/db/providers/couchbase/totp.go index a7e25fc..4a3b44f 100644 --- a/server/db/providers/couchbase/totp.go +++ b/server/db/providers/couchbase/totp.go @@ -3,8 +3,13 @@ package couchbase import ( "bytes" "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "fmt" "image/png" + "os" "time" "github.com/pquerna/otp/totp" @@ -66,3 +71,35 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str } } } + +func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { + key := os.Getenv("TOTP_PRIVATE_KEY") + var privateKey *rsa.PrivateKey + if key == "" { + privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, err + } + + privateKeyPEM := encodePrivateKeyToPEM(privateKey) + os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) + } + publicKey := privateKey.PublicKey + return &publicKey, nil +} + +func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { + // Marshal the private key to DER format. + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + + // Create a PEM block for the private key. + privateKeyPEMBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + } + + // Encode the PEM block to PEM format. + privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) + + return privateKeyPEM +} diff --git a/server/db/providers/dynamodb/totp.go b/server/db/providers/dynamodb/totp.go index 5207709..8a8a705 100644 --- a/server/db/providers/dynamodb/totp.go +++ b/server/db/providers/dynamodb/totp.go @@ -3,8 +3,13 @@ package dynamodb import ( "bytes" "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "fmt" "image/png" + "os" "time" "github.com/pquerna/otp/totp" @@ -66,3 +71,35 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str } } } + +func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { + key := os.Getenv("TOTP_PRIVATE_KEY") + var privateKey *rsa.PrivateKey + if key == "" { + privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, err + } + + privateKeyPEM := encodePrivateKeyToPEM(privateKey) + os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) + } + publicKey := privateKey.PublicKey + return &publicKey, nil +} + +func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { + // Marshal the private key to DER format. + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + + // Create a PEM block for the private key. + privateKeyPEMBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + } + + // Encode the PEM block to PEM format. + privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) + + return privateKeyPEM +} diff --git a/server/db/providers/mongodb/totp.go b/server/db/providers/mongodb/totp.go index 031d732..220c1c8 100644 --- a/server/db/providers/mongodb/totp.go +++ b/server/db/providers/mongodb/totp.go @@ -3,8 +3,13 @@ package mongodb import ( "bytes" "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "fmt" "image/png" + "os" "time" "github.com/pquerna/otp/totp" @@ -66,3 +71,35 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str } } } + +func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { + key := os.Getenv("TOTP_PRIVATE_KEY") + var privateKey *rsa.PrivateKey + if key == "" { + privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, err + } + + privateKeyPEM := encodePrivateKeyToPEM(privateKey) + os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) + } + publicKey := privateKey.PublicKey + return &publicKey, nil +} + +func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { + // Marshal the private key to DER format. + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + + // Create a PEM block for the private key. + privateKeyPEMBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + } + + // Encode the PEM block to PEM format. + privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) + + return privateKeyPEM +} diff --git a/server/db/providers/provider_template/totp.go b/server/db/providers/provider_template/totp.go index 1e2fda6..9da1d0d 100644 --- a/server/db/providers/provider_template/totp.go +++ b/server/db/providers/provider_template/totp.go @@ -3,8 +3,13 @@ package provider_template import ( "bytes" "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "fmt" "image/png" + "os" "time" "github.com/pquerna/otp/totp" @@ -66,3 +71,35 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str } } } + +func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { + key := os.Getenv("TOTP_PRIVATE_KEY") + var privateKey *rsa.PrivateKey + if key == "" { + privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, err + } + + privateKeyPEM := encodePrivateKeyToPEM(privateKey) + os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) + } + publicKey := privateKey.PublicKey + return &publicKey, nil +} + +func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { + // Marshal the private key to DER format. + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + + // Create a PEM block for the private key. + privateKeyPEMBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + } + + // Encode the PEM block to PEM format. + privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) + + return privateKeyPEM +} diff --git a/server/db/providers/sql/totp.go b/server/db/providers/sql/totp.go index cb4957b..2e8ad11 100644 --- a/server/db/providers/sql/totp.go +++ b/server/db/providers/sql/totp.go @@ -3,7 +3,10 @@ package sql import ( "bytes" "context" + "crypto/rand" + "crypto/rsa" "fmt" + log "github.com/sirupsen/logrus" "image/png" "time" @@ -47,7 +50,7 @@ func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) if err != nil { return nil, fmt.Errorf("error while updating user's totp secret") } - + log.Info("\n\n\n", &encodedText) return &encodedText, nil } @@ -59,10 +62,16 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str } // validate passcode inputted by user - for { - status := totp.Validate(passcode, *user.TotpSecret) - if status { - return status, nil - } - } + + status := totp.Validate(passcode, *user.TotpSecret) + return status, nil +} + +func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, err + } + publicKey := privateKey.PublicKey + return &publicKey, nil } diff --git a/server/env/env.go b/server/env/env.go index 46ae2f4..09ed82c 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -698,17 +698,8 @@ func InitAllEnv() error { 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 - envData[constants.EnvKeyDisableMailOTPLogin] = true } if val, ok := envData[constants.EnvKeyAllowedOrigins]; !ok || val == "" { @@ -870,21 +861,6 @@ func InitAllEnv() error { } } - if envData[constants.EnvKeyDisableTOTPLogin] == false && envData[constants.EnvKeyDisableMailOTPLogin].(bool) == false { - errors.New("can't enable both mfa") - } - - if envData[constants.EnvKeyDisableMultiFactorAuthentication].(bool) { - envData[constants.EnvKeyDisableTOTPLogin] = true - envData[constants.EnvKeyDisableMailOTPLogin] = true - } else { - if !envData[constants.EnvKeyDisableMailOTPLogin].(bool) && !envData[constants.EnvKeyDisableTOTPLogin].(bool) { - errors.New("can't enable both mfa methods at same time") - envData[constants.EnvKeyDisableMailOTPLogin] = false - envData[constants.EnvKeyDisableTOTPLogin] = true - } - } - err = memorystore.Provider.UpdateEnvStore(envData) if err != nil { log.Debug("Error while updating env store: ", err) diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 2694bc5..a7b3209 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -45,15 +45,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 - TotpBase64url func(childComplexity int) int - User func(childComplexity int) int + 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 + ShouldShowMobileTotpScreen func(childComplexity int) int + ShouldShowTotpScreen func(childComplexity int) int + TokenTotp func(childComplexity int) int + TotpBase64url func(childComplexity int) int + User func(childComplexity int) int } EmailTemplate struct { @@ -204,6 +207,7 @@ type ComplexityRoot struct { UpdateWebhook func(childComplexity int, params model.UpdateWebhookRequest) int VerifyEmail func(childComplexity int, params model.VerifyEmailInput) int VerifyOtp func(childComplexity int, params model.VerifyOTPRequest) int + VerifyTotp func(childComplexity int, params model.VerifyTOTPRequest) int } Pagination struct { @@ -350,6 +354,7 @@ type MutationResolver interface { Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) VerifyOtp(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error) ResendOtp(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error) + VerifyTotp(ctx context.Context, params model.VerifyTOTPRequest) (*model.AuthResponse, error) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error) AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error) @@ -449,6 +454,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AuthResponse.ShouldShowMobileOtpScreen(childComplexity), true + case "AuthResponse.should_show_mobile_totp_screen": + if e.complexity.AuthResponse.ShouldShowMobileTotpScreen == nil { + break + } + + return e.complexity.AuthResponse.ShouldShowMobileTotpScreen(childComplexity), true + + case "AuthResponse.should_show_totp_screen": + if e.complexity.AuthResponse.ShouldShowTotpScreen == nil { + break + } + + return e.complexity.AuthResponse.ShouldShowTotpScreen(childComplexity), true + + case "AuthResponse.tokenTOTP": + if e.complexity.AuthResponse.TokenTotp == nil { + break + } + + return e.complexity.AuthResponse.TokenTotp(childComplexity), true + case "AuthResponse.totpBase64URL": if e.complexity.AuthResponse.TotpBase64url == nil { break @@ -1490,6 +1516,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.VerifyOtp(childComplexity, args["params"].(model.VerifyOTPRequest)), true + case "Mutation.verify_totp": + if e.complexity.Mutation.VerifyTotp == nil { + break + } + + args, err := ec.field_Mutation_verify_totp_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.VerifyTotp(childComplexity, args["params"].(model.VerifyTOTPRequest)), true + case "Pagination.limit": if e.complexity.Pagination.Limit == nil { break @@ -2163,6 +2201,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputValidateSessionInput, ec.unmarshalInputVerifyEmailInput, ec.unmarshalInputVerifyOTPRequest, + ec.unmarshalInputVerifyTOTPRequest, ec.unmarshalInputWebhookRequest, ) first := true @@ -2320,12 +2359,15 @@ type AuthResponse { message: String! should_show_email_otp_screen: Boolean should_show_mobile_otp_screen: Boolean + should_show_mobile_totp_screen: Boolean + should_show_totp_screen: Boolean access_token: String id_token: String refresh_token: String expires_in: Int64 user: User totpBase64URL: String + tokenTOTP: String } type Response { @@ -2789,6 +2831,12 @@ input VerifyOTPRequest { state: String } +input VerifyTOTPRequest { + otp: String! + token: String! + state: String +} + input ResendOTPRequest { email: String phone_number: String @@ -2818,6 +2866,7 @@ type Mutation { revoke(params: OAuthRevokeInput!): Response! verify_otp(params: VerifyOTPRequest!): AuthResponse! resend_otp(params: ResendOTPRequest!): Response! + verify_totp(params: VerifyTOTPRequest!): AuthResponse! # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! @@ -3298,6 +3347,21 @@ func (ec *executionContext) field_Mutation_verify_otp_args(ctx context.Context, return args, nil } +func (ec *executionContext) field_Mutation_verify_totp_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.VerifyTOTPRequest + if tmp, ok := rawArgs["params"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) + arg0, err = ec.unmarshalNVerifyTOTPRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐVerifyTOTPRequest(ctx, tmp) + if err != nil { + return nil, err + } + } + args["params"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3627,6 +3691,88 @@ func (ec *executionContext) fieldContext_AuthResponse_should_show_mobile_otp_scr return fc, nil } +func (ec *executionContext) _AuthResponse_should_show_mobile_totp_screen(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AuthResponse_should_show_mobile_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.ShouldShowMobileTotpScreen, 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_mobile_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_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 { @@ -3915,6 +4061,47 @@ func (ec *executionContext) fieldContext_AuthResponse_totpBase64URL(ctx context. return fc, nil } +func (ec *executionContext) _AuthResponse_tokenTOTP(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AuthResponse_tokenTOTP(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.TokenTotp, 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_tokenTOTP(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 { @@ -8049,6 +8236,10 @@ 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_mobile_totp_screen": + return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -8061,6 +8252,8 @@ func (ec *executionContext) fieldContext_Mutation_signup(ctx context.Context, fi return ec.fieldContext_AuthResponse_user(ctx, field) case "totpBase64URL": return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) + case "tokenTOTP": + return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8124,6 +8317,10 @@ 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_mobile_totp_screen": + return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -8136,6 +8333,8 @@ func (ec *executionContext) fieldContext_Mutation_mobile_signup(ctx context.Cont return ec.fieldContext_AuthResponse_user(ctx, field) case "totpBase64URL": return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) + case "tokenTOTP": + return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8199,6 +8398,10 @@ 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_mobile_totp_screen": + return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -8211,6 +8414,8 @@ func (ec *executionContext) fieldContext_Mutation_login(ctx context.Context, fie return ec.fieldContext_AuthResponse_user(ctx, field) case "totpBase64URL": return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) + case "tokenTOTP": + return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8274,6 +8479,10 @@ 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_mobile_totp_screen": + return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -8286,6 +8495,8 @@ func (ec *executionContext) fieldContext_Mutation_mobile_login(ctx context.Conte return ec.fieldContext_AuthResponse_user(ctx, field) case "totpBase64URL": return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) + case "tokenTOTP": + return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8515,6 +8726,10 @@ 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_mobile_totp_screen": + return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -8527,6 +8742,8 @@ func (ec *executionContext) fieldContext_Mutation_verify_email(ctx context.Conte return ec.fieldContext_AuthResponse_user(ctx, field) case "totpBase64URL": return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) + case "tokenTOTP": + return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8826,6 +9043,10 @@ 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_mobile_totp_screen": + return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -8838,6 +9059,8 @@ func (ec *executionContext) fieldContext_Mutation_verify_otp(ctx context.Context return ec.fieldContext_AuthResponse_user(ctx, field) case "totpBase64URL": return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) + case "tokenTOTP": + return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8915,6 +9138,87 @@ func (ec *executionContext) fieldContext_Mutation_resend_otp(ctx context.Context return fc, nil } +func (ec *executionContext) _Mutation_verify_totp(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_verify_totp(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 ec.resolvers.Mutation().VerifyTotp(rctx, fc.Args["params"].(model.VerifyTOTPRequest)) + }) + 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.(*model.AuthResponse) + fc.Result = res + return ec.marshalNAuthResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAuthResponse(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_verify_totp(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "message": + return ec.fieldContext_AuthResponse_message(ctx, field) + case "should_show_email_otp_screen": + 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_mobile_totp_screen": + return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": + return ec.fieldContext_AuthResponse_id_token(ctx, field) + case "refresh_token": + return ec.fieldContext_AuthResponse_refresh_token(ctx, field) + case "expires_in": + return ec.fieldContext_AuthResponse_expires_in(ctx, field) + case "user": + return ec.fieldContext_AuthResponse_user(ctx, field) + case "totpBase64URL": + return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) + case "tokenTOTP": + return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_verify_totp_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return + } + return fc, nil +} + func (ec *executionContext) _Mutation__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation__delete_user(ctx, field) if err != nil { @@ -10250,6 +10554,10 @@ 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_mobile_totp_screen": + return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -10262,6 +10570,8 @@ func (ec *executionContext) fieldContext_Query_session(ctx context.Context, fiel return ec.fieldContext_AuthResponse_user(ctx, field) case "totpBase64URL": return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) + case "tokenTOTP": + return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -18261,6 +18571,50 @@ func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context, return it, nil } +func (ec *executionContext) unmarshalInputVerifyTOTPRequest(ctx context.Context, obj interface{}) (model.VerifyTOTPRequest, error) { + var it model.VerifyTOTPRequest + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"otp", "token", "state"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "otp": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("otp")) + it.Otp, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + case "token": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("token")) + it.Token, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + case "state": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state")) + it.State, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputWebhookRequest(ctx context.Context, obj interface{}) (model.WebhookRequest, error) { var it model.WebhookRequest asMap := map[string]interface{}{} @@ -18322,6 +18676,14 @@ func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.Selection out.Values[i] = ec._AuthResponse_should_show_mobile_otp_screen(ctx, field, obj) + case "should_show_mobile_totp_screen": + + out.Values[i] = ec._AuthResponse_should_show_mobile_totp_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) @@ -18346,6 +18708,10 @@ func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.Selection out.Values[i] = ec._AuthResponse_totpBase64URL(ctx, field, obj) + case "tokenTOTP": + + out.Values[i] = ec._AuthResponse_tokenTOTP(ctx, field, obj) + default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -19152,6 +19518,15 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) return ec._Mutation_resend_otp(ctx, field) }) + if out.Values[i] == graphql.Null { + invalids++ + } + case "verify_totp": + + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_verify_totp(ctx, field) + }) + if out.Values[i] == graphql.Null { invalids++ } @@ -21234,6 +21609,11 @@ func (ec *executionContext) unmarshalNVerifyOTPRequest2githubᚗcomᚋauthorizer return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNVerifyTOTPRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐVerifyTOTPRequest(ctx context.Context, v interface{}) (model.VerifyTOTPRequest, error) { + res, err := ec.unmarshalInputVerifyTOTPRequest(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNWebhook2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐWebhook(ctx context.Context, sel ast.SelectionSet, v model.Webhook) graphql.Marshaler { return ec._Webhook(ctx, sel, &v) } diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 7430472..8037700 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -26,15 +26,18 @@ type AdminSignupInput struct { } type AuthResponse struct { - Message string `json:"message"` - ShouldShowEmailOtpScreen *bool `json:"should_show_email_otp_screen"` - ShouldShowMobileOtpScreen *bool `json:"should_show_mobile_otp_screen"` - AccessToken *string `json:"access_token"` - IDToken *string `json:"id_token"` - RefreshToken *string `json:"refresh_token"` - ExpiresIn *int64 `json:"expires_in"` - User *User `json:"user"` - TotpBase64url *string `json:"totpBase64URL"` + Message string `json:"message"` + ShouldShowEmailOtpScreen *bool `json:"should_show_email_otp_screen"` + ShouldShowMobileOtpScreen *bool `json:"should_show_mobile_otp_screen"` + ShouldShowMobileTotpScreen *bool `json:"should_show_mobile_totp_screen"` + ShouldShowTotpScreen *bool `json:"should_show_totp_screen"` + AccessToken *string `json:"access_token"` + IDToken *string `json:"id_token"` + RefreshToken *string `json:"refresh_token"` + ExpiresIn *int64 `json:"expires_in"` + User *User `json:"user"` + TotpBase64url *string `json:"totpBase64URL"` + TokenTotp *string `json:"tokenTOTP"` } type DeleteEmailTemplateRequest struct { @@ -509,6 +512,12 @@ type VerifyOTPRequest struct { State *string `json:"state"` } +type VerifyTOTPRequest struct { + Otp string `json:"otp"` + Token string `json:"token"` + State *string `json:"state"` +} + type Webhook struct { ID string `json:"id"` EventName *string `json:"event_name"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 433144a..9fe68ba 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -94,12 +94,15 @@ type AuthResponse { message: String! should_show_email_otp_screen: Boolean should_show_mobile_otp_screen: Boolean + should_show_mobile_totp_screen: Boolean + should_show_totp_screen: Boolean access_token: String id_token: String refresh_token: String expires_in: Int64 user: User totpBase64URL: String + tokenTOTP: String } type Response { @@ -563,6 +566,12 @@ input VerifyOTPRequest { state: String } +input VerifyTOTPRequest { + otp: String! + token: String! + state: String +} + input ResendOTPRequest { email: String phone_number: String @@ -592,6 +601,7 @@ type Mutation { revoke(params: OAuthRevokeInput!): Response! verify_otp(params: VerifyOTPRequest!): AuthResponse! resend_otp(params: ResendOTPRequest!): Response! + verify_totp(params: VerifyTOTPRequest!): AuthResponse! # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index eecb6b2..fed9c4c 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -81,6 +81,11 @@ func (r *mutationResolver) ResendOtp(ctx context.Context, params model.ResendOTP return resolvers.ResendOTPResolver(ctx, params) } +// VerifyTotp is the resolver for the verify_totp field. +func (r *mutationResolver) VerifyTotp(ctx context.Context, params model.VerifyTOTPRequest) (*model.AuthResponse, error) { + return resolvers.VerifyTotpResolver(ctx, params) +} + // DeleteUser is the resolver for the _delete_user field. func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) { return resolvers.DeleteUserResolver(ctx, params) diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 1472697..91040be 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -166,10 +166,21 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes if err != nil { log.Debug("error while generating base64 url: ", err) } - res.TotpBase64url = base64URL + res = &model.AuthResponse{ + Message: `Proceed to totp screen`, + TotpBase64url: base64URL, + TokenTotp: &user.ID, + } + return res, nil + } else { + //res.TokenTotp = &user.ID + res = &model.AuthResponse{ + Message: `Proceed to totp screen`, + TokenTotp: &user.ID, + } + return res, nil } - } code := "" diff --git a/server/resolvers/update_env.go b/server/resolvers/update_env.go index 6b3fe0e..6ac770c 100644 --- a/server/resolvers/update_env.go +++ b/server/resolvers/update_env.go @@ -253,22 +253,13 @@ 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.EnvKeyDisableMagicLinkLogin].(bool) { - updatedData[constants.EnvKeyDisableMagicLinkLogin] = true + if !updatedData[constants.EnvKeyDisableMailOTPLogin].(bool) { + updatedData[constants.EnvKeyDisableMailOTPLogin] = true } - } - - 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") + if !updatedData[constants.EnvKeyDisableMagicLinkLogin].(bool) { updatedData[constants.EnvKeyDisableMailOTPLogin] = true updatedData[constants.EnvKeyDisableTOTPLogin] = false } @@ -285,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, diff --git a/server/resolvers/verify_totp.go b/server/resolvers/verify_totp.go new file mode 100644 index 0000000..c335aa0 --- /dev/null +++ b/server/resolvers/verify_totp.go @@ -0,0 +1,119 @@ +package resolvers + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + log "github.com/sirupsen/logrus" + + "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" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/memorystore" + "github.com/authorizerdev/authorizer/server/refs" + "github.com/authorizerdev/authorizer/server/token" + "github.com/authorizerdev/authorizer/server/utils" +) + +// VerifyTotpResolver resolver for verify totp mutation +func VerifyTotpResolver(ctx context.Context, params model.VerifyTOTPRequest) (*model.AuthResponse, error) { + var res *model.AuthResponse + + userID := params.Token + + gc, err := utils.GinContextFromContext(ctx) + if err != nil { + log.Debug("Failed to get GinContext: ", err) + return res, err + } + + user, err := db.Provider.GetUserByID(ctx, userID) + if err != nil { + return nil, err + } + + status, err := db.Provider.ValidatePasscode(ctx, params.Otp, userID) + if err != nil || !status { + return nil, fmt.Errorf("error while validating passcode", err) + } + + code := "" + codeChallenge := "" + nonce := "" + + roles := strings.Split(user.Roles, ",") + scope := []string{"openid", "email", "profile"} + + // Get state from store + if params.State != nil { + authorizeState, _ := memorystore.Provider.GetState(refs.StringValue(params.State)) + if authorizeState != "" { + authorizeStateSplit := strings.Split(authorizeState, "@@") + if len(authorizeStateSplit) > 1 { + code = authorizeStateSplit[0] + codeChallenge = authorizeStateSplit[1] + } else { + nonce = authorizeState + } + go memorystore.Provider.RemoveState(refs.StringValue(params.State)) + } + } + + if nonce == "" { + nonce = uuid.New().String() + } + + authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth, nonce, code) + if err != nil { + log.Debug("Failed to create auth token", err) + return res, err + } + + // TODO add to other login options as well + // Code challenge could be optional if PKCE flow is not used + if code != "" { + if err := memorystore.Provider.SetState(code, codeChallenge+"@@"+authToken.FingerPrintHash); err != nil { + log.Debug("SetState failed: ", err) + return res, err + } + } + + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } + + res = &model.AuthResponse{ + Message: `Logged in successfully`, + AccessToken: &authToken.AccessToken.Token, + IDToken: &authToken.IDToken.Token, + ExpiresIn: &expiresIn, + User: user.AsAPIUser(), + } + + cookie.SetSession(gc, authToken.FingerPrintHash) + sessionStoreKey := constants.AuthRecipeMethodBasicAuth + ":" + user.ID + memorystore.Provider.SetUserSession(sessionStoreKey, constants.TokenTypeSessionToken+"_"+authToken.FingerPrint, authToken.FingerPrintHash, authToken.SessionTokenExpiresAt) + memorystore.Provider.SetUserSession(sessionStoreKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token, authToken.AccessToken.ExpiresAt) + + if authToken.RefreshToken != nil { + res.RefreshToken = &authToken.RefreshToken.Token + memorystore.Provider.SetUserSession(sessionStoreKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token, authToken.RefreshToken.ExpiresAt) + } + + go func() { + utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodBasicAuth, user) + db.Provider.AddSession(ctx, &models.Session{ + UserID: user.ID, + UserAgent: utils.GetUserAgent(gc.Request), + IP: utils.GetIP(gc.Request), + }) + }() + + return res, nil +} From a3fa0eb6cd0e9409ee3727b3be7c9566f17d0153 Mon Sep 17 00:00:00 2001 From: lemonScaletech Date: Wed, 6 Sep 2023 18:49:54 +0530 Subject: [PATCH 5/8] feat: * encrypted userid * added totp_verified column in user table * started test for totp --- server/crypto/rsa.go | 24 ------ server/db/models/user.go | 1 + server/db/providers/arangodb/totp.go | 47 ++---------- server/db/providers/cassandradb/totp.go | 47 ++---------- server/db/providers/couchbase/totp.go | 47 ++---------- server/db/providers/dynamodb/totp.go | 47 ++---------- server/db/providers/mongodb/totp.go | 47 ++---------- server/db/providers/provider_template/totp.go | 47 ++---------- server/db/providers/sql/totp.go | 22 ++---- server/resolvers/login.go | 22 +++++- server/resolvers/verify_totp.go | 18 ++++- server/test/integration_test.go | 1 + server/test/verify_totp_test.go | 76 +++++++++++++++++++ 13 files changed, 157 insertions(+), 289 deletions(-) create mode 100644 server/test/verify_totp_test.go diff --git a/server/crypto/rsa.go b/server/crypto/rsa.go index 45be9ad..6eba852 100644 --- a/server/crypto/rsa.go +++ b/server/crypto/rsa.go @@ -8,7 +8,6 @@ import ( "encoding/base64" "encoding/pem" "errors" - "fmt" ) // NewRSAKey to generate new RSA Key if env is not set @@ -138,28 +137,5 @@ func DecryptRSA(cipherText string, privateKey rsa.PrivateKey) (string, error) { if err != nil { return "", err } - fmt.Println("Plaintext:", string(plaintext)) return string(plaintext), nil } - -func ParseRSAPublicKey(key string) (*rsa.PublicKey, error) { - // Decode the PEM-encoded public key data. - block, _ := pem.Decode([]byte(key)) - if block == nil { - return nil, fmt.Errorf("failed to parse PEM block containing public key") - } - - // Parse the DER-encoded public key data. - pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return nil, err - } - - // Type-assert the parsed public key to an rsa.PublicKey. - rsaPublicKey, ok := pubKey.(*rsa.PublicKey) - if !ok { - return nil, fmt.Errorf("parsed public key is not an RSA public key") - } - - return rsaPublicKey, nil -} diff --git a/server/db/models/user.go b/server/db/models/user.go index c077356..f0b1e4f 100644 --- a/server/db/models/user.go +++ b/server/db/models/user.go @@ -35,6 +35,7 @@ type User struct { CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at" dynamo:"created_at"` AppData *string `json:"app_data" bson:"app_data" cql:"app_data" dynamo:"app_data"` TotpSecret *string `json:"totp_secret" bson:"totp_secret" cql:"totp_secret" dynamo:"totp_secret"` + TotpVerified bool `json:"totp_verified" bson:"totp_verified" cql:"totp_verified" dynamo:"totp_verified"` } func (user *User) AsAPIUser() *model.User { diff --git a/server/db/providers/arangodb/totp.go b/server/db/providers/arangodb/totp.go index d64d821..35dd7a3 100644 --- a/server/db/providers/arangodb/totp.go +++ b/server/db/providers/arangodb/totp.go @@ -3,13 +3,8 @@ package arangodb import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" "image/png" - "os" "time" "github.com/pquerna/otp/totp" @@ -62,44 +57,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - for { - status := totp.Validate(passcode, *user.TotpSecret) + status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) return status, nil } + return status, nil } -} - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - key := os.Getenv("TOTP_PRIVATE_KEY") - var privateKey *rsa.PrivateKey - if key == "" { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - - privateKeyPEM := encodePrivateKeyToPEM(privateKey) - os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} - -func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { - // Marshal the private key to DER format. - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - - // Create a PEM block for the private key. - privateKeyPEMBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - - // Encode the PEM block to PEM format. - privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) - - return privateKeyPEM + return status, nil } diff --git a/server/db/providers/cassandradb/totp.go b/server/db/providers/cassandradb/totp.go index b34a6c3..38965d7 100644 --- a/server/db/providers/cassandradb/totp.go +++ b/server/db/providers/cassandradb/totp.go @@ -3,13 +3,8 @@ package cassandradb import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" "image/png" - "os" "time" "github.com/pquerna/otp/totp" @@ -62,44 +57,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - for { - status := totp.Validate(passcode, *user.TotpSecret) + status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) return status, nil } + return status, nil } -} - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - key := os.Getenv("TOTP_PRIVATE_KEY") - var privateKey *rsa.PrivateKey - if key == "" { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - - privateKeyPEM := encodePrivateKeyToPEM(privateKey) - os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} - -func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { - // Marshal the private key to DER format. - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - - // Create a PEM block for the private key. - privateKeyPEMBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - - // Encode the PEM block to PEM format. - privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) - - return privateKeyPEM + return status, nil } diff --git a/server/db/providers/couchbase/totp.go b/server/db/providers/couchbase/totp.go index 4a3b44f..cd8d56b 100644 --- a/server/db/providers/couchbase/totp.go +++ b/server/db/providers/couchbase/totp.go @@ -3,13 +3,8 @@ package couchbase import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" "image/png" - "os" "time" "github.com/pquerna/otp/totp" @@ -62,44 +57,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - for { - status := totp.Validate(passcode, *user.TotpSecret) + status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) return status, nil } + return status, nil } -} - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - key := os.Getenv("TOTP_PRIVATE_KEY") - var privateKey *rsa.PrivateKey - if key == "" { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - - privateKeyPEM := encodePrivateKeyToPEM(privateKey) - os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} - -func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { - // Marshal the private key to DER format. - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - - // Create a PEM block for the private key. - privateKeyPEMBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - - // Encode the PEM block to PEM format. - privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) - - return privateKeyPEM + return status, nil } diff --git a/server/db/providers/dynamodb/totp.go b/server/db/providers/dynamodb/totp.go index 8a8a705..844f57c 100644 --- a/server/db/providers/dynamodb/totp.go +++ b/server/db/providers/dynamodb/totp.go @@ -3,13 +3,8 @@ package dynamodb import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" "image/png" - "os" "time" "github.com/pquerna/otp/totp" @@ -62,44 +57,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - for { - status := totp.Validate(passcode, *user.TotpSecret) + status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) return status, nil } + return status, nil } -} - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - key := os.Getenv("TOTP_PRIVATE_KEY") - var privateKey *rsa.PrivateKey - if key == "" { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - - privateKeyPEM := encodePrivateKeyToPEM(privateKey) - os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} - -func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { - // Marshal the private key to DER format. - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - - // Create a PEM block for the private key. - privateKeyPEMBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - - // Encode the PEM block to PEM format. - privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) - - return privateKeyPEM + return status, nil } diff --git a/server/db/providers/mongodb/totp.go b/server/db/providers/mongodb/totp.go index 220c1c8..8070850 100644 --- a/server/db/providers/mongodb/totp.go +++ b/server/db/providers/mongodb/totp.go @@ -3,13 +3,8 @@ package mongodb import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" "image/png" - "os" "time" "github.com/pquerna/otp/totp" @@ -62,44 +57,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - for { - status := totp.Validate(passcode, *user.TotpSecret) + status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) return status, nil } + return status, nil } -} - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - key := os.Getenv("TOTP_PRIVATE_KEY") - var privateKey *rsa.PrivateKey - if key == "" { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - - privateKeyPEM := encodePrivateKeyToPEM(privateKey) - os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} - -func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { - // Marshal the private key to DER format. - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - - // Create a PEM block for the private key. - privateKeyPEMBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - - // Encode the PEM block to PEM format. - privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) - - return privateKeyPEM + return status, nil } diff --git a/server/db/providers/provider_template/totp.go b/server/db/providers/provider_template/totp.go index 9da1d0d..4e349e5 100644 --- a/server/db/providers/provider_template/totp.go +++ b/server/db/providers/provider_template/totp.go @@ -3,13 +3,8 @@ package provider_template import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" "image/png" - "os" "time" "github.com/pquerna/otp/totp" @@ -62,44 +57,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - for { - status := totp.Validate(passcode, *user.TotpSecret) + status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) return status, nil } + return status, nil } -} - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - key := os.Getenv("TOTP_PRIVATE_KEY") - var privateKey *rsa.PrivateKey - if key == "" { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - - privateKeyPEM := encodePrivateKeyToPEM(privateKey) - os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} - -func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { - // Marshal the private key to DER format. - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - - // Create a PEM block for the private key. - privateKeyPEMBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - - // Encode the PEM block to PEM format. - privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) - - return privateKeyPEM + return status, nil } diff --git a/server/db/providers/sql/totp.go b/server/db/providers/sql/totp.go index 2e8ad11..66a46aa 100644 --- a/server/db/providers/sql/totp.go +++ b/server/db/providers/sql/totp.go @@ -3,8 +3,6 @@ package sql import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" "fmt" log "github.com/sirupsen/logrus" "image/png" @@ -60,18 +58,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { + if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) + return status, nil + } + return status, nil + } return status, nil } - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 91040be..002a00b 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -3,6 +3,7 @@ package resolvers import ( "context" "fmt" + "github.com/authorizerdev/authorizer/server/crypto" "strings" "time" @@ -161,7 +162,21 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes } if !isMFADisabled && refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isTOTPLoginDisabled { - if user.TotpSecret == nil { + pubKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey) + if err != nil { + log.Debug("error while getting public key") + } + + publicKey, err := crypto.ParseRsaPublicKeyFromPemStr(pubKey) + if err != nil { + log.Debug("error while parsing public key") + } + + encryptedUserId, err := crypto.EncryptRSA(user.ID, *publicKey) + if err != nil { + log.Debug("error while encrypting user id") + } + if !user.TotpVerified { base64URL, err := db.Provider.GenerateTotp(ctx, user.ID) if err != nil { log.Debug("error while generating base64 url: ", err) @@ -170,14 +185,13 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes res = &model.AuthResponse{ Message: `Proceed to totp screen`, TotpBase64url: base64URL, - TokenTotp: &user.ID, + TokenTotp: &encryptedUserId, } return res, nil } else { - //res.TokenTotp = &user.ID res = &model.AuthResponse{ Message: `Proceed to totp screen`, - TokenTotp: &user.ID, + TokenTotp: &encryptedUserId, } return res, nil } diff --git a/server/resolvers/verify_totp.go b/server/resolvers/verify_totp.go index c335aa0..97aa592 100644 --- a/server/resolvers/verify_totp.go +++ b/server/resolvers/verify_totp.go @@ -3,6 +3,7 @@ package resolvers import ( "context" "fmt" + "github.com/authorizerdev/authorizer/server/crypto" "strings" "time" @@ -24,7 +25,22 @@ import ( func VerifyTotpResolver(ctx context.Context, params model.VerifyTOTPRequest) (*model.AuthResponse, error) { var res *model.AuthResponse - userID := params.Token + encryptedkey := params.Token + + pvtKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyJwtPrivateKey) + if err != nil { + log.Debug("error while getting private key") + } + + privateKey, err := crypto.ParseRsaPrivateKeyFromPemStr(pvtKey) + if err != nil { + log.Debug("error while parsing private key") + } + + userID, err := crypto.DecryptRSA(encryptedkey, *privateKey) + if err != nil { + log.Debug("error while decrypting userId") + } gc, err := utils.GinContextFromContext(ctx) if err != nil { diff --git a/server/test/integration_test.go b/server/test/integration_test.go index 3d4bb3d..415cb0a 100644 --- a/server/test/integration_test.go +++ b/server/test/integration_test.go @@ -141,6 +141,7 @@ func TestResolvers(t *testing.T) { inviteUserTest(t, s) validateJwtTokenTest(t, s) verifyOTPTest(t, s) + verifyTOTPTest(t, s) resendOTPTest(t, s) validateSessionTests(t, s) diff --git a/server/test/verify_totp_test.go b/server/test/verify_totp_test.go new file mode 100644 index 0000000..741f347 --- /dev/null +++ b/server/test/verify_totp_test.go @@ -0,0 +1,76 @@ +package test + +import ( + "testing" +) + +func verifyTOTPTest(t *testing.T, s TestSetup) { + //t.Helper() + //t.Run(`should verify totp`, func(t *testing.T) { + // req, ctx := createContext(s) + // email := "verify_otp." + s.TestInfo.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.GenerateTotp(ctx, loginRes.User.ID) + // 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) + // updateProfileRes, err := resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{ + // IsMultiFactorAuthEnabled: refs.NewBoolRef(true), + // }) + // assert.NoError(t, err) + // assert.NotEmpty(t, updateProfileRes.Message) + // + // // Login should not return error but access token should be empty as otp should have been sent + // 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.AccessToken) + // + // // Get otp from db + // otp, err := db.Provider.GetOTPByEmail(ctx, email) + // assert.NoError(t, err) + // assert.NotEmpty(t, otp.Otp) + // // Get user by email + // user, err := db.Provider.GetUserByEmail(ctx, email) + // assert.NoError(t, err) + // assert.NotNil(t, user) + // // Set mfa cookie session + // mfaSession := uuid.NewString() + // memorystore.Provider.SetMfaSession(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) + // verifyOtpRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ + // Email: &email, + // Otp: otp.Otp, + // }) + // assert.Nil(t, err) + // assert.NotEqual(t, verifyOtpRes.AccessToken, "", "access token should not be empty") + // cleanData(email) + //}) +} From 0e931d6e65562fdebacc07164fcda5093d0904ec Mon Sep 17 00:00:00 2001 From: lemonScaletech Date: Mon, 11 Sep 2023 11:45:32 +0530 Subject: [PATCH 6/8] feat: * test cases totp --- server/test/verify_totp_test.go | 141 +++++++++++++++++--------------- 1 file changed, 73 insertions(+), 68 deletions(-) diff --git a/server/test/verify_totp_test.go b/server/test/verify_totp_test.go index 741f347..104a4b0 100644 --- a/server/test/verify_totp_test.go +++ b/server/test/verify_totp_test.go @@ -1,76 +1,81 @@ package test import ( + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/resolvers" + "github.com/stretchr/testify/assert" "testing" ) func verifyTOTPTest(t *testing.T, s TestSetup) { - //t.Helper() - //t.Run(`should verify totp`, func(t *testing.T) { - // req, ctx := createContext(s) - // email := "verify_otp." + s.TestInfo.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.GenerateTotp(ctx, loginRes.User.ID) - // 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) - // updateProfileRes, err := resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{ - // IsMultiFactorAuthEnabled: refs.NewBoolRef(true), - // }) - // assert.NoError(t, err) - // assert.NotEmpty(t, updateProfileRes.Message) - // - // // Login should not return error but access token should be empty as otp should have been sent - // 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.AccessToken) - // - // // Get otp from db - // otp, err := db.Provider.GetOTPByEmail(ctx, email) - // assert.NoError(t, err) - // assert.NotEmpty(t, otp.Otp) - // // Get user by email - // user, err := db.Provider.GetUserByEmail(ctx, email) - // assert.NoError(t, err) - // assert.NotNil(t, user) - // // Set mfa cookie session - // mfaSession := uuid.NewString() - // memorystore.Provider.SetMfaSession(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) - // verifyOtpRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ - // Email: &email, - // Otp: otp.Otp, - // }) - // assert.Nil(t, err) - // assert.NotEqual(t, verifyOtpRes.AccessToken, "", "access token should not be empty") - // cleanData(email) - //}) + t.Helper() + t.Run(`should verify totp`, func(t *testing.T) { + _, ctx := createContext(s) + email := "verify_otp." + s.TestInfo.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) + _, err = db.Provider.GenerateTotp(ctx, loginRes.User.ID) + assert.Nil(t, err) + //assert.Equal(t, ??, string) + // 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) + // updateProfileRes, err := resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{ + // IsMultiFactorAuthEnabled: refs.NewBoolRef(true), + // }) + // assert.NoError(t, err) + // assert.NotEmpty(t, updateProfileRes.Message) + // + // // Login should not return error but access token should be empty as otp should have been sent + // 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.AccessToken) + // + // // Get otp from db + // otp, err := db.Provider.GetOTPByEmail(ctx, email) + // assert.NoError(t, err) + // assert.NotEmpty(t, otp.Otp) + // // Get user by email + // user, err := db.Provider.GetUserByEmail(ctx, email) + // assert.NoError(t, err) + // assert.NotNil(t, user) + // // Set mfa cookie session + // mfaSession := uuid.NewString() + // memorystore.Provider.SetMfaSession(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) + // verifyOtpRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ + // Email: &email, + // Otp: otp.Otp, + // }) + // assert.Nil(t, err) + // assert.NotEqual(t, verifyOtpRes.AccessToken, "", "access token should not be empty") + // cleanData(email) + //}) + }) } From 96fdc38c3cce3d81b56b9b56fa1d7f9caf462da6 Mon Sep 17 00:00:00 2001 From: lemonScaletech Date: Tue, 12 Sep 2023 19:09:37 +0530 Subject: [PATCH 7/8] test-cases: * completed test cases * tested for all dbs --- server/db/providers/cassandradb/provider.go | 8 + server/db/providers/cassandradb/user.go | 19 ++- server/db/providers/couchbase/user.go | 4 +- server/go.mod | 2 + server/go.sum | 10 ++ server/graph/generated/generated.go | 68 ++++++++ server/graph/model/models_gen.go | 1 + server/graph/schema.graphqls | 1 + server/test/resend_otp_test.go | 3 + server/test/revoke_access_test.go | 2 + server/test/verification_requests_test.go | 2 + server/test/verify_otp_test.go | 2 + server/test/verify_totp_test.go | 166 ++++++++++++++------ 13 files changed, 230 insertions(+), 58 deletions(-) diff --git a/server/db/providers/cassandradb/provider.go b/server/db/providers/cassandradb/provider.go index 6f1fe6b..d2afcfc 100644 --- a/server/db/providers/cassandradb/provider.go +++ b/server/db/providers/cassandradb/provider.go @@ -267,6 +267,14 @@ func NewProvider() (*provider, error) { if err != nil { return nil, err } + // add totp_secret and totp_verified on users table + totpTableAlterQuery := fmt.Sprintf(`ALTER TABLE %s.%s ADD (totp_verified boolean, totp_secret text, app_data text)`, KeySpace, models.Collections.User) + err = session.Query(totpTableAlterQuery).Exec() + if err != nil { + log.Debug("Failed to alter table as column exists: ", err) + // return nil, err + } + return &provider{ db: session, }, err diff --git a/server/db/providers/cassandradb/user.go b/server/db/providers/cassandradb/user.go index 376a6db..8b34a32 100644 --- a/server/db/providers/cassandradb/user.go +++ b/server/db/providers/cassandradb/user.go @@ -39,6 +39,7 @@ func (p *provider) AddUser(ctx context.Context, user *models.User) (*models.User user.CreatedAt = time.Now().Unix() user.UpdatedAt = time.Now().Unix() + user.TotpVerified = false bytes, err := json.Marshal(user) if err != nil { @@ -177,13 +178,19 @@ 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, 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,"+ + " created_at, updated_at, totp_verified 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.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.CreatedAt, &user.UpdatedAt, &user.TotpVerified) if err != nil { return nil, err } @@ -200,8 +207,8 @@ func (p *provider) ListUsers(ctx context.Context, pagination *model.Pagination) // GetUserByEmail to get user information from database using email address func (p *provider) GetUserByEmail(ctx context.Context, email string) (*models.User, error) { var user models.User - 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, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING", KeySpace+"."+models.Collections.User, email) - err := p.db.Query(query).Consistency(gocql.One).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.CreatedAt, &user.UpdatedAt) + 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, created_at, updated_at, totp_secret, totp_verified, app_data FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING", KeySpace+"."+models.Collections.User, email) + err := p.db.Query(query).Consistency(gocql.One).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.CreatedAt, &user.UpdatedAt, &user.TotpSecret, &user.TotpVerified, &user.AppData) if err != nil { return nil, err } @@ -211,8 +218,8 @@ func (p *provider) GetUserByEmail(ctx context.Context, email string) (*models.Us // GetUserByID to get user information from database using user ID func (p *provider) GetUserByID(ctx context.Context, id string) (*models.User, error) { var user models.User - 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, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1", KeySpace+"."+models.Collections.User, id) - err := p.db.Query(query).Consistency(gocql.One).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.CreatedAt, &user.UpdatedAt) + 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, created_at, updated_at, totp_secret, totp_verified, app_data FROM %s WHERE id = '%s' LIMIT 1", KeySpace+"."+models.Collections.User, id) + err := p.db.Query(query).Consistency(gocql.One).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.CreatedAt, &user.UpdatedAt, &user.TotpSecret, &user.TotpVerified, &user.AppData) if err != nil { return nil, err } diff --git a/server/db/providers/couchbase/user.go b/server/db/providers/couchbase/user.go index f5d2195..17c3d03 100644 --- a/server/db/providers/couchbase/user.go +++ b/server/db/providers/couchbase/user.go @@ -103,7 +103,7 @@ func (p *provider) ListUsers(ctx context.Context, pagination *model.Pagination) // GetUserByEmail to get user information from database using email address func (p *provider) GetUserByEmail(ctx context.Context, email string) (*models.User, error) { var user *models.User - 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, created_at, updated_at FROM %s.%s WHERE email = $1 LIMIT 1", p.scopeName, models.Collections.User) + 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, created_at, updated_at, totp_secret, totp_verified FROM %s.%s WHERE email = $1 LIMIT 1", p.scopeName, models.Collections.User) q, err := p.db.Query(query, &gocb.QueryOptions{ ScanConsistency: gocb.QueryScanConsistencyRequestPlus, Context: ctx, @@ -122,7 +122,7 @@ func (p *provider) GetUserByEmail(ctx context.Context, email string) (*models.Us // GetUserByID to get user information from database using user ID func (p *provider) GetUserByID(ctx context.Context, id string) (*models.User, error) { var user *models.User - 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, created_at, updated_at FROM %s.%s WHERE _id = $1 LIMIT 1", p.scopeName, models.Collections.User) + 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, created_at, updated_at, totp_secret, totp_verified FROM %s.%s WHERE _id = $1 LIMIT 1", p.scopeName, models.Collections.User) q, err := p.db.Query(query, &gocb.QueryOptions{ ScanConsistency: gocb.QueryScanConsistencyRequestPlus, Context: ctx, diff --git a/server/go.mod b/server/go.mod index 8404573..86efdc4 100644 --- a/server/go.mod +++ b/server/go.mod @@ -13,6 +13,7 @@ require ( github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/goccy/go-json v0.9.11 // indirect github.com/gocql/gocql v1.2.0 + github.com/gokyle/twofactor v1.0.1 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.6 // indirect @@ -26,6 +27,7 @@ require ( github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.8.0 + github.com/tuotoo/qrcode v0.0.0-20220425170535-52ccc2bebf5d github.com/twilio/twilio-go v1.7.2 github.com/vektah/gqlparser/v2 v2.5.1 go.mongodb.org/mongo-driver v1.8.1 diff --git a/server/go.sum b/server/go.sum index 4b61a81..467607b 100644 --- a/server/go.sum +++ b/server/go.sum @@ -56,6 +56,8 @@ github.com/aws/aws-sdk-go v1.44.298/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8 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.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= @@ -131,6 +133,8 @@ github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gocql/gocql v1.2.0 h1:TZhsCd7fRuye4VyHr3WCvWwIQaZUmjsqnSIXK9FcVCE= github.com/gocql/gocql v1.2.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.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 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= @@ -256,6 +260,8 @@ github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ic github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q= github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= +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= @@ -323,6 +329,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +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.7.2 h1:tX38DXbSuDWWIK+tKAE2AJSMR6d8i7lf9ksY8J29VLE= github.com/twilio/twilio-go v1.7.2/go.mod h1:tdnfQ5TjbewoAu4lf9bMsGvfuJ/QU9gYuv9yx3TSIXU= github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= @@ -747,5 +755,7 @@ modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.6.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +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= diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index a7b3209..7217bc5 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -272,6 +272,7 @@ type ComplexityRoot struct { RevokedTimestamp func(childComplexity int) int Roles func(childComplexity int) int SignupMethods func(childComplexity int) int + TotpVerified func(childComplexity int) int UpdatedAt func(childComplexity int) int } @@ -1900,6 +1901,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.User.SignupMethods(childComplexity), true + case "User.totp_verified": + if e.complexity.User.TotpVerified == nil { + break + } + + return e.complexity.User.TotpVerified(childComplexity), true + case "User.updated_at": if e.complexity.User.UpdatedAt == nil { break @@ -2317,6 +2325,7 @@ type User { revoked_timestamp: Int64 is_multi_factor_auth_enabled: Boolean app_data: Map + totp_verified: Boolean } type Users { @@ -4013,6 +4022,8 @@ func (ec *executionContext) fieldContext_AuthResponse_user(ctx context.Context, return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field) case "app_data": return ec.fieldContext_User_app_data(ctx, field) + case "totp_verified": + return ec.fieldContext_User_totp_verified(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -7524,6 +7535,8 @@ func (ec *executionContext) fieldContext_InviteMembersResponse_Users(ctx context return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field) case "app_data": return ec.fieldContext_User_app_data(ctx, field) + case "totp_verified": + return ec.fieldContext_User_totp_verified(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -9357,6 +9370,8 @@ func (ec *executionContext) fieldContext_Mutation__update_user(ctx context.Conte return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field) case "app_data": return ec.fieldContext_User_app_data(ctx, field) + case "totp_verified": + return ec.fieldContext_User_totp_verified(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -10669,6 +10684,8 @@ func (ec *executionContext) fieldContext_Query_profile(ctx context.Context, fiel return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field) case "app_data": return ec.fieldContext_User_app_data(ctx, field) + case "totp_verified": + return ec.fieldContext_User_totp_verified(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -10938,6 +10955,8 @@ func (ec *executionContext) fieldContext_Query__user(ctx context.Context, field return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field) case "app_data": return ec.fieldContext_User_app_data(ctx, field) + case "totp_verified": + return ec.fieldContext_User_totp_verified(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -12846,6 +12865,47 @@ func (ec *executionContext) fieldContext_User_app_data(ctx context.Context, fiel return fc, nil } +func (ec *executionContext) _User_totp_verified(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_User_totp_verified(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.TotpVerified, 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_User_totp_verified(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "User", + 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) _Users_pagination(ctx context.Context, field graphql.CollectedField, obj *model.Users) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Users_pagination(ctx, field) if err != nil { @@ -12979,6 +13039,8 @@ func (ec *executionContext) fieldContext_Users_users(ctx context.Context, field return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field) case "app_data": return ec.fieldContext_User_app_data(ctx, field) + case "totp_verified": + return ec.fieldContext_User_totp_verified(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -13194,6 +13256,8 @@ func (ec *executionContext) fieldContext_ValidateSessionResponse_user(ctx contex return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field) case "app_data": return ec.fieldContext_User_app_data(ctx, field) + case "totp_verified": + return ec.fieldContext_User_totp_verified(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -20329,6 +20393,10 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = ec._User_app_data(ctx, field, obj) + case "totp_verified": + + out.Values[i] = ec._User_totp_verified(ctx, field, obj) + default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 8037700..c5586a8 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -455,6 +455,7 @@ type User struct { RevokedTimestamp *int64 `json:"revoked_timestamp"` IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"` AppData map[string]interface{} `json:"app_data"` + TotpVerified *bool `json:"totp_verified"` } type Users struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 9fe68ba..7805861 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -52,6 +52,7 @@ type User { revoked_timestamp: Int64 is_multi_factor_auth_enabled: Boolean app_data: Map + totp_verified: Boolean } type Users { diff --git a/server/test/resend_otp_test.go b/server/test/resend_otp_test.go index 3f1e738..ffc0222 100644 --- a/server/test/resend_otp_test.go +++ b/server/test/resend_otp_test.go @@ -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), diff --git a/server/test/revoke_access_test.go b/server/test/revoke_access_test.go index 4be042d..2f83d3e 100644 --- a/server/test/revoke_access_test.go +++ b/server/test/revoke_access_test.go @@ -28,6 +28,8 @@ func revokeAccessTest(t *testing.T, s TestSetup) { verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{ Token: verificationRequest.Token, }) + fmt.Println("\n", verifyRes) + fmt.Println("\n", err) assert.NoError(t, err) assert.NotNil(t, verifyRes.AccessToken) diff --git a/server/test/verification_requests_test.go b/server/test/verification_requests_test.go index e5d5d73..11b7596 100644 --- a/server/test/verification_requests_test.go +++ b/server/test/verification_requests_test.go @@ -23,6 +23,8 @@ func verificationRequestsTest(t *testing.T, s TestSetup) { Password: s.TestInfo.Password, ConfirmPassword: s.TestInfo.Password, }) + fmt.Println("res", res) + fmt.Println("err", err) assert.NoError(t, err) assert.NotNil(t, res) limit := int64(10) diff --git a/server/test/verify_otp_test.go b/server/test/verify_otp_test.go index 455ac12..ca7b2fa 100644 --- a/server/test/verify_otp_test.go +++ b/server/test/verify_otp_test.go @@ -54,6 +54,8 @@ func verifyOTPTest(t *testing.T, s TestSetup) { }) assert.NoError(t, err) assert.NotEmpty(t, updateProfileRes.Message) + memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableMailOTPLogin, false) + memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableTOTPLogin, true) // Login should not return error but access token should be empty as otp should have been sent loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{ diff --git a/server/test/verify_totp_test.go b/server/test/verify_totp_test.go index 104a4b0..c4f38d2 100644 --- a/server/test/verify_totp_test.go +++ b/server/test/verify_totp_test.go @@ -1,18 +1,30 @@ package test import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "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/stretchr/testify/assert" + "github.com/tuotoo/qrcode" + "strings" "testing" ) func verifyTOTPTest(t *testing.T, s TestSetup) { t.Helper() t.Run(`should verify totp`, func(t *testing.T) { - _, ctx := createContext(s) - email := "verify_otp." + s.TestInfo.Email + 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, @@ -28,54 +40,108 @@ func verifyTOTPTest(t *testing.T, s TestSetup) { }) assert.Error(t, err) assert.Nil(t, loginRes) - _, err = db.Provider.GenerateTotp(ctx, loginRes.User.ID) + verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeBasicAuthSignup) assert.Nil(t, err) - //assert.Equal(t, ??, string) - // 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) - // updateProfileRes, err := resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{ - // IsMultiFactorAuthEnabled: refs.NewBoolRef(true), - // }) - // assert.NoError(t, err) - // assert.NotEmpty(t, updateProfileRes.Message) - // - // // Login should not return error but access token should be empty as otp should have been sent - // 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.AccessToken) - // - // // Get otp from db - // otp, err := db.Provider.GetOTPByEmail(ctx, email) - // assert.NoError(t, err) - // assert.NotEmpty(t, otp.Otp) - // // Get user by email - // user, err := db.Provider.GetUserByEmail(ctx, email) - // assert.NoError(t, err) - // assert.NotNil(t, user) - // // Set mfa cookie session - // mfaSession := uuid.NewString() - // memorystore.Provider.SetMfaSession(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) - // verifyOtpRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ - // Email: &email, - // Otp: otp.Otp, - // }) - // assert.Nil(t, err) - // assert.NotEqual(t, verifyOtpRes.AccessToken, "", "access token should not be empty") - // cleanData(email) - //}) + 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) + updateProfileRes, err := resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{ + IsMultiFactorAuthEnabled: refs.NewBoolRef(true), + }) + assert.NoError(t, err) + assert.NotEmpty(t, updateProfileRes.Message) + memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableTOTPLogin, false) + memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableMailOTPLogin, true) + + // 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.NotNil(t, loginRes.TotpBase64url) + assert.NotNil(t, loginRes.TokenTotp) + assert.Nil(t, loginRes.AccessToken) + assert.Equal(t, loginRes.Message, `Proceed to totp screen`) + + // get totp url for validation + pngBytes, err := base64.StdEncoding.DecodeString(*loginRes.TotpBase64url) + 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) + + valid, err := resolvers.VerifyTotpResolver(ctx, model.VerifyTOTPRequest{ + Otp: code, + Token: *loginRes.TokenTotp, + }) + + accessToken := *valid.AccessToken + assert.NoError(t, err) + assert.NotNil(t, accessToken) + assert.Equal(t, `Logged in successfully`, 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.Equal(t, logout.Message, `Logged out successfully`) + + loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{ + Email: email, + Password: s.TestInfo.Password, + }) + assert.NoError(t, err) + assert.NotNil(t, loginRes) + assert.NotNil(t, loginRes.TokenTotp) + assert.Nil(t, loginRes.TotpBase64url) + assert.Nil(t, loginRes.AccessToken) + assert.Equal(t, loginRes.Message, `Proceed to totp screen`) + + code = tf.OTP() + assert.NotEmpty(t, code) + + valid, err = resolvers.VerifyTotpResolver(ctx, model.VerifyTOTPRequest{ + Otp: code, + Token: *loginRes.TokenTotp, + }) + assert.NoError(t, err) + assert.NotNil(t, *valid.AccessToken) + assert.Equal(t, `Logged in successfully`, valid.Message) + + cleanData(email) }) } From af66958b5db4ad9a68a56679dc52b17f871735c0 Mon Sep 17 00:00:00 2001 From: lemonScaletech Date: Wed, 13 Sep 2023 14:14:56 +0530 Subject: [PATCH 8/8] fixes: * return variable to snake case * import refactoring --- server/graph/generated/generated.go | 264 +++++----------------- server/graph/model/models_gen.go | 22 +- server/graph/schema.graphqls | 6 +- server/resolvers/login.go | 18 +- server/resolvers/verify_totp.go | 5 +- server/test/revoke_access_test.go | 2 - server/test/verification_requests_test.go | 2 - server/test/verify_totp_test.go | 26 ++- 8 files changed, 103 insertions(+), 242 deletions(-) diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 7217bc5..4ad7cbe 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -45,18 +45,16 @@ 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 - ShouldShowMobileTotpScreen func(childComplexity int) int - ShouldShowTotpScreen func(childComplexity int) int - TokenTotp func(childComplexity int) int - TotpBase64url func(childComplexity int) int - User func(childComplexity int) int + 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 + TotpBase64URL func(childComplexity int) int + TotpToken func(childComplexity int) int + User func(childComplexity int) int } EmailTemplate struct { @@ -455,33 +453,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AuthResponse.ShouldShowMobileOtpScreen(childComplexity), true - case "AuthResponse.should_show_mobile_totp_screen": - if e.complexity.AuthResponse.ShouldShowMobileTotpScreen == nil { + case "AuthResponse.totp_base64_url": + if e.complexity.AuthResponse.TotpBase64URL == nil { break } - return e.complexity.AuthResponse.ShouldShowMobileTotpScreen(childComplexity), true + return e.complexity.AuthResponse.TotpBase64URL(childComplexity), true - case "AuthResponse.should_show_totp_screen": - if e.complexity.AuthResponse.ShouldShowTotpScreen == nil { + case "AuthResponse.totp_token": + if e.complexity.AuthResponse.TotpToken == nil { break } - return e.complexity.AuthResponse.ShouldShowTotpScreen(childComplexity), true - - case "AuthResponse.tokenTOTP": - if e.complexity.AuthResponse.TokenTotp == nil { - break - } - - return e.complexity.AuthResponse.TokenTotp(childComplexity), true - - case "AuthResponse.totpBase64URL": - if e.complexity.AuthResponse.TotpBase64url == nil { - break - } - - return e.complexity.AuthResponse.TotpBase64url(childComplexity), true + return e.complexity.AuthResponse.TotpToken(childComplexity), true case "AuthResponse.user": if e.complexity.AuthResponse.User == nil { @@ -2368,15 +2352,13 @@ type AuthResponse { message: String! should_show_email_otp_screen: Boolean should_show_mobile_otp_screen: Boolean - should_show_mobile_totp_screen: Boolean - should_show_totp_screen: Boolean access_token: String id_token: String refresh_token: String expires_in: Int64 user: User - totpBase64URL: String - tokenTOTP: String + totp_base64_url: String + totp_token: String } type Response { @@ -3700,88 +3682,6 @@ func (ec *executionContext) fieldContext_AuthResponse_should_show_mobile_otp_scr return fc, nil } -func (ec *executionContext) _AuthResponse_should_show_mobile_totp_screen(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_AuthResponse_should_show_mobile_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.ShouldShowMobileTotpScreen, 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_mobile_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_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 { @@ -4031,8 +3931,8 @@ func (ec *executionContext) fieldContext_AuthResponse_user(ctx context.Context, return fc, nil } -func (ec *executionContext) _AuthResponse_totpBase64URL(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) +func (ec *executionContext) _AuthResponse_totp_base64_url(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AuthResponse_totp_base64_url(ctx, field) if err != nil { return graphql.Null } @@ -4045,7 +3945,7 @@ func (ec *executionContext) _AuthResponse_totpBase64URL(ctx context.Context, fie }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.TotpBase64url, nil + return obj.TotpBase64URL, nil }) if err != nil { ec.Error(ctx, err) @@ -4059,7 +3959,7 @@ func (ec *executionContext) _AuthResponse_totpBase64URL(ctx context.Context, fie return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_AuthResponse_totpBase64URL(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_AuthResponse_totp_base64_url(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "AuthResponse", Field: field, @@ -4072,8 +3972,8 @@ func (ec *executionContext) fieldContext_AuthResponse_totpBase64URL(ctx context. return fc, nil } -func (ec *executionContext) _AuthResponse_tokenTOTP(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) +func (ec *executionContext) _AuthResponse_totp_token(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AuthResponse_totp_token(ctx, field) if err != nil { return graphql.Null } @@ -4086,7 +3986,7 @@ func (ec *executionContext) _AuthResponse_tokenTOTP(ctx context.Context, field g }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.TokenTotp, nil + return obj.TotpToken, nil }) if err != nil { ec.Error(ctx, err) @@ -4100,7 +4000,7 @@ func (ec *executionContext) _AuthResponse_tokenTOTP(ctx context.Context, field g return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_AuthResponse_tokenTOTP(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_AuthResponse_totp_token(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "AuthResponse", Field: field, @@ -8249,10 +8149,6 @@ 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_mobile_totp_screen": - return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -8263,10 +8159,10 @@ 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 "totpBase64URL": - return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) - case "tokenTOTP": - return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) + case "totp_base64_url": + return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field) + case "totp_token": + return ec.fieldContext_AuthResponse_totp_token(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8330,10 +8226,6 @@ 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_mobile_totp_screen": - return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -8344,10 +8236,10 @@ 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 "totpBase64URL": - return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) - case "tokenTOTP": - return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) + case "totp_base64_url": + return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field) + case "totp_token": + return ec.fieldContext_AuthResponse_totp_token(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8411,10 +8303,6 @@ 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_mobile_totp_screen": - return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -8425,10 +8313,10 @@ 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 "totpBase64URL": - return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) - case "tokenTOTP": - return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) + case "totp_base64_url": + return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field) + case "totp_token": + return ec.fieldContext_AuthResponse_totp_token(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8492,10 +8380,6 @@ 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_mobile_totp_screen": - return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -8506,10 +8390,10 @@ 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 "totpBase64URL": - return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) - case "tokenTOTP": - return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) + case "totp_base64_url": + return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field) + case "totp_token": + return ec.fieldContext_AuthResponse_totp_token(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -8739,10 +8623,6 @@ 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_mobile_totp_screen": - return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -8753,10 +8633,10 @@ 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 "totpBase64URL": - return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) - case "tokenTOTP": - return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) + case "totp_base64_url": + return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field) + case "totp_token": + return ec.fieldContext_AuthResponse_totp_token(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -9056,10 +8936,6 @@ 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_mobile_totp_screen": - return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -9070,10 +8946,10 @@ 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 "totpBase64URL": - return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) - case "tokenTOTP": - return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) + case "totp_base64_url": + return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field) + case "totp_token": + return ec.fieldContext_AuthResponse_totp_token(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -9196,10 +9072,6 @@ func (ec *executionContext) fieldContext_Mutation_verify_totp(ctx context.Contex 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_mobile_totp_screen": - return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -9210,10 +9082,10 @@ func (ec *executionContext) fieldContext_Mutation_verify_totp(ctx context.Contex return ec.fieldContext_AuthResponse_expires_in(ctx, field) case "user": return ec.fieldContext_AuthResponse_user(ctx, field) - case "totpBase64URL": - return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) - case "tokenTOTP": - return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) + case "totp_base64_url": + return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field) + case "totp_token": + return ec.fieldContext_AuthResponse_totp_token(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -10569,10 +10441,6 @@ 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_mobile_totp_screen": - return ec.fieldContext_AuthResponse_should_show_mobile_totp_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": @@ -10583,10 +10451,10 @@ 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 "totpBase64URL": - return ec.fieldContext_AuthResponse_totpBase64URL(ctx, field) - case "tokenTOTP": - return ec.fieldContext_AuthResponse_tokenTOTP(ctx, field) + case "totp_base64_url": + return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field) + case "totp_token": + return ec.fieldContext_AuthResponse_totp_token(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) }, @@ -18740,14 +18608,6 @@ func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.Selection out.Values[i] = ec._AuthResponse_should_show_mobile_otp_screen(ctx, field, obj) - case "should_show_mobile_totp_screen": - - out.Values[i] = ec._AuthResponse_should_show_mobile_totp_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) @@ -18768,13 +18628,13 @@ func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.Selection out.Values[i] = ec._AuthResponse_user(ctx, field, obj) - case "totpBase64URL": + case "totp_base64_url": - out.Values[i] = ec._AuthResponse_totpBase64URL(ctx, field, obj) + out.Values[i] = ec._AuthResponse_totp_base64_url(ctx, field, obj) - case "tokenTOTP": + case "totp_token": - out.Values[i] = ec._AuthResponse_tokenTOTP(ctx, field, obj) + out.Values[i] = ec._AuthResponse_totp_token(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index c5586a8..d5ae277 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -26,18 +26,16 @@ type AdminSignupInput struct { } type AuthResponse struct { - Message string `json:"message"` - ShouldShowEmailOtpScreen *bool `json:"should_show_email_otp_screen"` - ShouldShowMobileOtpScreen *bool `json:"should_show_mobile_otp_screen"` - ShouldShowMobileTotpScreen *bool `json:"should_show_mobile_totp_screen"` - ShouldShowTotpScreen *bool `json:"should_show_totp_screen"` - AccessToken *string `json:"access_token"` - IDToken *string `json:"id_token"` - RefreshToken *string `json:"refresh_token"` - ExpiresIn *int64 `json:"expires_in"` - User *User `json:"user"` - TotpBase64url *string `json:"totpBase64URL"` - TokenTotp *string `json:"tokenTOTP"` + Message string `json:"message"` + ShouldShowEmailOtpScreen *bool `json:"should_show_email_otp_screen"` + ShouldShowMobileOtpScreen *bool `json:"should_show_mobile_otp_screen"` + AccessToken *string `json:"access_token"` + IDToken *string `json:"id_token"` + RefreshToken *string `json:"refresh_token"` + ExpiresIn *int64 `json:"expires_in"` + User *User `json:"user"` + TotpBase64URL *string `json:"totp_base64_url"` + TotpToken *string `json:"totp_token"` } type DeleteEmailTemplateRequest struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 7805861..bcb0b07 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -95,15 +95,13 @@ type AuthResponse { message: String! should_show_email_otp_screen: Boolean should_show_mobile_otp_screen: Boolean - should_show_mobile_totp_screen: Boolean - should_show_totp_screen: Boolean access_token: String id_token: String refresh_token: String expires_in: Int64 user: User - totpBase64URL: String - tokenTOTP: String + totp_base64_url: String + totp_token: String } type Response { diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 002a00b..78a1f7f 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -3,16 +3,17 @@ package resolvers import ( "context" "fmt" - "github.com/authorizerdev/authorizer/server/crypto" "strings" "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/constants" "github.com/authorizerdev/authorizer/server/cookie" + "github.com/authorizerdev/authorizer/server/crypto" "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/email" @@ -161,6 +162,7 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes }, nil } + // if mfa enabled and also totp enabled if !isMFADisabled && refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isTOTPLoginDisabled { pubKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey) if err != nil { @@ -172,26 +174,30 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes log.Debug("error while parsing public key") } + //encrypting user id, so it can be used as token for verifying encryptedUserId, err := crypto.EncryptRSA(user.ID, *publicKey) if err != nil { log.Debug("error while encrypting user id") } + + // for first time user or whose totp is not verified if !user.TotpVerified { base64URL, err := db.Provider.GenerateTotp(ctx, user.ID) if err != nil { log.Debug("error while generating base64 url: ", err) } - + // when user is first time registering for totp res = &model.AuthResponse{ Message: `Proceed to totp screen`, - TotpBase64url: base64URL, - TokenTotp: &encryptedUserId, + TotpBase64URL: base64URL, + TotpToken: &encryptedUserId, } return res, nil } else { + //when user is already register for totp res = &model.AuthResponse{ Message: `Proceed to totp screen`, - TokenTotp: &encryptedUserId, + TotpToken: &encryptedUserId, } return res, nil } diff --git a/server/resolvers/verify_totp.go b/server/resolvers/verify_totp.go index 97aa592..900aaff 100644 --- a/server/resolvers/verify_totp.go +++ b/server/resolvers/verify_totp.go @@ -3,15 +3,16 @@ package resolvers import ( "context" "fmt" - "github.com/authorizerdev/authorizer/server/crypto" "strings" "time" "github.com/google/uuid" + log "github.com/sirupsen/logrus" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie" + "github.com/authorizerdev/authorizer/server/crypto" "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/graph/model" @@ -55,7 +56,7 @@ func VerifyTotpResolver(ctx context.Context, params model.VerifyTOTPRequest) (*m status, err := db.Provider.ValidatePasscode(ctx, params.Otp, userID) if err != nil || !status { - return nil, fmt.Errorf("error while validating passcode", err) + return nil, fmt.Errorf("error while validating passcode") } code := "" diff --git a/server/test/revoke_access_test.go b/server/test/revoke_access_test.go index 2f83d3e..4be042d 100644 --- a/server/test/revoke_access_test.go +++ b/server/test/revoke_access_test.go @@ -28,8 +28,6 @@ func revokeAccessTest(t *testing.T, s TestSetup) { verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{ Token: verificationRequest.Token, }) - fmt.Println("\n", verifyRes) - fmt.Println("\n", err) assert.NoError(t, err) assert.NotNil(t, verifyRes.AccessToken) diff --git a/server/test/verification_requests_test.go b/server/test/verification_requests_test.go index 11b7596..e5d5d73 100644 --- a/server/test/verification_requests_test.go +++ b/server/test/verification_requests_test.go @@ -23,8 +23,6 @@ func verificationRequestsTest(t *testing.T, s TestSetup) { Password: s.TestInfo.Password, ConfirmPassword: s.TestInfo.Password, }) - fmt.Println("res", res) - fmt.Println("err", err) assert.NoError(t, err) assert.NotNil(t, res) limit := int64(10) diff --git a/server/test/verify_totp_test.go b/server/test/verify_totp_test.go index c4f38d2..0bb28de 100644 --- a/server/test/verify_totp_test.go +++ b/server/test/verify_totp_test.go @@ -5,6 +5,13 @@ import ( "context" "encoding/base64" "fmt" + "strings" + "testing" + + "github.com/gokyle/twofactor" + "github.com/stretchr/testify/assert" + "github.com/tuotoo/qrcode" + "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/graph/model" @@ -12,11 +19,6 @@ import ( "github.com/authorizerdev/authorizer/server/refs" "github.com/authorizerdev/authorizer/server/resolvers" "github.com/authorizerdev/authorizer/server/token" - "github.com/gokyle/twofactor" - "github.com/stretchr/testify/assert" - "github.com/tuotoo/qrcode" - "strings" - "testing" ) func verifyTOTPTest(t *testing.T, s TestSetup) { @@ -67,13 +69,13 @@ func verifyTOTPTest(t *testing.T, s TestSetup) { }) assert.NoError(t, err) assert.NotNil(t, loginRes) - assert.NotNil(t, loginRes.TotpBase64url) - assert.NotNil(t, loginRes.TokenTotp) + assert.NotNil(t, loginRes.TotpBase64URL) + assert.NotNil(t, loginRes.TotpToken) assert.Nil(t, loginRes.AccessToken) assert.Equal(t, loginRes.Message, `Proceed to totp screen`) // get totp url for validation - pngBytes, err := base64.StdEncoding.DecodeString(*loginRes.TotpBase64url) + pngBytes, err := base64.StdEncoding.DecodeString(*loginRes.TotpBase64URL) assert.NoError(t, err) qrmatrix, err := qrcode.Decode(bytes.NewReader(pngBytes)) assert.NoError(t, err) @@ -89,7 +91,7 @@ func verifyTOTPTest(t *testing.T, s TestSetup) { valid, err := resolvers.VerifyTotpResolver(ctx, model.VerifyTOTPRequest{ Otp: code, - Token: *loginRes.TokenTotp, + Token: *loginRes.TotpToken, }) accessToken := *valid.AccessToken @@ -126,8 +128,8 @@ func verifyTOTPTest(t *testing.T, s TestSetup) { }) assert.NoError(t, err) assert.NotNil(t, loginRes) - assert.NotNil(t, loginRes.TokenTotp) - assert.Nil(t, loginRes.TotpBase64url) + assert.NotNil(t, loginRes.TotpToken) + assert.Nil(t, loginRes.TotpBase64URL) assert.Nil(t, loginRes.AccessToken) assert.Equal(t, loginRes.Message, `Proceed to totp screen`) @@ -136,7 +138,7 @@ func verifyTOTPTest(t *testing.T, s TestSetup) { valid, err = resolvers.VerifyTotpResolver(ctx, model.VerifyTOTPRequest{ Otp: code, - Token: *loginRes.TokenTotp, + Token: *loginRes.TotpToken, }) assert.NoError(t, err) assert.NotNil(t, *valid.AccessToken)