[v7.5.x] Auth: Allow soft token revocation (#32037)

* Auth: Allow soft token revocation (#31601)

* Add revoked_at field to user auth token to allow soft revokes

* Allow soft token revocations

* Update token revocations and tests

* Return error info on revokedTokenErr

* Override session cookie only when no revokedErr nor API request

* Display modal on revoked token error

* Feedback: Refactor TokenRevokedModal to FC

* Add GetUserRevokedTokens into UserTokenService

* Backendsrv: adds tests and refactors soft token path

* Apply feedback

* Write redirect cookie on token revoked error

* Update TokenRevokedModal style

* Return meaningful error info

* Some UI changes

* Update backend_srv tests

* Minor style fix on backend_srv tests

* Replace deprecated method usage to publish events

* Fix backend_srv tests

* Apply suggestions from code review

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>

* Apply suggestions from code review

* Apply suggestions from code review

Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>

* Minor style fix after PR suggestion commit

* Apply suggestions from code review

Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com>

* Prettier fixes

Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>
Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com>
(cherry picked from commit 610999cfa2)

* Back to the old method to emit app events

Co-authored-by: Joan López de la Franca Beltran <joanjan14@gmail.com>
This commit is contained in:
Grot (@grafanabot)
2021-03-17 10:18:42 +01:00
committed by GitHub
co-authored by Joan López de la Franca Beltran
parent f570fb2d6f
commit 30b91296ad
13 changed files with 279 additions and 31 deletions
+48 -8
View File
@@ -49,7 +49,7 @@ func (s *UserAuthTokenService) ActiveTokenCount(ctx context.Context) (int64, err
var err error
err = s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
var model userAuthToken
count, err = dbSession.Where(`created_at > ? AND rotated_at > ?`,
count, err = dbSession.Where(`created_at > ? AND rotated_at > ? AND revoked_at = 0`,
s.createdAfterParam(),
s.rotatedAfterParam()).
Count(&model)
@@ -84,6 +84,7 @@ func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *models.Use
CreatedAt: now,
UpdatedAt: now,
SeenAt: 0,
RevokedAt: 0,
AuthTokenSeen: false,
}
@@ -127,6 +128,13 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st
return nil, models.ErrUserTokenNotFound
}
if model.RevokedAt > 0 {
return nil, &models.TokenRevokedError{
UserID: model.UserId,
TokenID: model.Id,
}
}
if model.CreatedAt <= s.createdAfterParam() || model.RotatedAt <= s.rotatedAfterParam() {
return nil, &models.TokenExpiredError{
UserID: model.UserId,
@@ -278,7 +286,7 @@ func (s *UserAuthTokenService) TryRotateToken(ctx context.Context, token *models
return false, nil
}
func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *models.UserToken) error {
func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *models.UserToken, soft bool) error {
if token == nil {
return models.ErrUserTokenNotFound
}
@@ -289,10 +297,19 @@ func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *models.Us
}
var rowsAffected int64
err = s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
rowsAffected, err = dbSession.Delete(model)
return err
})
if soft {
model.RevokedAt = getTime().Unix()
err = s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
rowsAffected, err = dbSession.ID(model.Id).Update(model)
return err
})
} else {
err = s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
rowsAffected, err = dbSession.Delete(model)
return err
})
}
if err != nil {
return err
@@ -303,7 +320,7 @@ func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *models.Us
return models.ErrUserTokenNotFound
}
s.log.Debug("user auth token revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent)
s.log.Debug("user auth token revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "soft", soft)
return nil
}
@@ -380,7 +397,7 @@ func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64)
result := []*models.UserToken{}
err := s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
var tokens []*userAuthToken
err := dbSession.Where("user_id = ? AND created_at > ? AND rotated_at > ?",
err := dbSession.Where("user_id = ? AND created_at > ? AND rotated_at > ? AND revoked_at = 0",
userId,
s.createdAfterParam(),
s.rotatedAfterParam()).
@@ -403,6 +420,29 @@ func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64)
return result, err
}
func (s *UserAuthTokenService) GetUserRevokedTokens(ctx context.Context, userId int64) ([]*models.UserToken, error) {
result := []*models.UserToken{}
err := s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) 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 models.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()
}
+14 -4
View File
@@ -60,8 +60,18 @@ func TestUserAuthToken(t *testing.T) {
So(userToken, ShouldBeNil)
})
Convey("revoking existing token should delete token", func() {
err = userAuthTokenService.RevokeToken(context.Background(), userToken)
Convey("soft revoking existing token should not delete it", func() {
err = userAuthTokenService.RevokeToken(context.Background(), userToken, true)
So(err, ShouldBeNil)
model, err := ctx.getAuthTokenByID(userToken.Id)
So(err, ShouldBeNil)
So(model, ShouldNotBeNil)
So(model.RevokedAt, ShouldBeGreaterThan, 0)
})
Convey("revoking existing token should delete it", func() {
err = userAuthTokenService.RevokeToken(context.Background(), userToken, false)
So(err, ShouldBeNil)
model, err := ctx.getAuthTokenByID(userToken.Id)
@@ -70,13 +80,13 @@ func TestUserAuthToken(t *testing.T) {
})
Convey("revoking nil token should return error", func() {
err = userAuthTokenService.RevokeToken(context.Background(), nil)
err = userAuthTokenService.RevokeToken(context.Background(), nil, false)
So(err, ShouldEqual, models.ErrUserTokenNotFound)
})
Convey("revoking non-existing token should return error", func() {
userToken.Id = 1000
err = userAuthTokenService.RevokeToken(context.Background(), userToken)
err = userAuthTokenService.RevokeToken(context.Background(), userToken, false)
So(err, ShouldEqual, models.ErrUserTokenNotFound)
})
+3
View File
@@ -18,6 +18,7 @@ type userAuthToken struct {
RotatedAt int64
CreatedAt int64
UpdatedAt int64
RevokedAt int64
UnhashedToken string `xorm:"-"`
}
@@ -43,6 +44,7 @@ func (uat *userAuthToken) fromUserToken(ut *models.UserToken) error {
uat.RotatedAt = ut.RotatedAt
uat.CreatedAt = ut.CreatedAt
uat.UpdatedAt = ut.UpdatedAt
uat.RevokedAt = ut.RevokedAt
uat.UnhashedToken = ut.UnhashedToken
return nil
@@ -64,6 +66,7 @@ func (uat *userAuthToken) toUserToken(ut *models.UserToken) error {
ut.RotatedAt = uat.RotatedAt
ut.CreatedAt = uat.CreatedAt
ut.UpdatedAt = uat.UpdatedAt
ut.RevokedAt = uat.RevokedAt
ut.UnhashedToken = uat.UnhashedToken
return nil
+17 -12
View File
@@ -8,15 +8,16 @@ import (
)
type FakeUserAuthTokenService struct {
CreateTokenProvider func(ctx context.Context, user *models.User, clientIP net.IP, userAgent string) (*models.UserToken, error)
TryRotateTokenProvider func(ctx context.Context, token *models.UserToken, clientIP net.IP, userAgent string) (bool, error)
LookupTokenProvider func(ctx context.Context, unhashedToken string) (*models.UserToken, error)
RevokeTokenProvider func(ctx context.Context, token *models.UserToken) error
RevokeAllUserTokensProvider func(ctx context.Context, userId int64) error
ActiveAuthTokenCount func(ctx context.Context) (int64, error)
GetUserTokenProvider func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error)
GetUserTokensProvider func(ctx context.Context, userId int64) ([]*models.UserToken, error)
BatchRevokedTokenProvider func(ctx context.Context, userIds []int64) error
CreateTokenProvider func(ctx context.Context, user *models.User, clientIP net.IP, userAgent string) (*models.UserToken, error)
TryRotateTokenProvider func(ctx context.Context, token *models.UserToken, clientIP net.IP, userAgent string) (bool, error)
LookupTokenProvider func(ctx context.Context, unhashedToken string) (*models.UserToken, error)
RevokeTokenProvider func(ctx context.Context, token *models.UserToken, soft bool) error
RevokeAllUserTokensProvider func(ctx context.Context, userId int64) error
ActiveAuthTokenCount func(ctx context.Context) (int64, error)
GetUserTokenProvider func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error)
GetUserTokensProvider func(ctx context.Context, userId int64) ([]*models.UserToken, error)
GetUserRevokedTokensProvider func(ctx context.Context, userId int64) ([]*models.UserToken, error)
BatchRevokedTokenProvider func(ctx context.Context, userIds []int64) error
}
func NewFakeUserAuthTokenService() *FakeUserAuthTokenService {
@@ -36,7 +37,7 @@ func NewFakeUserAuthTokenService() *FakeUserAuthTokenService {
UnhashedToken: "",
}, nil
},
RevokeTokenProvider: func(ctx context.Context, token *models.UserToken) error {
RevokeTokenProvider: func(ctx context.Context, token *models.UserToken, soft bool) error {
return nil
},
RevokeAllUserTokensProvider: func(ctx context.Context, userId int64) error {
@@ -76,8 +77,8 @@ func (s *FakeUserAuthTokenService) TryRotateToken(ctx context.Context, token *mo
return s.TryRotateTokenProvider(context.Background(), token, clientIP, userAgent)
}
func (s *FakeUserAuthTokenService) RevokeToken(ctx context.Context, token *models.UserToken) error {
return s.RevokeTokenProvider(context.Background(), token)
func (s *FakeUserAuthTokenService) RevokeToken(ctx context.Context, token *models.UserToken, soft bool) error {
return s.RevokeTokenProvider(context.Background(), token, soft)
}
func (s *FakeUserAuthTokenService) RevokeAllUserTokens(ctx context.Context, userId int64) error {
@@ -96,6 +97,10 @@ func (s *FakeUserAuthTokenService) GetUserTokens(ctx context.Context, userId int
return s.GetUserTokensProvider(context.Background(), userId)
}
func (s *FakeUserAuthTokenService) GetUserRevokedTokens(ctx context.Context, userId int64) ([]*models.UserToken, error) {
return s.GetUserRevokedTokensProvider(context.Background(), userId)
}
func (s *FakeUserAuthTokenService) BatchRevokeAllUserTokens(ctx context.Context, userIds []int64) error {
return s.BatchRevokedTokenProvider(ctx, userIds)
}
@@ -257,7 +257,11 @@ func (h *ContextHandler) initContextWithToken(ctx *models.ReqContext, orgID int6
token, err := h.AuthTokenService.LookupToken(ctx.Req.Context(), rawToken)
if err != nil {
ctx.Logger.Error("Failed to look up user based on cookie", "error", err)
cookies.WriteSessionCookie(ctx, h.Cfg, "", -1)
var revokedErr *models.TokenRevokedError
if !errors.As(err, &revokedErr) || !ctx.IsApiRequest() {
cookies.WriteSessionCookie(ctx, h.Cfg, "", -1)
}
ctx.Data["lookupTokenErr"] = err
return false
@@ -32,4 +32,16 @@ func addUserAuthTokenMigrations(mg *Migrator) {
mg.AddMigration("add unique index user_auth_token.prev_auth_token", NewAddIndexMigration(userAuthTokenV1, userAuthTokenV1.Indices[1]))
mg.AddMigration("add index user_auth_token.user_id", NewAddIndexMigration(userAuthTokenV1, userAuthTokenV1.Indices[2]))
mg.AddMigration(
"Add revoked_at to the user auth token",
NewAddColumnMigration(
userAuthTokenV1,
&Column{
Name: "revoked_at",
Type: DB_Int,
Nullable: true,
},
),
)
}