Migrate to joserfc, remove python-jose (#150)

This commit is contained in:
Christiaan Goossens
2025-10-31 10:16:45 +01:00
committed by GitHub
parent a8e0162d25
commit 674c342a81
3 changed files with 37 additions and 106 deletions

View File

@@ -9,7 +9,7 @@ import ssl
from typing import Optional from typing import Optional
from functools import partial from functools import partial
import aiohttp import aiohttp
from jose import jwt, jwk from joserfc import jwt, jwk, jws, errors as joserfc_errors
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .types import UserDetails from .types import UserDetails
@@ -433,9 +433,7 @@ class OIDCClient:
"""Fetches JWKS.""" """Fetches JWKS."""
return await self.discovery_class.fetch_jwks(jwks_uri) return await self.discovery_class.fetch_jwks(jwks_uri)
async def _parse_id_token( async def _parse_id_token(self, id_token: str) -> Optional[dict]:
self, id_token: str, access_token: str | None
) -> Optional[dict]:
"""Parses the ID token into a dict containing token contents.""" """Parses the ID token into a dict containing token contents."""
if self.discovery_document is None: if self.discovery_document is None:
self.discovery_document = await self._fetch_discovery_document() self.discovery_document = await self._fetch_discovery_document()
@@ -445,7 +443,8 @@ class OIDCClient:
try: try:
# Obtain the id_token header # Obtain the id_token header
unverified_header = jwt.get_unverified_header(id_token) token_obj = jws.extract_compact(id_token.encode())
unverified_header = token_obj.protected
if not unverified_header: if not unverified_header:
_LOGGER.warning("Could not get header from received id_token.") _LOGGER.warning("Could not get header from received id_token.")
return None return None
@@ -474,7 +473,7 @@ class OIDCClient:
) )
raise OIDCIdTokenSigningAlgorithmInvalid() raise OIDCIdTokenSigningAlgorithmInvalid()
jwk_obj = jwk.construct( jwk_obj = jwk.import_key(
{ {
"kty": "oct", "kty": "oct",
"k": base64.urlsafe_b64encode( "k": base64.urlsafe_b64encode(
@@ -507,9 +506,9 @@ class OIDCClient:
signing_key["alg"] = alg signing_key["alg"] = alg
# Construct the JWK from the RSA key # Construct the JWK from the RSA key
jwk_obj = jwk.construct(signing_key) jwk_obj = jwk.import_key(signing_key)
# Verify the token # Decode the token, decode does not verify it
decoded_token = jwt.decode( decoded_token = jwt.decode(
id_token, id_token,
jwk_obj, jwk_obj,
@@ -518,48 +517,31 @@ class OIDCClient:
# according to JWS [JWS] using the algorithm specified in the JWT # according to JWS [JWS] using the algorithm specified in the JWT
# alg Header Parameter. # alg Header Parameter.
algorithms=[self.id_token_signing_alg], algorithms=[self.id_token_signing_alg],
)
# Create Claims Registry for validation
id_token_validator = jwt.JWTClaimsRegistry(
leeway=5,
# OpenID Connect Core 1.0 Section 3.1.3.7.3 # OpenID Connect Core 1.0 Section 3.1.3.7.3
# The Client MUST validate that the aud (audience) Claim contains # The Client MUST validate that the aud (audience) Claim contains
# its client_id value registered at the Issuer identified by the # its client_id value registered at the Issuer identified by the
# iss (issuer) Claim as an audience. # iss (issuer) Claim as an audience.
audience=self.client_id, aud={"essential": True, "value": self.client_id},
# OpenID Connect Core 1.0 Section 3.1.3.7.2 # OpenID Connect Core 1.0 Section 3.1.3.7.2
# The Issuer Identifier for the OpenID Provider MUST exactly # The Issuer Identifier for the OpenID Provider MUST exactly
# match the value of the iss (issuer) Claim. # match the value of the iss (issuer) Claim.
issuer=self.discovery_document["issuer"], iss={"essential": True, "value": self.discovery_document["issuer"]},
access_token=access_token, # OpenID Connect Core 1.0 Section 3.1.3.7.9
options={ # OpenID Connect Core 1.0 Section 3.1.3.7.10
# Verify everything if present # No need to specify exp, nbf, iat, they are in here by default
"verify_signature": True, sub={"essential": True},
"verify_aud": True,
"verify_iat": True,
"verify_exp": True,
"verify_nbf": True,
"verify_iss": True,
"verify_sub": True,
"verify_jti": True,
"verify_at_hash": True,
# OpenID Connect Core 1.0 Section 3.1.3.7.3
"require_aud": True,
# OpenID Connect Core 1.0 Section 3.1.3.7.10
"require_iat": True,
# OpenID Connect Core 1.0 Section 3.1.3.7.9
"require_exp": True,
# OpenID Connect Core 1.0 Section 3.1.3.7.2
"require_iss": True,
# We need the sub as it's used to identify the user
"require_sub": True,
# Other values, not required.
"require_nbf": False,
"require_jti": False,
"require_at_hash": False,
"leeway": 5,
},
) )
return decoded_token
except jwt.JWTError as e: id_token_validator.validate(decoded_token.claims)
_LOGGER.warning("JWT Verification failed: %s", e) return decoded_token.claims
except joserfc_errors.JoseError as e:
_LOGGER.warning("JWT verification failed: %s", e)
return None return None
async def async_get_authorization_url(self, redirect_uri: str) -> Optional[str]: async def async_get_authorization_url(self, redirect_uri: str) -> Optional[str]:
@@ -692,11 +674,9 @@ class OIDCClient:
) )
id_token = token_response.get("id_token") id_token = token_response.get("id_token")
access_token = token_response.get("access_token")
# Parse the id token to obtain the relevant details # Parse the id token to obtain the relevant details
# Access token is supplied to check at_hash if present id_token = await self._parse_id_token(id_token)
id_token = await self._parse_id_token(id_token, access_token)
if id_token is None: if id_token is None:
_LOGGER.warning("ID token could not be parsed!") _LOGGER.warning("ID token could not be parsed!")
@@ -710,6 +690,7 @@ class OIDCClient:
_LOGGER.warning("Nonce mismatch!") _LOGGER.warning("Nonce mismatch!")
return None return None
access_token = token_response.get("access_token")
data = await self.parse_user_details(id_token, access_token) data = await self.parse_user_details(id_token, access_token)
# Log which details were obtained for debugging # Log which details were obtained for debugging

View File

@@ -7,23 +7,22 @@ authors = [
] ]
license = "MIT" license = "MIT"
dependencies = [ dependencies = [
"python-jose~=3.5.0",
"aiofiles~=25.1", "aiofiles~=25.1",
"jinja2~=3.1", "jinja2~=3.1",
"bcrypt~=4.2", "bcrypt~=4.2",
"joserfc>=1.3.4", "joserfc~=1.4.0",
] ]
readme = "README.md" readme = "README.md"
requires-python = "~=3.13.7" requires-python = "~=3.13.7"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"homeassistant~=2025.8", "homeassistant~=2025.10",
"pylint~=4.0", "pylint~=4.0",
"pytest>=8.4.2", "pytest~=8.4.2",
"pytest-asyncio>=1.2.0", "pytest-asyncio~=1.2.0",
"pytest-cov>=7.0.0", "pytest-cov~=7.0.0",
"pytest-homeassistant-custom-component>=0.13.286", "pytest-homeassistant-custom-component~=0.13.286",
"ruff~=0.12", "ruff~=0.12",
] ]

61
uv.lock generated
View File

@@ -686,18 +686,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" },
] ]
[[package]]
name = "ecdsa"
version = "0.19.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793, upload-time = "2025-03-13T11:52:43.25Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" },
]
[[package]] [[package]]
name = "envs" name = "envs"
version = "1.4" version = "1.4"
@@ -904,7 +892,6 @@ dependencies = [
{ name = "bcrypt" }, { name = "bcrypt" },
{ name = "jinja2" }, { name = "jinja2" },
{ name = "joserfc" }, { name = "joserfc" },
{ name = "python-jose" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
@@ -923,18 +910,17 @@ requires-dist = [
{ name = "aiofiles", specifier = "~=25.1" }, { name = "aiofiles", specifier = "~=25.1" },
{ name = "bcrypt", specifier = "~=4.2" }, { name = "bcrypt", specifier = "~=4.2" },
{ name = "jinja2", specifier = "~=3.1" }, { name = "jinja2", specifier = "~=3.1" },
{ name = "joserfc", specifier = ">=1.3.4" }, { name = "joserfc", specifier = "~=1.4.0" },
{ name = "python-jose", specifier = "~=3.5.0" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "homeassistant", specifier = "~=2025.8" }, { name = "homeassistant", specifier = "~=2025.10" },
{ name = "pylint", specifier = "~=4.0" }, { name = "pylint", specifier = "~=4.0" },
{ name = "pytest", specifier = ">=8.4.2" }, { name = "pytest", specifier = "~=8.4.2" },
{ name = "pytest-asyncio", specifier = ">=1.2.0" }, { name = "pytest-asyncio", specifier = "~=1.2.0" },
{ name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-cov", specifier = "~=7.0.0" },
{ name = "pytest-homeassistant-custom-component", specifier = ">=0.13.286" }, { name = "pytest-homeassistant-custom-component", specifier = "~=0.13.286" },
{ name = "ruff", specifier = "~=0.12" }, { name = "ruff", specifier = "~=0.12" },
] ]
@@ -1457,15 +1443,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/48/8a0acb683d1fee78b966b15e78143b673154abb921061515254fb573aacd/psutil_home_assistant-0.0.1-py3-none-any.whl", hash = "sha256:35a782e93e23db845fc4a57b05df9c52c2d5c24f5b233bd63b01bae4efae3c41", size = 6300, upload-time = "2022-08-25T14:28:38.083Z" }, { url = "https://files.pythonhosted.org/packages/cb/48/8a0acb683d1fee78b966b15e78143b673154abb921061515254fb573aacd/psutil_home_assistant-0.0.1-py3-none-any.whl", hash = "sha256:35a782e93e23db845fc4a57b05df9c52c2d5c24f5b233bd63b01bae4efae3c41", size = 6300, upload-time = "2022-08-25T14:28:38.083Z" },
] ]
[[package]]
name = "pyasn1"
version = "0.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
]
[[package]] [[package]]
name = "pycares" name = "pycares"
version = "4.11.0" version = "4.11.0"
@@ -1892,20 +1869,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
] ]
[[package]]
name = "python-jose"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ecdsa" },
{ name = "pyasn1" },
{ name = "rsa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" },
]
[[package]] [[package]]
name = "python-slugify" name = "python-slugify"
version = "8.0.4" version = "8.0.4"
@@ -2006,18 +1969,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" },
] ]
[[package]]
name = "rsa"
version = "4.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.14.2" version = "0.14.2"