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"
|
||||
// AdminCookieName is the name of the cookie that is used to store the admin token
|
||||
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 {
|
||||
mutex sync.Mutex
|
||||
sessionStore *stores.SessionStore
|
||||
mfasessionStore *stores.SessionStore
|
||||
stateStore *stores.StateStore
|
||||
envStore *stores.EnvStore
|
||||
}
|
||||
|
@ -19,6 +20,7 @@ func NewInMemoryProvider() (*provider, error) {
|
|||
mutex: sync.Mutex{},
|
||||
envStore: stores.NewEnvStore(),
|
||||
sessionStore: stores.NewSessionStore(),
|
||||
mfasessionStore: stores.NewSessionStore(),
|
||||
stateStore: stores.NewStateStore(),
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -42,6 +42,27 @@ func (c *provider) DeleteSessionForNamespace(namespace string) error {
|
|||
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.
|
||||
func (c *provider) SetState(key, state string) error {
|
||||
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")
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,12 @@ type Provider interface {
|
|||
DeleteAllUserSessions(userId string) error
|
||||
// DeleteSessionForNamespace deletes the session for a given namespace
|
||||
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(key, state string) error
|
||||
|
|
|
@ -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,37 @@ func (c *provider) DeleteSessionForNamespace(namespace string) error {
|
|||
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.
|
||||
func (c *provider) SetState(key, value string) error {
|
||||
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 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(user.ID, 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{}{
|
||||
|
|
|
@ -122,15 +122,25 @@ func MobileLoginResolver(ctx context.Context, params model.MobileLoginInput) (*m
|
|||
smsBody := strings.Builder{}
|
||||
smsBody.WriteString("Your verification code is: ")
|
||||
smsBody.WriteString(smsCode)
|
||||
expires := time.Now().Add(duration).Unix()
|
||||
_, err := db.Provider.UpsertOTP(ctx, &models.OTP{
|
||||
PhoneNumber: params.PhoneNumber,
|
||||
Otp: smsCode,
|
||||
ExpiresAt: time.Now().Add(duration).Unix(),
|
||||
ExpiresAt: expires,
|
||||
})
|
||||
if err != nil {
|
||||
log.Debug("error while upserting OTP: ", err.Error())
|
||||
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() {
|
||||
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodMobileBasicAuth, user)
|
||||
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)
|
||||
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) == "" {
|
||||
log.Debug("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)
|
||||
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
|
||||
// TODO - Add Login method in DB when we introduce OTP for social media login
|
||||
loginMethod := constants.AuthRecipeMethodBasicAuth
|
||||
|
|
Loading…
Reference in New Issue
Block a user