mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
feat(auth): support private_key_jwt and client_secret_basic (#137)
revocation endpoints, joining the existing client_secret_post default. Both are opt-in via the new clientAuthMethod config field. Closes #135. private_key_jwt (RFC 7523 §2.2 / OpenID Connect Core §9) ======================================================== Plugin signs a short-lived JWT with a configured private key and presents it as client_assertion. Use when the IdP enforces short secret TTLs or requires secretless client auth (Microsoft Entra ID / Azure AD, Okta, Auth0, Keycloak). New Config fields: clientAuthMethod (default: client_secret_post) clientAssertionPrivateKey (inline PEM) clientAssertionKeyPath (PEM file path; mutually exclusive) clientAssertionKeyID (JWS kid header — required) clientAssertionAlg (default: RS256; RS/PS/ES 256–512 supported) PEM forms accepted: PKCS#8, PKCS#1, SEC1. Assertion claims: iss=sub=clientID, aud=tokenURL, iat=now, exp=now+60s, random 16-byte hex jti per request. ECDSA signatures are raw r||s per RFC 7515 (not ASN.1). client_secret_basic (RFC 6749 §2.3.1) ===================================== Sends credentials in the Authorization: Basic header instead of the body. Both halves are form-urlencoded individually before base64 — that encoding step is required by the spec and is NOT what stdlib's http.Request.SetBasicAuth does, so the plugin uses its own helper. The form body omits client_id and client_secret on this path. Wire-up ======= Both methods are dispatched at the same two call sites: helpers.go:exchangeTokens — auth_code + refresh_token grants token_manager.go:RevokeTokenWithProvider — RFC 7009 revocation Existing clientSecret deployments are unaffected — empty clientAuthMethod maps to the historical client_secret_post behavior, and clientAssertion remains nil unless the new fields are set. Yaegi compatibility =================== All required crypto/rsa, crypto/ecdsa, crypto/x509, encoding/pem and crypto/sha256/384/512 symbols are exposed by the traefik/yaegi stdlib symbol tables (RSA SignPKCS1v15 + SignPSS, ECDSA Sign, ParsePKCS8/1PrivateKey, ParseECPrivateKey). Tests (16 new) ============== Algorithm-family coverage: TestIssue135_SignerRSAFamily — RS256/384/512 + PS256/384/512 TestIssue135_SignerECDSAFamily — ES256/384/512, raw r||s shape TestIssue135_SignerRejectsAlgKeyMismatch TestIssue135_SignerJTIUniqueness — 50 sigs, all jti distinct TestIssue135_SignerPEMVariants — PKCS#8, PKCS#1, SEC1 Config validation: TestIssue135_ConfigValidation — full Validate() matrix TestIssue135_ConfigKeyPathLoadsFile Wire-up: TestIssue135_AuthCodeExchangeUsesAssertion TestIssue135_RefreshTokenUsesAssertion TestIssue135_BackcompatClientSecretPath TestIssue135_RevocationUsesAssertion TestIssue135_BuildSignerFromInlineConfig TestIssue135_BuildSignerDefaultsToRS256 TestIssue135_ClientSecretBasicAuth — Authorization header, no body creds TestIssue135_ClientSecretBasicURLEncodesReservedChars — :, +, /, @, =, & TestIssue135_ClientSecretBasicRevocation — revocation parity Documentation ============= README.md — required-row note + 5 optional rows + dedicated section docs/CONFIGURATION.md — new Client Authentication section with three method subsections, OpenSSL keygen snippet, RFC links docs/index.html — 5 new config-table rows + Private Key JWT explainer card .traefik.yml + examples/complete-traefik-config.yaml — commented opt-in example Out of scope (deferred) ======================= mTLS / tls_client_auth (RFC 8705) — separate change; requires per-call http.Client with tls.Config.Certificates and conflicts with the current pooled HTTP client architecture.
This commit is contained in:
@@ -96,7 +96,7 @@ More example configs in [`examples/`](examples/).
|
||||
|-----------|-------------|
|
||||
| `providerURL` | Issuer URL (used for OIDC discovery). |
|
||||
| `clientID` | OAuth 2.0 client ID. |
|
||||
| `clientSecret` | OAuth 2.0 client secret. Supports `urn:k8s:secret:ns:name:key`. |
|
||||
| `clientSecret` | OAuth 2.0 client secret. Supports `urn:k8s:secret:ns:name:key`. Required when `clientAuthMethod` is unset, `client_secret_post`, or `client_secret_basic`; optional with `private_key_jwt`. |
|
||||
| `sessionEncryptionKey` | Cookie encryption key, **min 32 bytes**. |
|
||||
| `callbackURL` | Callback path, e.g. `/oauth2/callback`. |
|
||||
|
||||
@@ -133,6 +133,11 @@ Full reference in [docs/CONFIGURATION.md](docs/CONFIGURATION.md).
|
||||
| `stripAuthCookies` | `false` | Strip OIDC cookies from backend hop (mitigates HTTP 431). |
|
||||
| `caCertPath` / `caCertPEM` | none | Trust an internal CA for the provider's TLS. |
|
||||
| `insecureSkipVerify` | `false` | **Local dev only.** Disables TLS verification, logs a security warning. |
|
||||
| `clientAuthMethod` | `client_secret_post` | Client auth method. Set `private_key_jwt` for RFC 7523 JWT assertions (Entra ID, Okta, Auth0, Keycloak). See [Client authentication via private key JWT](#client-authentication-via-private-key-jwt). |
|
||||
| `clientAssertionPrivateKey` | none | Inline PEM private key for `private_key_jwt`. Mutually exclusive with `clientAssertionKeyPath`. |
|
||||
| `clientAssertionKeyPath` | none | File path to PEM private key for `private_key_jwt`. |
|
||||
| `clientAssertionKeyID` | none | JWS `kid` header. Required when `clientAuthMethod=private_key_jwt`; must match the public key registered with the IdP. |
|
||||
| `clientAssertionAlg` | `RS256` | JWS alg for `private_key_jwt`. Supported: `RS256/384/512`, `PS256/384/512`, `ES256/384/512`. |
|
||||
| `enableBackchannelLogout` / `backchannelLogoutURL` | `false` / none | OIDC Back-Channel Logout (server-to-server). |
|
||||
| `enableFrontchannelLogout` / `frontchannelLogoutURL` | `false` / none | OIDC Front-Channel Logout (iframe). |
|
||||
| `redis` | disabled | See [docs/REDIS.md](docs/REDIS.md). |
|
||||
@@ -213,6 +218,44 @@ caCertPEM: |
|
||||
Both can be combined. An unparseable bundle fails the plugin at startup.
|
||||
See [#125](https://github.com/lukaszraczylo/traefikoidc/issues/125).
|
||||
|
||||
### Client authentication via private key JWT
|
||||
|
||||
Use when your IdP enforces short-lived secrets or pushes secretless client auth
|
||||
— Microsoft Entra ID / Azure AD, Okta, Auth0, Keycloak. Instead of sending a
|
||||
static `clientSecret`, the plugin signs a short-lived JWT and submits it as
|
||||
`client_assertion` per [RFC 7523](https://www.rfc-editor.org/rfc/rfc7523).
|
||||
|
||||
Minimal config:
|
||||
|
||||
```yaml
|
||||
clientAuthMethod: private_key_jwt
|
||||
clientAssertionKeyPath: /etc/traefik/oidc/client-key.pem
|
||||
clientAssertionKeyID: my-key-2026
|
||||
# clientAssertionAlg: RS256 # default; or PS256/384/512, ES256/384/512
|
||||
```
|
||||
|
||||
Or inline:
|
||||
|
||||
```yaml
|
||||
clientAuthMethod: private_key_jwt
|
||||
clientAssertionPrivateKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
...
|
||||
-----END PRIVATE KEY-----
|
||||
clientAssertionKeyID: my-key-2026
|
||||
```
|
||||
|
||||
Accepted PEM forms: PKCS#8 (`PRIVATE KEY`), PKCS#1 (`RSA PRIVATE KEY`), SEC1
|
||||
(`EC PRIVATE KEY`). The assertion uses `iss=sub=clientID`, `aud=tokenURL`, 60s
|
||||
lifetime, random hex `jti` per request. Sent on `/token` (auth-code + refresh)
|
||||
and `/revoke`. The `kid` must match the public key registered with the IdP.
|
||||
|
||||
`clientSecret` becomes optional with `private_key_jwt`. Existing
|
||||
`client_secret_post` setups are unaffected. Keys are parsed once at startup —
|
||||
rotation requires a Traefik reload.
|
||||
|
||||
See [issue #135](https://github.com/lukaszraczylo/traefikoidc/issues/135).
|
||||
|
||||
### Environment variable names containing `API`
|
||||
|
||||
Traefik reserves `TRAEFIK_API_*`. User vars whose name contains `API` (e.g.
|
||||
|
||||
Reference in New Issue
Block a user