auth package refactoring

moving middleware/hooks away from package
exposing public struct UserToken accessible from other packages
fix debug log lines so the same order and naming are used
This commit is contained in:
Marcus Efraimsson 2019-02-02 12:11:30 +01:00
parent 57457e2aa4
commit 7cd3cd6cd4
No known key found for this signature in database
GPG Key ID: EBFE0FB04612DD4A
9 changed files with 695 additions and 684 deletions

View File

@ -0,0 +1,6 @@
package auth
type UserToken interface {
GetUserId() int64
GetToken() string
}

View File

@ -1,279 +0,0 @@
package auth
import (
"crypto/sha256"
"encoding/hex"
"errors"
"net/http"
"net/url"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func init() {
registry.RegisterService(&UserAuthTokenServiceImpl{})
}
var (
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
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 {
SQLStore *sqlstore.SqlStore `inject:""`
ServerLockService *serverlock.ServerLockService `inject:""`
Cfg *setting.Cfg `inject:""`
log log.Logger
}
// Init this service
func (s *UserAuthTokenServiceImpl) Init() error {
s.log = log.New("auth")
return nil
}
func (s *UserAuthTokenServiceImpl) InitContextWithToken(ctx *models.ReqContext, orgID int64) bool {
//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)
token, err := util.RandomHex(16)
if err != nil {
return nil, err
}
hashedToken := hashToken(token)
now := getTime().Unix()
userToken := userAuthToken{
UserId: userId,
AuthToken: hashedToken,
PrevAuthToken: hashedToken,
ClientIp: clientIP,
UserAgent: userAgent,
RotatedAt: now,
CreatedAt: now,
UpdatedAt: now,
SeenAt: 0,
AuthTokenSeen: false,
}
_, err = s.SQLStore.NewSession().Insert(&userToken)
if err != nil {
return nil, err
}
userToken.UnhashedToken = token
return &userToken, nil
}
func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (*userAuthToken, error) {
hashedToken := hashToken(unhashedToken)
if setting.Env == setting.DEV {
s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
}
expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix()
var userToken userAuthToken
exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&userToken)
if err != nil {
return nil, err
}
if !exists {
return nil, ErrAuthTokenNotFound
}
if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen {
userTokenCopy := userToken
userTokenCopy.AuthTokenSeen = false
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)
if err != nil {
return nil, err
}
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)
} else {
s.log.Debug("prev seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
}
}
if !userToken.AuthTokenSeen && userToken.AuthToken == hashedToken {
userTokenCopy := userToken
userTokenCopy.AuthTokenSeen = true
userTokenCopy.SeenAt = getTime().Unix()
affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", userTokenCopy.Id, userTokenCopy.AuthToken).AllCols().Update(&userTokenCopy)
if err != nil {
return nil, err
}
if affectedRows == 1 {
userToken = userTokenCopy
}
if affectedRows == 0 {
s.log.Debug("seen wrong token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
} else {
s.log.Debug("seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
}
}
userToken.UnhashedToken = unhashedToken
return &userToken, nil
}
func (s *UserAuthTokenServiceImpl) RefreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) {
if token == nil {
return false, nil
}
now := getTime()
needsRotation := false
rotatedAt := time.Unix(token.RotatedAt, 0)
if token.AuthTokenSeen {
needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.LoginCookieRotation) * time.Minute))
} else {
needsRotation = rotatedAt.Before(now.Add(-UrgentRotateTime))
}
if !needsRotation {
return false, nil
}
s.log.Debug("refresh token needs rotation?", "auth_token_seen", token.AuthTokenSeen, "rotated_at", rotatedAt, "token.Id", token.Id)
clientIP = util.ParseIPAddress(clientIP)
newToken, _ := util.RandomHex(16)
hashedToken := hashToken(newToken)
// very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly
sql := `
UPDATE user_auth_token
SET
seen_at = 0,
user_agent = ?,
client_ip = ?,
prev_auth_token = case when auth_token_seen = ? then auth_token else prev_auth_token end,
auth_token = ?,
auth_token_seen = ?,
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())
if err != nil {
return false, err
}
affected, _ := res.RowsAffected()
s.log.Debug("rotated", "affected", affected, "auth_token_id", token.Id, "userId", token.UserId)
if affected > 0 {
token.UnhashedToken = newToken
return true, nil
}
return false, nil
}
func hashToken(token string) string {
hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
return hex.EncodeToString(hashBytes[:])
}

View File

@ -1,378 +0,0 @@
package auth
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/services/sqlstore"
. "github.com/smartystreets/goconvey/convey"
)
func TestUserAuthToken(t *testing.T) {
Convey("Test user auth token", t, func() {
ctx := createTestContext(t)
userAuthTokenService := ctx.tokenService
userID := int64(10)
t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC)
getTime = func() time.Time {
return t
}
Convey("When creating token", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
So(token.AuthTokenSeen, ShouldBeFalse)
Convey("When lookup unhashed token should return user auth token", func() {
LookupToken, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(LookupToken, ShouldNotBeNil)
So(LookupToken.UserId, ShouldEqual, userID)
So(LookupToken.AuthTokenSeen, ShouldBeTrue)
storedAuthToken, err := ctx.getAuthTokenByID(LookupToken.Id)
So(err, ShouldBeNil)
So(storedAuthToken, ShouldNotBeNil)
So(storedAuthToken.AuthTokenSeen, ShouldBeTrue)
})
Convey("When lookup hashed token should return user auth token not found error", func() {
LookupToken, err := userAuthTokenService.LookupToken(token.AuthToken)
So(err, ShouldEqual, ErrAuthTokenNotFound)
So(LookupToken, ShouldBeNil)
})
Convey("signing out should delete token and cookie if present", func() {
httpreq := &http.Request{Header: make(http.Header)}
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)
// makes sure we tell the browser to overwrite the cookie
cookieHeader := fmt.Sprintf("%s=; Path=/; Max-Age=0; HttpOnly", userAuthTokenService.Cfg.LoginCookieName)
So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, cookieHeader)
})
Convey("signing out an none existing session should return an error", func() {
httpreq := &http.Request{Header: make(http.Header)}
httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: ""})
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, ShouldNotBeNil)
})
})
Convey("expires correctly", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
_, err = userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
token, err = ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
getTime = func() time.Time {
return t.Add(time.Hour)
}
refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
_, err = userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
stillGood, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(stillGood, ShouldNotBeNil)
getTime = func() time.Time {
return t.Add(24 * 7 * time.Hour)
}
notGood, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldEqual, ErrAuthTokenNotFound)
So(notGood, ShouldBeNil)
})
Convey("can properly rotate tokens", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
prevToken := token.AuthToken
unhashedPrev := token.UnhashedToken
refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
So(err, ShouldBeNil)
So(refreshed, ShouldBeFalse)
updated, err := ctx.markAuthTokenAsSeen(token.Id)
So(err, ShouldBeNil)
So(updated, ShouldBeTrue)
token, err = ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
getTime = func() time.Time {
return t.Add(time.Hour)
}
refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
unhashedToken := token.UnhashedToken
token, err = ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
token.UnhashedToken = unhashedToken
So(token.RotatedAt, ShouldEqual, getTime().Unix())
So(token.ClientIp, ShouldEqual, "192.168.10.12")
So(token.UserAgent, ShouldEqual, "a new user agent")
So(token.AuthTokenSeen, ShouldBeFalse)
So(token.SeenAt, ShouldEqual, 0)
So(token.PrevAuthToken, ShouldEqual, prevToken)
// ability to auth using an old token
lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
So(lookedUp.AuthTokenSeen, ShouldBeTrue)
So(lookedUp.SeenAt, ShouldEqual, getTime().Unix())
lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
So(lookedUp.Id, ShouldEqual, token.Id)
So(lookedUp.AuthTokenSeen, ShouldBeTrue)
getTime = func() time.Time {
return t.Add(time.Hour + (2 * time.Minute))
}
lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
So(lookedUp.AuthTokenSeen, ShouldBeTrue)
lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
So(lookedUp.AuthTokenSeen, ShouldBeFalse)
refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
token, err = ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
So(token.SeenAt, ShouldEqual, 0)
})
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")
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
getTime = func() time.Time {
return t.Add(10 * time.Minute)
}
prevToken := token.UnhashedToken
refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
getTime = func() time.Time {
return t.Add(20 * time.Minute)
}
current, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(current, ShouldNotBeNil)
prev, err := userAuthTokenService.LookupToken(prevToken)
So(err, ShouldBeNil)
So(prev, ShouldNotBeNil)
})
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")
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
lookedUp, err = userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
So(lookedUp.AuthTokenSeen, ShouldBeTrue)
})
Convey("Rotate token", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
prevToken := token.AuthToken
Convey("Should rotate current token and previous token when auth token seen", func() {
updated, err := ctx.markAuthTokenAsSeen(token.Id)
So(err, ShouldBeNil)
So(updated, ShouldBeTrue)
getTime = func() time.Time {
return t.Add(10 * time.Minute)
}
refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
storedToken, err := ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
So(storedToken, ShouldNotBeNil)
So(storedToken.AuthTokenSeen, ShouldBeFalse)
So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
So(storedToken.AuthToken, ShouldNotEqual, prevToken)
prevToken = storedToken.AuthToken
updated, err = ctx.markAuthTokenAsSeen(token.Id)
So(err, ShouldBeNil)
So(updated, ShouldBeTrue)
getTime = func() time.Time {
return t.Add(20 * time.Minute)
}
refreshed, err = userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
storedToken, err = ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
So(storedToken, ShouldNotBeNil)
So(storedToken.AuthTokenSeen, ShouldBeFalse)
So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
So(storedToken.AuthToken, ShouldNotEqual, prevToken)
})
Convey("Should rotate current token, but keep previous token when auth token not seen", func() {
token.RotatedAt = getTime().Add(-2 * time.Minute).Unix()
getTime = func() time.Time {
return t.Add(2 * time.Minute)
}
refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
storedToken, err := ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
So(storedToken, ShouldNotBeNil)
So(storedToken.AuthTokenSeen, ShouldBeFalse)
So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
So(storedToken.AuthToken, ShouldNotEqual, prevToken)
})
})
Reset(func() {
getTime = time.Now
})
})
}
func createTestContext(t *testing.T) *testContext {
t.Helper()
sqlstore := sqlstore.InitTestDB(t)
tokenService := &UserAuthTokenServiceImpl{
SQLStore: sqlstore,
Cfg: &setting.Cfg{
LoginCookieName: "grafana_session",
LoginCookieMaxDays: 7,
LoginDeleteExpiredTokensAfterDays: 30,
LoginCookieRotation: 10,
},
log: log.New("test-logger"),
}
UrgentRotateTime = time.Minute
return &testContext{
sqlstore: sqlstore,
tokenService: tokenService,
}
}
type testContext struct {
sqlstore *sqlstore.SqlStore
tokenService *UserAuthTokenServiceImpl
}
func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) {
sess := c.sqlstore.NewSession()
var t userAuthToken
found, err := sess.ID(id).Get(&t)
if err != nil || !found {
return nil, err
}
return &t, nil
}
func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) {
sess := c.sqlstore.NewSession()
res, err := sess.Exec("UPDATE user_auth_token SET auth_token_seen = ? WHERE id = ?", c.sqlstore.Dialect.BooleanStr(true), id)
if err != nil {
return false, err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return false, err
}
return rowsAffected == 1, nil
}

View File

@ -0,0 +1,225 @@
package authtoken
import (
"crypto/sha256"
"encoding/hex"
"time"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func init() {
registry.Register(&registry.Descriptor{
Name: "AuthTokenService",
Instance: &UserAuthTokenServiceImpl{},
InitPriority: registry.Low,
})
}
var getTime = time.Now
const urgentRotateTime = 1 * time.Minute
type UserAuthTokenServiceImpl struct {
SQLStore *sqlstore.SqlStore `inject:""`
ServerLockService *serverlock.ServerLockService `inject:""`
Cfg *setting.Cfg `inject:""`
log log.Logger
}
func (s *UserAuthTokenServiceImpl) Init() error {
s.log = log.New("auth")
return nil
}
func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (auth.UserToken, error) {
clientIP = util.ParseIPAddress(clientIP)
token, err := util.RandomHex(16)
if err != nil {
return nil, err
}
hashedToken := hashToken(token)
now := getTime().Unix()
userAuthToken := userAuthToken{
UserId: userId,
AuthToken: hashedToken,
PrevAuthToken: hashedToken,
ClientIp: clientIP,
UserAgent: userAgent,
RotatedAt: now,
CreatedAt: now,
UpdatedAt: now,
SeenAt: 0,
AuthTokenSeen: false,
}
_, err = s.SQLStore.NewSession().Insert(&userAuthToken)
if err != nil {
return nil, err
}
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 userAuthToken.toUserToken()
}
func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (auth.UserToken, error) {
hashedToken := hashToken(unhashedToken)
if setting.Env == setting.DEV {
s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
}
expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix()
var model userAuthToken
exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&model)
if err != nil {
return nil, err
}
if !exists {
return nil, ErrAuthTokenNotFound
}
if model.AuthToken != hashedToken && model.PrevAuthToken == hashedToken && model.AuthTokenSeen {
modelCopy := model
modelCopy.AuthTokenSeen = false
expireBefore := getTime().Add(-urgentRotateTime).Unix()
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 {
return nil, err
}
if affectedRows == 0 {
s.log.Debug("prev seen token unchanged", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
} else {
s.log.Debug("prev seen token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
}
}
if !model.AuthTokenSeen && model.AuthToken == hashedToken {
modelCopy := model
modelCopy.AuthTokenSeen = true
modelCopy.SeenAt = getTime().Unix()
affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", modelCopy.Id, modelCopy.AuthToken).AllCols().Update(&modelCopy)
if err != nil {
return nil, err
}
if affectedRows == 1 {
model = modelCopy
}
if affectedRows == 0 {
s.log.Debug("seen wrong token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
} else {
s.log.Debug("seen token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
}
}
model.UnhashedToken = unhashedToken
return model.toUserToken()
}
func (s *UserAuthTokenServiceImpl) TryRotateToken(token auth.UserToken, clientIP, userAgent string) (bool, error) {
if token == nil {
return false, nil
}
model, err := extractModelFromToken(token)
if err != nil {
return false, err
}
now := getTime()
needsRotation := false
rotatedAt := time.Unix(model.RotatedAt, 0)
if model.AuthTokenSeen {
needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.LoginCookieRotation) * time.Minute))
} else {
needsRotation = rotatedAt.Before(now.Add(-urgentRotateTime))
}
if !needsRotation {
return false, nil
}
s.log.Debug("token needs rotation", "tokenId", model.Id, "authTokenSeen", model.AuthTokenSeen, "rotatedAt", rotatedAt)
clientIP = util.ParseIPAddress(clientIP)
newToken, err := util.RandomHex(16)
if err != nil {
return false, err
}
hashedToken := hashToken(newToken)
// very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly
sql := `
UPDATE user_auth_token
SET
seen_at = 0,
user_agent = ?,
client_ip = ?,
prev_auth_token = case when auth_token_seen = ? then auth_token else prev_auth_token end,
auth_token = ?,
auth_token_seen = ?,
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(), model.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix())
if err != nil {
return false, err
}
affected, _ := res.RowsAffected()
s.log.Debug("auth token rotated", "affected", affected, "auth_token_id", model.Id, "userId", model.UserId)
if affected > 0 {
model.UnhashedToken = newToken
return true, nil
}
return false, nil
}
func (s *UserAuthTokenServiceImpl) RevokeToken(token auth.UserToken) error {
if token == nil {
return ErrAuthTokenNotFound
}
model, err := extractModelFromToken(token)
if err != nil {
return err
}
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 ErrAuthTokenNotFound
}
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 {
hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
return hex.EncodeToString(hashBytes[:])
}

