From bbb1cf63019bfdf4414f34e89d79eb414e1580e8 Mon Sep 17 00:00:00 2001 From: lemonScaletech Date: Wed, 6 Sep 2023 11:26:22 +0530 Subject: [PATCH] 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 +}