Auth: Replace maximum inactive/lifetime settings of days to duration (#27150)

Allows login_maximum_inactive_lifetime_duration and 
login_maximum_lifetime_duration to be configured using 
time.Duration-compatible values while retaining backward compatibility.

Fixes #17554

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
Hansuuuuuuuuuu 2020-09-14 21:57:38 +08:00 committed by GitHub
parent f529223455
commit 8d971ab2f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 122 additions and 49 deletions

View File

@ -279,11 +279,11 @@ editors_can_admin = false
# Login cookie name # Login cookie name
login_cookie_name = grafana_session login_cookie_name = grafana_session
# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days. # The maximum lifetime (duration) an authenticated user can be inactive before being required to login at next visit. Default is 7 days (7d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month). The lifetime resets at each successful token rotation (token_rotation_interval_minutes).
login_maximum_inactive_lifetime_days = 7 login_maximum_inactive_lifetime_duration =
# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days. # The maximum lifetime (duration) an authenticated user can be logged in since login time before being required to login. Default is 30 days (30d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month).
login_maximum_lifetime_days = 30 login_maximum_lifetime_duration =
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes. # How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
token_rotation_interval_minutes = 10 token_rotation_interval_minutes = 10

View File

@ -278,11 +278,11 @@
# Login cookie name # Login cookie name
;login_cookie_name = grafana_session ;login_cookie_name = grafana_session
# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days, # The maximum lifetime (duration) an authenticated user can be inactive before being required to login at next visit. Default is 7 days (7d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month). The lifetime resets at each successful token rotation
;login_maximum_inactive_lifetime_days = 7 ;login_maximum_inactive_lifetime_duration =
# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days. # The maximum lifetime (duration) an authenticated user can be logged in since login time before being required to login. Default is 30 days (30d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month).
;login_maximum_lifetime_days = 30 ;login_maximum_lifetime_duration =
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes. # How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
;token_rotation_interval_minutes = 10 ;token_rotation_interval_minutes = 10

View File

@ -59,11 +59,13 @@ Example:
# Login cookie name # Login cookie name
login_cookie_name = grafana_session login_cookie_name = grafana_session
# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days.
login_maximum_inactive_lifetime_days = 7
# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days. # The maximum lifetime (duration) an authenticated user can be inactive before being required to login at next visit. Default is 7 days (7d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month). The lifetime resets at each successful token rotation (token_rotation_interval_minutes).
login_maximum_lifetime_days = 30 login_maximum_inactive_lifetime_duration =
# The maximum lifetime (duration) an authenticated user can be logged in since login time before being required to login. Default is 30 days (30d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month).
login_maximum_lifetime_duration =
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes. # How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
token_rotation_interval_minutes = 10 token_rotation_interval_minutes = 10

View File

@ -249,7 +249,7 @@ func (hs *HTTPServer) loginUserWithUser(user *models.User, c *models.ReqContext)
} }
hs.log.Info("Successful Login", "User", user.Email) hs.log.Info("Successful Login", "User", user.Email)
middleware.WriteSessionCookie(c, userToken.UnhashedToken, hs.Cfg.LoginMaxLifetimeDays) middleware.WriteSessionCookie(c, userToken.UnhashedToken, hs.Cfg.LoginMaxLifetime)
return nil return nil
} }

View File

