diff --git a/backend/handler/passcode.go b/backend/handler/passcode.go index 62b76a99f..523e184df 100644 --- a/backend/handler/passcode.go +++ b/backend/handler/passcode.go @@ -401,7 +401,7 @@ func (h *PasscodeHandler) Finish(c echo.Context) error { emailJwt = dto.JwtFromEmailModel(e) } - token, _, err := h.sessionManager.GenerateJWT(*passcode.UserId, emailJwt) + token, rawToken, err := h.sessionManager.GenerateJWT(*passcode.UserId, emailJwt) if err != nil { return fmt.Errorf("failed to generate jwt: %w", err) } @@ -411,6 +411,11 @@ func (h *PasscodeHandler) Finish(c echo.Context) error { return fmt.Errorf("failed to create session token: %w", err) } + err = storeSession(h.cfg, h.persister, *passcode.UserId, rawToken, c, tx) + if err != nil { + return fmt.Errorf("failed to store session in DB: %w", err) + } + c.Response().Header().Set("X-Session-Lifetime", fmt.Sprintf("%d", cookie.MaxAge)) if h.cfg.Session.EnableAuthTokenHeader { diff --git a/backend/handler/password.go b/backend/handler/password.go index ce8dbe2cc..4647d8fc2 100644 --- a/backend/handler/password.go +++ b/backend/handler/password.go @@ -223,7 +223,7 @@ func (h *PasswordHandler) Login(c echo.Context) error { emailJwt = dto.JwtFromEmailModel(e) } - token, _, err := h.sessionManager.GenerateJWT(pw.UserId, emailJwt) + token, rawToken, err := h.sessionManager.GenerateJWT(pw.UserId, emailJwt) if err != nil { return fmt.Errorf("failed to generate jwt: %w", err) } @@ -233,6 +233,11 @@ func (h *PasswordHandler) Login(c echo.Context) error { return fmt.Errorf("failed to create session cookie: %w", err) } + err = storeSession(h.cfg, h.persister, userId, rawToken, c, h.persister.GetConnection()) + if err != nil { + return fmt.Errorf("failed to store session in DB: %w", err) + } + c.Response().Header().Set("X-Session-Lifetime", fmt.Sprintf("%d", cookie.MaxAge)) if h.cfg.Session.EnableAuthTokenHeader { diff --git a/backend/handler/token.go b/backend/handler/token.go index 4de2daec2..a0246a1b0 100644 --- a/backend/handler/token.go +++ b/backend/handler/token.go @@ -92,7 +92,7 @@ func (h TokenHandler) Validate(c echo.Context) error { emailJwt = dto.JwtFromEmailModel(e) } - jwtToken, _, err := h.sessionManager.GenerateJWT(token.UserID, emailJwt) + jwtToken, rawToken, err := h.sessionManager.GenerateJWT(token.UserID, emailJwt) if err != nil { return fmt.Errorf("failed to generate jwt: %w", err) } @@ -102,6 +102,11 @@ func (h TokenHandler) Validate(c echo.Context) error { return fmt.Errorf("failed to create session token: %w", err) } + err = storeSession(h.cfg, h.persister, token.UserID, rawToken, c, h.persister.GetConnection()) + if err != nil { + return fmt.Errorf("failed to store session in DB: %w", err) + } + c.Response().Header().Set("X-Session-Lifetime", fmt.Sprintf("%d", cookie.MaxAge)) if h.cfg.Session.EnableAuthTokenHeader { diff --git a/backend/handler/utils.go b/backend/handler/utils.go index 3c2cbeef8..09a74f703 100644 --- a/backend/handler/utils.go +++ b/backend/handler/utils.go @@ -1,7 +1,15 @@ package handler import ( + "fmt" + "github.com/gobuffalo/nulls" + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" "github.com/labstack/echo/v4" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/persistence" + "github.com/teamhanko/hanko/backend/persistence/models" "net/http" ) @@ -21,3 +29,47 @@ func loadDto[I any](ctx echo.Context) (*I, error) { return &adminDto, nil } + +func storeSession(cfg *config.Config, persister persistence.Persister, userId uuid.UUID, rawToken jwt.Token, httpContext echo.Context, tx *pop.Connection) error { + activeSessions, err := persister.GetSessionPersisterWithConnection(tx).ListActive(userId) + if err != nil { + return fmt.Errorf("failed to list active sessions: %w", err) + } + + // remove all server side sessions that exceed the limit + if len(activeSessions) >= cfg.Session.Limit { + for i := cfg.Session.Limit - 1; i < len(activeSessions); i++ { + err = persister.GetSessionPersisterWithConnection(tx).Delete(activeSessions[i]) + if err != nil { + return fmt.Errorf("failed to remove latest session: %w", err) + } + } + } + + sessionID, _ := rawToken.Get("session_id") + + expirationTime := rawToken.Expiration() + sessionModel := models.Session{ + ID: uuid.FromStringOrNil(sessionID.(string)), + UserID: userId, + CreatedAt: rawToken.IssuedAt(), + UpdatedAt: rawToken.IssuedAt(), + ExpiresAt: &expirationTime, + LastUsed: rawToken.IssuedAt(), + } + + if cfg.Session.AcquireIPAddress { + sessionModel.IpAddress = nulls.NewString(httpContext.RealIP()) + } + + if cfg.Session.AcquireUserAgent { + sessionModel.UserAgent = nulls.NewString(httpContext.Request().UserAgent()) + } + + err = persister.GetSessionPersisterWithConnection(tx).Create(sessionModel) + if err != nil { + return fmt.Errorf("failed to store session: %w", err) + } + + return nil +} diff --git a/backend/handler/webauthn.go b/backend/handler/webauthn.go index 77d0d2994..16da7fae7 100644 --- a/backend/handler/webauthn.go +++ b/backend/handler/webauthn.go @@ -423,7 +423,7 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error { emailJwt = dto.JwtFromEmailModel(e) } - token, _, err := h.sessionManager.GenerateJWT(webauthnUser.UserId, emailJwt) + token, rawToken, err := h.sessionManager.GenerateJWT(webauthnUser.UserId, emailJwt) if err != nil { return fmt.Errorf("failed to generate jwt: %w", err) } @@ -433,6 +433,11 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error { return fmt.Errorf("failed to create session cookie: %w", err) } + err = storeSession(h.cfg, h.persister, webauthnUser.UserId, rawToken, c, tx) + if err != nil { + return fmt.Errorf("failed to store session in DB: %w", err) + } + c.Response().Header().Set("X-Session-Lifetime", fmt.Sprintf("%d", cookie.MaxAge)) if h.cfg.Session.EnableAuthTokenHeader { diff --git a/backend/test/config.go b/backend/test/config.go index 78e53e56b..7cb5c98db 100644 --- a/backend/test/config.go +++ b/backend/test/config.go @@ -37,6 +37,7 @@ var DefaultConfig = config.Config{ Cookie: config.Cookie{ SameSite: "none", }, + Limit: 5, }, Service: config.Service{ Name: "Test",