3
0

Fix session & cookie exp. setting logic

Ensures that cookies on non-permanent login are set without max-age to
ensure removal when browser/tab is closed.

Sessions stored in the database are now updated and expiration value
slides forward in time with every activity from the user.
This commit is contained in:
Denis Arh
2022-05-19 21:11:59 +02:00
parent dd3d25a5ae
commit 691e3e2900
13 changed files with 127 additions and 60 deletions

View File

@@ -504,13 +504,23 @@
# AUTH_SESSION_COOKIE_SECURE=<no value>
###############################################################################
# How long do we keep the temporary session
# Maximum time user is allowed to stay idle when logged in without "remember-me" option and before session is expired.
#
# Recomended value is between an hour and a day.
#
# [IMPORTANT]
# ====
# This affects only profile (/auth) pages. Using applications (admin, compose, ...) does not prolong the session.
# ====
#
# Type: time.Duration
# Default: 24h
# AUTH_SESSION_LIFETIME=24h
###############################################################################
# How long do we keep the permanent session
# Duration of the session in /auth lasts when user logs-in with "remember-me" option.
#
# If set to 0, "remember-me" option is removed.
# Type: time.Duration
# Default: 8640h
# AUTH_SESSION_PERM_LIFETIME=8640h

View File

@@ -103,6 +103,17 @@ func (app *CortezaApp) Setup() (err error) {
log.Warn("AUTH_JWT_EXPIRY is removed. " +
"JWT expiration value is set from AUTH_OAUTH2_ACCESS_TOKEN_LIFETIME")
}
if app.Opt.Auth.SessionLifetime < time.Hour {
log.Warn("AUTH_SESSION_LIFETIME is set to less then an hour, this might not be what you want." +
"When user logs-in without 'remember-me', AUTH_SESSION_LIFETIME is used to set a maximum time before session is expired if user does not interacts with Corteza. " +
"Recommended session lifetime value is between one hour (default) and a day")
}
if app.Opt.Auth.SessionPermLifetime < time.Hour {
log.Warn("AUTH_SESSION_PERM_LIFETIME is set to less then an hour, this might not be what you want. " +
"Recommended permanent session lifetime values are between a day and a year (default)")
}
}
hcd := healthcheck.Defaults()

View File

@@ -105,14 +105,29 @@ auth: schema.#optionsGroup & {
description: "Defaults to true when HTTPS is used. Corteza will try to guess the this setting by"
}
session_lifetime: {
type: "time.Duration"
description: "How long do we keep the temporary session"
type: "time.Duration"
description: """
Maximum time user is allowed to stay idle when logged in without \"remember-me\" option and before session is expired.
Recomended value is between an hour and a day.
[IMPORTANT]
====
This affects only profile (/auth) pages. Using applications (admin, compose, ...) does not prolong the session.
====
"""
defaultGoExpr: "24 * time.Hour"
defaultValue: "24h"
}
session_perm_lifetime: {
type: "time.Duration"
description: "How long do we keep the permanent session"
type: "time.Duration"
description: """
Duration of the session in /auth lasts when user logs-in with \"remember-me\" option.
If set to 0, \"remember-me\" option is removed.
"""
defaultGoExpr: "360 * 24 * time.Hour"
defaultValue: "8640h"
}

View File

@@ -43,6 +43,7 @@
autocomplete="current-password"
aria-label="{{ tr "login.template.form.password.label" }}">
</div>
{{ if .enableRememberMe }}
<div class="row">
<div class="col text-right">
<button
@@ -62,6 +63,7 @@
</button>
</div>
</div>
{{ end }}
{{ else }}
<div class="row">
<div class="col text-right">

View File

