Auth: Add feature flag to move token rotation to client (#65060)
* FeatureToggle: Add toggle to use a new way of rotating tokens * API: Add endpoints to perform token rotation, one endpoint for api request and one endpoint for redirectsd * Auth: Aling not authorized handling between auth middleware and access control middleware * API: add utility function to get redirect for login * API: Handle token rotation redirect for login page * Frontend: Add job scheduling for token rotation and make call to token rotation as fallback in retry request * ContextHandler: Prevent in-request rotation if feature flag is enabled and check if token needs to be rotated * AuthN: Prevent in-request rotation if feature flag is enabled and check if token needs to be rotated * Cookies: Add option NotHttpOnly * AuthToken: Add helper function to get next rotation time and another function to check if token need to be rotated * AuthN: Add function to delete session cookie and set expiry cookie Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"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/models/usertoken"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -21,9 +23,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
const urgentRotateTime = 1 * time.Minute
|
||||
|
||||
var getTime = time.Now
|
||||
var (
|
||||
getTime = time.Now
|
||||
errTokenNotRotated = errors.New("token was not rotated")
|
||||
)
|
||||
|
||||
func ProvideUserAuthTokenService(sqlStore db.DB,
|
||||
serverLockService *serverlock.ServerLockService,
|
||||
@@ -62,13 +65,11 @@ type UserAuthTokenService struct {
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) {
|
||||
token, err := util.RandomHex(16)
|
||||
token, hashedToken, err := generateAndHashToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hashedToken := hashToken(token)
|
||||
|
||||
now := getTime().Unix()
|
||||
clientIPStr := clientIP.String()
|
||||
if len(clientIP) == 0 {
|
||||
@@ -150,17 +151,16 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st
|
||||
|
||||
// Current incoming token is the previous auth token in the DB and the auth_token_seen is true
|
||||
if model.AuthToken != hashedToken && model.PrevAuthToken == hashedToken && model.AuthTokenSeen {
|
||||
modelCopy := model
|
||||
modelCopy.AuthTokenSeen = false
|
||||
expireBefore := getTime().Add(-urgentRotateTime).Unix()
|
||||
model.AuthTokenSeen = false
|
||||
model.RotatedAt = getTime().Add(-usertoken.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)
|
||||
model.Id,
|
||||
model.PrevAuthToken,
|
||||
model.RotatedAt).
|
||||
AllCols().Update(&model)
|
||||
|
||||
return err
|
||||
})
|
||||
@@ -178,16 +178,15 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st
|
||||
|
||||
// Current incoming token is not seen and it is the latest valid auth token in the db
|
||||
if !model.AuthTokenSeen && model.AuthToken == hashedToken {
|
||||
modelCopy := model
|
||||
modelCopy.AuthTokenSeen = true
|
||||
modelCopy.SeenAt = getTime().Unix()
|
||||
model.AuthTokenSeen = true
|
||||
model.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)
|
||||
model.Id,
|
||||
model.AuthToken).
|
||||
AllCols().Update(&model)
|
||||
|
||||
return err
|
||||
})
|
||||
@@ -196,10 +195,6 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st
|
||||
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 {
|
||||
@@ -215,6 +210,90 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st
|
||||
return &userToken, err
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateCommand) (*auth.UserToken, error) {
|
||||
if cmd.UnHashedToken == "" {
|
||||
return nil, auth.ErrInvalidSessionToken
|
||||
}
|
||||
|
||||
res, err, _ := s.singleflight.Do(cmd.UnHashedToken, func() (interface{}, error) {
|
||||
token, err := s.LookupToken(ctx, cmd.UnHashedToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newToken, err := s.rotateToken(ctx, token, cmd.IP, cmd.UserAgent)
|
||||
|
||||
if errors.Is(err, errTokenNotRotated) {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newToken, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.(*auth.UserToken), nil
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) rotateToken(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (*auth.UserToken, error) {
|
||||
var clientIPStr string
|
||||
if clientIP != nil {
|
||||
clientIPStr = clientIP.String()
|
||||
}
|
||||
|
||||
newToken, hashedToken, err := generateAndHashToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sql := `
|
||||
UPDATE user_auth_token
|
||||
SET
|
||||
seen_at = 0,
|
||||
user_agent = ?,
|
||||
client_ip = ?,
|
||||
prev_auth_token = auth_token,
|
||||
auth_token = ?,
|
||||
auth_token_seen = ?,
|
||||
rotated_at = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
now := getTime()
|
||||
var affected int64
|
||||
err = s.sqlStore.WithTransactionalDbSession(ctx, func(dbSession *db.Session) error {
|
||||
res, err := dbSession.Exec(sql, userAgent, clientIPStr, hashedToken, s.sqlStore.GetDialect().BooleanStr(false), now.Unix(), token.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
affected, err = res.RowsAffected()
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if affected < 1 {
|
||||
return nil, errTokenNotRotated
|
||||
}
|
||||
|
||||
token.PrevAuthToken = token.AuthToken
|
||||
token.AuthToken = hashedToken
|
||||
token.UnhashedToken = newToken
|
||||
token.AuthTokenSeen = false
|
||||
token.RotatedAt = now.Unix()
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) TryRotateToken(ctx context.Context, token *auth.UserToken,
|
||||
clientIP net.IP, userAgent string) (bool, *auth.UserToken, error) {
|
||||
if token == nil {
|
||||
@@ -239,7 +318,7 @@ func (s *UserAuthTokenService) TryRotateToken(ctx context.Context, token *auth.U
|
||||
if model.AuthTokenSeen {
|
||||
needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.cfg.TokenRotationIntervalMinutes) * time.Minute))
|
||||
} else {
|
||||
needsRotation = rotatedAt.Before(now.Add(-urgentRotateTime))
|
||||
needsRotation = rotatedAt.Before(now.Add(-usertoken.UrgentRotateTime))
|
||||
}
|
||||
|
||||
if !needsRotation {
|
||||
@@ -504,11 +583,29 @@ func (s *UserAuthTokenService) rotatedAfterParam() int64 {
|
||||
return getTime().Add(-s.cfg.LoginMaxInactiveLifetime).Unix()
|
||||
}
|
||||
|
||||
func createToken() (string, error) {
|
||||
token, err := util.RandomHex(16)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func hashToken(token string) string {
|
||||
hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
|
||||
return hex.EncodeToString(hashBytes[:])
|
||||
}
|
||||
|
||||
func generateAndHashToken() (string, string, error) {
|
||||
token, err := createToken()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return token, hashToken(token), nil
|
||||
}
|
||||
|
||||
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
|
||||
limits := "a.Map{}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user