mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-12 04:04:26 +00:00
fix(refresh): honor userIdentifierClaim in token refresh path (#132)
patch-release
The refresh path in token_manager.go hardcoded the "email" claim when
extracting the user identifier from a refreshed ID token, ignoring the
configured userIdentifierClaim. Keycloak users without an email claim
(using sub or another identifier) were kicked out on refresh even
though their initial login worked.
The callback path (auth_flow.go:226-239) already honored
userIdentifierClaim with "sub" fallback; PR #100 (commit a316a98)
added that support but missed the refresh path.
Mirror the callback logic in refreshToken so both paths behave the same.
Cleanup: rename Get/SetEmail to Get/SetUserIdentifier on SessionData
to match the actual semantics. The slot already stored the configured
identifier (email, sub, oid, upn, preferred_username), only the API
name was misleading. Storage key "email" → "user_identifier" and
combinedSessionPayload field E (json:"e") → Ui (json:"ui").
Compat note: existing user sessions invalidate on upgrade — every active
user re-authenticates once after deploying this change.
This commit is contained in:
+1
-1
@@ -1491,7 +1491,7 @@ func TestAudienceEndToEndScenario(t *testing.T) {
|
|||||||
if err := session.SetAuthenticated(true); err != nil {
|
if err := session.SetAuthenticated(true); err != nil {
|
||||||
t.Fatalf("Failed to set authenticated: %v", err)
|
t.Fatalf("Failed to set authenticated: %v", err)
|
||||||
}
|
}
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetIDToken(validJWT)
|
session.SetIDToken(validJWT)
|
||||||
session.SetAccessToken(validJWT)
|
session.SetAccessToken(validJWT)
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -43,7 +43,7 @@ func (t *TraefikOidc) generatePKCEParameters() (string, string, error) {
|
|||||||
func (t *TraefikOidc) prepareSessionForAuthentication(session *SessionData, csrfToken, nonce, codeVerifier, incomingPath string) {
|
func (t *TraefikOidc) prepareSessionForAuthentication(session *SessionData, csrfToken, nonce, codeVerifier, incomingPath string) {
|
||||||
// Clear all existing session data
|
// Clear all existing session data
|
||||||
_ = session.SetAuthenticated(false) // Safe to ignore: clearing authentication state on new flow
|
_ = session.SetAuthenticated(false) // Safe to ignore: clearing authentication state on new flow
|
||||||
session.SetEmail("")
|
session.SetUserIdentifier("")
|
||||||
session.SetAccessToken("")
|
session.SetAccessToken("")
|
||||||
session.SetRefreshToken("")
|
session.SetRefreshToken("")
|
||||||
session.SetIDToken("")
|
session.SetIDToken("")
|
||||||
@@ -250,7 +250,7 @@ func (t *TraefikOidc) handleCallback(rw http.ResponseWriter, req *http.Request,
|
|||||||
t.sendErrorResponse(rw, req, "Failed to update session", http.StatusInternalServerError)
|
t.sendErrorResponse(rw, req, "Failed to update session", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session.SetEmail(userIdentifier) // SetEmail stores the user identifier (email or other claim)
|
session.SetUserIdentifier(userIdentifier)
|
||||||
session.SetIDToken(tokenResponse.IDToken)
|
session.SetIDToken(tokenResponse.IDToken)
|
||||||
session.SetAccessToken(tokenResponse.AccessToken)
|
session.SetAccessToken(tokenResponse.AccessToken)
|
||||||
session.SetRefreshToken(tokenResponse.RefreshToken)
|
session.SetRefreshToken(tokenResponse.RefreshToken)
|
||||||
@@ -290,7 +290,7 @@ func (t *TraefikOidc) handleExpiredToken(rw http.ResponseWriter, req *http.Reque
|
|||||||
session.SetIDToken("")
|
session.SetIDToken("")
|
||||||
session.SetAccessToken("")
|
session.SetAccessToken("")
|
||||||
session.SetRefreshToken("")
|
session.SetRefreshToken("")
|
||||||
session.SetEmail("")
|
session.SetUserIdentifier("")
|
||||||
// Clear CSRF tokens to prevent replay attacks
|
// Clear CSRF tokens to prevent replay attacks
|
||||||
session.SetCSRF("")
|
session.SetCSRF("")
|
||||||
session.SetNonce("")
|
session.SetNonce("")
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ func (s *AuthFlowBehaviourSuite) TestPrepareSessionForAuthentication() {
|
|||||||
|
|
||||||
// Pre-populate session with old data
|
// Pre-populate session with old data
|
||||||
_ = session.SetAuthenticated(true)
|
_ = session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAccessToken("old-access-token-with-many-characters")
|
session.SetAccessToken("old-access-token-with-many-characters")
|
||||||
session.SetRefreshToken("old-refresh-token-with-many-characters")
|
session.SetRefreshToken("old-refresh-token-with-many-characters")
|
||||||
session.SetIDToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.signature")
|
session.SetIDToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.signature")
|
||||||
@@ -207,7 +207,7 @@ func (s *AuthFlowBehaviourSuite) TestPrepareSessionForAuthentication() {
|
|||||||
|
|
||||||
// Verify old data is cleared
|
// Verify old data is cleared
|
||||||
s.False(session.GetAuthenticated())
|
s.False(session.GetAuthenticated())
|
||||||
s.Empty(session.GetEmail())
|
s.Empty(session.GetUserIdentifier())
|
||||||
|
|
||||||
// Verify new data is set
|
// Verify new data is set
|
||||||
s.Equal(csrfToken, session.GetCSRF())
|
s.Equal(csrfToken, session.GetCSRF())
|
||||||
@@ -711,7 +711,7 @@ func (s *AuthFlowBehaviourSuite) TestHandleExpiredToken() {
|
|||||||
session, err := sessionManager.GetSession(req)
|
session, err := sessionManager.GetSession(req)
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
_ = session.SetAuthenticated(true)
|
_ = session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetIDToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.signature")
|
session.SetIDToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.signature")
|
||||||
session.mainSession.Values["redirect_count"] = 3
|
session.mainSession.Values["redirect_count"] = 3
|
||||||
|
|
||||||
@@ -720,7 +720,7 @@ func (s *AuthFlowBehaviourSuite) TestHandleExpiredToken() {
|
|||||||
|
|
||||||
// Session should be cleared
|
// Session should be cleared
|
||||||
s.False(session.GetAuthenticated())
|
s.False(session.GetAuthenticated())
|
||||||
s.Empty(session.GetEmail())
|
s.Empty(session.GetUserIdentifier())
|
||||||
s.Empty(session.GetIDToken())
|
s.Empty(session.GetIDToken())
|
||||||
|
|
||||||
// Redirect count should be reset to 0 and then incremented by defaultInitiateAuthentication
|
// Redirect count should be reset to 0 and then incremented by defaultInitiateAuthentication
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func TestCSRFTokenSessionManagement(t *testing.T) {
|
|||||||
session.SetCSRF(csrfToken)
|
session.SetCSRF(csrfToken)
|
||||||
session.SetNonce("test-nonce")
|
session.SetNonce("test-nonce")
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAccessToken("old-access-token")
|
session.SetAccessToken("old-access-token")
|
||||||
session.SetRefreshToken("old-refresh-token")
|
session.SetRefreshToken("old-refresh-token")
|
||||||
session.SetIDToken("old-id-token")
|
session.SetIDToken("old-id-token")
|
||||||
@@ -61,7 +61,7 @@ func TestCSRFTokenSessionManagement(t *testing.T) {
|
|||||||
|
|
||||||
// Now perform selective clearing (as done in the fix)
|
// Now perform selective clearing (as done in the fix)
|
||||||
session2.SetAuthenticated(false)
|
session2.SetAuthenticated(false)
|
||||||
session2.SetEmail("")
|
session2.SetUserIdentifier("")
|
||||||
session2.SetAccessToken("")
|
session2.SetAccessToken("")
|
||||||
session2.SetRefreshToken("")
|
session2.SetRefreshToken("")
|
||||||
session2.SetIDToken("")
|
session2.SetIDToken("")
|
||||||
@@ -303,7 +303,7 @@ func TestRegressionLoginLoop(t *testing.T) {
|
|||||||
|
|
||||||
// Set initial session data
|
// Set initial session data
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAccessToken("old-token")
|
session.SetAccessToken("old-token")
|
||||||
session.SetCSRF("existing-csrf")
|
session.SetCSRF("existing-csrf")
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ func TestRegressionLoginLoop(t *testing.T) {
|
|||||||
// OLD BEHAVIOR: session.Clear() would have been called here, losing CSRF
|
// OLD BEHAVIOR: session.Clear() would have been called here, losing CSRF
|
||||||
// NEW BEHAVIOR: Selective clearing
|
// NEW BEHAVIOR: Selective clearing
|
||||||
session2.SetAuthenticated(false)
|
session2.SetAuthenticated(false)
|
||||||
session2.SetEmail("")
|
session2.SetUserIdentifier("")
|
||||||
session2.SetAccessToken("")
|
session2.SetAccessToken("")
|
||||||
session2.SetRefreshToken("")
|
session2.SetRefreshToken("")
|
||||||
session2.SetIDToken("")
|
session2.SetIDToken("")
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package traefikoidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestIssue132_RefreshTokenHonorsUserIdentifierClaim reproduces and verifies
|
||||||
|
// the fix for issue #132: token refresh path hardcoded the "email" claim and
|
||||||
|
// ignored the configured userIdentifierClaim. Keycloak users without an email
|
||||||
|
// claim (using sub or another identifier) were being kicked out on refresh
|
||||||
|
// even though their initial login worked.
|
||||||
|
//
|
||||||
|
// The callback path (auth_flow.go) already honored userIdentifierClaim with
|
||||||
|
// "sub" fallback. The refresh path (token_manager.go) had drifted out of sync
|
||||||
|
// after PR #100 (commit a316a98).
|
||||||
|
func TestIssue132_RefreshTokenHonorsUserIdentifierClaim(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
claims map[string]any
|
||||||
|
name string
|
||||||
|
userIdentifierClaim string
|
||||||
|
expectedIdentifier string
|
||||||
|
expectSuccess bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "sub claim configured, only sub present (Keycloak no-email case)",
|
||||||
|
userIdentifierClaim: "sub",
|
||||||
|
claims: map[string]any{
|
||||||
|
"sub": "user-uuid-keycloak-12345",
|
||||||
|
"exp": float64(9999999999),
|
||||||
|
},
|
||||||
|
expectSuccess: true,
|
||||||
|
expectedIdentifier: "user-uuid-keycloak-12345",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preferred_username configured, claim present",
|
||||||
|
userIdentifierClaim: "preferred_username",
|
||||||
|
claims: map[string]any{
|
||||||
|
"sub": "user-uuid-12345",
|
||||||
|
"preferred_username": "alice",
|
||||||
|
"exp": float64(9999999999),
|
||||||
|
},
|
||||||
|
expectSuccess: true,
|
||||||
|
expectedIdentifier: "alice",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "configured claim missing, falls back to sub",
|
||||||
|
userIdentifierClaim: "preferred_username",
|
||||||
|
claims: map[string]any{
|
||||||
|
"sub": "fallback-sub-id",
|
||||||
|
"exp": float64(9999999999),
|
||||||
|
},
|
||||||
|
expectSuccess: true,
|
||||||
|
expectedIdentifier: "fallback-sub-id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "email default, email present (backward compatibility)",
|
||||||
|
userIdentifierClaim: "email",
|
||||||
|
claims: map[string]any{
|
||||||
|
"sub": "user-uuid-12345",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"exp": float64(9999999999),
|
||||||
|
},
|
||||||
|
expectSuccess: true,
|
||||||
|
expectedIdentifier: "[email protected]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "email default, no email and no sub - refresh fails",
|
||||||
|
userIdentifierClaim: "email",
|
||||||
|
claims: map[string]any{
|
||||||
|
"exp": float64(9999999999),
|
||||||
|
},
|
||||||
|
expectSuccess: false,
|
||||||
|
expectedIdentifier: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
sessionManager, err := NewSessionManager(
|
||||||
|
"test-encryption-key-32-bytes-long!!",
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
NewLogger("error"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("session manager: %v", err)
|
||||||
|
}
|
||||||
|
defer sessionManager.Shutdown()
|
||||||
|
|
||||||
|
capturedClaims := tt.claims
|
||||||
|
tOidc := &TraefikOidc{
|
||||||
|
logger: NewLogger("error"),
|
||||||
|
userIdentifierClaim: tt.userIdentifierClaim,
|
||||||
|
sessionManager: sessionManager,
|
||||||
|
tokenExchanger: &EnhancedMockTokenExchanger{
|
||||||
|
RefreshResponse: &TokenResponse{
|
||||||
|
AccessToken: "new-access-token",
|
||||||
|
RefreshToken: "new-refresh-token",
|
||||||
|
IDToken: "new-id-token-jwt",
|
||||||
|
ExpiresIn: 3600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tokenVerifier: &EnhancedMockTokenVerifier{Err: nil},
|
||||||
|
extractClaimsFunc: func(token string) (map[string]any, error) {
|
||||||
|
return capturedClaims, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
|
||||||
|
session, err := sessionManager.GetSession(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get session: %v", err)
|
||||||
|
}
|
||||||
|
defer session.returnToPoolSafely()
|
||||||
|
|
||||||
|
session.SetRefreshToken("initial-refresh-token")
|
||||||
|
|
||||||
|
refreshed := tOidc.refreshToken(rw, req, session)
|
||||||
|
|
||||||
|
if refreshed != tt.expectSuccess {
|
||||||
|
t.Fatalf("refreshToken() = %v, want %v", refreshed, tt.expectSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := session.GetUserIdentifier(); got != tt.expectedIdentifier {
|
||||||
|
t.Errorf("session.GetUserIdentifier() = %q, want %q", got, tt.expectedIdentifier)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-19
@@ -138,7 +138,7 @@ func TestServeHTTP_EventStream(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test session: %v", err)
|
t.Fatalf("failed to create test session: %v", err)
|
||||||
}
|
}
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
if err := session.SetAuthenticated(true); err != nil {
|
if err := session.SetAuthenticated(true); err != nil {
|
||||||
t.Fatalf("failed to mark session authenticated: %v", err)
|
t.Fatalf("failed to mark session authenticated: %v", err)
|
||||||
}
|
}
|
||||||
@@ -221,7 +221,7 @@ func TestServeHTTP_WebSocketUpgrade(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test session: %v", err)
|
t.Fatalf("failed to create test session: %v", err)
|
||||||
}
|
}
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
if err := session.SetAuthenticated(true); err != nil {
|
if err := session.SetAuthenticated(true); err != nil {
|
||||||
t.Fatalf("failed to mark session authenticated: %v", err)
|
t.Fatalf("failed to mark session authenticated: %v", err)
|
||||||
}
|
}
|
||||||
@@ -408,7 +408,7 @@ func TestProcessAuthorizedRequest(t *testing.T) {
|
|||||||
name: "successful authorization with email",
|
name: "successful authorization with email",
|
||||||
setupSession: func() *MockSessionData {
|
setupSession: func() *MockSessionData {
|
||||||
session := &MockSessionData{
|
session := &MockSessionData{
|
||||||
email: "[email protected]",
|
userIdentifier: "[email protected]",
|
||||||
idToken: "test-id-token",
|
idToken: "test-id-token",
|
||||||
accessToken: "test-access-token",
|
accessToken: "test-access-token",
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
@@ -440,7 +440,7 @@ func TestProcessAuthorizedRequest(t *testing.T) {
|
|||||||
name: "no email triggers reauth",
|
name: "no email triggers reauth",
|
||||||
setupSession: func() *MockSessionData {
|
setupSession: func() *MockSessionData {
|
||||||
return &MockSessionData{
|
return &MockSessionData{
|
||||||
email: "",
|
userIdentifier: "",
|
||||||
idToken: "test-id-token",
|
idToken: "test-id-token",
|
||||||
accessToken: "test-access-token",
|
accessToken: "test-access-token",
|
||||||
}
|
}
|
||||||
@@ -461,7 +461,7 @@ func TestProcessAuthorizedRequest(t *testing.T) {
|
|||||||
name: "roles and groups authorization",
|
name: "roles and groups authorization",
|
||||||
setupSession: func() *MockSessionData {
|
setupSession: func() *MockSessionData {
|
||||||
return &MockSessionData{
|
return &MockSessionData{
|
||||||
email: "[email protected]",
|
userIdentifier: "[email protected]",
|
||||||
idToken: "test-id-token",
|
idToken: "test-id-token",
|
||||||
accessToken: "test-access-token",
|
accessToken: "test-access-token",
|
||||||
}
|
}
|
||||||
@@ -494,7 +494,7 @@ func TestProcessAuthorizedRequest(t *testing.T) {
|
|||||||
name: "unauthorized role/group returns 403",
|
name: "unauthorized role/group returns 403",
|
||||||
setupSession: func() *MockSessionData {
|
setupSession: func() *MockSessionData {
|
||||||
return &MockSessionData{
|
return &MockSessionData{
|
||||||
email: "[email protected]",
|
userIdentifier: "[email protected]",
|
||||||
idToken: "test-id-token",
|
idToken: "test-id-token",
|
||||||
accessToken: "test-access-token",
|
accessToken: "test-access-token",
|
||||||
}
|
}
|
||||||
@@ -521,7 +521,7 @@ func TestProcessAuthorizedRequest(t *testing.T) {
|
|||||||
name: "template headers processing",
|
name: "template headers processing",
|
||||||
setupSession: func() *MockSessionData {
|
setupSession: func() *MockSessionData {
|
||||||
return &MockSessionData{
|
return &MockSessionData{
|
||||||
email: "[email protected]",
|
userIdentifier: "[email protected]",
|
||||||
idToken: "test-id-token",
|
idToken: "test-id-token",
|
||||||
accessToken: "test-access-token",
|
accessToken: "test-access-token",
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
@@ -553,7 +553,7 @@ func TestProcessAuthorizedRequest(t *testing.T) {
|
|||||||
name: "OPTIONS request with CORS",
|
name: "OPTIONS request with CORS",
|
||||||
setupSession: func() *MockSessionData {
|
setupSession: func() *MockSessionData {
|
||||||
return &MockSessionData{
|
return &MockSessionData{
|
||||||
email: "[email protected]",
|
userIdentifier: "[email protected]",
|
||||||
idToken: "test-id-token",
|
idToken: "test-id-token",
|
||||||
accessToken: "test-access-token",
|
accessToken: "test-access-token",
|
||||||
}
|
}
|
||||||
@@ -604,7 +604,7 @@ func TestProcessAuthorizedRequest(t *testing.T) {
|
|||||||
manager: &SessionManager{logger: NewLogger("debug")},
|
manager: &SessionManager{logger: NewLogger("debug")},
|
||||||
}
|
}
|
||||||
// Copy values from mock to concrete session
|
// Copy values from mock to concrete session
|
||||||
concreteSession.SetEmail(session.email)
|
concreteSession.SetUserIdentifier(session.userIdentifier)
|
||||||
concreteSession.SetIDToken(session.idToken)
|
concreteSession.SetIDToken(session.idToken)
|
||||||
concreteSession.SetAccessToken(session.accessToken)
|
concreteSession.SetAccessToken(session.accessToken)
|
||||||
concreteSession.SetRefreshToken(session.refreshToken)
|
concreteSession.SetRefreshToken(session.refreshToken)
|
||||||
@@ -654,7 +654,7 @@ func TestProcessAuthorizedRequest(t *testing.T) {
|
|||||||
|
|
||||||
// MockSessionData is a test implementation of SessionData interface
|
// MockSessionData is a test implementation of SessionData interface
|
||||||
type MockSessionData struct {
|
type MockSessionData struct {
|
||||||
email string
|
userIdentifier string
|
||||||
idToken string
|
idToken string
|
||||||
accessToken string
|
accessToken string
|
||||||
refreshToken string
|
refreshToken string
|
||||||
@@ -666,11 +666,11 @@ type MockSessionData struct {
|
|||||||
isDirty bool
|
isDirty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockSessionData) GetEmail() string { return m.email }
|
func (m *MockSessionData) GetUserIdentifier() string { return m.userIdentifier }
|
||||||
func (m *MockSessionData) GetIDToken() string { return m.idToken }
|
func (m *MockSessionData) GetIDToken() string { return m.idToken }
|
||||||
func (m *MockSessionData) GetAccessToken() string { return m.accessToken }
|
func (m *MockSessionData) GetAccessToken() string { return m.accessToken }
|
||||||
func (m *MockSessionData) GetRefreshToken() string { return m.refreshToken }
|
func (m *MockSessionData) GetRefreshToken() string { return m.refreshToken }
|
||||||
func (m *MockSessionData) SetEmail(email string) { m.email = email }
|
func (m *MockSessionData) SetUserIdentifier(userIdentifier string) { m.userIdentifier = userIdentifier }
|
||||||
func (m *MockSessionData) SetIDToken(token string) { m.idToken = token }
|
func (m *MockSessionData) SetIDToken(token string) { m.idToken = token }
|
||||||
func (m *MockSessionData) SetAccessToken(token string) { m.accessToken = token }
|
func (m *MockSessionData) SetAccessToken(token string) { m.accessToken = token }
|
||||||
func (m *MockSessionData) SetRefreshToken(token string) { m.refreshToken = token }
|
func (m *MockSessionData) SetRefreshToken(token string) { m.refreshToken = token }
|
||||||
@@ -762,7 +762,7 @@ func TestMinimalHeaders(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up session data
|
// Set up session data
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
|
|
||||||
// Call processAuthorizedRequest directly
|
// Call processAuthorizedRequest directly
|
||||||
@@ -837,7 +837,7 @@ func TestMinimalHeaders_TokenHeaderNotSet(t *testing.T) {
|
|||||||
t.Fatalf("Failed to get session: %v", err)
|
t.Fatalf("Failed to get session: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
|
|
||||||
oidc.processAuthorizedRequest(rw, req, session, "https://example.com/callback")
|
oidc.processAuthorizedRequest(rw, req, session, "https://example.com/callback")
|
||||||
@@ -923,7 +923,7 @@ func TestStripAuthCookies(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to get session: %v", err)
|
t.Fatalf("Failed to get session: %v", err)
|
||||||
}
|
}
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
|
|
||||||
// Now add OIDC session cookies (simulating what the browser would send)
|
// Now add OIDC session cookies (simulating what the browser would send)
|
||||||
@@ -1004,7 +1004,7 @@ func TestStripAuthCookies_NoCookies(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to get session: %v", err)
|
t.Fatalf("Failed to get session: %v", err)
|
||||||
}
|
}
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
|
|
||||||
oidc.processAuthorizedRequest(rw, req, session, "https://example.com/callback")
|
oidc.processAuthorizedRequest(rw, req, session, "https://example.com/callback")
|
||||||
@@ -1051,7 +1051,7 @@ func TestStripAuthCookies_OnlyOIDCCookies(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to get session: %v", err)
|
t.Fatalf("Failed to get session: %v", err)
|
||||||
}
|
}
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
|
|
||||||
// Add only OIDC cookies
|
// Add only OIDC cookies
|
||||||
@@ -1102,7 +1102,7 @@ func TestStripAuthCookies_OnlyAppCookies(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to get session: %v", err)
|
t.Fatalf("Failed to get session: %v", err)
|
||||||
}
|
}
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
|
|
||||||
// Add only non-OIDC cookies
|
// Add only non-OIDC cookies
|
||||||
@@ -1165,7 +1165,7 @@ func TestStripAuthCookies_CustomPrefix(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to get session: %v", err)
|
t.Fatalf("Failed to get session: %v", err)
|
||||||
}
|
}
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
|
|
||||||
// Add cookies with the custom prefix (should be stripped)
|
// Add cookies with the custom prefix (should be stripped)
|
||||||
|
|||||||
+15
-15
@@ -580,7 +580,7 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
requestPath: "/protected",
|
requestPath: "/protected",
|
||||||
setupSession: func(session *SessionData) {
|
setupSession: func(session *SessionData) {
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
// Generate a fresh valid token for this test case to avoid replay issues
|
// Generate a fresh valid token for this test case to avoid replay issues
|
||||||
freshToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
freshToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(1 * time.Hour).Unix(),
|
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(1 * time.Hour).Unix(),
|
||||||
@@ -603,7 +603,7 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
// even if session.SetAuthenticated(true) was called.
|
// even if session.SetAuthenticated(true) was called.
|
||||||
// We rely on needsRefresh=true and the presence of the refresh token to trigger the refresh attempt.
|
// We rely on needsRefresh=true and the presence of the refresh token to trigger the refresh attempt.
|
||||||
session.SetAuthenticated(true) // Set flag initially, though isUserAuthenticated will override based on token
|
session.SetAuthenticated(true) // Set flag initially, though isUserAuthenticated will override based on token
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
// Create an expired token for this test
|
// Create an expired token for this test
|
||||||
expiredToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
expiredToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(-1 * time.Hour).Unix(),
|
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(-1 * time.Hour).Unix(),
|
||||||
@@ -660,7 +660,7 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
requestPath: "/callback/logout", // Match the default logout path set in TestSuite.Setup
|
requestPath: "/callback/logout", // Match the default logout path set in TestSuite.Setup
|
||||||
setupSession: func(session *SessionData) {
|
setupSession: func(session *SessionData) {
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
// Generate a fresh valid token for this test case
|
// Generate a fresh valid token for this test case
|
||||||
freshToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
freshToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(1 * time.Hour).Unix(),
|
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(1 * time.Hour).Unix(),
|
||||||
@@ -678,7 +678,7 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
requestPath: "/protected",
|
requestPath: "/protected",
|
||||||
setupSession: func(session *SessionData) {
|
setupSession: func(session *SessionData) {
|
||||||
session.SetAuthenticated(true) // Set flag initially
|
session.SetAuthenticated(true) // Set flag initially
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
// Create an expired token for this test
|
// Create an expired token for this test
|
||||||
expiredToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
expiredToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(-1 * time.Hour).Unix(),
|
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(-1 * time.Hour).Unix(),
|
||||||
@@ -706,7 +706,7 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
requestPath: "/protected",
|
requestPath: "/protected",
|
||||||
setupSession: func(session *SessionData) {
|
setupSession: func(session *SessionData) {
|
||||||
session.SetAuthenticated(true) // Set flag initially
|
session.SetAuthenticated(true) // Set flag initially
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
// Create an expired token for this test
|
// Create an expired token for this test
|
||||||
expiredToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
expiredToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(-1 * time.Hour).Unix(),
|
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(-1 * time.Hour).Unix(),
|
||||||
@@ -741,7 +741,7 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
"sub": "test-subject", "email": "[email protected]", "jti": generateRandomString(16),
|
"sub": "test-subject", "email": "[email protected]", "jti": generateRandomString(16),
|
||||||
})
|
})
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAccessToken(nearExpiryToken)
|
session.SetAccessToken(nearExpiryToken)
|
||||||
session.SetRefreshToken("valid-refresh-token-for-near-expiry") // Refresh token MUST exist for proactive refresh
|
session.SetRefreshToken("valid-refresh-token-for-near-expiry") // Refresh token MUST exist for proactive refresh
|
||||||
},
|
},
|
||||||
@@ -772,7 +772,7 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
"sub": "test-subject", "email": "[email protected]", "jti": generateRandomString(16),
|
"sub": "test-subject", "email": "[email protected]", "jti": generateRandomString(16),
|
||||||
})
|
})
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAccessToken(validToken)
|
session.SetAccessToken(validToken)
|
||||||
session.SetIDToken(validToken) // Ensure ID token is also set
|
session.SetIDToken(validToken) // Ensure ID token is also set
|
||||||
session.SetRefreshToken("should-not-be-used-refresh-token")
|
session.SetRefreshToken("should-not-be-used-refresh-token")
|
||||||
@@ -792,7 +792,7 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
requestPath: "/protected",
|
requestPath: "/protected",
|
||||||
setupSession: func(session *SessionData) {
|
setupSession: func(session *SessionData) {
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]") // Use disallowed domain
|
session.SetUserIdentifier("[email protected]") // Use disallowed domain
|
||||||
// Generate a fresh valid token for this test case
|
// Generate a fresh valid token for this test case
|
||||||
freshToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
freshToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(1 * time.Hour).Unix(),
|
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(1 * time.Hour).Unix(),
|
||||||
@@ -814,7 +814,7 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
requestPath: "/protected",
|
requestPath: "/protected",
|
||||||
setupSession: func(session *SessionData) {
|
setupSession: func(session *SessionData) {
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]") // Use disallowed domain
|
session.SetUserIdentifier("[email protected]") // Use disallowed domain
|
||||||
// Generate a fresh valid token for this test case
|
// Generate a fresh valid token for this test case
|
||||||
freshToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
freshToken, _ := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(1 * time.Hour).Unix(),
|
"iss": "https://test-issuer.com", "aud": "test-client-id", "exp": time.Now().Add(1 * time.Hour).Unix(),
|
||||||
@@ -2179,7 +2179,7 @@ func TestHandleExpiredToken(t *testing.T) {
|
|||||||
"sub": "test-subject", "email": "[email protected]", "jti": generateRandomString(16),
|
"sub": "test-subject", "email": "[email protected]", "jti": generateRandomString(16),
|
||||||
})
|
})
|
||||||
session.SetAccessToken(expiredToken)
|
session.SetAccessToken(expiredToken)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
},
|
},
|
||||||
expectedPath: "/original/path",
|
expectedPath: "/original/path",
|
||||||
},
|
},
|
||||||
@@ -2756,7 +2756,7 @@ func TestServeHTTPRolesAndGroups(t *testing.T) {
|
|||||||
},
|
},
|
||||||
setupSession: func(session *SessionData) {
|
setupSession: func(session *SessionData) {
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
expectedHeaders: map[string]string{
|
expectedHeaders: map[string]string{
|
||||||
@@ -2782,7 +2782,7 @@ func TestServeHTTPRolesAndGroups(t *testing.T) {
|
|||||||
},
|
},
|
||||||
setupSession: func(session *SessionData) {
|
setupSession: func(session *SessionData) {
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
expectedHeaders: map[string]string{
|
expectedHeaders: map[string]string{
|
||||||
@@ -2809,7 +2809,7 @@ func TestServeHTTPRolesAndGroups(t *testing.T) {
|
|||||||
},
|
},
|
||||||
setupSession: func(session *SessionData) {
|
setupSession: func(session *SessionData) {
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusForbidden,
|
expectedStatus: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
@@ -2829,7 +2829,7 @@ func TestServeHTTPRolesAndGroups(t *testing.T) {
|
|||||||
},
|
},
|
||||||
setupSession: func(session *SessionData) {
|
setupSession: func(session *SessionData) {
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
expectedHeaders: map[string]string{
|
expectedHeaders: map[string]string{
|
||||||
@@ -2851,7 +2851,7 @@ func TestServeHTTPRolesAndGroups(t *testing.T) {
|
|||||||
},
|
},
|
||||||
setupSession: func(session *SessionData) {
|
setupSession: func(session *SessionData) {
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
expectedHeaders: map[string]string{},
|
expectedHeaders: map[string]string{},
|
||||||
|
|||||||
+15
-15
@@ -92,17 +92,17 @@ func (t *TraefikOidc) applyBypassUserHeaders(req *http.Request, reason string) b
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
email := session.GetEmail()
|
userIdentifier := session.GetUserIdentifier()
|
||||||
if email == "" {
|
if userIdentifier == "" {
|
||||||
t.logger.Debugf("%s bypass: rejecting request, session has no user identifier", reason)
|
t.logger.Debugf("%s bypass: rejecting request, session has no user identifier", reason)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("X-Forwarded-User", email)
|
req.Header.Set("X-Forwarded-User", userIdentifier)
|
||||||
if !t.minimalHeaders {
|
if !t.minimalHeaders {
|
||||||
req.Header.Set("X-Auth-Request-User", email)
|
req.Header.Set("X-Auth-Request-User", userIdentifier)
|
||||||
}
|
}
|
||||||
t.logger.Debugf("%s bypass: forwarded user %s from session", reason, email)
|
t.logger.Debugf("%s bypass: forwarded user %s from session", reason, userIdentifier)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userIdentifier := session.GetEmail() // GetEmail returns the stored user identifier (email or other claim)
|
userIdentifier := session.GetUserIdentifier()
|
||||||
// User authorization check
|
// User authorization check
|
||||||
if authenticated && userIdentifier != "" {
|
if authenticated && userIdentifier != "" {
|
||||||
if !t.isAllowedUser(userIdentifier) {
|
if !t.isAllowedUser(userIdentifier) {
|
||||||
@@ -361,7 +361,7 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
refreshed := t.refreshToken(rw, req, session)
|
refreshed := t.refreshToken(rw, req, session)
|
||||||
if refreshed {
|
if refreshed {
|
||||||
userIdentifier = session.GetEmail() // GetEmail returns the stored user identifier
|
userIdentifier = session.GetUserIdentifier()
|
||||||
if userIdentifier != "" && !t.isAllowedUser(userIdentifier) {
|
if userIdentifier != "" && !t.isAllowedUser(userIdentifier) {
|
||||||
t.logger.Infof("User with refreshed token %s is not authorized", userIdentifier)
|
t.logger.Infof("User with refreshed token %s is not authorized", userIdentifier)
|
||||||
errorMsg := fmt.Sprintf("Access denied: You are not authorized to access this resource. To log out, visit: %s", t.logoutURLPath)
|
errorMsg := fmt.Sprintf("Access denied: You are not authorized to access this resource. To log out, visit: %s", t.logoutURLPath)
|
||||||
@@ -411,9 +411,9 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
// - session: The user's session data containing tokens and claims.
|
// - session: The user's session data containing tokens and claims.
|
||||||
// - redirectURL: The callback URL for re-authentication if needed.
|
// - redirectURL: The callback URL for re-authentication if needed.
|
||||||
func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http.Request, session *SessionData, redirectURL string) {
|
func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http.Request, session *SessionData, redirectURL string) {
|
||||||
email := session.GetEmail()
|
userIdentifier := session.GetUserIdentifier()
|
||||||
if email == "" {
|
if userIdentifier == "" {
|
||||||
t.logger.Info("No email found in session during final processing, initiating re-auth")
|
t.logger.Info("No user identifier found in session during final processing, initiating re-auth")
|
||||||
// Reset redirect count to prevent loops when session is invalid
|
// Reset redirect count to prevent loops when session is invalid
|
||||||
session.ResetRedirectCount()
|
session.ResetRedirectCount()
|
||||||
t.defaultInitiateAuthentication(rw, req, session, redirectURL)
|
t.defaultInitiateAuthentication(rw, req, session, redirectURL)
|
||||||
@@ -426,7 +426,7 @@ func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http
|
|||||||
if idToken != "" {
|
if idToken != "" {
|
||||||
sid, sub, createdAt := t.extractSessionInfo(idToken)
|
sid, sub, createdAt := t.extractSessionInfo(idToken)
|
||||||
if t.isSessionInvalidated(sid, sub, createdAt) {
|
if t.isSessionInvalidated(sid, sub, createdAt) {
|
||||||
t.logger.Infof("Session for user %s has been invalidated via IdP-initiated logout", email)
|
t.logger.Infof("Session for user %s has been invalidated via IdP-initiated logout", userIdentifier)
|
||||||
// Clear the session and redirect to login
|
// Clear the session and redirect to login
|
||||||
if err := session.Clear(req, rw); err != nil {
|
if err := session.Clear(req, rw); err != nil {
|
||||||
t.logger.Errorf("Error clearing invalidated session: %v", err)
|
t.logger.Errorf("Error clearing invalidated session: %v", err)
|
||||||
@@ -502,19 +502,19 @@ func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !allowed {
|
if !allowed {
|
||||||
t.logger.Infof("User with email %s does not have any allowed roles or groups", email)
|
t.logger.Infof("User %s does not have any allowed roles or groups", userIdentifier)
|
||||||
errorMsg := fmt.Sprintf("Access denied: You do not have any of the allowed roles or groups. To log out, visit: %s", t.logoutURLPath)
|
errorMsg := fmt.Sprintf("Access denied: You do not have any of the allowed roles or groups. To log out, visit: %s", t.logoutURLPath)
|
||||||
t.sendErrorResponse(rw, req, errorMsg, http.StatusForbidden)
|
t.sendErrorResponse(rw, req, errorMsg, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("X-Forwarded-User", email)
|
req.Header.Set("X-Forwarded-User", userIdentifier)
|
||||||
|
|
||||||
// When minimalHeaders is enabled, skip extra headers to prevent 431 errors
|
// When minimalHeaders is enabled, skip extra headers to prevent 431 errors
|
||||||
if !t.minimalHeaders {
|
if !t.minimalHeaders {
|
||||||
req.Header.Set("X-Auth-Request-Redirect", req.URL.RequestURI())
|
req.Header.Set("X-Auth-Request-Redirect", req.URL.RequestURI())
|
||||||
req.Header.Set("X-Auth-Request-User", email)
|
req.Header.Set("X-Auth-Request-User", userIdentifier)
|
||||||
if idToken != "" {
|
if idToken != "" {
|
||||||
req.Header.Set("X-Auth-Request-Token", idToken)
|
req.Header.Set("X-Auth-Request-Token", idToken)
|
||||||
}
|
}
|
||||||
@@ -587,7 +587,7 @@ func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t.logger.Debugf("Request authorized for user %s, forwarding to next handler", email)
|
t.logger.Debugf("Request authorized for user %s, forwarding to next handler", userIdentifier)
|
||||||
|
|
||||||
t.next.ServeHTTP(rw, req)
|
t.next.ServeHTTP(rw, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ func TestMiddlewareDomainRestrictions(t *testing.T) {
|
|||||||
// Create authenticated session
|
// Create authenticated session
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||||
session, _ := sessionManager.GetSession(req)
|
session, _ := sessionManager.GetSession(req)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetIDToken("dummy-token")
|
session.SetIDToken("dummy-token")
|
||||||
session.Save(req, httptest.NewRecorder())
|
session.Save(req, httptest.NewRecorder())
|
||||||
@@ -203,7 +203,7 @@ func TestMiddlewareDomainRestrictions(t *testing.T) {
|
|||||||
// Create session with forbidden domain
|
// Create session with forbidden domain
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||||
session, _ := sessionManager.GetSession(req)
|
session, _ := sessionManager.GetSession(req)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
|
|
||||||
// Save and inject cookies
|
// Save and inject cookies
|
||||||
@@ -252,7 +252,7 @@ func TestMiddlewareOpaqueTokenHandling(t *testing.T) {
|
|||||||
// Create session with opaque token
|
// Create session with opaque token
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||||
session, _ := sessionManager.GetSession(req)
|
session, _ := sessionManager.GetSession(req)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetAccessToken("sk_live_abcdefghijklmnopqrstuvwxyz") // Opaque token (no dots)
|
session.SetAccessToken("sk_live_abcdefghijklmnopqrstuvwxyz") // Opaque token (no dots)
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
|
|
||||||
@@ -291,7 +291,7 @@ func TestMiddlewareProcessAuthorizedRequestEdgeCases(t *testing.T) {
|
|||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||||
session, _ := sessionManager.GetSession(req)
|
session, _ := sessionManager.GetSession(req)
|
||||||
session.SetEmail("") // No email
|
session.SetUserIdentifier("") // No email
|
||||||
session.SetIDToken("dummy-token")
|
session.SetIDToken("dummy-token")
|
||||||
|
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
@@ -321,7 +321,7 @@ func TestMiddlewareProcessAuthorizedRequestEdgeCases(t *testing.T) {
|
|||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||||
session, _ := sessionManager.GetSession(req)
|
session, _ := sessionManager.GetSession(req)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetIDToken("") // No ID token
|
session.SetIDToken("") // No ID token
|
||||||
session.SetAccessToken("") // No access token
|
session.SetAccessToken("") // No access token
|
||||||
|
|
||||||
@@ -349,7 +349,7 @@ func TestMiddlewareProcessAuthorizedRequestEdgeCases(t *testing.T) {
|
|||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||||
session, _ := sessionManager.GetSession(req)
|
session, _ := sessionManager.GetSession(req)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetIDToken("dummy-token")
|
session.SetIDToken("dummy-token")
|
||||||
|
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
@@ -383,7 +383,7 @@ func TestMiddlewareProcessAuthorizedRequestEdgeCases(t *testing.T) {
|
|||||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||||
session, _ := sessionManager.GetSession(req)
|
session, _ := sessionManager.GetSession(req)
|
||||||
testEmail := "[email protected]"
|
testEmail := "[email protected]"
|
||||||
session.SetEmail(testEmail)
|
session.SetUserIdentifier(testEmail)
|
||||||
session.SetIDToken("dummy-id-token")
|
session.SetIDToken("dummy-id-token")
|
||||||
|
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func testIssue53ReverseProxyHTTPS(t *testing.T) {
|
|||||||
|
|
||||||
// Simulate successful Azure authentication
|
// Simulate successful Azure authentication
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
// Azure may use opaque access tokens
|
// Azure may use opaque access tokens
|
||||||
session.SetAccessToken("opaque-azure-access-token")
|
session.SetAccessToken("opaque-azure-access-token")
|
||||||
session.SetIDToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ") // trufflehog:ignore
|
session.SetIDToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ") // trufflehog:ignore
|
||||||
@@ -152,7 +152,7 @@ func testIssue53ReverseProxyHTTPS(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, session2.GetAuthenticated(), "User should remain authenticated")
|
assert.True(t, session2.GetAuthenticated(), "User should remain authenticated")
|
||||||
assert.Equal(t, "[email protected]", session2.GetEmail())
|
assert.Equal(t, "[email protected]", session2.GetUserIdentifier())
|
||||||
assert.NotEmpty(t, session2.GetAccessToken(), "Access token should persist")
|
assert.NotEmpty(t, session2.GetAccessToken(), "Access token should persist")
|
||||||
assert.NotEmpty(t, session2.GetIDToken(), "ID token should persist")
|
assert.NotEmpty(t, session2.GetIDToken(), "ID token should persist")
|
||||||
assert.NotEmpty(t, session2.GetRefreshToken(), "Refresh token should persist")
|
assert.NotEmpty(t, session2.GetRefreshToken(), "Refresh token should persist")
|
||||||
|
|||||||
@@ -485,7 +485,7 @@ func TestSessionFixationAttack(t *testing.T) {
|
|||||||
|
|
||||||
// Set up the attacker's session with malicious data
|
// Set up the attacker's session with malicious data
|
||||||
attackerSession.SetAuthenticated(true)
|
attackerSession.SetAuthenticated(true)
|
||||||
attackerSession.SetEmail("[email protected]")
|
attackerSession.SetUserIdentifier("[email protected]")
|
||||||
attackerSession.SetIDToken(ValidIDToken)
|
attackerSession.SetIDToken(ValidIDToken)
|
||||||
attackerSession.SetAccessToken(ValidAccessToken)
|
attackerSession.SetAccessToken(ValidAccessToken)
|
||||||
|
|
||||||
@@ -512,7 +512,7 @@ func TestSessionFixationAttack(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the email from the session
|
// Get the email from the session
|
||||||
email := session.GetEmail()
|
email := session.GetUserIdentifier()
|
||||||
w.Header().Set("X-User-Email", email)
|
w.Header().Set("X-User-Email", email)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|||||||
+22
-22
@@ -100,7 +100,7 @@ type combinedSessionPayload struct {
|
|||||||
A string `json:"a,omitempty"`
|
A string `json:"a,omitempty"`
|
||||||
R string `json:"r,omitempty"`
|
R string `json:"r,omitempty"`
|
||||||
I string `json:"i,omitempty"`
|
I string `json:"i,omitempty"`
|
||||||
E string `json:"e,omitempty"`
|
Ui string `json:"ui,omitempty"`
|
||||||
Cs string `json:"cs,omitempty"`
|
Cs string `json:"cs,omitempty"`
|
||||||
N string `json:"n,omitempty"`
|
N string `json:"n,omitempty"`
|
||||||
Cv string `json:"cv,omitempty"`
|
Cv string `json:"cv,omitempty"`
|
||||||
@@ -116,7 +116,7 @@ var knownSessionKeys = map[string]bool{
|
|||||||
"access_token": true,
|
"access_token": true,
|
||||||
"refresh_token": true,
|
"refresh_token": true,
|
||||||
"id_token": true,
|
"id_token": true,
|
||||||
"email": true,
|
"user_identifier": true,
|
||||||
"authenticated": true,
|
"authenticated": true,
|
||||||
"csrf": true,
|
"csrf": true,
|
||||||
"nonce": true,
|
"nonce": true,
|
||||||
@@ -1134,7 +1134,7 @@ func (sm *SessionManager) loadFromCombinedCookies(r *http.Request, sessionData *
|
|||||||
sessionData.idTokenSession, _ = sm.store.Get(r, sm.idTokenCookieName())
|
sessionData.idTokenSession, _ = sm.store.Get(r, sm.idTokenCookieName())
|
||||||
|
|
||||||
// Populate legacy session values from combined payload
|
// Populate legacy session values from combined payload
|
||||||
sessionData.mainSession.Values["email"] = payload.E
|
sessionData.mainSession.Values["user_identifier"] = payload.Ui
|
||||||
sessionData.mainSession.Values["authenticated"] = payload.Au
|
sessionData.mainSession.Values["authenticated"] = payload.Au
|
||||||
sessionData.mainSession.Values["csrf"] = payload.Cs
|
sessionData.mainSession.Values["csrf"] = payload.Cs
|
||||||
sessionData.mainSession.Values["nonce"] = payload.N
|
sessionData.mainSession.Values["nonce"] = payload.N
|
||||||
@@ -1278,7 +1278,7 @@ func (sd *SessionData) saveCombined(r *http.Request, w http.ResponseWriter, opti
|
|||||||
A: sd.getAccessTokenUnsafe(),
|
A: sd.getAccessTokenUnsafe(),
|
||||||
R: sd.getRefreshTokenUnsafe(),
|
R: sd.getRefreshTokenUnsafe(),
|
||||||
I: sd.getIDTokenUnsafe(),
|
I: sd.getIDTokenUnsafe(),
|
||||||
E: sd.getEmailUnsafe(),
|
Ui: sd.getUserIdentifierUnsafe(),
|
||||||
Au: sd.getAuthenticatedUnsafe(),
|
Au: sd.getAuthenticatedUnsafe(),
|
||||||
Cs: sd.getCSRFUnsafe(),
|
Cs: sd.getCSRFUnsafe(),
|
||||||
N: sd.getNonceUnsafe(),
|
N: sd.getNonceUnsafe(),
|
||||||
@@ -2469,30 +2469,30 @@ func (sd *SessionData) SetCodeVerifier(codeVerifier string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEmail retrieves the authenticated user's email address.
|
// GetUserIdentifier retrieves the authenticated user's identifier as extracted
|
||||||
// The email is extracted from ID token claims and used for
|
// from the configured userIdentifierClaim of the ID token (email, sub, oid,
|
||||||
// authorization decisions and header injection.
|
// upn, preferred_username, etc.). The value is used for authorization
|
||||||
|
// decisions and header injection.
|
||||||
// Returns:
|
// Returns:
|
||||||
// - The user's email address string, or an empty string if not set.
|
// - The user identifier string, or an empty string if not set.
|
||||||
func (sd *SessionData) GetEmail() string {
|
func (sd *SessionData) GetUserIdentifier() string {
|
||||||
sd.sessionMutex.RLock()
|
sd.sessionMutex.RLock()
|
||||||
defer sd.sessionMutex.RUnlock()
|
defer sd.sessionMutex.RUnlock()
|
||||||
|
|
||||||
email, _ := sd.mainSession.Values["email"].(string)
|
userIdentifier, _ := sd.mainSession.Values["user_identifier"].(string)
|
||||||
return email
|
return userIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetEmail stores the authenticated user's email address.
|
// SetUserIdentifier stores the authenticated user's identifier value.
|
||||||
// The email is typically extracted from the 'email' claim in the ID token.
|
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - email: The user's email address to store.
|
// - userIdentifier: The user identifier to store (email, sub, or other claim value).
|
||||||
func (sd *SessionData) SetEmail(email string) {
|
func (sd *SessionData) SetUserIdentifier(userIdentifier string) {
|
||||||
sd.sessionMutex.Lock()
|
sd.sessionMutex.Lock()
|
||||||
defer sd.sessionMutex.Unlock()
|
defer sd.sessionMutex.Unlock()
|
||||||
|
|
||||||
currentVal, _ := sd.mainSession.Values["email"].(string)
|
currentVal, _ := sd.mainSession.Values["user_identifier"].(string)
|
||||||
if currentVal != email {
|
if currentVal != userIdentifier {
|
||||||
sd.mainSession.Values["email"] = email
|
sd.mainSession.Values["user_identifier"] = userIdentifier
|
||||||
sd.dirty = true
|
sd.dirty = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2626,10 +2626,10 @@ func (sd *SessionData) getRefreshTokenUnsafe() string {
|
|||||||
return result.Token
|
return result.Token
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEmailUnsafe retrieves the email without acquiring locks.
|
// getUserIdentifierUnsafe retrieves the user identifier without acquiring locks.
|
||||||
func (sd *SessionData) getEmailUnsafe() string {
|
func (sd *SessionData) getUserIdentifierUnsafe() string {
|
||||||
email, _ := sd.mainSession.Values["email"].(string)
|
userIdentifier, _ := sd.mainSession.Values["user_identifier"].(string)
|
||||||
return email
|
return userIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCSRFUnsafe retrieves the CSRF token without acquiring locks.
|
// getCSRFUnsafe retrieves the CSRF token without acquiring locks.
|
||||||
|
|||||||
@@ -320,17 +320,16 @@ func (s *SessionBehaviourSuite) TestSessionData_DirtyTracking() {
|
|||||||
s.False(session.IsDirty())
|
s.False(session.IsDirty())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSessionData_SetEmail tests email setter with dirty tracking
|
// TestSessionData_SetUserIdentifier tests user identifier setter with dirty tracking
|
||||||
func (s *SessionBehaviourSuite) TestSessionData_SetEmail() {
|
func (s *SessionBehaviourSuite) TestSessionData_SetUserIdentifier() {
|
||||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
|
||||||
session, err := s.sessionManager.GetSession(req)
|
session, err := s.sessionManager.GetSession(req)
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
defer session.returnToPoolSafely()
|
defer session.returnToPoolSafely()
|
||||||
|
|
||||||
// Set email
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetEmail("[email protected]")
|
s.Equal("[email protected]", session.GetUserIdentifier())
|
||||||
s.Equal("[email protected]", session.GetEmail())
|
|
||||||
s.True(session.IsDirty())
|
s.True(session.IsDirty())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,7 +567,7 @@ func (s *SessionBehaviourSuite) TestSessionData_Clear() {
|
|||||||
// Set some data
|
// Set some data
|
||||||
err = session.SetAuthenticated(true)
|
err = session.SetAuthenticated(true)
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.SetCSRF("csrf-token")
|
session.SetCSRF("csrf-token")
|
||||||
|
|
||||||
// Clear session
|
// Clear session
|
||||||
@@ -588,7 +587,7 @@ func (s *SessionBehaviourSuite) TestSessionData_Save() {
|
|||||||
defer session.returnToPoolSafely()
|
defer session.returnToPoolSafely()
|
||||||
|
|
||||||
// Modify session
|
// Modify session
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
s.True(session.IsDirty())
|
s.True(session.IsDirty())
|
||||||
|
|
||||||
// Save session
|
// Save session
|
||||||
|
|||||||
+6
-6
@@ -2688,7 +2688,7 @@ func TestSessionStatePreservationWithExpiredTokens(t *testing.T) {
|
|||||||
|
|
||||||
// Set up initial session state (what user has when first logging in)
|
// Set up initial session state (what user has when first logging in)
|
||||||
session1.SetAuthenticated(true)
|
session1.SetAuthenticated(true)
|
||||||
session1.SetEmail(originalUserData["email"].(string))
|
session1.SetUserIdentifier(originalUserData["email"].(string))
|
||||||
session1.SetAccessToken("initial-valid-access-token-longer-than-20-chars")
|
session1.SetAccessToken("initial-valid-access-token-longer-than-20-chars")
|
||||||
session1.SetIDToken("initial-valid-id-token-longer-than-20-chars")
|
session1.SetIDToken("initial-valid-id-token-longer-than-20-chars")
|
||||||
session1.SetRefreshToken("valid-refresh-token-should-last-30-days")
|
session1.SetRefreshToken("valid-refresh-token-should-last-30-days")
|
||||||
@@ -2732,7 +2732,7 @@ func TestSessionStatePreservationWithExpiredTokens(t *testing.T) {
|
|||||||
// Simulate what happens when middleware detects expired tokens
|
// Simulate what happens when middleware detects expired tokens
|
||||||
// It should preserve session state while attempting token refresh
|
// It should preserve session state while attempting token refresh
|
||||||
originalAuth := session2.GetAuthenticated()
|
originalAuth := session2.GetAuthenticated()
|
||||||
originalEmail := session2.GetEmail()
|
originalEmail := session2.GetUserIdentifier()
|
||||||
|
|
||||||
// Reconstruct user data from individual stored keys
|
// Reconstruct user data from individual stored keys
|
||||||
originalUserDataStored := make(map[string]interface{})
|
originalUserDataStored := make(map[string]interface{})
|
||||||
@@ -2813,7 +2813,7 @@ func TestSessionStatePreservationWithExpiredTokens(t *testing.T) {
|
|||||||
|
|
||||||
// Verify all session data is still intact after token refresh
|
// Verify all session data is still intact after token refresh
|
||||||
postRefreshAuth := session2.GetAuthenticated()
|
postRefreshAuth := session2.GetAuthenticated()
|
||||||
postRefreshEmail := session2.GetEmail()
|
postRefreshEmail := session2.GetUserIdentifier()
|
||||||
userDataPresent := true
|
userDataPresent := true
|
||||||
for k := range originalUserData {
|
for k := range originalUserData {
|
||||||
if session2.mainSession.Values["user_data_"+k] == nil {
|
if session2.mainSession.Values["user_data_"+k] == nil {
|
||||||
@@ -2907,7 +2907,7 @@ func TestSessionExpiryVsTokenExpiry(t *testing.T) {
|
|||||||
|
|
||||||
// Set up session with specific creation time
|
// Set up session with specific creation time
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
session.mainSession.Values["created_at"] = sessionCreatedAt.Unix()
|
session.mainSession.Values["created_at"] = sessionCreatedAt.Unix()
|
||||||
|
|
||||||
// Create tokens with specific expiry
|
// Create tokens with specific expiry
|
||||||
@@ -3018,7 +3018,7 @@ func TestSessionCleanupOnTokenExpiry(t *testing.T) {
|
|||||||
|
|
||||||
// Set up session with data that should be preserved or removed
|
// Set up session with data that should be preserved or removed
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetUserIdentifier("[email protected]")
|
||||||
|
|
||||||
session.mainSession.Values["user_data"] = "Test User|user-123"
|
session.mainSession.Values["user_data"] = "Test User|user-123"
|
||||||
session.mainSession.Values["preferences"] = "theme:dark,lang:en"
|
session.mainSession.Values["preferences"] = "theme:dark,lang:en"
|
||||||
@@ -3049,7 +3049,7 @@ func TestSessionCleanupOnTokenExpiry(t *testing.T) {
|
|||||||
if scenario.shouldCleanup {
|
if scenario.shouldCleanup {
|
||||||
if sessionTooOld {
|
if sessionTooOld {
|
||||||
session.SetAuthenticated(false)
|
session.SetAuthenticated(false)
|
||||||
session.SetEmail("")
|
session.SetUserIdentifier("")
|
||||||
session.SetAccessToken("")
|
session.SetAccessToken("")
|
||||||
session.SetRefreshToken("")
|
session.SetRefreshToken("")
|
||||||
for key := range session.mainSession.Values {
|
for key := range session.mainSession.Values {
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ func (tf *TestFramework) CreateAuthenticatedRequest(method, path string) (*http.
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail(tf.fixtures.UserEmail)
|
session.SetUserIdentifier(tf.fixtures.UserEmail)
|
||||||
session.SetAccessToken(tf.fixtures.AccessToken)
|
session.SetAccessToken(tf.fixtures.AccessToken)
|
||||||
session.SetRefreshToken(tf.fixtures.RefreshToken)
|
session.SetRefreshToken(tf.fixtures.RefreshToken)
|
||||||
session.SetIDToken(tf.GenerateJWT(tf.fixtures.Claims))
|
session.SetIDToken(tf.GenerateJWT(tf.fixtures.Claims))
|
||||||
|
|||||||
+12
-6
@@ -434,7 +434,7 @@ func (t *TraefikOidc) refreshToken(rw http.ResponseWriter, req *http.Request, se
|
|||||||
session.SetRefreshToken("")
|
session.SetRefreshToken("")
|
||||||
session.SetAccessToken("")
|
session.SetAccessToken("")
|
||||||
session.SetIDToken("")
|
session.SetIDToken("")
|
||||||
session.SetEmail("")
|
session.SetUserIdentifier("")
|
||||||
// Clear CSRF tokens as well to prevent any replay attacks
|
// Clear CSRF tokens as well to prevent any replay attacks
|
||||||
session.SetCSRF("")
|
session.SetCSRF("")
|
||||||
session.SetNonce("")
|
session.SetNonce("")
|
||||||
@@ -476,12 +476,18 @@ func (t *TraefikOidc) refreshToken(rw http.ResponseWriter, req *http.Request, se
|
|||||||
t.logger.Errorf("refreshToken failed: Failed to extract claims from refreshed token: %v", err)
|
t.logger.Errorf("refreshToken failed: Failed to extract claims from refreshed token: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
email, _ := claims["email"].(string)
|
userIdentifier, _ := claims[t.userIdentifierClaim].(string)
|
||||||
if email == "" {
|
if userIdentifier == "" {
|
||||||
t.logger.Errorf("refreshToken failed: Email claim missing or empty in refreshed token")
|
if t.userIdentifierClaim != "sub" {
|
||||||
|
userIdentifier, _ = claims["sub"].(string)
|
||||||
|
}
|
||||||
|
if userIdentifier == "" {
|
||||||
|
t.logger.Errorf("refreshToken failed: User identifier claim '%s' missing or empty in refreshed token", t.userIdentifierClaim)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
session.SetEmail(email)
|
t.logger.Debugf("Configured claim '%s' not found in refreshed token, using 'sub' claim as fallback", t.userIdentifierClaim)
|
||||||
|
}
|
||||||
|
session.SetUserIdentifier(userIdentifier)
|
||||||
|
|
||||||
// Get token expiry information for logging
|
// Get token expiry information for logging
|
||||||
var expiryTime time.Time
|
var expiryTime time.Time
|
||||||
@@ -507,7 +513,7 @@ func (t *TraefikOidc) refreshToken(rw http.ResponseWriter, req *http.Request, se
|
|||||||
session.SetAccessToken("")
|
session.SetAccessToken("")
|
||||||
session.SetIDToken("")
|
session.SetIDToken("")
|
||||||
session.SetRefreshToken("")
|
session.SetRefreshToken("")
|
||||||
session.SetEmail("")
|
session.SetUserIdentifier("")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user