The 2.4.20.x versions use a backwards incompatible session encryption key so existing sessions (created by versions <=2.4.19.x) are invalid.
Security
- crypto: stretch
OIDCCryptoPassphraseinto the AES key used for the internal "dir"/A256GCM JWE (state cookie, session cookie, cache value encryption) with PBKDF2-HMAC-SHA256 (210,000 iterations, a fixed application-specific salt) instead
NB: the internal session format has changed and is backwards incompatible: existing sessions and cookies will be invalid. - oauth: add
OIDCOAuthVerifyAudienceandOIDCOAuthVerifyIssuerto bind locally validated JWT access tokens to this resource server in OAuth 2.0 Resource Server mode (deprecated); as an alternative (or in addition) the audience can be enforced in the authorization policy with "Require claim aud:<value>" as documented in the Wiki for earlier versions; thanks @EQSTLab - oauth: require the "
exp" claim in locally validated JWT access tokens - discovery: bump the Discovery CSRF token from 8 to 16 random bytes (64 to 128 bits of entropy)
- state: use SHA-256 instead of SHA-1 to compute the browser-fingerprint hash bound into the "state" value (
X-Forwarded-For/User-Agentheaders plus the per-request nonce) - multi-provider: create metadata files (which can hold a dynamically registered client_secret) and file-cache entries with owner-only (0600) permissions instead of relying on the process umask via
APR_OS_DEFAULT - util: compare request-supplied secrets (the state-vs-browser-fingerprint check, the refresh-token XSRF access_token check, and the Discovery CSRF cookie check) using a new constant-time
oidc_util_strcmp_const_time()instead of a plain string compare - log: stop writing access tokens, refresh tokens, client secrets/assertions, authorization codes, PKCE code_verifiers and nonces in full to the debug log but keep a short prefix plus the length for log correlation and redact well-known sensitive parameters out of logged request bodies/URLs and the state-cookie debug dump
- cache/redis: never log the credentials that an
OIDCRedisCacheServervalue may carry - cache/redis: stop logging the cached value when the sanity check on a Redis reply fails: the length mismatch that check guards against is a property of the reply, not of its content, so the two lengths are reported instead of the value
- jwks: rate-limit the cache-bypassing JWKs refresh triggered by an unrecognized "kid"/"x5t" or a signature that does not verify - on the Resource Server path that comes straight off an unauthenticated request, so it could be turned into outbound fetches - and make the window configurable from its 60 second default with the
OIDC_JWKS_FORCED_REFRESH_INTERVALenvironment variable (0-3600 seconds, 0 disables the guard) - util: fix an out-of-bounds read in the
$<n>replacement handling ofOIDCRemoteUserClaim/OIDCOAuthRemoteUserClaimin builds against libpcre 1.x - http: redact every occurrence of a sensitive parameter in a logged request body instead of only the first, and only match a parameter name at a parameter boundary
- refresh: validate a refreshed id_token (signature, decryption and payload claims, plus a "
sub" that must match the one the session was established with) before applying it to the session, instead of storing its claims unverified; thanks @Pyolar
Features
- proto: add support for RFC 8705 - OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens to the OIDC 1.0 and FAPI 2.0 Relying Party profiles (
OIDCCertBoundAccessTokens) - jose/cfg: support publishing an "alg" (algorithm) parameter on the keys in the client JWKs and duplicating a single RSA/EC key under multiple algorithms;
OIDCPublicKeyFiles/OIDCPrivateKeyFilesnow accept an optional"<alg>[+<alg>...]@"prefix, e.g."enc:RSA-OAEP@<file>"publishes the key with"alg":"RSA-OAEP" - discovery: add
OIDCDiscoverIssuersAllowed, anOIDCRedirectURLsAllowed-style list of regular expressions that bounds which issuer values dynamic OpenID Connect Discovery (webfinger/URL-based, account-based, or direct issuer selection viaOIDCMetadataDir) is allowed to resolve to - oauth: add the
OIDCOAuthDecryptSharedKeysdirective to configure dedicated symmetric key(s) for decrypting encrypted JWT access tokens locally, instead of reusing the client secret configured for the OpenID Connect provider - logout: support back-channel logout tokens that carry only "sub" (allowed by OpenID Connect Back-Channel Logout 1.0 section 2.6) for sessions that were created with a distinct "sid"
- logout: support decrypting a symmetrically encrypted back-channel logout token by adding a symmetric key derived from the configured provider's client secret to the decryption key set (alongside the RP's private keys)
Bugfixes
- request: emit
"cty":"JWT"on an encrypted request object and stop appending a trailing NUL byte to the JWE plaintext, so that OPs which strictly follow RFC 7519 section 5.2 (e.g. Keycloak) can decrypt and parse a signed-then- encrypted request object - refresh: when our own refresh token grant fails (typically because another cluster node or worker already spent and rotated the exact same refresh token in a rolling-refresh setup), do a final best-effort re-read of the refresh token cache before locking the token out, and re-use the results another caller populated in the meantime instead of needlessly failing the refresh and killing the session
- refresh: bound the back-off loop that waits for a sibling caller's in-progress refresh grant to the refresh-lock TTL, so a cache backend that does not expire the lock entry promptly can no longer stall the request indefinitely
- jose: size the zlib deflate output buffer with
deflateBound(); the fixedinput_len * 2allocation was smaller than zlib's constant header/trailer overhead for values of just a few bytes, so compressing such values failed and any cache write of a tiny value (e.g. a cached jq expression result of "null") was silently dropped with a "could NOT store" warning - metadata: when no response_type is configured and the client metadata advertises a
response_typesarray that does not include the global/default value, the first advertised entry is now actually used, as documented - oauth: use the JWKs refresh interval (
OIDCJWKSRefreshInterval) as the cache TTL for keys retrieved fromOIDCOAuthVerifyJwksUri, as was documented and intended, instead of accidentally usingOIDCUserInfoRefreshInterval - const: make the NULL-safe _oidc_strcmp() wrapper a byte-exact
strcmp()instead ofapr_strnatcmp()"natural ordering", which skips whitespace and compares digit runs numerically so two different byte strings could compare equal - util: check the
apr_generate_random_bytes()return value when generating a trace-id inoidc_util_set_trace_parent()and skip setting the traceparent header (with a warning) on failure, instead of hashing uninitialized stack bytes into the header - logout: - fix a latent session-state leak on the logout cleanup path
- session management: persist the per-path authentication request parameters (
OIDCPathAuthRequestParams) and scope (OIDCPathScope) used at initial authentication in the session, and reuse them for the silent "check" re-authentication triggered by the RP iframe - metadata: inherit the token endpoint authentication algorithm from the global configuration in per-client
.conffiles; when a .conf does not set "token_endpoint_auth" the globally configured method was inherited but its ":" suffix (e.g. private_key_jwt:RS256) was dropped, so multi-provider setups silently signed client-authentication JWTs with the default algorithm; the fallback now re-appends the global algorithm (an explicit "token_endpoint_auth" in the .conf still overrides both) - appinfo: when concatenating a multi-valued (array) claim into a single delimiter-separated application header/environment variable, backslash-escape any delimiter (and the backslash escape character itself) occurring inside an element value, so a value that contains the delimiter can no longer be mistaken for - or used to inject - additional elements by the application (e.g. a group DN "CN=Admins,OU=Groups" no longer splits on its commas when the delimiter is ","); the escaping is reversible and only affects values that actually contain the delimiter or a backslash
- userinfo: only refresh the access token and retry the userinfo request when the endpoint responded with a 401 (invalid/expired token); other HTTP errors (e.g. 403 or 5xx) are no longer "resolved" by pointlessly presenting a freshly refreshed token, which also avoids needlessly spending a refresh token on a transient server-side error
- proto: when verifying a JWT signature, give dynamically obtained JWKS keys precedence over statically configured keys that carry the same "kid", instead of the other way around; the provider's currently published JWKS is authoritative for its signing keys, so a stale locally configured key with a colliding kid no longer permanently shadows a rotated JWKS key (a warning is logged when such a collision is detected)
- cfg: fix the token endpoint authentication method validation so that
private_key_jwtis consistently rejected for configurations without private signing keys - id_token: fix a memory leak on the id_token parse failure path
- request: fix a memory leak on the authentication request error path
- metrics: give the revoke-session counter a unique name ("request.revoke_session") instead of colliding with the session-management counter
- cfg/oauth: fix
OIDCOAuthIntrospectionEndpointKeyPasswordbeing lost in merged (virtual host) configurations - cfg/cache: fix
OIDCCacheShmMax/OIDCCacheShmEntrySizeMaxexplicitly set to their default value in a vhost not overriding a differing base server value - proto/request_object: fix a leak of the parsed
OIDCRequestObjectconfiguration on the request-object-type error paths - proto/token: fix a NULL-pointer dereference when the token endpoint returns a "use_dpop_nonce" error while DPoP is disabled
- util/jq: fix a negative
OIDC_JQ_FILTER_CACHE_TTLcausing jq filter results to be cached under a NULL key with an already-past expiry - cfg/metadata: fix the globally configured
OIDCProviderSignedJwksUriverification keys being silently dropped in metadata-directory mode when a provider's .conf did not itself supply a "signed_jwks_uri_key", which caused the signed JWKs signature verification to be skipped - metadata: fix the client-metadata "response_types" fallback never running in metadata-directory mode; .conf parsing pre-set response_type to the "code" default, so an OP that only advertises other response types (e.g. implicit) was still sent response_type=code
- oauth: fix a configured
OIDCProviderMetadataRefreshIntervalbeing applied to the OAuth AS metadata cache expiry as microseconds instead of seconds, which made the metadata document re-fetched over HTTP on almost every bearer-token request - metadata: fix several JSON/JWT memory leaks on the provider-metadata refresh, dynamic client (re-)registration and signed-JWKs validation error paths that accumulated over a worker process's lifetime
- cfg/parse: reject overflowing and trailing-junk integer values ("300x", out-of-range) that were silently accepted before, share one strict parse core so the lenient
_oidc_str_to_int()falls back to its default (not 0) on bad input, stop truncating timeout values aboveINT_MAXmicroseconds, and make an invalidOIDCHTTPTimeoutLong/Shortvalue fail config load instead of becoming a 0 (infinite) curl timeout - oauth: parse
OIDCOAuthServerMetadataURLmetadata into a private per-request config view instead of mutating the shared server config with request-pool memory, which was a cross-request use-after-free / data race under threaded MPMs (worker/event) - cfg: fix the "method[:alg]" token/introspection endpoint-authentication pair merging member-wise across virtual hosts, so a vhost that re-set only the method wrongly inherited the base server's algorithm instead of resetting it
- metadata: fix a per-authentication heap leak of the inline "keys" from a provider .conf in metadata-directory mode
- json: fix
oidc_json_object_get_string_arraypushing a NULL entry for each non-string array element instead of skipping it - cache/shm: align the shared memory segment and the per-slot stride to 64 bytes so the
aligned(64)entry slots really are aligned; SIMD stores into an under-aligned slot could crash the server (SIGSEGV) on builds targeting a newer CPU baseline (e.g. Amazon Linux 2023) - oauth: accept a token expiry claim that carries a non-integer value: a NumericDate is a JSON number and may hold a fraction (RFC 7519 section 2)
- session: take the
OIDCSessionCacheFallbackToCookiefallback when the cache has no entry for the session cookie, not only when the cache reports an error - id_token: accept an unsigned (
alg"none") id_token obtained over the back-channel again when the signing algorithm has been pinned to "none" itself, throughOIDCIDTokenSignedResponseAlgor the client metadata; since 2.4.19.4 any pinned algorithm suppressed the "code"-flow exception that OpenID Connect Core 1.0 section 3.1.3.7 item 6 allows, so a pin of "none" - the way an operator, or the OP's own client registration, declares that this OP issues unsigned id_tokens - demanded a signature that cannot exist and failed every login. A pin of any other algorithm still overrides the exception and rejects the unsigned token, and an unsigned id_token received over the front channel is still rejected in all cases - request: omit the
scope=openidparameter that accompanies arequest_urion the authorization request when the configuredOIDCProfileis FAPI20: OpenID Connect Core 1.0 requires it, but FAPI 2.0 Security Profile section 5.3.3.2 allows theclient_idandrequest_uriparameters only; this fixes a regression for FAPI 2.0 deployments introduced in 2.4.19.3 (and present in 2.4.19.4), which sent the parameter unconditionally - dpop: re-create the token endpoint client authentication when retrying a token endpoint call with a server-provided DPoP nonce
- session: refuse to write a client-side session cookie that would have to be split over more chunks than can be read back
- dpop: apply the FAPI 2.0 "mutual-TLS or DPoP" rule to a statically configured OP too, i.e. one whose endpoints are all set with
OIDCProvider*directives; standing down from mandating DPoP relied on a value that is only resolved while parsing provider metadata, so such an OP was still required to do DPoP - oauth: clamp a token expiry claim instead of letting it wrap; a far-future NumericDate turned into a negative - i.e. already expired - timestamp, which made the validation result uncacheable so every request went out to the introspection endpoint
- util: size the output buffer of a regular-expression substitution from what the result actually needs, so
OIDCRemoteUserClaim/OIDCOAuthRemoteUserClaimno longer fail on a claim value of 254 characters or more - metadata: report an
mtls_endpoint_aliasesentry that is not a valid URL as an error instead of quietly falling back to the conventional endpoint, which is precisely the one that does not bind the access token to the TLS client certificate
Performance
- perf: check Redis connections out per request from a per-process idle pool instead of serializing every worker thread's network round-trip on one shared connection under a mutex
- perf: keep the parsed session state of server-cache sessions in a per-process cache keyed by session id and validated against the raw cached session document, so the multi-KB JSON parse of the session runs only when the session actually changed
- perf: extend the parsed-session cache to client-cookie sessions so an unchanged session cookie skips the per-request JWE decryption, decompression and JSON parse
- perf: replace the curl-based URL encode/decode in http.c with a plain RFC 3986 percent-codec, avoiding a throwaway CURL handle init/cleanup plus heap churn on every call
- perf: keep a small pool of reusable libcurl easy handles instead of creating and destroying one per outbound call: a reused handle retains its connection cache, DNS cache and TLS session cache, so consecutive requests to the same endpoint (token, userinfo, introspection, JWKs, metadata) reuse the TCP/TLS connection instead of paying DNS + TCP + TLS handshake every time; handles whose request failed are destroyed rather than reused, handles inherited over
fork()are dropped in the child (their connections share descriptors and TLS state with the parent), and TCP keepalive is enabled on outbound connections - perf: index the
shmcache with an in-segment hash table (bucket chains plus a free list, 1-based slot indexes) instead of linearly scanning all slots on every get/set while holding the global mutex: operations become O(chain) independent ofOIDCCacheShmMax - perf: cache the flattened claim name/value pairs (header-name construction, value rendering of object/array claims, base64url/latin1 encoding) keyed by the identity of the shared claims object from the parsed-session cache, so the per-claim flattening work runs once per session change instead of on every request
- perf: release the Redis serialization mutex while sleeping between connect-retry attempts (default 300ms): other worker threads in the process can then proceed (and fail fast themselves) instead of queueing behind the sleeping thread for the whole interval on a transient Redis outage
- perf: compress internal encrypted JWTs (state/session cookies, encrypted cache values) with deflate level/window/memory parameters sized for their small payloads instead of
Z_BEST_COMPRESSIONwith a 32KB window, cutting the ~256KB of per-call zlib allocations by an order of magnitude and halving the envelope creation cost - perf: hoist the string-length evaluation out of the per-character loop in
oidc_http_hdr_normalize_name(was re-evaluated every iteration) - perf: keep parsed provider metadata from the metadata directory in a process-level cache validated by the file's mtime+size, so the per-request disk read + JSON parse of the provider .provider file only happens when the file actually changed
- perf: hand out JWKs-cache keys as shared objects instead of per-request refcounted copies
- perf: restore the parsed id_token/userinfo/scope request state from the previous request on internal redirects (ErrorDocument, mod_rewrite) instead of re-loading and re-parsing the whole session from the cache
- perf: cache parsed JWKs selection results per process keyed by (jwks-uri, kid, x5t, kty) so repeated JWT validations against the same signing key skip the per-validation JSON decode of the JWKs document and the bignum-parsing cjose key imports
- perf: take a shared (instead of exclusive) file lock when reading file-cache entries so concurrent requests reading the same (session) entry no longer serialize
- perf: cache locally validated JWT access token claims keyed by the token and bounded by its "
exp" claim, which is now a required claim, mirroring the existing introspection-result cache, so repeated resource-server requests with the same bearer token skip decryption-key setup and signature re-verification;OIDCOAuthTokenIntrospectionIntervalsemantics apply - perf: cache compiled Require-claim regular expressions for the lifetime of the server process instead of recompiling them on every authorization evaluation
- perf: evict only the least-recently-used entry from the process-local caches when they fill up, instead of clearing the whole cache, and log a rate-limited warning when one is undersized; the session and appinfo cache defaults are raised to 2000 and 1000
Commercial
- commercial subscription based support for large enterprise businesses is available via sales@openidc.com
- licensed binary packages for various other platforms such as Microsoft Windows, Red Hat Enterprise Linux 7, older Ubuntu and Debian distros, Oracle HTTP Server 12.x/14.x and IBM HTTP Server 9.x, are available under a commercial license and agreement via sales@openidc.com
- support for Redis/Valkey over TLS, Redis/Valkey (TLS) Sentinel, and Redis/Valkey (TLS) Cluster is available under a commercial license and agreement via sales@openidc.com
The RPM packages below are signed with the following RSA PGP key:
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBGh53lgBCADCyoOkfnE5h5rBLlf02oFpI/z2vUXK5W4T56xnNPu0/iIOxbBk
YX9rSypZFhfjv28lhGgelWEg28Ab/Yxs6l0obCgDEuFUDQ5Dv+N+YSMy67vtLwYW
9LM5p9fMN9bXOa62PwvtzRzh+xRyRBcIfMacGJC+SqUK6QhzC0lNwCsr1OaWjzon
mkaodwrloNMxEZVvFn63PvuQDZ3wwQty+0XpYiiChMssGBn6nmPDQJ7pDtQDkhfD
Z5FKY6K7AQJ4fneiVCLGngPBwTXBGcfWa+Y0HCS2ghQwDO6jYXd5GjowVDTjfMK3
QJ3e26Ox9X3V0Fl04R1i5EthEkAWGfy1lksvABEBAAG0HU9wZW5JREMgPHN1cHBv
cnRAb3BlbmlkYy5jb20+iQFRBBMBCgA7FiEEFdjWJA1IGDkAITSxnyZY1L0OSOMF
Amh53lgCGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQnyZY1L0OSONG
Jgf+II0wG96R0g28Kp+R4AYzSdX0CEqr6OhwHHw4cFpLsHxZNhojo7I4OnLKEdfc
lFl37rE+hG3QpzD/b4S/fpPjd4hcLkguBQxtdxqZZVAIT8HWbveHRkI8MNnjOPwv
Hy6jBncMs1IT/URV2si/Q34+PLo8tvo/lXNa16svVl2DoYXO8MCszgCE1bx055EF
XPh4Teu5Y4OLHECSicMxrmN746dAD121zy4bLLx9mZ0erhLjvkj1vkFmlHFKyvwY
/pbSqXs9hW/wweW1oQ/xEIJWWS71PeoutUBjr0WC4sILnR5PBPZplgNh297Qex6g
qaW3io0tCH9KxU1tXYn/iL/hbQ==
=mlOy
-----END PGP PUBLIC KEY BLOCK-----