Compare commits
5 Commits
v0.4.0-alp
...
v0.5.1-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63f5f175ee | ||
|
|
bfad0418ad | ||
|
|
00da053f50 | ||
|
|
2131fe5d36 | ||
|
|
72dbc49c6f |
24
README.md
24
README.md
@@ -69,6 +69,10 @@ With the default configuration, [a person entry](https://www.home-assistant.io/i
|
|||||||
| `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). |
|
||||||
|
| `roles.admin` | `string` | No | `admins` | Group name to require for users to get the 'admin' role in Home Assistant. Defaults to 'admins', the default group name for admins in Authentik. Doesn't do anything if no groups claim is found in your token. |
|
||||||
|
| `roles.user` | `string` | No | | Group name to require for users to get the 'user' role in Home Assistant. Defaults to giving all users this role, unless configured. |
|
||||||
|
| `network.tls_verify` | `boolean` | No | `true` | Verify TLS certificate. You may want to set this set to `false` when testing locally. |
|
||||||
|
| `network.tls_ca_path` | `string` | No | | Path to file containing a private certificate authority chain. |
|
||||||
|
|
||||||
#### Example: Migrating from HA username/password users to OIDC users
|
#### Example: 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):
|
||||||
@@ -86,6 +90,26 @@ Upon login, OIDC users will then automatically be linked to the HA user with the
|
|||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> It's recommended to only enable this temporarily as it may pose a security risk. Any OIDC user with a username corresponding to a user in Home Assistant can get access to that user, and it's existing rights (admin), even if MFA is currently enabled for that account. After you have migrated your users (and linked OIDC to all existing accounts) you can disable the feature and keep using the linked users.
|
> It's recommended to only enable this temporarily as it may pose a security risk. Any OIDC user with a username corresponding to a user in Home Assistant can get access to that user, and it's existing rights (admin), even if MFA is currently enabled for that account. After you have migrated your users (and linked OIDC to all existing accounts) you can disable the feature and keep using the linked users.
|
||||||
|
|
||||||
|
#### Example: Using a private certificate authority
|
||||||
|
If you use a private certificate authority to secure your OIDC provider (e.g. Keycloak), your CA must be able to be used by this component. Otherwise you will receive a certificate error (`[SSL: CERTIFICATE_VERIFY_FAILED]`) when connecting to the OIDC provider.
|
||||||
|
You can either make the CA known to the entire operating system or configure only this component to use the CA. If you only want to let this component know your CA, you can specify it via `network.tls_ca_path`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth_oidc:
|
||||||
|
network:
|
||||||
|
tls_ca_path: /path/to/private-ca.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to deactivate the validation of the certificates for test purposes, you can do this via `network.tls_verify: false`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth_oidc:
|
||||||
|
network:
|
||||||
|
tls_verify: false
|
||||||
|
```
|
||||||
|
|
||||||
|
In productive use, however, you should set `network.tls_verify` to `true`.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
This project uses the Rye package manager for development. You can find installation instructions here: https://rye.astral.sh/guide/installation/.
|
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.
|
Start by installing the dependencies using `rye sync` and then point your editor towards the environment created in the `.venv` directory.
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from .config import (
|
|||||||
ID_TOKEN_SIGNING_ALGORITHM,
|
ID_TOKEN_SIGNING_ALGORITHM,
|
||||||
FEATURES,
|
FEATURES,
|
||||||
CLAIMS,
|
CLAIMS,
|
||||||
|
ROLES,
|
||||||
|
NETWORK,
|
||||||
)
|
)
|
||||||
|
|
||||||
# pylint: enable=useless-import-alias
|
# pylint: enable=useless-import-alias
|
||||||
@@ -50,10 +52,11 @@ async def async_setup(hass: HomeAssistant, config):
|
|||||||
|
|
||||||
_LOGGER.info("Registered OIDC provider")
|
_LOGGER.info("Registered OIDC provider")
|
||||||
|
|
||||||
# We only use openid & profile, never email
|
# We only use openid, profile & groups, never email
|
||||||
scope = "openid profile"
|
scope = "openid profile groups"
|
||||||
|
|
||||||
oidc_client = oidc_client = OIDCClient(
|
oidc_client = oidc_client = OIDCClient(
|
||||||
|
hass=hass,
|
||||||
discovery_url=my_config.get(DISCOVERY_URL),
|
discovery_url=my_config.get(DISCOVERY_URL),
|
||||||
client_id=my_config.get(CLIENT_ID),
|
client_id=my_config.get(CLIENT_ID),
|
||||||
scope=scope,
|
scope=scope,
|
||||||
@@ -61,6 +64,8 @@ async def async_setup(hass: HomeAssistant, config):
|
|||||||
id_token_signing_alg=my_config.get(ID_TOKEN_SIGNING_ALGORITHM),
|
id_token_signing_alg=my_config.get(ID_TOKEN_SIGNING_ALGORITHM),
|
||||||
features=my_config.get(FEATURES, {}),
|
features=my_config.get(FEATURES, {}),
|
||||||
claims=my_config.get(CLAIMS, {}),
|
claims=my_config.get(CLAIMS, {}),
|
||||||
|
roles=my_config.get(ROLES, {}),
|
||||||
|
network=my_config.get(NETWORK, {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Register the views
|
# Register the views
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ CLAIMS = "claims"
|
|||||||
CLAIMS_DISPLAY_NAME = "display_name"
|
CLAIMS_DISPLAY_NAME = "display_name"
|
||||||
CLAIMS_USERNAME = "username"
|
CLAIMS_USERNAME = "username"
|
||||||
CLAIMS_GROUPS = "groups"
|
CLAIMS_GROUPS = "groups"
|
||||||
|
ROLES = "roles"
|
||||||
|
ROLE_ADMINS = "admin"
|
||||||
|
ROLE_USERS = "user"
|
||||||
|
|
||||||
|
NETWORK = "network"
|
||||||
|
NETWORK_TLS_VERIFY = "tls_verify"
|
||||||
|
NETWORK_TLS_CA_PATH = "tls_ca_path"
|
||||||
|
|
||||||
DEFAULT_TITLE = "OpenID Connect (SSO)"
|
DEFAULT_TITLE = "OpenID Connect (SSO)"
|
||||||
|
|
||||||
@@ -63,6 +70,29 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CLAIMS_GROUPS): vol.Coerce(str),
|
vol.Optional(CLAIMS_GROUPS): vol.Coerce(str),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
# Determine which specific group values will be mapped to which roles
|
||||||
|
# Optional, defaults user = null, admin = 'admins'
|
||||||
|
# If user role is set, users that do not have either will be rejected!
|
||||||
|
vol.Optional(ROLES): vol.Schema(
|
||||||
|
{
|
||||||
|
# Which group name should we use to assign the user role?
|
||||||
|
vol.Optional(ROLE_USERS): vol.Coerce(str),
|
||||||
|
# What group name should we use to assign the admin role?
|
||||||
|
# Defaults to admins
|
||||||
|
vol.Optional(ROLE_ADMINS): vol.Coerce(str),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
# Network options
|
||||||
|
vol.Optional(NETWORK): vol.Schema(
|
||||||
|
{
|
||||||
|
# Verify x509 certificates provided when starting TLS connections
|
||||||
|
vol.Optional(NETWORK_TLS_VERIFY, default=True): vol.Coerce(
|
||||||
|
bool
|
||||||
|
),
|
||||||
|
# Load custom certificate chain for private CAs
|
||||||
|
vol.Optional(NETWORK_TLS_CA_PATH): vol.Coerce(str),
|
||||||
|
}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,5 +52,15 @@ class OIDCCallbackView(HomeAssistantView):
|
|||||||
)
|
)
|
||||||
return web.Response(text=view_html, content_type="text/html")
|
return web.Response(text=view_html, content_type="text/html")
|
||||||
|
|
||||||
|
if user_details.get("role") == "invalid":
|
||||||
|
view_html = await get_view(
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"error": "User is not in the correct group to access Home Assistant, "
|
||||||
|
+ "contact your administrator!",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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))
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ class OIDCFinishView(HomeAssistantView):
|
|||||||
# 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
|
||||||
# This cookie should not be read by any Javascript or any other paths.
|
# This cookie should not be read by any Javascript or any other paths.
|
||||||
# It can be really short lifetime as we redirect immediately (15 seconds)
|
# It can be really short lifetime as we redirect immediately (5 seconds)
|
||||||
"set-cookie": "auth_oidc_code="
|
"set-cookie": "auth_oidc_code="
|
||||||
+ code
|
+ code
|
||||||
+ "; Path=/auth/login_flow; SameSite=Strict; HttpOnly; Max-Age=15",
|
+ "; Path=/auth/login_flow; SameSite=Strict; HttpOnly; Max-Age=5",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
"requirements": [
|
"requirements": [
|
||||||
"python-jose>=3.3.0",
|
"python-jose>=3.3.0",
|
||||||
"aiofiles>=24.1.0",
|
"aiofiles>=24.1.0",
|
||||||
"jinja2>=3.1.4"
|
"jinja2>=3.1.4",
|
||||||
|
"bcrypt>=4.2.0"
|
||||||
],
|
],
|
||||||
"version": "0.4.0"
|
"version": "0.5.1"
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,12 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import ssl
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from functools import partial
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from jose import jwt, jwk
|
from jose import jwt, jwk
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .types import UserDetails
|
from .types import UserDetails
|
||||||
from .config import (
|
from .config import (
|
||||||
@@ -15,6 +18,10 @@ from .config import (
|
|||||||
CLAIMS_DISPLAY_NAME,
|
CLAIMS_DISPLAY_NAME,
|
||||||
CLAIMS_USERNAME,
|
CLAIMS_USERNAME,
|
||||||
CLAIMS_GROUPS,
|
CLAIMS_GROUPS,
|
||||||
|
ROLE_ADMINS,
|
||||||
|
ROLE_USERS,
|
||||||
|
NETWORK_TLS_VERIFY,
|
||||||
|
NETWORK_TLS_CA_PATH,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -51,7 +58,18 @@ class OIDCClient:
|
|||||||
# Flows stores the state, code_verifier and nonce of all current flows.
|
# Flows stores the state, code_verifier and nonce of all current flows.
|
||||||
flows = {}
|
flows = {}
|
||||||
|
|
||||||
def __init__(self, discovery_url: str, client_id: str, scope: str, **kwargs: str):
|
# HTTP session to be used
|
||||||
|
http_session: aiohttp.ClientSession = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
discovery_url: str,
|
||||||
|
client_id: str,
|
||||||
|
scope: str,
|
||||||
|
**kwargs: str,
|
||||||
|
):
|
||||||
|
self.hass = hass
|
||||||
self.discovery_url = discovery_url
|
self.discovery_url = discovery_url
|
||||||
self.discovery_document = None
|
self.discovery_document = None
|
||||||
self.client_id = client_id
|
self.client_id = client_id
|
||||||
@@ -67,11 +85,25 @@ class OIDCClient:
|
|||||||
|
|
||||||
features = kwargs.get("features")
|
features = kwargs.get("features")
|
||||||
claims = kwargs.get("claims")
|
claims = kwargs.get("claims")
|
||||||
|
roles = kwargs.get("roles")
|
||||||
|
network = kwargs.get("network")
|
||||||
|
|
||||||
self.disable_pkce: bool = features.get(FEATURES_DISABLE_PKCE)
|
self.disable_pkce = features.get(FEATURES_DISABLE_PKCE, False)
|
||||||
self.display_name_claim = claims.get(CLAIMS_DISPLAY_NAME, "name")
|
self.display_name_claim = claims.get(CLAIMS_DISPLAY_NAME, "name")
|
||||||
self.username_claim = claims.get(CLAIMS_USERNAME, "preferred_username")
|
self.username_claim = claims.get(CLAIMS_USERNAME, "preferred_username")
|
||||||
self.groups_claim = claims.get(CLAIMS_GROUPS, "groups")
|
self.groups_claim = claims.get(CLAIMS_GROUPS, "groups")
|
||||||
|
self.user_role = roles.get(ROLE_USERS, None)
|
||||||
|
self.admin_role = roles.get(ROLE_ADMINS, "admins")
|
||||||
|
self.tls_verify = network.get(NETWORK_TLS_VERIFY, True)
|
||||||
|
self.tls_ca_path = network.get(NETWORK_TLS_CA_PATH)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""Cleanup the HTTP session."""
|
||||||
|
|
||||||
|
# HA never seems to run this, but it's good practice to close the session
|
||||||
|
if self.http_session:
|
||||||
|
_LOGGER.debug("Closing HTTP session")
|
||||||
|
self.http_session.close()
|
||||||
|
|
||||||
def _base64url_encode(self, value: str) -> str:
|
def _base64url_encode(self, value: str) -> str:
|
||||||
"""Uses base64url encoding on a given string"""
|
"""Uses base64url encoding on a given string"""
|
||||||
@@ -81,10 +113,37 @@ class OIDCClient:
|
|||||||
"""Generates a random URL safe string (base64_url encoded)"""
|
"""Generates a random URL safe string (base64_url encoded)"""
|
||||||
return self._base64url_encode(os.urandom(length))
|
return self._base64url_encode(os.urandom(length))
|
||||||
|
|
||||||
|
async def _get_http_session(self) -> aiohttp.ClientSession:
|
||||||
|
"""Create or get the existing client session with custom networking/TLS options"""
|
||||||
|
if self.http_session is not None:
|
||||||
|
return self.http_session
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Creating HTTP session provider with options: "
|
||||||
|
+ "verify certificates: %r, custom CA file: %s",
|
||||||
|
self.tls_verify,
|
||||||
|
self.tls_ca_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
tcp_connector_args = {"verify_ssl": self.tls_verify}
|
||||||
|
|
||||||
|
if self.tls_ca_path:
|
||||||
|
# Move to hass' executor to prevent blocking code inside non-blocking method
|
||||||
|
ssl_context = await self.hass.loop.run_in_executor(
|
||||||
|
None, partial(ssl.create_default_context, cafile=self.tls_ca_path)
|
||||||
|
)
|
||||||
|
tcp_connector_args["ssl"] = ssl_context
|
||||||
|
|
||||||
|
self.http_session = aiohttp.ClientSession(
|
||||||
|
connector=aiohttp.TCPConnector(**tcp_connector_args)
|
||||||
|
)
|
||||||
|
return self.http_session
|
||||||
|
|
||||||
async def _fetch_discovery_document(self):
|
async def _fetch_discovery_document(self):
|
||||||
"""Fetches discovery document from the given URL."""
|
"""Fetches discovery document from the given URL."""
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
session = await self._get_http_session()
|
||||||
|
|
||||||
async with session.get(self.discovery_url) as response:
|
async with session.get(self.discovery_url) as response:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return await response.json()
|
return await response.json()
|
||||||
@@ -100,7 +159,8 @@ class OIDCClient:
|
|||||||
async def _get_jwks(self, jwks_uri):
|
async def _get_jwks(self, jwks_uri):
|
||||||
"""Fetches JWKS from the given URL."""
|
"""Fetches JWKS from the given URL."""
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
session = await self._get_http_session()
|
||||||
|
|
||||||
async with session.get(jwks_uri) as response:
|
async with session.get(jwks_uri) as response:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return await response.json()
|
return await response.json()
|
||||||
@@ -111,7 +171,8 @@ class OIDCClient:
|
|||||||
async def _make_token_request(self, token_endpoint, query_params):
|
async def _make_token_request(self, token_endpoint, query_params):
|
||||||
"""Performs the token POST call"""
|
"""Performs the token POST call"""
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
session = await self._get_http_session()
|
||||||
|
|
||||||
async with session.post(token_endpoint, data=query_params) as response:
|
async with session.post(token_endpoint, data=query_params) as response:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return await response.json()
|
return await response.json()
|
||||||
@@ -356,6 +417,20 @@ class OIDCClient:
|
|||||||
|
|
||||||
# TODO: If the configured claims are not present in id_token, we should fetch userinfo
|
# 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
|
# Create a user details dict based on the contents of the id_token & userinfo
|
||||||
data: UserDetails = {
|
data: UserDetails = {
|
||||||
# Subject Identifier. A locally unique and never reassigned identifier within the
|
# Subject Identifier. A locally unique and never reassigned identifier within the
|
||||||
@@ -371,8 +446,8 @@ class OIDCClient:
|
|||||||
"display_name": id_token.get(self.display_name_claim),
|
"display_name": id_token.get(self.display_name_claim),
|
||||||
# Username, configurable
|
# Username, configurable
|
||||||
"username": id_token.get(self.username_claim),
|
"username": id_token.get(self.username_claim),
|
||||||
# Groups, configurable
|
# Role
|
||||||
"groups": id_token.get(self.groups_claim),
|
"role": role,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Log which details were obtained for debugging
|
# Log which details were obtained for debugging
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import logging
|
|||||||
|
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import bcrypt
|
||||||
from homeassistant.auth import EVENT_USER_ADDED
|
from homeassistant.auth import EVENT_USER_ADDED
|
||||||
from homeassistant.auth.providers import (
|
from homeassistant.auth.providers import (
|
||||||
AUTH_PROVIDERS,
|
AUTH_PROVIDERS,
|
||||||
@@ -258,14 +259,11 @@ class OpenIDAuthProvider(AuthProvider):
|
|||||||
sub = credentials.data["sub"]
|
sub = credentials.data["sub"]
|
||||||
meta = self._user_meta.get(sub, {})
|
meta = self._user_meta.get(sub, {})
|
||||||
|
|
||||||
groups = meta.get("groups", [])
|
role = meta.get("role")
|
||||||
|
|
||||||
# TODO: Allow setting which group is for admins
|
|
||||||
group = "system-admin" if "admins" in groups else "system-users"
|
|
||||||
return UserMeta(
|
return UserMeta(
|
||||||
name=meta.get("display_name"),
|
name=meta.get("display_name"),
|
||||||
is_active=True,
|
is_active=True,
|
||||||
group=group,
|
group=role,
|
||||||
local_only=False,
|
local_only=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -274,6 +272,14 @@ class OpenIdLoginFlow(LoginFlow):
|
|||||||
"""Handler for the login flow."""
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
async def _finalize_user(self, code: str) -> AuthFlowResult:
|
async def _finalize_user(self, code: str) -> AuthFlowResult:
|
||||||
|
# Verify a dummy hash to make it last a bit longer
|
||||||
|
# as security measure (limits the amount of attempts you have in 5 min)
|
||||||
|
# Similar to what the HomeAssistant auth provider does
|
||||||
|
dummy = b"$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO"
|
||||||
|
bcrypt.checkpw(b"foo", dummy)
|
||||||
|
|
||||||
|
# Actually look up the auth provider after,
|
||||||
|
# this doesn't take a lot of time (regardless of it's in there or not)
|
||||||
sub = await self._auth_provider.async_get_subject(code)
|
sub = await self._auth_provider.async_get_subject(code)
|
||||||
if sub:
|
if sub:
|
||||||
return await self.async_finish(
|
return await self.async_finish(
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""Generic data types"""
|
"""Generic data types"""
|
||||||
|
|
||||||
|
|
||||||
# Dict class to give a type to the user details
|
# Dict class to give a type to the user details
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
class UserDetails(dict):
|
class UserDetails(dict):
|
||||||
"""User details representation"""
|
"""User details representation"""
|
||||||
|
|
||||||
@@ -12,5 +14,5 @@ class UserDetails(dict):
|
|||||||
# Preferred username for the user, will be used when first generating the account
|
# Preferred username for the user, will be used when first generating the account
|
||||||
# or to link the account on first login
|
# or to link the account on first login
|
||||||
username: str
|
username: str
|
||||||
# Groups that the user has, if any are sent from the OIDC provider
|
# Home Assistant role to assign to this user
|
||||||
groups: list[str]
|
role: Literal["system-admin", "system-users", "invalid"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "hass-oidc-auth"
|
name = "hass-oidc-auth"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
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" }
|
||||||
@@ -10,6 +10,7 @@ dependencies = [
|
|||||||
"python-jose>=3.3.0",
|
"python-jose>=3.3.0",
|
||||||
"aiofiles>=24.1.0",
|
"aiofiles>=24.1.0",
|
||||||
"jinja2>=3.1.4",
|
"jinja2>=3.1.4",
|
||||||
|
"bcrypt>=4.2.0",
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">= 3.13"
|
requires-python = ">= 3.13"
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ audioop-lts==0.2.1
|
|||||||
awesomeversion==24.6.0
|
awesomeversion==24.6.0
|
||||||
# via homeassistant
|
# via homeassistant
|
||||||
bcrypt==4.2.0
|
bcrypt==4.2.0
|
||||||
|
# via hass-oidc-auth
|
||||||
# via homeassistant
|
# via homeassistant
|
||||||
bleak==0.22.3
|
bleak==0.22.3
|
||||||
# via bleak-retry-connector
|
# via bleak-retry-connector
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
-e file:.
|
-e file:.
|
||||||
aiofiles==24.1.0
|
aiofiles==24.1.0
|
||||||
# via hass-oidc-auth
|
# via hass-oidc-auth
|
||||||
|
bcrypt==4.2.1
|
||||||
|
# via hass-oidc-auth
|
||||||
ecdsa==0.19.0
|
ecdsa==0.19.0
|
||||||
# via python-jose
|
# via python-jose
|
||||||
jinja2==3.1.5
|
jinja2==3.1.5
|
||||||
|
|||||||
Reference in New Issue
Block a user