Merge pull request #15239 from grafana/auth_token_middleware_refactor

Auth token package and middleware refactoring
pull/15198/head^2
Marcus Efraimsson 7 years ago committed by GitHub
commit c71904e326
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 38
      conf/defaults.ini
  2. 40
      conf/sample.ini
  3. 21
      devenv/docker/ha_test/docker-compose.yaml
  4. 6
      devenv/docker/ha_test/grafana/provisioning/dashboards/dashboards.yaml
  5. 5397
      devenv/docker/ha_test/grafana/provisioning/dashboards/mysql/overview.json
  6. 14
      devenv/docker/ha_test/prometheus/prometheus.yml
  7. 14
      devenv/docker/loadtest/README.md
  8. 2
      devenv/docker/loadtest/auth_token_test.js
  9. 8
      devenv/docker/loadtest/run.sh
  10. 29
      docs/sources/auth/overview.md
  11. 8
      docs/sources/installation/configuration.md
  12. 40
      pkg/api/common_test.go
  13. 17
      pkg/api/http_server.go
  14. 18
      pkg/api/login.go
  15. 3
      pkg/api/login_oauth.go
  16. 1
      pkg/cmd/grafana-server/server.go
  17. 71
      pkg/middleware/middleware.go
  18. 149
      pkg/middleware/middleware_test.go
  19. 29
      pkg/middleware/org_redirect_test.go
  20. 15
      pkg/middleware/quota_test.go
  21. 1
      pkg/models/context.go
  22. 32
      pkg/models/user_token.go
  23. 218
      pkg/services/auth/auth_token.go
  24. 383
      pkg/services/auth/auth_token_test.go
  25. 55
      pkg/services/auth/model.go
  26. 38
      pkg/services/auth/session_cleanup.go
  27. 36
      pkg/services/auth/session_cleanup_test.go
  28. 57
      pkg/services/auth/token_cleanup.go
  29. 68
      pkg/services/auth/token_cleanup_test.go
  30. 80
      pkg/setting/setting.go

@ -106,25 +106,6 @@ path = grafana.db
# For "sqlite3" only. cache mode setting used for connecting to the database # For "sqlite3" only. cache mode setting used for connecting to the database
cache_mode = private cache_mode = private
#################################### Login ###############################
[login]
# Login cookie name
cookie_name = grafana_session
# Login cookie same site setting. defaults to `lax`. can be set to "lax", "strict" and "none"
cookie_samesite = lax
# How many days an session can be unused before we inactivate it
login_remember_days = 7
# How often should the login token be rotated. default to '10m'
rotate_token_minutes = 10
# How long should Grafana keep expired tokens before deleting them
delete_expired_token_after_days = 30
#################################### Session ############################# #################################### Session #############################
[session] [session]
# Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file" # Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
@ -206,8 +187,11 @@ data_source_proxy_whitelist =
# disable protection against brute force login attempts # disable protection against brute force login attempts
disable_brute_force_login_protection = false disable_brute_force_login_protection = false
# set cookies as https only. default is false # set to true if you host Grafana behind HTTPS. default is false.
https_flag_cookies = false cookie_secure = false
# set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict" and "none"
cookie_samesite = lax
#################################### Snapshots ########################### #################################### Snapshots ###########################
[snapshots] [snapshots]
@ -260,6 +244,18 @@ external_manage_info =
viewers_can_edit = false viewers_can_edit = false
[auth] [auth]
# Login cookie name
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.
login_maximum_lifetime_days = 30
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
token_rotation_interval_minutes = 10
# Set to true to disable (hide) the login form, useful if you use OAuth # Set to true to disable (hide) the login form, useful if you use OAuth
disable_login_form = false disable_login_form = false

@ -102,25 +102,6 @@ log_queries =
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared) # For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
;cache_mode = private ;cache_mode = private
#################################### Login ###############################
[login]
# Login cookie name
;cookie_name = grafana_session
# Login cookie same site setting. defaults to `lax`. can be set to "lax", "strict" and "none"
;cookie_samesite = lax
# How many days an session can be unused before we inactivate it
;login_remember_days = 7
# How often should the login token be rotated. default to '10'
;rotate_token_minutes = 10
# How long should Grafana keep expired tokens before deleting them
;delete_expired_token_after_days = 30
#################################### Session #################################### #################################### Session ####################################
[session] [session]
# Either "memory", "file", "redis", "mysql", "postgres", default is "file" # Either "memory", "file", "redis", "mysql", "postgres", default is "file"
@ -193,8 +174,11 @@ log_queries =
# disable protection against brute force login attempts # disable protection against brute force login attempts
;disable_brute_force_login_protection = false ;disable_brute_force_login_protection = false
# set cookies as https only. default is false # set to true if you host Grafana behind HTTPS. default is false.
;https_flag_cookies = false ;cookie_secure = false
# set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict" and "none"
;cookie_samesite = lax
#################################### Snapshots ########################### #################################### Snapshots ###########################
[snapshots] [snapshots]
@ -240,6 +224,18 @@ log_queries =
;viewers_can_edit = false ;viewers_can_edit = false
[auth] [auth]
# Login cookie name
;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.
;login_maximum_lifetime_days = 30
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
;token_rotation_interval_minutes = 10
# Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false # Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
;disable_login_form = false ;disable_login_form = false
@ -253,7 +249,7 @@ log_queries =
# This setting is ignored if multiple OAuth providers are configured. # This setting is ignored if multiple OAuth providers are configured.
;oauth_auto_login = false ;oauth_auto_login = false
#################################### Anonymous Auth ########################## #################################### Anonymous Auth ######################
[auth.anonymous] [auth.anonymous]
# enable anonymous access # enable anonymous access
;enabled = false ;enabled = false

@ -15,6 +15,7 @@ services:
MYSQL_DATABASE: grafana MYSQL_DATABASE: grafana
MYSQL_USER: grafana MYSQL_USER: grafana
MYSQL_PASSWORD: password MYSQL_PASSWORD: password
command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb_monitor_enable=all, --max-connections=1001]
ports: ports:
- 3306 - 3306
healthcheck: healthcheck:
@ -22,6 +23,16 @@ services:
timeout: 10s timeout: 10s
retries: 10 retries: 10
mysqld-exporter:
image: prom/mysqld-exporter
environment:
- DATA_SOURCE_NAME=root:rootpass@(db:3306)/
ports:
- 9104
depends_on:
db:
condition: service_healthy
# db: # db:
# image: postgres:9.3 # image: postgres:9.3
# environment: # environment:
@ -47,6 +58,7 @@ services:
- GF_DATABASE_PASSWORD=password - GF_DATABASE_PASSWORD=password
- GF_DATABASE_TYPE=mysql - GF_DATABASE_TYPE=mysql
- GF_DATABASE_HOST=db:3306 - GF_DATABASE_HOST=db:3306
- GF_DATABASE_MAX_OPEN_CONN=300
- GF_SESSION_PROVIDER=mysql - GF_SESSION_PROVIDER=mysql
- GF_SESSION_PROVIDER_CONFIG=grafana:password@tcp(db:3306)/grafana?allowNativePasswords=true - GF_SESSION_PROVIDER_CONFIG=grafana:password@tcp(db:3306)/grafana?allowNativePasswords=true
# - GF_DATABASE_TYPE=postgres # - GF_DATABASE_TYPE=postgres
@ -55,7 +67,7 @@ services:
# - GF_SESSION_PROVIDER=postgres # - GF_SESSION_PROVIDER=postgres
# - GF_SESSION_PROVIDER_CONFIG=user=grafana password=password host=db port=5432 dbname=grafana sslmode=disable # - GF_SESSION_PROVIDER_CONFIG=user=grafana password=password host=db port=5432 dbname=grafana sslmode=disable
- GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug,auth:debug - GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug,auth:debug
- GF_LOGIN_ROTATE_TOKEN_MINUTES=2 - GF_AUTH_TOKEN_ROTATION_INTERVAL_MINUTES=2
ports: ports:
- 3000 - 3000
depends_on: depends_on:
@ -70,10 +82,3 @@ services:
- VIRTUAL_HOST=prometheus.loc - VIRTUAL_HOST=prometheus.loc
ports: ports:
- 9090 - 9090
# mysqld-exporter:
# image: prom/mysqld-exporter
# environment:
# - DATA_SOURCE_NAME=grafana:password@(mysql:3306)/
# ports:
# - 9104

@ -6,3 +6,9 @@ providers:
type: file type: file
options: options:
path: /etc/grafana/provisioning/dashboards/alerts path: /etc/grafana/provisioning/dashboards/alerts
- name: 'MySQL'
folder: 'MySQL'
type: file
options:
path: /etc/grafana/provisioning/dashboards/mysql

File diff suppressed because it is too large Load Diff

@ -30,10 +30,10 @@ scrape_configs:
port: 3000 port: 3000
refresh_interval: 10s refresh_interval: 10s
# - job_name: 'mysql' - job_name: 'mysql'
# dns_sd_configs: dns_sd_configs:
# - names: - names:
# - 'mysqld-exporter' - 'mysqld-exporter'
# type: 'A' type: 'A'
# port: 9104 port: 9104
# refresh_interval: 10s refresh_interval: 10s

@ -8,7 +8,7 @@ Docker
## Run ## Run
Run load test for 15 minutes: Run load test for 15 minutes using 2 virtual users and targeting http://localhost:3000.
```bash ```bash
$ ./run.sh $ ./run.sh
@ -20,6 +20,18 @@ Run load test for custom duration:
$ ./run.sh -d 10s $ ./run.sh -d 10s
``` ```
Run load test for custom target url:
```bash
$ ./run.sh -u http://grafana.loc
```
Run load test for 10 virtual users:
```bash
$ ./run.sh -v 10
```
Example output: Example output:
```bash ```bash

