Compare commits
34 Commits
v0.6.1-alp
...
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 | ||
|
|
b4d5d7f2bf | ||
|
|
cb4d72a148 | ||
|
|
be59c415a0 | ||
|
|
ccd5fb2459 | ||
|
|
fbc47d11ef | ||
|
|
881a6cb0be | ||
|
|
178cd4df49 | ||
|
|
de321c8817 | ||
|
|
aaa977781c | ||
|
|
1fc4e0f21a |
17
.github/workflows/hacs.yaml
vendored
17
.github/workflows/hacs.yaml
vendored
@@ -4,19 +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@v4
|
||||
- name: HACS validation
|
||||
uses: hacs/action@22.5.0
|
||||
with:
|
||||
category: "integration"
|
||||
ignore: brands
|
||||
|
||||
- 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@v4
|
||||
- uses: home-assistant/actions/hassfest@master
|
||||
- uses: actions/checkout@v5
|
||||
- uses: home-assistant/actions/hassfest@master
|
||||
|
||||
23
.github/workflows/lint.yaml
vendored
23
.github/workflows/lint.yaml
vendored
@@ -5,16 +5,21 @@ on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install the latest version of rye
|
||||
uses: eifinger/setup-rye@v4
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Sync dependencies
|
||||
run: rye sync
|
||||
- name: Lint (pylint/rye lint)
|
||||
run: rye run 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.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).
|
||||
|
||||
### 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.
|
||||
|
||||
#### 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
|
||||
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:
|
||||
|
||||
|
||||
@@ -47,11 +47,14 @@
|
||||
Provides an OpenID Connect (OIDC) implementation for Home Assistant through a custom component/integration. Through this integration, you can create an SSO (single-sign-on) environment within your self-hosted application stack / homelab.
|
||||
|
||||
### Background
|
||||
If you would like to read the background/open letter that lead to this component, please see https://community.home-assistant.io/t/open-letter-for-improving-home-assistants-authentication-system-oidc-sso/494223. It is currently one of the most upvoted feature requests for Home Assistant.
|
||||
If you would like to read the background/open letter that lead to this component, you can find the original post at https://community.home-assistant.io/t/open-letter-for-improving-home-assistants-authentication-system-oidc-sso/494223. It is currently one of the most upvoted feature requests for Home Assistant.
|
||||
|
||||
> [!TIP]
|
||||
> If you support the addition of this feature to the Home Assistant core, please upvote https://github.com/orgs/home-assistant/discussions/48. It's the successor of the Home Assistant Community post mentioned above (with almost 900 upvotes).
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -17,11 +18,13 @@ from .config import (
|
||||
DISPLAY_NAME,
|
||||
ID_TOKEN_SIGNING_ALGORITHM,
|
||||
GROUPS_SCOPE,
|
||||
ADDITIONAL_SCOPES,
|
||||
FEATURES,
|
||||
CLAIMS,
|
||||
ROLES,
|
||||
NETWORK,
|
||||
FEATURES_INCLUDE_GROUPS_SCOPE,
|
||||
FEATURES_FORCE_HTTPS,
|
||||
)
|
||||
|
||||
# pylint: enable=useless-import-alias
|
||||
@@ -39,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()
|
||||
@@ -66,6 +76,13 @@ async def async_setup(hass: HomeAssistant, config):
|
||||
groups_scope = my_config.get(GROUPS_SCOPE, "groups")
|
||||
if include_groups_scope:
|
||||
scope += " " + groups_scope
|
||||
# Add additional scopes if configured
|
||||
additional_scopes = my_config.get(ADDITIONAL_SCOPES, [])
|
||||
if additional_scopes:
|
||||
# Ensure we have a space before adding additional scopes
|
||||
if scope:
|
||||
scope += " "
|
||||
scope += " ".join(additional_scopes)
|
||||
|
||||
# Create the OIDC client
|
||||
oidc_client = oidc_client = OIDCClient(
|
||||
@@ -83,12 +100,23 @@ async def async_setup(hass: HomeAssistant, config):
|
||||
|
||||
# Register the views
|
||||
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(OIDCRedirectView(oidc_client))
|
||||
hass.http.register_view(OIDCCallbackView(oidc_client, provider))
|
||||
hass.http.register_view(OIDCRedirectView(oidc_client, force_https))
|
||||
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
|
||||
|
||||
@@ -8,11 +8,13 @@ DISCOVERY_URL = "discovery_url"
|
||||
DISPLAY_NAME = "display_name"
|
||||
ID_TOKEN_SIGNING_ALGORITHM = "id_token_signing_alg"
|
||||
GROUPS_SCOPE = "groups_scope"
|
||||
ADDITIONAL_SCOPES = "additional_scopes"
|
||||
FEATURES = "features"
|
||||
FEATURES_AUTOMATIC_USER_LINKING = "automatic_user_linking"
|
||||
FEATURES_AUTOMATIC_PERSON_CREATION = "automatic_person_creation"
|
||||
FEATURES_DISABLE_PKCE = "disable_rfc7636"
|
||||
FEATURES_INCLUDE_GROUPS_SCOPE = "include_groups_scope"
|
||||
FEATURES_FORCE_HTTPS = "force_https"
|
||||
CLAIMS = "claims"
|
||||
CLAIMS_DISPLAY_NAME = "display_name"
|
||||
CLAIMS_USERNAME = "username"
|
||||
@@ -46,6 +48,9 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
# String value to allow changing the groups scope
|
||||
# Defaults to 'groups' which is used by Authelia and Authentik
|
||||
vol.Optional(GROUPS_SCOPE, default="groups"): vol.Coerce(str),
|
||||
# Additional scopes to request from the OIDC provider
|
||||
# Optional, this field is unnecessary if you only use the openid and profile scopes.
|
||||
vol.Optional(ADDITIONAL_SCOPES, default=[]): vol.Coerce(list[str]),
|
||||
# Which features should be enabled/disabled?
|
||||
# Optional, defaults to sane/secure defaults
|
||||
vol.Optional(FEATURES): vol.Schema(
|
||||
@@ -65,6 +70,10 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(
|
||||
FEATURES_INCLUDE_GROUPS_SCOPE, default=True
|
||||
): 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
|
||||
|
||||
@@ -17,10 +17,14 @@ class OIDCCallbackView(HomeAssistantView):
|
||||
name = "auth:oidc:callback"
|
||||
|
||||
def __init__(
|
||||
self, oidc_client: OIDCClient, oidc_provider: OpenIDAuthProvider
|
||||
self,
|
||||
oidc_client: OIDCClient,
|
||||
oidc_provider: OpenIDAuthProvider,
|
||||
force_https: bool,
|
||||
) -> None:
|
||||
self.oidc_client = oidc_client
|
||||
self.oidc_provider = oidc_provider
|
||||
self.force_https = force_https
|
||||
|
||||
async def get(self, request: web.Request) -> web.Response:
|
||||
"""Receive response."""
|
||||
@@ -38,7 +42,7 @@ class OIDCCallbackView(HomeAssistantView):
|
||||
)
|
||||
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(
|
||||
redirect_uri, code, state
|
||||
)
|
||||
@@ -63,4 +67,6 @@ class OIDCCallbackView(HomeAssistantView):
|
||||
return web.Response(text=view_html, content_type="text/html")
|
||||
|
||||
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 web.HTTPFound(
|
||||
location="/",
|
||||
location="/?storeToken=true",
|
||||
headers={
|
||||
# Set a cookie to enable autologin on only the specific path used
|
||||
# for the POST request, with all strict parameters set
|
||||
|
||||
@@ -17,17 +17,21 @@ class OIDCRedirectView(HomeAssistantView):
|
||||
url = PATH
|
||||
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.force_https = force_https
|
||||
|
||||
async def get(self, _: web.Request) -> web.Response:
|
||||
"""Receive response."""
|
||||
|
||||
redirect_uri = get_url("/auth/oidc/callback")
|
||||
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",
|
||||
|
||||
@@ -4,12 +4,14 @@ from homeassistant.components import http
|
||||
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."""
|
||||
if (req := http.current_request.get()) is None:
|
||||
raise RuntimeError("No current request in context")
|
||||
|
||||
base_uri = str(req.url).split("/auth", 2)[0]
|
||||
if force_https:
|
||||
base_uri = base_uri.replace("http://", "https://")
|
||||
return f"{base_uri}{path}"
|
||||
|
||||
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
"auth",
|
||||
"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",
|
||||
"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.1"
|
||||
"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
|
||||
@@ -47,6 +47,10 @@ class OIDCStateInvalid(OIDCClientException):
|
||||
"Raised when the state for your request cannot be matched against a stored state."
|
||||
|
||||
|
||||
class OIDCUserinfoInvalid(OIDCClientException):
|
||||
"Raised when the user info is invalid or cannot be obtained."
|
||||
|
||||
|
||||
class OIDCIdTokenSigningAlgorithmInvalid(OIDCTokenResponseInvalid):
|
||||
"Raised when the id_token is signed with the wrong algorithm, adjust your config accordingly."
|
||||
|
||||
@@ -220,9 +224,20 @@ class OIDCClient:
|
||||
|
||||
raise OIDCTokenResponseInvalid from e
|
||||
|
||||
async def _parse_id_token(
|
||||
self, id_token: str, access_token: str | None
|
||||
) -> Optional[dict]:
|
||||
async def _get_userinfo(self, userinfo_uri, access_token):
|
||||
"""Fetches userinfo from the given URL."""
|
||||
try:
|
||||
session = await self._get_http_session()
|
||||
headers = {"Authorization": "Bearer " + access_token}
|
||||
|
||||
async with session.get(userinfo_uri, headers=headers) as response:
|
||||
await self.http_raise_for_status(response)
|
||||
return await response.json()
|
||||
except HTTPClientError as e:
|
||||
_LOGGER.warning("Error fetching userinfo: %s", e)
|
||||
raise OIDCUserinfoInvalid from e
|
||||
|
||||
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()
|
||||
@@ -232,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
|
||||
@@ -261,7 +277,7 @@ class OIDCClient:
|
||||
)
|
||||
raise OIDCIdTokenSigningAlgorithmInvalid()
|
||||
|
||||
jwk_obj = jwk.construct(
|
||||
jwk_obj = jwk.import_key(
|
||||
{
|
||||
"kty": "oct",
|
||||
"k": base64.urlsafe_b64encode(
|
||||
@@ -294,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,
|
||||
@@ -305,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]:
|
||||
@@ -395,6 +394,57 @@ class OIDCClient:
|
||||
_LOGGER.warning("Error generating authorization URL: %s", e)
|
||||
return None
|
||||
|
||||
async def parse_user_details(self, id_token: str, access_token: str) -> UserDetails:
|
||||
"""Parses the ID token and/or userinfo into user details."""
|
||||
|
||||
# Fetch userinfo if there is an userinfo_endpoint available
|
||||
# and use the data to supply the missing values in id_token
|
||||
if "userinfo_endpoint" in self.discovery_document:
|
||||
userinfo_endpoint = self.discovery_document["userinfo_endpoint"]
|
||||
userinfo = await self._get_userinfo(userinfo_endpoint, access_token)
|
||||
|
||||
# Replace missing claims in the id_token with their userinfo version
|
||||
for claim in (
|
||||
self.groups_claim,
|
||||
self.display_name_claim,
|
||||
self.username_claim,
|
||||
):
|
||||
if claim not in id_token and claim in userinfo:
|
||||
id_token[claim] = userinfo[claim]
|
||||
|
||||
# Get and parse groups (to check if it's an array)
|
||||
groups = id_token.get(self.groups_claim, [])
|
||||
if not isinstance(groups, list):
|
||||
_LOGGER.warning("Groups claim is not a list, using empty list instead.")
|
||||
groups = []
|
||||
|
||||
# Assign role if user has the required groups
|
||||
role = "invalid"
|
||||
if self.user_role in groups or self.user_role is None:
|
||||
role = "system-users"
|
||||
|
||||
if self.admin_role in groups:
|
||||
role = "system-admin"
|
||||
|
||||
# Create a user details dict based on the contents of the id_token & userinfo
|
||||
return {
|
||||
# Subject Identifier. A locally unique and never reassigned identifier within the
|
||||
# Issuer for the End-User, which is intended to be consumed by the Client
|
||||
# Only unique per issuer, so we combine it with the issuer and hash it.
|
||||
# This might allow multiple OIDC providers to be used with this integration.
|
||||
"sub": hashlib.sha256(
|
||||
f"{self.discovery_document['issuer']}.{id_token.get('sub')}".encode(
|
||||
"utf-8"
|
||||
)
|
||||
).hexdigest(),
|
||||
# Display name, configurable
|
||||
"display_name": id_token.get(self.display_name_claim),
|
||||
# Username, configurable
|
||||
"username": id_token.get(self.username_claim),
|
||||
# Role
|
||||
"role": role,
|
||||
}
|
||||
|
||||
async def async_complete_token_flow(
|
||||
self, redirect_uri: str, code: str, state: str
|
||||
) -> Optional[UserDetails]:
|
||||
@@ -433,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!")
|
||||
@@ -451,40 +499,8 @@ class OIDCClient:
|
||||
_LOGGER.warning("Nonce mismatch!")
|
||||
return None
|
||||
|
||||
# TODO: If the configured claims are not present in id_token, we should fetch userinfo
|
||||
|
||||
# Get and parse groups (to check if it's an array)
|
||||
groups = id_token.get(self.groups_claim, [])
|
||||
if not isinstance(groups, list):
|
||||
_LOGGER.warning("Groups claim is not a list, using empty list instead.")
|
||||
groups = []
|
||||
|
||||
# Assign role if user has the required groups
|
||||
role = "invalid"
|
||||
if self.user_role in groups or self.user_role is None:
|
||||
role = "system-users"
|
||||
|
||||
if self.admin_role in groups:
|
||||
role = "system-admin"
|
||||
|
||||
# Create a user details dict based on the contents of the id_token & userinfo
|
||||
data: UserDetails = {
|
||||
# Subject Identifier. A locally unique and never reassigned identifier within the
|
||||
# Issuer for the End-User, which is intended to be consumed by the Client
|
||||
# Only unique per issuer, so we combine it with the issuer and hash it.
|
||||
# This might allow multiple OIDC providers to be used with this integration.
|
||||
"sub": hashlib.sha256(
|
||||
f"{self.discovery_document['issuer']}.{id_token.get('sub')}".encode(
|
||||
"utf-8"
|
||||
)
|
||||
).hexdigest(),
|
||||
# Display name, configurable
|
||||
"display_name": id_token.get(self.display_name_claim),
|
||||
# Username, configurable
|
||||
"username": id_token.get(self.username_claim),
|
||||
# Role
|
||||
"role": role,
|
||||
}
|
||||
access_token = token_response.get("access_token")
|
||||
data = await self.parse_user_details(id_token, access_token)
|
||||
|
||||
# Log which details were obtained for debugging
|
||||
# Also log the original subject identifier such that you can look it up in your provider
|
||||
|
||||
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>
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ For now, this integration is configured using YAML in your `configuration.yaml`
|
||||
|
||||
```yaml
|
||||
auth_oidc:
|
||||
client_id: ""
|
||||
discovery_url: ""
|
||||
client_id: ""
|
||||
discovery_url: ""
|
||||
```
|
||||
|
||||
The default settings assume that you configure Home Assistant as a **public client**, without a client secret. If so, you should only need to provide the `client_id` from your OIDC provider and it's discovery URL (ending in `.well-known/openid-configuration`).
|
||||
@@ -17,12 +17,13 @@ You don't have to configure other settings in most cases, as they have secure de
|
||||
## Provider Configurations
|
||||
Here are some documentation links for specific providers that you may want to follow:
|
||||
|
||||
| <img src="https://goauthentik.io/img/icon_top_brand_colour.svg" width="100"> | <img src="https://www.authelia.com/images/branding/logo-cropped.png" width="100"> | <img src="https://github.com/user-attachments/assets/4ceb2708-9f29-4694-b797-be833efce17d" width="100"> |
|
||||
|:-----------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:|
|
||||
| [Authentik](./provider-configurations/authentik.md) | [Authelia](./provider-configurations/authelia.md) | [Pocket ID](./provider-configurations/pocket-id.md) |
|
||||
* [Authentik](./provider-configurations/authentik.md)
|
||||
* [Authelia](./provider-configurations/authelia.md)
|
||||
* [Pocket ID](./provider-configurations/pocket-id.md)
|
||||
* [Kanidm](./provider-configurations/kanidm.md)
|
||||
* [Microsoft Entra ID](./provider-configurations/microsoft-entra.md)
|
||||
|
||||
|
||||
Are you using another provider? Another user might have added configuration instructions here: [Other providers](./provider-configurations/other.md)
|
||||
_Missing a provider? Submit your guide using a PR._
|
||||
|
||||
## Common Configurations
|
||||
### Configuring Client Secret
|
||||
@@ -30,9 +31,9 @@ If you want to configure Home Assistant as a **confidential client**, you should
|
||||
|
||||
```yaml
|
||||
auth_oidc:
|
||||
client_id: ""
|
||||
client_secret: !secret oidc_client_secret
|
||||
discovery_url: ""
|
||||
client_id: ""
|
||||
client_secret: !secret oidc_client_secret
|
||||
discovery_url: ""
|
||||
```
|
||||
|
||||
You should use the Home Assistant secrets helper (`!secret`) to make sure you store secrets securely. See https://www.home-assistant.io/docs/configuration/secrets/ for more information.
|
||||
@@ -46,17 +47,17 @@ If your provider isn't listed above, you might want to configure OIDC settings y
|
||||
|
||||
```yaml
|
||||
auth_oidc:
|
||||
client_id: ""
|
||||
discovery_url: ""
|
||||
id_token_signing_alg: <HS256 or RS256>
|
||||
groups_scope: <groups scope>
|
||||
claims:
|
||||
display_name: <display name claim from your provider>
|
||||
username: <username claim from your provider>
|
||||
groups: <groups claim from your provider>
|
||||
roles:
|
||||
admin: <group name to use for admins>
|
||||
user: <group name to use for users>
|
||||
client_id: ""
|
||||
discovery_url: ""
|
||||
id_token_signing_alg: <HS256, RS256, ES256, ...>
|
||||
groups_scope: <groups scope>
|
||||
claims:
|
||||
display_name: <display name claim from your provider>
|
||||
username: <username claim from your provider>
|
||||
groups: <groups claim from your provider>
|
||||
roles:
|
||||
admin: <group name to use for admins>
|
||||
user: <group name to use for users>
|
||||
```
|
||||
|
||||
If you configure the user role, OIDC users that have neither configured group name will be rejected! If you configure the admin role, users with that role will receive administrator rights in Home Assistant automatically upon login.
|
||||
@@ -66,22 +67,44 @@ If you would like to change the default name on the OIDC welcome screen and Home
|
||||
|
||||
```yaml
|
||||
auth_oidc:
|
||||
client_id: ""
|
||||
discovery_url: ""
|
||||
display_name: "Example"
|
||||
client_id: ""
|
||||
discovery_url: ""
|
||||
display_name: "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
|
||||
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):
|
||||
|
||||
```yaml
|
||||
auth_oidc:
|
||||
client_id: "someValueForTheClientId"
|
||||
discovery_url: "https://example.com/application/o/application/.well-known/openid-configuration"
|
||||
features:
|
||||
automatic_user_linking: true
|
||||
client_id: "someValueForTheClientId"
|
||||
discovery_url: "https://example.com/application/o/application/.well-known/openid-configuration"
|
||||
features:
|
||||
automatic_user_linking: true
|
||||
```
|
||||
|
||||
Upon login, OIDC users will then automatically be linked to the HA user with the same username. It's recommended to **only enable this temporarily** as it may pose a security risk. You should disable it after linking all your users, as existing links will still work if you disable it, but no new links will be created.
|
||||
@@ -92,6 +115,8 @@ Upon login, OIDC users will then automatically be linked to the HA user with the
|
||||
> [!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!
|
||||
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -99,16 +124,16 @@ You can either make the CA known to the entire operating system or configure onl
|
||||
|
||||
```yaml
|
||||
auth_oidc:
|
||||
network:
|
||||
tls_ca_path: /path/to/private-ca.pem
|
||||
network:
|
||||
tls_ca_path: /path/to/private-ca.pem
|
||||
```
|
||||
|
||||
If you want to deactivate the validation of all TLS certificates for test purposes, you can do this via `network.tls_verify: false`:
|
||||
|
||||
```yaml
|
||||
auth_oidc:
|
||||
network:
|
||||
tls_verify: false
|
||||
network:
|
||||
tls_verify: false
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
@@ -126,10 +151,12 @@ Here's a table of all options that you can set:
|
||||
| `display_name` | `string` | No | `"OpenID Connect (SSO)"` | The name to display on the login screen, both for the Home Assistant screen and the OIDC welcome screen. |
|
||||
| `id_token_signing_alg` | `string` | No | `RS256` | The signing algorithm that is used for your id_tokens.
|
||||
| `groups_scope` | `string` | No | `groups` | Override the default grups scope with another scope of your choice. |
|
||||
| `additional_scopes`|`list of strings`| No | `empty list` | Add additional scopes to request for custom identity provider configurations in addition to the automatic `openid` and `profile` scopes and the `groups_scope` configuration option |
|
||||
| `features.automatic_user_linking` | `boolean`| No | `false` | Automatically links users to existing Home Assistant users based on the OIDC username claim. Disabled by default for security. When disabled, OIDC users will get their own new user profile upon first login. |
|
||||
| `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.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.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). |
|
||||
|
||||
@@ -24,7 +24,7 @@ identity_providers:
|
||||
- 'openid'
|
||||
- 'profile'
|
||||
- 'groups'
|
||||
userinfo_signed_response_alg: 'RS256'
|
||||
id_token_signed_response_alg: 'RS256'
|
||||
```
|
||||
|
||||
Home Assistant `configuration.yaml`
|
||||
@@ -56,7 +56,7 @@ identity_providers:
|
||||
- 'openid'
|
||||
- 'profile'
|
||||
- 'groups'
|
||||
userinfo_signed_response_alg: 'RS256'
|
||||
id_token_signed_response_alg: 'RS256'
|
||||
token_endpoint_auth_method: 'client_secret_post'
|
||||
```
|
||||
|
||||
|
||||
145
docs/provider-configurations/kanidm.md
Normal file
145
docs/provider-configurations/kanidm.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Kanidm
|
||||
|
||||
## Public client configuration
|
||||
|
||||
[Home Assistant](https://github.com/home-assistant/core) `/var/lib/hass/configuration.yaml`
|
||||
|
||||
```yaml
|
||||
auth_oidc:
|
||||
client_id: "homeassistant"
|
||||
discovery_url: "https://idm.example.org/oauth2/openid/homeassistant/.well-known/openid-configuration"
|
||||
features:
|
||||
automatic_person_creation: true
|
||||
id_token_signing_alg: "ES256"
|
||||
roles:
|
||||
admin: "homeassistant_admins@idm.example.org"
|
||||
user: "idm_all_persons@idm.example.org"
|
||||
```
|
||||
|
||||
[Kanidm](https://github.com/kanidm/kanidm)
|
||||
|
||||
1. Create your Kanidm account, if you don't have one already:
|
||||
|
||||
```shell
|
||||
kanidm person create "your_username" "Your Username" --name "idm_admin"
|
||||
```
|
||||
|
||||
2. Create a new Kanidm group for your HomeAssistant administrators (`homeassistant_admins`), and add your regular account to it:
|
||||
|
||||
```shell
|
||||
kanidm group create "homeassistant_admins" --name "idm_admin"
|
||||
kanidm group add-members "homeassistant_admins" "your_username" --name "idm_admin"
|
||||
```
|
||||
|
||||
3. Create a new OAuth2 application configuration in Kanidm (`homeassistant`), configure the redirect URL, and scope access:
|
||||
|
||||
```shell
|
||||
kanidm system oauth2 create-public "homeassistant" "Home Assistant" "https://hass.example.org/auth/oidc/welcome" --name "idm_admin"
|
||||
kanidm system oauth2 add-redirect-url "homeassistant" "https://hass.example.org/auth/oidc/callback" --name "idm_admin"
|
||||
kanidm system oauth2 update-scope-map "homeassistant" "homeassistant_users" "email" "groups" "openid" "profile" --name "idm_admin"
|
||||
```
|
||||
|
||||
[Kanidm Provision](https://github.com/oddlama/kanidm-provision) `state.json`
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"groups": {
|
||||
"homeassistant_admins": {
|
||||
"members": ["your_username"]
|
||||
}
|
||||
},
|
||||
"persons": {
|
||||
"your_username": {
|
||||
"displayName": "Your Username"
|
||||
},
|
||||
},
|
||||
"systems": {
|
||||
"oauth2": {
|
||||
"homeassistant": {
|
||||
"displayName": "Home Assistant",
|
||||
"originLanding": "https://hass.example.org/auth/oidc/welcome",
|
||||
"originUrl": "https://hass.example.org/auth/oidc/callback",
|
||||
"public": true,
|
||||
"scopeMaps": {
|
||||
"homeassistant_users": ["email", "groups", "openid", "profile"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Confidential client configuration
|
||||
|
||||
[Home Assistant](https://github.com/home-assistant/core) `/var/lib/hass/configuration.yaml`
|
||||
|
||||
```yaml
|
||||
auth_oidc:
|
||||
client_id: "homeassistant"
|
||||
client_secret: !secret oidc_client_secret
|
||||
discovery_url: "https://idm.example.org/oauth2/openid/homeassistant/.well-known/openid-configuration"
|
||||
features:
|
||||
automatic_person_creation: true
|
||||
id_token_signing_alg: "ES256"
|
||||
roles:
|
||||
admin: "homeassistant_admins@idm.example.org"
|
||||
user: "idm_all_persons@idm.example.org"
|
||||
```
|
||||
|
||||
[Kanidm](https://github.com/kanidm/kanidm)
|
||||
|
||||
1. Create your Kanidm account, if you don't have one already:
|
||||
|
||||
```shell
|
||||
kanidm person create "your_username" "Your Username" --name "idm_admin"
|
||||
```
|
||||
|
||||
2. Create a new Kanidm group for your HomeAssistant administrators (`homeassistant_admins`), and add your regular account to it:
|
||||
|
||||
```shell
|
||||
kanidm group create "homeassistant_admins" --name "idm_admin"
|
||||
kanidm group add-members "homeassistant_admins" "your_username" --name "idm_admin"
|
||||
```
|
||||
|
||||
3. Create a new OAuth2 application configuration in Kanidm (`homeassistant`), configure the redirect URL, and scope access:
|
||||
|
||||
```shell
|
||||
kanidm system oauth2 create "homeassistant" "Home Assistant" "https://hass.example.org/auth/oidc/welcome" --name "idm_admin"
|
||||
kanidm system oauth2 add-redirect-url "homeassistant" "https://hass.example.org/auth/oidc/callback" --name "idm_admin"
|
||||
kanidm system oauth2 update-scope-map "homeassistant" "homeassistant_users" "email" "groups" "openid" "profile" --name "idm_admin"
|
||||
```
|
||||
|
||||
4. Get the `homeassistant` OAuth2 client secret from Kanidm:
|
||||
|
||||
```shell
|
||||
kanidm system oauth2 show-basic-secret "homeassistant" --name "idm_admin" | xargs echo 'oidc_client_secret: {}' | tee --append "/var/lib/hass/secrets.yaml"
|
||||
```
|
||||
|
||||
[Kanidm Provision](https://github.com/oddlama/kanidm-provision) `state.json`
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"groups": {
|
||||
"homeassistant_admins": {
|
||||
"members": ["your_username"]
|
||||
}
|
||||
},
|
||||
"persons": {
|
||||
"your_username": {
|
||||
"displayName": "Your Username"
|
||||
},
|
||||
},
|
||||
"systems": {
|
||||
"oauth2": {
|
||||
"homeassistant": {
|
||||
"displayName": "Home Assistant",
|
||||
"originLanding": "https://hass.example.org/auth/oidc/welcome",
|
||||
"originUrl": "https://hass.example.org/auth/oidc/callback",
|
||||
"scopeMaps": {
|
||||
"homeassistant_users": ["email", "groups", "openid", "profile"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,10 +1,7 @@
|
||||
# Other providers
|
||||
Under construction.
|
||||
|
||||
## Microsoft Entra ID
|
||||
# Microsoft Entra ID
|
||||
> [!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.
|
||||
|
||||
## Basic configuration
|
||||
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.
|
||||
3. Copy the 'Application (client) ID' on the overview page of your app and use it as your `client_id`.
|
||||
@@ -28,3 +25,27 @@ auth_oidc:
|
||||
|
||||
> [!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!
|
||||
|
||||
## 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.
|
||||
@@ -1,2 +1,58 @@
|
||||
# Pocket ID
|
||||
Under construction.
|
||||
|
||||
## Public client configuration
|
||||
|
||||
### Pocket ID configuration
|
||||
1. Login to Pocket ID and go to `OIDC Clients`
|
||||
|
||||
2. Click on `Add OIDC Client`
|
||||
|
||||
3. Fill the following details:
|
||||
- Name: `Home Assistant`
|
||||
- Callback URLs: `<your-homeassistant-url>/auth/oidc/callback` (for example: https://hass.example.com/auth/oidc/callback)
|
||||
- Click on `Public Client` (PKCE will be automatically marked when doing this)
|
||||
|
||||
4. Click on `Save`
|
||||
|
||||
5. Click on `Show more details` and note down your `Client ID` and `OIDC Discovery URL` since you will need them later.
|
||||
|
||||
### Home Assistant configuration
|
||||
1. Add following configuration in Home Assistant's configuration.yaml:
|
||||
```yaml
|
||||
auth_oidc:
|
||||
client_id: <The Client ID you have noted down>
|
||||
discovery_url: <The OIDC Discovery URL you have noted down> (for example: https://id.example.com/.well-known/openid-configuration)
|
||||
```
|
||||
|
||||
2. Restart Home Assistant and go to your Home Assistant OIDC URL (for example: https://hass.example.com/auth/oidc/welcome)
|
||||
|
||||
## Confidential client configuration
|
||||
|
||||
### Pocket ID configuration
|
||||
1. Login to Pocket ID and go to `OIDC Clients`
|
||||
|
||||
2. Click on `Add OIDC Client`
|
||||
|
||||
3. Fill the following details:
|
||||
- Name: `Home Assistant`
|
||||
- Callback URLs: `<your-homeassistant-url>/auth/oidc/callback` (for example: https://hass.example.com/auth/oidc/callback)
|
||||
|
||||
4. Click on `Save`
|
||||
|
||||
5. Click on `Show more details` and note down your:
|
||||
- `Client ID`
|
||||
- `Client secret`
|
||||
- `OIDC Discovery URL`
|
||||
|
||||
### Home Assistant configuration
|
||||
1. Add following configuration in Home Assistant's configuration.yaml:
|
||||
```yaml
|
||||
auth_oidc:
|
||||
client_id: <The Client ID you have noted down>
|
||||
client_secret: <The Client secret you have noted down>
|
||||
discovery_url: <The OIDC Discovery URL you have noted down> (for example: https://id.example.com/.well-known/openid-configuration)
|
||||
```
|
||||
|
||||
2. Restart Home Assistant and go to your Home Assistant OIDC URL (for example: https://hass.example.com/auth/oidc/welcome)
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Install the integration through [HACS](https://hacs.xyz/). You can add it automa
|
||||
|
||||
|
||||
### Step 2: Configuration of the integration
|
||||
The integration is currently configurable through YAML only. See the [Configuration Guide](./docs/configuration.md) for more details or pick your OIDC provider below:
|
||||
The integration is currently configurable through YAML only. See the [Configuration Guide](./configuration.md) for more details or pick your OIDC provider below (additional providers are available in the Configuration Guide):
|
||||
|
||||
| <img src="https://goauthentik.io/img/icon_top_brand_colour.svg" width="100"> | <img src="https://www.authelia.com/images/branding/logo-cropped.png" width="100"> | <img src="https://github.com/user-attachments/assets/4ceb2708-9f29-4694-b797-be833efce17d" width="100"> |
|
||||
|:-----------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:|
|
||||
@@ -19,8 +19,8 @@ By default, the integration assumes you configure Home Assistant as a **public c
|
||||
|
||||
```yaml
|
||||
auth_oidc:
|
||||
client_id: "example"
|
||||
discovery_url: "https://example.com/.well-known/openid-configuration"
|
||||
client_id: "example"
|
||||
discovery_url: "https://example.com/.well-known/openid-configuration"
|
||||
```
|
||||
|
||||
When registering Home Assistant at your OIDC provider, use `<your HA URL>/auth/oidc/callback` as the callback URL and select 'public client'. You should now get the `client_id` and `issuer_url` or `discovery_url` to fill in.
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
"name": "OpenID Connect",
|
||||
"hide_default_branch": 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]
|
||||
name = "hass-oidc-auth"
|
||||
version = "0.6.1"
|
||||
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"
|
||||
requires-python = "~=3.14.2"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"homeassistant~=2026.1",
|
||||
"pylint~=4.0",
|
||||
"ruff~=0.12",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.rye]
|
||||
[tool.uv]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"homeassistant~=2024.12",
|
||||
"pylint~=3.3",
|
||||
]
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
@@ -32,11 +35,5 @@ allow-direct-references = true
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["custom_components/auth_oidc"]
|
||||
|
||||
[tool.rye.scripts]
|
||||
check = { chain = ["check-lint", "check-fmt", "check-pylint" ] }
|
||||
"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"
|
||||
[tool.ruff]
|
||||
target-version = "py313"
|
||||
@@ -14,11 +14,16 @@
|
||||
],
|
||||
"prCreation": "immediate"
|
||||
},
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Group all GitHub Actions updates",
|
||||
"matchDatasources": [
|
||||
"github-actions"
|
||||
"github-actions",
|
||||
"github-tags",
|
||||
"github-runners"
|
||||
],
|
||||
"groupName": "Github Actions Updates",
|
||||
"automerge": true
|
||||
@@ -34,7 +39,7 @@
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "Version updates for other pip packages",
|
||||
"description": "Version updates for other Python packages",
|
||||
"matchDatasources": [
|
||||
"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