Skip to content

Commit 7a76b78

Browse files
authored
Merge pull request #372 from catusax/main
feat: add mfa session to secure otp login
2 parents e5400bc + 0c33485 commit 7a76b78

File tree

10 files changed

+206
-10
lines changed

10 files changed

+206
-10
lines changed

server/constants/cookie.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ const (
55
AppCookieName = "cookie"
66
// AdminCookieName is the name of the cookie that is used to store the admin token
77
AdminCookieName = "authorizer-admin"
8+
// MfaCookieName is the name of the cookie that is used to store the mfa session
9+
MfaCookieName = "mfa"
810
)

server/cookie/mfa_session.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package cookie
2+
3+
import (
4+
"net/http"
5+
"net/url"
6+
7+
log "github.com/sirupsen/logrus"
8+
9+
"github.com/authorizerdev/authorizer/server/constants"
10+
"github.com/authorizerdev/authorizer/server/memorystore"
11+
"github.com/authorizerdev/authorizer/server/parsers"
12+
"github.com/gin-gonic/gin"
13+
)
14+
15+
// SetMfaSession sets the mfa session cookie in the response
16+
func SetMfaSession(gc *gin.Context, sessionID string) {
17+
appCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAppCookieSecure)
18+
if err != nil {
19+
log.Debug("Error while getting app cookie secure from env variable: %v", err)
20+
appCookieSecure = true
21+
}
22+
23+
secure := appCookieSecure
24+
httpOnly := appCookieSecure
25+
hostname := parsers.GetHost(gc)
26+
host, _ := parsers.GetHostParts(hostname)
27+
domain := parsers.GetDomainName(hostname)
28+
if domain != "localhost" {
29+
domain = "." + domain
30+
}
31+
32+
// Since app cookie can come from cross site it becomes important to set this in lax mode when insecure.
33+
// Example person using custom UI on their app domain and making request to authorizer domain.
34+
// For more information check:
35+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
36+
// https://github.com/gin-gonic/gin/blob/master/context.go#L86
37+
// TODO add ability to sameSite = none / strict from dashboard
38+
if !appCookieSecure {
39+
gc.SetSameSite(http.SameSiteLaxMode)
40+
} else {
41+
gc.SetSameSite(http.SameSiteNoneMode)
42+
}
43+
// TODO allow configuring from dashboard
44+
age := 60
45+
46+
gc.SetCookie(constants.MfaCookieName+"_session", sessionID, age, "/", host, secure, httpOnly)
47+
gc.SetCookie(constants.MfaCookieName+"_session_domain", sessionID, age, "/", domain, secure, httpOnly)
48+
}
49+
50+
// DeleteMfaSession deletes the mfa session cookies to expire
51+
func DeleteMfaSession(gc *gin.Context) {
52+
appCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAppCookieSecure)
53+
if err != nil {
54+
log.Debug("Error while getting app cookie secure from env variable: %v", err)
55+
appCookieSecure = true
56+
}
57+
58+
secure := appCookieSecure
59+
httpOnly := appCookieSecure
60+
hostname := parsers.GetHost(gc)
61+
host, _ := parsers.GetHostParts(hostname)
62+
domain := parsers.GetDomainName(hostname)
63+
if domain != "localhost" {
64+
domain = "." + domain
65+
}
66+
67+
gc.SetSameSite(http.SameSiteNoneMode)
68+
gc.SetCookie(constants.MfaCookieName+"_session", "", -1, "/", host, secure, httpOnly)
69+
gc.SetCookie(constants.MfaCookieName+"_session_domain", "", -1, "/", domain, secure, httpOnly)
70+
}
71+
72+
// GetMfaSession gets the mfa session cookie from context
73+
func GetMfaSession(gc *gin.Context) (string, error) {
74+
var cookie *http.Cookie
75+
var err error
76+
cookie, err = gc.Request.Cookie(constants.MfaCookieName + "_session")
77+
if err != nil {
78+
cookie, err = gc.Request.Cookie(constants.MfaCookieName + "_session_domain")
79+
if err != nil {
80+
return "", err
81+
}
82+
}
83+
84+
decodedValue, err := url.PathUnescape(cookie.Value)
85+
if err != nil {
86+
return "", err
87+
}
88+
return decodedValue, nil
89+
}

server/memorystore/providers/inmemory/provider.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@ import (
77
)
88

99
type provider struct {
10-
mutex sync.Mutex
11-
sessionStore *stores.SessionStore
12-
stateStore *stores.StateStore
13-
envStore *stores.EnvStore
10+
mutex sync.Mutex
11+
sessionStore *stores.SessionStore
12+
mfasessionStore *stores.SessionStore
13+
stateStore *stores.StateStore
14+
envStore *stores.EnvStore
1415
}
1516

