Compare commits
10 Commits
v0.6.3-alp
...
v0.6.5-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e77b321fd | ||
|
|
b688cc872f | ||
|
|
2dea5c6b58 | ||
|
|
5465c1d213 | ||
|
|
759ea57bc8 | ||
|
|
a0e833ba69 | ||
|
|
9bf2372b7e | ||
|
|
653c716ea8 | ||
|
|
f53c16b20e | ||
|
|
d54046245f |
16
.github/workflows/hacs.yaml
vendored
16
.github/workflows/hacs.yaml
vendored
@@ -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"
|
||||
|
||||
9
.github/workflows/hassfest.yaml
vendored
9
.github/workflows/hassfest.yaml
vendored
@@ -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
|
||||
|
||||
27
.github/workflows/lint.yaml
vendored
27
.github/workflows/lint.yaml
vendored
@@ -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
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -108,3 +108,8 @@ dmypy.json
|
||||
config/
|
||||
|
||||
.venv
|
||||
|
||||
.pytest_logs.log
|
||||
|
||||
|
||||
node_modules
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.13.7
|
||||
3.14.2
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
3
custom_components/auth_oidc/static/input.css
Normal file
3
custom_components/auth_oidc/static/input.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@source "../views/templates";
|
||||
2
custom_components/auth_oidc/static/style.css
Normal file
2
custom_components/auth_oidc/static/style.css
Normal file
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
1123
package-lock.json
generated
Normal file
1123
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
package.json
Normal file
11
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py313"
|
||||
Reference in New Issue
Block a user