10 Commits

Author SHA1 Message Date
Christiaan Goossens
4e77b321fd Bump to 0.6.5 2026-02-06 12:41:03 +01:00
Christiaan Goossens
b688cc872f Detect misconfiguration on downgrade 2026-02-06 11:54:04 +01:00
Christiaan Goossens
2dea5c6b58 Reset min required HA 2026-02-06 11:44:41 +01:00
Christiaan Goossens
5465c1d213 Run correct workflows 2026-02-06 11:42:14 +01:00
Christiaan Goossens
759ea57bc8 Bump versions & dep maintenance 2026-02-06 11:40:37 +01:00
Andrew Garrett
a0e833ba69 Enable Jinja2 autoescaping (#200)
- Enable Jinja2 autoescape by default in the template environment.
- Use json.dumps to safely inject sso_name into JavaScript context.
- Fix linting issue (line too long) in injected_auth_page.py.
- Update tests to verify escaping and safe injection.

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: werdnum <271070+werdnum@users.noreply.github.com>
2026-02-06 11:38:01 +01:00
Tricked
9bf2372b7e Use tailwind cli to compile css instead of tailwind cdn (#132)
* implement feature
* use npm instead of cli
2026-02-06 11:36:16 +01:00
Christiaan Goossens
653c716ea8 Fix 500 on redirect path (#201)
* Fix 500 on redirect path

Co-authored-by: anntnzrb <anntnzrb@proton.me>
2026-02-06 11:30:27 +01:00
Christiaan Goossens
f53c16b20e Fix manifest json requirements (#152) 2026-02-06 11:26:02 +01:00
Christiaan Goossens
d54046245f Migrate to joserfc, remove python-jose (#150) 2026-02-06 11:25:49 +01:00
18 changed files with 2105 additions and 945 deletions

View File

@@ -4,18 +4,18 @@ name: hacs
on:
push:
branches:
- main
- main
- release/*
pull_request:
schedule:
- cron: "0 0 * * *"
- cron: "0 0 * * *"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: HACS validation
uses: hacs/action@22.5.0
with:
category: "integration"
- uses: actions/checkout@v5
- name: HACS validation
uses: hacs/action@22.5.0
with:
category: "integration"

View File

@@ -4,14 +4,15 @@ name: hassfest
on:
push:
branches:
- main
- main
- release/*
pull_request:
schedule:
- cron: "0 0 * * *"
- cron: "0 0 * * *"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: home-assistant/actions/hassfest@master
- uses: actions/checkout@v5
- uses: home-assistant/actions/hassfest@master

View File

@@ -5,20 +5,21 @@ on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Sync dependencies
run: scripts/sync
- name: Lint (pylint/ruff lint)
run: scripts/check
- uses: actions/checkout@v5
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Sync dependencies
run: scripts/sync
- name: Lint (pylint/ruff lint)
run: scripts/check

7
.gitignore vendored
View File

@@ -107,4 +107,9 @@ dmypy.json
# End of https://www.gitignore.io/api/python
config/
.venv
.venv
.pytest_logs.log
node_modules

View File

@@ -1 +1 @@
3.13.7
3.14.2

View File

@@ -24,6 +24,12 @@ Some useful scripts are in the `scripts` directory. If you run Linux (or WSL und
You can also run these commands manually on Windows:
##### Compiling css
To compile tailwind css styles for the pages you need the NodeJS and NPM installed.
You can run the `npm run css` script to generate the css once and you can run the `npm run css:watch` to recompile the css every time the templates change
##### Check
```
uv run ruff check

View File

@@ -4,6 +4,7 @@ import logging
from typing import OrderedDict
from homeassistant.core import HomeAssistant
from homeassistant.components.http import StaticPathConfig
# Import and re-export config schema explictly
# pylint: disable=useless-import-alias
@@ -41,6 +42,13 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config):
"""Add the OIDC Auth Provider to the providers in Home Assistant"""
if DOMAIN not in config:
_LOGGER.warning(
"Setup was triggered, but no configuration was found. "
+ "Did you downgrade from 0.7+ without deleting the OIDC UI configuration?"
)
return False
my_config = config[DOMAIN]
providers = OrderedDict()
@@ -99,6 +107,16 @@ async def async_setup(hass: HomeAssistant, config):
hass.http.register_view(OIDCCallbackView(oidc_client, provider, force_https))
hass.http.register_view(OIDCFinishView())
await hass.http.async_register_static_paths(
[
StaticPathConfig(
"/auth/oidc/static/style.css",
hass.config.path("custom_components/auth_oidc/static/style.css"),
cache_headers=False,
),
]
)
_LOGGER.info("Registered OIDC views")
return True

View File

@@ -24,11 +24,14 @@ class OIDCRedirectView(HomeAssistantView):
async def get(self, _: web.Request) -> web.Response:
"""Receive response."""
redirect_uri = get_url("/auth/oidc/callback", self.force_https)
auth_url = await self.oidc_client.async_get_authorization_url(redirect_uri)
try:
redirect_uri = get_url("/auth/oidc/callback", self.force_https)
auth_url = await self.oidc_client.async_get_authorization_url(redirect_uri)
if auth_url:
return web.HTTPFound(auth_url)
if auth_url:
raise web.HTTPFound(auth_url)
except RuntimeError:
pass
view_html = await get_view(
"error",

View File

@@ -9,15 +9,15 @@
"auth",
"http"
],
"documentation": "https://github.com/christiaangoossens/hass-oidc-auth/blob/v0.6.3-alpha/docs/configuration.md",
"documentation": "https://github.com/christiaangoossens/hass-oidc-auth/blob/v0.6.4-alpha/docs/configuration.md",
"integration_type": "service",
"iot_class": "calculated",
"issue_tracker": "https://github.com/christiaangoossens/hass-oidc-auth/issues",
"requirements": [
"python-jose>=3.3.0",
"aiofiles>=24.1.0",
"jinja2>=3.1.4",
"bcrypt>=4.2.0"
"aiofiles",
"jinja2",
"bcrypt",
"joserfc"
],
"version": "0.6.3"
"version": "0.6.5"
}

View File

@@ -9,7 +9,7 @@ import ssl
from typing import Optional
from functools import partial
import aiohttp
from jose import jwt, jwk
from joserfc import jwt, jwk, jws, errors as joserfc_errors
from homeassistant.core import HomeAssistant
from .types import UserDetails
@@ -237,9 +237,7 @@ class OIDCClient:
_LOGGER.warning("Error fetching userinfo: %s", e)
raise OIDCUserinfoInvalid from e
async def _parse_id_token(
self, id_token: str, access_token: str | None
) -> Optional[dict]:
async def _parse_id_token(self, id_token: str) -> Optional[dict]:
"""Parses the ID token into a dict containing token contents."""
if self.discovery_document is None:
self.discovery_document = await self._fetch_discovery_document()
@@ -249,7 +247,8 @@ class OIDCClient:
try:
# 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:
_LOGGER.warning("Could not get header from received id_token.")
return None
@@ -278,7 +277,7 @@ class OIDCClient:
)
raise OIDCIdTokenSigningAlgorithmInvalid()
jwk_obj = jwk.construct(
jwk_obj = jwk.import_key(
{
"kty": "oct",
"k": base64.urlsafe_b64encode(
@@ -311,9 +310,9 @@ class OIDCClient:
signing_key["alg"] = alg
# 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(
id_token,
jwk_obj,
@@ -322,48 +321,31 @@ class OIDCClient:
# according to JWS [JWS] using the algorithm specified in the JWT
# alg Header Parameter.
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
# The Client MUST validate that the aud (audience) Claim contains
# its client_id value registered at the Issuer identified by the
# 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
# The Issuer Identifier for the OpenID Provider MUST exactly
# match the value of the iss (issuer) Claim.
issuer=self.discovery_document["issuer"],
access_token=access_token,
options={
# Verify everything if present
"verify_signature": 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,
},
iss={"essential": True, "value": self.discovery_document["issuer"]},
# OpenID Connect Core 1.0 Section 3.1.3.7.9
# OpenID Connect Core 1.0 Section 3.1.3.7.10
# No need to specify exp, nbf, iat, they are in here by default
sub={"essential": True},
)
return decoded_token
except jwt.JWTError as e:
_LOGGER.warning("JWT Verification failed: %s", e)
id_token_validator.validate(decoded_token.claims)
return decoded_token.claims
except joserfc_errors.JoseError as e:
_LOGGER.warning("JWT verification failed: %s", e)
return None
async def async_get_authorization_url(self, redirect_uri: str) -> Optional[str]:
@@ -501,11 +483,9 @@ class OIDCClient:
)
id_token = token_response.get("id_token")
access_token = token_response.get("access_token")
# 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, access_token)
id_token = await self._parse_id_token(id_token)
if id_token is None:
_LOGGER.warning("ID token could not be parsed!")
@@ -519,6 +499,7 @@ class OIDCClient:
_LOGGER.warning("Nonce mismatch!")
return None
access_token = token_response.get("access_token")
data = await self.parse_user_details(id_token, access_token)
# Log which details were obtained for debugging

View File

@@ -0,0 +1,3 @@
@import "tailwindcss";
@source "../views/templates";

File diff suppressed because one or more lines are too long

View File

@@ -54,7 +54,9 @@ class AsyncTemplateRenderer:
if template_name not in templates:
raise ValueError(f"Template '{template_name}' not found.")
env = Environment(loader=DictLoader(templates), enable_async=True)
env = Environment(
loader=DictLoader(templates), enable_async=True, autoescape=True
)
template = env.get_template(template_name)
# Render template

View File

@@ -6,7 +6,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/auth/oidc/static/style.css">
{% endblock %}
</head>
@@ -16,4 +16,4 @@
</div>
</body>
</html>
</html>

1123
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

11
package.json Normal file
View File

@@ -0,0 +1,11 @@
{
"name": "hass-oidc-auth",
"scripts": {
"css": "tailwindcss -i ./custom_components/auth_oidc/static/input.css -o ./custom_components/auth_oidc/static/style.css --minify",
"css:watch": "tailwindcss -i ./custom_components/auth_oidc/static/input.css -o ./custom_components/auth_oidc/static/style.css --watch --minify"
},
"dependencies": {
"@tailwindcss/cli": "^4.1.14",
"tailwindcss": "^4.1.14"
}
}

View File

@@ -1,19 +1,26 @@
[project]
name = "hass-oidc-auth"
version = "0.6.3"
version = "0.6.4"
description = "OIDC component for Home Assistant"
authors = [
{ name = "Christiaan Goossens", email = "contact@christiaangoossens.nl" }
]
license = "MIT"
dependencies = [
"python-jose>=3.3.0",
"aiofiles>=24.1.0",
"jinja2>=3.1.4",
"bcrypt>=4.2.0",
"aiofiles~=25.1",
"jinja2~=3.1",
"bcrypt~=5.0",
"joserfc~=1.6.0",
]
readme = "README.md"
requires-python = ">= 3.13.2"
requires-python = "~=3.14.2"
[dependency-groups]
dev = [
"homeassistant~=2026.1",
"pylint~=4.0",
"ruff~=0.12",
]
[build-system]
requires = ["hatchling"]
@@ -21,14 +28,12 @@ build-backend = "hatchling.build"
[tool.uv]
managed = true
dev-dependencies = [
"homeassistant~=2025.8",
"pylint~=3.3",
"ruff>=0.12.11",
]
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["custom_components/auth_oidc"]
packages = ["custom_components/auth_oidc"]
[tool.ruff]
target-version = "py313"

1697
uv.lock generated

File diff suppressed because it is too large Load Diff