Downgrade gorilla sessions preventing the publishing by traefik hub temporarily.

This commit is contained in:
2024-12-10 10:19:34 +00:00
parent 8101fb2bf6
commit a6fa4d8789
15 changed files with 205 additions and 149 deletions
+1
View File
@@ -13,6 +13,7 @@ testData:
clientSecret: secret clientSecret: secret
callbackURL: /oauth2/callback callbackURL: /oauth2/callback
logoutURL: /oauth2/logout logoutURL: /oauth2/logout
postLogoutRedirectURI: /oidc/different-logout # If not provided it will redirect to the "/" URL
scopes: # If not provided, default scopes will be used (openid, email, profile) scopes: # If not provided, default scopes will be used (openid, email, profile)
- openid - openid
- email - email
+1
View File
@@ -38,6 +38,7 @@ spec:
sessionEncryptionKey: vvv sessionEncryptionKey: vvv
callbackURL: /cool-oidc/callback callbackURL: /cool-oidc/callback
logoutURL: /cool-oidc/logout logoutURL: /cool-oidc/logout
postLogoutRedirectURI: /my-website/you-have-logged-out # Optional post logout URL redirection
scopes: scopes:
- openid - openid
- email - email
+1 -1
View File
@@ -6,7 +6,7 @@ toolchain go1.23.1
require ( require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.4.0 github.com/gorilla/sessions v1.3.0
golang.org/x/time v0.7.0 golang.org/x/time v0.7.0
) )
+2 -2
View File
@@ -4,7 +4,7 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+82 -84
View File
@@ -27,14 +27,6 @@ func generateNonce() (string, error) {
return base64.URLEncoding.EncodeToString(nonceBytes), nil return base64.URLEncoding.EncodeToString(nonceBytes), nil
} }
// buildFullURL constructs a full URL from scheme, host, and path
func buildFullURL(scheme, host, path string) string {
if scheme == "" {
scheme = "http"
}
return fmt.Sprintf("%s://%s%s", scheme, host, path)
}
// exchangeTokens exchanges a code or refresh token for tokens // exchangeTokens exchanges a code or refresh token for tokens
func (t *TraefikOidc) exchangeTokens(ctx context.Context, grantType, codeOrToken, redirectURL string) (*TokenResponse, error) { func (t *TraefikOidc) exchangeTokens(ctx context.Context, grantType, codeOrToken, redirectURL string) (*TokenResponse, error) {
data := url.Values{ data := url.Values{
@@ -97,78 +89,8 @@ func (t *TraefikOidc) getNewTokenWithRefreshToken(refreshToken string) (*TokenRe
return tokenResponse, nil return tokenResponse, nil
} }
// handleLogout handles the logout process
func (t *TraefikOidc) handleLogout(w http.ResponseWriter, r *http.Request) {
session, err := t.store.Get(r, cookieName)
if err != nil {
handleError(w, fmt.Sprintf("Error getting session: %v", err), http.StatusInternalServerError, t.logger)
return
}
// Get tokens from session
idToken, _ := session.Values["id_token"].(string)
refreshToken, _ := session.Values["refresh_token"].(string)
accessToken, _ := session.Values["access_token"].(string)
// Revoke tokens if they exist
if refreshToken != "" {
t.RevokeTokenWithProvider(refreshToken, "refresh_token")
t.RevokeToken(refreshToken)
}
if accessToken != "" {
t.RevokeTokenWithProvider(accessToken, "access_token")
t.RevokeToken(accessToken)
}
// Clear session
session.Options.MaxAge = -1
session.Values = make(map[interface{}]interface{})
if err := session.Save(r, w); err != nil {
handleError(w, fmt.Sprintf("Error saving session: %v", err), http.StatusInternalServerError, t.logger)
return
}
// Determine redirect URL
host := r.Header.Get("X-Forwarded-Host")
if host == "" {
host = r.Host
}
scheme := "http"
if r.Header.Get("X-Forwarded-Proto") == "https" || t.forceHTTPS {
scheme = "https"
}
baseURL := fmt.Sprintf("%s://%s/", scheme, host)
if t.endSessionURL != "" && idToken != "" {
logoutURL, err := BuildLogoutURL(t.endSessionURL, idToken, baseURL)
if err != nil {
handleError(w, fmt.Sprintf("Invalid end session URL: %v", err), http.StatusInternalServerError, t.logger)
return
}
http.Redirect(w, r, logoutURL, http.StatusFound)
return
}
http.Redirect(w, r, baseURL, http.StatusFound)
}
// BuildLogoutURL constructs the logout URL with proper encoding
func BuildLogoutURL(endSessionURL, idToken, postLogoutRedirectURI string) (string, error) {
u, err := url.Parse(endSessionURL)
if err != nil {
return "", fmt.Errorf("invalid end session URL: %v", err)
}
q := u.Query()
q.Set("id_token_hint", idToken)
q.Set("post_logout_redirect_uri", postLogoutRedirectURI)
u.RawQuery = q.Encode()
return u.String(), nil
}
// handleExpiredToken handles the case when a token has expired // handleExpiredToken handles the case when a token has expired
func (t *TraefikOidc) handleExpiredToken(rw http.ResponseWriter, req *http.Request, session *sessions.Session) { func (t *TraefikOidc) handleExpiredToken(rw http.ResponseWriter, req *http.Request, session *sessions.Session, redirectURL string) {
// Clear the existing session // Clear the existing session
session.Options.MaxAge = -1 session.Options.MaxAge = -1
for k := range session.Values { for k := range session.Values {
@@ -189,11 +111,11 @@ func (t *TraefikOidc) handleExpiredToken(rw http.ResponseWriter, req *http.Reque
} }
// Initiate a new authentication flow // Initiate a new authentication flow
t.initiateAuthenticationFunc(rw, req, session, t.redirectURL) t.initiateAuthenticationFunc(rw, req, session, redirectURL)
} }
// handleCallback handles the callback from the OIDC provider // handleCallback handles the callback from the OIDC provider
func (t *TraefikOidc) handleCallback(rw http.ResponseWriter, req *http.Request) { func (t *TraefikOidc) handleCallback(rw http.ResponseWriter, req *http.Request, redirectURL string) {
session, err := t.store.Get(req, cookieName) session, err := t.store.Get(req, cookieName)
if err != nil { if err != nil {
t.logger.Errorf("Session error: %v", err) t.logger.Errorf("Session error: %v", err)
@@ -238,7 +160,7 @@ func (t *TraefikOidc) handleCallback(rw http.ResponseWriter, req *http.Request)
return return
} }
tokenResponse, err := t.exchangeCodeForTokenFunc(code) tokenResponse, err := t.exchangeCodeForTokenFunc(code, redirectURL)
if err != nil { if err != nil {
t.logger.Errorf("Failed to exchange code for token: %v", err) t.logger.Errorf("Failed to exchange code for token: %v", err)
http.Error(rw, "Authentication failed", http.StatusInternalServerError) http.Error(rw, "Authentication failed", http.StatusInternalServerError)
@@ -424,9 +346,9 @@ func (tc *TokenCache) Cleanup() {
} }
// exchangeCodeForToken exchanges the authorization code for tokens // exchangeCodeForToken exchanges the authorization code for tokens
func (t *TraefikOidc) exchangeCodeForToken(code string) (*TokenResponse, error) { func (t *TraefikOidc) exchangeCodeForToken(code string, redirectURL string) (*TokenResponse, error) {
ctx := context.Background() ctx := context.Background()
tokenResponse, err := t.exchangeTokens(ctx, "authorization_code", code, t.redirectURL) tokenResponse, err := t.exchangeTokens(ctx, "authorization_code", code, redirectURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to exchange code for token: %w", err) return nil, fmt.Errorf("failed to exchange code for token: %w", err)
} }
@@ -441,3 +363,79 @@ func createStringMap(keys []string) map[string]struct{} {
} }
return result return result
} }
// handleLogout handles the logout request
func (t *TraefikOidc) handleLogout(rw http.ResponseWriter, req *http.Request) {
session, err := t.store.Get(req, cookieName)
if err != nil {
t.logger.Errorf("Error getting session: %v", err)
http.Error(rw, "Session error", http.StatusInternalServerError)
return
}
// Get the id_token before clearing the session
idToken, _ := session.Values["id_token"].(string)
// Clear and expire the session
session.Values = make(map[interface{}]interface{})
session.Options.MaxAge = -1
if err := session.Save(req, rw); err != nil {
t.logger.Errorf("Error saving session: %v", err)
http.Error(rw, "Session error", http.StatusInternalServerError)
return
}
// Get the base URL for redirects
host := t.determineHost(req)
scheme := t.determineScheme(req)
baseURL := fmt.Sprintf("%s://%s", scheme, host)
// Determine post logout redirect URI
var postLogoutRedirectURI string
if t.postLogoutRedirectURI != "" {
// Use explicitly configured postLogoutRedirectURI
if strings.HasPrefix(t.postLogoutRedirectURI, "http://") || strings.HasPrefix(t.postLogoutRedirectURI, "https://") {
postLogoutRedirectURI = t.postLogoutRedirectURI
} else {
postLogoutRedirectURI = fmt.Sprintf("%s%s", baseURL, t.postLogoutRedirectURI)
}
} else {
postLogoutRedirectURI = fmt.Sprintf("%s%s", baseURL, "/")
}
t.logger.Debugf("Using post logout redirect URI: %s", postLogoutRedirectURI)
// If we have an end session endpoint and an ID token, use OIDC end session
if t.endSessionURL != "" && idToken != "" {
logoutURL, err := BuildLogoutURL(t.endSessionURL, idToken, postLogoutRedirectURI)
if err != nil {
handleError(rw, fmt.Sprintf("Failed to build logout URL: %v", err), http.StatusInternalServerError, t.logger)
return
}
t.logger.Debugf("Redirecting to end session URL: %s", logoutURL)
http.Redirect(rw, req, logoutURL, http.StatusFound)
return
}
// If no end session endpoint or no ID token, just redirect to the post logout URI
t.logger.Debugf("Redirecting to post logout URI: %s", postLogoutRedirectURI)
http.Redirect(rw, req, postLogoutRedirectURI, http.StatusFound)
}
// BuildLogoutURL constructs the OIDC end session URL
func BuildLogoutURL(endSessionURL, idToken, postLogoutRedirectURI string) (string, error) {
u, err := url.Parse(endSessionURL)
if err != nil {
return "", fmt.Errorf("failed to parse end session URL: %w", err)
}
q := u.Query()
q.Set("id_token_hint", idToken)
if postLogoutRedirectURI != "" {
// Ensure postLogoutRedirectURI is properly URL encoded
q.Set("post_logout_redirect_uri", postLogoutRedirectURI)
}
u.RawQuery = q.Encode()
return u.String(), nil
}
+35 -12
View File
@@ -53,18 +53,18 @@ type TraefikOidc struct {
tokenCache *TokenCache tokenCache *TokenCache
httpClient *http.Client httpClient *http.Client
logger *Logger logger *Logger
redirectURL string
tokenVerifier TokenVerifier tokenVerifier TokenVerifier
jwtVerifier JWTVerifier jwtVerifier JWTVerifier
excludedURLs map[string]struct{} excludedURLs map[string]struct{}
allowedUserDomains map[string]struct{} allowedUserDomains map[string]struct{}
allowedRolesAndGroups map[string]struct{} allowedRolesAndGroups map[string]struct{}
initiateAuthenticationFunc func(rw http.ResponseWriter, req *http.Request, session *sessions.Session, redirectURL string) initiateAuthenticationFunc func(rw http.ResponseWriter, req *http.Request, session *sessions.Session, redirectURL string)
exchangeCodeForTokenFunc func(code string) (*TokenResponse, error) exchangeCodeForTokenFunc func(code string, redirectURL string) (*TokenResponse, error)
extractClaimsFunc func(tokenString string) (map[string]interface{}, error) extractClaimsFunc func(tokenString string) (map[string]interface{}, error)
initComplete chan struct{} initComplete chan struct{}
endSessionURL string endSessionURL string
baseURL string baseURL string
postLogoutRedirectURI string
} }
// ProviderMetadata holds OIDC provider metadata // ProviderMetadata holds OIDC provider metadata
@@ -227,6 +227,12 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
} }
return config.LogoutURL return config.LogoutURL
}(), }(),
postLogoutRedirectURI: func() string {
if config.PostLogoutRedirectURI == "" {
return "/"
}
return config.PostLogoutRedirectURI
}(),
tokenBlacklist: NewTokenBlacklist(), tokenBlacklist: NewTokenBlacklist(),
jwkCache: &JWKCache{}, jwkCache: &JWKCache{},
clientID: config.ClientID, clientID: config.ClientID,
@@ -375,10 +381,12 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
defaultSessionOptions.Secure = t.scheme == "https" defaultSessionOptions.Secure = t.scheme == "https"
host := t.determineHost(req) host := t.determineHost(req)
redirectURL := buildFullURL(t.scheme, host, t.redirURLPath)
// Build the redirect URL if not already set // Build the redirect URL if not already set
if t.redirectURL == "" { if redirectURL == "" {
t.redirectURL = buildFullURL(t.scheme, host, t.redirURLPath) redirectURL = buildFullURL(t.scheme, host, t.redirURLPath)
t.logger.Debugf("Redirect URL updated to: %s", t.redirectURL) t.logger.Debugf("Redirect URL updated to: %s", redirectURL)
} }
// Get the session // Get the session
@@ -399,7 +407,7 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Handle callback URL // Handle callback URL
if req.URL.Path == t.redirURLPath { if req.URL.Path == t.redirURLPath {
t.handleCallback(rw, req) t.handleCallback(rw, req, redirectURL)
return return
} }
@@ -407,19 +415,19 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
authenticated, needsRefresh, expired := t.isUserAuthenticated(session) authenticated, needsRefresh, expired := t.isUserAuthenticated(session)
if expired { if expired {
t.handleExpiredToken(rw, req, session) t.handleExpiredToken(rw, req, session, redirectURL)
return return
} }
if !authenticated { if !authenticated {
t.defaultInitiateAuthentication(rw, req, session, t.redirectURL) t.defaultInitiateAuthentication(rw, req, session, redirectURL)
return return
} }
if needsRefresh { if needsRefresh {
refreshed := t.refreshToken(rw, req, session) refreshed := t.refreshToken(rw, req, session)
if !refreshed { if !refreshed {
t.handleExpiredToken(rw, req, session) t.handleExpiredToken(rw, req, session, redirectURL)
return return
} }
} }
@@ -428,21 +436,21 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
idToken, ok := session.Values["id_token"].(string) idToken, ok := session.Values["id_token"].(string)
if !ok || idToken == "" { if !ok || idToken == "" {
t.logger.Errorf("No id_token found in session") t.logger.Errorf("No id_token found in session")
t.defaultInitiateAuthentication(rw, req, session, t.redirectURL) t.defaultInitiateAuthentication(rw, req, session, redirectURL)
return return
} }
claims, err := extractClaims(idToken) claims, err := extractClaims(idToken)
if err != nil { if err != nil {
t.logger.Errorf("Failed to extract claims: %v", err) t.logger.Errorf("Failed to extract claims: %v", err)
t.defaultInitiateAuthentication(rw, req, session, t.redirectURL) t.defaultInitiateAuthentication(rw, req, session, redirectURL)
return return
} }
email, _ := claims["email"].(string) email, _ := claims["email"].(string)
if email == "" { if email == "" {
t.logger.Debugf("No email found in token claims") t.logger.Debugf("No email found in token claims")
t.defaultInitiateAuthentication(rw, req, session, t.redirectURL) t.defaultInitiateAuthentication(rw, req, session, redirectURL)
return return
} }
@@ -767,3 +775,18 @@ func (t *TraefikOidc) extractGroupsAndRoles(idToken string) ([]string, []string,
return groups, roles, nil return groups, roles, nil
} }
// buildFullURL constructs a full URL from scheme, host and path
func buildFullURL(scheme, host, path string) string {
// If the path is already a full URL, return it as-is
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
return path
}
// Ensure the path starts with a forward slash
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return fmt.Sprintf("%s://%s%s", scheme, host, path)
}
+20 -20
View File
@@ -104,7 +104,7 @@ func (ts *TestSuite) Setup() {
} }
// Helper functions used by TraefikOidc // Helper functions used by TraefikOidc
func (ts *TestSuite) exchangeCodeForTokenFunc(code string) (*TokenResponse, error) { func (ts *TestSuite) exchangeCodeForTokenFunc(code string, redirectURL string) (*TokenResponse, error) {
return &TokenResponse{ return &TokenResponse{
IDToken: ts.token, IDToken: ts.token,
RefreshToken: "test-refresh-token", RefreshToken: "test-refresh-token",
@@ -452,10 +452,12 @@ func TestHandleCallback(t *testing.T) {
ts := &TestSuite{t: t} ts := &TestSuite{t: t}
ts.Setup() ts.Setup()
redirectURL := "http://example.com/"
tests := []struct { tests := []struct {
name string name string
queryParams string queryParams string
exchangeCodeForToken func(code string) (*TokenResponse, error) exchangeCodeForToken func(code string, redirectURL string) (*TokenResponse, error)
extractClaimsFunc func(tokenString string) (map[string]interface{}, error) extractClaimsFunc func(tokenString string) (map[string]interface{}, error)
sessionSetupFunc func(session *sessions.Session) sessionSetupFunc func(session *sessions.Session)
expectedStatus int expectedStatus int
@@ -463,7 +465,7 @@ func TestHandleCallback(t *testing.T) {
{ {
name: "Success", name: "Success",
queryParams: "?code=test-code&state=test-csrf-token", queryParams: "?code=test-code&state=test-csrf-token",
exchangeCodeForToken: func(code string) (*TokenResponse, error) { exchangeCodeForToken: func(code string, redirectURL string) (*TokenResponse, error) {
return &TokenResponse{ return &TokenResponse{
IDToken: ts.token, IDToken: ts.token,
RefreshToken: "test-refresh-token", RefreshToken: "test-refresh-token",
@@ -493,7 +495,7 @@ func TestHandleCallback(t *testing.T) {
{ {
name: "Exchange Code Error", name: "Exchange Code Error",
queryParams: "?code=test-code&state=test-csrf-token", queryParams: "?code=test-code&state=test-csrf-token",
exchangeCodeForToken: func(code string) (*TokenResponse, error) { exchangeCodeForToken: func(code string, redirectURL string) (*TokenResponse, error) {
return nil, fmt.Errorf("exchange code error") return nil, fmt.Errorf("exchange code error")
}, },
sessionSetupFunc: func(session *sessions.Session) { sessionSetupFunc: func(session *sessions.Session) {
@@ -505,7 +507,7 @@ func TestHandleCallback(t *testing.T) {
{ {
name: "Missing ID Token", name: "Missing ID Token",
queryParams: "?code=test-code&state=test-csrf-token", queryParams: "?code=test-code&state=test-csrf-token",
exchangeCodeForToken: func(code string) (*TokenResponse, error) { exchangeCodeForToken: func(code string, redirectURL string) (*TokenResponse, error) {
return &TokenResponse{}, nil return &TokenResponse{}, nil
}, },
sessionSetupFunc: func(session *sessions.Session) { sessionSetupFunc: func(session *sessions.Session) {
@@ -517,7 +519,7 @@ func TestHandleCallback(t *testing.T) {
{ {
name: "Disallowed Email", name: "Disallowed Email",
queryParams: "?code=test-code&state=test-csrf-token", queryParams: "?code=test-code&state=test-csrf-token",
exchangeCodeForToken: func(code string) (*TokenResponse, error) { exchangeCodeForToken: func(code string, redirectURL string) (*TokenResponse, error) {
return &TokenResponse{ return &TokenResponse{
IDToken: ts.token, IDToken: ts.token,
RefreshToken: "test-refresh-token", RefreshToken: "test-refresh-token",
@@ -538,7 +540,7 @@ func TestHandleCallback(t *testing.T) {
{ {
name: "Invalid State Parameter", name: "Invalid State Parameter",
queryParams: "?code=test-code&state=invalid-csrf-token", queryParams: "?code=test-code&state=invalid-csrf-token",
exchangeCodeForToken: func(code string) (*TokenResponse, error) { exchangeCodeForToken: func(code string, redirectURL string) (*TokenResponse, error) {
return &TokenResponse{ return &TokenResponse{
IDToken: ts.token, IDToken: ts.token,
RefreshToken: "test-refresh-token", RefreshToken: "test-refresh-token",
@@ -559,7 +561,7 @@ func TestHandleCallback(t *testing.T) {
{ {
name: "Nonce Mismatch", name: "Nonce Mismatch",
queryParams: "?code=test-code&state=test-csrf-token", queryParams: "?code=test-code&state=test-csrf-token",
exchangeCodeForToken: func(code string) (*TokenResponse, error) { exchangeCodeForToken: func(code string, redirectURL string) (*TokenResponse, error) {
return &TokenResponse{ return &TokenResponse{
IDToken: ts.token, IDToken: ts.token,
RefreshToken: "test-refresh-token", RefreshToken: "test-refresh-token",
@@ -580,7 +582,7 @@ func TestHandleCallback(t *testing.T) {
{ {
name: "Missing Nonce in Claims", name: "Missing Nonce in Claims",
queryParams: "?code=test-code&state=test-csrf-token", queryParams: "?code=test-code&state=test-csrf-token",
exchangeCodeForToken: func(code string) (*TokenResponse, error) { exchangeCodeForToken: func(code string, redirectURL string) (*TokenResponse, error) {
return &TokenResponse{ return &TokenResponse{
IDToken: ts.token, IDToken: ts.token,
RefreshToken: "test-refresh-token", RefreshToken: "test-refresh-token",
@@ -633,7 +635,7 @@ func TestHandleCallback(t *testing.T) {
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
// Call handleCallback // Call handleCallback
tOidc.handleCallback(rr, req) tOidc.handleCallback(rr, req, redirectURL)
// Check response // Check response
if rr.Code != tc.expectedStatus { if rr.Code != tc.expectedStatus {
@@ -688,7 +690,7 @@ func TestOIDCHandler(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
queryParams string queryParams string
exchangeCodeForToken func(code string) (*TokenResponse, error) exchangeCodeForToken func(code string, redirectURL string) (*TokenResponse, error)
extractClaimsFunc func(tokenString string) (map[string]interface{}, error) extractClaimsFunc func(tokenString string) (map[string]interface{}, error)
sessionSetupFunc func(session *sessions.Session) sessionSetupFunc func(session *sessions.Session)
expectedStatus int expectedStatus int
@@ -704,7 +706,7 @@ func TestOIDCHandler(t *testing.T) {
session.Values["csrf"] = "test-csrf-token" session.Values["csrf"] = "test-csrf-token"
session.Values["nonce"] = "test-nonce" session.Values["nonce"] = "test-nonce"
}, },
exchangeCodeForToken: func(code string) (*TokenResponse, error) { exchangeCodeForToken: func(code string, redirectURL string) (*TokenResponse, error) {
// Simulate token exchange // Simulate token exchange
return &TokenResponse{ return &TokenResponse{
IDToken: ts.token, IDToken: ts.token,
@@ -728,7 +730,7 @@ func TestOIDCHandler(t *testing.T) {
session.Values["csrf"] = "test-csrf-token" session.Values["csrf"] = "test-csrf-token"
session.Values["nonce"] = "test-nonce" session.Values["nonce"] = "test-nonce"
}, },
exchangeCodeForToken: func(code string) (*TokenResponse, error) { exchangeCodeForToken: func(code string, redirectURL string) (*TokenResponse, error) {
// Simulate token exchange // Simulate token exchange
return &TokenResponse{ return &TokenResponse{
IDToken: ts.token, IDToken: ts.token,
@@ -751,7 +753,7 @@ func TestOIDCHandler(t *testing.T) {
session.Values["csrf"] = "test-csrf-token" session.Values["csrf"] = "test-csrf-token"
session.Values["nonce"] = "test-nonce" session.Values["nonce"] = "test-nonce"
}, },
exchangeCodeForToken: func(code string) (*TokenResponse, error) { exchangeCodeForToken: func(code string, redirectURL string) (*TokenResponse, error) {
// Simulate token exchange // Simulate token exchange
return &TokenResponse{ return &TokenResponse{
IDToken: ts.token, IDToken: ts.token,
@@ -775,7 +777,7 @@ func TestOIDCHandler(t *testing.T) {
session.Values["csrf"] = "test-csrf-token" session.Values["csrf"] = "test-csrf-token"
session.Values["nonce"] = "test-nonce" session.Values["nonce"] = "test-nonce"
}, },
exchangeCodeForToken: func(code string) (*TokenResponse, error) { exchangeCodeForToken: func(code string, redirectURL string) (*TokenResponse, error) {
// Simulate token exchange // Simulate token exchange
return &TokenResponse{ return &TokenResponse{
IDToken: ts.token, IDToken: ts.token,
@@ -863,9 +865,8 @@ func TestHandleLogout(t *testing.T) {
}, },
endSessionURL: "https://provider/end-session", endSessionURL: "https://provider/end-session",
expectedStatus: http.StatusFound, expectedStatus: http.StatusFound,
// Fix: The entire URL should be URL-encoded expectedURL: "https://provider/end-session?id_token_hint=test.id.token&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2F",
expectedURL: "https://provider/end-session?id_token_hint=test.id.token&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2F", host: "test-host",
host: "test-host",
}, },
{ {
name: "Successful logout without end session endpoint", name: "Successful logout without end session endpoint",
@@ -1184,7 +1185,6 @@ func TestHandleExpiredToken(t *testing.T) {
tOidc := &TraefikOidc{ tOidc := &TraefikOidc{
store: sessions.NewCookieStore([]byte("test-secret-key")), store: sessions.NewCookieStore([]byte("test-secret-key")),
logger: NewLogger("info"), logger: NewLogger("info"),
redirectURL: "http://example.com/callback",
tokenVerifier: ts.tOidc.tokenVerifier, tokenVerifier: ts.tOidc.tokenVerifier,
jwtVerifier: ts.tOidc.jwtVerifier, jwtVerifier: ts.tOidc.jwtVerifier,
initComplete: make(chan struct{}), initComplete: make(chan struct{}),
@@ -1205,7 +1205,7 @@ func TestHandleExpiredToken(t *testing.T) {
tc.setupSession(session) tc.setupSession(session)
// Handle expired token // Handle expired token
tOidc.handleExpiredToken(rr, req, session) tOidc.handleExpiredToken(rr, req, session, tc.expectedPath)
// Verify session is cleaned // Verify session is cleaned
if len(session.Values) != 3 { // Should only have csrf, incoming_path, and nonce if len(session.Values) != 3 { // Should only have csrf, incoming_path, and nonce
+1
View File
@@ -31,6 +31,7 @@ type Config struct {
AllowedUserDomains []string `json:"allowedUserDomains"` AllowedUserDomains []string `json:"allowedUserDomains"`
AllowedRolesAndGroups []string `json:"allowedRolesAndGroups"` AllowedRolesAndGroups []string `json:"allowedRolesAndGroups"`
OIDCEndSessionURL string `json:"oidcEndSessionURL"` OIDCEndSessionURL string `json:"oidcEndSessionURL"`
PostLogoutRedirectURI string `json:"postLogoutRedirectURI"`
HTTPClient *http.Client HTTPClient *http.Client
} }
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2024 The Gorilla Authors. All rights reserved. Copyright (c) 2023 The Gorilla Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are modification, are permitted provided that the following conditions are
+1 -5
View File
@@ -1,7 +1,4 @@
# Gorilla Sessions # sessions
> [!IMPORTANT]
> The latest version of this repository requires go 1.23 because of the new partitioned attribute. The last version that is compatible with older versions of go is v1.3.0.
![testing](https://github.com/gorilla/sessions/actions/workflows/test.yml/badge.svg) ![testing](https://github.com/gorilla/sessions/actions/workflows/test.yml/badge.svg)
[![codecov](https://codecov.io/github/gorilla/sessions/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/sessions) [![codecov](https://codecov.io/github/gorilla/sessions/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/sessions)
@@ -77,7 +74,6 @@ Other implementations of the `sessions.Store` interface:
- [github.com/dsoprea/go-appengine-sessioncascade](https://github.com/dsoprea/go-appengine-sessioncascade) - Memcache/Datastore/Context in AppEngine - [github.com/dsoprea/go-appengine-sessioncascade](https://github.com/dsoprea/go-appengine-sessioncascade) - Memcache/Datastore/Context in AppEngine
- [github.com/kidstuff/mongostore](https://github.com/kidstuff/mongostore) - MongoDB - [github.com/kidstuff/mongostore](https://github.com/kidstuff/mongostore) - MongoDB
- [github.com/srinathgs/mysqlstore](https://github.com/srinathgs/mysqlstore) - MySQL - [github.com/srinathgs/mysqlstore](https://github.com/srinathgs/mysqlstore) - MySQL
- [github.com/danielepintore/gorilla-sessions-mysql](https://github.com/danielepintore/gorilla-sessions-mysql) - MySQL
- [github.com/EnumApps/clustersqlstore](https://github.com/EnumApps/clustersqlstore) - MySQL Cluster - [github.com/EnumApps/clustersqlstore](https://github.com/EnumApps/clustersqlstore) - MySQL Cluster
- [github.com/antonlindstrom/pgstore](https://github.com/antonlindstrom/pgstore) - PostgreSQL - [github.com/antonlindstrom/pgstore](https://github.com/antonlindstrom/pgstore) - PostgreSQL
- [github.com/boj/redistore](https://github.com/boj/redistore) - Redis - [github.com/boj/redistore](https://github.com/boj/redistore) - Redis
+9 -12
View File
@@ -1,6 +1,5 @@
// Copyright 2012 The Gorilla Authors. All rights reserved. //go:build !go1.11
// Use of this source code is governed by a BSD-style // +build !go1.11
// license that can be found in the LICENSE file.
package sessions package sessions
@@ -9,15 +8,13 @@ import "net/http"
// newCookieFromOptions returns an http.Cookie with the options set. // newCookieFromOptions returns an http.Cookie with the options set.
func newCookieFromOptions(name, value string, options *Options) *http.Cookie { func newCookieFromOptions(name, value string, options *Options) *http.Cookie {
return &http.Cookie{ return &http.Cookie{
Name: name, Name: name,
Value: value, Value: value,
Path: options.Path, Path: options.Path,
Domain: options.Domain, Domain: options.Domain,
MaxAge: options.MaxAge, MaxAge: options.MaxAge,
Secure: options.Secure, Secure: options.Secure,
HttpOnly: options.HttpOnly, HttpOnly: options.HttpOnly,
Partitioned: options.Partitioned,
SameSite: options.SameSite,
} }
} }
+21
View File
@@ -0,0 +1,21 @@
//go:build go1.11
// +build go1.11
package sessions
import "net/http"
// newCookieFromOptions returns an http.Cookie with the options set.
func newCookieFromOptions(name, value string, options *Options) *http.Cookie {
return &http.Cookie{
Name: name,
Value: value,
Path: options.Path,
Domain: options.Domain,
MaxAge: options.MaxAge,
Secure: options.Secure,
HttpOnly: options.HttpOnly,
SameSite: options.SameSite,
}
}
+5 -10
View File
@@ -1,11 +1,8 @@
// Copyright 2012 The Gorilla Authors. All rights reserved. //go:build !go1.11
// Use of this source code is governed by a BSD-style // +build !go1.11
// license that can be found in the LICENSE file.
package sessions package sessions
import "net/http"
// Options stores configuration for a session or session store. // Options stores configuration for a session or session store.
// //
// Fields are a subset of http.Cookie fields. // Fields are a subset of http.Cookie fields.
@@ -16,9 +13,7 @@ type Options struct {
// deleted after the browser session ends. // deleted after the browser session ends.
// MaxAge<0 means delete cookie immediately. // MaxAge<0 means delete cookie immediately.
// MaxAge>0 means Max-Age attribute present and given in seconds. // MaxAge>0 means Max-Age attribute present and given in seconds.
MaxAge int MaxAge int
Secure bool Secure bool
HttpOnly bool HttpOnly bool
Partitioned bool
SameSite http.SameSite
} }
+23
View File
@@ -0,0 +1,23 @@
//go:build go1.11
// +build go1.11
package sessions
import "net/http"
// Options stores configuration for a session or session store.
//
// Fields are a subset of http.Cookie fields.
type Options struct {
Path string
Domain string
// MaxAge=0 means no Max-Age attribute specified and the cookie will be
// deleted after the browser session ends.
// MaxAge<0 means delete cookie immediately.
// MaxAge>0 means Max-Age attribute present and given in seconds.
MaxAge int
Secure bool
HttpOnly bool
// Defaults to http.SameSiteDefaultMode
SameSite http.SameSite
}
+2 -2
View File
@@ -4,8 +4,8 @@ github.com/google/uuid
# github.com/gorilla/securecookie v1.1.2 # github.com/gorilla/securecookie v1.1.2
## explicit; go 1.20 ## explicit; go 1.20
github.com/gorilla/securecookie github.com/gorilla/securecookie
# github.com/gorilla/sessions v1.4.0 # github.com/gorilla/sessions v1.3.0
## explicit; go 1.23 ## explicit; go 1.20
github.com/gorilla/sessions github.com/gorilla/sessions
# golang.org/x/time v0.7.0 # golang.org/x/time v0.7.0
## explicit; go 1.18 ## explicit; go 1.18