mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-18 04:47:28 +00:00
Downgrade gorilla sessions preventing the publishing by traefik hub temporarily.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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.
|
|
||||||
|
|
||||||

|

|
||||||
[](https://codecov.io/github/gorilla/sessions)
|
[](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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
}
|
||||||
Vendored
+2
-2
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user