Allow forcing HTTPS in URL generation (#92)

* Force HTTPS feature
* Add docs
This commit is contained in:
Christiaan Goossens
2025-07-16 12:21:11 +02:00
committed by GitHub
parent 054f0e4bca
commit e22f960d69
7 changed files with 51 additions and 15 deletions

View File

@@ -24,6 +24,8 @@ from .config import (
ROLES, ROLES,
NETWORK, NETWORK,
FEATURES_INCLUDE_GROUPS_SCOPE, FEATURES_INCLUDE_GROUPS_SCOPE,
FEATURES_DISABLE_FRONTEND_INJECTION,
FEATURES_FORCE_HTTPS,
) )
# pylint: enable=useless-import-alias # pylint: enable=useless-import-alias
@@ -93,14 +95,23 @@ async def async_setup(hass: HomeAssistant, config):
# Register the views # Register the views
is_frontend_injection_enabled = ( is_frontend_injection_enabled = (
features_config.get("disable_frontend_changes", False) is False features_config.get(FEATURES_DISABLE_FRONTEND_INJECTION, False) is False
) )
name = config[DOMAIN].get(DISPLAY_NAME, DEFAULT_TITLE) name = config[DOMAIN].get(DISPLAY_NAME, DEFAULT_TITLE)
name = re.sub(r"[^A-Za-z0-9 _\-\(\)]", "", name) name = re.sub(r"[^A-Za-z0-9 _\-\(\)]", "", name)
hass.http.register_view(OIDCWelcomeView(name, is_frontend_injection_enabled)) force_https = features_config.get(FEATURES_FORCE_HTTPS, False)
hass.http.register_view(OIDCRedirectView(oidc_client))
hass.http.register_view(OIDCCallbackView(oidc_client, provider)) hass.http.register_view(
OIDCWelcomeView(
name,
# Welcome view is not enabled if frontend injection is enabled
not is_frontend_injection_enabled,
force_https,
)
)
hass.http.register_view(OIDCRedirectView(oidc_client, force_https))
hass.http.register_view(OIDCCallbackView(oidc_client, provider, force_https))
hass.http.register_view(OIDCFinishView()) hass.http.register_view(OIDCFinishView())
_LOGGER.info("Registered OIDC views") _LOGGER.info("Registered OIDC views")

View File

@@ -14,7 +14,8 @@ FEATURES_AUTOMATIC_USER_LINKING = "automatic_user_linking"
FEATURES_AUTOMATIC_PERSON_CREATION = "automatic_person_creation" FEATURES_AUTOMATIC_PERSON_CREATION = "automatic_person_creation"
FEATURES_DISABLE_PKCE = "disable_rfc7636" FEATURES_DISABLE_PKCE = "disable_rfc7636"
FEATURES_INCLUDE_GROUPS_SCOPE = "include_groups_scope" FEATURES_INCLUDE_GROUPS_SCOPE = "include_groups_scope"
FEATURE_DISABLE_FRONTEND_INJECTION = "disable_frontend_changes" FEATURES_DISABLE_FRONTEND_INJECTION = "disable_frontend_changes"
FEATURES_FORCE_HTTPS = "force_https"
CLAIMS = "claims" CLAIMS = "claims"
CLAIMS_DISPLAY_NAME = "display_name" CLAIMS_DISPLAY_NAME = "display_name"
CLAIMS_USERNAME = "username" CLAIMS_USERNAME = "username"
@@ -72,8 +73,12 @@ CONFIG_SCHEMA = vol.Schema(
): vol.Coerce(bool), ): vol.Coerce(bool),
# Disable frontend injection of OIDC login button # Disable frontend injection of OIDC login button
vol.Optional( vol.Optional(
FEATURE_DISABLE_FRONTEND_INJECTION, default=False FEATURES_DISABLE_FRONTEND_INJECTION, default=False
): vol.Coerce(bool), ): vol.Coerce(bool),
# Force HTTPS on all generated URLs (like redirect_uri)
vol.Optional(FEATURES_FORCE_HTTPS, default=False): vol.Coerce(
bool
),
} }
), ),
# Determine which specific claims will be used from the id_token # Determine which specific claims will be used from the id_token

View File

@@ -17,10 +17,14 @@ class OIDCCallbackView(HomeAssistantView):
name = "auth:oidc:callback" name = "auth:oidc:callback"
def __init__( def __init__(
self, oidc_client: OIDCClient, oidc_provider: OpenIDAuthProvider self,
oidc_client: OIDCClient,
oidc_provider: OpenIDAuthProvider,
force_https: bool,
) -> None: ) -> None:
self.oidc_client = oidc_client self.oidc_client = oidc_client
self.oidc_provider = oidc_provider self.oidc_provider = oidc_provider
self.force_https = force_https
async def get(self, request: web.Request) -> web.Response: async def get(self, request: web.Request) -> web.Response:
"""Receive response.""" """Receive response."""
@@ -38,7 +42,7 @@ class OIDCCallbackView(HomeAssistantView):
) )
return web.Response(text=view_html, content_type="text/html") return web.Response(text=view_html, content_type="text/html")
redirect_uri = get_url("/auth/oidc/callback") redirect_uri = get_url("/auth/oidc/callback", self.force_https)
user_details = await self.oidc_client.async_complete_token_flow( user_details = await self.oidc_client.async_complete_token_flow(
redirect_uri, code, state redirect_uri, code, state
) )
@@ -63,4 +67,6 @@ class OIDCCallbackView(HomeAssistantView):
return web.Response(text=view_html, content_type="text/html") 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, self.force_https)
)

