mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +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. |
|
||||
| `postLogoutRedirectURI` | `/` | Where to send users after logout. |
|
||||
| `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. |
|
||||
| `allowedUserDomains` | none | Restrict to email domains. |
|
||||
| `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,
|
||||
enablePKCE: config.EnablePKCE,
|
||||
extraAuthParams: config.ExtraAuthParams,
|
||||
overrideScopes: config.OverrideScopes,
|
||||
strictAudienceValidation: config.StrictAudienceValidation,
|
||||
allowOpaqueTokens: config.AllowOpaqueTokens,
|
||||
|
||||
@@ -54,6 +54,7 @@ type Config struct {
|
||||
AllowedUserDomains []string `json:"allowedUserDomains"`
|
||||
AllowedUsers []string `json:"allowedUsers"`
|
||||
Headers []TemplatedHeader `json:"headers"`
|
||||
ExtraAuthParams map[string]string `json:"extraAuthParams,omitempty"`
|
||||
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
|
||||
// MaxRefreshTokenAgeSeconds is a heuristic upper bound on the lifetime of
|
||||
// a stored refresh token. Once the token has been in the session longer
|
||||
|
||||
@@ -165,6 +165,7 @@ type TraefikOidc struct {
|
||||
frontchannelLogoutPath string
|
||||
scopesSupported []string
|
||||
scopes []string
|
||||
extraAuthParams map[string]string
|
||||
refreshGracePeriod time.Duration
|
||||
maxRefreshTokenAge time.Duration
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
t.metadataMu.RLock()
|
||||
authURL := t.authURL
|
||||
|
||||
@@ -554,3 +554,54 @@ func TestForceHTTPSIntegration(t *testing.T) {
|
||||
"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