diff --git a/custom_components/auth_oidc/__init__.py b/custom_components/auth_oidc/__init__.py index 355cc54..c84a94c 100644 --- a/custom_components/auth_oidc/__init__.py +++ b/custom_components/auth_oidc/__init__.py @@ -24,6 +24,8 @@ from .config import ( ROLES, NETWORK, FEATURES_INCLUDE_GROUPS_SCOPE, + FEATURES_DISABLE_FRONTEND_INJECTION, + FEATURES_FORCE_HTTPS, ) # pylint: enable=useless-import-alias @@ -93,14 +95,23 @@ async def async_setup(hass: HomeAssistant, config): # Register the views 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 = re.sub(r"[^A-Za-z0-9 _\-\(\)]", "", name) - hass.http.register_view(OIDCWelcomeView(name, is_frontend_injection_enabled)) - hass.http.register_view(OIDCRedirectView(oidc_client)) - hass.http.register_view(OIDCCallbackView(oidc_client, provider)) + force_https = features_config.get(FEATURES_FORCE_HTTPS, False) + + 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()) _LOGGER.info("Registered OIDC views") diff --git a/custom_components/auth_oidc/config.py b/custom_components/auth_oidc/config.py index eb5303b..9425eb1 100644 --- a/custom_components/auth_oidc/config.py +++ b/custom_components/auth_oidc/config.py @@ -14,7 +14,8 @@ FEATURES_AUTOMATIC_USER_LINKING = "automatic_user_linking" FEATURES_AUTOMATIC_PERSON_CREATION = "automatic_person_creation" FEATURES_DISABLE_PKCE = "disable_rfc7636" 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_DISPLAY_NAME = "display_name" CLAIMS_USERNAME = "username" @@ -72,8 +73,12 @@ CONFIG_SCHEMA = vol.Schema( ): vol.Coerce(bool), # Disable frontend injection of OIDC login button vol.Optional( - FEATURE_DISABLE_FRONTEND_INJECTION, default=False + FEATURES_DISABLE_FRONTEND_INJECTION, default=False ): 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 diff --git a/custom_components/auth_oidc/endpoints/callback.py b/custom_components/auth_oidc/endpoints/callback.py index e12d196..3ae5ff1 100644 --- a/custom_components/auth_oidc/endpoints/callback.py +++ b/custom_components/auth_oidc/endpoints/callback.py @@ -17,10 +17,14 @@ class OIDCCallbackView(HomeAssistantView): name = "auth:oidc:callback" def __init__( - self, oidc_client: OIDCClient, oidc_provider: OpenIDAuthProvider + self, + oidc_client: OIDCClient, + oidc_provider: OpenIDAuthProvider, + force_https: bool, ) -> None: self.oidc_client = oidc_client self.oidc_provider = oidc_provider + self.force_https = force_https async def get(self, request: web.Request) -> web.Response: """Receive response.""" @@ -38,7 +42,7 @@ class OIDCCallbackView(HomeAssistantView): ) 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( redirect_uri, code, state ) @@ -63,4 +67,6 @@ class OIDCCallbackView(HomeAssistantView): return web.Response(text=view_html, content_type="text/html") 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) + ) diff --git a/custom_components/auth_oidc/endpoints/redirect.py b/custom_components/auth_oidc/endpoints/redirect.py index f95fa77..7e59da6 100644 --- a/custom_components/auth_oidc/endpoints/redirect.py +++ b/custom_components/auth_oidc/endpoints/redirect.py @@ -17,13 +17,14 @@ class OIDCRedirectView(HomeAssistantView): url = PATH 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.force_https = force_https async def get(self, _: web.Request) -> web.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) if auth_url: diff --git a/custom_components/auth_oidc/endpoints/welcome.py b/custom_components/auth_oidc/endpoints/welcome.py index 66e1d5c..cf3da77 100644 --- a/custom_components/auth_oidc/endpoints/welcome.py +++ b/custom_components/auth_oidc/endpoints/welcome.py @@ -14,15 +14,16 @@ class OIDCWelcomeView(HomeAssistantView): url = PATH 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.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: """Receive response.""" 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}) return web.Response(text=view_html, content_type="text/html") diff --git a/custom_components/auth_oidc/helpers.py b/custom_components/auth_oidc/helpers.py index 84d6936..cd7746c 100644 --- a/custom_components/auth_oidc/helpers.py +++ b/custom_components/auth_oidc/helpers.py @@ -4,12 +4,14 @@ from homeassistant.components import http 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.""" if (req := http.current_request.get()) is None: raise RuntimeError("No current request in context") base_uri = str(req.url).split("/auth", 2)[0] + if force_https: + base_uri = base_uri.replace("http://", "https://") return f"{base_uri}{path}" diff --git a/docs/configuration.md b/docs/configuration.md index 42741d7..e4d968f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -93,6 +93,15 @@ Upon login, OIDC users will then automatically be linked to the HA user with the > [!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! +### 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 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.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.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.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). |