Skip to content
This repository was archived by the owner on Oct 23, 2025. It is now read-only.
Open
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ checked against usernames AND full names. Users have to be created in Home Assis
auth_header:
# Optionally set this if you're not using authentik proxy or oauth2_proxy
# username_header: X-Forwarded-Preferred-Username
# Optionally set this if you want to enable Single Logout
# single_logout_url: https://your.homeassistant.instance/oauth2/sign_out?rd=https%3A%2F%2Fyour.idp.com%2Foauth2%2Flogout
# Optionally set this if you don't want to bypass the login prompt
# allow_bypass_login: false
# Optionally enable debug mode to see the headers Home-Assistant gets
Expand Down Expand Up @@ -54,6 +56,22 @@ On boot, two main things are done when the integration is enabled:

This ensures that Header auth is tried first, and if it fails the user can still use username/password.

3. If the single logout URL is available, we patch `_handleLogout` as soon as the home-assistant element is available

This redirects the user to the configured SLO url after the stock logic to revoke the access token is executed

## Single Logout Support

The component can patch the stock log out handler to clear the authentication proxy's session and log you out of your IDP if you specify a `single_logout_url`. This is usually a call to your authentication proxy (such as oauth2-proxy) with a rd parameter to redirect the user to your IDP's logout endpoint after. The IDP's logout endpoint needs to be URL-encoded in the configuration.

```
https://your.homeassistant.instance/oauth2/sign_out?rd=https%3A%2F%2Fyour.idp.com%2Foauth2%2Flogout

https://your.homeassistant.instance/oauth2/sign_out?rd= > your oauth proxy's SLO endpoint
https%3A%2F%2Fyour.idp.com%2Foauth2%2Flogout > your IDP's SLO endpoint URL-encoded
```

You can also configure a front-channel logout URL in your IDP to call the same endpoint to clear the proxy's session however, this does not revoke your currently logged in access token.

## Help! Everything is broken!

Expand Down
34 changes: 28 additions & 6 deletions custom_components/auth_header/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from homeassistant.components.auth import DOMAIN as AUTH_DOMAIN
from homeassistant.components.auth import indieauth
from homeassistant.components.auth.login_flow import LoginFlowIndexView
from homeassistant.components.http import StaticPathConfig
from homeassistant.components.http import StaticPathConfig, HomeAssistantView
from homeassistant.components.http.ban import log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.core import HomeAssistant
Expand All @@ -24,6 +24,7 @@
from aiohttp.web_urldispatcher import UrlDispatcher, AbstractResource

DOMAIN = "auth_header"
CONF_SINGLE_LOGOUT_URL = "single_logout_url"
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
Expand All @@ -32,6 +33,7 @@
vol.Optional("username_header", default="X-Forwarded-Preferred-Username"): cv.string,
vol.Optional("allow_bypass_login", default=True): cv.boolean,
vol.Optional("debug", default=False): cv.boolean,
vol.Optional(CONF_SINGLE_LOGOUT_URL): cv.url,
}
)
},
Expand All @@ -41,6 +43,9 @@

async def async_setup(hass: HomeAssistant, config):
"""Register custom view which includes request in context"""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
hass.data[DOMAIN][CONF_SINGLE_LOGOUT_URL] = config[DOMAIN].get(CONF_SINGLE_LOGOUT_URL)
# Because we start after auth, we have access to store_result
store_result = hass.data[AUTH_DOMAIN]
router: "FastUrlDispatcher" | "UrlDispatcher" = hass.http.app.router
Expand Down Expand Up @@ -68,15 +73,22 @@ async def async_setup(hass: HomeAssistant, config):

# Load script to store tokens in local storage, else we'll re-auth on every browser refresh.
await hass.http.async_register_static_paths(
[StaticPathConfig(
"/auth_header/store-token.js",
os.path.join(os.path.dirname(__file__), 'store-token.js'),
True
[
StaticPathConfig(
"/auth_header/store-token.js",
os.path.join(os.path.dirname(__file__), "store-token.js"),
True,
),
StaticPathConfig(
"/auth_header/logout-slo.js",
os.path.join(os.path.dirname(__file__), "logout-slo.js"),
True,
)
]
)
)

add_extra_js_url(hass, '/auth_header/store-token.js')
add_extra_js_url(hass, '/auth_header/logout-slo.js')

# Inject Auth-Header provider.
providers = OrderedDict()
Expand All @@ -89,6 +101,7 @@ async def async_setup(hass: HomeAssistant, config):
providers.update(hass.auth._providers)
hass.auth._providers = providers
_LOGGER.debug("Injected auth_header provider")
hass.http.register_view(SLOUrlView())
return True


Expand Down Expand Up @@ -156,3 +169,12 @@ async def post(self, request: Request, data: dict[str, Any]) -> Response:
)

return await self._async_flow_result_to_response(request, client_id, result)

class SLOUrlView(HomeAssistantView):
url = "/api/auth_header/slo_url"
name = "auth_header:slo_url"
requires_auth = False

async def get(self, request: Request) -> Response:
slo = request.app["hass"].data[DOMAIN].get(CONF_SINGLE_LOGOUT_URL, "")
return Response(text=slo, content_type="text/plain")
25 changes: 25 additions & 0 deletions custom_components/auth_header/logout-slo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
(() => {
// Replace the frontend's _handleLogout() handler to redirect to the IdP single‑logout URL if configured.
const SLO_ENDPOINT = "/api/auth_header/slo_url";
const fetchSlo = async () => {
try {
const resp = await fetch(SLO_ENDPOINT, { cache: "no-cache" });
return resp.ok ? resp.text() : "";
} catch {
return "";
}
};
const patchLogout = async () => {
const sloUrl = await fetchSlo();
if (!sloUrl) return; // nothing to do
await customElements.whenDefined("home-assistant");
const proto = customElements.get("home-assistant").prototype;
if (!proto._handleLogout) return;
const orig = proto._handleLogout;
proto._handleLogout = async function () {
await orig.call(this);
window.location.href = sloUrl;
};
};
patchLogout();
})();
2 changes: 1 addition & 1 deletion custom_components/auth_header/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"auth"
],
"codeowners": ["@BeryJu"],
"version": "1.4"
"version": "1.5"
}