View File

@ -0,0 +1,386 @@
package authtoken
import (
"testing"
"time"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/services/sqlstore"
. "github.com/smartystreets/goconvey/convey"
)
func TestUserAuthToken(t *testing.T) {
Convey("Test user auth token", t, func() {
ctx := createTestContext(t)
userAuthTokenService := ctx.tokenService
userID := int64(10)
t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC)
getTime = func() time.Time {
return t
}
Convey("When creating token", func() {
userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
model, err := extractModelFromToken(userToken)
So(err, ShouldBeNil)
So(model, ShouldNotBeNil)
So(model.AuthTokenSeen, ShouldBeFalse)
Convey("When lookup unhashed token should return user auth token", func() {
userToken, err := userAuthTokenService.LookupToken(model.UnhashedToken)
So(err, ShouldBeNil)
lookedUpModel, err := extractModelFromToken(userToken)
So(err, ShouldBeNil)
So(lookedUpModel, ShouldNotBeNil)
So(lookedUpModel.UserId, ShouldEqual, userID)
So(lookedUpModel.AuthTokenSeen, ShouldBeTrue)
storedAuthToken, err := ctx.getAuthTokenByID(lookedUpModel.Id)
So(err, ShouldBeNil)
So(storedAuthToken, ShouldNotBeNil)
So(storedAuthToken.AuthTokenSeen, ShouldBeTrue)
})
Convey("When lookup hashed token should return user auth token not found error", func() {
userToken, err := userAuthTokenService.LookupToken(model.AuthToken)
So(err, ShouldEqual, ErrAuthTokenNotFound)
So(userToken, ShouldBeNil)
})
Convey("revoking existing token should delete token", func() {
err = userAuthTokenService.RevokeToken(userToken)
So(err, ShouldBeNil)
model, err := ctx.getAuthTokenByID(model.Id)
So(err, ShouldBeNil)
So(model, ShouldBeNil)
})
Convey("revoking nil token should return error", func() {
err = userAuthTokenService.RevokeToken(nil)
So(err, ShouldEqual, ErrAuthTokenNotFound)
})
Convey("revoking non-existing token should return error", func() {
model.Id = 1000
nonExistingToken, err := model.toUserToken()
So(err, ShouldBeNil)
err = userAuthTokenService.RevokeToken(nonExistingToken)
So(err, ShouldEqual, ErrAuthTokenNotFound)
})
})
Convey("expires correctly", func() {
userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
model, err := extractModelFromToken(userToken)
So(err, ShouldBeNil)
So(model, ShouldNotBeNil)
_, err = userAuthTokenService.LookupToken(model.UnhashedToken)
So(err, ShouldBeNil)
model, err = ctx.getAuthTokenByID(model.Id)
So(err, ShouldBeNil)
userToken, err = model.toUserToken()
So(err, ShouldBeNil)
getTime = func() time.Time {
return t.Add(time.Hour)
}
rotated, err := userAuthTokenService.TryRotateToken(userToken, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(rotated, ShouldBeTrue)
_, err = userAuthTokenService.LookupToken(model.UnhashedToken)
So(err, ShouldBeNil)
stillGood, err := userAuthTokenService.LookupToken(model.UnhashedToken)
So(err, ShouldBeNil)
So(stillGood, ShouldNotBeNil)
getTime = func() time.Time {
return t.Add(24 * 7 * time.Hour)
}
notGood, err := userAuthTokenService.LookupToken(model.UnhashedToken)
So(err, ShouldEqual, ErrAuthTokenNotFound)
So(notGood, ShouldBeNil)
})
Convey("can properly rotate tokens", func() {
userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
model, err := extractModelFromToken(userToken)
So(err, ShouldBeNil)
So(model, ShouldNotBeNil)
prevToken := model.AuthToken
unhashedPrev := model.UnhashedToken
rotated, err := userAuthTokenService.TryRotateToken(userToken, "192.168.10.12:1234", "a new user agent")
So(err, ShouldBeNil)
So(rotated, ShouldBeFalse)
updated, err := ctx.markAuthTokenAsSeen(model.Id)
So(err, ShouldBeNil)
So(updated, ShouldBeTrue)
model, err = ctx.getAuthTokenByID(model.Id)
So(err, ShouldBeNil)
tok, err := model.toUserToken()
So(err, ShouldBeNil)
getTime = func() time.Time {
return t.Add(time.Hour)
}
rotated, err = userAuthTokenService.TryRotateToken(tok, "192.168.10.12:1234", "a new user agent")
So(err, ShouldBeNil)
So(rotated, ShouldBeTrue)
unhashedToken := model.UnhashedToken
model, err = ctx.getAuthTokenByID(model.Id)
So(err, ShouldBeNil)
model.UnhashedToken = unhashedToken
So(model.RotatedAt, ShouldEqual, getTime().Unix())
So(model.ClientIp, ShouldEqual, "192.168.10.12")
So(model.UserAgent, ShouldEqual, "a new user agent")
So(model.AuthTokenSeen, ShouldBeFalse)
So(model.SeenAt, ShouldEqual, 0)
So(model.PrevAuthToken, ShouldEqual, prevToken)
// ability to auth using an old token
lookedUpUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken)
So(err, ShouldBeNil)
lookedUpModel, err := extractModelFromToken(lookedUpUserToken)
So(err, ShouldBeNil)
So(lookedUpModel, ShouldNotBeNil)
So(lookedUpModel.AuthTokenSeen, ShouldBeTrue)
So(lookedUpModel.SeenAt, ShouldEqual, getTime().Unix())
lookedUpUserToken, err = userAuthTokenService.LookupToken(unhashedPrev)
So(err, ShouldBeNil)
So(lookedUpModel, ShouldNotBeNil)
So(lookedUpModel.Id, ShouldEqual, model.Id)
So(lookedUpModel.AuthTokenSeen, ShouldBeTrue)
getTime = func() time.Time {
return t.Add(time.Hour + (2 * time.Minute))
}
lookedUpUserToken, err = userAuthTokenService.LookupToken(unhashedPrev)
So(err, ShouldBeNil)
lookedUpModel, err = extractModelFromToken(lookedUpUserToken)
So(err, ShouldBeNil)
So(lookedUpModel, ShouldNotBeNil)
So(lookedUpModel.AuthTokenSeen, ShouldBeTrue)
lookedUpModel, err = ctx.getAuthTokenByID(lookedUpModel.Id)
So(err, ShouldBeNil)
So(lookedUpModel, ShouldNotBeNil)
So(lookedUpModel.AuthTokenSeen, ShouldBeFalse)
rotated, err = userAuthTokenService.TryRotateToken(userToken, "192.168.10.12:1234", "a new user agent")
So(err, ShouldBeNil)
So(rotated, ShouldBeTrue)
model, err = ctx.getAuthTokenByID(model.Id)
So(err, ShouldBeNil)
So(model, ShouldNotBeNil)
So(model.SeenAt, ShouldEqual, 0)
})
Convey("keeps prev token valid for 1 minute after it is confirmed", func() {
userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
model, err := extractModelFromToken(userToken)
So(err, ShouldBeNil)
So(model, ShouldNotBeNil)
lookedUpUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken)
So(err, ShouldBeNil)
So(lookedUpUserToken, ShouldNotBeNil)
getTime = func() time.Time {
return t.Add(10 * time.Minute)
}
prevToken := model.UnhashedToken
rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox")
So(err, ShouldBeNil)
So(rotated, ShouldBeTrue)
getTime = func() time.Time {
return t.Add(20 * time.Minute)
}
currentUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken)
So(err, ShouldBeNil)
So(currentUserToken, ShouldNotBeNil)
prevUserToken, err := userAuthTokenService.LookupToken(prevToken)
So(err, ShouldBeNil)
So(prevUserToken, ShouldNotBeNil)
})
Convey("will not mark token unseen when prev and current are the same", func() {
userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
model, err := extractModelFromToken(userToken)
So(err, ShouldBeNil)
So(model, ShouldNotBeNil)
lookedUpUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken)
So(err, ShouldBeNil)
lookedUpModel, err := extractModelFromToken(lookedUpUserToken)
So(err, ShouldBeNil)
So(lookedUpModel, ShouldNotBeNil)
lookedUpUserToken, err = userAuthTokenService.LookupToken(model.UnhashedToken)
So(err, ShouldBeNil)
lookedUpModel, err = extractModelFromToken(lookedUpUserToken)
So(err, ShouldBeNil)
So(lookedUpModel, ShouldNotBeNil)
lookedUpModel, err = ctx.getAuthTokenByID(lookedUpModel.Id)
So(err, ShouldBeNil)
So(lookedUpModel, ShouldNotBeNil)
So(lookedUpModel.AuthTokenSeen, ShouldBeTrue)
})
Convey("Rotate token", func() {
userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
model, err := extractModelFromToken(userToken)
So(err, ShouldBeNil)
So(model, ShouldNotBeNil)
prevToken := model.AuthToken
Convey("Should rotate current token and previous token when auth token seen", func() {
updated, err := ctx.markAuthTokenAsSeen(model.Id)
So(err, ShouldBeNil)
So(updated, ShouldBeTrue)
getTime = func() time.Time {
return t.Add(10 * time.Minute)
}
rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox")
So(err, ShouldBeNil)
So(rotated, ShouldBeTrue)
storedToken, err := ctx.getAuthTokenByID(model.Id)
So(err, ShouldBeNil)
So(storedToken, ShouldNotBeNil)
So(storedToken.AuthTokenSeen, ShouldBeFalse)
So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
So(storedToken.AuthToken, ShouldNotEqual, prevToken)
prevToken = storedToken.AuthToken
updated, err = ctx.markAuthTokenAsSeen(model.Id)
So(err, ShouldBeNil)
So(updated, ShouldBeTrue)
getTime = func() time.Time {
return t.Add(20 * time.Minute)
}
rotated, err = userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox")
So(err, ShouldBeNil)
So(rotated, ShouldBeTrue)
storedToken, err = ctx.getAuthTokenByID(model.Id)
So(err, ShouldBeNil)
So(storedToken, ShouldNotBeNil)
So(storedToken.AuthTokenSeen, ShouldBeFalse)
So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
So(storedToken.AuthToken, ShouldNotEqual, prevToken)
})
Convey("Should rotate current token, but keep previous token when auth token not seen", func() {
model.RotatedAt = getTime().Add(-2 * time.Minute).Unix()
getTime = func() time.Time {
return t.Add(2 * time.Minute)
}
rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox")
So(err, ShouldBeNil)
So(rotated, ShouldBeTrue)
storedToken, err := ctx.getAuthTokenByID(model.Id)
So(err, ShouldBeNil)
So(storedToken, ShouldNotBeNil)
So(storedToken.AuthTokenSeen, ShouldBeFalse)
So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
So(storedToken.AuthToken, ShouldNotEqual, prevToken)
})
})
Reset(func() {
getTime = time.Now
})
})
}
func createTestContext(t *testing.T) *testContext {
t.Helper()
sqlstore := sqlstore.InitTestDB(t)
tokenService := &UserAuthTokenServiceImpl{
SQLStore: sqlstore,
Cfg: &setting.Cfg{
LoginCookieName: "grafana_session",
LoginCookieMaxDays: 7,
LoginDeleteExpiredTokensAfterDays: 30,
LoginCookieRotation: 10,
},
log: log.New("test-logger"),
}
return &testContext{
sqlstore: sqlstore,
tokenService: tokenService,
}
}
type testContext struct {
sqlstore *sqlstore.SqlStore
tokenService *UserAuthTokenServiceImpl
}
func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) {
sess := c.sqlstore.NewSession()
var t userAuthToken
found, err := sess.ID(id).Get(&t)
if err != nil || !found {
return nil, err
}
return &t, nil
}
func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) {
sess := c.sqlstore.NewSession()
res, err := sess.Exec("UPDATE user_auth_token SET auth_token_seen = ? WHERE id = ?", c.sqlstore.Dialect.BooleanStr(true), id)
if err != nil {
return false, err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return false, err
}
return rowsAffected == 1, nil
}

