mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
feat(oidc): Add refresh token support for long-lived sessions
When offline_access scope is configured, Pulse now stores and uses OIDC refresh tokens to automatically extend sessions. Sessions remain valid as long as the IdP allows token refresh (typically 30-90 days). Changes: - Store OIDC tokens (refresh token, expiry, issuer) alongside sessions - Automatically refresh tokens when access token nears expiry - Invalidate session if IdP revokes access (forces re-login) - Add background token refresh with concurrency protection - Persist OIDC tokens across restarts Related to #854
This commit is contained in:
17
docs/OIDC.md
17
docs/OIDC.md
@@ -32,6 +32,23 @@ Restrict access to specific users or groups:
|
||||
* **Allowed Domains**: Restrict to specific email domains (e.g., `example.com`).
|
||||
* **Allowed Emails**: Allow specific email addresses.
|
||||
|
||||
### Long-Lived Sessions with `offline_access`
|
||||
For persistent sessions that don't require frequent re-authentication:
|
||||
|
||||
1. **Add `offline_access` scope**: Include `offline_access` in your OIDC scopes (e.g., `openid profile email offline_access`).
|
||||
2. **Configure your IdP**: Ensure your identity provider issues refresh tokens when `offline_access` is requested.
|
||||
|
||||
**How it works:**
|
||||
- When you login with `offline_access`, Pulse stores the refresh token alongside your session.
|
||||
- When your access token expires, Pulse automatically refreshes it using the stored refresh token.
|
||||
- Your session remains valid as long as the refresh token is valid (typically 30-90 days depending on your IdP).
|
||||
- If the IdP revokes access (user disabled, token revoked), Pulse detects this on the next refresh attempt and logs you out.
|
||||
|
||||
**Security considerations:**
|
||||
- Refresh tokens are stored encrypted at rest.
|
||||
- If the IdP configuration changes, existing sessions with mismatched issuers are automatically invalidated.
|
||||
- Failed refresh attempts immediately invalidate the session.
|
||||
|
||||
## 📚 Provider Examples
|
||||
|
||||
### Authentik
|
||||
|
||||
@@ -209,6 +209,15 @@ func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool
|
||||
if ValidateSession(cookie.Value) {
|
||||
// Check if this is an OIDC session
|
||||
if username := GetSessionUsername(cookie.Value); username != "" {
|
||||
// Check if OIDC tokens need refresh
|
||||
session := GetSessionStore().GetSession(cookie.Value)
|
||||
if session != nil && session.OIDCRefreshToken != "" {
|
||||
// Check if access token is expired or about to expire (5 min buffer)
|
||||
if time.Now().Add(5 * time.Minute).After(session.OIDCAccessTokenExp) {
|
||||
// Token needs refresh - attempt it asynchronously
|
||||
go refreshOIDCSessionTokens(cfg, cookie.Value, session)
|
||||
}
|
||||
}
|
||||
w.Header().Set("X-Authenticated-User", username)
|
||||
w.Header().Set("X-Auth-Method", "oidc")
|
||||
return true
|
||||
@@ -689,3 +698,77 @@ func adminBypassEnabled() bool {
|
||||
})
|
||||
return adminBypassState.enabled
|
||||
}
|
||||
|
||||
// oidcRefreshMutex prevents concurrent refresh attempts for the same session
|
||||
var oidcRefreshMutex sync.Map
|
||||
|
||||
// refreshOIDCSessionTokens refreshes OIDC tokens for a session in the background
|
||||
// If refresh fails, the session is invalidated and the user will need to re-login
|
||||
func refreshOIDCSessionTokens(cfg *config.Config, sessionToken string, session *SessionData) {
|
||||
// Prevent concurrent refresh attempts for the same session
|
||||
if _, loaded := oidcRefreshMutex.LoadOrStore(sessionToken, true); loaded {
|
||||
return // Another goroutine is already refreshing this session
|
||||
}
|
||||
defer oidcRefreshMutex.Delete(sessionToken)
|
||||
|
||||
// Mark session as refreshing to prevent duplicate attempts
|
||||
GetSessionStore().SetTokenRefreshing(sessionToken, true)
|
||||
defer GetSessionStore().SetTokenRefreshing(sessionToken, false)
|
||||
|
||||
log.Debug().
|
||||
Str("issuer", session.OIDCIssuer).
|
||||
Time("token_expiry", session.OIDCAccessTokenExp).
|
||||
Msg("Attempting OIDC token refresh")
|
||||
|
||||
// Create a context with timeout for the refresh operation
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Get or create OIDC service for this session's issuer
|
||||
oidcCfg := cfg.OIDC
|
||||
if oidcCfg == nil || !oidcCfg.Enabled {
|
||||
log.Warn().Msg("OIDC not enabled, cannot refresh tokens")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the session's issuer matches our config
|
||||
if oidcCfg.IssuerURL != session.OIDCIssuer {
|
||||
log.Warn().
|
||||
Str("session_issuer", session.OIDCIssuer).
|
||||
Str("config_issuer", oidcCfg.IssuerURL).
|
||||
Msg("OIDC issuer mismatch, cannot refresh tokens")
|
||||
GetSessionStore().InvalidateSession(sessionToken)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a temporary OIDC service for refreshing
|
||||
service, err := NewOIDCService(ctx, oidcCfg)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create OIDC service for token refresh")
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt to refresh the token
|
||||
result, err := service.RefreshToken(ctx, session.OIDCRefreshToken)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("issuer", session.OIDCIssuer).
|
||||
Msg("OIDC token refresh failed - invalidating session")
|
||||
|
||||
// Token refresh failed - this usually means the refresh token was revoked
|
||||
// or expired. Invalidate the session to force re-login.
|
||||
GetSessionStore().InvalidateSession(sessionToken)
|
||||
LogAuditEvent("oidc_token_refresh", "", "", "", false, "Token refresh failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Update the session with new tokens
|
||||
GetSessionStore().UpdateOIDCTokens(sessionToken, result.RefreshToken, result.Expiry)
|
||||
|
||||
log.Info().
|
||||
Time("new_expiry", result.Expiry).
|
||||
Msg("OIDC token refresh successful - session extended")
|
||||
|
||||
LogAuditEvent("oidc_token_refresh", "", "", "", true, "Token refreshed successfully")
|
||||
}
|
||||
|
||||
@@ -218,7 +218,22 @@ func (r *Router) handleOIDCCallback(w http.ResponseWriter, req *http.Request) {
|
||||
log.Debug().Msg("User group membership verified")
|
||||
}
|
||||
|
||||
if err := r.establishSession(w, req, username); err != nil {
|
||||
// Prepare OIDC token info for session storage (enables refresh token support)
|
||||
var oidcTokens *OIDCTokenInfo
|
||||
if token.RefreshToken != "" {
|
||||
oidcTokens = &OIDCTokenInfo{
|
||||
RefreshToken: token.RefreshToken,
|
||||
AccessTokenExp: token.Expiry,
|
||||
Issuer: cfg.IssuerURL,
|
||||
ClientID: cfg.ClientID,
|
||||
}
|
||||
log.Debug().
|
||||
Time("access_token_expiry", token.Expiry).
|
||||
Bool("has_refresh_token", true).
|
||||
Msg("OIDC tokens will be stored for session refresh")
|
||||
}
|
||||
|
||||
if err := r.establishOIDCSession(w, req, username, oidcTokens); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to establish session after OIDC login")
|
||||
LogAuditEvent("oidc_login", username, GetClientIP(req), req.URL.Path, false, "Session creation failed")
|
||||
r.redirectOIDCError(w, req, entry.ReturnTo, "session_failed")
|
||||
|
||||
@@ -210,6 +210,58 @@ func (s *OIDCService) contextWithHTTPClient(ctx context.Context) context.Context
|
||||
return oidc.ClientContext(ctx, s.httpClient)
|
||||
}
|
||||
|
||||
// OIDCRefreshResult contains the result of a token refresh operation
|
||||
type OIDCRefreshResult struct {
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
// RefreshToken uses the refresh token to obtain new access and refresh tokens from the IdP
|
||||
func (s *OIDCService) RefreshToken(ctx context.Context, refreshToken string) (*OIDCRefreshResult, error) {
|
||||
if refreshToken == "" {
|
||||
return nil, errors.New("no refresh token provided")
|
||||
}
|
||||
|
||||
ctx = s.contextWithHTTPClient(ctx)
|
||||
|
||||
// Create a token source from the refresh token
|
||||
token := &oauth2.Token{
|
||||
RefreshToken: refreshToken,
|
||||
// Set expiry in the past to force refresh
|
||||
Expiry: time.Now().Add(-time.Hour),
|
||||
}
|
||||
|
||||
tokenSource := s.oauth2Cfg.TokenSource(ctx, token)
|
||||
|
||||
// This will trigger a refresh since the token is expired
|
||||
newToken, err := tokenSource.Token()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("OIDC token refresh failed")
|
||||
return nil, fmt.Errorf("failed to refresh token: %w", err)
|
||||
}
|
||||
|
||||
result := &OIDCRefreshResult{
|
||||
AccessToken: newToken.AccessToken,
|
||||
Expiry: newToken.Expiry,
|
||||
}
|
||||
|
||||
// The new refresh token might be the same or different depending on the IdP
|
||||
if newToken.RefreshToken != "" {
|
||||
result.RefreshToken = newToken.RefreshToken
|
||||
} else {
|
||||
// Keep the old refresh token if a new one wasn't issued
|
||||
result.RefreshToken = refreshToken
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Time("new_expiry", result.Expiry).
|
||||
Bool("new_refresh_token", newToken.RefreshToken != "").
|
||||
Msg("OIDC token refresh successful")
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func newOIDCHTTPClient(caBundle string) (*http.Client, string, error) {
|
||||
transport, ok := http.DefaultTransport.(*http.Transport)
|
||||
var clone *http.Transport
|
||||
|
||||
@@ -2680,6 +2680,48 @@ func (r *Router) establishSession(w http.ResponseWriter, req *http.Request, user
|
||||
return nil
|
||||
}
|
||||
|
||||
// establishOIDCSession creates a session with OIDC token information for refresh token support
|
||||
func (r *Router) establishOIDCSession(w http.ResponseWriter, req *http.Request, username string, oidcTokens *OIDCTokenInfo) error {
|
||||
token := generateSessionToken()
|
||||
if token == "" {
|
||||
return fmt.Errorf("failed to generate session token")
|
||||
}
|
||||
|
||||
userAgent := req.Header.Get("User-Agent")
|
||||
clientIP := GetClientIP(req)
|
||||
|
||||
// Create session with OIDC tokens
|
||||
GetSessionStore().CreateOIDCSession(token, 24*time.Hour, userAgent, clientIP, oidcTokens)
|
||||
|
||||
if username != "" {
|
||||
TrackUserSession(username, token)
|
||||
}
|
||||
|
||||
csrfToken := generateCSRFToken(token)
|
||||
isSecure, sameSitePolicy := getCookieSettings(req)
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "pulse_session",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: isSecure,
|
||||
SameSite: sameSitePolicy,
|
||||
MaxAge: 86400,
|
||||
})
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "pulse_csrf",
|
||||
Value: csrfToken,
|
||||
Path: "/",
|
||||
Secure: isSecure,
|
||||
SameSite: sameSitePolicy,
|
||||
MaxAge: 86400,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleLogin handles login requests and provides detailed feedback about lockouts
|
||||
func (r *Router) handleLogin(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
|
||||
200
internal/api/session_oidc_test.go
Normal file
200
internal/api/session_oidc_test.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCreateOIDCSession(t *testing.T) {
|
||||
// Create a temporary directory for test sessions
|
||||
tmpDir, err := os.MkdirTemp("", "pulse-session-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
store := NewSessionStore(tmpDir)
|
||||
|
||||
// Create an OIDC session with token info
|
||||
token := "test-session-token-123"
|
||||
oidcInfo := &OIDCTokenInfo{
|
||||
RefreshToken: "test-refresh-token",
|
||||
AccessTokenExp: time.Now().Add(1 * time.Hour),
|
||||
Issuer: "https://example.com",
|
||||
ClientID: "test-client-id",
|
||||
}
|
||||
|
||||
store.CreateOIDCSession(token, 24*time.Hour, "TestAgent", "127.0.0.1", oidcInfo)
|
||||
|
||||
// Verify session was created
|
||||
if !store.ValidateSession(token) {
|
||||
t.Error("Created OIDC session should be valid")
|
||||
}
|
||||
|
||||
// Verify OIDC token info was stored
|
||||
session := store.GetSession(token)
|
||||
if session == nil {
|
||||
t.Fatal("GetSession returned nil for valid session")
|
||||
}
|
||||
|
||||
if session.OIDCRefreshToken != oidcInfo.RefreshToken {
|
||||
t.Errorf("RefreshToken mismatch: got %q, want %q", session.OIDCRefreshToken, oidcInfo.RefreshToken)
|
||||
}
|
||||
|
||||
if session.OIDCIssuer != oidcInfo.Issuer {
|
||||
t.Errorf("Issuer mismatch: got %q, want %q", session.OIDCIssuer, oidcInfo.Issuer)
|
||||
}
|
||||
|
||||
if session.OIDCClientID != oidcInfo.ClientID {
|
||||
t.Errorf("ClientID mismatch: got %q, want %q", session.OIDCClientID, oidcInfo.ClientID)
|
||||
}
|
||||
|
||||
if session.OIDCAccessTokenExp.IsZero() {
|
||||
t.Error("AccessTokenExp should not be zero")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateOIDCTokens(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "pulse-session-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
store := NewSessionStore(tmpDir)
|
||||
|
||||
token := "test-session-token-456"
|
||||
originalExpiry := time.Now().Add(1 * time.Hour)
|
||||
oidcInfo := &OIDCTokenInfo{
|
||||
RefreshToken: "original-refresh-token",
|
||||
AccessTokenExp: originalExpiry,
|
||||
Issuer: "https://example.com",
|
||||
ClientID: "test-client-id",
|
||||
}
|
||||
|
||||
store.CreateOIDCSession(token, 24*time.Hour, "TestAgent", "127.0.0.1", oidcInfo)
|
||||
|
||||
// Update the tokens (simulating a refresh)
|
||||
newExpiry := time.Now().Add(2 * time.Hour)
|
||||
store.UpdateOIDCTokens(token, "new-refresh-token", newExpiry)
|
||||
|
||||
// Verify tokens were updated
|
||||
session := store.GetSession(token)
|
||||
if session == nil {
|
||||
t.Fatal("GetSession returned nil for valid session")
|
||||
}
|
||||
|
||||
if session.OIDCRefreshToken != "new-refresh-token" {
|
||||
t.Errorf("RefreshToken should be updated: got %q, want %q", session.OIDCRefreshToken, "new-refresh-token")
|
||||
}
|
||||
|
||||
if !session.OIDCAccessTokenExp.After(originalExpiry) {
|
||||
t.Error("AccessTokenExp should be updated to new expiry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCSessionPersistence(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "pulse-session-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
token := "test-session-token-persist"
|
||||
oidcInfo := &OIDCTokenInfo{
|
||||
RefreshToken: "persist-refresh-token",
|
||||
AccessTokenExp: time.Now().Add(1 * time.Hour),
|
||||
Issuer: "https://example.com",
|
||||
ClientID: "test-client-id",
|
||||
}
|
||||
|
||||
// Create session in first store instance
|
||||
store1 := NewSessionStore(tmpDir)
|
||||
store1.CreateOIDCSession(token, 24*time.Hour, "TestAgent", "127.0.0.1", oidcInfo)
|
||||
|
||||
// Verify file was written
|
||||
sessionFile := filepath.Join(tmpDir, "sessions.json")
|
||||
if _, err := os.Stat(sessionFile); os.IsNotExist(err) {
|
||||
t.Fatal("Session file should exist after creating session")
|
||||
}
|
||||
|
||||
// Create new store instance (simulates restart)
|
||||
store2 := NewSessionStore(tmpDir)
|
||||
|
||||
// Verify session was loaded with OIDC info
|
||||
session := store2.GetSession(token)
|
||||
if session == nil {
|
||||
t.Fatal("Session should be restored after reload")
|
||||
}
|
||||
|
||||
if session.OIDCRefreshToken != oidcInfo.RefreshToken {
|
||||
t.Errorf("RefreshToken should persist: got %q, want %q", session.OIDCRefreshToken, oidcInfo.RefreshToken)
|
||||
}
|
||||
|
||||
if session.OIDCIssuer != oidcInfo.Issuer {
|
||||
t.Errorf("Issuer should persist: got %q, want %q", session.OIDCIssuer, oidcInfo.Issuer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOIDCSession_NilTokenInfo(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "pulse-session-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
store := NewSessionStore(tmpDir)
|
||||
|
||||
// Create OIDC session with nil token info (no refresh token available)
|
||||
token := "test-session-no-refresh"
|
||||
store.CreateOIDCSession(token, 24*time.Hour, "TestAgent", "127.0.0.1", nil)
|
||||
|
||||
// Verify session was created
|
||||
if !store.ValidateSession(token) {
|
||||
t.Error("Session should be valid even without OIDC tokens")
|
||||
}
|
||||
|
||||
session := store.GetSession(token)
|
||||
if session == nil {
|
||||
t.Fatal("GetSession returned nil")
|
||||
}
|
||||
|
||||
if session.OIDCRefreshToken != "" {
|
||||
t.Errorf("RefreshToken should be empty when nil info passed: got %q", session.OIDCRefreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidateSession(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "pulse-session-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
store := NewSessionStore(tmpDir)
|
||||
|
||||
token := "test-session-invalidate"
|
||||
oidcInfo := &OIDCTokenInfo{
|
||||
RefreshToken: "refresh-token",
|
||||
AccessTokenExp: time.Now().Add(1 * time.Hour),
|
||||
Issuer: "https://example.com",
|
||||
ClientID: "test-client-id",
|
||||
}
|
||||
|
||||
store.CreateOIDCSession(token, 24*time.Hour, "TestAgent", "127.0.0.1", oidcInfo)
|
||||
|
||||
// Verify session exists
|
||||
if !store.ValidateSession(token) {
|
||||
t.Error("Session should be valid before invalidation")
|
||||
}
|
||||
|
||||
// Invalidate the session
|
||||
store.InvalidateSession(token)
|
||||
|
||||
// Verify session no longer exists
|
||||
if store.ValidateSession(token) {
|
||||
t.Error("Session should be invalid after invalidation")
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,12 @@ type sessionPersisted struct {
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
OriginalDuration time.Duration `json:"original_duration,omitempty"`
|
||||
// OIDC token fields for refresh token support
|
||||
OIDCRefreshToken string `json:"oidc_refresh_token,omitempty"`
|
||||
OIDCAccessTokenExp time.Time `json:"oidc_access_token_exp,omitempty"`
|
||||
OIDCIssuer string `json:"oidc_issuer,omitempty"`
|
||||
OIDCClientID string `json:"oidc_client_id,omitempty"`
|
||||
OIDCTokenRefreshing bool `json:"-"` // transient, not persisted
|
||||
}
|
||||
|
||||
// SessionData represents a user session
|
||||
@@ -42,6 +48,12 @@ type SessionData struct {
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
OriginalDuration time.Duration `json:"original_duration,omitempty"` // Track original duration for sliding expiration
|
||||
// OIDC token fields for refresh token support
|
||||
OIDCRefreshToken string `json:"oidc_refresh_token,omitempty"` // Encrypted at rest
|
||||
OIDCAccessTokenExp time.Time `json:"oidc_access_token_exp,omitempty"` // When the access token expires
|
||||
OIDCIssuer string `json:"oidc_issuer,omitempty"` // IdP issuer URL
|
||||
OIDCClientID string `json:"oidc_client_id,omitempty"` // OIDC client ID
|
||||
OIDCTokenRefreshing bool `json:"-"` // Prevents concurrent refresh attempts
|
||||
}
|
||||
|
||||
// NewSessionStore creates a new persistent session store
|
||||
@@ -94,6 +106,96 @@ func (s *SessionStore) CreateSession(token string, duration time.Duration, userA
|
||||
s.saveUnsafe()
|
||||
}
|
||||
|
||||
// OIDCTokenInfo contains OAuth2 token information from the IdP
|
||||
type OIDCTokenInfo struct {
|
||||
RefreshToken string
|
||||
AccessTokenExp time.Time
|
||||
Issuer string
|
||||
ClientID string
|
||||
}
|
||||
|
||||
// CreateOIDCSession creates a new session with OIDC token information
|
||||
func (s *SessionStore) CreateOIDCSession(token string, duration time.Duration, userAgent, ip string, oidc *OIDCTokenInfo) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
key := sessionHash(token)
|
||||
session := &SessionData{
|
||||
ExpiresAt: time.Now().Add(duration),
|
||||
CreatedAt: time.Now(),
|
||||
UserAgent: userAgent,
|
||||
IP: ip,
|
||||
OriginalDuration: duration,
|
||||
}
|
||||
|
||||
if oidc != nil {
|
||||
session.OIDCRefreshToken = oidc.RefreshToken
|
||||
session.OIDCAccessTokenExp = oidc.AccessTokenExp
|
||||
session.OIDCIssuer = oidc.Issuer
|
||||
session.OIDCClientID = oidc.ClientID
|
||||
}
|
||||
|
||||
s.sessions[key] = session
|
||||
|
||||
// Save immediately for important operations
|
||||
s.saveUnsafe()
|
||||
}
|
||||
|
||||
// GetSession returns a copy of the session data for the given token
|
||||
func (s *SessionStore) GetSession(token string) *SessionData {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
session, exists := s.sessions[sessionHash(token)]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return a copy to avoid race conditions
|
||||
copy := *session
|
||||
return ©
|
||||
}
|
||||
|
||||
// UpdateOIDCTokens updates the OIDC tokens for a session after a successful refresh
|
||||
func (s *SessionStore) UpdateOIDCTokens(token string, refreshToken string, accessTokenExp time.Time) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
key := sessionHash(token)
|
||||
session, exists := s.sessions[key]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
session.OIDCRefreshToken = refreshToken
|
||||
session.OIDCAccessTokenExp = accessTokenExp
|
||||
session.OIDCTokenRefreshing = false
|
||||
|
||||
// Also extend the session expiry since the token is still valid
|
||||
if session.OriginalDuration > 0 {
|
||||
session.ExpiresAt = time.Now().Add(session.OriginalDuration)
|
||||
}
|
||||
|
||||
// Save immediately after token refresh
|
||||
s.saveUnsafe()
|
||||
}
|
||||
|
||||
// InvalidateSession removes a session (used when OIDC refresh fails)
|
||||
func (s *SessionStore) InvalidateSession(token string) {
|
||||
s.DeleteSession(token)
|
||||
}
|
||||
|
||||
// SetTokenRefreshing marks a session as currently refreshing tokens
|
||||
func (s *SessionStore) SetTokenRefreshing(token string, refreshing bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
key := sessionHash(token)
|
||||
if session, exists := s.sessions[key]; exists {
|
||||
session.OIDCTokenRefreshing = refreshing
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateSession checks if a session is valid
|
||||
func (s *SessionStore) ValidateSession(token string) bool {
|
||||
s.mu.RLock()
|
||||
@@ -176,12 +278,16 @@ func (s *SessionStore) saveUnsafe() {
|
||||
persisted := make([]sessionPersisted, 0, len(s.sessions))
|
||||
for key, session := range s.sessions {
|
||||
persisted = append(persisted, sessionPersisted{
|
||||
Key: key,
|
||||
ExpiresAt: session.ExpiresAt,
|
||||
CreatedAt: session.CreatedAt,
|
||||
UserAgent: session.UserAgent,
|
||||
IP: session.IP,
|
||||
OriginalDuration: session.OriginalDuration,
|
||||
Key: key,
|
||||
ExpiresAt: session.ExpiresAt,
|
||||
CreatedAt: session.CreatedAt,
|
||||
UserAgent: session.UserAgent,
|
||||
IP: session.IP,
|
||||
OriginalDuration: session.OriginalDuration,
|
||||
OIDCRefreshToken: session.OIDCRefreshToken,
|
||||
OIDCAccessTokenExp: session.OIDCAccessTokenExp,
|
||||
OIDCIssuer: session.OIDCIssuer,
|
||||
OIDCClientID: session.OIDCClientID,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -229,11 +335,15 @@ func (s *SessionStore) load() {
|
||||
continue
|
||||
}
|
||||
s.sessions[entry.Key] = &SessionData{
|
||||
ExpiresAt: entry.ExpiresAt,
|
||||
CreatedAt: entry.CreatedAt,
|
||||
UserAgent: entry.UserAgent,
|
||||
IP: entry.IP,
|
||||
OriginalDuration: entry.OriginalDuration,
|
||||
ExpiresAt: entry.ExpiresAt,
|
||||
CreatedAt: entry.CreatedAt,
|
||||
UserAgent: entry.UserAgent,
|
||||
IP: entry.IP,
|
||||
OriginalDuration: entry.OriginalDuration,
|
||||
OIDCRefreshToken: entry.OIDCRefreshToken,
|
||||
OIDCAccessTokenExp: entry.OIDCAccessTokenExp,
|
||||
OIDCIssuer: entry.OIDCIssuer,
|
||||
OIDCClientID: entry.OIDCClientID,
|
||||
}
|
||||
}
|
||||
log.Info().Int("loaded", len(s.sessions)).Int("total", len(persisted)).Msg("Sessions loaded from disk (hashed format)")
|
||||
|
||||
Reference in New Issue
Block a user