@ -65,7 +65,7 @@ export default (data) => {
} }
}); });
sleep(1) sleep(5)
} }
export const teardown = (data) => {} export const teardown = (data) => {}

@ -5,8 +5,9 @@ PWD=$(pwd)
run() { run() {
duration='15m' duration='15m'
url='http://localhost:3000' url='http://localhost:3000'
vus='2'
while getopts ":d:u:" o; do while getopts ":d:u:v:" o; do
case "${o}" in case "${o}" in
d) d)
duration=${OPTARG} duration=${OPTARG}
@ -14,11 +15,14 @@ run() {
u) u)
url=${OPTARG} url=${OPTARG}
;; ;;
v)
vus=${OPTARG}
;;
esac esac
done done
shift $((OPTIND-1)) shift $((OPTIND-1))
docker run -t --network=host -v $PWD:/src -e URL=$url --rm -i loadimpact/k6:master run --vus 2 --duration $duration src/auth_token_test.js docker run -t --network=host -v $PWD:/src -e URL=$url --rm -i loadimpact/k6:master run --vus $vus --duration $duration src/auth_token_test.js
} }
run "$@" run "$@"

@ -36,6 +36,35 @@ Grafana of course has a built in user authentication system with password authen
disable authentication by enabling anonymous access. You can also hide login form and only allow login through an auth disable authentication by enabling anonymous access. You can also hide login form and only allow login through an auth
provider (listed above). There is also options for allowing self sign up. provider (listed above). There is also options for allowing self sign up.
### Login and short-lived tokens
> The followung applies when using Grafana's built in user authentication, LDAP (without Auth proxy) or OAuth integration.
Grafana are using short-lived tokens as a mechanism for verifying authenticated users.
These short-lived tokens are rotated each `token_rotation_interval_minutes` for an active authenticated user.
An active authenticated user that gets it token rotated will extend the `login_maximum_inactive_lifetime_days` time from "now" that Grafana will remember the user.
This means that a user can close its browser and come back before `now + login_maximum_inactive_lifetime_days` and still being authenticated.
This is true as long as the time since user login is less than `login_maximum_lifetime_days`.
Example:
```bash
[auth]
# Login cookie name
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.
login_maximum_lifetime_days = 30
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
token_rotation_interval_minutes = 10
```
### Anonymous authentication ### Anonymous authentication
You can make Grafana accessible without any login required by enabling anonymous access in the configuration file. You can make Grafana accessible without any login required by enabling anonymous access in the configuration file.

@ -287,6 +287,14 @@ Default is `false`.
Define a white list of allowed ips/domains to use in data sources. Format: `ip_or_domain:port` separated by spaces Define a white list of allowed ips/domains to use in data sources. Format: `ip_or_domain:port` separated by spaces
### cookie_secure
Set to `true` if you host Grafana behind HTTPS. Default is `false`.
### cookie_samesite
Sets the `SameSite` cookie attribute and prevents the browser from sending this cookie along with cross-site requests. The main goal is mitigate the risk of cross-origin information leakage. It also provides some protection against cross-site request forgery attacks (CSRF), [read more here](https://www.owasp.org/index.php/SameSite). Valid values are `lax`, `strict` and `none`. Default is `lax`.
<hr /> <hr />
## [users] ## [users]

@ -94,14 +94,13 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
} }
type scenarioContext struct { type scenarioContext struct {
m *macaron.Macaron m *macaron.Macaron
context *m.ReqContext context *m.ReqContext
resp *httptest.ResponseRecorder resp *httptest.ResponseRecorder
handlerFunc handlerFunc handlerFunc handlerFunc
defaultHandler macaron.Handler defaultHandler macaron.Handler
req *http.Request req *http.Request
url string url string
userAuthTokenService *fakeUserAuthTokenService
} }
func (sc *scenarioContext) exec() { func (sc *scenarioContext) exec() {
@ -123,30 +122,7 @@ func setupScenarioContext(url string) *scenarioContext {
Delims: macaron.Delims{Left: "[[", Right: "]]"}, Delims: macaron.Delims{Left: "[[", Right: "]]"},
})) }))
sc.userAuthTokenService = newFakeUserAuthTokenService() sc.m.Use(middleware.GetContextHandler(nil))
sc.m.Use(middleware.GetContextHandler(sc.userAuthTokenService))
return sc return sc
} }
type fakeUserAuthTokenService struct {
initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool
}
func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
return &fakeUserAuthTokenService{
initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool {
return false
},
}
}
func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool {
return s.initContextWithTokenProvider(ctx, orgID)
}
func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error {
return nil
}
func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil }

@ -21,7 +21,6 @@ import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/cache" "github.com/grafana/grafana/pkg/services/cache"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/hooks"
@ -48,14 +47,14 @@ type HTTPServer struct {
streamManager *live.StreamManager streamManager *live.StreamManager
httpSrv *http.Server httpSrv *http.Server
RouteRegister routing.RouteRegister `inject:""` RouteRegister routing.RouteRegister `inject:""`
Bus bus.Bus `inject:""` Bus bus.Bus `inject:""`
RenderService rendering.Service `inject:""` RenderService rendering.Service `inject:""`
Cfg *setting.Cfg `inject:""` Cfg *setting.Cfg `inject:""`
HooksService *hooks.HooksService `inject:""` HooksService *hooks.HooksService `inject:""`
CacheService *cache.CacheService `inject:""` CacheService *cache.CacheService `inject:""`
DatasourceCache datasources.CacheService `inject:""` DatasourceCache datasources.CacheService `inject:""`
AuthTokenService auth.UserAuthTokenService `inject:""` AuthTokenService models.UserTokenService `inject:""`
} }
func (hs *HTTPServer) Init() error { func (hs *HTTPServer) Init() error {

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
@ -126,17 +127,23 @@ func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response
func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) { func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) {
if user == nil { if user == nil {
hs.log.Error("User login with nil user") hs.log.Error("user login with nil user")
} }
err := hs.AuthTokenService.UserAuthenticatedHook(user, c) userToken, err := hs.AuthTokenService.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent())
if err != nil { if err != nil {
hs.log.Error("User auth hook failed", "error", err) hs.log.Error("failed to create auth token", "error", err)
} }
middleware.WriteSessionCookie(c, userToken.UnhashedToken, hs.Cfg.LoginMaxLifetimeDays)
} }
func (hs *HTTPServer) Logout(c *m.ReqContext) { func (hs *HTTPServer) Logout(c *m.ReqContext) {
hs.AuthTokenService.SignOutUser(c) if err := hs.AuthTokenService.RevokeToken(c.UserToken); err != nil && err != m.ErrUserTokenNotFound {
hs.log.Error("failed to revoke auth token", "error", err)
}
middleware.WriteSessionCookie(c, "", -1)
if setting.SignoutRedirectUrl != "" { if setting.SignoutRedirectUrl != "" {
c.Redirect(setting.SignoutRedirectUrl) c.Redirect(setting.SignoutRedirectUrl)
@ -176,7 +183,8 @@ func (hs *HTTPServer) trySetEncryptedCookie(ctx *m.ReqContext, cookieName string
Value: hex.EncodeToString(encryptedError), Value: hex.EncodeToString(encryptedError),
HttpOnly: true, HttpOnly: true,
Path: setting.AppSubUrl + "/", Path: setting.AppSubUrl + "/",
Secure: hs.Cfg.SecurityHTTPSCookies, Secure: hs.Cfg.CookieSecure,
SameSite: hs.Cfg.CookieSameSite,
}) })
return nil return nil

@ -214,7 +214,8 @@ func (hs *HTTPServer) writeCookie(w http.ResponseWriter, name string, value stri
Value: value, Value: value,
HttpOnly: true, HttpOnly: true,
Path: setting.AppSubUrl + "/", Path: setting.AppSubUrl + "/",
Secure: hs.Cfg.SecurityHTTPSCookies, Secure: hs.Cfg.CookieSecure,
SameSite: hs.Cfg.CookieSameSite,
}) })
} }

