Compare commits
24 Commits
v1.0.1
...
v0.6.5-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e77b321fd | ||
|
|
b688cc872f | ||
|
|
2dea5c6b58 | ||
|
|
5465c1d213 | ||
|
|
759ea57bc8 | ||
|
|
a0e833ba69 | ||
|
|
9bf2372b7e | ||
|
|
653c716ea8 | ||
|
|
f53c16b20e | ||
|
|
d54046245f | ||
|
|
951f85816d | ||
|
|
99603b4b25 | ||
|
|
6d32757829 | ||
|
|
833360a66d | ||
|
|
c821ac19f7 | ||
|
|
e601a63a3d | ||
|
|
17a96da715 | ||
|
|
11b29f2f3b | ||
|
|
b1519b865d | ||
|
|
7a31b10d0e | ||
|
|
a6955e64a0 | ||
|
|
c217e46909 | ||
|
|
f614092af2 | ||
|
|
4f29740fa0 |
17
.github/workflows/hacs.yaml
vendored
17
.github/workflows/hacs.yaml
vendored
@@ -4,19 +4,18 @@ name: hacs
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- release/*
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *"
|
- cron: "0 0 * * *"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: HACS validation
|
- name: HACS validation
|
||||||
uses: hacs/action@22.5.0
|
uses: hacs/action@22.5.0
|
||||||
with:
|
with:
|
||||||
category: "integration"
|
category: "integration"
|
||||||
ignore: brands
|
|
||||||
|
|
||||||
|
|||||||
9
.github/workflows/hassfest.yaml
vendored
9
.github/workflows/hassfest.yaml
vendored
@@ -4,14 +4,15 @@ name: hassfest
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- release/*
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *"
|
- cron: "0 0 * * *"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- uses: home-assistant/actions/hassfest@master
|
- uses: home-assistant/actions/hassfest@master
|
||||||
|
|||||||
23
.github/workflows/lint.yaml
vendored
23
.github/workflows/lint.yaml
vendored
@@ -5,16 +5,21 @@ on:
|
|||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: Install the latest version of rye
|
- name: "Set up Python"
|
||||||
uses: eifinger/setup-rye@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
python-version-file: ".python-version"
|
||||||
- name: Sync dependencies
|
- name: Install the latest version of uv
|
||||||
run: rye sync
|
uses: astral-sh/setup-uv@v6
|
||||||
- name: Lint (pylint/rye lint)
|
with:
|
||||||
run: rye run check
|
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/
|
config/
|
||||||
|
|
||||||
.venv
|
.venv
|
||||||
|
|
||||||
|
.pytest_logs.log
|
||||||
|
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
3.13.1
|
3.14.2
|
||||||
@@ -13,9 +13,36 @@ If you are not a programmer, you can still contribute by:
|
|||||||
You may also submit Pull Requests (PRs) to add features yourself! You can find a list that we are currently working on below. Please note that workflows will be run on your pull request and a pull request will only be merged when all checks pass and a review has been conducted (together with a manual test).
|
You may also submit Pull Requests (PRs) to add features yourself! You can find a list that we are currently working on below. Please note that workflows will be run on your pull request and a pull request will only be merged when all checks pass and a review has been conducted (together with a manual test).
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
This project uses the Rye package manager for development. You can find installation instructions here: https://rye.astral.sh/guide/installation/. Start by installing the dependencies using rye sync and then point your editor towards the environment created in the .venv directory.
|
This project uses the uv package manager for development. You can find installation instructions here: https://docs.astral.sh/uv/getting-started/installation/. Start by installing the dependencies using `uv sync` and then point your editor towards the environment created in the .venv directory.
|
||||||
You can then run Home Assistant and put the `custom_components/auth_oidc` directory in your HA `config` folder.
|
You can then run Home Assistant and put the `custom_components/auth_oidc` directory in your HA `config` folder.
|
||||||
|
|
||||||
|
#### Other useful commands
|
||||||
|
Some useful scripts are in the `scripts` directory. If you run Linux (or WSL under Windows), you can run these directly:
|
||||||
|
|
||||||
|
- `scripts/check` will check your Python files for linting errors
|
||||||
|
- `scripts/fix` will fix some formatting mistakes automatically
|
||||||
|
|
||||||
|
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
|
||||||
|
uv run ruff format --check
|
||||||
|
uv run pylint custom_components
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Fix
|
||||||
|
```
|
||||||
|
uv run ruff check --fix
|
||||||
|
uv run ruff format
|
||||||
|
```
|
||||||
|
|
||||||
### Docker Compose Development Environment
|
### Docker Compose Development Environment
|
||||||
You can also use the following Docker Compose configuration to automatically start up the latest HA release with the `auth_oidc` integration:
|
You can also use the following Docker Compose configuration to automatically start up the latest HA release with the `auth_oidc` integration:
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright 2024 Christiaan Goossens
|
Copyright 2024-2025 Christiaan Goossens
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ If you would like to read the background/open letter that lead to this component
|
|||||||
|
|
||||||
## Installation guide
|
## Installation guide
|
||||||
|
|
||||||
1. Add this repository to [HACS](https://hacs.xyz/).
|
1. Add this repository to [HACS](https://hacs.xyz/) (or search for "OpenID Connect" in HACS).
|
||||||
|
|
||||||
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=christiaangoossens&repository=hass-oidc-auth&category=Integration)
|
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=christiaangoossens&repository=hass-oidc-auth&category=Integration)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import logging
|
|||||||
from typing import OrderedDict
|
from typing import OrderedDict
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.components.http import StaticPathConfig
|
||||||
|
|
||||||
# Import and re-export config schema explictly
|
# Import and re-export config schema explictly
|
||||||
# pylint: disable=useless-import-alias
|
# pylint: disable=useless-import-alias
|
||||||
@@ -23,6 +24,7 @@ from .config import (
|
|||||||
ROLES,
|
ROLES,
|
||||||
NETWORK,
|
NETWORK,
|
||||||
FEATURES_INCLUDE_GROUPS_SCOPE,
|
FEATURES_INCLUDE_GROUPS_SCOPE,
|
||||||
|
FEATURES_FORCE_HTTPS,
|
||||||
)
|
)
|
||||||
|
|
||||||
# pylint: enable=useless-import-alias
|
# pylint: enable=useless-import-alias
|
||||||
@@ -40,6 +42,13 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config):
|
async def async_setup(hass: HomeAssistant, config):
|
||||||
"""Add the OIDC Auth Provider to the providers in Home Assistant"""
|
"""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]
|
my_config = config[DOMAIN]
|
||||||
|
|
||||||
providers = OrderedDict()
|
providers = OrderedDict()
|
||||||
@@ -91,12 +100,23 @@ async def async_setup(hass: HomeAssistant, config):
|
|||||||
|
|
||||||
# Register the views
|
# Register the views
|
||||||
name = config[DOMAIN].get(DISPLAY_NAME, DEFAULT_TITLE)
|
name = config[DOMAIN].get(DISPLAY_NAME, DEFAULT_TITLE)
|
||||||
|
force_https = features_config.get(FEATURES_FORCE_HTTPS, False)
|
||||||
|
|
||||||
hass.http.register_view(OIDCWelcomeView(name))
|
hass.http.register_view(OIDCWelcomeView(name))
|
||||||
hass.http.register_view(OIDCRedirectView(oidc_client))
|
hass.http.register_view(OIDCRedirectView(oidc_client, force_https))
|
||||||
hass.http.register_view(OIDCCallbackView(oidc_client, provider))
|
hass.http.register_view(OIDCCallbackView(oidc_client, provider, force_https))
|
||||||
hass.http.register_view(OIDCFinishView())
|
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")
|
_LOGGER.info("Registered OIDC views")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ FEATURES_AUTOMATIC_USER_LINKING = "automatic_user_linking"
|
|||||||
FEATURES_AUTOMATIC_PERSON_CREATION = "automatic_person_creation"
|
FEATURES_AUTOMATIC_PERSON_CREATION = "automatic_person_creation"
|
||||||
FEATURES_DISABLE_PKCE = "disable_rfc7636"
|
FEATURES_DISABLE_PKCE = "disable_rfc7636"
|
||||||
FEATURES_INCLUDE_GROUPS_SCOPE = "include_groups_scope"
|
FEATURES_INCLUDE_GROUPS_SCOPE = "include_groups_scope"
|
||||||
|
FEATURES_FORCE_HTTPS = "force_https"
|
||||||
CLAIMS = "claims"
|
CLAIMS = "claims"
|
||||||
CLAIMS_DISPLAY_NAME = "display_name"
|
CLAIMS_DISPLAY_NAME = "display_name"
|
||||||
CLAIMS_USERNAME = "username"
|
CLAIMS_USERNAME = "username"
|
||||||
@@ -69,6 +70,10 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(
|
vol.Optional(
|
||||||
FEATURES_INCLUDE_GROUPS_SCOPE, default=True
|
FEATURES_INCLUDE_GROUPS_SCOPE, default=True
|
||||||
): vol.Coerce(bool),
|
): vol.Coerce(bool),
|
||||||
|
# Force HTTPS on all generated URLs (like redirect_uri)
|
||||||
|
vol.Optional(FEATURES_FORCE_HTTPS, default=False): vol.Coerce(
|
||||||
|
bool
|
||||||
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
# Determine which specific claims will be used from the id_token
|
# Determine which specific claims will be used from the id_token
|
||||||
|
|||||||
@@ -17,10 +17,14 @@ class OIDCCallbackView(HomeAssistantView):
|
|||||||
name = "auth:oidc:callback"
|
name = "auth:oidc:callback"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, oidc_client: OIDCClient, oidc_provider: OpenIDAuthProvider
|
self,
|
||||||
|
oidc_client: OIDCClient,
|
||||||
|
oidc_provider: OpenIDAuthProvider,
|
||||||
|
force_https: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.oidc_client = oidc_client
|
self.oidc_client = oidc_client
|
||||||
self.oidc_provider = oidc_provider
|
self.oidc_provider = oidc_provider
|
||||||
|
self.force_https = force_https
|
||||||
|
|
||||||
async def get(self, request: web.Request) -> web.Response:
|
async def get(self, request: web.Request) -> web.Response:
|
||||||
"""Receive response."""
|
"""Receive response."""
|
||||||
@@ -38,7 +42,7 @@ class OIDCCallbackView(HomeAssistantView):
|
|||||||
)
|
)
|
||||||
return web.Response(text=view_html, content_type="text/html")
|
return web.Response(text=view_html, content_type="text/html")
|
||||||
|
|
||||||
redirect_uri = get_url("/auth/oidc/callback")
|
redirect_uri = get_url("/auth/oidc/callback", self.force_https)
|
||||||
user_details = await self.oidc_client.async_complete_token_flow(
|
user_details = await self.oidc_client.async_complete_token_flow(
|
||||||
redirect_uri, code, state
|
redirect_uri, code, state
|
||||||
)
|
)
|
||||||
@@ -63,4 +67,6 @@ class OIDCCallbackView(HomeAssistantView):
|
|||||||
return web.Response(text=view_html, content_type="text/html")
|
return web.Response(text=view_html, content_type="text/html")
|
||||||
|
|
||||||
code = await self.oidc_provider.async_save_user_info(user_details)
|
code = await self.oidc_provider.async_save_user_info(user_details)
|
||||||
return web.HTTPFound(get_url("/auth/oidc/finish?code=" + code))
|
return web.HTTPFound(
|
||||||
|
get_url("/auth/oidc/finish?code=" + code, self.force_https)
|
||||||
|
)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class OIDCFinishView(HomeAssistantView):
|
|||||||
|
|
||||||
# Return redirect to the main page for sign in with a cookie
|
# Return redirect to the main page for sign in with a cookie
|
||||||
return web.HTTPFound(
|
return web.HTTPFound(
|
||||||
location="/",
|
location="/?storeToken=true",
|
||||||
headers={
|
headers={
|
||||||
# Set a cookie to enable autologin on only the specific path used
|
# Set a cookie to enable autologin on only the specific path used
|
||||||
# for the POST request, with all strict parameters set
|
# for the POST request, with all strict parameters set
|
||||||
|
|||||||
@@ -17,17 +17,21 @@ class OIDCRedirectView(HomeAssistantView):
|
|||||||
url = PATH
|
url = PATH
|
||||||
name = "auth:oidc:redirect"
|
name = "auth:oidc:redirect"
|
||||||
|
|
||||||
def __init__(self, oidc_client: OIDCClient) -> None:
|
def __init__(self, oidc_client: OIDCClient, force_https: bool) -> None:
|
||||||
self.oidc_client = oidc_client
|
self.oidc_client = oidc_client
|
||||||
|
self.force_https = force_https
|
||||||
|
|
||||||
async def get(self, _: web.Request) -> web.Response:
|
async def get(self, _: web.Request) -> web.Response:
|
||||||
"""Receive response."""
|
"""Receive response."""
|
||||||
|
|
||||||
redirect_uri = get_url("/auth/oidc/callback")
|
try:
|
||||||
auth_url = await self.oidc_client.async_get_authorization_url(redirect_uri)
|
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:
|
if auth_url:
|
||||||
return web.HTTPFound(auth_url)
|
raise web.HTTPFound(auth_url)
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
view_html = await get_view(
|
view_html = await get_view(
|
||||||
"error",
|
"error",
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ from homeassistant.components import http
|
|||||||
from .views.loader import AsyncTemplateRenderer
|
from .views.loader import AsyncTemplateRenderer
|
||||||
|
|
||||||
|
|
||||||
def get_url(path: str) -> str:
|
def get_url(path: str, force_https: bool) -> str:
|
||||||
"""Returns the requested path appended to the current request base URL."""
|
"""Returns the requested path appended to the current request base URL."""
|
||||||
if (req := http.current_request.get()) is None:
|
if (req := http.current_request.get()) is None:
|
||||||
raise RuntimeError("No current request in context")
|
raise RuntimeError("No current request in context")
|
||||||
|
|
||||||
base_uri = str(req.url).split("/auth", 2)[0]
|
base_uri = str(req.url).split("/auth", 2)[0]
|
||||||
|
if force_https:
|
||||||
|
base_uri = base_uri.replace("http://", "https://")
|
||||||
return f"{base_uri}{path}"
|
return f"{base_uri}{path}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,15 @@
|
|||||||
"auth",
|
"auth",
|
||||||
"http"
|
"http"
|
||||||
],
|
],
|
||||||
"documentation": "https://github.com/christiaangoossens/hass-oidc-auth",
|
"documentation": "https://github.com/christiaangoossens/hass-oidc-auth/blob/v0.6.4-alpha/docs/configuration.md",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "calculated",
|
"iot_class": "calculated",
|
||||||
"issue_tracker": "https://github.com/christiaangoossens/hass-oidc-auth/issues",
|
"issue_tracker": "https://github.com/christiaangoossens/hass-oidc-auth/issues",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"python-jose>=3.3.0",
|
"aiofiles",
|
||||||
"aiofiles>=24.1.0",
|
"jinja2",
|
||||||
"jinja2>=3.1.4",
|
"bcrypt",
|
||||||
"bcrypt>=4.2.0"
|
"joserfc"
|
||||||
],
|
],
|
||||||
"version": "0.6.2"
|
"version": "0.6.5"
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -237,9 +237,7 @@ class OIDCClient:
|
|||||||
_LOGGER.warning("Error fetching userinfo: %s", e)
|
_LOGGER.warning("Error fetching userinfo: %s", e)
|
||||||
raise OIDCUserinfoInvalid from e
|
raise OIDCUserinfoInvalid from e
|
||||||
|
|
||||||
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()
|
||||||
@@ -249,7 +247,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
|
||||||
@@ -278,7 +277,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(
|
||||||
@@ -311,9 +310,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,
|
||||||
@@ -322,48 +321,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]:
|
||||||
@@ -501,11 +483,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!")
|
||||||
@@ -519,6 +499,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
|
||||||
|
|||||||
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:
|
if template_name not in templates:
|
||||||
raise ValueError(f"Template '{template_name}' not found.")
|
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)
|
template = env.get_template(template_name)
|
||||||
|
|
||||||
# Render template
|
# Render template
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<link rel="stylesheet" href="/auth/oidc/static/style.css">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,28 @@ auth_oidc:
|
|||||||
|
|
||||||
This will show the provider on the login screen as: "Login with Example".
|
This will show the provider on the login screen as: "Login with Example".
|
||||||
|
|
||||||
|
### Forcing HTTPS
|
||||||
|
First check if you are setting the header `X-Forwarded-Proto` in your proxy and if the [proxy settings for Home Assistant](https://www.home-assistant.io/integrations/http/#use_x_forwarded_for) are configured correctly. You should also check if IP addresses in your logs actually match the origin IP (instead of proxy IP). If you cannot find any mistakes, you may use the following config option to force HTTPS regardless:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth_oidc:
|
||||||
|
features:
|
||||||
|
force_https: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disabling registration for new users
|
||||||
|
This integration does not allow disabling registration for new users, as there is no way to abort registration that late in the process while providing a good user experience.
|
||||||
|
You can however set both roles to groups that only contain certain users or to a non-existant group.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth_oidc:
|
||||||
|
roles:
|
||||||
|
user: "non_existent"
|
||||||
|
admin: "admins"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that if you put both on non-existent groups, no users will be able to login.
|
||||||
|
|
||||||
### Migrating from HA username/password users to OIDC users
|
### Migrating from HA username/password users to OIDC users
|
||||||
If you already have users created within Home Assistant and would like to re-use the current user profile for your OIDC login, you can (temporarily) enable `features.automatic_user_linking`, with the following config (example):
|
If you already have users created within Home Assistant and would like to re-use the current user profile for your OIDC login, you can (temporarily) enable `features.automatic_user_linking`, with the following config (example):
|
||||||
|
|
||||||
@@ -93,6 +115,8 @@ Upon login, OIDC users will then automatically be linked to the HA user with the
|
|||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> MFA is ignored when using this setting, thus bypassing any MFA configuration the user has originally configured, as long as the username is an exact match. This is dangerous if you are not aware of it!
|
> MFA is ignored when using this setting, thus bypassing any MFA configuration the user has originally configured, as long as the username is an exact match. This is dangerous if you are not aware of it!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Using a private certificate authority
|
### Using a private certificate authority
|
||||||
If you use a private certificate authority to secure your OIDC provider, you must configure the root certificates of your private certificate authority. Otherwise you will get an error (`[SSL: CERTIFICATE_VERIFY_FAILED]`) when connecting to the OIDC provider.
|
If you use a private certificate authority to secure your OIDC provider, you must configure the root certificates of your private certificate authority. Otherwise you will get an error (`[SSL: CERTIFICATE_VERIFY_FAILED]`) when connecting to the OIDC provider.
|
||||||
|
|
||||||
@@ -132,6 +156,7 @@ Here's a table of all options that you can set:
|
|||||||
| `features.automatic_person_creation` | `boolean` | No | `true` | Automatically creates a person entry for new user profiles created by this integration. Recommended if you would like to assign presence detection to OIDC users. |
|
| `features.automatic_person_creation` | `boolean` | No | `true` | Automatically creates a person entry for new user profiles created by this integration. Recommended if you would like to assign presence detection to OIDC users. |
|
||||||
| `features.disable_rfc7636` | `boolean`| No | `false` | Disables PKCE (RFC 7636) for OIDC providers that don't support it. You should not need this with most providers. |
|
| `features.disable_rfc7636` | `boolean`| No | `false` | Disables PKCE (RFC 7636) for OIDC providers that don't support it. You should not need this with most providers. |
|
||||||
| `features.include_groups_scope` | `boolean` | No | `true` | Include the 'groups' scope in the OIDC request. Set to `false` to exclude it. |
|
| `features.include_groups_scope` | `boolean` | No | `true` | Include the 'groups' scope in the OIDC request. Set to `false` to exclude it. |
|
||||||
|
| `features.force_https` | `boolean` | No | `false` | Set to `true` to force all URLs generated to use `https` instead of automatically determining based on the request scheme or `X-Forwarded-Proto`. |
|
||||||
| `claims.display_name` | `string` | No | `name` | The claim to use to obtain the display name.
|
| `claims.display_name` | `string` | No | `name` | The claim to use to obtain the display name.
|
||||||
| `claims.username` | `string` | No | `preferred_username` | The claim to use to obtain the username.
|
| `claims.username` | `string` | No | `preferred_username` | The claim to use to obtain the username.
|
||||||
| `claims.groups` | `string` | No | `groups` | The claim to use to obtain the user's group(s). |
|
| `claims.groups` | `string` | No | `groups` | The claim to use to obtain the user's group(s). |
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Microsoft Entra ID
|
# Microsoft Entra ID
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Microsoft Entra ID does not support public clients that are not Single Page Applications (SPA's). Therefore, you will have to use a client secret.
|
> Microsoft Entra ID does not support public clients that are not Single Page Applications (SPA's). Therefore, you will have to use a client secret.
|
||||||
|
## Basic configuration
|
||||||
1. Go to app registrations in Entra ID.
|
1. Go to app registrations in Entra ID.
|
||||||
2. Create a new app, use the "Web" type for the redirect URI and fill in your URL: `<ha url>/auth/oidc/callback`. Note that you either have to use localhost, or HTTPS.
|
2. Create a new app, use the "Web" type for the redirect URI and fill in your URL: `<ha url>/auth/oidc/callback`. Note that you either have to use localhost, or HTTPS.
|
||||||
3. Copy the 'Application (client) ID' on the overview page of your app and use it as your `client_id`.
|
3. Copy the 'Application (client) ID' on the overview page of your app and use it as your `client_id`.
|
||||||
@@ -25,3 +25,27 @@ auth_oidc:
|
|||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> Be careful! Configuring Entra ID wrong may leave your Home Assistant install open for anyone with a Microsoft account. Please use "Single tenant" account types only. Do not enable "Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant)" or personal account modes without enabling the mode to only allow specific accounts first!
|
> Be careful! Configuring Entra ID wrong may leave your Home Assistant install open for anyone with a Microsoft account. Please use "Single tenant" account types only. Do not enable "Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant)" or personal account modes without enabling the mode to only allow specific accounts first!
|
||||||
|
|
||||||
|
## Configuring user roles
|
||||||
|
If you like to configure the Home Assistant users roles based on your Entra ID settings, you have to create 2 roles within your Entra ID app registration.
|
||||||
|
Go to "App registrations" and select app roles. Create two new roles for admins and users, giving them sensible names and values (the example uses `users` and `admins`), that you will need later in your HA configuration.
|
||||||
|
|
||||||
|
<img width="1205" height="965" alt="Entra-HA-Roles" src="https://github.com/user-attachments/assets/568a1526-0607-4f88-945f-7c4f1fcc0ac2" />
|
||||||
|
|
||||||
|
Then you need to create the users and assign them a role of your choice.
|
||||||
|
Go to "Enterprise apps" chose your app registration again and select "Users and groups" within the manage section. Add users, or groups from your tenant or AD-sync and assign them a role, from the ones you created before.
|
||||||
|
|
||||||
|
<img width="1112" height="570" alt="Entra-HA-Users" src="https://github.com/user-attachments/assets/13a49cee-798b-4b53-8fee-d2792ccd7763" />
|
||||||
|
|
||||||
|
Last thing to do is to include
|
||||||
|
```
|
||||||
|
claims:
|
||||||
|
groups: "roles"
|
||||||
|
roles:
|
||||||
|
admin: "admins"
|
||||||
|
user: "users"
|
||||||
|
```
|
||||||
|
in your auth_oidc config, where the roles values correspond to the ones you chose in your Entra ID roles.
|
||||||
|
Make sure, you keep the "include_groups_scope: False" from the basic configuration, as the claim needed for Entra ID is "roles".
|
||||||
|
|
||||||
|
Newly created users will get the role assigned in Entra ID, but there is no update to user roles. A user created with user role in HA will not get the admin role, if you change the assignment later on in Entra ID.
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
"name": "OpenID Connect",
|
"name": "OpenID Connect",
|
||||||
"hide_default_branch": true,
|
"hide_default_branch": true,
|
||||||
"render_readme": true,
|
"render_readme": true,
|
||||||
"homeassistant": "2024.12"
|
"homeassistant": "2025.08"
|
||||||
}
|
}
|
||||||
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,30 +1,33 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "hass-oidc-auth"
|
name = "hass-oidc-auth"
|
||||||
version = "0.6.2"
|
version = "0.6.4"
|
||||||
description = "OIDC component for Home Assistant"
|
description = "OIDC component for Home Assistant"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Christiaan Goossens", email = "contact@christiaangoossens.nl" }
|
{ name = "Christiaan Goossens", email = "contact@christiaangoossens.nl" }
|
||||||
]
|
]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"python-jose>=3.3.0",
|
"aiofiles~=25.1",
|
||||||
"aiofiles>=24.1.0",
|
"jinja2~=3.1",
|
||||||
"jinja2>=3.1.4",
|
"bcrypt~=5.0",
|
||||||
"bcrypt>=4.2.0",
|
"joserfc~=1.6.0",
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">= 3.13"
|
requires-python = "~=3.14.2"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"homeassistant~=2026.1",
|
||||||
|
"pylint~=4.0",
|
||||||
|
"ruff~=0.12",
|
||||||
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["hatchling"]
|
||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[tool.rye]
|
[tool.uv]
|
||||||
managed = true
|
managed = true
|
||||||
dev-dependencies = [
|
|
||||||
"homeassistant~=2024.12",
|
|
||||||
"pylint~=3.3",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.hatch.metadata]
|
[tool.hatch.metadata]
|
||||||
allow-direct-references = true
|
allow-direct-references = true
|
||||||
@@ -32,11 +35,5 @@ allow-direct-references = true
|
|||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["custom_components/auth_oidc"]
|
packages = ["custom_components/auth_oidc"]
|
||||||
|
|
||||||
[tool.rye.scripts]
|
[tool.ruff]
|
||||||
check = { chain = ["check-lint", "check-fmt", "check-pylint" ] }
|
target-version = "py313"
|
||||||
"check-lint" = "rye lint"
|
|
||||||
"check-fmt" = "rye fmt --check"
|
|
||||||
"check-pylint" = "pylint custom_components"
|
|
||||||
fix = { chain = ["fix-lint", "fix-fmt" ] }
|
|
||||||
"fix-lint" = "rye lint --fix"
|
|
||||||
"fix-fmt" = "rye fmt"
|
|
||||||
@@ -14,11 +14,16 @@
|
|||||||
],
|
],
|
||||||
"prCreation": "immediate"
|
"prCreation": "immediate"
|
||||||
},
|
},
|
||||||
|
"lockFileMaintenance": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"description": "Group all GitHub Actions updates",
|
"description": "Group all GitHub Actions updates",
|
||||||
"matchDatasources": [
|
"matchDatasources": [
|
||||||
"github-actions"
|
"github-actions",
|
||||||
|
"github-tags",
|
||||||
|
"github-runners"
|
||||||
],
|
],
|
||||||
"groupName": "Github Actions Updates",
|
"groupName": "Github Actions Updates",
|
||||||
"automerge": true
|
"automerge": true
|
||||||
@@ -34,7 +39,7 @@
|
|||||||
"automerge": false
|
"automerge": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Version updates for other pip packages",
|
"description": "Version updates for other Python packages",
|
||||||
"matchDatasources": [
|
"matchDatasources": [
|
||||||
"pypi"
|
"pypi"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,286 +0,0 @@
|
|||||||
# generated by rye
|
|
||||||
# use `rye lock` or `rye sync` to update this lockfile
|
|
||||||
#
|
|
||||||
# last locked with the following flags:
|
|
||||||
# pre: false
|
|
||||||
# features: []
|
|
||||||
# all-features: false
|
|
||||||
# with-sources: false
|
|
||||||
# generate-hashes: false
|
|
||||||
# universal: false
|
|
||||||
|
|
||||||
-e file:.
|
|
||||||
acme==3.0.1
|
|
||||||
# via hass-nabucasa
|
|
||||||
aiodns==3.2.0
|
|
||||||
# via homeassistant
|
|
||||||
aiofiles==24.1.0
|
|
||||||
# via hass-oidc-auth
|
|
||||||
aiohappyeyeballs==2.4.4
|
|
||||||
# via aiohttp
|
|
||||||
aiohasupervisor==0.2.1
|
|
||||||
# via homeassistant
|
|
||||||
aiohttp==3.11.11
|
|
||||||
# via aiohasupervisor
|
|
||||||
# via aiohttp-cors
|
|
||||||
# via aiohttp-fast-zlib
|
|
||||||
# via hass-nabucasa
|
|
||||||
# via homeassistant
|
|
||||||
# via snitun
|
|
||||||
aiohttp-cors==0.7.0
|
|
||||||
# via homeassistant
|
|
||||||
aiohttp-fast-zlib==0.2.0
|
|
||||||
# via homeassistant
|
|
||||||
aiooui==0.1.7
|
|
||||||
# via bluetooth-adapters
|
|
||||||
aiosignal==1.3.2
|
|
||||||
# via aiohttp
|
|
||||||
aiozoneinfo==0.2.1
|
|
||||||
# via homeassistant
|
|
||||||
anyio==4.7.0
|
|
||||||
# via httpx
|
|
||||||
astral==2.2
|
|
||||||
# via homeassistant
|
|
||||||
astroid==3.3.8
|
|
||||||
# via pylint
|
|
||||||
async-interrupt==1.2.0
|
|
||||||
# via habluetooth
|
|
||||||
# via homeassistant
|
|
||||||
async-timeout==5.0.1
|
|
||||||
# via snitun
|
|
||||||
atomicwrites-homeassistant==1.4.1
|
|
||||||
# via hass-nabucasa
|
|
||||||
# via homeassistant
|
|
||||||
attrs==24.2.0
|
|
||||||
# via aiohttp
|
|
||||||
# via hass-nabucasa
|
|
||||||
# via homeassistant
|
|
||||||
# via snitun
|
|
||||||
audioop-lts==0.2.1
|
|
||||||
# via homeassistant
|
|
||||||
# via standard-aifc
|
|
||||||
awesomeversion==24.6.0
|
|
||||||
# via homeassistant
|
|
||||||
bcrypt==4.2.0
|
|
||||||
# via hass-oidc-auth
|
|
||||||
# via homeassistant
|
|
||||||
bleak==0.22.3
|
|
||||||
# via bleak-retry-connector
|
|
||||||
# via bluetooth-adapters
|
|
||||||
# via habluetooth
|
|
||||||
bleak-retry-connector==3.6.0
|
|
||||||
# via habluetooth
|
|
||||||
bluetooth-adapters==0.20.2
|
|
||||||
# via bleak-retry-connector
|
|
||||||
# via bluetooth-auto-recovery
|
|
||||||
# via habluetooth
|
|
||||||
bluetooth-auto-recovery==1.4.2
|
|
||||||
# via habluetooth
|
|
||||||
bluetooth-data-tools==1.20.0
|
|
||||||
# via habluetooth
|
|
||||||
boto3==1.35.87
|
|
||||||
# via pycognito
|
|
||||||
botocore==1.35.87
|
|
||||||
# via boto3
|
|
||||||
# via s3transfer
|
|
||||||
btsocket==0.3.0
|
|
||||||
# via bluetooth-auto-recovery
|
|
||||||
certifi==2024.12.14
|
|
||||||
# via homeassistant
|
|
||||||
# via httpcore
|
|
||||||
# via httpx
|
|
||||||
# via requests
|
|
||||||
cffi==1.17.1
|
|
||||||
# via cryptography
|
|
||||||
# via pycares
|
|
||||||
charset-normalizer==3.4.0
|
|
||||||
# via requests
|
|
||||||
ciso8601==2.3.1
|
|
||||||
# via hass-nabucasa
|
|
||||||
# via homeassistant
|
|
||||||
cryptography==43.0.1
|
|
||||||
# via acme
|
|
||||||
# via bluetooth-data-tools
|
|
||||||
# via hass-nabucasa
|
|
||||||
# via homeassistant
|
|
||||||
# via josepy
|
|
||||||
# via pyjwt
|
|
||||||
# via pyopenssl
|
|
||||||
# via securetar
|
|
||||||
# via snitun
|
|
||||||
dbus-fast==2.24.4
|
|
||||||
# via bleak
|
|
||||||
# via bleak-retry-connector
|
|
||||||
# via bluetooth-adapters
|
|
||||||
dill==0.3.9
|
|
||||||
# via pylint
|
|
||||||
ecdsa==0.19.0
|
|
||||||
# via python-jose
|
|
||||||
envs==1.4
|
|
||||||
# via pycognito
|
|
||||||
fnv-hash-fast==1.0.2
|
|
||||||
# via homeassistant
|
|
||||||
fnvhash==0.1.0
|
|
||||||
# via fnv-hash-fast
|
|
||||||
frozenlist==1.5.0
|
|
||||||
# via aiohttp
|
|
||||||
# via aiosignal
|
|
||||||
h11==0.14.0
|
|
||||||
# via httpcore
|
|
||||||
habluetooth==3.6.0
|
|
||||||
# via home-assistant-bluetooth
|
|
||||||
hass-nabucasa==0.86.0
|
|
||||||
# via homeassistant
|
|
||||||
home-assistant-bluetooth==1.13.0
|
|
||||||
# via homeassistant
|
|
||||||
homeassistant==2024.12.5
|
|
||||||
httpcore==1.0.7
|
|
||||||
# via httpx
|
|
||||||
httpx==0.27.2
|
|
||||||
# via homeassistant
|
|
||||||
idna==3.10
|
|
||||||
# via anyio
|
|
||||||
# via httpx
|
|
||||||
# via requests
|
|
||||||
# via yarl
|
|
||||||
ifaddr==0.2.0
|
|
||||||
# via homeassistant
|
|
||||||
isort==5.13.2
|
|
||||||
# via pylint
|
|
||||||
jinja2==3.1.4
|
|
||||||
# via hass-oidc-auth
|
|
||||||
# via homeassistant
|
|
||||||
jmespath==1.0.1
|
|
||||||
# via boto3
|
|
||||||
# via botocore
|
|
||||||
josepy==1.14.0
|
|
||||||
# via acme
|
|
||||||
lru-dict==1.3.0
|
|
||||||
# via homeassistant
|
|
||||||
markupsafe==3.0.2
|
|
||||||
# via jinja2
|
|
||||||
mashumaro==3.15
|
|
||||||
# via aiohasupervisor
|
|
||||||
# via webrtc-models
|
|
||||||
mccabe==0.7.0
|
|
||||||
# via pylint
|
|
||||||
multidict==6.1.0
|
|
||||||
# via aiohttp
|
|
||||||
# via yarl
|
|
||||||
orjson==3.10.12
|
|
||||||
# via aiohasupervisor
|
|
||||||
# via homeassistant
|
|
||||||
# via webrtc-models
|
|
||||||
packaging==24.2
|
|
||||||
# via homeassistant
|
|
||||||
pillow==11.0.0
|
|
||||||
# via homeassistant
|
|
||||||
platformdirs==4.3.6
|
|
||||||
# via pylint
|
|
||||||
propcache==0.2.1
|
|
||||||
# via aiohttp
|
|
||||||
# via homeassistant
|
|
||||||
# via yarl
|
|
||||||
psutil==6.1.1
|
|
||||||
# via psutil-home-assistant
|
|
||||||
psutil-home-assistant==0.0.1
|
|
||||||
# via homeassistant
|
|
||||||
pyasn1==0.6.1
|
|
||||||
# via python-jose
|
|
||||||
# via rsa
|
|
||||||
pycares==4.5.0
|
|
||||||
# via aiodns
|
|
||||||
pycognito==2024.5.1
|
|
||||||
# via hass-nabucasa
|
|
||||||
pycparser==2.22
|
|
||||||
# via cffi
|
|
||||||
pyjwt==2.10.1
|
|
||||||
# via hass-nabucasa
|
|
||||||
# via homeassistant
|
|
||||||
# via pycognito
|
|
||||||
pylint==3.3.3
|
|
||||||
pyopenssl==24.2.1
|
|
||||||
# via acme
|
|
||||||
# via homeassistant
|
|
||||||
# via josepy
|
|
||||||
pyrfc3339==2.0.1
|
|
||||||
# via acme
|
|
||||||
pyric==0.1.6.3
|
|
||||||
# via bluetooth-auto-recovery
|
|
||||||
python-dateutil==2.9.0.post0
|
|
||||||
# via botocore
|
|
||||||
python-jose==3.3.0
|
|
||||||
# via hass-oidc-auth
|
|
||||||
python-slugify==8.0.4
|
|
||||||
# via homeassistant
|
|
||||||
pytz==2024.2
|
|
||||||
# via acme
|
|
||||||
# via astral
|
|
||||||
pyyaml==6.0.2
|
|
||||||
# via homeassistant
|
|
||||||
requests==2.32.3
|
|
||||||
# via acme
|
|
||||||
# via homeassistant
|
|
||||||
# via pycognito
|
|
||||||
rsa==4.9
|
|
||||||
# via python-jose
|
|
||||||
s3transfer==0.10.4
|
|
||||||
# via boto3
|
|
||||||
securetar==2024.11.0
|
|
||||||
# via homeassistant
|
|
||||||
setuptools==75.6.0
|
|
||||||
# via acme
|
|
||||||
six==1.17.0
|
|
||||||
# via ecdsa
|
|
||||||
# via python-dateutil
|
|
||||||
sniffio==1.3.1
|
|
||||||
# via anyio
|
|
||||||
# via httpx
|
|
||||||
snitun==0.39.1
|
|
||||||
# via hass-nabucasa
|
|
||||||
sqlalchemy==2.0.36
|
|
||||||
# via homeassistant
|
|
||||||
standard-aifc==3.13.0
|
|
||||||
# via homeassistant
|
|
||||||
standard-chunk==3.13.0
|
|
||||||
# via standard-aifc
|
|
||||||
standard-telnetlib==3.13.0
|
|
||||||
# via homeassistant
|
|
||||||
text-unidecode==1.3
|
|
||||||
# via python-slugify
|
|
||||||
tomlkit==0.13.2
|
|
||||||
# via pylint
|
|
||||||
typing-extensions==4.12.2
|
|
||||||
# via homeassistant
|
|
||||||
# via mashumaro
|
|
||||||
# via sqlalchemy
|
|
||||||
tzdata==2024.2
|
|
||||||
# via aiozoneinfo
|
|
||||||
uart-devices==0.1.0
|
|
||||||
# via bluetooth-adapters
|
|
||||||
ulid-transform==1.0.2
|
|
||||||
# via homeassistant
|
|
||||||
urllib3==1.26.20
|
|
||||||
# via botocore
|
|
||||||
# via homeassistant
|
|
||||||
# via requests
|
|
||||||
usb-devices==0.4.5
|
|
||||||
# via bluetooth-adapters
|
|
||||||
# via bluetooth-auto-recovery
|
|
||||||
uv==0.5.4
|
|
||||||
# via homeassistant
|
|
||||||
voluptuous==0.15.2
|
|
||||||
# via homeassistant
|
|
||||||
# via voluptuous-openapi
|
|
||||||
# via voluptuous-serialize
|
|
||||||
voluptuous-openapi==0.0.5
|
|
||||||
# via homeassistant
|
|
||||||
voluptuous-serialize==2.6.0
|
|
||||||
# via homeassistant
|
|
||||||
webrtc-models==0.3.0
|
|
||||||
# via hass-nabucasa
|
|
||||||
# via homeassistant
|
|
||||||
yarl==1.18.3
|
|
||||||
# via aiohasupervisor
|
|
||||||
# via aiohttp
|
|
||||||
# via homeassistant
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# generated by rye
|
|
||||||
# use `rye lock` or `rye sync` to update this lockfile
|
|
||||||
#
|
|
||||||
# last locked with the following flags:
|
|
||||||
# pre: false
|
|
||||||
# features: []
|
|
||||||
# all-features: false
|
|
||||||
# with-sources: false
|
|
||||||
# generate-hashes: false
|
|
||||||
# universal: false
|
|
||||||
|
|
||||||
-e file:.
|
|
||||||
aiofiles==24.1.0
|
|
||||||
# via hass-oidc-auth
|
|
||||||
bcrypt==4.2.1
|
|
||||||
# via hass-oidc-auth
|
|
||||||
ecdsa==0.19.0
|
|
||||||
# via python-jose
|
|
||||||
jinja2==3.1.5
|
|
||||||
# via hass-oidc-auth
|
|
||||||
markupsafe==3.0.2
|
|
||||||
# via jinja2
|
|
||||||
pyasn1==0.6.1
|
|
||||||
# via python-jose
|
|
||||||
# via rsa
|
|
||||||
python-jose==3.3.0
|
|
||||||
# via hass-oidc-auth
|
|
||||||
rsa==4.9
|
|
||||||
# via python-jose
|
|
||||||
six==1.17.0
|
|
||||||
# via ecdsa
|
|
||||||
4
scripts/check
Executable file
4
scripts/check
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
uv run ruff check
|
||||||
|
uv run ruff format --check
|
||||||
|
uv run pylint custom_components
|
||||||
3
scripts/fix
Executable file
3
scripts/fix
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
uv run ruff check --fix
|
||||||
|
uv run ruff format
|
||||||
2
scripts/sync
Executable file
2
scripts/sync
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
uv sync --locked
|
||||||
Reference in New Issue
Block a user