diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1e70e1..f5a359c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - name: Check out code uses: actions/checkout@v3 diff --git a/CHANGES.txt b/CHANGES.txt index 914a5dc..e8bb0c6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -3,6 +3,12 @@ CHANGELOG This document describes changes between each past release. +0.9.0 (2025-12-11) +================== + +- Migrate to use jwtoxide +- Remove Python 3.8 support + 0.8.1 (2025-05-01) ================== diff --git a/fxa/__init__.py b/fxa/__init__.py index 9e4ca1f..5c904e6 100644 --- a/fxa/__init__.py +++ b/fxa/__init__.py @@ -7,7 +7,7 @@ """ -__version__ = "0.8.1" +__version__ = "0.9.0" __ver_tuple__ = tuple(__version__.split(".")) diff --git a/fxa/oauth.py b/fxa/oauth.py index bb0aa18..d2b7d3d 100644 --- a/fxa/oauth.py +++ b/fxa/oauth.py @@ -9,6 +9,7 @@ from urllib.parse import urlparse, urlunparse, urlencode, parse_qs import jwt +from jwtoxide import DecodingKey, Jwk, ValidationOptions, decode from fxa.cache import MemoryCache, DEFAULT_CACHE_EXPIRY from fxa.constants import PRODUCTION_URLS from fxa.errors import OutOfProtocolError, ScopeMismatchError, TrustError @@ -198,15 +199,30 @@ def authorize_token(self, session, scope=None, client_id=None): return resp['access_token'] def _verify_jwt_token(self, key, token): - pubkey = jwt.algorithms.RSAAlgorithm.from_jwk(key) # The FxA OAuth ecosystem currently doesn't make good use of aud, and # instead relies on scope for restricting which services can accept # which tokens. So there's no value in checking it here, and in fact if # we check it here, it fails because the right audience isn't being # requested. - decoded = jwt.decode( - token, pubkey, algorithms=['RS256'], options={'verify_aud': False} - ) + try: + # Try to first decode with jwtoxide + decoded = decode( + token, + DecodingKey.from_jwk(Jwk.from_json(key)), + ValidationOptions( + aud=None, + iss=None, + required_spec_claims={"iat", "exp"}, + validate_aud=False, + algorithms=["RS256"], + ), + ) + except Exception: + # If something goes wrong, fallback to PyJWT + pubkey = jwt.algorithms.RSAAlgorithm.from_jwk(key) + decoded = jwt.decode( + token, pubkey, algorithms=["RS256"], options={"verify_aud": False} + ) # Ref https://tools.ietf.org/html/rfc7515#section-4.1.9 the `typ` header # is lowercase and has an implicit default `application/` prefix. typ = jwt.get_unverified_header(token).get('typ', '') diff --git a/pyproject.toml b/pyproject.toml index ab792a9..7bb8d7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,13 +16,12 @@ license = "MPL-2.0" authors = [ { name = "Mozilla Services", email = "services-dev@mozilla.org" }, ] -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -32,6 +31,7 @@ dynamic = [ "version" ] dependencies = [ "cryptography", "hawkauthlib", + "jwtoxide==0.2.0", "pyjwt", "requests>=2.4.2", ] @@ -71,7 +71,7 @@ cov = "pytest --cov-config=pyproject.toml --cov=fxa/ --cov-report term-missing { [[tool.hatch.envs.test.matrix]] # Note: When changing these, also update the .github/workflows/test.yml file. -python = ["3.8", "3.9", "3.10", "3.11", "3.12"] +python = ["3.9", "3.10", "3.11", "3.12"] [tool.flake8] max-line-length = 99