@ -32,6 +32,7 @@ import (
_ "github.com/grafana/grafana/pkg/metrics" _ "github.com/grafana/grafana/pkg/metrics"
_ "github.com/grafana/grafana/pkg/plugins" _ "github.com/grafana/grafana/pkg/plugins"
_ "github.com/grafana/grafana/pkg/services/alerting" _ "github.com/grafana/grafana/pkg/services/alerting"
_ "github.com/grafana/grafana/pkg/services/auth"
_ "github.com/grafana/grafana/pkg/services/cleanup" _ "github.com/grafana/grafana/pkg/services/cleanup"
_ "github.com/grafana/grafana/pkg/services/notifications" _ "github.com/grafana/grafana/pkg/services/notifications"
_ "github.com/grafana/grafana/pkg/services/provisioning" _ "github.com/grafana/grafana/pkg/services/provisioning"

@ -1,13 +1,15 @@
package middleware package middleware
import ( import (
"net/http"
"net/url"
"strconv" "strconv"
"time"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
@ -21,7 +23,7 @@ var (
ReqOrgAdmin = RoleAuth(m.ROLE_ADMIN) ReqOrgAdmin = RoleAuth(m.ROLE_ADMIN)
) )
func GetContextHandler(ats auth.UserAuthTokenService) macaron.Handler { func GetContextHandler(ats m.UserTokenService) macaron.Handler {
return func(c *macaron.Context) { return func(c *macaron.Context) {
ctx := &m.ReqContext{ ctx := &m.ReqContext{
Context: c, Context: c,
@ -49,7 +51,7 @@ func GetContextHandler(ats auth.UserAuthTokenService) macaron.Handler {
case initContextWithApiKey(ctx): case initContextWithApiKey(ctx):
case initContextWithBasicAuth(ctx, orgId): case initContextWithBasicAuth(ctx, orgId):
case initContextWithAuthProxy(ctx, orgId): case initContextWithAuthProxy(ctx, orgId):
case ats.InitContextWithToken(ctx, orgId): case initContextWithToken(ats, ctx, orgId):
case initContextWithAnonymousUser(ctx): case initContextWithAnonymousUser(ctx):
} }
@ -166,6 +168,69 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
return true return true
} }
func initContextWithToken(authTokenService m.UserTokenService, ctx *m.ReqContext, orgID int64) bool {
rawToken := ctx.GetCookie(setting.LoginCookieName)
if rawToken == "" {
return false
}
token, err := authTokenService.LookupToken(rawToken)
if err != nil {
ctx.Logger.Error("failed to look up user based on cookie", "error", err)
WriteSessionCookie(ctx, "", -1)
return false
}
query := m.GetSignedInUserQuery{UserId: token.UserId, OrgId: orgID}
if err := bus.Dispatch(&query); err != nil {
ctx.Logger.Error("failed to get user with id", "userId", token.UserId, "error", err)
return false
}
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
ctx.UserToken = token
rotated, err := authTokenService.TryRotateToken(token, ctx.RemoteAddr(), ctx.Req.UserAgent())
if err != nil {
ctx.Logger.Error("failed to rotate token", "error", err)
return true
}
if rotated {
WriteSessionCookie(ctx, token.UnhashedToken, setting.LoginMaxLifetimeDays)
}
return true
}
func WriteSessionCookie(ctx *m.ReqContext, value string, maxLifetimeDays int) {
if setting.Env == setting.DEV {
ctx.Logger.Info("new token", "unhashed token", value)
}
var maxAge int
if maxLifetimeDays <= 0 {
maxAge = -1
} else {
maxAgeHours := (time.Duration(setting.LoginMaxLifetimeDays) * 24 * time.Hour) + time.Hour
maxAge = int(maxAgeHours.Seconds())
}
ctx.Resp.Header().Del("Set-Cookie")
cookie := http.Cookie{
Name: setting.LoginCookieName,
Value: url.QueryEscape(value),
HttpOnly: true,
Path: setting.AppSubUrl + "/",
Secure: setting.CookieSecure,
MaxAge: maxAge,
SameSite: setting.CookieSameSite,
}
http.SetCookie(ctx.Resp, &cookie)
}
func AddDefaultResponseHeaders() macaron.Handler { func AddDefaultResponseHeaders() macaron.Handler {
return func(ctx *m.ReqContext) { return func(ctx *m.ReqContext) {
if ctx.IsApiRequest() && ctx.Req.Method == "GET" { if ctx.IsApiRequest() && ctx.Req.Method == "GET" {

@ -6,6 +6,7 @@ import (
"net/http/httptest" "net/http/httptest"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
msession "github.com/go-macaron/session" msession "github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
@ -146,17 +147,95 @@ func TestMiddlewareContext(t *testing.T) {
}) })
}) })
middlewareScenario("Auth token service", func(sc *scenarioContext) { middlewareScenario("Non-expired auth token in cookie which not are being rotated", func(sc *scenarioContext) {
var wasCalled bool sc.withTokenSessionCookie("token")
sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
wasCalled = true bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
return false query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
return nil
})
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 12,
UnhashedToken: unhashedToken,
}, nil
}
sc.fakeReq("GET", "/").exec()
Convey("should init context with user info", func() {
So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 12)
So(sc.context.UserToken.UserId, ShouldEqual, 12)
So(sc.context.UserToken.UnhashedToken, ShouldEqual, "token")
})
Convey("should not set cookie", func() {
So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, "")
})
})
middlewareScenario("Non-expired auth token in cookie which are being rotated", func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
return nil
})
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 12,
UnhashedToken: "",
}, nil
}
sc.userAuthTokenService.tryRotateTokenProvider = func(userToken *m.UserToken, clientIP, userAgent string) (bool, error) {
userToken.UnhashedToken = "rotated"
return true, nil
}
maxAgeHours := (time.Duration(setting.LoginMaxLifetimeDays) * 24 * time.Hour)
maxAge := (maxAgeHours + time.Hour).Seconds()
expectedCookie := &http.Cookie{
Name: setting.LoginCookieName,
Value: "rotated",
Path: setting.AppSubUrl + "/",
HttpOnly: true,
MaxAge: int(maxAge),
Secure: setting.CookieSecure,
SameSite: setting.CookieSameSite,
} }
sc.fakeReq("GET", "/").exec() sc.fakeReq("GET", "/").exec()
Convey("should call middleware", func() { Convey("should init context with user info", func() {
So(wasCalled, ShouldBeTrue) So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 12)
So(sc.context.UserToken.UserId, ShouldEqual, 12)
So(sc.context.UserToken.UnhashedToken, ShouldEqual, "rotated")
})
Convey("should set cookie", func() {
So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, expectedCookie.String())
})
})
middlewareScenario("Invalid/expired auth token in cookie", func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return nil, m.ErrUserTokenNotFound
}
sc.fakeReq("GET", "/").exec()
Convey("should not init context with user info", func() {
So(sc.context.IsSignedIn, ShouldBeFalse)
So(sc.context.UserId, ShouldEqual, 0)
So(sc.context.UserToken, ShouldBeNil)
}) })
}) })
@ -469,6 +548,9 @@ func middlewareScenario(desc string, fn scenarioFunc) {
Convey(desc, func() { Convey(desc, func() {
defer bus.ClearBusHandlers() defer bus.ClearBusHandlers()
setting.LoginCookieName = "grafana_session"
setting.LoginMaxLifetimeDays = 30
sc := &scenarioContext{} sc := &scenarioContext{}
viewsPath, _ := filepath.Abs("../../public/views") viewsPath, _ := filepath.Abs("../../public/views")
@ -508,6 +590,7 @@ type scenarioContext struct {
resp *httptest.ResponseRecorder resp *httptest.ResponseRecorder
apiKey string apiKey string
authHeader string authHeader string
tokenSessionCookie string
respJson map[string]interface{} respJson map[string]interface{}
handlerFunc handlerFunc handlerFunc handlerFunc
defaultHandler macaron.Handler defaultHandler macaron.Handler
@ -522,6 +605,11 @@ func (sc *scenarioContext) withValidApiKey() *scenarioContext {
return sc return sc
} }
func (sc *scenarioContext) withTokenSessionCookie(unhashedToken string) *scenarioContext {
sc.tokenSessionCookie = unhashedToken
return sc
}
func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioContext { func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioContext {
sc.authHeader = authHeader sc.authHeader = authHeader
return sc return sc
@ -571,6 +659,13 @@ func (sc *scenarioContext) exec() {
sc.req.Header.Add("Authorization", sc.authHeader) sc.req.Header.Add("Authorization", sc.authHeader)
} }
if sc.tokenSessionCookie != "" {
sc.req.AddCookie(&http.Cookie{
Name: setting.LoginCookieName,
Value: sc.tokenSessionCookie,
})
}
sc.m.ServeHTTP(sc.resp, sc.req) sc.m.ServeHTTP(sc.resp, sc.req)
if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" { if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" {
@ -583,23 +678,47 @@ type scenarioFunc func(c *scenarioContext)
type handlerFunc func(c *m.ReqContext) type handlerFunc func(c *m.ReqContext)
type fakeUserAuthTokenService struct { type fakeUserAuthTokenService struct {
initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool createTokenProvider func(userId int64, clientIP, userAgent string) (*m.UserToken, error)
tryRotateTokenProvider func(token *m.UserToken, clientIP, userAgent string) (bool, error)
lookupTokenProvider func(unhashedToken string) (*m.UserToken, error)
revokeTokenProvider func(token *m.UserToken) error
} }
func newFakeUserAuthTokenService() *fakeUserAuthTokenService { func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
return &fakeUserAuthTokenService{ return &fakeUserAuthTokenService{
initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool { createTokenProvider: func(userId int64, clientIP, userAgent string) (*m.UserToken, error) {
return false return &m.UserToken{
UserId: 0,
UnhashedToken: "",
}, nil
},
tryRotateTokenProvider: func(token *m.UserToken, clientIP, userAgent string) (bool, error) {
return false, nil
},
lookupTokenProvider: func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 0,
UnhashedToken: "",
}, nil
},
revokeTokenProvider: func(token *m.UserToken) error {
return nil
}, },
} }
} }
func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool { func (s *fakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*m.UserToken, error) {
return s.initContextWithTokenProvider(ctx, orgID) return s.createTokenProvider(userId, clientIP, userAgent)
} }
func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error { func (s *fakeUserAuthTokenService) LookupToken(unhashedToken string) (*m.UserToken, error) {
return nil return s.lookupTokenProvider(unhashedToken)
} }
func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil } func (s *fakeUserAuthTokenService) TryRotateToken(token *m.UserToken, clientIP, userAgent string) (bool, error) {
return s.tryRotateTokenProvider(token, clientIP, userAgent)
}
func (s *fakeUserAuthTokenService) RevokeToken(token *m.UserToken) error {
return s.revokeTokenProvider(token)
}

@ -14,14 +14,21 @@ func TestOrgRedirectMiddleware(t *testing.T) {
Convey("Can redirect to correct org", t, func() { Convey("Can redirect to correct org", t, func() {
middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) { middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error { bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
return nil return nil
}) })
sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool { bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12} query.Result = &m.SignedInUser{OrgId: 1, UserId: 12}
ctx.IsSignedIn = true return nil
return true })
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 0,
UnhashedToken: "",
}, nil
} }
sc.m.Get("/", sc.defaultHandler) sc.m.Get("/", sc.defaultHandler)
@ -33,21 +40,23 @@ func TestOrgRedirectMiddleware(t *testing.T) {
}) })
middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) { middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error { bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
return fmt.Errorf("") return fmt.Errorf("")
}) })
sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12}
ctx.IsSignedIn = true
return true
}
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 1, UserId: 12} query.Result = &m.SignedInUser{OrgId: 1, UserId: 12}
return nil return nil
}) })
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 12,
UnhashedToken: "",
}, nil
}
sc.m.Get("/", sc.defaultHandler) sc.m.Get("/", sc.defaultHandler)
sc.fakeReq("GET", "/?orgId=3").exec() sc.fakeReq("GET", "/?orgId=3").exec()

