Auth: Refactor auth package (#58920)
* Auth: move interface to its own file * Auth: move to test package * Auth: move quota consts to auth file * Auth: move service to impl package * Auth: move interfaces and related models to auth package * Auth: Create sub package and type alias to avoid circular dependency
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
package authimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
const ServiceName = "UserAuthTokenService"
|
||||
|
||||
var getTime = time.Now
|
||||
|
||||
const urgentRotateTime = 1 * time.Minute
|
||||
|
||||
func ProvideUserAuthTokenService(sqlStore db.DB, serverLockService *serverlock.ServerLockService,
|
||||
cfg *setting.Cfg) *UserAuthTokenService {
|
||||
s := &UserAuthTokenService{
|
||||
SQLStore: sqlStore,
|
||||
ServerLockService: serverLockService,
|
||||
Cfg: cfg,
|
||||
log: log.New("auth"),
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type UserAuthTokenService struct {
|
||||
SQLStore db.DB
|
||||
ServerLockService *serverlock.ServerLockService
|
||||
Cfg *setting.Cfg
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
type ActiveAuthTokenService struct {
|
||||
cfg *setting.Cfg
|
||||
sqlStore db.DB
|
||||
}
|
||||
|
||||
func ProvideActiveAuthTokenService(cfg *setting.Cfg, sqlStore db.DB, quotaService quota.Service) (*ActiveAuthTokenService, error) {
|
||||
s := &ActiveAuthTokenService{
|
||||
cfg: cfg,
|
||||
sqlStore: sqlStore,
|
||||
}
|
||||
|
||||
defaultLimits, err := readQuotaConfig(cfg)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
if err := quotaService.RegisterQuotaReporter("a.NewUsageReporter{
|
||||
TargetSrv: auth.QuotaTargetSrv,
|
||||
DefaultLimits: defaultLimits,
|
||||
Reporter: s.ActiveTokenCount,
|
||||
}); err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (a *ActiveAuthTokenService) ActiveTokenCount(ctx context.Context, _ *quota.ScopeParameters) (*quota.Map, error) {
|
||||
var count int64
|
||||
var err error
|
||||
err = a.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
var model userAuthToken
|
||||
count, err = dbSession.Where(`created_at > ? AND rotated_at > ? AND revoked_at = 0`,
|
||||
getTime().Add(-a.cfg.LoginMaxLifetime).Unix(),
|
||||
getTime().Add(-a.cfg.LoginMaxInactiveLifetime).Unix()).
|
||||
Count(&model)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
tag, err := quota.NewTag(auth.QuotaTargetSrv, auth.QuotaTarget, quota.GlobalScope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u := "a.Map{}
|
||||
u.Set(tag, count)
|
||||
|
||||
return u, err
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) {
|
||||
token, err := util.RandomHex(16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hashedToken := hashToken(token)
|
||||
|
||||
now := getTime().Unix()
|
||||
clientIPStr := clientIP.String()
|
||||
if len(clientIP) == 0 {
|
||||
clientIPStr = ""
|
||||
}
|
||||
|
||||
userAuthToken := userAuthToken{
|
||||
UserId: user.ID,
|
||||
AuthToken: hashedToken,
|
||||
PrevAuthToken: hashedToken,
|
||||
ClientIp: clientIPStr,
|
||||
UserAgent: userAgent,
|
||||
RotatedAt: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
SeenAt: 0,
|
||||
RevokedAt: 0,
|
||||
AuthTokenSeen: false,
|
||||
}
|
||||
|
||||
err = s.SQLStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
_, err = dbSession.Insert(&userAuthToken)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userAuthToken.UnhashedToken = token
|
||||
|
||||
ctxLogger := s.log.FromContext(ctx)
|
||||
ctxLogger.Debug("user auth token created", "tokenId", userAuthToken.Id, "userId", userAuthToken.UserId, "clientIP", userAuthToken.ClientIp, "userAgent", userAuthToken.UserAgent, "authToken", userAuthToken.AuthToken)
|
||||
|
||||
var userToken auth.UserToken
|
||||
err = userAuthToken.toUserToken(&userToken)
|
||||
|
||||
return &userToken, err
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*auth.UserToken, error) {
|
||||
hashedToken := hashToken(unhashedToken)
|
||||
var model userAuthToken
|
||||
var exists bool
|
||||
var err error
|
||||
err = s.SQLStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
exists, err = dbSession.Where("(auth_token = ? OR prev_auth_token = ?)",
|
||||
hashedToken,
|
||||
hashedToken).
|
||||
Get(&model)
|
||||
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return nil, auth.ErrUserTokenNotFound
|
||||
}
|
||||
|
||||
ctxLogger := s.log.FromContext(ctx)
|
||||
|
||||
if model.RevokedAt > 0 {
|
||||
ctxLogger.Debug("user token has been revoked", "user ID", model.UserId, "token ID", model.Id)
|
||||
return nil, &auth.TokenRevokedError{
|
||||
UserID: model.UserId,
|
||||
TokenID: model.Id,
|
||||
}
|
||||
}
|
||||
|
||||
if model.CreatedAt <= s.createdAfterParam() || model.RotatedAt <= s.rotatedAfterParam() {
|
||||
ctxLogger.Debug("user token has expired", "user ID", model.UserId, "token ID", model.Id)
|
||||
return nil, &auth.TokenExpiredError{
|
||||
UserID: model.UserId,
|
||||
TokenID: model.Id,
|
||||
}
|
||||
}
|
||||
|
||||
if model.AuthToken != hashedToken && model.PrevAuthToken == hashedToken && model.AuthTokenSeen {
|
||||
modelCopy := model
|
||||
modelCopy.AuthTokenSeen = false
|
||||
expireBefore := getTime().Add(-urgentRotateTime).Unix()
|
||||
|
||||
var affectedRows int64
|
||||
err = s.SQLStore.WithTransactionalDbSession(ctx, func(dbSession *db.Session) error {
|
||||
affectedRows, err = dbSession.Where("id = ? AND prev_auth_token = ? AND rotated_at < ?",
|
||||
modelCopy.Id,
|
||||
modelCopy.PrevAuthToken,
|
||||
expireBefore).
|
||||
AllCols().Update(&modelCopy)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if affectedRows == 0 {
|
||||
ctxLogger.Debug("prev seen token unchanged", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
|
||||
} else {
|
||||
ctxLogger.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()
|
||||
|
||||
var affectedRows int64
|
||||
err = s.SQLStore.WithTransactionalDbSession(ctx, func(dbSession *db.Session) error {
|
||||
affectedRows, err = dbSession.Where("id = ? AND auth_token = ?",
|
||||
modelCopy.Id,
|
||||
modelCopy.AuthToken).
|
||||
AllCols().Update(&modelCopy)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if affectedRows == 1 {
|
||||
model = modelCopy
|
||||
}
|
||||
|
||||
if affectedRows == 0 {
|
||||
ctxLogger.Debug("seen wrong token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
|
||||
} else {
|
||||
ctxLogger.Debug("seen token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
|
||||
}
|
||||
}
|
||||
|
||||
model.UnhashedToken = unhashedToken
|
||||
|
||||
var userToken auth.UserToken
|
||||
err = model.toUserToken(&userToken)
|
||||
|
||||
return &userToken, err
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) TryRotateToken(ctx context.Context, token *auth.UserToken,
|
||||
clientIP net.IP, userAgent string) (bool, error) {
|
||||
if token == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
model, err := userAuthTokenFromUserToken(token)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
now := getTime()
|
||||
|
||||
var needsRotation bool
|
||||
rotatedAt := time.Unix(model.RotatedAt, 0)
|
||||
if model.AuthTokenSeen {
|
||||
needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.TokenRotationIntervalMinutes) * time.Minute))
|
||||
} else {
|
||||
needsRotation = rotatedAt.Before(now.Add(-urgentRotateTime))
|
||||
}
|
||||
|
||||
if !needsRotation {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ctxLogger := s.log.FromContext(ctx)
|
||||
ctxLogger.Debug("token needs rotation", "tokenId", model.Id, "authTokenSeen", model.AuthTokenSeen, "rotatedAt", rotatedAt)
|
||||
|
||||
clientIPStr := clientIP.String()
|
||||
if len(clientIP) == 0 {
|
||||
clientIPStr = ""
|
||||
}
|
||||
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 < ?)`
|
||||
|
||||
var affected int64
|
||||
err = s.SQLStore.WithTransactionalDbSession(ctx, func(dbSession *db.Session) error {
|
||||
res, err := dbSession.Exec(sql, userAgent, clientIPStr, s.SQLStore.GetDialect().BooleanStr(true), hashedToken,
|
||||
s.SQLStore.GetDialect().BooleanStr(false), now.Unix(), model.Id, s.SQLStore.GetDialect().BooleanStr(true),
|
||||
now.Add(-30*time.Second).Unix())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
affected, err = res.RowsAffected()
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
ctxLogger.Debug("auth token rotated", "affected", affected, "auth_token_id", model.Id, "userId", model.UserId)
|
||||
if affected > 0 {
|
||||
model.UnhashedToken = newToken
|
||||
if err := model.toUserToken(token); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *auth.UserToken, soft bool) error {
|
||||
if token == nil {
|
||||
return auth.ErrUserTokenNotFound
|
||||
}
|
||||
|
||||
model, err := userAuthTokenFromUserToken(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rowsAffected int64
|
||||
|
||||
if soft {
|
||||
model.RevokedAt = getTime().Unix()
|
||||
err = s.SQLStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
rowsAffected, err = dbSession.ID(model.Id).Update(model)
|
||||
return err
|
||||
})
|
||||
} else {
|
||||
err = s.SQLStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
rowsAffected, err = dbSession.Delete(model)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctxLogger := s.log.FromContext(ctx)
|
||||
|
||||
if rowsAffected == 0 {
|
||||
ctxLogger.Debug("user auth token not found/revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent)
|
||||
return auth.ErrUserTokenNotFound
|
||||
}
|
||||
|
||||
ctxLogger.Debug("user auth token revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "soft", soft)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) RevokeAllUserTokens(ctx context.Context, userId int64) error {
|
||||
return s.SQLStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
sql := `DELETE from user_auth_token WHERE user_id = ?`
|
||||
res, err := dbSession.Exec(sql, userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.log.FromContext(ctx).Debug("all user tokens for user revoked", "userId", userId, "count", affected)
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) BatchRevokeAllUserTokens(ctx context.Context, userIds []int64) error {
|
||||
return s.SQLStore.WithTransactionalDbSession(ctx, func(dbSession *db.Session) error {
|
||||
if len(userIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
user_id_params := strings.Repeat(",?", len(userIds)-1)
|
||||
sql := "DELETE from user_auth_token WHERE user_id IN (?" + user_id_params + ")"
|
||||
|
||||
params := []interface{}{sql}
|
||||
for _, v := range userIds {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
res, err := dbSession.Exec(params...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.log.FromContext(ctx).Debug("all user tokens for given users revoked", "usersCount", len(userIds), "count", affected)
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) GetUserToken(ctx context.Context, userId, userTokenId int64) (*auth.UserToken, error) {
|
||||
var result auth.UserToken
|
||||
err := s.SQLStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
var token userAuthToken
|
||||
exists, err := dbSession.Where("id = ? AND user_id = ?", userTokenId, userId).Get(&token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return auth.ErrUserTokenNotFound
|
||||
}
|
||||
|
||||
return token.toUserToken(&result)
|
||||
})
|
||||
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64) ([]*auth.UserToken, error) {
|
||||
result := []*auth.UserToken{}
|
||||
err := s.SQLStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
var tokens []*userAuthToken
|
||||
err := dbSession.Where("user_id = ? AND created_at > ? AND rotated_at > ? AND revoked_at = 0",
|
||||
userId,
|
||||
s.createdAfterParam(),
|
||||
s.rotatedAfterParam()).
|
||||
Find(&tokens)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, token := range tokens {
|
||||
var userToken auth.UserToken
|
||||
if err := token.toUserToken(&userToken); err != nil {
|
||||
return err
|
||||
}
|
||||
result = append(result, &userToken)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) GetUserRevokedTokens(ctx context.Context, userId int64) ([]*auth.UserToken, error) {
|
||||
result := []*auth.UserToken{}
|
||||
err := s.SQLStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
var tokens []*userAuthToken
|
||||
err := dbSession.Where("user_id = ? AND revoked_at > 0", userId).Find(&tokens)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, token := range tokens {
|
||||
var userToken auth.UserToken
|
||||
if err := token.toUserToken(&userToken); err != nil {
|
||||
return err
|
||||
}
|
||||
result = append(result, &userToken)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) createdAfterParam() int64 {
|
||||
return getTime().Add(-s.Cfg.LoginMaxLifetime).Unix()
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) rotatedAfterParam() int64 {
|
||||
return getTime().Add(-s.Cfg.LoginMaxInactiveLifetime).Unix()
|
||||
}
|
||||
|
||||
func hashToken(token string) string {
|
||||
hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
|
||||
return hex.EncodeToString(hashBytes[:])
|
||||
}
|
||||
|
||||
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
|
||||
limits := "a.Map{}
|
||||
|
||||
if cfg == nil {
|
||||
return limits, nil
|
||||
}
|
||||
|
||||
globalQuotaTag, err := quota.NewTag(auth.QuotaTargetSrv, auth.QuotaTarget, quota.GlobalScope)
|
||||
if err != nil {
|
||||
return limits, err
|
||||
}
|
||||
|
||||
limits.Set(globalQuotaTag, cfg.Quota.Global.Session)
|
||||
return limits, nil
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
package authimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestUserAuthToken(t *testing.T) {
|
||||
ctx := createTestContext(t)
|
||||
user := &user.User{ID: int64(10)}
|
||||
// userID := user.Id
|
||||
|
||||
now := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC)
|
||||
getTime = func() time.Time { return now }
|
||||
defer func() { getTime = time.Now }()
|
||||
|
||||
t.Run("When creating token", func(t *testing.T) {
|
||||
createToken := func() *auth.UserToken {
|
||||
userToken, err := ctx.tokenService.CreateToken(context.Background(), user,
|
||||
net.ParseIP("192.168.10.11"), "some user agent")
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, userToken)
|
||||
require.False(t, userToken.AuthTokenSeen)
|
||||
return userToken
|
||||
}
|
||||
|
||||
userToken := createToken()
|
||||
|
||||
t.Run("Can count active tokens", func(t *testing.T) {
|
||||
m, err := ctx.activeTokenService.ActiveTokenCount(context.Background(), "a.ScopeParameters{})
|
||||
require.Nil(t, err)
|
||||
tag, err := quota.NewTag(auth.QuotaTargetSrv, auth.QuotaTarget, quota.GlobalScope)
|
||||
require.NoError(t, err)
|
||||
count, ok := m.Get(tag)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, int64(1), count)
|
||||
})
|
||||
|
||||
t.Run("When lookup unhashed token should return user auth token", func(t *testing.T) {
|
||||
userToken, err := ctx.tokenService.LookupToken(context.Background(), userToken.UnhashedToken)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, userToken)
|
||||
require.Equal(t, user.ID, userToken.UserId)
|
||||
require.True(t, userToken.AuthTokenSeen)
|
||||
|
||||
storedAuthToken, err := ctx.getAuthTokenByID(userToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, storedAuthToken)
|
||||
require.True(t, storedAuthToken.AuthTokenSeen)
|
||||
})
|
||||
|
||||
t.Run("When lookup hashed token should return user auth token not found error", func(t *testing.T) {
|
||||
userToken, err := ctx.tokenService.LookupToken(context.Background(), userToken.AuthToken)
|
||||
require.Equal(t, auth.ErrUserTokenNotFound, err)
|
||||
require.Nil(t, userToken)
|
||||
})
|
||||
|
||||
t.Run("soft revoking existing token should not delete it", func(t *testing.T) {
|
||||
err := ctx.tokenService.RevokeToken(context.Background(), userToken, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
model, err := ctx.getAuthTokenByID(userToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, model)
|
||||
require.Greater(t, model.RevokedAt, int64(0))
|
||||
})
|
||||
|
||||
t.Run("revoking existing token should delete it", func(t *testing.T) {
|
||||
err := ctx.tokenService.RevokeToken(context.Background(), userToken, false)
|
||||
require.Nil(t, err)
|
||||
|
||||
model, err := ctx.getAuthTokenByID(userToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.Nil(t, model)
|
||||
})
|
||||
|
||||
t.Run("revoking nil token should return error", func(t *testing.T) {
|
||||
err := ctx.tokenService.RevokeToken(context.Background(), nil, false)
|
||||
require.Equal(t, auth.ErrUserTokenNotFound, err)
|
||||
})
|
||||
|
||||
t.Run("revoking non-existing token should return error", func(t *testing.T) {
|
||||
userToken.Id = 1000
|
||||
err := ctx.tokenService.RevokeToken(context.Background(), userToken, false)
|
||||
require.Equal(t, auth.ErrUserTokenNotFound, err)
|
||||
})
|
||||
|
||||
ctx = createTestContext(t)
|
||||
userToken = createToken()
|
||||
|
||||
t.Run("When creating an additional token", func(t *testing.T) {
|
||||
userToken2, err := ctx.tokenService.CreateToken(context.Background(), user,
|
||||
net.ParseIP("192.168.10.11"), "some user agent")
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, userToken2)
|
||||
|
||||
t.Run("Can get first user token", func(t *testing.T) {
|
||||
token, err := ctx.tokenService.GetUserToken(context.Background(), user.ID, userToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, token)
|
||||
require.Equal(t, userToken.Id, token.Id)
|
||||
})
|
||||
|
||||
t.Run("Can get second user token", func(t *testing.T) {
|
||||
token, err := ctx.tokenService.GetUserToken(context.Background(), user.ID, userToken2.Id)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, token)
|
||||
require.Equal(t, userToken2.Id, token.Id)
|
||||
})
|
||||
|
||||
t.Run("Can get user tokens", func(t *testing.T) {
|
||||
tokens, err := ctx.tokenService.GetUserTokens(context.Background(), user.ID)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 2, len(tokens))
|
||||
require.Equal(t, userToken.Id, tokens[0].Id)
|
||||
require.Equal(t, userToken2.Id, tokens[1].Id)
|
||||
})
|
||||
|
||||
t.Run("Can revoke all user tokens", func(t *testing.T) {
|
||||
err := ctx.tokenService.RevokeAllUserTokens(context.Background(), user.ID)
|
||||
require.Nil(t, err)
|
||||
|
||||
model, err := ctx.getAuthTokenByID(userToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.Nil(t, model)
|
||||
|
||||
model2, err := ctx.getAuthTokenByID(userToken2.Id)
|
||||
require.Nil(t, err)
|
||||
require.Nil(t, model2)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("When revoking users tokens in a batch", func(t *testing.T) {
|
||||
t.Run("Can revoke all users tokens", func(t *testing.T) {
|
||||
userIds := []int64{}
|
||||
for i := 0; i < 3; i++ {
|
||||
userId := user.ID + int64(i+1)
|
||||
userIds = append(userIds, userId)
|
||||
_, err := ctx.tokenService.CreateToken(context.Background(), user,
|
||||
net.ParseIP("192.168.10.11"), "some user agent")
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
err := ctx.tokenService.BatchRevokeAllUserTokens(context.Background(), userIds)
|
||||
require.Nil(t, err)
|
||||
|
||||
for _, v := range userIds {
|
||||
tokens, err := ctx.tokenService.GetUserTokens(context.Background(), v)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, len(tokens))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("expires correctly", func(t *testing.T) {
|
||||
ctx := createTestContext(t)
|
||||
userToken, err := ctx.tokenService.CreateToken(context.Background(), user,
|
||||
net.ParseIP("192.168.10.11"), "some user agent")
|
||||
require.Nil(t, err)
|
||||
|
||||
userToken, err = ctx.tokenService.LookupToken(context.Background(), userToken.UnhashedToken)
|
||||
require.Nil(t, err)
|
||||
|
||||
getTime = func() time.Time { return now.Add(time.Hour) }
|
||||
|
||||
rotated, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("192.168.10.11"), "some user agent")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
|
||||
userToken, err = ctx.tokenService.LookupToken(context.Background(), userToken.UnhashedToken)
|
||||
require.Nil(t, err)
|
||||
|
||||
stillGood, err := ctx.tokenService.LookupToken(context.Background(), userToken.UnhashedToken)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, stillGood)
|
||||
|
||||
model, err := ctx.getAuthTokenByID(userToken.Id)
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("when rotated_at is 6:59:59 ago should find token", func(t *testing.T) {
|
||||
getTime = func() time.Time {
|
||||
return time.Unix(model.RotatedAt, 0).Add(24 * 7 * time.Hour).Add(-time.Second)
|
||||
}
|
||||
|
||||
stillGood, err = ctx.tokenService.LookupToken(context.Background(), stillGood.UnhashedToken)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, stillGood)
|
||||
})
|
||||
|
||||
t.Run("when rotated_at is 7:00:00 ago should return token expired error", func(t *testing.T) {
|
||||
getTime = func() time.Time {
|
||||
return time.Unix(model.RotatedAt, 0).Add(24 * 7 * time.Hour)
|
||||
}
|
||||
|
||||
notGood, err := ctx.tokenService.LookupToken(context.Background(), userToken.UnhashedToken)
|
||||
require.Equal(t, reflect.TypeOf(err), reflect.TypeOf(&auth.TokenExpiredError{}))
|
||||
require.Nil(t, notGood)
|
||||
|
||||
t.Run("should not find active token when expired", func(t *testing.T) {
|
||||
m, err := ctx.activeTokenService.ActiveTokenCount(context.Background(), "a.ScopeParameters{})
|
||||
require.Nil(t, err)
|
||||
tag, err := quota.NewTag(auth.QuotaTargetSrv, auth.QuotaTarget, quota.GlobalScope)
|
||||
require.NoError(t, err)
|
||||
count, ok := m.Get(tag)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, int64(0), count)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("when rotated_at is 5 days ago and created_at is 29 days and 23:59:59 ago should not find token", func(t *testing.T) {
|
||||
updated, err := ctx.updateRotatedAt(model.Id, time.Unix(model.CreatedAt, 0).Add(24*25*time.Hour).Unix())
|
||||
require.Nil(t, err)
|
||||
require.True(t, updated)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return time.Unix(model.CreatedAt, 0).Add(24 * 30 * time.Hour).Add(-time.Second)
|
||||
}
|
||||
|
||||
stillGood, err = ctx.tokenService.LookupToken(context.Background(), stillGood.UnhashedToken)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, stillGood)
|
||||
})
|
||||
|
||||
t.Run("when rotated_at is 5 days ago and created_at is 30 days ago should return token expired error", func(t *testing.T) {
|
||||
updated, err := ctx.updateRotatedAt(model.Id, time.Unix(model.CreatedAt, 0).Add(24*25*time.Hour).Unix())
|
||||
require.Nil(t, err)
|
||||
require.True(t, updated)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return time.Unix(model.CreatedAt, 0).Add(24 * 30 * time.Hour)
|
||||
}
|
||||
|
||||
notGood, err := ctx.tokenService.LookupToken(context.Background(), userToken.UnhashedToken)
|
||||
require.Equal(t, reflect.TypeOf(err), reflect.TypeOf(&auth.TokenExpiredError{}))
|
||||
require.Nil(t, notGood)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("can properly rotate tokens", func(t *testing.T) {
|
||||
getTime = func() time.Time { return now }
|
||||
ctx := createTestContext(t)
|
||||
userToken, err := ctx.tokenService.CreateToken(context.Background(), user,
|
||||
net.ParseIP("192.168.10.11"), "some user agent")
|
||||
require.Nil(t, err)
|
||||
|
||||
prevToken := userToken.AuthToken
|
||||
unhashedPrev := userToken.UnhashedToken
|
||||
|
||||
rotated, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("192.168.10.12"), "a new user agent")
|
||||
require.Nil(t, err)
|
||||
require.False(t, rotated)
|
||||
|
||||
updated, err := ctx.markAuthTokenAsSeen(userToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.True(t, updated)
|
||||
|
||||
model, err := ctx.getAuthTokenByID(userToken.Id)
|
||||
require.Nil(t, err)
|
||||
|
||||
var tok auth.UserToken
|
||||
err = model.toUserToken(&tok)
|
||||
require.Nil(t, err)
|
||||
|
||||
getTime = func() time.Time { return now.Add(time.Hour) }
|
||||
|
||||
rotated, err = ctx.tokenService.TryRotateToken(context.Background(), &tok,
|
||||
net.ParseIP("192.168.10.12"), "a new user agent")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
|
||||
unhashedToken := tok.UnhashedToken
|
||||
|
||||
model, err = ctx.getAuthTokenByID(tok.Id)
|
||||
require.Nil(t, err)
|
||||
model.UnhashedToken = unhashedToken
|
||||
|
||||
require.Equal(t, getTime().Unix(), model.RotatedAt)
|
||||
require.Equal(t, "192.168.10.12", model.ClientIp)
|
||||
require.Equal(t, "a new user agent", model.UserAgent)
|
||||
require.False(t, model.AuthTokenSeen)
|
||||
require.Equal(t, int64(0), model.SeenAt)
|
||||
require.Equal(t, prevToken, model.PrevAuthToken)
|
||||
|
||||
// ability to auth using an old token
|
||||
|
||||
lookedUpUserToken, err := ctx.tokenService.LookupToken(context.Background(), model.UnhashedToken)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, lookedUpUserToken)
|
||||
require.True(t, lookedUpUserToken.AuthTokenSeen)
|
||||
require.Equal(t, getTime().Unix(), lookedUpUserToken.SeenAt)
|
||||
|
||||
lookedUpUserToken, err = ctx.tokenService.LookupToken(context.Background(), unhashedPrev)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, lookedUpUserToken)
|
||||
require.Equal(t, model.Id, lookedUpUserToken.Id)
|
||||
require.True(t, lookedUpUserToken.AuthTokenSeen)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return now.Add(time.Hour + (2 * time.Minute))
|
||||
}
|
||||
|
||||
lookedUpUserToken, err = ctx.tokenService.LookupToken(context.Background(), unhashedPrev)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, lookedUpUserToken)
|
||||
require.True(t, lookedUpUserToken.AuthTokenSeen)
|
||||
|
||||
lookedUpModel, err := ctx.getAuthTokenByID(lookedUpUserToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, lookedUpModel)
|
||||
require.False(t, lookedUpModel.AuthTokenSeen)
|
||||
|
||||
rotated, err = ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("192.168.10.12"), "a new user agent")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
|
||||
model, err = ctx.getAuthTokenByID(userToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, model)
|
||||
require.Equal(t, int64(0), model.SeenAt)
|
||||
})
|
||||
|
||||
t.Run("keeps prev token valid for 1 minute after it is confirmed", func(t *testing.T) {
|
||||
getTime = func() time.Time { return now }
|
||||
userToken, err := ctx.tokenService.CreateToken(context.Background(), user,
|
||||
net.ParseIP("192.168.10.11"), "some user agent")
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, userToken)
|
||||
|
||||
lookedUpUserToken, err := ctx.tokenService.LookupToken(context.Background(), userToken.UnhashedToken)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, lookedUpUserToken)
|
||||
|
||||
getTime = func() time.Time { return now.Add(10 * time.Minute) }
|
||||
|
||||
prevToken := userToken.UnhashedToken
|
||||
rotated, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("1.1.1.1"), "firefox")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return now.Add(20 * time.Minute)
|
||||
}
|
||||
|
||||
currentUserToken, err := ctx.tokenService.LookupToken(context.Background(), userToken.UnhashedToken)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, currentUserToken)
|
||||
|
||||
prevUserToken, err := ctx.tokenService.LookupToken(context.Background(), prevToken)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, prevUserToken)
|
||||
})
|
||||
|
||||
t.Run("will not mark token unseen when prev and current are the same", func(t *testing.T) {
|
||||
userToken, err := ctx.tokenService.CreateToken(context.Background(), user,
|
||||
net.ParseIP("192.168.10.11"), "some user agent")
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, userToken)
|
||||
|
||||
lookedUpUserToken, err := ctx.tokenService.LookupToken(context.Background(), userToken.UnhashedToken)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, lookedUpUserToken)
|
||||
|
||||
lookedUpUserToken, err = ctx.tokenService.LookupToken(context.Background(), userToken.UnhashedToken)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, lookedUpUserToken)
|
||||
|
||||
lookedUpModel, err := ctx.getAuthTokenByID(lookedUpUserToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, lookedUpModel)
|
||||
require.True(t, lookedUpModel.AuthTokenSeen)
|
||||
})
|
||||
|
||||
t.Run("Rotate token", func(t *testing.T) {
|
||||
t.Run("Should rotate current token and previous token when auth token seen", func(t *testing.T) {
|
||||
getTime = func() time.Time { return now }
|
||||
userToken, err := ctx.tokenService.CreateToken(context.Background(), user,
|
||||
net.ParseIP("192.168.10.11"), "some user agent")
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, userToken)
|
||||
|
||||
prevToken := userToken.AuthToken
|
||||
|
||||
updated, err := ctx.markAuthTokenAsSeen(userToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.True(t, updated)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return now.Add(10 * time.Minute)
|
||||
}
|
||||
|
||||
rotated, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("1.1.1.1"), "firefox")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
|
||||
storedToken, err := ctx.getAuthTokenByID(userToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, storedToken)
|
||||
require.False(t, storedToken.AuthTokenSeen)
|
||||
require.Equal(t, prevToken, storedToken.PrevAuthToken)
|
||||
require.NotEqual(t, prevToken, storedToken.AuthToken)
|
||||
|
||||
prevToken = storedToken.AuthToken
|
||||
|
||||
updated, err = ctx.markAuthTokenAsSeen(userToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.True(t, updated)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return now.Add(20 * time.Minute)
|
||||
}
|
||||
|
||||
rotated, err = ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("1.1.1.1"), "firefox")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
|
||||
storedToken, err = ctx.getAuthTokenByID(userToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, storedToken)
|
||||
require.False(t, storedToken.AuthTokenSeen)
|
||||
require.Equal(t, prevToken, storedToken.PrevAuthToken)
|
||||
require.NotEqual(t, prevToken, storedToken.AuthToken)
|
||||
})
|
||||
|
||||
t.Run("Should rotate current token, but keep previous token when auth token not seen", func(t *testing.T) {
|
||||
getTime = func() time.Time { return now }
|
||||
userToken, err := ctx.tokenService.CreateToken(context.Background(), user,
|
||||
net.ParseIP("192.168.10.11"), "some user agent")
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, userToken)
|
||||
|
||||
prevToken := userToken.AuthToken
|
||||
userToken.RotatedAt = now.Add(-2 * time.Minute).Unix()
|
||||
|
||||
getTime = func() time.Time {
|
||||
return now.Add(2 * time.Minute)
|
||||
}
|
||||
|
||||
rotated, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("1.1.1.1"), "firefox")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
|
||||
storedToken, err := ctx.getAuthTokenByID(userToken.Id)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, storedToken)
|
||||
require.False(t, storedToken.AuthTokenSeen)
|
||||
require.Equal(t, prevToken, storedToken.PrevAuthToken)
|
||||
require.NotEqual(t, prevToken, storedToken.AuthToken)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("When populating userAuthToken from UserToken should copy all properties", func(t *testing.T) {
|
||||
ut := auth.UserToken{
|
||||
Id: 1,
|
||||
UserId: 2,
|
||||
AuthToken: "a",
|
||||
PrevAuthToken: "b",
|
||||
UserAgent: "c",
|
||||
ClientIp: "d",
|
||||
AuthTokenSeen: true,
|
||||
SeenAt: 3,
|
||||
RotatedAt: 4,
|
||||
CreatedAt: 5,
|
||||
UpdatedAt: 6,
|
||||
UnhashedToken: "e",
|
||||
}
|
||||
utBytes, err := json.Marshal(ut)
|
||||
require.Nil(t, err)
|
||||
utJSON, err := simplejson.NewJson(utBytes)
|
||||
require.Nil(t, err)
|
||||
utMap := utJSON.MustMap()
|
||||
|
||||
var uat userAuthToken
|
||||
err = uat.fromUserToken(&ut)
|
||||
require.Nil(t, err)
|
||||
uatBytes, err := json.Marshal(uat)
|
||||
require.Nil(t, err)
|
||||
uatJSON, err := simplejson.NewJson(uatBytes)
|
||||
require.Nil(t, err)
|
||||
uatMap := uatJSON.MustMap()
|
||||
|
||||
require.True(t, reflect.DeepEqual(uatMap, utMap))
|
||||
})
|
||||
|
||||
t.Run("When populating userToken from userAuthToken should copy all properties", func(t *testing.T) {
|
||||
uat := userAuthToken{
|
||||
Id: 1,
|
||||
UserId: 2,
|
||||
AuthToken: "a",
|
||||
PrevAuthToken: "b",
|
||||
UserAgent: "c",
|
||||
ClientIp: "d",
|
||||
AuthTokenSeen: true,
|
||||
SeenAt: 3,
|
||||
RotatedAt: 4,
|
||||
CreatedAt: 5,
|
||||
UpdatedAt: 6,
|
||||
UnhashedToken: "e",
|
||||
}
|
||||
uatBytes, err := json.Marshal(uat)
|
||||
require.Nil(t, err)
|
||||
uatJSON, err := simplejson.NewJson(uatBytes)
|
||||
require.Nil(t, err)
|
||||
uatMap := uatJSON.MustMap()
|
||||
|
||||
var ut auth.UserToken
|
||||
err = uat.toUserToken(&ut)
|
||||
require.Nil(t, err)
|
||||
utBytes, err := json.Marshal(ut)
|
||||
require.Nil(t, err)
|
||||
utJSON, err := simplejson.NewJson(utBytes)
|
||||
require.Nil(t, err)
|
||||
utMap := utJSON.MustMap()
|
||||
|
||||
require.True(t, reflect.DeepEqual(utMap, uatMap))
|
||||
})
|
||||
}
|
||||
|
||||
func createTestContext(t *testing.T) *testContext {
|
||||
t.Helper()
|
||||
maxInactiveDurationVal, _ := time.ParseDuration("168h")
|
||||
maxLifetimeDurationVal, _ := time.ParseDuration("720h")
|
||||
sqlstore := db.InitTestDB(t)
|
||||
|
||||
cfg := &setting.Cfg{
|
||||
LoginMaxInactiveLifetime: maxInactiveDurationVal,
|
||||
LoginMaxLifetime: maxLifetimeDurationVal,
|
||||
TokenRotationIntervalMinutes: 10,
|
||||
}
|
||||
|
||||
tokenService := &UserAuthTokenService{
|
||||
SQLStore: sqlstore,
|
||||
Cfg: cfg,
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
activeTokenService := &ActiveAuthTokenService{
|
||||
cfg: cfg,
|
||||
sqlStore: sqlstore,
|
||||
}
|
||||
|
||||
return &testContext{
|
||||
sqlstore: sqlstore,
|
||||
tokenService: tokenService,
|
||||
activeTokenService: activeTokenService,
|
||||
}
|
||||
}
|
||||
|
||||
type testContext struct {
|
||||
sqlstore db.DB
|
||||
tokenService *UserAuthTokenService
|
||||
activeTokenService *ActiveAuthTokenService
|
||||
}
|
||||
|
||||
func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) {
|
||||
var res *userAuthToken
|
||||
err := c.sqlstore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||
var t userAuthToken
|
||||
found, err := sess.ID(id).Get(&t)
|
||||
if err != nil || !found {
|
||||
return err
|
||||
}
|
||||
|
||||
res = &t
|
||||
return nil
|
||||
})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) {
|
||||
hasRowsAffected := false
|
||||
err := c.sqlstore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||
res, err := sess.Exec("UPDATE user_auth_token SET auth_token_seen = ? WHERE id = ?", c.sqlstore.GetDialect().BooleanStr(true), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hasRowsAffected = rowsAffected == 1
|
||||
return nil
|
||||
})
|
||||
return hasRowsAffected, err
|
||||
}
|
||||
|
||||
func (c *testContext) updateRotatedAt(id, rotatedAt int64) (bool, error) {
|
||||
hasRowsAffected := false
|
||||
err := c.sqlstore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||
res, err := sess.Exec("UPDATE user_auth_token SET rotated_at = ? WHERE id = ?", rotatedAt, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasRowsAffected = rowsAffected == 1
|
||||
return nil
|
||||
})
|
||||
return hasRowsAffected, err
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package authimpl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
)
|
||||
|
||||
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
|
||||
RevokedAt int64
|
||||
UnhashedToken string `xorm:"-"`
|
||||
}
|
||||
|
||||
func userAuthTokenFromUserToken(ut *auth.UserToken) (*userAuthToken, error) {
|
||||
var uat userAuthToken
|
||||
err := uat.fromUserToken(ut)
|
||||
return &uat, err
|
||||
}
|
||||
|
||||
func (uat *userAuthToken) fromUserToken(ut *auth.UserToken) error {
|
||||
if uat == nil {
|
||||
return fmt.Errorf("needs pointer to userAuthToken struct")
|
||||
}
|
||||
|
||||
uat.Id = ut.Id
|
||||
uat.UserId = ut.UserId
|
||||
uat.AuthToken = ut.AuthToken
|
||||
uat.PrevAuthToken = ut.PrevAuthToken
|
||||
uat.UserAgent = ut.UserAgent
|
||||
uat.ClientIp = ut.ClientIp
|
||||
uat.AuthTokenSeen = ut.AuthTokenSeen
|
||||
uat.SeenAt = ut.SeenAt
|
||||
uat.RotatedAt = ut.RotatedAt
|
||||
uat.CreatedAt = ut.CreatedAt
|
||||
uat.UpdatedAt = ut.UpdatedAt
|
||||
uat.RevokedAt = ut.RevokedAt
|
||||
uat.UnhashedToken = ut.UnhashedToken
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uat *userAuthToken) toUserToken(ut *auth.UserToken) error {
|
||||
if uat == nil {
|
||||
return fmt.Errorf("needs pointer to userAuthToken struct")
|
||||
}
|
||||
|
||||
ut.Id = uat.Id
|
||||
ut.UserId = uat.UserId
|
||||
ut.AuthToken = uat.AuthToken
|
||||
ut.PrevAuthToken = uat.PrevAuthToken
|
||||
ut.UserAgent = uat.UserAgent
|
||||
ut.ClientIp = uat.ClientIp
|
||||
ut.AuthTokenSeen = uat.AuthTokenSeen
|
||||
ut.SeenAt = uat.SeenAt
|
||||
ut.RotatedAt = uat.RotatedAt
|
||||
ut.CreatedAt = uat.CreatedAt
|
||||
ut.UpdatedAt = uat.UpdatedAt
|
||||
ut.RevokedAt = uat.RevokedAt
|
||||
ut.UnhashedToken = uat.UnhashedToken
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package authimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
)
|
||||
|
||||
func (s *UserAuthTokenService) Run(ctx context.Context) error {
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
maxInactiveLifetime := s.Cfg.LoginMaxInactiveLifetime
|
||||
maxLifetime := s.Cfg.LoginMaxLifetime
|
||||
|
||||
err := s.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func(context.Context) {
|
||||
if _, err := s.deleteExpiredTokens(ctx, maxInactiveLifetime, maxLifetime); err != nil {
|
||||
s.log.Error("An error occurred while deleting expired tokens", "err", err)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
s.log.Error("failed to lock and execute cleanup of expired auth token", "error", err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
err = s.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func(context.Context) {
|
||||
if _, err := s.deleteExpiredTokens(ctx, maxInactiveLifetime, maxLifetime); err != nil {
|
||||
s.log.Error("An error occurred while deleting expired tokens", "err", err)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
s.log.Error("failed to lock and execute cleanup of expired auth token", "error", err)
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) deleteExpiredTokens(ctx context.Context, maxInactiveLifetime, maxLifetime time.Duration) (int64, error) {
|
||||
createdBefore := getTime().Add(-maxLifetime)
|
||||
rotatedBefore := getTime().Add(-maxInactiveLifetime)
|
||||
|
||||
s.log.Debug("starting cleanup of expired auth tokens", "createdBefore", createdBefore, "rotatedBefore", rotatedBefore)
|
||||
|
||||
var affected int64
|
||||
err := s.SQLStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
sql := `DELETE from user_auth_token WHERE created_at <= ? OR rotated_at <= ?`
|
||||
res, err := dbSession.Exec(sql, createdBefore.Unix(), rotatedBefore.Unix())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
affected, err = res.RowsAffected()
|
||||
if err != nil {
|
||||
s.log.Error("failed to cleanup expired auth tokens", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
s.log.Debug("cleanup of expired auth tokens done", "count", affected)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return affected, err
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package authimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
)
|
||||
|
||||
func TestUserAuthTokenCleanup(t *testing.T) {
|
||||
setup := func() *testContext {
|
||||
ctx := createTestContext(t)
|
||||
maxInactiveLifetime, _ := time.ParseDuration("168h")
|
||||
maxLifetime, _ := time.ParseDuration("720h")
|
||||
ctx.tokenService.Cfg.LoginMaxInactiveLifetime = maxInactiveLifetime
|
||||
ctx.tokenService.Cfg.LoginMaxLifetime = maxLifetime
|
||||
return ctx
|
||||
}
|
||||
|
||||
insertToken := func(ctx *testContext, token string, prev string, createdAt, rotatedAt int64) {
|
||||
ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, CreatedAt: createdAt, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""}
|
||||
err := ctx.sqlstore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||
_, err := sess.Insert(&ut)
|
||||
require.Nil(t, err)
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
now := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC)
|
||||
getTime = func() time.Time { return now }
|
||||
|
||||
t.Run("should delete tokens where token rotation age is older than or equal 7 days", func(t *testing.T) {
|
||||
ctx := setup()
|
||||
from := now.Add(-168 * time.Hour)
|
||||
|
||||
// insert three old tokens that should be deleted
|
||||
for i := 0; i < 3; i++ {
|
||||
insertToken(ctx, fmt.Sprintf("oldA%d", i), fmt.Sprintf("oldB%d", i), from.Unix(), from.Unix())
|
||||
}
|
||||
|
||||
// insert three active tokens that should not be deleted
|
||||
for i := 0; i < 3; i++ {
|
||||
from = from.Add(time.Second)
|
||||
insertToken(ctx, fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), from.Unix(), from.Unix())
|
||||
}
|
||||
|
||||
affected, err := ctx.tokenService.deleteExpiredTokens(context.Background(), 168*time.Hour, 30*24*time.Hour)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, int64(3), affected)
|
||||
})
|
||||
|
||||
t.Run("should delete tokens where token age is older than or equal 30 days", func(t *testing.T) {
|
||||
ctx := setup()
|
||||
from := now.Add(-30 * 24 * time.Hour)
|
||||
fromRotate := now.Add(-time.Second)
|
||||
|
||||
// insert three old tokens that should be deleted
|
||||
for i := 0; i < 3; i++ {
|
||||
insertToken(ctx, fmt.Sprintf("oldA%d", i), fmt.Sprintf("oldB%d", i), from.Unix(), fromRotate.Unix())
|
||||
}
|
||||
|
||||
// insert three active tokens that should not be deleted
|
||||
for i := 0; i < 3; i++ {
|
||||
from = from.Add(time.Second)
|
||||
insertToken(ctx, fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), from.Unix(), fromRotate.Unix())
|
||||
}
|
||||
|
||||
affected, err := ctx.tokenService.deleteExpiredTokens(context.Background(), 7*24*time.Hour, 30*24*time.Hour)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, int64(3), affected)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user