Merge pull request #372 from catusax/main
feat: add mfa session to secure otp login
This commit is contained in:
commit
7a76b783b1
|
@ -5,4 +5,6 @@ const (
|
||||||
AppCookieName = "cookie"
|
AppCookieName = "cookie"
|
||||||
// AdminCookieName is the name of the cookie that is used to store the admin token
|
// AdminCookieName is the name of the cookie that is used to store the admin token
|
||||||
AdminCookieName = "authorizer-admin"
|
AdminCookieName = "authorizer-admin"
|
||||||
|
// MfaCookieName is the name of the cookie that is used to store the mfa session
|
||||||
|
MfaCookieName = "mfa"
|
||||||
)
|
)
|
||||||
|
|
89
server/cookie/mfa_session.go
Normal file
89
server/cookie/mfa_session.go
Normal file
|
@ -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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetMfaSession sets the mfa 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMfaSession deletes the mfa 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMfaSession gets the mfa 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
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import (
|
||||||
type provider struct {
|
type provider struct {
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
sessionStore *stores.SessionStore
|
sessionStore *stores.SessionStore
|
||||||
|
mfasessionStore *stores.SessionStore
|
||||||
stateStore *stores.StateStore
|
stateStore *stores.StateStore
|
||||||
envStore *stores.EnvStore
|
envStore *stores.EnvStore
|
||||||
}
|
}
|
||||||
|
@ -19,6 +20,7 @@ func NewInMemoryProvider() (*provider, error) {
|
||||||
mutex: sync.Mutex{},
|
mutex: sync.Mutex{},
|
||||||
envStore: stores.NewEnvStore(),
|
envStore: stores.NewEnvStore(),
|
||||||
sessionStore: stores.NewSessionStore(),
|
sessionStore: stores.NewSessionStore(),
|
||||||
|
mfasessionStore: stores.NewSessionStore(),
|
||||||
stateStore: stores.NewStateStore(),
|
stateStore: stores.NewStateStore(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,27 @@ func (c *provider) DeleteSessionForNamespace(namespace string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMfaSession sets the mfa session with key and value of userId
|
||||||
|
func (c *provider) SetMfaSession(userId, key string, expiration int64) error {
|
||||||
|
c.mfasessionStore.Set(userId, key, userId, expiration)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMfaSession returns value of given mfa session
|
||||||
|
func (c *provider) GetMfaSession(userId, key string) (string, error) {
|
||||||
|
val := c.mfasessionStore.Get(userId, key)
|
||||||
|
if val == "" {
|
||||||
|
return "", fmt.Errorf("Not found")
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMfaSession deletes given mfa session from in-memory store.
|
||||||
|
func (c *provider) DeleteMfaSession(userId, key string) error {
|
||||||
|
c.mfasessionStore.Remove(userId, key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetState sets the state in the in-memory store.
|
// SetState sets the state in the in-memory store.
|
||||||
func (c *provider) SetState(key, state string) error {
|
func (c *provider) SetState(key, state string) error {
|
||||||
if os.Getenv("ENV") != constants.TestEnv {
|
if os.Getenv("ENV") != constants.TestEnv {
|
||||||
|
|
|
@ -112,4 +112,15 @@ func ProviderTests(t *testing.T, p Provider) {
|
||||||
key, err = p.GetUserSession("auth_provider1:124", "access_token_key")
|
key, err = p.GetUserSession("auth_provider1:124", "access_token_key")
|
||||||
assert.Empty(t, key)
|
assert.Empty(t, key)
|
||||||
assert.Error(t, err)
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,12 @@ type Provider interface {
|
||||||
DeleteAllUserSessions(userId string) error
|
DeleteAllUserSessions(userId string) error
|
||||||
// DeleteSessionForNamespace deletes the session for a given namespace
|
// DeleteSessionForNamespace deletes the session for a given namespace
|
||||||
DeleteSessionForNamespace(namespace string) error
|
DeleteSessionForNamespace(namespace string) error
|
||||||
|
// SetMfaSession sets the mfa session with key and value of userId
|
||||||
|
SetMfaSession(userId, key string, expiration int64) error
|
||||||
|
// GetMfaSession returns value of given mfa session
|
||||||
|
GetMfaSession(userId, key string) (string, error)
|
||||||
|
// DeleteMfaSession deletes given mfa session from in-memory store.
|
||||||
|
DeleteMfaSession(userId, key string) error
|
||||||
|
|
||||||
// SetState sets the login state (key, value form) in the session store
|
// SetState sets the login state (key, value form) in the session store
|
||||||
SetState(key, state string) error
|
SetState(key, state string) error
|
||||||
|
|
|
@ -16,6 +16,8 @@ var (
|
||||||
envStorePrefix = "authorizer_env"
|
envStorePrefix = "authorizer_env"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const mfaSessionPrefix = "mfa_sess_"
|
||||||
|
|
||||||
// SetUserSession sets the user session for given user identifier in form recipe:user_id
|
// 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 {
|
func (c *provider) SetUserSession(userId, key, token string, expiration int64) error {
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
|
@ -91,6 +93,37 @@ func (c *provider) DeleteSessionForNamespace(namespace string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMfaSession sets the mfa session with key and value of userId
|
||||||
|
func (c *provider) SetMfaSession(userId, 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, userId, key), userId, duration).Err()
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("Error saving user session to redis: ", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMfaSession returns value of given mfa session
|
||||||
|
func (c *provider) GetMfaSession(userId, key string) (string, error) {
|
||||||
|
data, err := c.store.Get(c.ctx, fmt.Sprintf("%s%s:%s", mfaSessionPrefix, userId, key)).Result()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMfaSession deletes given mfa session from in-memory store.
|
||||||
|
func (c *provider) DeleteMfaSession(userId, key string) error {
|
||||||
|
if err := c.store.Del(c.ctx, fmt.Sprintf("%s%s:%s", mfaSessionPrefix, userId, key)).Err(); err != nil {
|
||||||
|
log.Debug("Error deleting user session from redis: ", err)
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetState sets the state in redis store.
|
// SetState sets the state in redis store.
|
||||||
func (c *provider) SetState(key, value string) error {
|
func (c *provider) SetState(key, value string) error {
|
||||||
err := c.store.Set(c.ctx, stateStorePrefix+key, value, 0).Err()
|
err := c.store.Set(c.ctx, stateStorePrefix+key, value, 0).Err()
|
||||||
|
|
|
@ -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 email service is not enabled continue the process in any way
|
||||||
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && isEmailServiceEnabled && !isMFADisabled {
|
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && isEmailServiceEnabled && !isMFADisabled {
|
||||||
otp := utils.GenerateOTP()
|
otp := utils.GenerateOTP()
|
||||||
|
expires := time.Now().Add(1 * time.Minute).Unix()
|
||||||
otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{
|
otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Otp: otp,
|
Otp: otp,
|
||||||
ExpiresAt: time.Now().Add(1 * time.Minute).Unix(),
|
ExpiresAt: expires,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("Failed to add otp: ", err)
|
log.Debug("Failed to add otp: ", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mfaSession := uuid.NewString()
|
||||||
|
err = memorystore.Provider.SetMfaSession(user.ID, mfaSession, expires)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("Failed to add mfasession: ", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cookie.SetMfaSession(gc, mfaSession)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
// exec it as go routine so that we can reduce the api latency
|
// exec it as go routine so that we can reduce the api latency
|
||||||
go email.SendEmail([]string{params.Email}, constants.VerificationTypeOTP, map[string]interface{}{
|
go email.SendEmail([]string{params.Email}, constants.VerificationTypeOTP, map[string]interface{}{
|
||||||
|
|
|
@ -122,15 +122,25 @@ func MobileLoginResolver(ctx context.Context, params model.MobileLoginInput) (*m
|
||||||
smsBody := strings.Builder{}
|
smsBody := strings.Builder{}
|
||||||
smsBody.WriteString("Your verification code is: ")
|
smsBody.WriteString("Your verification code is: ")
|
||||||
smsBody.WriteString(smsCode)
|
smsBody.WriteString(smsCode)
|
||||||
|
expires := time.Now().Add(duration).Unix()
|
||||||
_, err := db.Provider.UpsertOTP(ctx, &models.OTP{
|
_, err := db.Provider.UpsertOTP(ctx, &models.OTP{
|
||||||
PhoneNumber: params.PhoneNumber,
|
PhoneNumber: params.PhoneNumber,
|
||||||
Otp: smsCode,
|
Otp: smsCode,
|
||||||
ExpiresAt: time.Now().Add(duration).Unix(),
|
ExpiresAt: expires,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("error while upserting OTP: ", err.Error())
|
log.Debug("error while upserting OTP: ", err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mfaSession := uuid.NewString()
|
||||||
|
err = memorystore.Provider.SetMfaSession(user.ID, mfaSession, expires)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("Failed to add mfasession: ", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cookie.SetMfaSession(gc, mfaSession)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodMobileBasicAuth, user)
|
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodMobileBasicAuth, user)
|
||||||
smsproviders.SendSMS(params.PhoneNumber, smsBody.String())
|
smsproviders.SendSMS(params.PhoneNumber, smsBody.String())
|
||||||
|
|
|
@ -27,6 +27,13 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
|
||||||
log.Debug("Failed to get GinContext: ", err)
|
log.Debug("Failed to get GinContext: ", err)
|
||||||
return res, err
|
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 refs.StringValue(params.Email) == "" && refs.StringValue(params.PhoneNumber) == "" {
|
if refs.StringValue(params.Email) == "" && refs.StringValue(params.PhoneNumber) == "" {
|
||||||
log.Debug("Email or phone number is required")
|
log.Debug("Email or phone number is required")
|
||||||
return res, fmt.Errorf(`email or phone_number is required`)
|
return res, fmt.Errorf(`email or phone_number is required`)
|
||||||
|
@ -66,6 +73,12 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
|
||||||
log.Debug("Failed to get user by email or phone number: ", err)
|
log.Debug("Failed to get user by email or phone number: ", err)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := memorystore.Provider.GetMfaSession(user.ID, mfaSession); err != nil {
|
||||||
|
log.Debug("Failed to get mfa session: ", err)
|
||||||
|
return res, fmt.Errorf(`invalid session: %s`, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
isSignUp := user.EmailVerifiedAt == nil && user.PhoneNumberVerifiedAt == nil
|
isSignUp := user.EmailVerifiedAt == nil && user.PhoneNumberVerifiedAt == nil
|
||||||
// TODO - Add Login method in DB when we introduce OTP for social media login
|
// TODO - Add Login method in DB when we introduce OTP for social media login
|
||||||
loginMethod := constants.AuthRecipeMethodBasicAuth
|
loginMethod := constants.AuthRecipeMethodBasicAuth
|
||||||
|
|
Loading…
Reference in New Issue
Block a user