Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-Authlib for openSUSE:Factory checked in at 2026-05-28 17:28:26 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-Authlib (Old) and /work/SRC/openSUSE:Factory/.python-Authlib.new.1937 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-Authlib" Thu May 28 17:28:26 2026 rev:32 rq:1355533 version:1.7.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-Authlib/python-Authlib.changes 2026-04-25 21:37:38.146419499 +0200 +++ /work/SRC/openSUSE:Factory/.python-Authlib.new.1937/python-Authlib.changes 2026-05-28 17:29:20.437683245 +0200 @@ -1,0 +2,10 @@ +Thu May 28 06:41:00 UTC 2026 - Markéta Machová <[email protected]> + +- Update to 1.7.2 (bsc#1266665, CVE-2026-44681) + * Allow non-recommended algorithms in ClientSecretJWT and PrivateKey + * Validate BCP47 language tags with a regex + * Fix RFC7523 signing with non RSA keys + * redirecting to unvalidated redirect_uri on InvalidScopeError in OIDC grants + * authlib.jose deprecation warning poping from _joserfc_helpers + +------------------------------------------------------------------- Old: ---- authlib-1.7.0.tar.gz New: ---- authlib-1.7.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-Authlib.spec ++++++ --- /var/tmp/diff_new_pack.w2rEIC/_old 2026-05-28 17:29:21.233716195 +0200 +++ /var/tmp/diff_new_pack.w2rEIC/_new 2026-05-28 17:29:21.233716195 +0200 @@ -19,7 +19,7 @@ %define modname authlib %{?sle15_python_module_pythons} Name: python-Authlib -Version: 1.7.0 +Version: 1.7.2 Release: 0 Summary: Python library for building OAuth and OpenID Connect servers License: BSD-3-Clause ++++++ authlib-1.7.0.tar.gz -> authlib-1.7.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/.github/workflows/pypi.yml new/authlib-1.7.2/.github/workflows/pypi.yml --- old/authlib-1.7.0/.github/workflows/pypi.yml 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/.github/workflows/pypi.yml 2026-05-06 10:08:51.000000000 +0200 @@ -54,3 +54,15 @@ - name: Push build artifacts to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + + github-release: + name: create github release + needs: publish + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/.pre-commit-config.yaml new/authlib-1.7.2/.pre-commit-config.yaml --- old/authlib-1.7.0/.pre-commit-config.yaml 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/.pre-commit-config.yaml 2026-05-06 10:08:51.000000000 +0200 @@ -4,7 +4,7 @@ - commit-msg repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.8 + rev: v0.15.12 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/README.md new/authlib-1.7.2/README.md --- old/authlib-1.7.0/README.md 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/README.md 2026-05-06 10:08:51.000000000 +0200 @@ -45,33 +45,33 @@ Generic, spec-compliant implementation to build clients and providers: -- [The OAuth 1.0 Protocol](https://docs.authlib.org/en/stable/basic/oauth1.html) - - [RFC5849: The OAuth 1.0 Protocol](https://docs.authlib.org/en/stable/specs/rfc5849.html) -- [The OAuth 2.0 Authorization Framework](https://docs.authlib.org/en/stable/basic/oauth2.html) - - [RFC6749: The OAuth 2.0 Authorization Framework](https://docs.authlib.org/en/stable/specs/rfc6749.html) - - [RFC6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://docs.authlib.org/en/stable/specs/rfc6750.html) - - [RFC7009: OAuth 2.0 Token Revocation](https://docs.authlib.org/en/stable/specs/rfc7009.html) - - [RFC7523: JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://docs.authlib.org/en/stable/specs/rfc7523.html) - - [RFC7591: OAuth 2.0 Dynamic Client Registration Protocol](https://docs.authlib.org/en/stable/specs/rfc7591.html) - - [RFC7592: OAuth 2.0 Dynamic Client Registration Management Protocol](https://docs.authlib.org/en/stable/specs/rfc7592.html) - - [RFC7636: Proof Key for Code Exchange by OAuth Public Clients](https://docs.authlib.org/en/stable/specs/rfc7636.html) - - [RFC7662: OAuth 2.0 Token Introspection](https://docs.authlib.org/en/stable/specs/rfc7662.html) - - [RFC8414: OAuth 2.0 Authorization Server Metadata](https://docs.authlib.org/en/stable/specs/rfc8414.html) - - [RFC8628: OAuth 2.0 Device Authorization Grant](https://docs.authlib.org/en/stable/specs/rfc8628.html) - - [RFC9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://docs.authlib.org/en/stable/specs/rfc9068.html) - - [RFC9101: The OAuth 2.0 Authorization Framework: JWT-Secured Authorization Request (JAR)](https://docs.authlib.org/en/stable/specs/rfc9101.html) - - [RFC9207: OAuth 2.0 Authorization Server Issuer Identification](https://docs.authlib.org/en/stable/specs/rfc9207.html) +- [The OAuth 1.0 Protocol](https://docs.authlib.org/en/stable/oauth1/index.html) + - [RFC5849: The OAuth 1.0 Protocol](https://docs.authlib.org/en/stable/oauth1/specs/rfc5849.html) +- [The OAuth 2.0 Authorization Framework](https://docs.authlib.org/en/stable/oauth2/index.html) + - [RFC6749: The OAuth 2.0 Authorization Framework](https://docs.authlib.org/en/stable/oauth2/specs/rfc6749.html) + - [RFC6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://docs.authlib.org/en/stable/oauth2/specs/rfc6750.html) + - [RFC7009: OAuth 2.0 Token Revocation](https://docs.authlib.org/en/stable/oauth2/specs/rfc7009.html) + - [RFC7523: JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://docs.authlib.org/en/stable/oauth2/specs/rfc7523.html) + - [RFC7591: OAuth 2.0 Dynamic Client Registration Protocol](https://docs.authlib.org/en/stable/oauth2/specs/rfc7591.html) + - [RFC7592: OAuth 2.0 Dynamic Client Registration Management Protocol](https://docs.authlib.org/en/stable/oauth2/specs/rfc7592.html) + - [RFC7636: Proof Key for Code Exchange by OAuth Public Clients](https://docs.authlib.org/en/stable/oauth2/specs/rfc7636.html) + - [RFC7662: OAuth 2.0 Token Introspection](https://docs.authlib.org/en/stable/oauth2/specs/rfc7662.html) + - [RFC8414: OAuth 2.0 Authorization Server Metadata](https://docs.authlib.org/en/stable/oauth2/specs/rfc8414.html) + - [RFC8628: OAuth 2.0 Device Authorization Grant](https://docs.authlib.org/en/stable/oauth2/specs/rfc8628.html) + - [RFC9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://docs.authlib.org/en/stable/oauth2/specs/rfc9068.html) + - [RFC9101: The OAuth 2.0 Authorization Framework: JWT-Secured Authorization Request (JAR)](https://docs.authlib.org/en/stable/oauth2/specs/rfc9101.html) + - [RFC9207: OAuth 2.0 Authorization Server Issuer Identification](https://docs.authlib.org/en/stable/oauth2/specs/rfc9207.html) - [Javascript Object Signing and Encryption](https://docs.authlib.org/en/stable/jose/index.html) - [RFC7515: JSON Web Signature](https://docs.authlib.org/en/stable/jose/jws.html) - [RFC7516: JSON Web Encryption](https://docs.authlib.org/en/stable/jose/jwe.html) - [RFC7517: JSON Web Key](https://docs.authlib.org/en/stable/jose/jwk.html) - - [RFC7518: JSON Web Algorithms](https://docs.authlib.org/en/stable/specs/rfc7518.html) + - [RFC7518: JSON Web Algorithms](https://docs.authlib.org/en/stable/jose/specs/rfc7518.html) - [RFC7519: JSON Web Token](https://docs.authlib.org/en/stable/jose/jwt.html) - - [RFC7638: JSON Web Key (JWK) Thumbprint](https://docs.authlib.org/en/stable/specs/rfc7638.html) + - [RFC7638: JSON Web Key (JWK) Thumbprint](https://docs.authlib.org/en/stable/jose/specs/rfc7638.html) - [ ] RFC7797: JSON Web Signature (JWS) Unencoded Payload Option - - [RFC8037: ECDH in JWS and JWE](https://docs.authlib.org/en/stable/specs/rfc8037.html) + - [RFC8037: ECDH in JWS and JWE](https://docs.authlib.org/en/stable/jose/specs/rfc8037.html) - [ ] draft-madden-jose-ecdh-1pu-04: Public Key Authenticated Encryption for JOSE: ECDH-1PU -- [OpenID Connect 1.0](https://docs.authlib.org/en/stable/specs/oidc.html) +- [OpenID Connect 1.0](https://docs.authlib.org/en/stable/oauth2/specs/oidc.html) - [x] OpenID Connect Core 1.0 - [x] OpenID Connect Discovery 1.0 - [x] OpenID Connect Dynamic Client Registration 1.0 @@ -80,30 +80,30 @@ Connect third party OAuth providers with Authlib built-in client integrations: - Requests - - [OAuth1Session](https://docs.authlib.org/en/stable/client/requests.html#requests-oauth-1-0) - - [OAuth2Session](https://docs.authlib.org/en/stable/client/requests.html#requests-oauth-2-0) - - [OpenID Connect](https://docs.authlib.org/en/stable/client/requests.html#requests-openid-connect) - - [AssertionSession](https://docs.authlib.org/en/stable/client/requests.html#requests-service-account) + - [OAuth1Session](https://docs.authlib.org/en/stable/oauth1/client/http/requests.html) + - [OAuth2Session](https://docs.authlib.org/en/stable/oauth2/client/http/requests.html#requests-oauth-2-0) + - [OpenID Connect](https://docs.authlib.org/en/stable/oauth2/client/http/requests.html#requests-openid-connect) + - [AssertionSession](https://docs.authlib.org/en/stable/oauth2/client/http/requests.html#requests-service-account) - HTTPX - - [AsyncOAuth1Client](https://docs.authlib.org/en/stable/client/httpx.html#httpx-oauth-1-0) - - [AsyncOAuth2Client](https://docs.authlib.org/en/stable/client/httpx.html#httpx-oauth-2-0) - - [OpenID Connect](https://docs.authlib.org/en/stable/client/httpx.html#httpx-oauth-2-0) - - [AsyncAssertionClient](https://docs.authlib.org/en/stable/client/httpx.html#async-service-account) -- [Flask OAuth Client](https://docs.authlib.org/en/stable/client/flask.html) -- [Django OAuth Client](https://docs.authlib.org/en/stable/client/django.html) -- [Starlette OAuth Client](https://docs.authlib.org/en/stable/client/starlette.html) -- [FastAPI OAuth Client](https://docs.authlib.org/en/stable/client/fastapi.html) + - [AsyncOAuth1Client](https://docs.authlib.org/en/stable/oauth1/client/http/httpx.html) + - [AsyncOAuth2Client](https://docs.authlib.org/en/stable/oauth2/client/http/httpx.html#httpx-oauth-2-0) + - [OpenID Connect](https://docs.authlib.org/en/stable/oauth2/client/http/httpx.html#httpx-oauth-2-0) + - [AsyncAssertionClient](https://docs.authlib.org/en/stable/oauth2/client/http/httpx.html#async-service-account) +- [Flask OAuth Client](https://docs.authlib.org/en/stable/oauth2/client/web/flask.html) +- [Django OAuth Client](https://docs.authlib.org/en/stable/oauth2/client/web/django.html) +- [Starlette OAuth Client](https://docs.authlib.org/en/stable/oauth2/client/web/starlette.html) +- [FastAPI OAuth Client](https://docs.authlib.org/en/stable/oauth2/client/web/fastapi.html) Build your own OAuth 1.0, OAuth 2.0, and OpenID Connect providers: - Flask - - [Flask OAuth 1.0 Provider](https://docs.authlib.org/en/stable/flask/1/) - - [Flask OAuth 2.0 Provider](https://docs.authlib.org/en/stable/flask/2/) - - [Flask OpenID Connect 1.0 Provider](https://docs.authlib.org/en/stable/flask/2/openid-connect.html) + - [Flask OAuth 1.0 Provider](https://docs.authlib.org/en/stable/oauth1/provider/flask/index.html) + - [Flask OAuth 2.0 Provider](https://docs.authlib.org/en/stable/oauth2/authorization-server/flask/index.html) + - [Flask OpenID Connect 1.0 Provider](https://docs.authlib.org/en/stable/oauth2/authorization-server/flask/openid-connect.html) - Django - - [Django OAuth 1.0 Provider](https://docs.authlib.org/en/stable/django/1/) - - [Django OAuth 2.0 Provider](https://docs.authlib.org/en/stable/django/2/) - - [Django OpenID Connect 1.0 Provider](https://docs.authlib.org/en/stable/django/2/openid-connect.html) + - [Django OAuth 1.0 Provider](https://docs.authlib.org/en/stable/oauth1/provider/django/index.html) + - [Django OAuth 2.0 Provider](https://docs.authlib.org/en/stable/oauth2/authorization-server/django/index.html) + - [Django OpenID Connect 1.0 Provider](https://docs.authlib.org/en/stable/oauth2/authorization-server/django/openid-connect.html) ## Useful Links diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/authlib/_joserfc_helpers.py new/authlib-1.7.2/authlib/_joserfc_helpers.py --- old/authlib-1.7.0/authlib/_joserfc_helpers.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/authlib/_joserfc_helpers.py 2026-05-06 10:08:51.000000000 +0200 @@ -1,3 +1,4 @@ +import sys from typing import Any from joserfc.jwk import KeySet @@ -5,16 +6,18 @@ from authlib.common.encoding import json_loads from authlib.deprecate import deprecate -from authlib.jose import ECKey -from authlib.jose import OctKey -from authlib.jose import OKPKey -from authlib.jose import RSAKey def import_any_key(data: Any): - if isinstance(data, (OctKey, RSAKey, ECKey, OKPKey)): - deprecate("Please use joserfc to import keys.", version="2.0.0") - return import_key(data.as_dict(is_private=not data.public_only)) + if "authlib.jose" in sys.modules: + from authlib.jose.rfc7518 import ECKey + from authlib.jose.rfc7518 import OctKey + from authlib.jose.rfc7518 import RSAKey + from authlib.jose.rfc8037 import OKPKey + + if isinstance(data, (OctKey, RSAKey, ECKey, OKPKey)): + deprecate("Please use joserfc to import keys.", version="2.0.0") + return import_key(data.as_dict(is_private=not data.public_only)) if ( isinstance(data, str) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/authlib/common/language.py new/authlib-1.7.2/authlib/common/language.py --- old/authlib-1.7.0/authlib/common/language.py 1970-01-01 01:00:00.000000000 +0100 +++ new/authlib-1.7.2/authlib/common/language.py 2026-05-06 10:08:51.000000000 +0200 @@ -0,0 +1,13 @@ +import re + +# Structurally validates BCP 47 language tags (RFC 5646). +# Accepts private-use tags (x-...) and standard tags (2-8 alpha + optional subtags). +# Does not validate subtags against the IANA registry. +_LANGUAGE_TAG_RE = re.compile( + r"^(x(-[a-zA-Z0-9]{1,8})+|[a-zA-Z]{2,8}(-[a-zA-Z0-9]{1,8})*)$" +) + + +def is_valid_language_tag(tag): + """Return True if tag is a structurally valid BCP 47 language tag.""" + return isinstance(tag, str) and bool(_LANGUAGE_TAG_RE.match(tag)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/authlib/consts.py new/authlib-1.7.2/authlib/consts.py --- old/authlib-1.7.0/authlib/consts.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/authlib/consts.py 2026-05-06 10:08:51.000000000 +0200 @@ -1,5 +1,5 @@ name = "Authlib" -version = "1.7.0" +version = "1.7.2" author = "Hsiaoming Yang <[email protected]>" homepage = "https://authlib.org" default_user_agent = f"{name}/{version} (+{homepage})" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/authlib/oauth2/rfc7523/assertion.py new/authlib-1.7.2/authlib/oauth2/rfc7523/assertion.py --- old/authlib-1.7.0/authlib/oauth2/rfc7523/assertion.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/authlib/oauth2/rfc7523/assertion.py 2026-05-06 10:08:51.000000000 +0200 @@ -44,7 +44,7 @@ if claims: payload.update(claims) - return jwt.encode(header, payload, import_any_key(key)) + return jwt.encode(header, payload, import_any_key(key), algorithms=[header["alg"]]) def client_secret_jwt_sign( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/authlib/oauth2/rfc7523/auth.py new/authlib-1.7.2/authlib/oauth2/rfc7523/auth.py --- old/authlib-1.7.0/authlib/oauth2/rfc7523/auth.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/authlib/oauth2/rfc7523/auth.py 2026-05-06 10:08:51.000000000 +0200 @@ -1,5 +1,7 @@ from joserfc.jwk import OctKey from joserfc.jwk import RSAKey +from joserfc.jwk import ECKey +from joserfc.jwk import OKPKey from authlib.common.urls import add_params_to_qs @@ -100,7 +102,7 @@ alg = "RS256" def sign(self, auth, token_endpoint): - if isinstance(auth.client_secret, RSAKey): + if isinstance(auth.client_secret, (RSAKey, ECKey, OKPKey)): key = auth.client_secret else: key = RSAKey.import_key(auth.client_secret) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/authlib/oauth2/rfc8414/models.py new/authlib-1.7.2/authlib/oauth2/rfc8414/models.py --- old/authlib-1.7.0/authlib/oauth2/rfc8414/models.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/authlib/oauth2/rfc8414/models.py 2026-05-06 10:08:51.000000000 +0200 @@ -1,3 +1,4 @@ +from authlib.common.language import is_valid_language_tag from authlib.common.security import is_secure_transport from authlib.common.urls import is_valid_url from authlib.common.urls import urlparse @@ -208,7 +209,7 @@ [RFC5646]. If omitted, the set of supported languages and scripts is unspecified. """ - validate_array_value(self, "ui_locales_supported") + validate_language_tags_array(self, "ui_locales_supported") def validate_op_policy_uri(self): """OPTIONAL. URL that the authorization server provides to the @@ -411,6 +412,15 @@ raise ValueError(f'"{key}" MUST be JSON array') +def validate_language_tags_array(metadata, key): + validate_array_value(metadata, key) + values = metadata.get(key) + if values is not None: + for tag in values: + if not is_valid_language_tag(tag): + raise ValueError(f'"{key}" MUST contain BCP 47 language tags') + + def validate_boolean_value(metadata, key): if key not in metadata: return diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/authlib/oidc/core/claims.py new/authlib-1.7.2/authlib/oidc/core/claims.py --- old/authlib-1.7.0/authlib/oidc/core/claims.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/authlib/oidc/core/claims.py 2026-05-06 10:08:51.000000000 +0200 @@ -6,6 +6,7 @@ from joserfc.errors import MissingClaimError from authlib.common.encoding import to_bytes +from authlib.common.language import is_valid_language_tag from authlib.oauth2.claims import JWTClaims from authlib.oauth2.rfc6749.util import scope_to_list @@ -244,6 +245,12 @@ "phone": ["phone_number", "phone_number_verified"], } + def validate_locale(self): + """Validate the locale claim is a BCP 47 language tag.""" + locale = self.get("locale") + if locale is not None and not is_valid_language_tag(locale): + raise ValueError('"locale" MUST be a BCP 47 language tag') + def filter(self, scope: str): """Return a new UserInfo object containing only the claims matching the scope passed in parameter.""" scope = scope_to_list(scope) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/authlib/oidc/core/grants/hybrid.py new/authlib-1.7.2/authlib/oidc/core/grants/hybrid.py --- old/authlib-1.7.0/authlib/oidc/core/grants/hybrid.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/authlib/oidc/core/grants/hybrid.py 2026-05-06 10:08:51.000000000 +0200 @@ -49,19 +49,20 @@ raise NotImplementedError() def validate_authorization_request(self): - if not is_openid_scope(self.request.payload.scope): - raise InvalidScopeError( - "Missing 'openid' scope", - redirect_uri=self.request.payload.redirect_uri, - redirect_fragment=True, - ) self.register_hook( "after_validate_authorization_request_payload", lambda grant, redirect_uri: validate_nonce( grant.request, grant.exists_nonce, required=True ), ) - return validate_code_authorization_request(self) + redirect_uri = validate_code_authorization_request(self) + if not is_openid_scope(self.request.payload.scope): + raise InvalidScopeError( + "Missing 'openid' scope", + redirect_uri=redirect_uri, + redirect_fragment=True, + ) + return redirect_uri def create_granted_params(self, grant_user): self.request.user = grant_user diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/authlib/oidc/core/grants/implicit.py new/authlib-1.7.2/authlib/oidc/core/grants/implicit.py --- old/authlib-1.7.0/authlib/oidc/core/grants/implicit.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/authlib/oidc/core/grants/implicit.py 2026-05-06 10:08:51.000000000 +0200 @@ -68,13 +68,13 @@ return [client.get_client_id()] def validate_authorization_request(self): + redirect_uri = super().validate_authorization_request() if not is_openid_scope(self.request.payload.scope): raise InvalidScopeError( "Missing 'openid' scope", - redirect_uri=self.request.payload.redirect_uri, + redirect_uri=redirect_uri, redirect_fragment=True, ) - redirect_uri = super().validate_authorization_request() try: validate_nonce(self.request, self.exists_nonce, required=True) except OAuth2Error as error: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/authlib/oidc/discovery/models.py new/authlib-1.7.2/authlib/oidc/discovery/models.py --- old/authlib-1.7.0/authlib/oidc/discovery/models.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/authlib/oidc/discovery/models.py 2026-05-06 10:08:51.000000000 +0200 @@ -1,6 +1,7 @@ from authlib.oauth2.rfc8414 import AuthorizationServerMetadata from authlib.oauth2.rfc8414.models import validate_array_value from authlib.oauth2.rfc8414.models import validate_boolean_value +from authlib.oauth2.rfc8414.models import validate_language_tags_array class OpenIDProviderMetadata(AuthorizationServerMetadata): @@ -236,7 +237,7 @@ language tag values. Not all languages and scripts are necessarily supported for all Claim values. """ - validate_array_value(self, "claims_locales_supported") + validate_language_tags_array(self, "claims_locales_supported") def validate_claims_parameter_supported(self): """OPTIONAL. Boolean value specifying whether the OP supports use of diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/docs/upgrades/changelog.rst new/authlib-1.7.2/docs/upgrades/changelog.rst --- old/authlib-1.7.0/docs/upgrades/changelog.rst 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/docs/upgrades/changelog.rst 2026-05-06 10:08:51.000000000 +0200 @@ -6,6 +6,30 @@ Here you can see the full list of changes between each Authlib release. +Version 1.7.2 +------------- + +**Released on May 6, 2026** + +- Fix ``PrivateKeyJWT`` rejecting ``ECKey`` private keys (e.g. for ``ES256``). + The client private key is no longer forcibly imported as an ``RSAKey``; + ``joserfc``'s key auto-detection is used instead. :pr:`852` +- Allow ``ClientSecretJWT`` and ``PrivateKeyJWT`` to sign client assertions + with non-recommended algorithms (e.g. ``RS384``) when explicitly set via the + ``alg`` parameter. :issue:`883` +- Validate BCP 47 language tags in ``ui_locales_supported``, ``claims_locales_supported`` + and ``UserInfo.locale``. :pr:`873` + +Version 1.7.1 +------------- + +**Released on May 4, 2026** + +- Fix ``AuthlibDeprecationWarning`` being emitted on import when using integrations + that do not use ``authlib.jose`` directly. :issue:`880` +- Fix redirecting to unvalidated ``redirect_uri`` on ``InvalidScopeError`` + in ``OpenIDImplicitGrant`` and ``OpenIDHybridGrant``. + Version 1.7.0 ------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/tests/core/test_oauth2/test_rfc7523_private_key.py new/authlib-1.7.2/tests/core/test_oauth2/test_rfc7523_private_key.py --- old/authlib-1.7.0/tests/core/test_oauth2/test_rfc7523_private_key.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/tests/core/test_oauth2/test_rfc7523_private_key.py 2026-05-06 10:08:51.000000000 +0200 @@ -1,7 +1,9 @@ import time +from types import SimpleNamespace from unittest import mock from joserfc import jwt +from joserfc.jwk import ECKey from joserfc.jwk import RSAKey from authlib.oauth2.rfc7523 import PrivateKeyJWT @@ -229,3 +231,32 @@ "role": "bar", } assert {"alg": "RS256", "typ": "JWT"} == decoded.header + + +def test_sign_with_ec_key(): + """``PrivateKeyJWT`` should accept an ``ECKey`` private key and sign with ES256.""" + ec_key = ECKey.generate_key("P-256", private=True) + jwt_signer = PrivateKeyJWT(alg="ES256") + auth = SimpleNamespace(client_id="client_id_1", client_secret=ec_key) + + data = jwt_signer.sign(auth, "https://provider.test/oauth/access_token") + decoded = jwt.decode(data, ec_key) + + assert decoded.claims["iss"] == "client_id_1" + assert decoded.claims["sub"] == "client_id_1" + assert decoded.claims["aud"] == "https://provider.test/oauth/access_token" + assert decoded.header["alg"] == "ES256" + + +def test_sign_with_non_recommended_alg(): + """Signing must work with algorithms outside joserfc's recommended set.""" + jwt_signer = PrivateKeyJWT(alg="RS384") + + auth = mock.MagicMock() + auth.client_id = "client_id_1" + auth.client_secret = private_key + + data = jwt_signer.sign(auth, "https://provider.test/oauth/access_token") + decoded = jwt.decode(data, RSAKey.import_key(public_key), algorithms=["RS384"]) + + assert decoded.header["alg"] == "RS384" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/tests/core/test_oauth2/test_rfc8414.py new/authlib-1.7.2/tests/core/test_oauth2/test_rfc8414.py --- old/authlib-1.7.0/tests/core/test_oauth2/test_rfc8414.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/tests/core/test_oauth2/test_rfc8414.py 2026-05-06 10:08:51.000000000 +0200 @@ -260,14 +260,38 @@ metadata = AuthorizationServerMetadata() metadata.validate_ui_locales_supported() + # valid + metadata = AuthorizationServerMetadata({"ui_locales_supported": ["en"]}) + metadata.validate_ui_locales_supported() + + metadata = AuthorizationServerMetadata( + {"ui_locales_supported": ["en-US", "fr-FR", "zh-Hans-CN"]} + ) + metadata.validate_ui_locales_supported() + + # private use + metadata = AuthorizationServerMetadata({"ui_locales_supported": ["x-custom"]}) + metadata.validate_ui_locales_supported() + # not array metadata = AuthorizationServerMetadata({"ui_locales_supported": "en"}) with pytest.raises(ValueError, match="JSON array"): metadata.validate_ui_locales_supported() - # valid - metadata = AuthorizationServerMetadata({"ui_locales_supported": ["en"]}) - metadata.validate_ui_locales_supported() + # underscore instead of hyphen + metadata = AuthorizationServerMetadata({"ui_locales_supported": ["fr_FR"]}) + with pytest.raises(ValueError, match="BCP 47"): + metadata.validate_ui_locales_supported() + + # single char (not private-use) + metadata = AuthorizationServerMetadata({"ui_locales_supported": ["a-US"]}) + with pytest.raises(ValueError, match="BCP 47"): + metadata.validate_ui_locales_supported() + + # subtag too long + metadata = AuthorizationServerMetadata({"ui_locales_supported": ["toolongsubtag"]}) + with pytest.raises(ValueError, match="BCP 47"): + metadata.validate_ui_locales_supported() def test_validate_op_policy_uri(): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/tests/core/test_oidc/test_core.py new/authlib-1.7.2/tests/core/test_oidc/test_core.py --- old/authlib-1.7.0/tests/core/test_oidc/test_core.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/tests/core/test_oidc/test_core.py 2026-05-06 10:08:51.000000000 +0200 @@ -172,3 +172,17 @@ assert user.email is None with pytest.raises(AttributeError): user.invalid # noqa: B018 + + +def test_userinfo_validate_locale(): + UserInfo({"sub": "1"}).validate_locale() + UserInfo({"sub": "1", "locale": "en"}).validate_locale() + UserInfo({"sub": "1", "locale": "en-US"}).validate_locale() + UserInfo({"sub": "1", "locale": "zh-Hans-CN"}).validate_locale() + UserInfo({"sub": "1", "locale": "x-custom"}).validate_locale() + + with pytest.raises(ValueError, match="BCP 47"): + UserInfo({"sub": "1", "locale": "fr_FR"}).validate_locale() + + with pytest.raises(ValueError, match="BCP 47"): + UserInfo({"sub": "1", "locale": "toolongsubtag"}).validate_locale() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/tests/core/test_oidc/test_discovery.py new/authlib-1.7.2/tests/core/test_oidc/test_discovery.py --- old/authlib-1.7.0/tests/core/test_oidc/test_discovery.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/tests/core/test_oidc/test_discovery.py 2026-05-06 10:08:51.000000000 +0200 @@ -119,6 +119,19 @@ def test_validate_claims_locales_supported(): _call_validate_array("claims_locales_supported", ["en-US"]) + metadata = OpenIDProviderMetadata( + {"claims_locales_supported": ["en-US", "fr-FR", "zh-Hans-CN"]} + ) + metadata.validate_claims_locales_supported() + + metadata = OpenIDProviderMetadata({"claims_locales_supported": ["fr_FR"]}) + with pytest.raises(ValueError, match="BCP 47"): + metadata.validate_claims_locales_supported() + + metadata = OpenIDProviderMetadata({"claims_locales_supported": ["toolongsubtag"]}) + with pytest.raises(ValueError, match="BCP 47"): + metadata.validate_claims_locales_supported() + def test_validate_claims_parameter_supported(): _call_validate_boolean("claims_parameter_supported") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/tests/flask/test_oauth2/test_openid_hybrid_grant.py new/authlib-1.7.2/tests/flask/test_oauth2/test_openid_hybrid_grant.py --- old/authlib-1.7.0/tests/flask/test_oauth2/test_openid_hybrid_grant.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/tests/flask/test_oauth2/test_openid_hybrid_grant.py 2026-05-06 10:08:51.000000000 +0200 @@ -160,6 +160,23 @@ assert "error=invalid_scope" in rv.location +def test_missing_openid_in_scope_does_not_redirect_to_unregistered_uri(test_client): + """An unregistered redirect_uri must not be followed even when openid scope is missing.""" + rv = test_client.post( + "/oauth/authorize", + data={ + "client_id": "client-id", + "response_type": "code id_token", + "state": "s", + "nonce": "n", + "scope": "profile", + "redirect_uri": "https://evil.example.com/phish", + }, + ) + assert rv.status_code == 400 + assert rv.headers.get("Location") is None + + def test_access_denied(test_client): rv = test_client.post( "/oauth/authorize", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/authlib-1.7.0/tests/flask/test_oauth2/test_openid_implict_grant.py new/authlib-1.7.2/tests/flask/test_oauth2/test_openid_implict_grant.py --- old/authlib-1.7.0/tests/flask/test_oauth2/test_openid_implict_grant.py 2026-04-18 12:59:19.000000000 +0200 +++ new/authlib-1.7.2/tests/flask/test_oauth2/test_openid_implict_grant.py 2026-05-06 10:08:51.000000000 +0200 @@ -107,6 +107,23 @@ assert "error=invalid_scope" in rv.location +def test_missing_openid_in_scope_does_not_redirect_to_unregistered_uri(test_client): + """An unregistered redirect_uri must not be followed even when openid scope is missing.""" + rv = test_client.post( + "/oauth/authorize", + data={ + "response_type": "id_token token", + "client_id": "client-id", + "scope": "profile", + "state": "s", + "nonce": "n", + "redirect_uri": "https://evil.example.com/phish", + }, + ) + assert rv.status_code == 400 + assert rv.headers.get("Location") is None + + def test_denied(test_client): rv = test_client.post( "/oauth/authorize",
