Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions custom_components/auth_oidc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
FEATURES_DISABLE_FRONTEND_INJECTION,
FEATURES_FORCE_HTTPS,
REQUIRED_SCOPES,
VERBOSE_DEBUG_MODE,
NETWORK_USERINFO_FALLBACK,
)

from .config import convert_ui_config_entry_to_internal_format
Expand Down Expand Up @@ -134,6 +136,8 @@ async def _setup_oidc_provider(hass: HomeAssistant, my_config: dict, display_nam
claims=my_config.get(CLAIMS, {}),
roles=my_config.get(ROLES, {}),
network=my_config.get(NETWORK, {}),
enable_verbose_debug_mode=my_config.get(VERBOSE_DEBUG_MODE, False),
userinfo_fallback=my_config.get(NETWORK_USERINFO_FALLBACK, False),
)

# Register the views
Expand Down
4 changes: 3 additions & 1 deletion custom_components/auth_oidc/config/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@
NETWORK = "network"
NETWORK_TLS_VERIFY = "tls_verify"
NETWORK_TLS_CA_PATH = "tls_ca_path"
NETWORK_USERINFO_FALLBACK = "userinfo_fallback"
VERBOSE_DEBUG_MODE = "enable_verbose_debug_mode"

## ===
## Default configurations for providers
## ===

REQUIRED_SCOPES = "openid profile"
REQUIRED_SCOPES = "openid profile email"
DEFAULT_ID_TOKEN_SIGNING_ALGORITHM = "RS256"

DEFAULT_GROUPS_SCOPE = "groups"
Expand Down
12 changes: 12 additions & 0 deletions custom_components/auth_oidc/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
NETWORK,
NETWORK_TLS_VERIFY,
NETWORK_TLS_CA_PATH,
NETWORK_USERINFO_FALLBACK,
DOMAIN,
DEFAULT_GROUPS_SCOPE,
VERBOSE_DEBUG_MODE,
)

CONFIG_SCHEMA = vol.Schema(
Expand All @@ -53,6 +55,11 @@
# 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]),
# Added for debugging purposes
# If enabled, logging will include more detailed information regarding
# the full OIDC auth chain (including tokens) and is captured within:
# <component_dir>/custom_components/auth_oidc/verbose_debug/
vol.Optional(VERBOSE_DEBUG_MODE, default=False): vol.Coerce(bool),
# Which features should be enabled/disabled?
# Optional, defaults to sane/secure defaults
vol.Optional(FEATURES): vol.Schema(
Expand Down Expand Up @@ -115,6 +122,11 @@
),
# Load custom certificate chain for private CAs
vol.Optional(NETWORK_TLS_CA_PATH): vol.Coerce(str),
# Constructed Userinfo endpoint fallback if not provided in discovery
# Some OPs omit this endpoint
vol.Optional(
NETWORK_USERINFO_FALLBACK, default=False
): vol.Coerce(bool),
}
),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ async def frontend_injection(hass: HomeAssistant, sso_name: str) -> None:
"/auth/oidc/static/style.css",
hass.config.path("custom_components/auth_oidc/static/style.css"),
cache_headers=False,
)
),
]
)