1617
// NewInMemoryStore returns a new in-memory store.
1718
func NewInMemoryProvider() (*provider, error) {
1819
return &provider{
19-
mutex: sync.Mutex{},
20-
envStore: stores.NewEnvStore(),
21-
sessionStore: stores.NewSessionStore(),
22-
stateStore: stores.NewStateStore(),
20+
mutex: sync.Mutex{},
21+
envStore: stores.NewEnvStore(),
22+
sessionStore: stores.NewSessionStore(),
23+
mfasessionStore: stores.NewSessionStore(),
24+
stateStore: stores.NewStateStore(),
2325
}, nil
2426
}

server/memorystore/providers/inmemory/store.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,27 @@ func (c *provider) DeleteSessionForNamespace(namespace string) error {
4242
return nil
4343
}
4444

45+
// SetMfaSession sets the mfa session with key and value of userId
46+
func (c *provider) SetMfaSession(userId, key string, expiration int64) error {
47+
c.mfasessionStore.Set(userId, key, userId, expiration)
48+
return nil
49+
}
50+
51+
// GetMfaSession returns value of given mfa session
52+
func (c *provider) GetMfaSession(userId, key string) (string, error) {
53+
val := c.mfasessionStore.Get(userId, key)
54+
if val == "" {
55+
return "", fmt.Errorf("Not found")
56+
}
57+
return val, nil
58+
}
59+
60+
// DeleteMfaSession deletes given mfa session from in-memory store.
61+
func (c *provider) DeleteMfaSession(userId, key string) error {
62+
c.mfasessionStore.Remove(userId, key)
63+
return nil
64+
}
65+
4566
// SetState sets the state in the in-memory store.
4667
func (c *provider) SetState(key, state string) error {
4768
if os.Getenv("ENV") != constants.TestEnv {

server/memorystore/providers/provider_tests.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,15 @@ func ProviderTests(t *testing.T, p Provider) {
112112
key, err = p.GetUserSession("auth_provider1:124", "access_token_key")
113113
assert.Empty(t, key)
114114
assert.Error(t, err)
115+
116+
err = p.SetMfaSession("auth_provider:123", "session123", time.Now().Add(60*time.Second).Unix())
117+
assert.NoError(t, err)
118+
key, err = p.GetMfaSession("auth_provider:123", "session123")
119+
assert.NoError(t, err)
120+
assert.Equal(t, "auth_provider:123", key)
121+
err = p.DeleteMfaSession("auth_provider:123", "session123")
122+
assert.NoError(t, err)
123+
key, err = p.GetMfaSession("auth_provider:123", "session123")
124+
assert.Error(t, err)
125+
assert.Empty(t, key)
115126
}

server/memorystore/providers/providers.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ type Provider interface {
1212
DeleteAllUserSessions(userId string) error
1313
// DeleteSessionForNamespace deletes the session for a given namespace
1414
DeleteSessionForNamespace(namespace string) error
15+
// SetMfaSession sets the mfa session with key and value of userId
16+
SetMfaSession(userId, key string, expiration int64) error
17+
// GetMfaSession returns value of given mfa session
18+
GetMfaSession(userId, key string) (string, error)
19+
// DeleteMfaSession deletes given mfa session from in-memory store.
20+
DeleteMfaSession(userId, key string) error
1521

1622
// SetState sets the login state (key, value form) in the session store
1723
SetState(key, state string) error

server/memorystore/providers/redis/store.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ var (
1616
envStorePrefix = "authorizer_env"
1717
)
1818

19+
const mfaSessionPrefix = "mfa_sess_"
20+
1921
// SetUserSession sets the user session for given user identifier in form recipe:user_id
2022
func (c *provider) SetUserSession(userId, key, token string, expiration int64) error {
2123
currentTime := time.Now()
@@ -91,6 +93,37 @@ func (c *provider) DeleteSessionForNamespace(namespace string) error {
9193
return nil
9294
}
9395

96+
// SetMfaSession sets the mfa session with key and value of userId
97+
func (c *provider) SetMfaSession(userId, key string, expiration int64) error {
98+
currentTime := time.Now()
99+
expireTime := time.Unix(expiration, 0)
100+
duration := expireTime.Sub(currentTime)
101+
err := c.store.Set(c.ctx, fmt.Sprintf("%s%s:%s", mfaSessionPrefix, userId, key), userId, duration).Err()
102+
if err != nil {
103+
log.Debug("Error saving user session to redis: ", err)
104+
return err
105+
}
106+
return nil
107+
}
108+
109+
// GetMfaSession returns value of given mfa session
110+
func (c *provider) GetMfaSession(userId, key string) (string, error) {
111+
data, err := c.store.Get(c.ctx, fmt.Sprintf("%s%s:%s", mfaSessionPrefix, userId, key)).Result()
112+
if err != nil {
113+
return "", err
114+
}
115+
return data, nil
116+
}
117+
118+
// DeleteMfaSession deletes given mfa session from in-memory store.
119+
func (c *provider) DeleteMfaSession(userId, key string) error {
120+
if err := c.store.Del(c.ctx, fmt.Sprintf("%s%s:%s", mfaSessionPrefix, userId, key)).Err(); err != nil {
121+
log.Debug("Error deleting user session from redis: ", err)
122+
// continue
123+
}
124+
return nil
125+
}
126+
94127
// SetState sets the state in redis store.
95128
func (c *provider) SetState(key, value string) error {
96129
err := c.store.Set(c.ctx, stateStorePrefix+key, value, 0).Err()

server/resolvers/login.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,25 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
113113
// If email service is not enabled continue the process in any way
114114
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && isEmailServiceEnabled && !isMFADisabled {
115115
otp := utils.GenerateOTP()
116+
expires := time.Now().Add(1 * time.Minute).Unix()
116117
otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{
117118
Email: user.Email,
118119
Otp: otp,
119-
ExpiresAt: time.Now().Add(1 * time.Minute).Unix(),
120+
ExpiresAt: expires,
120121
})
121122
if err != nil {
122123
log.Debug("Failed to add otp: ", err)
123124
return nil, err
124125
}
125126

127+
mfaSession := uuid.NewString()
128+
err = memorystore.Provider.SetMfaSession(user.ID, mfaSession, expires)
129+
if err != nil {
130+
log.Debug("Failed to add mfasession: ", err)
131+
return nil, err
132+
}
133+
cookie.SetMfaSession(gc, mfaSession)
134+
126135
go func() {
127136
// exec it as go routine so that we can reduce the api latency
128137
go email.SendEmail([]string{params.Email}, constants.VerificationTypeOTP, map[string]interface{}{

server/resolvers/mobile_login.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,25 @@ func MobileLoginResolver(ctx context.Context, params model.MobileLoginInput) (*m
122122
smsBody := strings.Builder{}
123123
smsBody.WriteString("Your verification code is: ")
124124
smsBody.WriteString(smsCode)
125+
expires := time.Now().Add(duration).Unix()
125126
_, err := db.Provider.UpsertOTP(ctx, &models.OTP{
126127
PhoneNumber: params.PhoneNumber,
127128
Otp: smsCode,
128-
ExpiresAt: time.Now().Add(duration).Unix(),
129+
ExpiresAt: expires,
129130
})
130131
if err != nil {
131132
log.Debug("error while upserting OTP: ", err.Error())
132133
return nil, err
133134
}
135+
136+
mfaSession := uuid.NewString()
137+
err = memorystore.Provider.SetMfaSession(user.ID, mfaSession, expires)
138+
if err != nil {
139+
log.Debug("Failed to add mfasession: ", err)
140+
return nil, err
141+
}
142+
cookie.SetMfaSession(gc, mfaSession)
143+
134144
go func() {
135145
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodMobileBasicAuth, user)
136146
smsproviders.SendSMS(params.PhoneNumber, smsBody.String())

server/resolvers/verify_otp.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
2727
log.Debug("Failed to get GinContext: ", err)
2828
return res, err
2929
}
30+
31+
mfaSession, err := cookie.GetMfaSession(gc)
32+
if err != nil {
33+
log.Debug("Failed to get otp request by email: ", err)
34+
return res, fmt.Errorf(`invalid session: %s`, err.Error())
35+
}
36+
3037
if refs.StringValue(params.Email) == "" && refs.StringValue(params.PhoneNumber) == "" {
3138
log.Debug("Email or phone number is required")
3239
return res, fmt.Errorf(`email or phone_number is required`)
@@ -66,6 +73,12 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
6673
log.Debug("Failed to get user by email or phone number: ", err)
6774
return res, err
6875
}
76+
77+
if _, err := memorystore.Provider.GetMfaSession(user.ID, mfaSession); err != nil {
78+
log.Debug("Failed to get mfa session: ", err)
79+
return res, fmt.Errorf(`invalid session: %s`, err.Error())
80+
}
81+
6982
isSignUp := user.EmailVerifiedAt == nil && user.PhoneNumberVerifiedAt == nil
7083
// TODO - Add Login method in DB when we introduce OTP for social media login
7184
loginMethod := constants.AuthRecipeMethodBasicAuth

0 commit comments

Comments
 (0)