@@ -32,7 +32,7 @@ func (h *AuthHandlers) createPasswordForm(req *request.AuthReq) (err error) {
passwordSet = h.AuthService.PasswordSet(req.Context(), user.Email)
if !passwordSet {
// login user
req.AuthUser = request.NewAuthUser(h.Settings, user, false, h.Opt.SessionLifetime)
req.AuthUser = request.NewAuthUser(h.Settings, user, false)
// redirect back to self (but without token and with user in session)
h.Log.Debug("valid password create token found, refreshing page with stored user")

View File

@@ -62,9 +62,6 @@ func (h AuthHandlers) handleSuccessfulExternalAuth(w http.ResponseWriter, r *htt
// external logins are never permanent!
false,
// set def. lifetime for this session
h.Opt.SessionLifetime,
)
// auto-complete EmailOTP and TOTP when authenticating via external identity provider

View File

@@ -32,6 +32,7 @@ func (h *AuthHandlers) loginForm(req *request.AuthReq) error {
}
req.Data["form"] = kv
req.Data["enableRememberMe"] = h.Opt.SessionPermLifetime > 0
return nil
}
@@ -88,7 +89,7 @@ func (h *AuthHandlers) loginProc(req *request.AuthReq) (err error) {
lifetime = h.Opt.SessionPermLifetime
}
req.AuthUser = request.NewAuthUser(h.Settings, user, isPerm, lifetime)
req.AuthUser = request.NewAuthUser(h.Settings, user, isPerm)
req.AuthUser.Save(req.Session)

View File

@@ -54,7 +54,7 @@ func (h *AuthHandlers) resetPasswordForm(req *request.AuthReq) (err error) {
user, err = h.AuthService.ValidatePasswordResetToken(req.Context(), token)
if err == nil {
// login user
req.AuthUser = request.NewAuthUser(h.Settings, user, false, h.Opt.SessionLifetime)
req.AuthUser = request.NewAuthUser(h.Settings, user, false)
if req.AuthUser.PendingEmailOTP() {
// Email OTP enabled & pending

View File

@@ -44,7 +44,7 @@ func (h *AuthHandlers) signupProc(req *request.AuthReq) error {
)
req.RedirectTo = GetLinks().Profile
req.AuthUser = request.NewAuthUser(h.Settings, newUser, false, h.Opt.SessionLifetime)
req.AuthUser = request.NewAuthUser(h.Settings, newUser, false)
// auto-complete EmailOTP
req.AuthUser.CompleteEmailOTP()
@@ -110,7 +110,7 @@ func (h *AuthHandlers) confirmEmail(req *request.AuthReq) (err error) {
req.RedirectTo = GetLinks().Profile
req.AuthUser = request.NewAuthUser(h.Settings, user, false, h.Opt.SessionLifetime)
req.AuthUser = request.NewAuthUser(h.Settings, user, false)
// auto-complete EmailOTP
req.AuthUser.CompleteEmailOTP()

View File

@@ -4,7 +4,6 @@ import (
"context"
"net/http"
"net/http/httptest"
"time"
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/auth/settings"
@@ -360,7 +359,7 @@ func prepareClientAuthReq(h *AuthHandlers, req *http.Request, user *types.User)
}
if user != nil {
authReq.AuthUser = request.NewAuthUser(s, user, true, time.Duration(time.Hour))
authReq.AuthUser = request.NewAuthUser(s, user, true)
}
return authReq

View File

@@ -2,7 +2,6 @@ package request
import (
"encoding/gob"
"time"
"github.com/cortezaproject/corteza-server/auth/settings"
"github.com/cortezaproject/corteza-server/system/types"
@@ -15,8 +14,8 @@ type (
authUser struct {
User *types.User
PermSession bool
PermLifetime time.Duration
// is user logged-in with "remember-me"?
PermSession bool
MFAStatus map[authType]authStatus
}
@@ -52,12 +51,11 @@ func init() {
gob.Register(&authUser{})
}
func NewAuthUser(s *settings.Settings, u *types.User, perm bool, permLifetime time.Duration) *authUser {
func NewAuthUser(s *settings.Settings, u *types.User, perm bool) *authUser {
au := &authUser{
User: u,
PermSession: perm,
PermLifetime: permLifetime,
MFAStatus: make(map[authType]authStatus),
User: u,
PermSession: perm,
MFAStatus: make(map[authType]authStatus),
}
au.set(s, u)
@@ -154,7 +152,7 @@ func (au *authUser) ResetTOTP() {
func (au *authUser) Forget(ses *sessions.Session) {
delete(ses.Values, keyAuthUser)
delete(ses.Values, keyPermanent)
delete(ses.Values, keyRememberMe)
}
func (au *authUser) Save(ses *sessions.Session) {
@@ -163,11 +161,9 @@ func (au *authUser) Save(ses *sessions.Session) {
// explicitly save roles
ses.Values[keyRoles] = au.User.Roles()
if au.PermLifetime > 0 {
ses.Options.MaxAge = int(au.PermLifetime / time.Second)
ses.Values[keyPermanent] = true
if au.PermSession {
ses.Values[keyRememberMe] = true
} else {
ses.Options.MaxAge = 0
delete(ses.Values, keyPermanent)
delete(ses.Values, keyRememberMe)
}
}

View File

@@ -32,7 +32,7 @@ type (
Codecs []securecookie.Codec
Options *sessions.Options
opt options.AuthOpt
}
)
@@ -58,13 +58,7 @@ func CortezaSessionStore(store store.AuthSessions, opt options.AuthOpt) *corteza
return &cortezaSessionStore{
store: store,
Codecs: securecookie.CodecsFromPairs([]byte(opt.Secret)),
Options: &sessions.Options{
Path: opt.SessionCookiePath,
Domain: domain,
MaxAge: int(opt.SessionLifetime / time.Second),
Secure: opt.SessionCookieSecure,
HttpOnly: true,
},
opt: opt,
}
}
@@ -72,17 +66,28 @@ func (s cortezaSessionStore) Get(r *http.Request, name string) (*sessions.Sessio
return sessions.GetRegistry(r).Get(s, name)
}
func (s cortezaSessionStore) New(r *http.Request, name string) (*sessions.Session, error) {
session := sessions.NewSession(s, name)
session.Options = &sessions.Options{
Path: s.Options.Path,
Domain: s.Options.Domain,
MaxAge: s.Options.MaxAge,
Secure: s.Options.Secure,
HttpOnly: s.Options.HttpOnly,
func (s cortezaSessionStore) New(r *http.Request, name string) (session *sessions.Session, err error) {
var (
domain = s.opt.SessionCookieDomain
)
if strings.Contains(domain, ":") {
// do not set domain on the cookie if it contains colon
// this most likely means it contains port
//
// browsers might misbehave
domain = ""
}
session = sessions.NewSession(s, name)
session.IsNew = true
var err error
session.Options = &sessions.Options{
Path: s.opt.SessionCookiePath,
Domain: domain,
Secure: s.opt.SessionCookieSecure,
HttpOnly: true,
}
if cook, errCookie := r.Cookie(name); errCookie == nil {
err = securecookie.DecodeMulti(name, cook.Value, &session.ID, s.Codecs...)
if err == nil {
@@ -94,7 +99,8 @@ func (s cortezaSessionStore) New(r *http.Request, name string) (*sessions.Sessio
}
}
}
return session, err
return
}
func (s cortezaSessionStore) load(ctx context.Context, ses *sessions.Session) error {
@@ -103,13 +109,10 @@ func (s cortezaSessionStore) load(ctx context.Context, ses *sessions.Session) er
return err
}
var maxAge = int(cortezaSession.ExpiresAt.Sub(*now()) / time.Second)
if maxAge < 0 {
if now().After(cortezaSession.ExpiresAt) {
return fmt.Errorf("session expired")
}
ses.Options.MaxAge = maxAge
if err = gob.NewDecoder(bytes.NewReader(cortezaSession.Data)).Decode(&ses.Values); err != nil {
return fmt.Errorf("failed to decode session: %w", err)
}
@@ -151,23 +154,48 @@ func (s cortezaSessionStore) Save(r *http.Request, w http.ResponseWriter, ses *s
ses.ID = string(rand.Bytes(64))
}
if err = s.save(r.Context(), ses); err != nil {
var (
// make a copy of options for cookie
cOpt = *ses.Options
// expiration for stored session
exp time.Time
)
if IsPermLogin(ses) {
// if permanent login, make sure max-age is recalculated
// so that it slides in the future with every new session-save
cOpt.MaxAge = int(s.opt.SessionPermLifetime / time.Second)
// recalculate expiration for the stored (db) session
exp = now().Add(s.opt.SessionPermLifetime)
} else {
// Warning!
// when session is not permanent, max-age is NOT set on the cookie
// making cookie last only for the length of the session
// recalculate expiration for the stored (db) session
// we do that even if the cookie does not have a value
exp = now().Add(s.opt.SessionLifetime)
}
if err = s.save(r.Context(), ses, exp); err != nil {
return err
}
// Keep the session ID key in a cookie so it can be looked up in DB later.
// Keep the session ID key in a cookie so that it can be looked up in DB later.
encoded, err := securecookie.EncodeMulti(ses.Name(), ses.ID, s.Codecs...)
if err != nil {
return err
}
http.SetCookie(w, sessions.NewCookie(ses.Name(), encoded, ses.Options))
http.SetCookie(w, sessions.NewCookie(ses.Name(), encoded, &cOpt))
return nil
}
// save writes encoded session.Values to a database record.
// writes to http_sessions table by default.
func (s cortezaSessionStore) save(ctx context.Context, ses *sessions.Session) (err error) {
func (s cortezaSessionStore) save(ctx context.Context, ses *sessions.Session, expires time.Time) (err error) {
var (
buf = &bytes.Buffer{}
cortezaSession *types.AuthSession
@@ -191,11 +219,10 @@ func (s cortezaSessionStore) save(ctx context.Context, ses *sessions.Session) (e
extra := auth.GetExtraReqInfoFromContext(ctx)
cortezaSession.UserAgent = extra.UserAgent
cortezaSession.RemoteAddr = extra.RemoteAddr
// calculate expiration date from max-age
cortezaSession.ExpiresAt = now().Add(time.Second * time.Duration(ses.Options.MaxAge))
}
cortezaSession.ExpiresAt = expires
delete(ses.Values, keyOriginalSession)
if err = gob.NewEncoder(buf).Encode(ses.Values); err != nil {
return fmt.Errorf("failed to encode session: %w", err)

View File

@@ -8,7 +8,7 @@ import (
)
const (
keyPermanent = "permanent"
keyRememberMe = "remember-me"
keyOriginalSession = "originalSession"
keyAuthUser = "authUser"
keyRoles = "roles"
@@ -98,3 +98,12 @@ func SetOauth2ClientAuthorized(ses *sessions.Session, val bool) {
delete(ses.Values, keyOAuth2ClientAuthorized)
}
}
// IsPermLogin decodes remember-me flag from the session and returns true if set
func IsPermLogin(ses *sessions.Session) (p bool) {
if aux, has := ses.Values[keyRememberMe]; has {
p, _ = aux.(bool)
}
return
}