소스 검색

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
Marcus Efraimsson 6 년 전
부모
커밋
7cd3cd6cd4

+ 6 - 0
pkg/services/auth/auth.go

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

+ 0 - 279
pkg/services/auth/auth_token.go

@@ -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[:])
-}

+ 0 - 378
pkg/services/auth/auth_token_test.go

@@ -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
-}

+ 225 - 0
pkg/services/auth/authtoken/auth_token.go

@@ -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[:])
+}

+ 386 - 0
pkg/services/auth/authtoken/auth_token_test.go

@@ -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
+}

+ 76 - 0
pkg/services/auth/authtoken/model.go

@@ -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
+}

+ 1 - 1
pkg/services/auth/session_cleanup.go → pkg/services/auth/authtoken/session_cleanup.go

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

+ 1 - 1
pkg/services/auth/session_cleanup_test.go → pkg/services/auth/authtoken/session_cleanup_test.go

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

+ 0 - 25
pkg/services/auth/model.go

@@ -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:"-"`
-}