mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
feat: feat: add extraAuthParams (extra authorization request parameters) (#139)
Adds optional extraAuthParams map[string]string config. Extra params are appended to the authorization request but can never override plugin-managed params (client_id, state, nonce, etc.).
This commit is contained in:
@@ -111,6 +111,7 @@ Full reference in [docs/CONFIGURATION.md](docs/CONFIGURATION.md).
|
|||||||
| `logoutURL` | `callbackURL + "/logout"` | RP-initiated logout path. |
|
| `logoutURL` | `callbackURL + "/logout"` | RP-initiated logout path. |
|
||||||
| `postLogoutRedirectURI` | `/` | Where to send users after logout. |
|
| `postLogoutRedirectURI` | `/` | Where to send users after logout. |
|
||||||
| `scopes` | appended to `openid profile email` | Extra OAuth scopes. Set `overrideScopes: true` to replace defaults. |
|
| `scopes` | appended to `openid profile email` | Extra OAuth scopes. Set `overrideScopes: true` to replace defaults. |
|
||||||
|
| `extraAuthParams` | none | Map of extra query parameters appended to the authorization request (e.g. `screen_hint: signup`, `login_hint`, `ui_locales`, `prompt`). Plugin-managed params (`client_id`, `state`, `nonce`, `redirect_uri`, `code_challenge`, `scope`, `response_type`, …) cannot be overridden. |
|
||||||
| `excludedURLs` | none | Prefix-matched paths that bypass auth. |
|
| `excludedURLs` | none | Prefix-matched paths that bypass auth. |
|
||||||
| `allowedUserDomains` | none | Restrict to email domains. |
|
| `allowedUserDomains` | none | Restrict to email domains. |
|
||||||
| `allowedUsers` | none | Restrict to specific addresses (or claim values when `userIdentifierClaim != email`). |
|
| `allowedUsers` | none | Restrict to specific addresses (or claim values when `userIdentifierClaim != email`). |
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ func NewWithContext(ctx context.Context, config *Config, next http.Handler, name
|
|||||||
}(),
|
}(),
|
||||||
forceHTTPS: config.ForceHTTPS,
|
forceHTTPS: config.ForceHTTPS,
|
||||||
enablePKCE: config.EnablePKCE,
|
enablePKCE: config.EnablePKCE,
|
||||||
|
extraAuthParams: config.ExtraAuthParams,
|
||||||
overrideScopes: config.OverrideScopes,
|
overrideScopes: config.OverrideScopes,
|
||||||
strictAudienceValidation: config.StrictAudienceValidation,
|
strictAudienceValidation: config.StrictAudienceValidation,
|
||||||
allowOpaqueTokens: config.AllowOpaqueTokens,
|
allowOpaqueTokens: config.AllowOpaqueTokens,
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type Config struct {
|
|||||||
AllowedUserDomains []string `json:"allowedUserDomains"`
|
AllowedUserDomains []string `json:"allowedUserDomains"`
|
||||||
AllowedUsers []string `json:"allowedUsers"`
|
AllowedUsers []string `json:"allowedUsers"`
|
||||||
Headers []TemplatedHeader `json:"headers"`
|
Headers []TemplatedHeader `json:"headers"`
|
||||||
|
ExtraAuthParams map[string]string `json:"extraAuthParams,omitempty"`
|
||||||
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
|
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
|
||||||
// MaxRefreshTokenAgeSeconds is a heuristic upper bound on the lifetime of
|
// MaxRefreshTokenAgeSeconds is a heuristic upper bound on the lifetime of
|
||||||
// a stored refresh token. Once the token has been in the session longer
|
// a stored refresh token. Once the token has been in the session longer
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ type TraefikOidc struct {
|
|||||||
frontchannelLogoutPath string
|
frontchannelLogoutPath string
|
||||||
scopesSupported []string
|
scopesSupported []string
|
||||||
scopes []string
|
scopes []string
|
||||||
|
extraAuthParams map[string]string
|
||||||
refreshGracePeriod time.Duration
|
refreshGracePeriod time.Duration
|
||||||
maxRefreshTokenAge time.Duration
|
maxRefreshTokenAge time.Duration
|
||||||
metadataMu sync.RWMutex
|
metadataMu sync.RWMutex
|
||||||
|
|||||||
@@ -146,6 +146,21 @@ func (t *TraefikOidc) buildAuthURL(redirectURL, state, nonce, codeChallenge stri
|
|||||||
t.logger.Debugf("TraefikOidc.buildAuthURL: Final scope string being sent to OIDC provider: %s", finalScopeString)
|
t.logger.Debugf("TraefikOidc.buildAuthURL: Final scope string being sent to OIDC provider: %s", finalScopeString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply operator-configured extra authorization parameters (e.g.
|
||||||
|
// screen_hint, login_hint, ui_locales, prompt). These are added last but
|
||||||
|
// can never override parameters the plugin itself manages (client_id,
|
||||||
|
// state, nonce, redirect_uri, code_challenge, scope, response_type, ...):
|
||||||
|
// a key already present in params is left untouched, so this cannot
|
||||||
|
// weaken security-critical parameters.
|
||||||
|
for key, value := range t.extraAuthParams {
|
||||||
|
if params.Get(key) == "" {
|
||||||
|
params.Set(key, value)
|
||||||
|
t.logger.Debugf("TraefikOidc.buildAuthURL: Added extra auth param %s", key)
|
||||||
|
} else {
|
||||||
|
t.logger.Debugf("TraefikOidc.buildAuthURL: Skipped extra auth param %s (already set by plugin)", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Read authURL with RLock
|
// Read authURL with RLock
|
||||||
t.metadataMu.RLock()
|
t.metadataMu.RLock()
|
||||||
authURL := t.authURL
|
authURL := t.authURL
|
||||||
|
|||||||
@@ -554,3 +554,54 @@ func TestForceHTTPSIntegration(t *testing.T) {
|
|||||||
"should use https from X-Forwarded-Proto when forceHTTPS is false")
|
"should use https from X-Forwarded-Proto when forceHTTPS is false")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBuildAuthURLExtraAuthParams verifies operator-configured extra
|
||||||
|
// authorization parameters are appended to the authorization URL, and that
|
||||||
|
// they can never override parameters the plugin itself manages.
|
||||||
|
func TestBuildAuthURLExtraAuthParams(t *testing.T) {
|
||||||
|
t.Run("extra params are added (e.g. screen_hint=signup)", func(t *testing.T) {
|
||||||
|
middleware := createMinimalMiddleware()
|
||||||
|
middleware.extraAuthParams = map[string]string{
|
||||||
|
"screen_hint": "signup",
|
||||||
|
"ui_locales": "en",
|
||||||
|
}
|
||||||
|
|
||||||
|
authURL := middleware.buildAuthURL(
|
||||||
|
"https://app.com/callback", "state123", "nonce456", "",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Contains(t, authURL, "screen_hint=signup")
|
||||||
|
assert.Contains(t, authURL, "ui_locales=en")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil/empty extraAuthParams is a no-op", func(t *testing.T) {
|
||||||
|
middleware := createMinimalMiddleware()
|
||||||
|
// extraAuthParams left nil
|
||||||
|
authURL := middleware.buildAuthURL(
|
||||||
|
"https://app.com/callback", "state123", "nonce456", "",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Contains(t, authURL, "client_id=test-client")
|
||||||
|
assert.NotContains(t, authURL, "screen_hint")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("extra params CANNOT override plugin-managed params", func(t *testing.T) {
|
||||||
|
middleware := createMinimalMiddleware()
|
||||||
|
middleware.extraAuthParams = map[string]string{
|
||||||
|
"client_id": "ATTACKER",
|
||||||
|
"state": "ATTACKER",
|
||||||
|
"redirect_uri": "https://evil.example.com",
|
||||||
|
"response_type": "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
authURL := middleware.buildAuthURL(
|
||||||
|
"https://app.com/callback", "state123", "nonce456", "",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Plugin-managed values must win; injected values must be absent.
|
||||||
|
assert.Contains(t, authURL, "client_id=test-client")
|
||||||
|
assert.NotContains(t, authURL, "ATTACKER")
|
||||||
|
assert.NotContains(t, authURL, "evil.example.com")
|
||||||
|
assert.Contains(t, authURL, "response_type=code")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user