mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
57457e2aa4
commit
7cd3cd6cd4
6
pkg/services/auth/auth.go
Normal file
6
pkg/services/auth/auth.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
type UserToken interface {
|
||||||
|
GetUserId() int64
|
||||||
|
GetToken() string
|
||||||
|
}
|
@ -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[:])
|
|
||||||
}
|
|
@ -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
pkg/services/auth/authtoken/auth_token.go
Normal file
225
pkg/services/auth/authtoken/auth_token.go
Normal 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(®istry.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
pkg/services/auth/authtoken/auth_token_test.go
Normal file
386
pkg/services/auth/authtoken/auth_token_test.go
Normal 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
|
||||||
|
}
|
76
pkg/services/auth/authtoken/model.go
Normal file
76
pkg/services/auth/authtoken/model.go
Normal 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
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package auth
|
package authtoken
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@ -1,4 +1,4 @@
|
|||||||
package auth
|
package authtoken
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
@ -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:"-"`
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user