View File

@ -0,0 +1,76 @@
package authtoken
import (
"errors"
"fmt"
"github.com/grafana/grafana/pkg/services/auth"
)
// Typed errors
var (
ErrAuthTokenNotFound = errors.New("user auth token not found")
)
type userAuthToken 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 `xorm:"-"`
}
func (uat *userAuthToken) toUserToken() (auth.UserToken, error) {
if uat == nil {
return nil, fmt.Errorf("needs pointer to userAuthToken struct")
}
return &userTokenImpl{
userAuthToken: uat,
}, nil
}
type userToken interface {
auth.UserToken
GetModel() *userAuthToken
}
type userTokenImpl struct {
*userAuthToken
}
func (ut *userTokenImpl) GetUserId() int64 {
return ut.UserId
}
func (ut *userTokenImpl) GetToken() string {
return ut.UnhashedToken
}
func (ut *userTokenImpl) GetModel() *userAuthToken {
return ut.userAuthToken
}
func extractModelFromToken(token auth.UserToken) (*userAuthToken, error) {
ut, ok := token.(userToken)
if !ok {
return nil, fmt.Errorf("failed to cast token")
}
return ut.GetModel(), nil
}
// UserAuthTokenService are used for generating and validating user auth tokens
type UserAuthTokenService interface {
CreateToken(userId int64, clientIP, userAgent string) (auth.UserToken, error)
LookupToken(unhashedToken string) (auth.UserToken, error)
TryRotateToken(token auth.UserToken, clientIP, userAgent string) (bool, error)
RevokeToken(token auth.UserToken) error
}

View File

@ -1,4 +1,4 @@
package auth
package authtoken
import (
"context"

View File

@ -1,4 +1,4 @@
package auth
package authtoken
import (
"fmt"

View File

@ -1,25 +0,0 @@
package auth
import (
"errors"
)
// Typed errors
var (
ErrAuthTokenNotFound = errors.New("User auth token not found")
)
type userAuthToken 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 `xorm:"-"`
}