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:
committed by
GitHub
parent
c7672f65d9
commit
baf3ac6b5a
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
"requirements": [
|
||||
"aiofiles",
|
||||
"jinja2",
|
||||
"bcrypt",
|
||||
"joserfc"
|
||||
],
|
||||
"version": "1.0.0-rc1"
|
||||
"version": "1.0.0-rc2"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user