From c40897ac09eea9ae9053a4f0f83ce11517adc9ad Mon Sep 17 00:00:00 2001
From: bjoern-m <56024829+bjoern-m@users.noreply.github.com>
Date: Fri, 20 Dec 2024 09:52:52 +0100
Subject: [PATCH] feat: always persist sessions server-side, config adjustments
(#1997)
* feat: always persist sessions server-side, config adjustments
---
backend/cmd/jwt/create.go | 32 +++---
backend/config/config.yaml | 8 +-
backend/config/config_default.go | 11 +-
backend/config/config_session.go | 23 ++--
backend/dto/session.go | 37 ++++---
.../flow/profile/action_session_delete.go | 2 +-
.../flow/profile/hook_get_sessions.go | 2 +-
.../flow/shared/hook_issue_session.go | 55 +++++-----
backend/flow_api/handler.go | 54 +++++-----
backend/handler/email.go | 17 ++-
backend/handler/email_test.go | 43 ++------
backend/handler/helpers_test.go | 40 +++++++
backend/handler/password_test.go | 16 +--
backend/handler/public_router.go | 2 +-
backend/handler/session.go | 92 ++++++++--------
backend/handler/session_admin.go | 63 ++++++-----
backend/handler/user_test.go | 101 ++++--------------
backend/handler/webauthn_test.go | 71 ++----------
backend/json_schema/hanko.config.json | 48 +++++----
backend/middleware/session.go | 44 ++++----
backend/persistence/models/session.go | 17 +--
backend/session/session.go | 24 ++---
backend/session/session_test.go | 21 +---
.../accordion/ListSessionsAccordion.tsx | 26 ++---
.../src/lib/flow-api/types/payload.ts | 6 +-
25 files changed, 364 insertions(+), 491 deletions(-)
create mode 100644 backend/handler/helpers_test.go
diff --git a/backend/cmd/jwt/create.go b/backend/cmd/jwt/create.go
index b170d5fb4..2c4edd06c 100644
--- a/backend/cmd/jwt/create.go
+++ b/backend/cmd/jwt/create.go
@@ -73,26 +73,22 @@ func NewCreateCommand() *cobra.Command {
return
}
- if cfg.Session.ServerSide.Enabled {
- sessionID, _ := rawToken.Get("session_id")
+ sessionID, _ := rawToken.Get("session_id")
- expirationTime := rawToken.Expiration()
- sessionModel := models.Session{
- ID: uuid.FromStringOrNil(sessionID.(string)),
- UserID: userId,
- UserAgent: "",
- IpAddress: "",
- CreatedAt: rawToken.IssuedAt(),
- UpdatedAt: rawToken.IssuedAt(),
- ExpiresAt: &expirationTime,
- LastUsed: rawToken.IssuedAt(),
- }
+ expirationTime := rawToken.Expiration()
+ sessionModel := models.Session{
+ ID: uuid.FromStringOrNil(sessionID.(string)),
+ UserID: userId,
+ CreatedAt: rawToken.IssuedAt(),
+ UpdatedAt: rawToken.IssuedAt(),
+ ExpiresAt: &expirationTime,
+ LastUsed: rawToken.IssuedAt(),
+ }
- err = persister.GetSessionPersister().Create(sessionModel)
- if err != nil {
- fmt.Printf("failed to store session: %s", err)
- return
- }
+ err = persister.GetSessionPersister().Create(sessionModel)
+ if err != nil {
+ fmt.Printf("failed to store session: %s", err)
+ return
}
fmt.Printf("token: %s", token)
diff --git a/backend/config/config.yaml b/backend/config/config.yaml
index 9c824a493..ae7851d6c 100644
--- a/backend/config/config.yaml
+++ b/backend/config/config.yaml
@@ -90,16 +90,18 @@ server:
service:
name: Hanko Authentication Service
session:
+ allow_revocation: true
+ acquire_ip_address: true
+ acquire_user_agent: true
lifespan: 12h
enable_auth_token_header: false
- server_side:
- enabled: false
- limit: 100
+ limit: 5
cookie:
http_only: true
retention: persistent
same_site: strict
secure: true
+ show_on_profile: true
third_party:
providers:
apple:
diff --git a/backend/config/config_default.go b/backend/config/config_default.go
index 32aac92ab..2b2c99618 100644
--- a/backend/config/config_default.go
+++ b/backend/config/config_default.go
@@ -69,17 +69,18 @@ func DefaultConfig() *Config {
Host: "localhost",
},
Session: Session{
- Lifespan: "12h",
+ AllowRevocation: true,
+ AcquireIPAddress: true,
+ AcquireUserAgent: true,
+ Lifespan: "12h",
Cookie: Cookie{
HttpOnly: true,
Retention: "persistent",
SameSite: "strict",
Secure: true,
},
- ServerSide: ServerSide{
- Enabled: false,
- Limit: 100,
- },
+ Limit: 5,
+ ShowOnProfile: true,
},
AuditLog: AuditLog{
ConsoleOutput: AuditLogConsole{
diff --git a/backend/config/config_session.go b/backend/config/config_session.go
index cb53c9e0f..e9fbf757c 100644
--- a/backend/config/config_session.go
+++ b/backend/config/config_session.go
@@ -7,10 +7,16 @@ import (
)
type Session struct {
+ // `allow_revocation` allows users to revoke their own sessions.
+ AllowRevocation bool `yaml:"allow_revocation" json:"allow_revocation,omitempty" koanf:"allow_revocation" jsonschema:"default=true"`
// `audience` is a list of strings that identifies the recipients that the JWT is intended for.
// The audiences are placed in the `aud` claim of the JWT.
// If not set, it defaults to the value of the`webauthn.relying_party.id` configuration parameter.
Audience []string `yaml:"audience" json:"audience,omitempty" koanf:"audience"`
+ // `acquire_ip_address` stores the user's IP address in the database.
+ AcquireIPAddress bool `yaml:"acquire_ip_address" json:"acquire_ip_address,omitempty" koanf:"acquire_ip_address" jsonschema:"default=true"`
+ // `acquire_user_agent` stores the user's user agent in the database.
+ AcquireUserAgent bool `yaml:"acquire_user_agent" json:"acquire_user_agent,omitempty" koanf:"acquire_user_agent" jsonschema:"default=true"`
// `cookie` contains configuration for the session cookie issued on successful registration or login.
Cookie Cookie `yaml:"cookie" json:"cookie,omitempty" koanf:"cookie"`
// `enable_auth_token_header` determines whether a session token (JWT) is returned in an `X-Auth-Token`
@@ -24,8 +30,11 @@ type Session struct {
// numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m".
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
Lifespan string `yaml:"lifespan" json:"lifespan,omitempty" koanf:"lifespan" jsonschema:"default=12h"`
- // `server_side` contains configuration for server-side sessions.
- ServerSide ServerSide `yaml:"server_side" json:"server_side" koanf:"server_side"`
+ // `limit` determines the maximum number of server-side sessions a user can have. When the limit is exceeded,
+ // older sessions are invalidated.
+ Limit int `yaml:"limit" json:"limit,omitempty" koanf:"limit" jsonschema:"default=5"`
+ // `show_on_profile` indicates that the sessions should be listed on the profile.
+ ShowOnProfile bool `yaml:"show_on_profile" json:"show_on_profile,omitempty" koanf:"show_on_profile" jsonschema:"default=true"`
}
func (s *Session) Validate() error {
@@ -75,13 +84,3 @@ func (c *Cookie) GetName() string {
return "hanko"
}
-
-type ServerSide struct {
- // `enabled` determines whether server-side sessions are enabled.
- //
- // NOTE: When enabled the session endpoint must be used in order to check if a session is still valid.
- Enabled bool `yaml:"enabled" json:"enabled,omitempty" koanf:"enabled" jsonschema:"default=false"`
- // `limit` determines the maximum number of server-side sessions a user can have. When the limit is exceeded,
- // older sessions are invalidated.
- Limit int `yaml:"limit" json:"limit,omitempty" koanf:"limit" jsonschema:"default=100"`
-}
diff --git a/backend/dto/session.go b/backend/dto/session.go
index 60ed33e14..52859d090 100644
--- a/backend/dto/session.go
+++ b/backend/dto/session.go
@@ -10,9 +10,9 @@ import (
type SessionData struct {
ID uuid.UUID `json:"id"`
- UserAgentRaw string `json:"user_agent_raw"`
- UserAgent string `json:"user_agent"`
- IpAddress string `json:"ip_address"`
+ UserAgentRaw *string `json:"user_agent_raw,omitempty"`
+ UserAgent *string `json:"user_agent,omitempty"`
+ IpAddress *string `json:"ip_address,omitempty"`
Current bool `json:"current"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
@@ -20,17 +20,28 @@ type SessionData struct {
}
func FromSessionModel(model models.Session, current bool) SessionData {
- ua := useragent.Parse(model.UserAgent)
- return SessionData{
- ID: model.ID,
- UserAgentRaw: model.UserAgent,
- UserAgent: fmt.Sprintf("%s (%s)", ua.OS, ua.Name),
- IpAddress: model.IpAddress,
- Current: current,
- CreatedAt: model.CreatedAt,
- ExpiresAt: model.ExpiresAt,
- LastUsed: model.LastUsed,
+ sessionData := SessionData{
+ ID: model.ID,
+ Current: current,
+ CreatedAt: model.CreatedAt,
+ ExpiresAt: model.ExpiresAt,
+ LastUsed: model.LastUsed,
}
+
+ if model.UserAgent.Valid {
+ raw := model.UserAgent.String
+ sessionData.UserAgentRaw = &raw
+ ua := useragent.Parse(model.UserAgent.String)
+ parsed := fmt.Sprintf("%s (%s)", ua.OS, ua.Name)
+ sessionData.UserAgent = &parsed
+ }
+
+ if model.IpAddress.Valid {
+ s := model.IpAddress.String
+ sessionData.IpAddress = &s
+ }
+
+ return sessionData
}
type ValidateSessionResponse struct {
diff --git a/backend/flow_api/flow/profile/action_session_delete.go b/backend/flow_api/flow/profile/action_session_delete.go
index 21488be8f..2e941834b 100644
--- a/backend/flow_api/flow/profile/action_session_delete.go
+++ b/backend/flow_api/flow/profile/action_session_delete.go
@@ -22,7 +22,7 @@ func (a SessionDelete) GetDescription() string {
func (a SessionDelete) Initialize(c flowpilot.InitializationContext) {
deps := a.GetDeps(c)
- if !deps.Cfg.Session.ServerSide.Enabled {
+ if !deps.Cfg.Session.AllowRevocation {
c.SuspendAction()
return
}
diff --git a/backend/flow_api/flow/profile/hook_get_sessions.go b/backend/flow_api/flow/profile/hook_get_sessions.go
index 4b91ce97b..ca68f697e 100644
--- a/backend/flow_api/flow/profile/hook_get_sessions.go
+++ b/backend/flow_api/flow/profile/hook_get_sessions.go
@@ -17,7 +17,7 @@ type GetSessions struct {
func (h GetSessions) Execute(c flowpilot.HookExecutionContext) error {
deps := h.GetDeps(c)
- if !deps.Cfg.Session.ServerSide.Enabled {
+ if !deps.Cfg.Session.ShowOnProfile {
return nil
}
diff --git a/backend/flow_api/flow/shared/hook_issue_session.go b/backend/flow_api/flow/shared/hook_issue_session.go
index 5efdc6660..e5486cf66 100644
--- a/backend/flow_api/flow/shared/hook_issue_session.go
+++ b/backend/flow_api/flow/shared/hook_issue_session.go
@@ -3,6 +3,7 @@ package shared
import (
"errors"
"fmt"
+ "github.com/gobuffalo/nulls"
"github.com/gofrs/uuid"
auditlog "github.com/teamhanko/hanko/backend/audit_log"
"github.com/teamhanko/hanko/backend/dto"
@@ -49,35 +50,39 @@ func (h IssueSession) Execute(c flowpilot.HookExecutionContext) error {
return fmt.Errorf("failed to list active sessions: %w", err)
}
- if deps.Cfg.Session.ServerSide.Enabled {
- // remove all server side sessions that exceed the limit
- if len(activeSessions) >= deps.Cfg.Session.ServerSide.Limit {
- for i := deps.Cfg.Session.ServerSide.Limit - 1; i < len(activeSessions); i++ {
- err = deps.Persister.GetSessionPersisterWithConnection(deps.Tx).Delete(activeSessions[i])
- if err != nil {
- return fmt.Errorf("failed to remove latest session: %w", err)
- }
+ // remove all server side sessions that exceed the limit
+ if len(activeSessions) >= deps.Cfg.Session.Limit {
+ for i := deps.Cfg.Session.Limit - 1; i < len(activeSessions); i++ {
+ err = deps.Persister.GetSessionPersisterWithConnection(deps.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,
- UserAgent: deps.HttpContext.Request().UserAgent(),
- IpAddress: deps.HttpContext.RealIP(),
- CreatedAt: rawToken.IssuedAt(),
- UpdatedAt: rawToken.IssuedAt(),
- ExpiresAt: &expirationTime,
- LastUsed: rawToken.IssuedAt(),
- }
+ sessionID, _ := rawToken.Get("session_id")
- err = deps.Persister.GetSessionPersisterWithConnection(deps.Tx).Create(sessionModel)
- if err != nil {
- return fmt.Errorf("failed to store session: %w", err)
- }
+ 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 deps.Cfg.Session.AcquireIPAddress {
+ sessionModel.IpAddress = nulls.NewString(deps.HttpContext.RealIP())
+ }
+
+ if deps.Cfg.Session.AcquireUserAgent {
+ sessionModel.UserAgent = nulls.NewString(deps.HttpContext.Request().UserAgent())
+ }
+
+ err = deps.Persister.GetSessionPersisterWithConnection(deps.Tx).Create(sessionModel)
+ if err != nil {
+ return fmt.Errorf("failed to store session: %w", err)
}
rememberMeSelected := c.Stash().Get(StashPathRememberMeSelected).Bool()
diff --git a/backend/flow_api/handler.go b/backend/flow_api/handler.go
index f5087b61c..809ddf604 100644
--- a/backend/flow_api/handler.go
+++ b/backend/flow_api/handler.go
@@ -85,34 +85,32 @@ func (h *FlowPilotHandler) validateSession(c echo.Context) error {
continue
}
- if h.Cfg.Session.ServerSide.Enabled {
- // check that the session id is stored in the database
- sessionId, ok := token.Get("session_id")
- if !ok {
- lastTokenErr = errors.New("no session id found in token")
- continue
- }
- sessionID, err := uuid.FromString(sessionId.(string))
- if err != nil {
- lastTokenErr = errors.New("session id has wrong format")
- continue
- }
-
- sessionModel, err := h.Persister.GetSessionPersister().Get(sessionID)
- if err != nil {
- return fmt.Errorf("failed to get session from database: %w", err)
- }
- if sessionModel == nil {
- lastTokenErr = fmt.Errorf("session id not found in database")
- continue
- }
-
- // Update lastUsed field
- sessionModel.LastUsed = time.Now().UTC()
- err = h.Persister.GetSessionPersister().Update(*sessionModel)
- if err != nil {
- return dto.ToHttpError(err)
- }
+ // check that the session id is stored in the database
+ sessionId, ok := token.Get("session_id")
+ if !ok {
+ lastTokenErr = errors.New("no session id found in token")
+ continue
+ }
+ sessionID, err := uuid.FromString(sessionId.(string))
+ if err != nil {
+ lastTokenErr = errors.New("session id has wrong format")
+ continue
+ }
+
+ sessionModel, err := h.Persister.GetSessionPersister().Get(sessionID)
+ if err != nil {
+ return fmt.Errorf("failed to get session from database: %w", err)
+ }
+ if sessionModel == nil {
+ lastTokenErr = fmt.Errorf("session id not found in database")
+ continue
+ }
+
+ // Update lastUsed field
+ sessionModel.LastUsed = time.Now().UTC()
+ err = h.Persister.GetSessionPersister().Update(*sessionModel)
+ if err != nil {
+ return dto.ToHttpError(err)
}
c.Set("session", token)
diff --git a/backend/handler/email.go b/backend/handler/email.go
index 7a2999dad..5e47437ea 100644
--- a/backend/handler/email.go
+++ b/backend/handler/email.go
@@ -12,7 +12,6 @@ import (
"github.com/teamhanko/hanko/backend/dto"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/persistence/models"
- "github.com/teamhanko/hanko/backend/session"
"github.com/teamhanko/hanko/backend/webhooks/events"
"github.com/teamhanko/hanko/backend/webhooks/utils"
"net/http"
@@ -20,18 +19,16 @@ import (
)
type EmailHandler struct {
- persister persistence.Persister
- cfg *config.Config
- sessionManager session.Manager
- auditLogger auditlog.Logger
+ persister persistence.Persister
+ cfg *config.Config
+ auditLogger auditlog.Logger
}
-func NewEmailHandler(cfg *config.Config, persister persistence.Persister, sessionManager session.Manager, auditLogger auditlog.Logger) *EmailHandler {
+func NewEmailHandler(cfg *config.Config, persister persistence.Persister, auditLogger auditlog.Logger) *EmailHandler {
return &EmailHandler{
- persister: persister,
- cfg: cfg,
- sessionManager: sessionManager,
- auditLogger: auditLogger,
+ persister: persister,
+ cfg: cfg,
+ auditLogger: auditLogger,
}
}
diff --git a/backend/handler/email_test.go b/backend/handler/email_test.go
index db6dd83fa..ccb89c96a 100644
--- a/backend/handler/email_test.go
+++ b/backend/handler/email_test.go
@@ -7,9 +7,7 @@ import (
"github.com/gofrs/uuid"
"github.com/stretchr/testify/suite"
"github.com/teamhanko/hanko/backend/config"
- "github.com/teamhanko/hanko/backend/crypto/jwk"
"github.com/teamhanko/hanko/backend/dto"
- "github.com/teamhanko/hanko/backend/session"
"github.com/teamhanko/hanko/backend/test"
"net/http"
"net/http/httptest"
@@ -26,7 +24,7 @@ type emailSuite struct {
}
func (s *emailSuite) TestEmailHandler_New() {
- emailHandler := NewEmailHandler(&config.Config{}, s.Storage, sessionManager{}, test.NewAuditLogger())
+ emailHandler := NewEmailHandler(&config.Config{}, s.Storage, test.NewAuditLogger())
s.NotEmpty(emailHandler)
}
@@ -40,11 +38,6 @@ func (s *emailSuite) TestEmailHandler_List() {
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
- jwkManager, err := jwk.NewDefaultManager(test.DefaultConfig.Secrets.Keys, s.Storage.GetJwkPersister())
- s.Require().NoError(err)
- sessionManager, err := session.NewManager(jwkManager, test.DefaultConfig)
- s.Require().NoError(err)
-
tests := []struct {
name string
userId uuid.UUID
@@ -64,9 +57,7 @@ func (s *emailSuite) TestEmailHandler_List() {
for _, currentTest := range tests {
s.Run(currentTest.name, func() {
- token, _, err := sessionManager.GenerateJWT(currentTest.userId, nil)
- s.Require().NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
+ cookie, err := generateSessionCookie(s.Storage, currentTest.userId)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodGet, "/emails", nil)
@@ -172,14 +163,7 @@ func (s *emailSuite) TestEmailHandler_Create() {
cfg.Email.RequireVerification = currentTest.requiresVerification
cfg.Email.Limit = currentTest.maxNumberOfAddresses
e := NewPublicRouter(&cfg, s.Storage, nil, nil)
- jwkManager, err := jwk.NewDefaultManager(cfg.Secrets.Keys, s.Storage.GetJwkPersister())
- s.Require().NoError(err)
- sessionManager, err := session.NewManager(jwkManager, cfg)
- s.Require().NoError(err)
-
- token, _, err := sessionManager.GenerateJWT(currentTest.userId, nil)
- s.Require().NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
+ cookie, err := generateSessionCookie(s.Storage, currentTest.userId)
s.Require().NoError(err)
body := dto.EmailCreateRequest{
@@ -235,19 +219,11 @@ func (s *emailSuite) TestEmailHandler_SetPrimaryEmail() {
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
- jwkManager, err := jwk.NewDefaultManager(test.DefaultConfig.Secrets.Keys, s.Storage.GetJwkPersister())
- s.Require().NoError(err)
- sessionManager, err := session.NewManager(jwkManager, test.DefaultConfig)
- s.Require().NoError(err)
-
oldPrimaryEmailId := uuid.FromStringOrNil("51b7c175-ceb6-45ba-aae6-0092221c1b84")
newPrimaryEmailId := uuid.FromStringOrNil("8bb4c8a7-a3e6-48bb-b54f-20e3b485ab33")
userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
-
- token, _, err := sessionManager.GenerateJWT(userId, nil)
- s.NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
- s.NoError(err)
+ cookie, err := generateSessionCookie(s.Storage, userId)
+ s.Require().NoError(err)
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/emails/%s/set_primary", newPrimaryEmailId), nil)
req.AddCookie(cookie)
@@ -280,16 +256,9 @@ func (s *emailSuite) TestEmailHandler_Delete() {
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
- jwkManager, err := jwk.NewDefaultManager(test.DefaultConfig.Secrets.Keys, s.Storage.GetJwkPersister())
- s.Require().NoError(err)
- sessionManager, err := session.NewManager(jwkManager, test.DefaultConfig)
+ cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
- token, _, err := sessionManager.GenerateJWT(userId, nil)
- s.NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
- s.NoError(err)
-
tests := []struct {
name string
emailId uuid.UUID
diff --git a/backend/handler/helpers_test.go b/backend/handler/helpers_test.go
new file mode 100644
index 000000000..498558734
--- /dev/null
+++ b/backend/handler/helpers_test.go
@@ -0,0 +1,40 @@
+package handler
+
+import (
+ "github.com/gofrs/uuid"
+ "github.com/teamhanko/hanko/backend/crypto/jwk"
+ "github.com/teamhanko/hanko/backend/persistence"
+ "github.com/teamhanko/hanko/backend/persistence/models"
+ "github.com/teamhanko/hanko/backend/session"
+ "github.com/teamhanko/hanko/backend/test"
+ "net/http"
+ "time"
+)
+
+func getDefaultSessionManager(storage persistence.Persister) session.Manager {
+ jwkManager, _ := jwk.NewDefaultManager(test.DefaultConfig.Secrets.Keys, storage.GetJwkPersister())
+ sessionManager, _ := session.NewManager(jwkManager, test.DefaultConfig)
+ return sessionManager
+}
+
+func generateSessionCookie(storage persistence.Persister, userId uuid.UUID) (*http.Cookie, error) {
+ manager := getDefaultSessionManager(storage)
+ token, rawToken, err := manager.GenerateJWT(userId, nil)
+ if err != nil {
+ return nil, err
+ }
+ sessionID, _ := rawToken.Get("session_id")
+ _ = storage.GetSessionPersister().Create(models.Session{
+ ID: uuid.FromStringOrNil(sessionID.(string)),
+ UserID: userId,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ ExpiresAt: nil,
+ LastUsed: time.Now(),
+ })
+ cookie, err := manager.GenerateCookie(token)
+ if err != nil {
+ return nil, err
+ }
+ return cookie, nil
+}
diff --git a/backend/handler/password_test.go b/backend/handler/password_test.go
index 0deeb6d9b..d3805ef78 100644
--- a/backend/handler/password_test.go
+++ b/backend/handler/password_test.go
@@ -6,8 +6,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/teamhanko/hanko/backend/config"
- "github.com/teamhanko/hanko/backend/crypto/jwk"
- "github.com/teamhanko/hanko/backend/session"
"github.com/teamhanko/hanko/backend/test"
"golang.org/x/crypto/bcrypt"
"net/http"
@@ -90,10 +88,7 @@ func (s *passwordSuite) TestPasswordHandler_Set_Create() {
err := s.LoadFixtures("../test/fixtures/password")
s.Require().NoError(err)
- sessionManager := s.GetDefaultSessionManager()
- token, _, err := sessionManager.GenerateJWT(currentTest.userId, nil)
- s.Require().NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
+ cookie, err := generateSessionCookie(s.Storage, currentTest.userId)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodPut, "/password", strings.NewReader(currentTest.body))
@@ -196,15 +191,6 @@ func (s *passwordSuite) TestPasswordHandler_Login() {
}
}
-func (s *passwordSuite) GetDefaultSessionManager() session.Manager {
- jwkManager, err := jwk.NewDefaultManager(test.DefaultConfig.Secrets.Keys, s.Storage.GetJwkPersister())
- s.Require().NoError(err)
- sessionManager, err := session.NewManager(jwkManager, test.DefaultConfig)
- s.Require().NoError(err)
-
- return sessionManager
-}
-
// TestMaxPasswordLength bcrypt since version 0.5.0 only accepts passwords at least 72 bytes long. This test documents this behaviour.
func TestMaxPasswordLength(t *testing.T) {
tests := []struct {
diff --git a/backend/handler/public_router.go b/backend/handler/public_router.go
index 0f0958837..cb2120ba5 100644
--- a/backend/handler/public_router.go
+++ b/backend/handler/public_router.go
@@ -169,7 +169,7 @@ func NewPublicRouter(cfg *config.Config, persister persistence.Persister, promet
wellKnown.GET("/jwks.json", wellKnownHandler.GetPublicKeys)
wellKnown.GET("/config", wellKnownHandler.GetConfig)
- emailHandler := NewEmailHandler(cfg, persister, sessionManager, auditLogger)
+ emailHandler := NewEmailHandler(cfg, persister, auditLogger)
if cfg.Passkey.Enabled {
webauthnHandler, err := NewWebauthnHandler(cfg, persister, sessionManager, auditLogger, authenticatorMetadata)
diff --git a/backend/handler/session.go b/backend/handler/session.go
index c27e41d47..81390b75f 100644
--- a/backend/handler/session.go
+++ b/backend/handler/session.go
@@ -48,31 +48,29 @@ func (h *SessionHandler) ValidateSession(c echo.Context) error {
continue
}
- if h.cfg.Session.ServerSide.Enabled {
- // check that the session id is stored in the database
- sessionId, ok := t.Get("session_id")
- if !ok {
- continue
- }
- sessionID, err := uuid.FromString(sessionId.(string))
- if err != nil {
- continue
- }
-
- sessionModel, err := h.persister.GetSessionPersister().Get(sessionID)
- if err != nil {
- return fmt.Errorf("failed to get session from database: %w", err)
- }
- if sessionModel == nil {
- continue
- }
-
- // Update lastUsed field
- sessionModel.LastUsed = time.Now().UTC()
- err = h.persister.GetSessionPersister().Update(*sessionModel)
- if err != nil {
- return dto.ToHttpError(err)
- }
+ // check that the session id is stored in the database
+ sessionId, ok := t.Get("session_id")
+ if !ok {
+ continue
+ }
+ sessionID, err := uuid.FromString(sessionId.(string))
+ if err != nil {
+ continue
+ }
+
+ sessionModel, err := h.persister.GetSessionPersister().Get(sessionID)
+ if err != nil {
+ return fmt.Errorf("failed to get session from database: %w", err)
+ }
+ if sessionModel == nil {
+ continue
+ }
+
+ // Update lastUsed field
+ sessionModel.LastUsed = time.Now().UTC()
+ err = h.persister.GetSessionPersister().Update(*sessionModel)
+ if err != nil {
+ return dto.ToHttpError(err)
}
token = t
@@ -110,32 +108,30 @@ func (h *SessionHandler) ValidateSessionFromBody(c echo.Context) error {
return c.JSON(http.StatusOK, dto.ValidateSessionResponse{IsValid: false})
}
- if h.cfg.Session.ServerSide.Enabled {
- // check that the session id is stored in the database
- sessionId, ok := token.Get("session_id")
- if !ok {
- return c.JSON(http.StatusOK, dto.ValidateSessionResponse{IsValid: false})
- }
- sessionID, err := uuid.FromString(sessionId.(string))
- if err != nil {
- return c.JSON(http.StatusOK, dto.ValidateSessionResponse{IsValid: false})
- }
+ // check that the session id is stored in the database
+ sessionId, ok := token.Get("session_id")
+ if !ok {
+ return c.JSON(http.StatusOK, dto.ValidateSessionResponse{IsValid: false})
+ }
+ sessionID, err := uuid.FromString(sessionId.(string))
+ if err != nil {
+ return c.JSON(http.StatusOK, dto.ValidateSessionResponse{IsValid: false})
+ }
- sessionModel, err := h.persister.GetSessionPersister().Get(sessionID)
- if err != nil {
- return dto.ToHttpError(err)
- }
+ sessionModel, err := h.persister.GetSessionPersister().Get(sessionID)
+ if err != nil {
+ return dto.ToHttpError(err)
+ }
- if sessionModel == nil {
- return c.JSON(http.StatusOK, dto.ValidateSessionResponse{IsValid: false})
- }
+ if sessionModel == nil {
+ return c.JSON(http.StatusOK, dto.ValidateSessionResponse{IsValid: false})
+ }
- // update lastUsed field
- sessionModel.LastUsed = time.Now().UTC()
- err = h.persister.GetSessionPersister().Update(*sessionModel)
- if err != nil {
- return dto.ToHttpError(err)
- }
+ // update lastUsed field
+ sessionModel.LastUsed = time.Now().UTC()
+ err = h.persister.GetSessionPersister().Update(*sessionModel)
+ if err != nil {
+ return dto.ToHttpError(err)
}
expirationTime := token.Expiration()
diff --git a/backend/handler/session_admin.go b/backend/handler/session_admin.go
index c9c7ec55f..40bbc4b35 100644
--- a/backend/handler/session_admin.go
+++ b/backend/handler/session_admin.go
@@ -2,6 +2,7 @@ package handler
import (
"fmt"
+ "github.com/gobuffalo/nulls"
"github.com/gofrs/uuid"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
@@ -65,40 +66,44 @@ func (h *SessionAdminHandler) Generate(ctx echo.Context) error {
return fmt.Errorf("failed to generate JWT: %w", err)
}
- if h.cfg.Session.ServerSide.Enabled {
- activeSessions, err := h.persister.GetSessionPersister().ListActive(userID)
- if err != nil {
- return fmt.Errorf("failed to list active sessions: %w", err)
- }
+ activeSessions, err := h.persister.GetSessionPersister().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) >= h.cfg.Session.ServerSide.Limit {
- for i := h.cfg.Session.ServerSide.Limit - 1; i < len(activeSessions); i++ {
- err = h.persister.GetSessionPersister().Delete(activeSessions[i])
- if err != nil {
- return fmt.Errorf("failed to remove latest session: %w", err)
- }
+ // remove all server side sessions that exceed the limit
+ if len(activeSessions) >= h.cfg.Session.Limit {
+ for i := h.cfg.Session.Limit - 1; i < len(activeSessions); i++ {
+ err = h.persister.GetSessionPersister().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,
- UserAgent: body.UserAgent,
- IpAddress: body.IpAddress,
- CreatedAt: rawToken.IssuedAt(),
- UpdatedAt: rawToken.IssuedAt(),
- ExpiresAt: &expirationTime,
- LastUsed: rawToken.IssuedAt(),
- }
+ sessionID, _ := rawToken.Get("session_id")
- err = h.persister.GetSessionPersister().Create(sessionModel)
- if err != nil {
- return fmt.Errorf("failed to store session: %w", err)
- }
+ 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 len(body.UserAgent) > 0 {
+ sessionModel.UserAgent = nulls.NewString(body.UserAgent)
+ }
+
+ if len(body.IpAddress) > 0 {
+ sessionModel.IpAddress = nulls.NewString(body.IpAddress)
+ }
+
+ err = h.persister.GetSessionPersister().Create(sessionModel)
+ if err != nil {
+ return fmt.Errorf("failed to store session: %w", err)
}
response := admin.CreateSessionTokenResponse{
diff --git a/backend/handler/user_test.go b/backend/handler/user_test.go
index 10ab070ab..d7ecbcf74 100644
--- a/backend/handler/user_test.go
+++ b/backend/handler/user_test.go
@@ -7,10 +7,8 @@ import (
"github.com/gofrs/uuid"
_ "github.com/lib/pq"
"github.com/stretchr/testify/suite"
- "github.com/teamhanko/hanko/backend/crypto/jwk"
"github.com/teamhanko/hanko/backend/dto"
"github.com/teamhanko/hanko/backend/persistence/models"
- "github.com/teamhanko/hanko/backend/session"
"github.com/teamhanko/hanko/backend/test"
"golang.org/x/exp/slices"
"net/http"
@@ -249,24 +247,14 @@ func (s *userSuite) TestUserHandler_Get() {
err := s.LoadFixtures("../test/fixtures/user")
s.Require().NoError(err)
- userId := "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5"
+ userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
- jwkManager, err := jwk.NewDefaultManager(test.DefaultConfig.Secrets.Keys, s.Storage.GetJwkPersister())
- if err != nil {
- panic(fmt.Errorf("failed to create jwk manager: %w", err))
- }
- sessionManager, err := session.NewManager(jwkManager, test.DefaultConfig)
- if err != nil {
- panic(fmt.Errorf("failed to create session generator: %w", err))
- }
- token, _, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil)
- s.Require().NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
+ cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
- req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/users/%s", userId), nil)
+ req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/users/%s", userId.String()), nil)
req.AddCookie(cookie)
rec := httptest.NewRecorder()
@@ -277,7 +265,7 @@ func (s *userSuite) TestUserHandler_Get() {
user := models.User{}
err := json.Unmarshal(rec.Body.Bytes(), &user)
s.NoError(err)
- s.Equal(userId, user.ID.String())
+ s.Equal(userId.String(), user.ID.String())
s.Equal(len(user.WebauthnCredentials), 0)
}
}
@@ -289,24 +277,14 @@ func (s *userSuite) TestUserHandler_GetUserWithWebAuthnCredential() {
err := s.LoadFixtures("../test/fixtures/user_with_webauthn_credential")
s.Require().NoError(err)
- userId := "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5"
+ userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
- jwkManager, err := jwk.NewDefaultManager(test.DefaultConfig.Secrets.Keys, s.Storage.GetJwkPersister())
- if err != nil {
- panic(fmt.Errorf("failed to create jwk manager: %w", err))
- }
- sessionManager, err := session.NewManager(jwkManager, test.DefaultConfig)
- if err != nil {
- panic(fmt.Errorf("failed to create session generator: %w", err))
- }
- token, _, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil)
- s.Require().NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
+ cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
- req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/users/%s", userId), nil)
+ req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/users/%s", userId.String()), nil)
rec := httptest.NewRecorder()
req.AddCookie(cookie)
@@ -317,7 +295,7 @@ func (s *userSuite) TestUserHandler_GetUserWithWebAuthnCredential() {
user := models.User{}
err := json.Unmarshal(rec.Body.Bytes(), &user)
s.Require().NoError(err)
- s.Equal(userId, user.ID.String())
+ s.Equal(userId.String(), user.ID.String())
s.Equal(len(user.WebauthnCredentials), 1)
}
}
@@ -328,19 +306,12 @@ func (s *userSuite) TestUserHandler_Get_InvalidUserId() {
}
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
- userId := "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5"
-
- jwkManager, err := jwk.NewDefaultManager(test.DefaultConfig.Secrets.Keys, s.Storage.GetJwkPersister())
- if err != nil {
- panic(fmt.Errorf("failed to create jwk manager: %w", err))
- }
- sessionManager, err := session.NewManager(jwkManager, test.DefaultConfig)
- if err != nil {
- panic(fmt.Errorf("failed to create session generator: %w", err))
- }
- token, _, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil)
+ err := s.LoadFixtures("../test/fixtures/user")
s.Require().NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
+
+ userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
+
+ cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodGet, "/users/invalidUserId", nil)
@@ -463,21 +434,11 @@ func (s *userSuite) TestUserHandler_Me() {
err := s.LoadFixtures("../test/fixtures/user_with_webauthn_credential")
s.Require().NoError(err)
- userId := "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5"
+ userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
- jwkManager, err := jwk.NewDefaultManager(test.DefaultConfig.Secrets.Keys, s.Storage.GetJwkPersister())
- if err != nil {
- panic(fmt.Errorf("failed to create jwk manager: %w", err))
- }
- sessionManager, err := session.NewManager(jwkManager, test.DefaultConfig)
- if err != nil {
- panic(fmt.Errorf("failed to create session generator: %w", err))
- }
- token, _, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil)
- s.Require().NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
+ cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodGet, "/me", nil)
@@ -493,7 +454,7 @@ func (s *userSuite) TestUserHandler_Me() {
}{}
err = json.Unmarshal(rec.Body.Bytes(), &response)
s.NoError(err)
- s.Equal(userId, response.UserId)
+ s.Equal(userId.String(), response.UserId)
}
}
@@ -501,20 +462,14 @@ func (s *userSuite) TestUserHandler_Logout() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
- userId, _ := uuid.NewV4()
- e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
- jwkManager, err := jwk.NewDefaultManager(test.DefaultConfig.Secrets.Keys, s.Storage.GetJwkPersister())
- if err != nil {
- panic(fmt.Errorf("failed to create jwk manager: %w", err))
- }
- sessionManager, err := session.NewManager(jwkManager, test.DefaultConfig)
- if err != nil {
- panic(fmt.Errorf("failed to create session generator: %w", err))
- }
- token, _, err := sessionManager.GenerateJWT(userId, nil)
+ err := s.LoadFixtures("../test/fixtures/user")
s.Require().NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
+
+ userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
+ e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
+
+ cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodPost, "/logout", nil)
@@ -545,17 +500,7 @@ func (s *userSuite) TestUserHandler_Delete() {
cfg.Account.AllowDeletion = true
e := NewPublicRouter(&cfg, s.Storage, nil, nil)
- jwkManager, err := jwk.NewDefaultManager(test.DefaultConfig.Secrets.Keys, s.Storage.GetJwkPersister())
- if err != nil {
- panic(fmt.Errorf("failed to create jwk manager: %w", err))
- }
- sessionManager, err := session.NewManager(jwkManager, test.DefaultConfig)
- if err != nil {
- panic(fmt.Errorf("failed to create session generator: %w", err))
- }
- token, _, err := sessionManager.GenerateJWT(userId, nil)
- s.Require().NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
+ cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodDelete, "/user", nil)
diff --git a/backend/handler/webauthn_test.go b/backend/handler/webauthn_test.go
index 8c457dbc0..62a1d060d 100644
--- a/backend/handler/webauthn_test.go
+++ b/backend/handler/webauthn_test.go
@@ -6,12 +6,8 @@ import (
"github.com/go-webauthn/webauthn/protocol"
"github.com/gofrs/uuid"
"github.com/labstack/echo/v4"
- "github.com/lestrrat-go/jwx/v2/jwt"
"github.com/stretchr/testify/suite"
- "github.com/teamhanko/hanko/backend/crypto/jwk"
- "github.com/teamhanko/hanko/backend/dto"
"github.com/teamhanko/hanko/backend/persistence/models"
- "github.com/teamhanko/hanko/backend/session"
"github.com/teamhanko/hanko/backend/test"
"net/http"
"net/http/httptest"
@@ -32,7 +28,8 @@ func (s *webauthnSuite) TestWebauthnHandler_NewHandler() {
if testing.Short() {
s.T().Skip("skipping test in short mode")
}
- handler, err := NewWebauthnHandler(&test.DefaultConfig, s.Storage, s.GetDefaultSessionManager(), test.NewAuditLogger(), nil)
+ manager := getDefaultSessionManager(s.Storage)
+ handler, err := NewWebauthnHandler(&test.DefaultConfig, s.Storage, manager, test.NewAuditLogger(), nil)
s.NoError(err)
s.NotEmpty(handler)
}
@@ -45,14 +42,11 @@ func (s *webauthnSuite) TestWebauthnHandler_BeginRegistration() {
err := s.LoadFixtures("../test/fixtures/webauthn")
s.Require().NoError(err)
- userId := "ec4ef049-5b88-4321-a173-21b0eff06a04"
+ userId := uuid.FromStringOrNil("ec4ef049-5b88-4321-a173-21b0eff06a04")
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
- sessionManager := s.GetDefaultSessionManager()
- token, _, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil)
- s.Require().NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
+ cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodPost, "/webauthn/registration/initialize", nil)
@@ -70,7 +64,7 @@ func (s *webauthnSuite) TestWebauthnHandler_BeginRegistration() {
s.Require().NoError(err)
s.NotEmpty(creationOptions.Response.Challenge)
- s.Equal(uuid.FromStringOrNil(userId).Bytes(), uId)
+ s.Equal(uuid.FromStringOrNil(userId.String()).Bytes(), uId)
s.Equal(test.DefaultConfig.Webauthn.RelyingParty.Id, creationOptions.Response.RelyingParty.ID)
s.Equal(protocol.ResidentKeyRequirementRequired, creationOptions.Response.AuthenticatorSelection.ResidentKey)
s.Equal(protocol.VerificationPreferred, creationOptions.Response.AuthenticatorSelection.UserVerification)
@@ -86,14 +80,11 @@ func (s *webauthnSuite) TestWebauthnHandler_FinalizeRegistration() {
err := s.LoadFixtures("../test/fixtures/webauthn_registration")
s.Require().NoError(err)
- userId := "ec4ef049-5b88-4321-a173-21b0eff06a04"
+ userId := uuid.FromStringOrNil("ec4ef049-5b88-4321-a173-21b0eff06a04")
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
- sessionManager := s.GetDefaultSessionManager()
- token, _, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil)
- s.Require().NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
+ cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
body := `{
@@ -132,14 +123,11 @@ func (s *webauthnSuite) TestWebauthnHandler_FinalizeRegistration_SessionDataExpi
err := s.LoadFixtures("../test/fixtures/webauthn_registration")
s.Require().NoError(err)
- userId := "ec4ef049-5b88-4321-a173-21b0eff06a04"
+ userId := uuid.FromStringOrNil("ec4ef049-5b88-4321-a173-21b0eff06a04")
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
- sessionManager := s.GetDefaultSessionManager()
- token, _, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil)
- s.Require().NoError(err)
- cookie, err := sessionManager.GenerateCookie(token)
+ cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
body := `{
@@ -318,49 +306,8 @@ func (s *webauthnSuite) TestWebauthnHandler_FinalizeAuthentication_TokenInHeader
}
}
-func (s *webauthnSuite) GetDefaultSessionManager() session.Manager {
- jwkManager, err := jwk.NewDefaultManager(test.DefaultConfig.Secrets.Keys, s.Storage.GetJwkPersister())
- s.Require().NoError(err)
- sessionManager, err := session.NewManager(jwkManager, test.DefaultConfig)
- s.Require().NoError(err)
-
- return sessionManager
-}
-
var userId = "ec4ef049-5b88-4321-a173-21b0eff06a04"
-type sessionManager struct {
-}
-
-func (s sessionManager) GenerateJWT(_ uuid.UUID, _ *dto.EmailJwt, _ ...session.JWTOptions) (string, jwt.Token, error) {
- return userId, nil, nil
-}
-
-func (s sessionManager) GenerateCookie(token string) (*http.Cookie, error) {
- return &http.Cookie{
- Name: "hanko",
- Value: token,
- Secure: true,
- HttpOnly: true,
- SameSite: http.SameSiteLaxMode,
- }, nil
-}
-
-func (s sessionManager) DeleteCookie() (*http.Cookie, error) {
- return &http.Cookie{
- Name: "hanko",
- Value: "",
- Secure: true,
- HttpOnly: true,
- SameSite: http.SameSiteLaxMode,
- MaxAge: -1,
- }, nil
-}
-
-func (s sessionManager) Verify(_ string) (jwt.Token, error) {
- return nil, nil
-}
-
var uId, _ = uuid.FromString(userId)
var emails = []models.Email{
diff --git a/backend/json_schema/hanko.config.json b/backend/json_schema/hanko.config.json
index aae4b9b20..644996896 100644
--- a/backend/json_schema/hanko.config.json
+++ b/backend/json_schema/hanko.config.json
@@ -1169,22 +1169,6 @@
"additionalProperties": false,
"type": "object"
},
- "ServerSide": {
- "properties": {
- "enabled": {
- "type": "boolean",
- "description": "`enabled` determines whether server-side sessions are enabled.\n\nNOTE: When enabled the session endpoint must be used in order to check if a session is still valid.",
- "default": false
- },
- "limit": {
- "type": "integer",
- "description": "`limit` determines the maximum number of server-side sessions a user can have. When the limit is exceeded,\nolder sessions are invalidated.",
- "default": 100
- }
- },
- "additionalProperties": false,
- "type": "object"
- },
"Service": {
"properties": {
"name": {
@@ -1197,6 +1181,11 @@
},
"Session": {
"properties": {
+ "allow_revocation": {
+ "type": "boolean",
+ "description": "`allow_revocation` allows users to revoke their own sessions.",
+ "default": true
+ },
"audience": {
"items": {
"type": "string"
@@ -1204,6 +1193,16 @@
"type": "array",
"description": "`audience` is a list of strings that identifies the recipients that the JWT is intended for.\nThe audiences are placed in the `aud` claim of the JWT.\nIf not set, it defaults to the value of the`webauthn.relying_party.id` configuration parameter."
},
+ "acquire_ip_address": {
+ "type": "boolean",
+ "description": "`acquire_ip_address` stores the user's IP address in the database.",
+ "default": true
+ },
+ "acquire_user_agent": {
+ "type": "boolean",
+ "description": "`acquire_user_agent` stores the user's user agent in the database.",
+ "default": true
+ },
"cookie": {
"$ref": "#/$defs/Cookie",
"description": "`cookie` contains configuration for the session cookie issued on successful registration or login."
@@ -1222,16 +1221,19 @@
"description": "`lifespan` determines the maximum duration for which a session token (JWT) is valid. It must be a (possibly signed) sequence of decimal\nnumbers, each with optional fraction and a unit suffix, such as \"300ms\", \"-1.5h\" or \"2h45m\".\nValid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".",
"default": "12h"
},
- "server_side": {
- "$ref": "#/$defs/ServerSide",
- "description": "`server_side` contains configuration for server-side sessions."
+ "limit": {
+ "type": "integer",
+ "description": "`limit` determines the maximum number of server-side sessions a user can have. When the limit is exceeded,\nolder sessions are invalidated.",
+ "default": 5
+ },
+ "show_on_profile": {
+ "type": "boolean",
+ "description": "`show_on_profile` indicates that the sessions should be listed on the profile.",
+ "default": true
}
},
"additionalProperties": false,
- "type": "object",
- "required": [
- "server_side"
- ]
+ "type": "object"
},
"TOTP": {
"properties": {
diff --git a/backend/middleware/session.go b/backend/middleware/session.go
index ab2b361f5..9ac4cfcc9 100644
--- a/backend/middleware/session.go
+++ b/backend/middleware/session.go
@@ -35,31 +35,29 @@ func parseToken(cfg config.Session, persister persistence.Persister, generator s
return nil, err
}
- if cfg.ServerSide.Enabled {
- // check that the session id is stored in the database
- sessionId, ok := token.Get("session_id")
- if !ok {
- return nil, errors.New("no session id found in token")
- }
- sessionID, err := uuid.FromString(sessionId.(string))
- if err != nil {
- return nil, errors.New("session id has wrong format")
- }
+ // check that the session id is stored in the database
+ sessionId, ok := token.Get("session_id")
+ if !ok {
+ return nil, errors.New("no session id found in token")
+ }
+ sessionID, err := uuid.FromString(sessionId.(string))
+ if err != nil {
+ return nil, errors.New("session id has wrong format")
+ }
- sessionModel, err := persister.GetSessionPersister().Get(sessionID)
- if err != nil {
- return nil, fmt.Errorf("failed to get session from database: %w", err)
- }
- if sessionModel == nil {
- return nil, fmt.Errorf("session id not found in database")
- }
+ sessionModel, err := persister.GetSessionPersister().Get(sessionID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get session from database: %w", err)
+ }
+ if sessionModel == nil {
+ return nil, fmt.Errorf("session id not found in database")
+ }
- // Update lastUsed field
- sessionModel.LastUsed = time.Now().UTC()
- err = persister.GetSessionPersister().Update(*sessionModel)
- if err != nil {
- return nil, err
- }
+ // Update lastUsed field
+ sessionModel.LastUsed = time.Now().UTC()
+ err = persister.GetSessionPersister().Update(*sessionModel)
+ if err != nil {
+ return nil, err
}
return token, nil
diff --git a/backend/persistence/models/session.go b/backend/persistence/models/session.go
index d0d6f6597..81278f71d 100644
--- a/backend/persistence/models/session.go
+++ b/backend/persistence/models/session.go
@@ -1,6 +1,7 @@
package models
import (
+ "github.com/gobuffalo/nulls"
"github.com/gobuffalo/pop/v6"
"github.com/gobuffalo/validate/v3"
"github.com/gobuffalo/validate/v3/validators"
@@ -9,14 +10,14 @@ import (
)
type Session struct {
- ID uuid.UUID `db:"id" json:"id"`
- UserID uuid.UUID `db:"user_id" json:"user_id"`
- UserAgent string `db:"user_agent" json:"user_agent"`
- IpAddress string `db:"ip_address" json:"ip_address"`
- CreatedAt time.Time `db:"created_at" json:"created_at"`
- UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
- ExpiresAt *time.Time `db:"expires_at" json:"expires_at"`
- LastUsed time.Time `db:"last_used" json:"last_used"`
+ ID uuid.UUID `db:"id" json:"id"`
+ UserID uuid.UUID `db:"user_id" json:"user_id"`
+ UserAgent nulls.String `db:"user_agent" json:"user_agent"`
+ IpAddress nulls.String `db:"ip_address" json:"ip_address"`
+ CreatedAt time.Time `db:"created_at" json:"created_at"`
+ UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
+ ExpiresAt *time.Time `db:"expires_at" json:"expires_at"`
+ LastUsed time.Time `db:"last_used" json:"last_used"`
}
func (session *Session) Validate(tx *pop.Connection) (*validate.Errors, error) {
diff --git a/backend/session/session.go b/backend/session/session.go
index 0431773bf..b8cc5e8d0 100644
--- a/backend/session/session.go
+++ b/backend/session/session.go
@@ -21,12 +21,11 @@ type Manager interface {
// Manager is used to create and verify session JWTs
type manager struct {
- jwtGenerator hankoJwt.Generator
- sessionLength time.Duration
- cookieConfig cookieConfig
- issuer string
- audience []string
- serverSideSessionsEnabled bool
+ jwtGenerator hankoJwt.Generator
+ sessionLength time.Duration
+ cookieConfig cookieConfig
+ issuer string
+ audience []string
}
type cookieConfig struct {
@@ -86,8 +85,7 @@ func NewManager(jwkManager hankoJwk.Manager, config config.Config) (Manager, err
SameSite: sameSite,
Secure: config.Session.Cookie.Secure,
},
- audience: audience,
- serverSideSessionsEnabled: config.Session.ServerSide.Enabled,
+ audience: audience,
}, nil
}
@@ -102,13 +100,11 @@ func (m *manager) GenerateJWT(userId uuid.UUID, email *dto.EmailJwt, opts ...JWT
_ = token.Set(jwt.ExpirationKey, expiration)
_ = token.Set(jwt.AudienceKey, m.audience)
- if m.serverSideSessionsEnabled {
- sessionID, err := uuid.NewV4()
- if err != nil {
- return "", nil, err
- }
- _ = token.Set("session_id", sessionID.String())
+ sessionID, err := uuid.NewV4()
+ if err != nil {
+ return "", nil, err
}
+ _ = token.Set("session_id", sessionID.String())
if email != nil {
_ = token.Set("email", &email)
diff --git a/backend/session/session_test.go b/backend/session/session_test.go
index 29009958b..50e3c019e 100644
--- a/backend/session/session_test.go
+++ b/backend/session/session_test.go
@@ -153,26 +153,9 @@ func Test_GenerateJWT_SessionID(t *testing.T) {
tokenShouldHaveSessionID bool
}{
{
- name: "token has no session id claim if server side sessions are disabled",
+ name: "token has a session id claim",
config: config.Config{
- Session: config.Session{
- Lifespan: "5m",
- ServerSide: config.ServerSide{
- Enabled: false,
- },
- },
- },
- tokenShouldHaveSessionID: false,
- },
- {
- name: "token has a session id claim if server side sessions are disabled",
- config: config.Config{
- Session: config.Session{
- Lifespan: "5m",
- ServerSide: config.ServerSide{
- Enabled: true,
- },
- },
+ Session: config.Session{Lifespan: "5m"},
},
tokenShouldHaveSessionID: true,
},
diff --git a/frontend/elements/src/components/accordion/ListSessionsAccordion.tsx b/frontend/elements/src/components/accordion/ListSessionsAccordion.tsx
index c6c180ae7..b71ca0d47 100644
--- a/frontend/elements/src/components/accordion/ListSessionsAccordion.tsx
+++ b/frontend/elements/src/components/accordion/ListSessionsAccordion.tsx
@@ -28,23 +28,19 @@ const ListSessionsAccordion = ({
const { t } = useContext(TranslateContext);
const labels = (session: Session) => {
- const description = (
+ const headline = (
+ {session.user_agent ? session.user_agent : session.id}
+ );
+ const description = session.current ? (
- {session.current ? (
-
- {" -"} {t("labels.currentSession")}
-
- ) : null}
+
+ {" -"} {t("labels.currentSession")}
+
- );
- return session.current ? (
-
- {session.user_agent}
- {description}
-
- ) : (
+ ) : null;
+ return (
- {session.user_agent}
+ {headline}
{description}
);
@@ -54,7 +50,7 @@ const ListSessionsAccordion = ({
const contents = (session: Session) => (
-
+
{t("headlines.ipAddress")}
{session.ip_address}
diff --git a/frontend/frontend-sdk/src/lib/flow-api/types/payload.ts b/frontend/frontend-sdk/src/lib/flow-api/types/payload.ts
index 412c08031..9e9eaaf82 100644
--- a/frontend/frontend-sdk/src/lib/flow-api/types/payload.ts
+++ b/frontend/frontend-sdk/src/lib/flow-api/types/payload.ts
@@ -77,9 +77,9 @@ export interface User {
export interface Session {
readonly id: string;
- readonly user_agent: string;
- readonly user_agent_raw: string;
- readonly ip_address: string;
+ readonly user_agent?: string;
+ readonly user_agent_raw?: string;
+ readonly ip_address?: string;
readonly created_at: string;
readonly last_used: string;
readonly current: boolean;