Skip to content

Commit

Permalink
Auth: Attach external session info to Grafana session (grafana#93849)
Browse files Browse the repository at this point in the history
* initial from poc changes

* wip

* Remove public external session service

* Update swagger

* Fix merge

* Cleanup

* Add backgroud service for cleanup

* Add auth_module to user_external_session

* Add tests for token revocation functions

* Add secret migration capabilities for user_external_session fields

* Cleanup, refactor to address feedback

* Fix test
  • Loading branch information
mgyongyosi authored Oct 8, 2024
1 parent 9eea0e9 commit bd78508
Show file tree
Hide file tree
Showing 26 changed files with 1,419 additions and 161 deletions.
2 changes: 1 addition & 1 deletion pkg/api/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ func (hs *HTTPServer) loginUserWithUser(user *user.User, c *contextmodel.ReqCont

hs.log.Debug("Got IP address from client address", "addr", addr, "ip", ip)
ctx := context.WithValue(c.Req.Context(), loginservice.RequestURIKey{}, c.Req.RequestURI)
userToken, err := hs.AuthTokenService.CreateToken(ctx, user, ip, c.Req.UserAgent())
userToken, err := hs.AuthTokenService.CreateToken(ctx, &auth.CreateTokenCommand{User: user, ClientIP: ip, UserAgent: c.Req.UserAgent()})
if err != nil {
return fmt.Errorf("%v: %w", "failed to create auth token", err)
}
Expand Down
27 changes: 14 additions & 13 deletions pkg/models/usertoken/user_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,20 @@ func (e *TokenRevokedError) Unwrap() error { return ErrInvalidSessionToken }

// UserToken represents a user token
type UserToken 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
Id int64
UserId int64
ExternalSessionId int64
AuthToken string
PrevAuthToken string
UserAgent string
ClientIp string
AuthTokenSeen bool
SeenAt int64
RotatedAt int64
CreatedAt int64
UpdatedAt int64
RevokedAt int64
UnhashedToken string
}

const UrgentRotateTime = 1 * time.Minute
Expand Down
18 changes: 15 additions & 3 deletions pkg/services/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ const (

// Typed errors
var (
ErrUserTokenNotFound = errors.New("user token not found")
ErrInvalidSessionToken = usertoken.ErrInvalidSessionToken
ErrUserTokenNotFound = errors.New("user token not found")
ErrInvalidSessionToken = usertoken.ErrInvalidSessionToken
ErrExternalSessionNotFound = errors.New("external session not found")
)

type (
Expand Down Expand Up @@ -65,10 +66,21 @@ type RotateCommand struct {
UserAgent string
}

type CreateTokenCommand struct {
User *user.User
ClientIP net.IP
UserAgent string
ExternalSession *ExternalSession
}

// UserTokenService are used for generating and validating user tokens
type UserTokenService interface {
CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*UserToken, error)
CreateToken(ctx context.Context, cmd *CreateTokenCommand) (*UserToken, error)
LookupToken(ctx context.Context, unhashedToken string) (*UserToken, error)
GetTokenByExternalSessionID(ctx context.Context, externalSessionID int64) (*UserToken, error)
GetExternalSession(ctx context.Context, extSessionID int64) (*ExternalSession, error)
FindExternalSessions(ctx context.Context, query *ListExternalSessionQuery) ([]*ExternalSession, error)

// RotateToken will always rotate a valid token
RotateToken(ctx context.Context, cmd RotateCommand) (*UserToken, error)
RevokeToken(ctx context.Context, token *UserToken, soft bool) error
Expand Down
153 changes: 115 additions & 38 deletions pkg/services/auth/authimpl/auth_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ 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/infra/tracing"
"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"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
Expand All @@ -28,17 +29,21 @@ var (
errUserIDInvalid = errors.New("invalid user ID")
)

var _ auth.UserTokenService = (*UserAuthTokenService)(nil)

func ProvideUserAuthTokenService(sqlStore db.DB,
serverLockService *serverlock.ServerLockService,
quotaService quota.Service,
cfg *setting.Cfg) (*UserAuthTokenService, error) {
quotaService quota.Service, secretService secrets.Service,
cfg *setting.Cfg, tracer tracing.Tracer,
) (*UserAuthTokenService, error) {
s := &UserAuthTokenService{
sqlStore: sqlStore,
serverLockService: serverLockService,
cfg: cfg,
log: log.New("auth"),
singleflight: new(singleflight.Group),
}
s.externalSessionStore = provideExternalSessionStore(sqlStore, secretService, tracer)

defaultLimits, err := readQuotaConfig(cfg)
if err != nil {
Expand All @@ -57,31 +62,32 @@ func ProvideUserAuthTokenService(sqlStore db.DB,
}

type UserAuthTokenService struct {
sqlStore db.DB
serverLockService *serverlock.ServerLockService
cfg *setting.Cfg
log log.Logger
singleflight *singleflight.Group
sqlStore db.DB
serverLockService *serverlock.ServerLockService
cfg *setting.Cfg
log log.Logger
externalSessionStore auth.ExternalSessionStore
singleflight *singleflight.Group
}

func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) {
func (s *UserAuthTokenService) CreateToken(ctx context.Context, cmd *auth.CreateTokenCommand) (*auth.UserToken, error) {
token, hashedToken, err := generateAndHashToken(s.cfg.SecretKey)
if err != nil {
return nil, err
}

now := getTime().Unix()
clientIPStr := clientIP.String()
if len(clientIP) == 0 {
clientIPStr := cmd.ClientIP.String()
if len(cmd.ClientIP) == 0 {
clientIPStr = ""
}

userAuthToken := userAuthToken{
UserId: user.ID,
UserId: cmd.User.ID,
AuthToken: hashedToken,
PrevAuthToken: hashedToken,
ClientIp: clientIPStr,
UserAgent: userAgent,
UserAgent: cmd.UserAgent,
RotatedAt: now,
CreatedAt: now,
UpdatedAt: now,
Expand All @@ -90,11 +96,21 @@ func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User,
AuthTokenSeen: false,
}

err = s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
_, err = dbSession.Insert(&userAuthToken)
return err
})
err = s.sqlStore.InTransaction(ctx, func(ctx context.Context) error {
if cmd.ExternalSession != nil {
inErr := s.externalSessionStore.Create(ctx, cmd.ExternalSession)
if inErr != nil {
return inErr
}
userAuthToken.ExternalSessionId = cmd.ExternalSession.ID
}

inErr := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
_, err := dbSession.Insert(&userAuthToken)
return err
})
return inErr
})
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -164,7 +180,6 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st

return err
})

if err != nil {
return nil, err
}
Expand All @@ -190,7 +205,6 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st

return err
})