@ -74,10 +74,17 @@ func TestMiddlewareQuota(t *testing.T) {
}) })
middlewareScenario("with user logged in", func(sc *scenarioContext) { middlewareScenario("with user logged in", func(sc *scenarioContext) {
sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool { sc.withTokenSessionCookie("token")
ctx.SignedInUser = &m.SignedInUser{OrgId: 2, UserId: 12} bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
ctx.IsSignedIn = true query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
return true return nil
})
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 12,
UnhashedToken: "",
}, nil
} }
bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error { bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error {

@ -13,6 +13,7 @@ import (
type ReqContext struct { type ReqContext struct {
*macaron.Context *macaron.Context
*SignedInUser *SignedInUser
UserToken *UserToken
// This should only be used by the auth_proxy // This should only be used by the auth_proxy
Session session.SessionStore Session session.SessionStore

@ -0,0 +1,32 @@
package models
import "errors"
// Typed errors
var (
ErrUserTokenNotFound = errors.New("user token not found")
)
// UserToken represents a user token
type UserToken struct {
Id int64
UserId int64
AuthToken string
PrevAuthToken string
UserAgent string
ClientIp string
AuthTokenSeen bool
SeenAt int64
RotatedAt int64
CreatedAt int64
UpdatedAt int64
UnhashedToken string
}
// UserTokenService are used for generating and validating user tokens
type UserTokenService interface {
CreateToken(userId int64, clientIP, userAgent string) (*UserToken, error)
LookupToken(unhashedToken string) (*UserToken, error)
TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error)
RevokeToken(token *UserToken) error
}

@ -3,13 +3,10 @@ package auth
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors"
"net/http"
"net/url"
"time" "time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
@ -19,116 +16,26 @@ import (
) )
func init() { func init() {
registry.RegisterService(&UserAuthTokenServiceImpl{}) registry.RegisterService(&UserAuthTokenService{})
} }
var ( var getTime = time.Now
getTime = time.Now
UrgentRotateTime = 1 * time.Minute
oneYearInSeconds = 31557600 //used as default maxage for session cookies. We validate/rotate them more often.
)
// UserAuthTokenService are used for generating and validating user auth tokens const urgentRotateTime = 1 * time.Minute
type UserAuthTokenService interface {
InitContextWithToken(ctx *models.ReqContext, orgID int64) bool
UserAuthenticatedHook(user *models.User, c *models.ReqContext) error
SignOutUser(c *models.ReqContext) error
}
type UserAuthTokenServiceImpl struct { type UserAuthTokenService struct {
SQLStore *sqlstore.SqlStore `inject:""` SQLStore *sqlstore.SqlStore `inject:""`
ServerLockService *serverlock.ServerLockService `inject:""` ServerLockService *serverlock.ServerLockService `inject:""`
Cfg *setting.Cfg `inject:""` Cfg *setting.Cfg `inject:""`
log log.Logger log log.Logger
} }
// Init this service func (s *UserAuthTokenService) Init() error {
func (s *UserAuthTokenServiceImpl) Init() error {
s.log = log.New("auth") s.log = log.New("auth")
return nil return nil
} }
func (s *UserAuthTokenServiceImpl) InitContextWithToken(ctx *models.ReqContext, orgID int64) bool { func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserToken, error) {
//auth User
unhashedToken := ctx.GetCookie(s.Cfg.LoginCookieName)
if unhashedToken == "" {
return false
}
userToken, err := s.LookupToken(unhashedToken)
if err != nil {
ctx.Logger.Info("failed to look up user based on cookie", "error", err)
return false
}
query := models.GetSignedInUserQuery{UserId: userToken.UserId, OrgId: orgID}
if err := bus.Dispatch(&query); err != nil {
ctx.Logger.Error("Failed to get user with id", "userId", userToken.UserId, "error", err)
return false
}
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
//rotate session token if needed.
rotated, err := s.RefreshToken(userToken, ctx.RemoteAddr(), ctx.Req.UserAgent())
if err != nil {
ctx.Logger.Error("failed to rotate token", "error", err, "userId", userToken.UserId, "tokenId", userToken.Id)
return true
}
if rotated {
s.writeSessionCookie(ctx, userToken.UnhashedToken, oneYearInSeconds)
}
return true
}
func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) {
if setting.Env == setting.DEV {
ctx.Logger.Debug("new token", "unhashed token", value)
}
ctx.Resp.Header().Del("Set-Cookie")
cookie := http.Cookie{
Name: s.Cfg.LoginCookieName,
Value: url.QueryEscape(value),
HttpOnly: true,
Path: setting.AppSubUrl + "/",
Secure: s.Cfg.SecurityHTTPSCookies,
MaxAge: maxAge,
SameSite: s.Cfg.LoginCookieSameSite,
}
http.SetCookie(ctx.Resp, &cookie)
}
func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *models.ReqContext) error {
userToken, err := s.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent())
if err != nil {
return err
}
s.writeSessionCookie(c, userToken.UnhashedToken, oneYearInSeconds)
return nil
}
func (s *UserAuthTokenServiceImpl) SignOutUser(c *models.ReqContext) error {
unhashedToken := c.GetCookie(s.Cfg.LoginCookieName)
if unhashedToken == "" {
return errors.New("cannot logout without session token")
}
hashedToken := hashToken(unhashedToken)
sql := `DELETE FROM user_auth_token WHERE auth_token = ?`
_, err := s.SQLStore.NewSession().Exec(sql, hashedToken)
s.writeSessionCookie(c, "", -1)
return err
}
func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) {
clientIP = util.ParseIPAddress(clientIP) clientIP = util.ParseIPAddress(clientIP)
token, err := util.RandomHex(16) token, err := util.RandomHex(16)
if err != nil { if err != nil {
@ -139,7 +46,7 @@ func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent
now := getTime().Unix() now := getTime().Unix()
userToken := userAuthToken{ userAuthToken := userAuthToken{
UserId: userId, UserId: userId,
AuthToken: hashedToken, AuthToken: hashedToken,
PrevAuthToken: hashedToken, PrevAuthToken: hashedToken,
@ -151,98 +58,114 @@ func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent
SeenAt: 0, SeenAt: 0,
AuthTokenSeen: false, AuthTokenSeen: false,
} }
_, err = s.SQLStore.NewSession().Insert(&userToken) _, err = s.SQLStore.NewSession().Insert(&userAuthToken)
if err != nil { if err != nil {
return nil, err return nil, err
} }
userToken.UnhashedToken = token userAuthToken.UnhashedToken = token
s.log.Debug("user auth token created", "tokenId", userAuthToken.Id, "userId", userAuthToken.UserId, "clientIP", userAuthToken.ClientIp, "userAgent", userAuthToken.UserAgent, "authToken", userAuthToken.AuthToken)
return &userToken, nil var userToken models.UserToken
err = userAuthToken.toUserToken(&userToken)
return &userToken, err
} }
func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (*userAuthToken, error) { func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserToken, error) {
hashedToken := hashToken(unhashedToken) hashedToken := hashToken(unhashedToken)
if setting.Env == setting.DEV { if setting.Env == setting.DEV {
s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken) s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
} }
expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix() tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
tokenMaxInactiveLifetime := time.Duration(s.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour
createdAfter := getTime().Add(-tokenMaxLifetime).Unix()
rotatedAfter := getTime().Add(-tokenMaxInactiveLifetime).Unix()
var userToken userAuthToken var model userAuthToken
exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&userToken) exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ? AND rotated_at > ?", hashedToken, hashedToken, createdAfter, rotatedAfter).Get(&model)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !exists { if !exists {
return nil, ErrAuthTokenNotFound return nil, models.ErrUserTokenNotFound
} }
if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen { if model.AuthToken != hashedToken && model.PrevAuthToken == hashedToken && model.AuthTokenSeen {
userTokenCopy := userToken modelCopy := model
userTokenCopy.AuthTokenSeen = false modelCopy.AuthTokenSeen = false
expireBefore := getTime().Add(-UrgentRotateTime).Unix() expireBefore := getTime().Add(-urgentRotateTime).Unix()
affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", userTokenCopy.Id, userTokenCopy.PrevAuthToken, expireBefore).AllCols().Update(&userTokenCopy) affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", modelCopy.Id, modelCopy.PrevAuthToken, expireBefore).AllCols().Update(&modelCopy)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if affectedRows == 0 { if affectedRows == 0 {
s.log.Debug("prev seen token unchanged", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) s.log.Debug("prev seen token unchanged", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
} else { } else {
s.log.Debug("prev seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) s.log.Debug("prev seen token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
} }
} }
if !userToken.AuthTokenSeen && userToken.AuthToken == hashedToken { if !model.AuthTokenSeen && model.AuthToken == hashedToken {
userTokenCopy := userToken modelCopy := model
userTokenCopy.AuthTokenSeen = true modelCopy.AuthTokenSeen = true
userTokenCopy.SeenAt = getTime().Unix() modelCopy.SeenAt = getTime().Unix()
affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", userTokenCopy.Id, userTokenCopy.AuthToken).AllCols().Update(&userTokenCopy) affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", modelCopy.Id, modelCopy.AuthToken).AllCols().Update(&modelCopy)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if affectedRows == 1 { if affectedRows == 1 {
userToken = userTokenCopy model = modelCopy
} }
if affectedRows == 0 { if affectedRows == 0 {
s.log.Debug("seen wrong token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) s.log.Debug("seen wrong token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
} else { } else {
s.log.Debug("seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) s.log.Debug("seen token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
} }
} }
userToken.UnhashedToken = unhashedToken model.UnhashedToken = unhashedToken
var userToken models.UserToken
err = model.toUserToken(&userToken)
return &userToken, nil return &userToken, err
} }
func (s *UserAuthTokenServiceImpl) RefreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) { func (s *UserAuthTokenService) TryRotateToken(token *models.UserToken, clientIP, userAgent string) (bool, error) {
if token == nil { if token == nil {
return false, nil return false, nil
} }
model := userAuthTokenFromUserToken(token)
now := getTime() now := getTime()
needsRotation := false needsRotation := false
rotatedAt := time.Unix(token.RotatedAt, 0) rotatedAt := time.Unix(model.RotatedAt, 0)
if token.AuthTokenSeen { if model.AuthTokenSeen {
needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.LoginCookieRotation) * time.Minute)) needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.TokenRotationIntervalMinutes) * time.Minute))
} else { } else {
needsRotation = rotatedAt.Before(now.Add(-UrgentRotateTime)) needsRotation = rotatedAt.Before(now.Add(-urgentRotateTime))
} }
if !needsRotation { if !needsRotation {
return false, nil return false, nil
} }
s.log.Debug("refresh token needs rotation?", "auth_token_seen", token.AuthTokenSeen, "rotated_at", rotatedAt, "token.Id", token.Id) s.log.Debug("token needs rotation", "tokenId", model.Id, "authTokenSeen", model.AuthTokenSeen, "rotatedAt", rotatedAt)
clientIP = util.ParseIPAddress(clientIP) clientIP = util.ParseIPAddress(clientIP)
newToken, _ := util.RandomHex(16) newToken, err := util.RandomHex(16)
if err != nil {
return false, err
}
hashedToken := hashToken(newToken) hashedToken := hashToken(newToken)
// very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly // very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly
@ -258,21 +181,44 @@ func (s *UserAuthTokenServiceImpl) RefreshToken(token *userAuthToken, clientIP,
rotated_at = ? rotated_at = ?
WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)` WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)`
res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, s.SQLStore.Dialect.BooleanStr(true), hashedToken, s.SQLStore.Dialect.BooleanStr(false), now.Unix(), token.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix()) res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, s.SQLStore.Dialect.BooleanStr(true), hashedToken, s.SQLStore.Dialect.BooleanStr(false), now.Unix(), model.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix())
if err != nil { if err != nil {
return false, err return false, err
} }
affected, _ := res.RowsAffected() affected, _ := res.RowsAffected()
s.log.Debug("rotated", "affected", affected, "auth_token_id", token.Id, "userId", token.UserId) s.log.Debug("auth token rotated", "affected", affected, "auth_token_id", model.Id, "userId", model.UserId)
if affected > 0 { if affected > 0 {
token.UnhashedToken = newToken model.UnhashedToken = newToken
model.toUserToken(token)
return true, nil return true, nil
} }
return false, nil return false, nil
} }
func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error {
if token == nil {
return models.ErrUserTokenNotFound
}
model := userAuthTokenFromUserToken(token)
rowsAffected, err := s.SQLStore.NewSession().Delete(model)
if err != nil {
return err
}
if rowsAffected == 0 {
s.log.Debug("user auth token not found/revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent)
return models.ErrUserTokenNotFound
}
s.log.Debug("user auth token revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent)
return nil
}
func hashToken(token string) string { func hashToken(token string) string {
hashBytes := sha256.Sum256([]byte(token + setting.SecretKey)) hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
return hex.EncodeToString(hashBytes[:]) return hex.EncodeToString(hashBytes[:])

@ -1,17 +1,15 @@
package auth package auth
import ( import (
"fmt" "encoding/json"
"net/http"
"net/http/httptest"
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
) )
@ -28,236 +26,265 @@ func TestUserAuthToken(t *testing.T) {
} }
Convey("When creating token", func() { Convey("When creating token", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(token, ShouldNotBeNil) So(userToken, ShouldNotBeNil)
So(token.AuthTokenSeen, ShouldBeFalse) So(userToken.AuthTokenSeen, ShouldBeFalse)
Convey("When lookup unhashed token should return user auth token", func() { Convey("When lookup unhashed token should return user auth token", func() {
LookupToken, err := userAuthTokenService.LookupToken(token.UnhashedToken) userToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(LookupToken, ShouldNotBeNil) So(userToken, ShouldNotBeNil)
So(LookupToken.UserId, ShouldEqual, userID) So(userToken.UserId, ShouldEqual, userID)
So(LookupToken.AuthTokenSeen, ShouldBeTrue) So(userToken.AuthTokenSeen, ShouldBeTrue)
storedAuthToken, err := ctx.getAuthTokenByID(LookupToken.Id) storedAuthToken, err := ctx.getAuthTokenByID(userToken.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(storedAuthToken, ShouldNotBeNil) So(storedAuthToken, ShouldNotBeNil)
So(storedAuthToken.AuthTokenSeen, ShouldBeTrue) So(storedAuthToken.AuthTokenSeen, ShouldBeTrue)
}) })
Convey("When lookup hashed token should return user auth token not found error", func() { Convey("When lookup hashed token should return user auth token not found error", func() {
LookupToken, err := userAuthTokenService.LookupToken(token.AuthToken) userToken, err := userAuthTokenService.LookupToken(userToken.AuthToken)
So(err, ShouldEqual, ErrAuthTokenNotFound) So(err, ShouldEqual, models.ErrUserTokenNotFound)
So(LookupToken, ShouldBeNil) So(userToken, ShouldBeNil)
}) })
Convey("signing out should delete token and cookie if present", func() { Convey("revoking existing token should delete token", func() {
httpreq := &http.Request{Header: make(http.Header)} err = userAuthTokenService.RevokeToken(userToken)
httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: token.UnhashedToken})
ctx := &models.ReqContext{Context: &macaron.Context{
Req: macaron.Request{Request: httpreq},
Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()),
},
Logger: log.New("fakelogger"),
}
err = userAuthTokenService.SignOutUser(ctx)
So(err, ShouldBeNil) So(err, ShouldBeNil)
// makes sure we tell the browser to overwrite the cookie model, err := ctx.getAuthTokenByID(userToken.Id)
cookieHeader := fmt.Sprintf("%s=; Path=/; Max-Age=0; HttpOnly", userAuthTokenService.Cfg.LoginCookieName) So(err, ShouldBeNil)
So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, cookieHeader) So(model, ShouldBeNil)
}) })
Convey("signing out an none existing session should return an error", func() { Convey("revoking nil token should return error", func() {
httpreq := &http.Request{Header: make(http.Header)} err = userAuthTokenService.RevokeToken(nil)
httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: ""}) So(err, ShouldEqual, models.ErrUserTokenNotFound)
})
ctx := &models.ReqContext{Context: &macaron.Context{
Req: macaron.Request{Request: httpreq},
Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()),
},
Logger: log.New("fakelogger"),
}
err = userAuthTokenService.SignOutUser(ctx) Convey("revoking non-existing token should return error", func() {
So(err, ShouldNotBeNil) userToken.Id = 1000
err = userAuthTokenService.RevokeToken(userToken)
So(err, ShouldEqual, models.ErrUserTokenNotFound)
}) })
}) })
Convey("expires correctly", func() { Convey("expires correctly", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
_, err = userAuthTokenService.LookupToken(token.UnhashedToken) userToken, err = userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldBeNil)
token, err = ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
getTime = func() time.Time { getTime = func() time.Time {
return t.Add(time.Hour) return t.Add(time.Hour)
} }
refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.11:1234", "some user agent") rotated, err := userAuthTokenService.TryRotateToken(userToken, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue) So(rotated, ShouldBeTrue)
_, err = userAuthTokenService.LookupToken(token.UnhashedToken) userToken, err = userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldBeNil) So(err, ShouldBeNil)
stillGood, err := userAuthTokenService.LookupToken(token.UnhashedToken) stillGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(stillGood, ShouldNotBeNil) So(stillGood, ShouldNotBeNil)
getTime = func() time.Time { model, err := ctx.getAuthTokenByID(userToken.Id)
return t.Add(24 * 7 * time.Hour) So(err, ShouldBeNil)
}
notGood, err := userAuthTokenService.LookupToken(token.UnhashedToken) Convey("when rotated_at is 6:59:59 ago should find token", func() {
So(err, ShouldEqual, ErrAuthTokenNotFound) getTime = func() time.Time {
So(notGood, ShouldBeNil) return time.Unix(model.RotatedAt, 0).Add(24 * 7 * time.Hour).Add(-time.Second)
}
stillGood, err = userAuthTokenService.LookupToken(stillGood.UnhashedToken)
So(err, ShouldBeNil)
So(stillGood, ShouldNotBeNil)
})
Convey("when rotated_at is 7:00:00 ago should not find token", func() {
getTime = func() time.Time {
return time.Unix(model.RotatedAt, 0).Add(24 * 7 * time.Hour)
}
notGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldEqual, models.ErrUserTokenNotFound)
So(notGood, ShouldBeNil)
})
Convey("when rotated_at is 5 days ago and created_at is 29 days and 23:59:59 ago should not find token", func() {
updated, err := ctx.updateRotatedAt(model.Id, time.Unix(model.CreatedAt, 0).Add(24*25*time.Hour).Unix())
So(err, ShouldBeNil)
So(updated, ShouldBeTrue)
getTime = func() time.Time {
return time.Unix(model.CreatedAt, 0).Add(24 * 30 * time.Hour).Add(-time.Second)
}
stillGood, err = userAuthTokenService.LookupToken(stillGood.UnhashedToken)
So(err, ShouldBeNil)
So(stillGood, ShouldNotBeNil)
})
Convey("when rotated_at is 5 days ago and created_at is 30 days ago should not find token", func() {
updated, err := ctx.updateRotatedAt(model.Id, time.Unix(model.CreatedAt, 0).Add(24*25*time.Hour).Unix())
So(err, ShouldBeNil)
So(updated, ShouldBeTrue)
getTime = func() time.Time {
return time.Unix(model.CreatedAt, 0).Add(24 * 30 * time.Hour)
}
notGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldEqual, models.ErrUserTokenNotFound)
So(notGood, ShouldBeNil)
})
}) })
Convey("can properly rotate tokens", func() { Convey("can properly rotate tokens", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
prevToken := token.AuthToken prevToken := userToken.AuthToken
unhashedPrev := token.UnhashedToken unhashedPrev := userToken.UnhashedToken
refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") rotated, err := userAuthTokenService.TryRotateToken(userToken, "192.168.10.12:1234", "a new user agent")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(refreshed, ShouldBeFalse) So(rotated, ShouldBeFalse)
updated, err := ctx.markAuthTokenAsSeen(token.Id) updated, err := ctx.markAuthTokenAsSeen(userToken.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(updated, ShouldBeTrue) So(updated, ShouldBeTrue)
token, err = ctx.getAuthTokenByID(token.Id) model, err := ctx.getAuthTokenByID(userToken.Id)
So(err, ShouldBeNil)
var tok models.UserToken
err = model.toUserToken(&tok)
So(err, ShouldBeNil) So(err, ShouldBeNil)
getTime = func() time.Time { getTime = func() time.Time {
return t.Add(time.Hour) return t.Add(time.Hour)
} }
refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") rotated, err = userAuthTokenService.TryRotateToken(&tok, "192.168.10.12:1234", "a new user agent")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue) So(rotated, ShouldBeTrue)
unhashedToken := token.UnhashedToken unhashedToken := tok.UnhashedToken
token, err = ctx.getAuthTokenByID(token.Id) model, err = ctx.getAuthTokenByID(tok.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
token.UnhashedToken = unhashedToken model.UnhashedToken = unhashedToken
So(token.RotatedAt, ShouldEqual, getTime().Unix()) So(model.RotatedAt, ShouldEqual, getTime().Unix())
So(token.ClientIp, ShouldEqual, "192.168.10.12") So(model.ClientIp, ShouldEqual, "192.168.10.12")
So(token.UserAgent, ShouldEqual, "a new user agent") So(model.UserAgent, ShouldEqual, "a new user agent")
So(token.AuthTokenSeen, ShouldBeFalse) So(model.AuthTokenSeen, ShouldBeFalse)
So(token.SeenAt, ShouldEqual, 0) So(model.SeenAt, ShouldEqual, 0)
So(token.PrevAuthToken, ShouldEqual, prevToken) So(model.PrevAuthToken, ShouldEqual, prevToken)
// ability to auth using an old token // ability to auth using an old token
lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken) lookedUpUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil) So(lookedUpUserToken, ShouldNotBeNil)
So(lookedUp.AuthTokenSeen, ShouldBeTrue) So(lookedUpUserToken.AuthTokenSeen, ShouldBeTrue)
So(lookedUp.SeenAt, ShouldEqual, getTime().Unix()) So(lookedUpUserToken.SeenAt, ShouldEqual, getTime().Unix())
lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev) lookedUpUserToken, err = userAuthTokenService.LookupToken(unhashedPrev)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil) So(lookedUpUserToken, ShouldNotBeNil)
So(lookedUp.Id, ShouldEqual, token.Id) So(lookedUpUserToken.Id, ShouldEqual, model.Id)
So(lookedUp.AuthTokenSeen, ShouldBeTrue) So(lookedUpUserToken.AuthTokenSeen, ShouldBeTrue)
getTime = func() time.Time { getTime = func() time.Time {
return t.Add(time.Hour + (2 * time.Minute)) return t.Add(time.Hour + (2 * time.Minute))
} }
lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev) lookedUpUserToken, err = userAuthTokenService.LookupToken(unhashedPrev)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil) So(lookedUpUserToken, ShouldNotBeNil)
So(lookedUp.AuthTokenSeen, ShouldBeTrue) So(lookedUpUserToken.AuthTokenSeen, ShouldBeTrue)
lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id) lookedUpModel, err := ctx.getAuthTokenByID(lookedUpUserToken.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil) So(lookedUpModel, ShouldNotBeNil)
So(lookedUp.AuthTokenSeen, ShouldBeFalse) So(lookedUpModel.AuthTokenSeen, ShouldBeFalse)
refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") rotated, err = userAuthTokenService.TryRotateToken(userToken, "192.168.10.12:1234", "a new user agent")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue) So(rotated, ShouldBeTrue)
token, err = ctx.getAuthTokenByID(token.Id) model, err = ctx.getAuthTokenByID(userToken.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(token, ShouldNotBeNil) So(model, ShouldNotBeNil)
So(token.SeenAt, ShouldEqual, 0) So(model.SeenAt, ShouldEqual, 0)
}) })
Convey("keeps prev token valid for 1 minute after it is confirmed", func() { Convey("keeps prev token valid for 1 minute after it is confirmed", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(token, ShouldNotBeNil) So(userToken, ShouldNotBeNil)
lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken) lookedUpUserToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil) So(lookedUpUserToken, ShouldNotBeNil)
getTime = func() time.Time { getTime = func() time.Time {
return t.Add(10 * time.Minute) return t.Add(10 * time.Minute)
} }
prevToken := token.UnhashedToken prevToken := userToken.UnhashedToken
refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox") rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue) So(rotated, ShouldBeTrue)
getTime = func() time.Time { getTime = func() time.Time {
return t.Add(20 * time.Minute) return t.Add(20 * time.Minute)
} }
current, err := userAuthTokenService.LookupToken(token.UnhashedToken) currentUserToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(current, ShouldNotBeNil) So(currentUserToken, ShouldNotBeNil)
prev, err := userAuthTokenService.LookupToken(prevToken) prevUserToken, err := userAuthTokenService.LookupToken(prevToken)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(prev, ShouldNotBeNil) So(prevUserToken, ShouldNotBeNil)
}) })
Convey("will not mark token unseen when prev and current are the same", func() { Convey("will not mark token unseen when prev and current are the same", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(token, ShouldNotBeNil) So(userToken, ShouldNotBeNil)
lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken) lookedUpUserToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil) So(lookedUpUserToken, ShouldNotBeNil)
lookedUp, err = userAuthTokenService.LookupToken(token.UnhashedToken) lookedUpUserToken, err = userAuthTokenService.LookupToken(userToken.UnhashedToken)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil) So(lookedUpUserToken, ShouldNotBeNil)
lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id) lookedUpModel, err := ctx.getAuthTokenByID(lookedUpUserToken.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil) So(lookedUpModel, ShouldNotBeNil)
So(lookedUp.AuthTokenSeen, ShouldBeTrue) So(lookedUpModel.AuthTokenSeen, ShouldBeTrue)
}) })
Convey("Rotate token", func() { Convey("Rotate token", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(token, ShouldNotBeNil) So(userToken, ShouldNotBeNil)
prevToken := token.AuthToken prevToken := userToken.AuthToken
Convey("Should rotate current token and previous token when auth token seen", func() { Convey("Should rotate current token and previous token when auth token seen", func() {
updated, err := ctx.markAuthTokenAsSeen(token.Id) updated, err := ctx.markAuthTokenAsSeen(userToken.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(updated, ShouldBeTrue) So(updated, ShouldBeTrue)
@ -265,11 +292,11 @@ func TestUserAuthToken(t *testing.T) {
return t.Add(10 * time.Minute) return t.Add(10 * time.Minute)
} }
refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox") rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue) So(rotated, ShouldBeTrue)
storedToken, err := ctx.getAuthTokenByID(token.Id) storedToken, err := ctx.getAuthTokenByID(userToken.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(storedToken, ShouldNotBeNil) So(storedToken, ShouldNotBeNil)
So(storedToken.AuthTokenSeen, ShouldBeFalse) So(storedToken.AuthTokenSeen, ShouldBeFalse)
@ -278,7 +305,7 @@ func TestUserAuthToken(t *testing.T) {
prevToken = storedToken.AuthToken prevToken = storedToken.AuthToken
updated, err = ctx.markAuthTokenAsSeen(token.Id) updated, err = ctx.markAuthTokenAsSeen(userToken.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(updated, ShouldBeTrue) So(updated, ShouldBeTrue)
@ -286,11 +313,11 @@ func TestUserAuthToken(t *testing.T) {
return t.Add(20 * time.Minute) return t.Add(20 * time.Minute)
} }
refreshed, err = userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox") rotated, err = userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue) So(rotated, ShouldBeTrue)
storedToken, err = ctx.getAuthTokenByID(token.Id) storedToken, err = ctx.getAuthTokenByID(userToken.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(storedToken, ShouldNotBeNil) So(storedToken, ShouldNotBeNil)
So(storedToken.AuthTokenSeen, ShouldBeFalse) So(storedToken.AuthTokenSeen, ShouldBeFalse)
@ -299,17 +326,17 @@ func TestUserAuthToken(t *testing.T) {
}) })
Convey("Should rotate current token, but keep previous token when auth token not seen", func() { Convey("Should rotate current token, but keep previous token when auth token not seen", func() {
token.RotatedAt = getTime().Add(-2 * time.Minute).Unix() userToken.RotatedAt = getTime().Add(-2 * time.Minute).Unix()
getTime = func() time.Time { getTime = func() time.Time {
return t.Add(2 * time.Minute) return t.Add(2 * time.Minute)
} }
refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox") rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue) So(rotated, ShouldBeTrue)
storedToken, err := ctx.getAuthTokenByID(token.Id) storedToken, err := ctx.getAuthTokenByID(userToken.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(storedToken, ShouldNotBeNil) So(storedToken, ShouldNotBeNil)
So(storedToken.AuthTokenSeen, ShouldBeFalse) So(storedToken.AuthTokenSeen, ShouldBeFalse)
@ -318,6 +345,71 @@ func TestUserAuthToken(t *testing.T) {
}) })
}) })
Convey("When populating userAuthToken from UserToken should copy all properties", func() {
ut := models.UserToken{
Id: 1,
UserId: 2,
AuthToken: "a",
PrevAuthToken: "b",
UserAgent: "c",
ClientIp: "d",
AuthTokenSeen: true,
SeenAt: 3,
RotatedAt: 4,
CreatedAt: 5,
UpdatedAt: 6,
UnhashedToken: "e",
}
utBytes, err := json.Marshal(ut)
So(err, ShouldBeNil)
utJSON, err := simplejson.NewJson(utBytes)
So(err, ShouldBeNil)
utMap := utJSON.MustMap()
var uat userAuthToken
uat.fromUserToken(&ut)
uatBytes, err := json.Marshal(uat)
So(err, ShouldBeNil)
uatJSON, err := simplejson.NewJson(uatBytes)
So(err, ShouldBeNil)
uatMap := uatJSON.MustMap()
So(uatMap, ShouldResemble, utMap)
})
Convey("When populating userToken from userAuthToken should copy all properties", func() {
uat := userAuthToken{
Id: 1,
UserId: 2,
AuthToken: "a",
PrevAuthToken: "b",
UserAgent: "c",
ClientIp: "d",
AuthTokenSeen: true,
SeenAt: 3,
RotatedAt: 4,
CreatedAt: 5,
UpdatedAt: 6,
UnhashedToken: "e",
}
uatBytes, err := json.Marshal(uat)
So(err, ShouldBeNil)
uatJSON, err := simplejson.NewJson(uatBytes)
So(err, ShouldBeNil)
uatMap := uatJSON.MustMap()
var ut models.UserToken
err = uat.toUserToken(&ut)
So(err, ShouldBeNil)
utBytes, err := json.Marshal(ut)
So(err, ShouldBeNil)
utJSON, err := simplejson.NewJson(utBytes)
So(err, ShouldBeNil)
utMap := utJSON.MustMap()
So(utMap, ShouldResemble, uatMap)
})
Reset(func() { Reset(func() {
getTime = time.Now getTime = time.Now
}) })
@ -328,19 +420,16 @@ func createTestContext(t *testing.T) *testContext {
t.Helper() t.Helper()
sqlstore := sqlstore.InitTestDB(t) sqlstore := sqlstore.InitTestDB(t)
tokenService := &UserAuthTokenServiceImpl{ tokenService := &UserAuthTokenService{
SQLStore: sqlstore, SQLStore: sqlstore,
Cfg: &setting.Cfg{ Cfg: &setting.Cfg{
LoginCookieName: "grafana_session", LoginMaxInactiveLifetimeDays: 7,
LoginCookieMaxDays: 7, LoginMaxLifetimeDays: 30,
LoginDeleteExpiredTokensAfterDays: 30, TokenRotationIntervalMinutes: 10,
LoginCookieRotation: 10,
}, },
log: log.New("test-logger"), log: log.New("test-logger"),
} }
UrgentRotateTime = time.Minute
return &testContext{ return &testContext{
sqlstore: sqlstore, sqlstore: sqlstore,
tokenService: tokenService, tokenService: tokenService,
@ -349,7 +438,7 @@ func createTestContext(t *testing.T) *testContext {
type testContext struct { type testContext struct {
sqlstore *sqlstore.SqlStore sqlstore *sqlstore.SqlStore
tokenService *UserAuthTokenServiceImpl tokenService *UserAuthTokenService
} }
func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) { func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) {
@ -376,3 +465,17 @@ func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) {
} }
return rowsAffected == 1, nil return rowsAffected == 1, nil
} }
func (c *testContext) updateRotatedAt(id, rotatedAt int64) (bool, error) {
sess := c.sqlstore.NewSession()
res, err := sess.Exec("UPDATE user_auth_token SET rotated_at = ? WHERE id = ?", rotatedAt, id)
if err != nil {
return false, err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return false, err
}
return rowsAffected == 1, nil
}

@ -1,12 +1,9 @@
package auth package auth
import ( import (
"errors" "fmt"
)
// Typed errors "github.com/grafana/grafana/pkg/models"
var (
ErrAuthTokenNotFound = errors.New("User auth token not found")
) )
type userAuthToken struct { type userAuthToken struct {
@ -23,3 +20,51 @@ type userAuthToken struct {
UpdatedAt int64 UpdatedAt int64
UnhashedToken string `xorm:"-"` UnhashedToken string `xorm:"-"`
} }
func userAuthTokenFromUserToken(ut *models.UserToken) *userAuthToken {
var uat userAuthToken
uat.fromUserToken(ut)
return &uat
}
func (uat *userAuthToken) fromUserToken(ut *models.UserToken) error {
if uat == nil {
return fmt.Errorf("needs pointer to userAuthToken struct")
}
uat.Id = ut.Id
uat.UserId = ut.UserId
uat.AuthToken = ut.AuthToken
uat.PrevAuthToken = ut.PrevAuthToken
uat.UserAgent = ut.UserAgent
uat.ClientIp = ut.ClientIp
uat.AuthTokenSeen = ut.AuthTokenSeen
uat.SeenAt = ut.SeenAt
uat.RotatedAt = ut.RotatedAt
uat.CreatedAt = ut.CreatedAt
uat.UpdatedAt = ut.UpdatedAt
uat.UnhashedToken = ut.UnhashedToken
return nil
}
func (uat *userAuthToken) toUserToken(ut *models.UserToken) error {
if uat == nil {
return fmt.Errorf("needs pointer to userAuthToken struct")
}
ut.Id = uat.Id
ut.UserId = uat.UserId
ut.AuthToken = uat.AuthToken
ut.PrevAuthToken = uat.PrevAuthToken
ut.UserAgent = uat.UserAgent
ut.ClientIp = uat.ClientIp
ut.AuthTokenSeen = uat.AuthTokenSeen
ut.SeenAt = uat.SeenAt
ut.RotatedAt = uat.RotatedAt
ut.CreatedAt = uat.CreatedAt
ut.UpdatedAt = uat.UpdatedAt
ut.UnhashedToken = uat.UnhashedToken
return nil
}

@ -1,38 +0,0 @@
package auth
import (
"context"
"time"
)
func (srv *UserAuthTokenServiceImpl) Run(ctx context.Context) error {
ticker := time.NewTicker(time.Hour * 12)
deleteSessionAfter := time.Hour * 24 * time.Duration(srv.Cfg.LoginDeleteExpiredTokensAfterDays)
for {
select {
case <-ticker.C:
srv.ServerLockService.LockAndExecute(ctx, "delete old sessions", time.Hour*12, func() {
srv.deleteOldSession(deleteSessionAfter)
})
case <-ctx.Done():
return ctx.Err()
}
}
}
func (srv *UserAuthTokenServiceImpl) deleteOldSession(deleteSessionAfter time.Duration) (int64, error) {
sql := `DELETE from user_auth_token WHERE rotated_at < ?`
deleteBefore := getTime().Add(-deleteSessionAfter)
res, err := srv.SQLStore.NewSession().Exec(sql, deleteBefore.Unix())
if err != nil {
return 0, err
}
affected, err := res.RowsAffected()
srv.log.Info("deleted old sessions", "count", affected)
return affected, err
}

@ -1,36 +0,0 @@
package auth
import (
"fmt"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func TestUserAuthTokenCleanup(t *testing.T) {
Convey("Test user auth token cleanup", t, func() {
ctx := createTestContext(t)
insertToken := func(token string, prev string, rotatedAt int64) {
ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""}
_, err := ctx.sqlstore.NewSession().Insert(&ut)
So(err, ShouldBeNil)
}
// insert three old tokens that should be deleted
for i := 0; i < 3; i++ {
insertToken(fmt.Sprintf("oldA%d", i), fmt.Sprintf("oldB%d", i), int64(i))
}
// insert three active tokens that should not be deleted
for i := 0; i < 3; i++ {
insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), getTime().Unix())
}
affected, err := ctx.tokenService.deleteOldSession(time.Hour)
So(err, ShouldBeNil)
So(affected, ShouldEqual, 3)
})
}

@ -0,0 +1,57 @@
package auth
import (
"context"
"time"
)
func (srv *UserAuthTokenService) Run(ctx context.Context) error {
ticker := time.NewTicker(time.Hour)
maxInactiveLifetime := time.Duration(srv.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour
maxLifetime := time.Duration(srv.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
err := srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() {
srv.deleteExpiredTokens(maxInactiveLifetime, maxLifetime)
})
if err != nil {
srv.log.Error("failed to lock and execite cleanup of expired auth token", "erro", err)
}
for {
select {
case <-ticker.C:
err := srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() {
srv.deleteExpiredTokens(maxInactiveLifetime, maxLifetime)
})
if err != nil {
srv.log.Error("failed to lock and execite cleanup of expired auth token", "erro", err)
}
case <-ctx.Done():
return ctx.Err()
}
}
}
func (srv *UserAuthTokenService) deleteExpiredTokens(maxInactiveLifetime, maxLifetime time.Duration) (int64, error) {
createdBefore := getTime().Add(-maxLifetime)
rotatedBefore := getTime().Add(-maxInactiveLifetime)
srv.log.Debug("starting cleanup of expired auth tokens", "createdBefore", createdBefore, "rotatedBefore", rotatedBefore)
sql := `DELETE from user_auth_token WHERE created_at <= ? OR rotated_at <= ?`
res, err := srv.SQLStore.NewSession().Exec(sql, createdBefore.Unix(), rotatedBefore.Unix())
if err != nil {
return 0, err
}
affected, err := res.RowsAffected()
if err != nil {
srv.log.Error("failed to cleanup expired auth tokens", "error", err)
return 0, nil
}
srv.log.Info("cleanup of expired auth tokens done", "count", affected)
return affected, err
}

@ -0,0 +1,68 @@
package auth
import (
"fmt"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func TestUserAuthTokenCleanup(t *testing.T) {
Convey("Test user auth token cleanup", t, func() {
ctx := createTestContext(t)
ctx.tokenService.Cfg.LoginMaxInactiveLifetimeDays = 7
ctx.tokenService.Cfg.LoginMaxLifetimeDays = 30
insertToken := func(token string, prev string, createdAt, rotatedAt int64) {
ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, CreatedAt: createdAt, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""}
_, err := ctx.sqlstore.NewSession().Insert(&ut)
So(err, ShouldBeNil)
}
t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC)
getTime = func() time.Time {
return t
}
Convey("should delete tokens where token rotation age is older than or equal 7 days", func() {
from := t.Add(-7 * 24 * time.Hour)
// insert three old tokens that should be deleted
for i := 0; i < 3; i++ {
insertToken(fmt.Sprintf("oldA%d", i), fmt.Sprintf("oldB%d", i), from.Unix(), from.Unix())
}
// insert three active tokens that should not be deleted
for i := 0; i < 3; i++ {
from = from.Add(time.Second)
insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), from.Unix(), from.Unix())
}
affected, err := ctx.tokenService.deleteExpiredTokens(7*24*time.Hour, 30*24*time.Hour)
So(err, ShouldBeNil)
So(affected, ShouldEqual, 3)
})
Convey("should delete tokens where token age is older than or equal 30 days", func() {
from := t.Add(-30 * 24 * time.Hour)
fromRotate := t.Add(-time.Second)
// insert three old tokens that should be deleted
for i := 0; i < 3; i++ {
insertToken(fmt.Sprintf("oldA%d", i), fmt.Sprintf("oldB%d", i), from.Unix(), fromRotate.Unix())
}
// insert three active tokens that should not be deleted
for i := 0; i < 3; i++ {
from = from.Add(time.Second)
insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), from.Unix(), fromRotate.Unix())
}
affected, err := ctx.tokenService.deleteExpiredTokens(7*24*time.Hour, 30*24*time.Hour)
So(err, ShouldBeNil)
So(affected, ShouldEqual, 3)
})
})
}

