Merge pull request #372 from catusax/main

feat: add mfa session to secure otp login
This commit is contained in:
Lakhan Samani 2023-08-03 12:34:39 +05:30 committed by GitHub
commit 7a76b783b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 206 additions and 10 deletions

View File

@ -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"
)

View 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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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

View File

@ -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()

View File

@ -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{}{

View File

@ -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())

View File

@ -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