Skip to content

Commit

Permalink
feat: always persist sessions server-side, config adjustments (#1997)
Browse files Browse the repository at this point in the history
* feat: always persist sessions server-side, config adjustments
  • Loading branch information
bjoern-m authored Dec 20, 2024
1 parent 26562d9 commit c40897a
Show file tree
Hide file tree
Showing 25 changed files with 364 additions and 491 deletions.
32 changes: 14 additions & 18 deletions backend/cmd/jwt/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 5 additions & 3 deletions backend/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 6 additions & 5 deletions backend/config/config_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
23 changes: 11 additions & 12 deletions backend/config/config_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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 {
Expand Down Expand Up @@ -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"`
}
37 changes: 24 additions & 13 deletions backend/dto/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,38 @@ 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"`
LastUsed time.Time `json:"last_used"`
}

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 {
Expand Down
2 changes: 1 addition & 1 deletion backend/flow_api/flow/profile/action_session_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion backend/flow_api/flow/profile/hook_get_sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
55 changes: 30 additions & 25 deletions backend/flow_api/flow/shared/hook_issue_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down
54 changes: 26 additions & 28 deletions backend/flow_api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 7 additions & 10 deletions backend/handler/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,23 @@ 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"
"strings"
)

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,
}
}

Expand Down
Loading

0 comments on commit c40897a

Please sign in to comment.