Fixes for known bugs in v1.0.0-rc1 (#241)

* Fix #238 for same-site cookies

* Redirect in Python + bump to rc2
This commit is contained in:
Christiaan Goossens
2026-04-14 09:43:58 +02:00
committed by GitHub
parent c7672f65d9
commit baf3ac6b5a
10 changed files with 190 additions and 101 deletions

View File

@@ -157,6 +157,6 @@ async def _setup_oidc_provider(hass: HomeAssistant, my_config: dict, display_nam
_LOGGER.info("Registered OIDC views")
# Inject OIDC code into the frontend for /auth/authorize for automatic redirect
await OIDCInjectedAuthPage.inject(hass)
await OIDCInjectedAuthPage.inject(hass, force_https)
return True

View File

@@ -8,9 +8,7 @@ from typing import Any, Dict
DEFAULT_TITLE = "OpenID Connect (SSO)"
DOMAIN = "auth_oidc"
REPO_ROOT_URL = (
"https://github.com/christiaangoossens/hass-oidc-auth/tree/v1.0.0-rc1"
)
REPO_ROOT_URL = "https://github.com/christiaangoossens/hass-oidc-auth/tree/v1.0.0-rc1"
## ===
## Config keys

View File

@@ -1,12 +1,18 @@
"""Injected authorization page, replacing the original"""
import base64
import logging
from functools import partial
from homeassistant.components.http import HomeAssistantView, StaticPathConfig
from homeassistant.core import HomeAssistant
from urllib.parse import quote, unquote
from aiohttp import web
from aiofiles import open as async_open
from homeassistant.components.http import HomeAssistantView, StaticPathConfig
from homeassistant.core import HomeAssistant
from .welcome import PATH as WELCOME_PATH
from ..tools.helpers import get_url
PATH = "/auth/authorize"
_LOGGER = logging.getLogger(__name__)
@@ -18,7 +24,7 @@ async def read_file(path: str) -> str:
return await f.read()
async def frontend_injection(hass: HomeAssistant) -> None:
async def frontend_injection(hass: HomeAssistant, force_https: bool) -> None:
"""Inject new frontend code into /auth/authorize."""
router = hass.http.app.router
frontend_path = None
@@ -61,7 +67,7 @@ async def frontend_injection(hass: HomeAssistant) -> None:
frontend_code = await read_file(frontend_path)
# Inject JS and register that route
injection_js = "<script src='/auth/oidc/static/injection.js?v=4'></script>"
injection_js = "<script src='/auth/oidc/static/injection.js?v=6'></script>"
frontend_code = frontend_code.replace("</body>", f"{injection_js}</body>")
await hass.http.async_register_static_paths(
@@ -80,7 +86,7 @@ async def frontend_injection(hass: HomeAssistant) -> None:
)
# If everything is succesful, register a fake view that just returns the modified HTML
hass.http.register_view(OIDCInjectedAuthPage(frontend_code))
hass.http.register_view(OIDCInjectedAuthPage(frontend_code, force_https))
_LOGGER.info("Performed OIDC frontend injection")
@@ -91,18 +97,46 @@ class OIDCInjectedAuthPage(HomeAssistantView):
url = PATH
name = "auth:oidc:authorize_page"
def __init__(self, html: str) -> None:
def __init__(self, html: str, force_https: bool) -> None:
"""Initialize the injected auth page."""
self.html = html
self.force_https = force_https
@staticmethod
async def inject(hass: HomeAssistant) -> None:
async def inject(hass: HomeAssistant, force_https: bool) -> None:
"""Inject the OIDC auth page into the frontend."""
try:
await frontend_injection(hass)
await frontend_injection(hass, force_https)
except Exception as e: # pylint: disable=broad-except
_LOGGER.error("Failed to inject OIDC auth page: %s", e)
async def get(self, _) -> web.Response:
"""Return the screen"""
@staticmethod
def _should_do_oidc_redirect(req: web.Request) -> bool:
"""Check if we should redirect to the OIDC flow."""
if req.query.get("skip_oidc_redirect") == "true":
return False
redirect_uri = req.query.get("redirect_uri")
if not redirect_uri:
return False
# Handle both encoded and plain redirect_uri values.
decoded_redirect_uri = unquote(redirect_uri)
return "skip_oidc_redirect=true" not in decoded_redirect_uri
def _get_welcome_redirect_location(self, req: web.Request) -> str:
"""Build the welcome URL for the injected auth page redirect."""
encoded_current_url = quote(
base64.b64encode(str(req.url).encode("utf-8")).decode("ascii")
)
return get_url(
f"{WELCOME_PATH}?redirect_uri={encoded_current_url}",
self.force_https,
)
async def get(self, req: web.Request) -> web.Response:
"""Return the original page or redirect into the OIDC flow."""
if self._should_do_oidc_redirect(req):
raise web.HTTPFound(location=self._get_welcome_redirect_location(req))
return web.Response(text=self.html, content_type="text/html")

View File

@@ -16,8 +16,7 @@
"requirements": [
"aiofiles",
"jinja2",
"bcrypt",
"joserfc"
],
"version": "1.0.0-rc1"
"version": "1.0.0-rc2"
}

View File

@@ -6,7 +6,6 @@ import logging
from typing import Dict, Optional
import asyncio
import bcrypt
from homeassistant.auth import EVENT_USER_ADDED
from homeassistant.auth.providers import (
AUTH_PROVIDERS,
@@ -236,7 +235,7 @@ class OpenIDAuthProvider(AuthProvider):
# Keep cookie lifetime aligned with state lifetime in storage (5 minutes).
"set-cookie": f"{COOKIE_NAME}="
+ state_id
+ "; Path=/auth/; SameSite=Strict; HttpOnly; Max-Age=300"
+ "; Path=/auth/; SameSite=Lax; HttpOnly; Max-Age=300"
+ secure_flag,
}
@@ -367,14 +366,6 @@ class OpenIdLoginFlow(LoginFlow):
"""Handler for the login flow."""
async def _finalize_user(self, state_id: 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(state_id)
if sub:
return await self.async_finish(
@@ -396,11 +387,10 @@ class OpenIdLoginFlow(LoginFlow):
state_cookie = req.cookies.get(COOKIE_NAME)
if state_cookie:
_LOGGER.debug("State cookie found on login: %s", state_cookie)
try:
return await self._finalize_user(state_cookie)
except InvalidAuthError:
pass
return self.async_abort(reason="oidc_cookie_invalid")
# If no cookie is found, abort.
# User should either be redirected or start manually on the welcome

View File

@@ -1,58 +1,19 @@
/**
* OIDC Frontend Redirect injection script
* This script is injected because the 'hass-oidc-auth' custom component is active.
* hass-oidc-auth - UX script to automatically select the Home Assistant auth provider when the "Login aborted" message is shown.
*/
function attempt_oidc_redirect() {
// Get URL parameters
const urlParams = new URLSearchParams(window.location.search);
let authFlowElement = null
// Check if we have skip_oidc_redirect directly here
if (urlParams.get('skip_oidc_redirect') === 'true') {
// No console log because this is intended behavior
return;
}
const originalUrl = urlParams.get('redirect_uri');
if (!originalUrl) {
console.warn('[OIDC] No OAuth2 redirect_uri parameter found in the URL. Frontend redirect cancelled.');
return;
}
try {
// Parse the redirect URI
const redirectUrl = new URL(originalUrl);
// Check if redirect URI has a query parameter to stop OIDC injection
if (redirectUrl.searchParams.get('skip_oidc_redirect') === 'true') {
// No console log because this is intended behavior
return;
}
} catch (error) {
console.error('[OIDC] Invalid redirect_uri parameter:', error);
}
window.stop(); // Stop loading the current page before redirecting
// Redirect to the OIDC auth URL
const base64encodeUrl = btoa(window.location.href);
const oidcAuthUrl = '/auth/oidc/welcome?redirect_uri=' + encodeURIComponent(base64encodeUrl);
window.location.href = oidcAuthUrl;
}
function click_alternative_provider_instead() {
setTimeout(() => {
// Find ha-auth-flow
const authFlowElement = document.querySelector('ha-auth-flow');
function update() {
// Find ha-auth-flow
authFlowElement = document.querySelector('ha-auth-flow');
if (!authFlowElement) {
console.warn("[OIDC] ha-auth-flow element not found. Not automatically selecting HA provider.");
return;
}
// Check if the text "Login aborted" is present on the page
if (!authFlowElement.innerText.includes('Login aborted')) {
console.warn("[OIDC] 'Login aborted' text not found. Not automatically selecting HA provider.");
return;
}
@@ -60,7 +21,6 @@ function click_alternative_provider_instead() {
const authProviderElement = document.querySelector('ha-pick-auth-provider');
if (!authProviderElement) {
console.warn("[OIDC] ha-pick-auth-provider not found. Not automatically selecting HA provider.");
return;
}
@@ -72,11 +32,30 @@ function click_alternative_provider_instead() {
}
firstListItem.click();
}, 500);
}
// Run OIDC injection upon load
(() => {
attempt_oidc_redirect();
click_alternative_provider_instead();
})();
// Hide the content until ready
let ready = false
document.querySelector(".content").style.display = "none"
const observer = new MutationObserver((mutationsList, observer) => {
update();
if (!ready) {
ready = Boolean(authFlowElement)
if (ready) {
document.querySelector(".content").style.display = ""
}
}
})
observer.observe(document.body, { childList: true, subtree: true })
setTimeout(() => {
if (!ready) {
console.warn("[hass-oidc-auth]: Document was not ready after 300ms seconds, showing content anyway.")
}
// Force display the content
document.querySelector(".content").style.display = "";
}, 300)