mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-15 04:18:03 +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:
+56
-2
@@ -93,6 +93,38 @@ type Config struct {
|
||||
// providers. Enabling this in production is a security hole — prefer
|
||||
// CACertPath/CACertPEM. Emits a loud warning at startup.
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
|
||||
|
||||
// ClientAuthMethod selects the OAuth 2.0 client authentication method used
|
||||
// at the token / revocation / introspection endpoints. Supported values:
|
||||
//
|
||||
// - "client_secret_post" (default, current behavior): clientSecret is
|
||||
// sent in the request body alongside client_id.
|
||||
// - "private_key_jwt" (RFC 7523 §2.2): the plugin signs a short-lived JWT
|
||||
// assertion with a configured private key and sends it as
|
||||
// client_assertion. Use this when your IdP enforces short-lived secrets
|
||||
// or mandates secretless client auth (Entra ID, Okta, Auth0, Keycloak).
|
||||
//
|
||||
// When set to "private_key_jwt", clientSecret may be left empty and one of
|
||||
// clientAssertionPrivateKey / clientAssertionKeyPath must be configured.
|
||||
ClientAuthMethod string `json:"clientAuthMethod,omitempty"`
|
||||
|
||||
// ClientAssertionPrivateKey is an inline PEM-encoded private key used to
|
||||
// sign client_assertion JWTs. Mutually exclusive with
|
||||
// ClientAssertionKeyPath. Supports PKCS#8, PKCS#1 (RSA), and SEC1 (EC).
|
||||
ClientAssertionPrivateKey string `json:"clientAssertionPrivateKey,omitempty"`
|
||||
|
||||
// ClientAssertionKeyPath is a filesystem path to a PEM-encoded private key,
|
||||
// equivalent to ClientAssertionPrivateKey but loaded from disk.
|
||||
ClientAssertionKeyPath string `json:"clientAssertionKeyPath,omitempty"`
|
||||
|
||||
// ClientAssertionKeyID is the JWK key id (kid) advertised in the JWS
|
||||
// header. Required when using private_key_jwt so the IdP can locate the
|
||||
// matching public key registered for the client.
|
||||
ClientAssertionKeyID string `json:"clientAssertionKeyID,omitempty"`
|
||||
|
||||
// ClientAssertionAlg is the JWS signing algorithm. Defaults to RS256.
|
||||
// Supported: RS256/384/512, PS256/384/512, ES256/384/512.
|
||||
ClientAssertionAlg string `json:"clientAssertionAlg,omitempty"`
|
||||
}
|
||||
|
||||
// loadCACertPool assembles an x509.CertPool from CACertPath and CACertPEM.
|
||||
@@ -323,8 +355,30 @@ func (c *Config) Validate() error {
|
||||
if c.ClientID == "" {
|
||||
return fmt.Errorf("clientID is required")
|
||||
}
|
||||
if c.ClientSecret == "" {
|
||||
return fmt.Errorf("clientSecret is required")
|
||||
authMethod := c.ClientAuthMethod
|
||||
if authMethod == "" {
|
||||
authMethod = "client_secret_post"
|
||||
}
|
||||
switch authMethod {
|
||||
case "client_secret_post", "client_secret_basic":
|
||||
if c.ClientSecret == "" {
|
||||
return fmt.Errorf("clientSecret is required when clientAuthMethod is %q", authMethod)
|
||||
}
|
||||
case "private_key_jwt":
|
||||
if c.ClientAssertionPrivateKey == "" && c.ClientAssertionKeyPath == "" {
|
||||
return fmt.Errorf("clientAssertionPrivateKey or clientAssertionKeyPath is required when clientAuthMethod is private_key_jwt")
|
||||
}
|
||||
if c.ClientAssertionPrivateKey != "" && c.ClientAssertionKeyPath != "" {
|
||||
return fmt.Errorf("only one of clientAssertionPrivateKey or clientAssertionKeyPath may be set")
|
||||
}
|
||||
if c.ClientAssertionKeyID == "" {
|
||||
return fmt.Errorf("clientAssertionKeyID is required when clientAuthMethod is private_key_jwt")
|
||||
}
|
||||
if c.ClientAssertionAlg != "" && !isSupportedClientAssertionAlg(c.ClientAssertionAlg) {
|
||||
return fmt.Errorf("clientAssertionAlg %q is not supported (use RS256/384/512, PS256/384/512, or ES256/384/512)", c.ClientAssertionAlg)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("clientAuthMethod %q is not supported", authMethod)
|
||||
}
|
||||
|
||||
// Validate session encryption key
|
||||
|
||||
Reference in New Issue
Block a user