if err != nil {
return nil, err
}
Expand All @@ -210,6 +224,38 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st
return &userToken, err
}

func (s *UserAuthTokenService) GetTokenByExternalSessionID(ctx context.Context, externalSessionID int64) (*auth.UserToken, error) {
var token userAuthToken
err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
exists, err := dbSession.Where("external_session_id = ?", externalSessionID).Get(&token)
if err != nil {
return err
}

if !exists {
return auth.ErrUserTokenNotFound
}

return nil
})
if err != nil {
return nil, err
}

var userToken auth.UserToken
err = token.toUserToken(&userToken)

return &userToken, err
}

func (s *UserAuthTokenService) GetExternalSession(ctx context.Context, extSessionID int64) (*auth.ExternalSession, error) {
return s.externalSessionStore.Get(ctx, extSessionID)
}

func (s *UserAuthTokenService) FindExternalSessions(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error) {
return s.externalSessionStore.List(ctx, query)
}

func (s *UserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateCommand) (*auth.UserToken, error) {
if cmd.UnHashedToken == "" {
return nil, auth.ErrInvalidSessionToken
Expand Down Expand Up @@ -277,7 +323,6 @@ func (s *UserAuthTokenService) rotateToken(ctx context.Context, token *auth.User
affected, err = res.RowsAffected()
return err
})

if err != nil {
return nil, err
}
Expand Down Expand Up @@ -305,6 +350,8 @@ func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *auth.User
return err
}

ctxLogger := s.log.FromContext(ctx)

var rowsAffected int64

if soft {
Expand All @@ -324,7 +371,13 @@ func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *auth.User
return err
}

ctxLogger := s.log.FromContext(ctx)
if model.ExternalSessionId != 0 {
err = s.externalSessionStore.Delete(ctx, model.ExternalSessionId)
if err != nil {
// Intentionally not returning error here, as the token has been revoked -> the backround job will clean up orphaned external sessions
ctxLogger.Warn("Failed to delete external session", "externalSessionID", model.ExternalSessionId, "err", err)
}
}

if rowsAffected == 0 {
ctxLogger.Debug("User auth token not found/revoked", "tokenID", model.Id, "userID", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent)
Expand All @@ -337,51 +390,75 @@ func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *auth.User
}

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)
return s.sqlStore.InTransaction(ctx, func(ctx context.Context) error {
ctxLogger := s.log.FromContext(ctx)
err := 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
}

ctxLogger.Debug("All user tokens for user revoked", "userID", userId, "count", affected)

return nil
})
if err != nil {
return err
}

affected, err := res.RowsAffected()
err = s.externalSessionStore.DeleteExternalSessionsByUserID(ctx, userId)
if err != nil {
return err
// Intentionally not returning error here, as the token has been revoked -> the backround job will clean up orphaned external sessions
ctxLogger.Warn("Failed to delete external sessions for user", "userID", userId, "err", err)
}

s.log.FromContext(ctx).Debug("All user tokens for user revoked", "userID", userId, "count", affected)

return err
return nil
})
}

func (s *UserAuthTokenService) BatchRevokeAllUserTokens(ctx context.Context, userIds []int64) error {
return s.sqlStore.WithTransactionalDbSession(ctx, func(dbSession *db.Session) error {
return s.sqlStore.InTransaction(ctx, func(ctx context.Context) error {
ctxLogger := s.log.FromContext(ctx)
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 + ")"
userIdParams := strings.Repeat(",?", len(userIds)-1)
sql := "DELETE from user_auth_token WHERE user_id IN (?" + userIdParams + ")"

params := []any{sql}
for _, v := range userIds {
params = append(params, v)
}

res, err := dbSession.Exec(params...)
var affected int64

err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
res, inErr := dbSession.Exec(params...)
if inErr != nil {
return inErr
}

affected, inErr = res.RowsAffected()
return inErr
})
if err != nil {
return err
}

affected, err := res.RowsAffected()
err = s.externalSessionStore.BatchDeleteExternalSessionsByUserIDs(ctx, userIds)
if err != nil {
return err
ctxLogger.Warn("Failed to delete external sessions for users", "users", userIds, "err", err)
}

s.log.FromContext(ctx).Debug("All user tokens for given users revoked", "usersCount", len(userIds), "count", affected)
ctxLogger.Debug("All user tokens for given users revoked", "usersCount", len(userIds), "count", affected)

return err
return nil
})
}

Expand Down
Loading

0 comments on commit bd78508

Please sign in to comment.