Expand Down
7 changes: 3 additions & 4 deletions custom_components/auth_oidc/stores/code_store.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Code Store, stores the codes and their associated authenticated user temporarily."""

import random
import string
import secrets

from datetime import datetime, timedelta, timezone
from typing import cast, Optional
Expand Down Expand Up @@ -37,8 +36,8 @@ async def _async_save(self) -> None:
await self._store.async_save(self._data)

def _generate_code(self) -> str:
"""Generate a random six-digit code."""
return "".join(random.choices(string.digits, k=6))
"""Generate a secure URL-safe code for temporary handoff."""
return secrets.token_urlsafe(16)

async def async_generate_code_for_userinfo(self, user_info: UserDetails) -> str:
"""Generates a one time code and adds it to the database for 5 minutes."""
Expand Down
110 changes: 110 additions & 0 deletions custom_components/auth_oidc/tools/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"""Helper functions for the integration."""

import logging
from pathlib import Path
from typing import Optional

import aiofiles
from homeassistant.components import http

from ..views.loader import AsyncTemplateRenderer


Expand All @@ -22,3 +28,107 @@ async def get_view(template: str, parameters: dict | None = None) -> str:

renderer = AsyncTemplateRenderer()
return await renderer.render_template(f"{template}.html", **parameters)


def compute_allowed_signing_algs(
discovery: dict,
id_token_signing_alg: Optional[str],
verbose_debug_mode: bool,
logger: logging.Logger,
) -> list[str]:
"""Compute allowed ID token signing algorithms from config and OP discovery document.

- If `id_token_signing_alg` set: Use only it (warn if not in OP-supported).
- Else: Use OP's `id_token_signing_alg_values_supported` (fallback ['RS256']).

Args:
discovery: Fetched OIDC discovery document.
id_token_signing_alg: Configured alg from
self.id_token_signing_alg (or None; falls
back to DEFAULT_ID_TOKEN_SIGNING_ALGORITHM="RS256").
verbose_debug_mode: Enable debug logs.
logger: Logger instance (e.g., _LOGGER).

Returns:
List of allowed algs (e.g., ['RS256', 'ES256']).
"""
supported_algs = discovery.get("id_token_signing_alg_values_supported", [])

if id_token_signing_alg:
allowed_algs = [id_token_signing_alg]
if id_token_signing_alg not in supported_algs:
logger.warning(
(
"Configured signing algorithm '%s' is not in OP"
" supported algorithms: %s. Proceeding anyway."
),
id_token_signing_alg,
supported_algs,
)
else:
allowed_algs = supported_algs or ["RS256"]
if not supported_algs:
logger.info(
(
"No signing algorithms supported from OP"
" discovery document! Will default to RS256"
)
)

if verbose_debug_mode:
logger.debug("Allowed ID token signing algorithms: %s", allowed_algs)

return allowed_algs


async def capture_auth_flows(
log_info: tuple[logging.Logger, int],
verbose_debug_mode: bool,
capture_dir: Path | None,
debug_msg: str,
filename: str,
content: str,
mode: str = "a",
header: str = "",
is_request: bool = False,
) -> None:
"""Helper to log verbose debug messages and optionally capture content to file.

Reduces repetition in OIDCClient/OIDCDiscoveryClient verbose logging and file captures.
Only writes/captures if verbose_debug_mode is True and capture_dir exists.

Args:
log_info: Tuple containing logger instance (e.g., (_LOGGER, 10) is Debug level).
verbose_debug_mode: Whether verbose mode is enabled.
capture_dir: Directory path for captures (if None, skips file write).
debug_msg: Message for _LOGGER.debug().
filename: Base filename for capture file (e.g., 'get_discovery.txt').
content: Content to write (e.g., JSON string or URL).
mode: File write mode ('w' to overwrite, 'a' to append).
header: Prepend header comment to content (e.g., discovery endpoint info).
is_request: If True, uses 'BEGIN REQUEST' header; else 'BEGIN RESPONSE'.
"""

# Unpack logger and log level
logger, log_level = log_info

if verbose_debug_mode:
logger.log(log_level, debug_msg)

if verbose_debug_mode and capture_dir:
header_str = (
f"/*\n----------BEGIN {'REQUEST' if is_request else 'RESPONSE'}----------\n"
f"{header}*/\n\n"
if header
else ""
)
full_content = header_str + content
file_path = capture_dir / filename
async with aiofiles.open(file_path, mode=mode, encoding="utf-8") as f:
await f.write(full_content)
logger.log(
log_level,
"Check %s capture in: %s for more details...",
filename,
file_path,
)
Loading
Loading