mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
454 lines
13 KiB
Go
454 lines
13 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestPostLogoutRedirectURIConfiguration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
postLogoutRedirectURI string
|
|
expectedValue string
|
|
expectDefault bool
|
|
}{
|
|
{
|
|
name: "custom post logout redirect URI",
|
|
postLogoutRedirectURI: "/home",
|
|
expectDefault: false,
|
|
expectedValue: "/home",
|
|
},
|
|
{
|
|
name: "empty uses default",
|
|
postLogoutRedirectURI: "",
|
|
expectDefault: true,
|
|
expectedValue: "/",
|
|
},
|
|
{
|
|
name: "external URL allowed",
|
|
postLogoutRedirectURI: "https://example.com/goodbye",
|
|
expectDefault: false,
|
|
expectedValue: "https://example.com/goodbye",
|
|
},
|
|
{
|
|
name: "relative path with query",
|
|
postLogoutRedirectURI: "/logout-success?msg=goodbye",
|
|
expectDefault: false,
|
|
expectedValue: "/logout-success?msg=goodbye",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := createTestConfig()
|
|
config.PostLogoutRedirectURI = tt.postLogoutRedirectURI
|
|
|
|
oidc, _ := setupTestOIDCMiddleware(t, config)
|
|
|
|
// Check the configured value
|
|
if tt.expectDefault {
|
|
assert.Equal(t, tt.expectedValue, oidc.postLogoutRedirectURI)
|
|
} else {
|
|
assert.Equal(t, tt.postLogoutRedirectURI, oidc.postLogoutRedirectURI)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLogoutWithPostLogoutRedirect(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping test in short mode")
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
postLogoutRedirectURI string
|
|
oidcEndSessionURL string
|
|
expectRedirectTo string
|
|
expectEndSession bool
|
|
}{
|
|
{
|
|
name: "redirect to custom URI without end session",
|
|
postLogoutRedirectURI: "/goodbye",
|
|
oidcEndSessionURL: "",
|
|
expectRedirectTo: "http://example.com/goodbye",
|
|
expectEndSession: false,
|
|
},
|
|
{
|
|
name: "redirect to default when not configured",
|
|
postLogoutRedirectURI: "",
|
|
oidcEndSessionURL: "",
|
|
expectRedirectTo: "http://example.com/",
|
|
expectEndSession: false,
|
|
},
|
|
{
|
|
name: "end session URL takes precedence",
|
|
postLogoutRedirectURI: "/goodbye",
|
|
oidcEndSessionURL: "https://auth.example.com/logout",
|
|
expectRedirectTo: "https://auth.example.com/logout",
|
|
expectEndSession: true,
|
|
},
|
|
{
|
|
name: "external post logout redirect",
|
|
postLogoutRedirectURI: "https://app.example.com/logged-out",
|
|
oidcEndSessionURL: "",
|
|
expectRedirectTo: "https://app.example.com/logged-out",
|
|
expectEndSession: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := createTestConfig()
|
|
config.PostLogoutRedirectURI = tt.postLogoutRedirectURI
|
|
config.LogoutURL = "/logout"
|
|
|
|
oidc, _ := setupTestOIDCMiddleware(t, config)
|
|
oidc.endSessionURL = tt.oidcEndSessionURL
|
|
|
|
// Create authenticated session
|
|
session := createTestSession()
|
|
session.SetIDToken(createMockJWT(t, "user123", "[email protected]"))
|
|
session.SetAccessToken("test-access-token")
|
|
|
|
// Create logout request
|
|
req := httptest.NewRequest("GET", "/logout", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
// Inject session into request
|
|
injectSessionIntoRequest(t, req, session)
|
|
|
|
// Handle logout
|
|
oidc.ServeHTTP(rec, req)
|
|
|
|
// Check redirect
|
|
assert.Equal(t, http.StatusFound, rec.Code)
|
|
location := rec.Header().Get("Location")
|
|
|
|
if tt.expectEndSession {
|
|
// When end session URL is present, it should redirect there
|
|
assert.Contains(t, location, tt.oidcEndSessionURL)
|
|
// Should include id_token_hint
|
|
assert.Contains(t, location, "id_token_hint=")
|
|
// Should include post_logout_redirect_uri
|
|
if tt.postLogoutRedirectURI != "" {
|
|
assert.Contains(t, location, "post_logout_redirect_uri=")
|
|
}
|
|
} else {
|
|
// Otherwise, should redirect to post logout redirect URI
|
|
assert.Equal(t, tt.expectRedirectTo, location)
|
|
}
|
|
|
|
// Session should be cleared
|
|
cookies := rec.Result().Cookies()
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "oidc_session" {
|
|
assert.Equal(t, -1, cookie.MaxAge, "Session cookie should be deleted")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildLogoutURLWithPostLogoutRedirect(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping test in short mode")
|
|
}
|
|
|
|
tests := []struct {
|
|
expectedParams map[string]string
|
|
name string
|
|
oidcEndSessionURL string
|
|
postLogoutRedirectURI string
|
|
idToken string
|
|
}{
|
|
{
|
|
name: "includes all parameters",
|
|
oidcEndSessionURL: "https://auth.example.com/logout",
|
|
postLogoutRedirectURI: "https://app.example.com/goodbye",
|
|
idToken: "test-id-token",
|
|
expectedParams: map[string]string{
|
|
"id_token_hint": "test-id-token",
|
|
"post_logout_redirect_uri": "https://app.example.com/goodbye",
|
|
},
|
|
},
|
|
{
|
|
name: "relative post logout URI",
|
|
oidcEndSessionURL: "https://auth.example.com/logout",
|
|
postLogoutRedirectURI: "/logout-success",
|
|
idToken: "test-id-token",
|
|
expectedParams: map[string]string{
|
|
"id_token_hint": "test-id-token",
|
|
"post_logout_redirect_uri": "/logout-success",
|
|
},
|
|
},
|
|
{
|
|
name: "empty post logout URI omitted",
|
|
oidcEndSessionURL: "https://auth.example.com/logout",
|
|
postLogoutRedirectURI: "",
|
|
idToken: "test-id-token",
|
|
expectedParams: map[string]string{
|
|
"id_token_hint": "test-id-token",
|
|
},
|
|
},
|
|
{
|
|
name: "special characters in URI",
|
|
oidcEndSessionURL: "https://auth.example.com/logout",
|
|
postLogoutRedirectURI: "/logout?msg=Thank you!",
|
|
idToken: "test-id-token",
|
|
expectedParams: map[string]string{
|
|
"id_token_hint": "test-id-token",
|
|
"post_logout_redirect_uri": "/logout?msg=Thank you!",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Test the BuildLogoutURL function directly without middleware setup
|
|
logoutURL, err := BuildLogoutURL(tt.oidcEndSessionURL, tt.idToken, tt.postLogoutRedirectURI)
|
|
require.NoError(t, err)
|
|
|
|
parsedURL, err := url.Parse(logoutURL)
|
|
require.NoError(t, err)
|
|
|
|
// Check base URL
|
|
expectedBase := tt.oidcEndSessionURL
|
|
actualBase := parsedURL.Scheme + "://" + parsedURL.Host + parsedURL.Path
|
|
assert.Equal(t, expectedBase, actualBase)
|
|
|
|
// Check query parameters
|
|
params := parsedURL.Query()
|
|
for key, expectedValue := range tt.expectedParams {
|
|
assert.Equal(t, expectedValue, params.Get(key), "Parameter %s mismatch", key)
|
|
}
|
|
|
|
// Ensure no extra parameters
|
|
if tt.postLogoutRedirectURI == "" {
|
|
assert.Empty(t, params.Get("post_logout_redirect_uri"))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLogoutFlowIntegration(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping test in short mode")
|
|
}
|
|
|
|
// Mock provider's end session endpoint
|
|
providerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// This won't be called in a unit test, but we keep it for completeness
|
|
if r.URL.Path == "/endsession" {
|
|
// Provider would handle logout and redirect to post_logout_redirect_uri
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
}))
|
|
defer providerServer.Close()
|
|
|
|
config := createTestConfig()
|
|
config.LogoutURL = "/logout"
|
|
config.PostLogoutRedirectURI = "/thank-you"
|
|
config.OIDCEndSessionURL = providerServer.URL + "/endsession"
|
|
|
|
oidc, _ := setupTestOIDCMiddleware(t, config)
|
|
oidc.endSessionURL = config.OIDCEndSessionURL
|
|
oidc.postLogoutRedirectURI = config.PostLogoutRedirectURI
|
|
|
|
// Create authenticated session
|
|
idToken := createMockJWT(t, "user123", "[email protected]")
|
|
session := createTestSession()
|
|
session.SetIDToken(idToken)
|
|
session.SetAccessToken("test-access-token")
|
|
|
|
// Initiate logout
|
|
req := httptest.NewRequest("GET", "/logout", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
// Inject session into request
|
|
injectSessionIntoRequest(t, req, session)
|
|
|
|
oidc.ServeHTTP(rec, req)
|
|
|
|
// Verify redirect to provider's end session
|
|
assert.Equal(t, http.StatusFound, rec.Code)
|
|
location := rec.Header().Get("Location")
|
|
|
|
// Parse the redirect URL to check parameters
|
|
parsedURL, err := url.Parse(location)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify it's redirecting to the correct endpoint
|
|
assert.Equal(t, providerServer.URL+"/endsession", parsedURL.Scheme+"://"+parsedURL.Host+parsedURL.Path)
|
|
|
|
// Verify query parameters
|
|
queryParams := parsedURL.Query()
|
|
assert.Equal(t, idToken, queryParams.Get("id_token_hint"))
|
|
assert.Equal(t, "http://example.com/thank-you", queryParams.Get("post_logout_redirect_uri"))
|
|
|
|
// Note: The provider server won't actually be called in a unit test,
|
|
// as the redirect response is returned to the test client
|
|
}
|
|
|
|
func TestLogoutWithoutSession(t *testing.T) {
|
|
config := createTestConfig()
|
|
config.LogoutURL = "/logout"
|
|
config.PostLogoutRedirectURI = "/goodbye"
|
|
|
|
oidc, _ := setupTestOIDCMiddleware(t, config)
|
|
|
|
// Logout request without session
|
|
req := httptest.NewRequest("GET", "/logout", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
oidc.ServeHTTP(rec, req)
|
|
|
|
// Should still redirect to post logout URI
|
|
assert.Equal(t, http.StatusFound, rec.Code)
|
|
// Relative URLs get converted to absolute URLs
|
|
assert.Equal(t, "http://example.com/goodbye", rec.Header().Get("Location"))
|
|
}
|
|
|
|
func TestPostLogoutRedirectEdgeCases(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping test in short mode")
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
postLogoutRedirectURI string
|
|
requestURL string
|
|
expectedBehavior string
|
|
}{
|
|
{
|
|
name: "preserves fragment in redirect",
|
|
postLogoutRedirectURI: "/app#section",
|
|
requestURL: "/logout",
|
|
expectedBehavior: "Should preserve URL fragment",
|
|
},
|
|
{
|
|
name: "handles encoded characters",
|
|
postLogoutRedirectURI: "/message?text=Thank%20you%21",
|
|
requestURL: "/logout",
|
|
expectedBehavior: "Should handle URL encoding properly",
|
|
},
|
|
{
|
|
name: "absolute URL with different domain",
|
|
postLogoutRedirectURI: "https://other-app.com/logout-landing",
|
|
requestURL: "/logout",
|
|
expectedBehavior: "Should allow external redirects",
|
|
},
|
|
{
|
|
name: "protocol-relative URL",
|
|
postLogoutRedirectURI: "//example.com/logout",
|
|
requestURL: "/logout",
|
|
expectedBehavior: "Should handle protocol-relative URLs",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := createTestConfig()
|
|
config.LogoutURL = "/logout"
|
|
config.PostLogoutRedirectURI = tt.postLogoutRedirectURI
|
|
|
|
oidc, _ := setupTestOIDCMiddleware(t, config)
|
|
|
|
req := httptest.NewRequest("GET", tt.requestURL, nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
// Add minimal session
|
|
session := createTestSession()
|
|
session.SetIDToken("dummy-token")
|
|
|
|
// Inject session into request
|
|
injectSessionIntoRequest(t, req, session)
|
|
|
|
oidc.ServeHTTP(rec, req)
|
|
|
|
assert.Equal(t, http.StatusFound, rec.Code)
|
|
location := rec.Header().Get("Location")
|
|
|
|
// Check based on the type of URL
|
|
switch {
|
|
case strings.HasPrefix(tt.postLogoutRedirectURI, "https://") || strings.HasPrefix(tt.postLogoutRedirectURI, "http://"):
|
|
// Absolute URLs should be preserved
|
|
assert.Equal(t, tt.postLogoutRedirectURI, location, tt.expectedBehavior)
|
|
case strings.HasPrefix(tt.postLogoutRedirectURI, "//"):
|
|
// Protocol-relative URLs get the scheme prepended
|
|
assert.Equal(t, "http://example.com"+tt.postLogoutRedirectURI, location, tt.expectedBehavior)
|
|
default:
|
|
// Relative URLs get the full base URL prepended
|
|
assert.Equal(t, "http://example.com"+tt.postLogoutRedirectURI, location, tt.expectedBehavior)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLogoutURLConfiguration(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping test in short mode")
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
logoutURL string
|
|
callbackURL string
|
|
expectedLogoutURL string
|
|
}{
|
|
{
|
|
name: "custom logout URL",
|
|
logoutURL: "/auth/logout",
|
|
callbackURL: "/auth/callback",
|
|
expectedLogoutURL: "/auth/logout",
|
|
},
|
|
{
|
|
name: "default logout URL from callback",
|
|
logoutURL: "",
|
|
callbackURL: "/oauth2/callback",
|
|
expectedLogoutURL: "/oauth2/callback/logout",
|
|
},
|
|
{
|
|
name: "logout URL with trailing slash",
|
|
logoutURL: "/logout/",
|
|
callbackURL: "/callback",
|
|
expectedLogoutURL: "/logout/",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := createTestConfig()
|
|
config.LogoutURL = tt.logoutURL
|
|
config.CallbackURL = tt.callbackURL
|
|
|
|
oidc, _ := setupTestOIDCMiddleware(t, config)
|
|
|
|
// The logout URL should be set correctly
|
|
assert.Equal(t, tt.expectedLogoutURL, oidc.logoutURLPath)
|
|
|
|
// Test that the logout URL is recognized
|
|
req := httptest.NewRequest("GET", tt.expectedLogoutURL, nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
// Add session to trigger logout logic
|
|
session := createTestSession()
|
|
session.SetIDToken("test-token")
|
|
|
|
// Inject session into request
|
|
injectSessionIntoRequest(t, req, session)
|
|
|
|
oidc.ServeHTTP(rec, req)
|
|
|
|
// Should trigger logout (redirect)
|
|
assert.Equal(t, http.StatusFound, rec.Code)
|
|
})
|
|
}
|
|
}
|