@ -261,22 +261,21 @@ func rotateEndOfRequestFunc(ctx *models.ReqContext, authTokenService models.User
} }
if rotated { if rotated {
WriteSessionCookie(ctx, token.UnhashedToken, setting.LoginMaxLifetimeDays) WriteSessionCookie(ctx, token.UnhashedToken, setting.LoginMaxLifetime)
} }
} }
} }
func WriteSessionCookie(ctx *models.ReqContext, value string, maxLifetimeDays int) { func WriteSessionCookie(ctx *models.ReqContext, value string, maxLifetime time.Duration) {
if setting.Env == setting.DEV { if setting.Env == setting.DEV {
ctx.Logger.Info("New token", "unhashed token", value) ctx.Logger.Info("New token", "unhashed token", value)
} }
var maxAge int var maxAge int
if maxLifetimeDays <= 0 { if maxLifetime <= 0 {
maxAge = -1 maxAge = -1
} else { } else {
maxAgeHours := (time.Duration(setting.LoginMaxLifetimeDays) * 24 * time.Hour) + time.Hour maxAge = int(maxLifetime.Seconds())
maxAge = int(maxAgeHours.Seconds())
} }
WriteCookie(ctx.Resp, setting.LoginCookieName, url.QueryEscape(value), maxAge, newCookieOptions) WriteCookie(ctx.Resp, setting.LoginCookieName, url.QueryEscape(value), maxAge, newCookieOptions)

View File

@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/gtime"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/remotecache"
authproxy "github.com/grafana/grafana/pkg/middleware/auth_proxy" authproxy "github.com/grafana/grafana/pkg/middleware/auth_proxy"
@ -253,8 +254,7 @@ func TestMiddlewareContext(t *testing.T) {
return true, nil return true, nil
} }
maxAgeHours := (time.Duration(setting.LoginMaxLifetimeDays) * 24 * time.Hour) maxAge := int(setting.LoginMaxLifetime.Seconds())
maxAge := (maxAgeHours + time.Hour).Seconds()
sameSitePolicies := []http.SameSite{ sameSitePolicies := []http.SameSite{
http.SameSiteNoneMode, http.SameSiteNoneMode,
@ -272,7 +272,7 @@ func TestMiddlewareContext(t *testing.T) {
Value: "rotated", Value: "rotated",
Path: expectedCookiePath, Path: expectedCookiePath,
HttpOnly: true, HttpOnly: true,
MaxAge: int(maxAge), MaxAge: maxAge,
Secure: setting.CookieSecure, Secure: setting.CookieSecure,
SameSite: sameSitePolicy, SameSite: sameSitePolicy,
} }
@ -303,7 +303,7 @@ func TestMiddlewareContext(t *testing.T) {
Value: "rotated", Value: "rotated",
Path: expectedCookiePath, Path: expectedCookiePath,
HttpOnly: true, HttpOnly: true,
MaxAge: int(maxAge), MaxAge: maxAge,
Secure: setting.CookieSecure, Secure: setting.CookieSecure,
} }
@ -546,7 +546,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) {
defer bus.ClearBusHandlers() defer bus.ClearBusHandlers()
setting.LoginCookieName = "grafana_session" setting.LoginCookieName = "grafana_session"
setting.LoginMaxLifetimeDays = 30 setting.LoginMaxLifetime, _ = gtime.ParseInterval("30d")
sc := &scenarioContext{} sc := &scenarioContext{}
@ -637,7 +637,7 @@ func TestTokenRotationAtEndOfRequest(t *testing.T) {
func initTokenRotationTest(ctx context.Context) (*models.ReqContext, *httptest.ResponseRecorder, error) { func initTokenRotationTest(ctx context.Context) (*models.ReqContext, *httptest.ResponseRecorder, error) {
setting.LoginCookieName = "login_token" setting.LoginCookieName = "login_token"
setting.LoginMaxLifetimeDays = 7 setting.LoginMaxLifetime, _ = gtime.ParseInterval("7d")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
req, err := http.NewRequestWithContext(ctx, "", "", nil) req, err := http.NewRequestWithContext(ctx, "", "", nil)

View File

@ -397,13 +397,11 @@ func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64)
} }
func (s *UserAuthTokenService) createdAfterParam() int64 { func (s *UserAuthTokenService) createdAfterParam() int64 {
tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour return getTime().Add(-s.Cfg.LoginMaxLifetime).Unix()
return getTime().Add(-tokenMaxLifetime).Unix()
} }
func (s *UserAuthTokenService) rotatedAfterParam() int64 { func (s *UserAuthTokenService) rotatedAfterParam() int64 {
tokenMaxInactiveLifetime := time.Duration(s.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour return getTime().Add(-s.Cfg.LoginMaxInactiveLifetime).Unix()
return getTime().Add(-tokenMaxInactiveLifetime).Unix()
} }
func hashToken(token string) string { func hashToken(token string) string {

View File

@ -494,13 +494,14 @@ func TestUserAuthToken(t *testing.T) {
func createTestContext(t *testing.T) *testContext { func createTestContext(t *testing.T) *testContext {
t.Helper() t.Helper()
maxInactiveDurationVal, _ := time.ParseDuration("168h")
maxLifetimeDurationVal, _ := time.ParseDuration("720h")
sqlstore := sqlstore.InitTestDB(t) sqlstore := sqlstore.InitTestDB(t)
tokenService := &UserAuthTokenService{ tokenService := &UserAuthTokenService{
SQLStore: sqlstore, SQLStore: sqlstore,
Cfg: &setting.Cfg{ Cfg: &setting.Cfg{
LoginMaxInactiveLifetimeDays: 7, LoginMaxInactiveLifetime: maxInactiveDurationVal,
LoginMaxLifetimeDays: 30, LoginMaxLifetime: maxLifetimeDurationVal,
TokenRotationIntervalMinutes: 10, TokenRotationIntervalMinutes: 10,
}, },
log: log.New("test-logger"), log: log.New("test-logger"),

View File

@ -8,11 +8,12 @@ import (
) )
func (srv *UserAuthTokenService) Run(ctx context.Context) error { func (srv *UserAuthTokenService) Run(ctx context.Context) error {
var err error
ticker := time.NewTicker(time.Hour) ticker := time.NewTicker(time.Hour)
maxInactiveLifetime := time.Duration(srv.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour maxInactiveLifetime := srv.Cfg.LoginMaxInactiveLifetime
maxLifetime := time.Duration(srv.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour maxLifetime := srv.Cfg.LoginMaxLifetime
err := srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() { err = srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() {
if _, err := srv.deleteExpiredTokens(ctx, maxInactiveLifetime, maxLifetime); err != nil { if _, err := srv.deleteExpiredTokens(ctx, maxInactiveLifetime, maxLifetime); err != nil {
srv.log.Error("An error occurred while deleting expired tokens", "err", err) srv.log.Error("An error occurred while deleting expired tokens", "err", err)
} }
@ -24,7 +25,7 @@ func (srv *UserAuthTokenService) Run(ctx context.Context) error {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
err := srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() { err = srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() {
if _, err := srv.deleteExpiredTokens(ctx, maxInactiveLifetime, maxLifetime); err != nil { if _, err := srv.deleteExpiredTokens(ctx, maxInactiveLifetime, maxLifetime); err != nil {
srv.log.Error("An error occurred while deleting expired tokens", "err", err) srv.log.Error("An error occurred while deleting expired tokens", "err", err)
} }

View File

@ -12,8 +12,10 @@ import (
func TestUserAuthTokenCleanup(t *testing.T) { func TestUserAuthTokenCleanup(t *testing.T) {
Convey("Test user auth token cleanup", t, func() { Convey("Test user auth token cleanup", t, func() {
ctx := createTestContext(t) ctx := createTestContext(t)
ctx.tokenService.Cfg.LoginMaxInactiveLifetimeDays = 7 maxInactiveLifetime, _ := time.ParseDuration("168h")
ctx.tokenService.Cfg.LoginMaxLifetimeDays = 30 maxLifetime, _ := time.ParseDuration("720h")
ctx.tokenService.Cfg.LoginMaxInactiveLifetime = maxInactiveLifetime
ctx.tokenService.Cfg.LoginMaxLifetime = maxLifetime
insertToken := func(token string, prev string, createdAt, rotatedAt int64) { insertToken := func(token string, prev string, createdAt, rotatedAt int64) {
ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, CreatedAt: createdAt, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""} ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, CreatedAt: createdAt, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""}
@ -27,7 +29,7 @@ func TestUserAuthTokenCleanup(t *testing.T) {
} }
Convey("should delete tokens where token rotation age is older than or equal 7 days", func() { Convey("should delete tokens where token rotation age is older than or equal 7 days", func() {
from := t.Add(-7 * 24 * time.Hour) from := t.Add(-168 * time.Hour)
// insert three old tokens that should be deleted // insert three old tokens that should be deleted
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
@ -40,7 +42,7 @@ func TestUserAuthTokenCleanup(t *testing.T) {
insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), from.Unix(), from.Unix()) insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), from.Unix(), from.Unix())
} }
affected, err := ctx.tokenService.deleteExpiredTokens(context.Background(), 7*24*time.Hour, 30*24*time.Hour) affected, err := ctx.tokenService.deleteExpiredTokens(context.Background(), 168*time.Hour, 30*24*time.Hour)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(affected, ShouldEqual, 3) So(affected, ShouldEqual, 3)
}) })

View File

@ -140,10 +140,10 @@ var (
ViewersCanEdit bool ViewersCanEdit bool
// Http auth // Http auth
AdminUser string AdminUser string
AdminPassword string AdminPassword string
LoginCookieName string LoginCookieName string
LoginMaxLifetimeDays int LoginMaxLifetime time.Duration
AnonymousEnabled bool AnonymousEnabled bool
AnonymousOrgName string AnonymousOrgName string
@ -278,8 +278,8 @@ type Cfg struct {
// Auth // Auth
LoginCookieName string LoginCookieName string
LoginMaxInactiveLifetimeDays int LoginMaxInactiveLifetime time.Duration
LoginMaxLifetimeDays int LoginMaxLifetime time.Duration
TokenRotationIntervalMinutes int TokenRotationIntervalMinutes int
// OAuth // OAuth
@ -946,15 +946,38 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error {
return nil return nil
} }
func readAuthSettings(iniFile *ini.File, cfg *Cfg) error { func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
auth := iniFile.Section("auth") auth := iniFile.Section("auth")
LoginCookieName = valueAsString(auth, "login_cookie_name", "grafana_session") LoginCookieName = valueAsString(auth, "login_cookie_name", "grafana_session")
cfg.LoginCookieName = LoginCookieName cfg.LoginCookieName = LoginCookieName
cfg.LoginMaxInactiveLifetimeDays = auth.Key("login_maximum_inactive_lifetime_days").MustInt(7) maxInactiveDaysVal := auth.Key("login_maximum_inactive_lifetime_days").MustString("")
if maxInactiveDaysVal != "" {
maxInactiveDaysVal = fmt.Sprintf("%sd", maxInactiveDaysVal)
cfg.Logger.Warn("[Deprecated] the configuration setting 'login_maximum_inactive_lifetime_days' is deprecated, please use 'login_maximum_inactive_lifetime_duration' instead")
} else {
maxInactiveDaysVal = "7d"
}
maxInactiveDurationVal := valueAsString(auth, "login_maximum_inactive_lifetime_duration", maxInactiveDaysVal)
cfg.LoginMaxInactiveLifetime, err = gtime.ParseInterval(maxInactiveDurationVal)
if err != nil {
return err
}
maxLifetimeDaysVal := auth.Key("login_maximum_lifetime_days").MustString("")
if maxLifetimeDaysVal != "" {
maxLifetimeDaysVal = fmt.Sprintf("%sd", maxLifetimeDaysVal)
cfg.Logger.Warn("[Deprecated] the configuration setting 'login_maximum_lifetime_days' is deprecated, please use 'login_maximum_lifetime_duration' instead")
} else {
maxLifetimeDaysVal = "7d"
}
maxLifetimeDurationVal := valueAsString(auth, "login_maximum_lifetime_duration", maxLifetimeDaysVal)
cfg.LoginMaxLifetime, err = gtime.ParseInterval(maxLifetimeDurationVal)
if err != nil {
return err
}
LoginMaxLifetime = cfg.LoginMaxLifetime
LoginMaxLifetimeDays = auth.Key("login_maximum_lifetime_days").MustInt(30)
cfg.LoginMaxLifetimeDays = LoginMaxLifetimeDays
cfg.ApiKeyMaxSecondsToLive = auth.Key("api_key_max_seconds_to_live").MustInt64(-1) cfg.ApiKeyMaxSecondsToLive = auth.Key("api_key_max_seconds_to_live").MustInt64(-1)
cfg.TokenRotationIntervalMinutes = auth.Key("token_rotation_interval_minutes").MustInt(10) cfg.TokenRotationIntervalMinutes = auth.Key("token_rotation_interval_minutes").MustInt(10)

View File

@ -8,6 +8,7 @@ import (
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
"time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -314,3 +315,49 @@ func TestParseAppUrlAndSubUrl(t *testing.T) {
require.Equal(t, tc.expectedAppSubURL, appSubURL) require.Equal(t, tc.expectedAppSubURL, appSubURL)
} }
} }
func TestAuthDurationSettings(t *testing.T) {
f := ini.Empty()
cfg := NewCfg()
sec, err := f.NewSection("auth")
require.NoError(t, err)
_, err = sec.NewKey("login_maximum_inactive_lifetime_days", "10")
require.NoError(t, err)
_, err = sec.NewKey("login_maximum_inactive_lifetime_duration", "")
require.NoError(t, err)
maxInactiveDaysTest, _ := time.ParseDuration("240h")
err = readAuthSettings(f, cfg)
require.NoError(t, err)
require.Equal(t, maxInactiveDaysTest, cfg.LoginMaxInactiveLifetime)
f = ini.Empty()
sec, err = f.NewSection("auth")
require.NoError(t, err)
_, err = sec.NewKey("login_maximum_inactive_lifetime_duration", "824h")
require.NoError(t, err)
maxInactiveDurationTest, _ := time.ParseDuration("824h")
err = readAuthSettings(f, cfg)
require.NoError(t, err)
require.Equal(t, maxInactiveDurationTest, cfg.LoginMaxInactiveLifetime)
f = ini.Empty()
sec, err = f.NewSection("auth")
require.NoError(t, err)
_, err = sec.NewKey("login_maximum_lifetime_days", "24")
require.NoError(t, err)
_, err = sec.NewKey("login_maximum_lifetime_duration", "")
require.NoError(t, err)
maxLifetimeDaysTest, _ := time.ParseDuration("576h")
err = readAuthSettings(f, cfg)
require.NoError(t, err)
require.Equal(t, maxLifetimeDaysTest, cfg.LoginMaxLifetime)
f = ini.Empty()
sec, err = f.NewSection("auth")
require.NoError(t, err)
_, err = sec.NewKey("login_maximum_lifetime_duration", "824h")
require.NoError(t, err)
maxLifetimeDurationTest, _ := time.ParseDuration("824h")
err = readAuthSettings(f, cfg)
require.NoError(t, err)
require.Equal(t, maxLifetimeDurationTest, cfg.LoginMaxLifetime)
}