diff --git a/server/constants/cookie.go b/server/constants/cookie.go index 71320a9..6dda49d 100644 --- a/server/constants/cookie.go +++ b/server/constants/cookie.go @@ -5,4 +5,6 @@ const ( AppCookieName = "cookie" // AdminCookieName is the name of the cookie that is used to store the admin token AdminCookieName = "authorizer-admin" + + MfaCookieName = "mfa" ) diff --git a/server/cookie/mfa_session.go b/server/cookie/mfa_session.go new file mode 100644 index 0000000..bf691ce --- /dev/null +++ b/server/cookie/mfa_session.go @@ -0,0 +1,89 @@ +package cookie + +import ( + "net/http" + "net/url" + + log "github.com/sirupsen/logrus" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/memorystore" + "github.com/authorizerdev/authorizer/server/parsers" + "github.com/gin-gonic/gin" +) + +// SetSession sets the session cookie in the response +func SetMfaSession(gc *gin.Context, sessionID string) { + appCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAppCookieSecure) + if err != nil { + log.Debug("Error while getting app cookie secure from env variable: %v", err) + appCookieSecure = true + } + + secure := appCookieSecure + httpOnly := appCookieSecure + hostname := parsers.GetHost(gc) + host, _ := parsers.GetHostParts(hostname) + domain := parsers.GetDomainName(hostname) + if domain != "localhost" { + domain = "." + domain + } + + // Since app cookie can come from cross site it becomes important to set this in lax mode when insecure. + // Example person using custom UI on their app domain and making request to authorizer domain. + // For more information check: + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + // https://github.com/gin-gonic/gin/blob/master/context.go#L86 + // TODO add ability to sameSite = none / strict from dashboard + if !appCookieSecure { + gc.SetSameSite(http.SameSiteLaxMode) + } else { + gc.SetSameSite(http.SameSiteNoneMode) + } + // TODO allow configuring from dashboard + age := 60 + + gc.SetCookie(constants.MfaCookieName+"_session", sessionID, age, "/", host, secure, httpOnly) + gc.SetCookie(constants.MfaCookieName+"_session_domain", sessionID, age, "/", domain, secure, httpOnly) +} + +// DeleteSession sets session cookies to expire +func DeleteMfaSession(gc *gin.Context) { + appCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAppCookieSecure) + if err != nil { + log.Debug("Error while getting app cookie secure from env variable: %v", err) + appCookieSecure = true + } + + secure := appCookieSecure + httpOnly := appCookieSecure + hostname := parsers.GetHost(gc) + host, _ := parsers.GetHostParts(hostname) + domain := parsers.GetDomainName(hostname) + if domain != "localhost" { + domain = "." + domain + } + + gc.SetSameSite(http.SameSiteNoneMode) + gc.SetCookie(constants.MfaCookieName+"_session", "", -1, "/", host, secure, httpOnly) + gc.SetCookie(constants.MfaCookieName+"_session_domain", "", -1, "/", domain, secure, httpOnly) +} + +// GetSession gets the session cookie from context +func GetMfaSession(gc *gin.Context) (string, error) { + var cookie *http.Cookie + var err error + cookie, err = gc.Request.Cookie(constants.MfaCookieName + "_session") + if err != nil { + cookie, err = gc.Request.Cookie(constants.MfaCookieName + "_session_domain") + if err != nil { + return "", err + } + } + + decodedValue, err := url.PathUnescape(cookie.Value) + if err != nil { + return "", err + } + return decodedValue, nil +} diff --git a/server/memorystore/providers/inmemory/provider.go b/server/memorystore/providers/inmemory/provider.go index 952092d..e726502 100644 --- a/server/memorystore/providers/inmemory/provider.go +++ b/server/memorystore/providers/inmemory/provider.go @@ -7,18 +7,20 @@ import ( ) type provider struct { - mutex sync.Mutex - sessionStore *stores.SessionStore - stateStore *stores.StateStore - envStore *stores.EnvStore + mutex sync.Mutex + sessionStore *stores.SessionStore + mfasessionStore *stores.SessionStore + stateStore *stores.StateStore + envStore *stores.EnvStore } // NewInMemoryStore returns a new in-memory store. func NewInMemoryProvider() (*provider, error) { return &provider{ - mutex: sync.Mutex{}, - envStore: stores.NewEnvStore(), - sessionStore: stores.NewSessionStore(), - stateStore: stores.NewStateStore(), + mutex: sync.Mutex{}, + envStore: stores.NewEnvStore(), + sessionStore: stores.NewSessionStore(), + mfasessionStore: stores.NewSessionStore(), + stateStore: stores.NewStateStore(), }, nil } diff --git a/server/memorystore/providers/inmemory/store.go b/server/memorystore/providers/inmemory/store.go index 4a8e8ce..45a7986 100644 --- a/server/memorystore/providers/inmemory/store.go +++ b/server/memorystore/providers/inmemory/store.go @@ -42,6 +42,24 @@ func (c *provider) DeleteSessionForNamespace(namespace string) error { return nil } +func (c *provider) SetMfaSession(email, key string, expiration int64) error { + c.mfasessionStore.Set(email, key, email, expiration) + return nil +} + +func (c *provider) GetMfaSession(email, key string) (string, error) { + val := c.mfasessionStore.Get(email, key) + if val == "" { + return "", fmt.Errorf("Not found") + } + return val, nil +} + +func (c *provider) DeleteMfaSession(email, key string) error { + c.mfasessionStore.Remove(email, key) + return nil +} + // SetState sets the state in the in-memory store. func (c *provider) SetState(key, state string) error { if os.Getenv("ENV") != constants.TestEnv { diff --git a/server/memorystore/providers/provider_tests.go b/server/memorystore/providers/provider_tests.go index e569fe8..47f4dba 100644 --- a/server/memorystore/providers/provider_tests.go +++ b/server/memorystore/providers/provider_tests.go @@ -112,4 +112,15 @@ func ProviderTests(t *testing.T, p Provider) { key, err = p.GetUserSession("auth_provider1:124", "access_token_key") assert.Empty(t, key) assert.Error(t, err) + + err = p.SetMfaSession("auth_provider:123", "session123", time.Now().Add(60*time.Second).Unix()) + assert.NoError(t, err) + key, err = p.GetMfaSession("auth_provider:123", "session123") + assert.NoError(t, err) + assert.Equal(t, "auth_provider:123", key) + err = p.DeleteMfaSession("auth_provider:123", "session123") + assert.NoError(t, err) + key, err = p.GetMfaSession("auth_provider:123", "session123") + assert.Error(t, err) + assert.Empty(t, key) } diff --git a/server/memorystore/providers/providers.go b/server/memorystore/providers/providers.go index db58aa7..ae1249f 100644 --- a/server/memorystore/providers/providers.go +++ b/server/memorystore/providers/providers.go @@ -13,6 +13,10 @@ type Provider interface { // DeleteSessionForNamespace deletes the session for a given namespace DeleteSessionForNamespace(namespace string) error + SetMfaSession(email, key string, expiration int64) error + GetMfaSession(email, key string) (string, error) + DeleteMfaSession(email, key string) error + // SetState sets the login state (key, value form) in the session store SetState(key, state string) error // GetState returns the state from the session store diff --git a/server/memorystore/providers/redis/store.go b/server/memorystore/providers/redis/store.go index 058e95e..d55e741 100644 --- a/server/memorystore/providers/redis/store.go +++ b/server/memorystore/providers/redis/store.go @@ -16,6 +16,8 @@ var ( envStorePrefix = "authorizer_env" ) +const mfaSessionPrefix = "mfa_sess_" + // SetUserSession sets the user session for given user identifier in form recipe:user_id func (c *provider) SetUserSession(userId, key, token string, expiration int64) error { currentTime := time.Now() @@ -91,6 +93,34 @@ func (c *provider) DeleteSessionForNamespace(namespace string) error { return nil } +func (c *provider) SetMfaSession(email, key string, expiration int64) error { + currentTime := time.Now() + expireTime := time.Unix(expiration, 0) + duration := expireTime.Sub(currentTime) + err := c.store.Set(c.ctx, fmt.Sprintf("%s%s:%s", mfaSessionPrefix, email, key), email, duration).Err() + if err != nil { + log.Debug("Error saving user session to redis: ", err) + return err + } + return nil +} + +func (c *provider) GetMfaSession(email, key string) (string, error) { + data, err := c.store.Get(c.ctx, fmt.Sprintf("%s%s:%s", mfaSessionPrefix, email, key)).Result() + if err != nil { + return "", err + } + return data, nil +} + +func (c *provider) DeleteMfaSession(email, key string) error { + if err := c.store.Del(c.ctx, fmt.Sprintf("%s%s:%s", mfaSessionPrefix, email, key)).Err(); err != nil { + log.Debug("Error deleting user session from redis: ", err) + // continue + } + return nil +} + // SetState sets the state in redis store. func (c *provider) SetState(key, value string) error { err := c.store.Set(c.ctx, stateStorePrefix+key, value, 0).Err() diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 28a2289..e05d012 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -113,16 +113,25 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes // If email service is not enabled continue the process in any way if refs.BoolValue(user.IsMultiFactorAuthEnabled) && isEmailServiceEnabled && !isMFADisabled { otp := utils.GenerateOTP() + expires := time.Now().Add(1 * time.Minute).Unix() otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{ Email: user.Email, Otp: otp, - ExpiresAt: time.Now().Add(1 * time.Minute).Unix(), + ExpiresAt: expires, }) if err != nil { log.Debug("Failed to add otp: ", err) return nil, err } + mfaSession := uuid.NewString() + err = memorystore.Provider.SetMfaSession(params.Email, mfaSession, expires) + if err != nil { + log.Debug("Failed to add mfasession: ", err) + return nil, err + } + cookie.SetMfaSession(gc, mfaSession) + go func() { // exec it as go routine so that we can reduce the api latency go email.SendEmail([]string{params.Email}, constants.VerificationTypeOTP, map[string]interface{}{ diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index 80080d9..aac55e2 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -28,6 +28,17 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod return res, err } + mfaSession, err := cookie.GetMfaSession(gc) + if err != nil { + log.Debug("Failed to get otp request by email: ", err) + return res, fmt.Errorf(`invalid session: %s`, err.Error()) + } + + if _, err := memorystore.Provider.GetMfaSession(params.Email, mfaSession); err != nil { + log.Debug("Failed to get mfa session: ", err) + return res, fmt.Errorf(`invalid session: %s`, err.Error()) + } + otp, err := db.Provider.GetOTPByEmail(ctx, params.Email) if err != nil { log.Debug("Failed to get otp request by email: ", err)