View File

@@ -17,13 +17,14 @@ class OIDCRedirectView(HomeAssistantView):
url = PATH url = PATH
name = "auth:oidc:redirect" name = "auth:oidc:redirect"
def __init__(self, oidc_client: OIDCClient) -> None: def __init__(self, oidc_client: OIDCClient, force_https: bool) -> None:
self.oidc_client = oidc_client self.oidc_client = oidc_client
self.force_https = force_https
async def get(self, _: web.Request) -> web.Response: async def get(self, _: web.Request) -> web.Response:
"""Receive response.""" """Receive response."""
redirect_uri = get_url("/auth/oidc/callback") redirect_uri = get_url("/auth/oidc/callback", self.force_https)
auth_url = await self.oidc_client.async_get_authorization_url(redirect_uri) auth_url = await self.oidc_client.async_get_authorization_url(redirect_uri)
if auth_url: if auth_url:

View File

@@ -14,15 +14,16 @@ class OIDCWelcomeView(HomeAssistantView):
url = PATH url = PATH
name = "auth:oidc:welcome" name = "auth:oidc:welcome"
def __init__(self, name: str, is_frontend_injection_enabled: bool) -> None: def __init__(self, name: str, is_enabled: bool, force_https: bool) -> None:
self.name = name self.name = name
self.is_enabled = not is_frontend_injection_enabled self.is_enabled = is_enabled
self.force_https = force_https
async def get(self, _: web.Request) -> web.Response: async def get(self, _: web.Request) -> web.Response:
"""Receive response.""" """Receive response."""
if not self.is_enabled: if not self.is_enabled:
return web.HTTPTemporaryRedirect(get_url("/")) return web.HTTPTemporaryRedirect(get_url("/", self.force_https))
view_html = await get_view("welcome", {"name": self.name}) view_html = await get_view("welcome", {"name": self.name})
return web.Response(text=view_html, content_type="text/html") return web.Response(text=view_html, content_type="text/html")

View File

@@ -4,12 +4,14 @@ from homeassistant.components import http
from .views.loader import AsyncTemplateRenderer from .views.loader import AsyncTemplateRenderer
def get_url(path: str) -> str: def get_url(path: str, force_https: bool) -> str:
"""Returns the requested path appended to the current request base URL.""" """Returns the requested path appended to the current request base URL."""
if (req := http.current_request.get()) is None: if (req := http.current_request.get()) is None:
raise RuntimeError("No current request in context") raise RuntimeError("No current request in context")
base_uri = str(req.url).split("/auth", 2)[0] base_uri = str(req.url).split("/auth", 2)[0]
if force_https:
base_uri = base_uri.replace("http://", "https://")
return f"{base_uri}{path}" return f"{base_uri}{path}"

View File

@@ -93,6 +93,15 @@ Upon login, OIDC users will then automatically be linked to the HA user with the
> [!CAUTION] > [!CAUTION]
> MFA is ignored when using this setting, thus bypassing any MFA configuration the user has originally configured, as long as the username is an exact match. This is dangerous if you are not aware of it! > MFA is ignored when using this setting, thus bypassing any MFA configuration the user has originally configured, as long as the username is an exact match. This is dangerous if you are not aware of it!
### Forcing HTTPS
First check if you are setting the header `X-Forwarded-Proto` in your proxy and if the [proxy settings for Home Assistant](https://www.home-assistant.io/integrations/http/#use_x_forwarded_for) are configured correctly. You should also check if IP addresses in your logs actually match the origin IP (instead of proxy IP). If you cannot find any mistakes, you may use the following config option to force HTTPS regardless:
```yaml
auth_oidc:
features:
force_https: true
```
### Using a private certificate authority ### Using a private certificate authority
If you use a private certificate authority to secure your OIDC provider, you must configure the root certificates of your private certificate authority. Otherwise you will get an error (`[SSL: CERTIFICATE_VERIFY_FAILED]`) when connecting to the OIDC provider. If you use a private certificate authority to secure your OIDC provider, you must configure the root certificates of your private certificate authority. Otherwise you will get an error (`[SSL: CERTIFICATE_VERIFY_FAILED]`) when connecting to the OIDC provider.
@@ -133,6 +142,7 @@ Here's a table of all options that you can set:
| `features.disable_rfc7636` | `boolean`| No | `false` | Disables PKCE (RFC 7636) for OIDC providers that don't support it. You should not need this with most providers. | | `features.disable_rfc7636` | `boolean`| No | `false` | Disables PKCE (RFC 7636) for OIDC providers that don't support it. You should not need this with most providers. |
| `features.include_groups_scope` | `boolean` | No | `true` | Include the 'groups' scope in the OIDC request. Set to `false` to exclude it. | | `features.include_groups_scope` | `boolean` | No | `true` | Include the 'groups' scope in the OIDC request. Set to `false` to exclude it. |
| `features.disable_frontend_changes` | `boolean` | No | `false` | Set to `true` to disable all changes made to the HA frontend for better compatbility with future HA versions, or if you are not comfortable with injecting Javascript into the existing frontend code. | | `features.disable_frontend_changes` | `boolean` | No | `false` | Set to `true` to disable all changes made to the HA frontend for better compatbility with future HA versions, or if you are not comfortable with injecting Javascript into the existing frontend code. |
| `features.force_https` | `boolean` | No | `false` | Set to `true` to force all URLs generated to use `https` instead of automatically determining based on the request scheme or `X-Forwarded-Proto`. |
| `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). |