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:
@@ -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[:])
|
||||
}
|
||||
Reference in New Issue
Block a user