diff --git a/app/package-lock.json b/app/package-lock.json index 4a9978e..26598e3 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "^0.25.0", + "@authorizerdev/authorizer-react": "^0.26.0-beta.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", @@ -26,9 +26,9 @@ } }, "node_modules/@authorizerdev/authorizer-js": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.14.0.tgz", - "integrity": "sha512-cpeeFrmG623QPLn+nf+ACHayZYqW8xokIidGikeboBDJtuAAQB50a54/7HwLHriG2FB7WvPuHQ/9LFFX//N1lg==", + "version": "0.17.0-beta.1", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.17.0-beta.1.tgz", + "integrity": "sha512-jUlFUrs4Ys6LZ5hclPeRt84teygi+bA57d/IpV9GAqOrfifv70jkFeDln4+Bs0mZk74el23Xn+DR9380mqE4Cg==", "dependencies": { "node-fetch": "^2.6.1" }, @@ -37,11 +37,11 @@ } }, "node_modules/@authorizerdev/authorizer-react": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.25.0.tgz", - "integrity": "sha512-Dt2rZf+cGCVb8dqcJ/9l8Trx+QeXnTdfhER6r/cq0iOnFC9MqWzQPB3RgrlUoMLHtZvKNDXIk1HvfD5hSX9lhw==", + "version": "0.26.0-beta.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.26.0-beta.0.tgz", + "integrity": "sha512-YfyiGYBmbsp3tLWIxOrOZ/hUTCmdMXVE9SLE8m1xsFsxzJJlUhepp0AMahSbH5EyLj5bchOhOw/rzgpnDZDvMw==", "dependencies": { - "@authorizerdev/authorizer-js": "^0.14.0", + "@authorizerdev/authorizer-js": "^0.17.0-beta.1", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" @@ -852,19 +852,19 @@ }, "dependencies": { "@authorizerdev/authorizer-js": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.14.0.tgz", - "integrity": "sha512-cpeeFrmG623QPLn+nf+ACHayZYqW8xokIidGikeboBDJtuAAQB50a54/7HwLHriG2FB7WvPuHQ/9LFFX//N1lg==", + "version": "0.17.0-beta.1", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.17.0-beta.1.tgz", + "integrity": "sha512-jUlFUrs4Ys6LZ5hclPeRt84teygi+bA57d/IpV9GAqOrfifv70jkFeDln4+Bs0mZk74el23Xn+DR9380mqE4Cg==", "requires": { "node-fetch": "^2.6.1" } }, "@authorizerdev/authorizer-react": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.25.0.tgz", - "integrity": "sha512-Dt2rZf+cGCVb8dqcJ/9l8Trx+QeXnTdfhER6r/cq0iOnFC9MqWzQPB3RgrlUoMLHtZvKNDXIk1HvfD5hSX9lhw==", + "version": "0.26.0-beta.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.26.0-beta.0.tgz", + "integrity": "sha512-YfyiGYBmbsp3tLWIxOrOZ/hUTCmdMXVE9SLE8m1xsFsxzJJlUhepp0AMahSbH5EyLj5bchOhOw/rzgpnDZDvMw==", "requires": { - "@authorizerdev/authorizer-js": "^0.14.0", + "@authorizerdev/authorizer-js": "^0.17.0-beta.1", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" diff --git a/app/package.json b/app/package.json index 8c5b77e..c3234c9 100644 --- a/app/package.json +++ b/app/package.json @@ -11,7 +11,7 @@ "author": "Lakhan Samani", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "^0.25.0", + "@authorizerdev/authorizer-react": "^0.26.0-beta.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 41d31f9..c04cac0 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -2529,7 +2529,8 @@ "@chakra-ui/css-reset": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-1.1.1.tgz", - "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==" + "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==", + "requires": {} }, "@chakra-ui/descendant": { "version": "2.1.1", @@ -3133,7 +3134,8 @@ "@graphql-typed-document-node/core": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", - "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==" + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", + "requires": {} }, "@popperjs/core": { "version": "2.11.0", @@ -3843,7 +3845,8 @@ "react-icons": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz", - "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==" + "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==", + "requires": {} }, "react-is": { "version": "16.13.1", @@ -4029,7 +4032,8 @@ "use-callback-ref": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz", - "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==" + "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==", + "requires": {} }, "use-sidecar": { "version": "1.0.5", diff --git a/server/email/otp.go b/server/email/otp.go new file mode 100644 index 0000000..181a1e0 --- /dev/null +++ b/server/email/otp.go @@ -0,0 +1,118 @@ +package email + +import ( + log "github.com/sirupsen/logrus" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/memorystore" +) + +// SendOtpMail to send otp email +func SendOtpMail(toEmail, otp string) error { + // The receiver needs to be in slice as the receive supports multiple receiver + Receiver := []string{toEmail} + + Subject := "OTP for your multi factor authentication" + message := ` + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + + +
+
+
+ + + ` + data := make(map[string]interface{}, 3) + var err error + data["org_logo"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo) + if err != nil { + return err + } + data["org_name"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName) + if err != nil { + return err + } + data["otp"] = otp + message = addEmailTemplate(message, data, "otp.tmpl") + // bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) + + err = SendMail(Receiver, Subject, message) + if err != nil { + log.Warn("error sending email: ", err) + } + return err +} diff --git a/server/env/persist_env.go b/server/env/persist_env.go index 355ef7d..d783b93 100644 --- a/server/env/persist_env.go +++ b/server/env/persist_env.go @@ -221,9 +221,10 @@ func PersistEnv() error { // handle derivative cases like disabling email verification & magic login // in case SMTP is off but env is set to true if storeData[constants.EnvKeySmtpHost] == "" || storeData[constants.EnvKeySmtpUsername] == "" || storeData[constants.EnvKeySmtpPassword] == "" || storeData[constants.EnvKeySenderEmail] == "" && storeData[constants.EnvKeySmtpPort] == "" { + storeData[constants.EnvKeyIsEmailServiceEnabled] = false + if !storeData[constants.EnvKeyDisableEmailVerification].(bool) { storeData[constants.EnvKeyDisableEmailVerification] = true - storeData[constants.EnvKeyIsEmailServiceEnabled] = false hasChanged = true } diff --git a/server/memorystore/memory_store.go b/server/memorystore/memory_store.go index ad6a418..9cbbbb4 100644 --- a/server/memorystore/memory_store.go +++ b/server/memorystore/memory_store.go @@ -31,6 +31,7 @@ func InitMemStore() error { constants.EnvKeyDisableLoginPage: false, constants.EnvKeyDisableSignUp: false, constants.EnvKeyDisableStrongPassword: false, + constants.EnvKeyIsEmailServiceEnabled: false, } requiredEnvs := RequiredEnvStoreObj.GetRequiredEnv() diff --git a/server/memorystore/providers/redis/store.go b/server/memorystore/providers/redis/store.go index 36b4b0c..d6ee1df 100644 --- a/server/memorystore/providers/redis/store.go +++ b/server/memorystore/providers/redis/store.go @@ -160,7 +160,7 @@ func (c *provider) GetEnvStore() (map[string]interface{}, error) { return nil, err } for key, value := range data { - if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword { + if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword || key == constants.EnvKeyIsEmailServiceEnabled { boolValue, err := strconv.ParseBool(value) if err != nil { return res, err diff --git a/server/resolvers/invite_members.go b/server/resolvers/invite_members.go index 2316456..914e7bb 100644 --- a/server/resolvers/invite_members.go +++ b/server/resolvers/invite_members.go @@ -35,13 +35,13 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) } // this feature is only allowed if email server is configured - isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) + EnvKeyIsEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled) if err != nil { log.Debug("Error getting email verification disabled: ", err) - isEmailVerificationDisabled = true + EnvKeyIsEmailServiceEnabled = false } - if isEmailVerificationDisabled { + if !EnvKeyIsEmailServiceEnabled { log.Debug("Email server is not configured") return nil, errors.New("email sending is disabled") } diff --git a/server/resolvers/login.go b/server/resolvers/login.go index c7fafe3..7d1f28e 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -2,6 +2,7 @@ package resolvers import ( "context" + "errors" "fmt" "strings" "time" @@ -13,6 +14,7 @@ import ( "github.com/authorizerdev/authorizer/server/cookie" "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db/models" + "github.com/authorizerdev/authorizer/server/email" "github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/refs" @@ -99,12 +101,29 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes } if refs.BoolValue(user.IsMultiFactorAuthEnabled) { - //TODO - send email based on email config - db.Provider.UpsertOTP(ctx, &models.OTP{ + isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled) + if err != nil || !isEnvServiceEnabled { + log.Debug("Email service not enabled:") + return nil, errors.New("email service not enabled") + } + otp := utils.GenerateOTP() + otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{ Email: user.Email, - Otp: utils.GenerateOTP(), + Otp: otp, ExpiresAt: time.Now().Add(1 * time.Minute).Unix(), }) + if err != nil { + log.Debug("Failed to add otp: ", err) + return nil, err + } + + go func() { + err := email.SendOtpMail(user.Email, otpData.Otp) + if err != nil { + log.Debug("Failed to send otp email: ", err) + } + }() + return &model.AuthResponse{ Message: "Please check the OTP in your inbox", ShouldShowOtpScreen: refs.NewBoolRef(true), diff --git a/server/resolvers/resend_otp.go b/server/resolvers/resend_otp.go index 1eb1333..60367c1 100644 --- a/server/resolvers/resend_otp.go +++ b/server/resolvers/resend_otp.go @@ -2,6 +2,7 @@ package resolvers import ( "context" + "errors" "fmt" "strings" "time" @@ -10,6 +11,7 @@ import ( "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db/models" + "github.com/authorizerdev/authorizer/server/email" "github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/refs" "github.com/authorizerdev/authorizer/server/utils" @@ -17,8 +19,6 @@ import ( // ResendOTPResolver is a resolver for resend otp mutation func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error) { - var res *model.Response - log := log.WithFields(log.Fields{ "email": params.Email, }) @@ -26,34 +26,57 @@ func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*mod user, err := db.Provider.GetUserByEmail(ctx, params.Email) if err != nil { log.Debug("Failed to get user by email: ", err) - return res, fmt.Errorf(`user with this email not found`) + return nil, fmt.Errorf(`user with this email not found`) } if user.RevokedTimestamp != nil { log.Debug("User access is revoked") - return res, fmt.Errorf(`user access has been revoked`) + return nil, fmt.Errorf(`user access has been revoked`) } if user.EmailVerifiedAt == nil { log.Debug("User email is not verified") - return res, fmt.Errorf(`email not verified`) + return nil, fmt.Errorf(`email not verified`) } if !refs.BoolValue(user.IsMultiFactorAuthEnabled) { log.Debug("User multi factor authentication is not enabled") - return res, fmt.Errorf(`multi factor authentication not enabled`) + return nil, fmt.Errorf(`multi factor authentication not enabled`) } - //TODO - send email based on email config - db.Provider.UpsertOTP(ctx, &models.OTP{ + // get otp by email + otpData, err := db.Provider.GetOTPByEmail(ctx, params.Email) + if err != nil { + log.Debug("Failed to get otp for given email: ", err) + return nil, err + } + + if otpData == nil { + log.Debug("No otp found for given email: ", params.Email) + return &model.Response{ + Message: "Failed to get for given email", + }, errors.New("failed to get otp for given email") + } + + otp := utils.GenerateOTP() + otpData, err = db.Provider.UpsertOTP(ctx, &models.OTP{ Email: user.Email, - Otp: utils.GenerateOTP(), + Otp: otp, ExpiresAt: time.Now().Add(1 * time.Minute).Unix(), }) - - res = &model.Response{ - Message: `OTP has been sent. Please check your inbox`, + if err != nil { + log.Debug("Error generating new otp: ", err) + return nil, err } - return res, nil + go func() { + err := email.SendOtpMail(params.Email, otp) + if err != nil { + log.Debug("Error sending otp email: ", otp) + } + }() + + return &model.Response{ + Message: `OTP has been sent. Please check your inbox`, + }, nil } diff --git a/server/resolvers/update_env.go b/server/resolvers/update_env.go index 508d47e..30abe9e 100644 --- a/server/resolvers/update_env.go +++ b/server/resolvers/update_env.go @@ -234,6 +234,7 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model // handle derivative cases like disabling email verification & magic login // 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 if !updatedData[constants.EnvKeyDisableEmailVerification].(bool) { updatedData[constants.EnvKeyDisableEmailVerification] = true } @@ -243,6 +244,10 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model } } + if updatedData[constants.EnvKeySmtpHost] != "" || updatedData[constants.EnvKeySmtpUsername] != "" || updatedData[constants.EnvKeySmtpPassword] != "" || updatedData[constants.EnvKeySenderEmail] != "" && updatedData[constants.EnvKeySmtpPort] != "" { + updatedData[constants.EnvKeyIsEmailServiceEnabled] = true + } + // check the roles change if len(params.Roles) > 0 { if len(params.DefaultRoles) > 0 { diff --git a/server/resolvers/update_profile.go b/server/resolvers/update_profile.go index ac2947f..0a47376 100644 --- a/server/resolvers/update_profile.go +++ b/server/resolvers/update_profile.go @@ -2,6 +2,7 @@ package resolvers import ( "context" + "errors" "fmt" "strings" "time" @@ -96,6 +97,13 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput) if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) { user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled + if refs.BoolValue(params.IsMultiFactorAuthEnabled) { + isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled) + if err != nil || !isEnvServiceEnabled { + log.Debug("Email service not enabled:") + return nil, errors.New("email service not enabled, so cannot enable multi factor authentication") + } + } } isPasswordChanging := false diff --git a/server/resolvers/update_user.go b/server/resolvers/update_user.go index da9c58d..d20e4a9 100644 --- a/server/resolvers/update_user.go +++ b/server/resolvers/update_user.go @@ -2,6 +2,7 @@ package resolvers import ( "context" + "errors" "fmt" "strings" "time" @@ -91,6 +92,13 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) { user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled + if refs.BoolValue(params.IsMultiFactorAuthEnabled) { + isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled) + if err != nil || !isEnvServiceEnabled { + log.Debug("Email service not enabled:") + return nil, errors.New("email service not enabled, so cannot enable multi factor authentication") + } + } } if params.EmailVerified != nil { diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index 95bb78d..b792adb 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -52,8 +52,7 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod isSignUp := user.EmailVerifiedAt == nil - // TODO - Add Login method in DB - + // TODO - Add Login method in DB when we introduce OTP for social media login loginMethod := constants.AuthRecipeMethodBasicAuth roles := strings.Split(user.Roles, ",") @@ -65,11 +64,7 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod } go func() { - err = db.Provider.DeleteOTP(gc, otp) - - if err != nil { - log.Debug("Failed to delete otp: ", err) - } + db.Provider.DeleteOTP(gc, otp) if isSignUp { utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user) } else { diff --git a/server/utils/gin_context.go b/server/utils/gin_context.go index 72fd480..7e3ced6 100644 --- a/server/utils/gin_context.go +++ b/server/utils/gin_context.go @@ -7,7 +7,7 @@ import ( "github.com/gin-gonic/gin" ) -// TODO renamae GinContextKey -> GinContext +// TODO re-name GinContextKey -> GinContext // GinContext to get gin context from context func GinContextFromContext(ctx context.Context) (*gin.Context, error) {