@ -89,6 +89,8 @@ var (
EmailCodeValidMinutes int EmailCodeValidMinutes int
DataProxyWhiteList map[string]bool DataProxyWhiteList map[string]bool
DisableBruteForceLoginProtection bool DisableBruteForceLoginProtection bool
CookieSecure bool
CookieSameSite http.SameSite
// Snapshots // Snapshots
ExternalSnapshotUrl string ExternalSnapshotUrl string
@ -118,8 +120,10 @@ var (
ViewersCanEdit bool ViewersCanEdit bool
// Http auth // Http auth
AdminUser string AdminUser string
AdminPassword string AdminPassword string
LoginCookieName string
LoginMaxLifetimeDays int
AnonymousEnabled bool AnonymousEnabled bool
AnonymousOrgName string AnonymousOrgName string
@ -215,7 +219,11 @@ type Cfg struct {
RendererLimit int RendererLimit int
RendererLimitAlerting int RendererLimitAlerting int
// Security
DisableBruteForceLoginProtection bool DisableBruteForceLoginProtection bool
CookieSecure bool
CookieSameSite http.SameSite
TempDataLifetime time.Duration TempDataLifetime time.Duration
MetricsEndpointEnabled bool MetricsEndpointEnabled bool
MetricsEndpointBasicAuthUsername string MetricsEndpointBasicAuthUsername string
@ -224,13 +232,11 @@ type Cfg struct {
DisableSanitizeHtml bool DisableSanitizeHtml bool
EnterpriseLicensePath string EnterpriseLicensePath string
LoginCookieName string // Auth
LoginCookieMaxDays int LoginCookieName string
LoginCookieRotation int LoginMaxInactiveLifetimeDays int
LoginDeleteExpiredTokensAfterDays int LoginMaxLifetimeDays int
LoginCookieSameSite http.SameSite TokenRotationIntervalMinutes int
SecurityHTTPSCookies bool
} }
type CommandLineArgs struct { type CommandLineArgs struct {
@ -554,30 +560,6 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
ApplicationName = APP_NAME_ENTERPRISE ApplicationName = APP_NAME_ENTERPRISE
} }
//login
login := iniFile.Section("login")
cfg.LoginCookieName = login.Key("cookie_name").MustString("grafana_session")
cfg.LoginCookieMaxDays = login.Key("login_remember_days").MustInt(7)
cfg.LoginDeleteExpiredTokensAfterDays = login.Key("delete_expired_token_after_days").MustInt(30)
samesiteString := login.Key("cookie_samesite").MustString("lax")
validSameSiteValues := map[string]http.SameSite{
"lax": http.SameSiteLaxMode,
"strict": http.SameSiteStrictMode,
"none": http.SameSiteDefaultMode,
}
if samesite, ok := validSameSiteValues[samesiteString]; ok {
cfg.LoginCookieSameSite = samesite
} else {
cfg.LoginCookieSameSite = http.SameSiteLaxMode
}
cfg.LoginCookieRotation = login.Key("rotate_token_minutes").MustInt(10)
if cfg.LoginCookieRotation < 2 {
cfg.LoginCookieRotation = 2
}
Env = iniFile.Section("").Key("app_mode").MustString("development") Env = iniFile.Section("").Key("app_mode").MustString("development")
InstanceName = iniFile.Section("").Key("instance_name").MustString("unknown_instance_name") InstanceName = iniFile.Section("").Key("instance_name").MustString("unknown_instance_name")
PluginsPath = makeAbsolute(iniFile.Section("paths").Key("plugins").String(), HomePath) PluginsPath = makeAbsolute(iniFile.Section("paths").Key("plugins").String(), HomePath)
@ -621,9 +603,26 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
SecretKey = security.Key("secret_key").String() SecretKey = security.Key("secret_key").String()
DisableGravatar = security.Key("disable_gravatar").MustBool(true) DisableGravatar = security.Key("disable_gravatar").MustBool(true)
cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false) cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false)
cfg.SecurityHTTPSCookies = security.Key("https_flag_cookies").MustBool(false)
DisableBruteForceLoginProtection = cfg.DisableBruteForceLoginProtection DisableBruteForceLoginProtection = cfg.DisableBruteForceLoginProtection
CookieSecure = security.Key("cookie_secure").MustBool(false)
cfg.CookieSecure = CookieSecure
samesiteString := security.Key("cookie_samesite").MustString("lax")
validSameSiteValues := map[string]http.SameSite{
"lax": http.SameSiteLaxMode,
"strict": http.SameSiteStrictMode,
"none": http.SameSiteDefaultMode,
}
if samesite, ok := validSameSiteValues[samesiteString]; ok {
CookieSameSite = samesite
cfg.CookieSameSite = CookieSameSite
} else {
CookieSameSite = http.SameSiteLaxMode
cfg.CookieSameSite = CookieSameSite
}
// read snapshots settings // read snapshots settings
snapshots := iniFile.Section("snapshots") snapshots := iniFile.Section("snapshots")
ExternalSnapshotUrl = snapshots.Key("external_snapshot_url").String() ExternalSnapshotUrl = snapshots.Key("external_snapshot_url").String()
@ -661,6 +660,19 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
// auth // auth
auth := iniFile.Section("auth") auth := iniFile.Section("auth")
LoginCookieName = auth.Key("login_cookie_name").MustString("grafana_session")
cfg.LoginCookieName = LoginCookieName
cfg.LoginMaxInactiveLifetimeDays = auth.Key("login_maximum_inactive_lifetime_days").MustInt(7)
LoginMaxLifetimeDays = auth.Key("login_maximum_lifetime_days").MustInt(30)
cfg.LoginMaxLifetimeDays = LoginMaxLifetimeDays
cfg.TokenRotationIntervalMinutes = auth.Key("token_rotation_interval_minutes").MustInt(10)
if cfg.TokenRotationIntervalMinutes < 2 {
cfg.TokenRotationIntervalMinutes = 2
}
DisableLoginForm = auth.Key("disable_login_form").MustBool(false) DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false) DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false)
OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false) OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false)

Loading…
Cancel
Save