Allow for skipping the welcome screen (even if HA username/password is still registered) (#272)
* Allow for skipping the welcome screen (even if HA username/password is still registered) * Linting & formatting * Typing & tests
This commit is contained in:
committed by
GitHub
parent
f90a7d5346
commit
3ba65adc8b
@@ -28,6 +28,7 @@ from .config import (
|
|||||||
ROLES,
|
ROLES,
|
||||||
NETWORK,
|
NETWORK,
|
||||||
FEATURES_INCLUDE_GROUPS_SCOPE,
|
FEATURES_INCLUDE_GROUPS_SCOPE,
|
||||||
|
FEATURES_DEFAULT_REDIRECT,
|
||||||
FEATURES_FORCE_HTTPS,
|
FEATURES_FORCE_HTTPS,
|
||||||
REQUIRED_SCOPES,
|
REQUIRED_SCOPES,
|
||||||
)
|
)
|
||||||
@@ -43,6 +44,7 @@ from .endpoints import (
|
|||||||
OIDCDeviceSSE,
|
OIDCDeviceSSE,
|
||||||
)
|
)
|
||||||
from .tools.oidc_client import OIDCClient
|
from .tools.oidc_client import OIDCClient
|
||||||
|
from .tools.types import OIDCWelcomeOptions
|
||||||
from .provider import OpenIDAuthProvider
|
from .provider import OpenIDAuthProvider
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -146,6 +148,7 @@ async def _setup_oidc_provider(hass: HomeAssistant, my_config: dict, display_nam
|
|||||||
name = re.sub(r"[^A-Za-z0-9 _\-\(\)]", "", name)
|
name = re.sub(r"[^A-Za-z0-9 _\-\(\)]", "", name)
|
||||||
|
|
||||||
force_https = features_config.get(FEATURES_FORCE_HTTPS, False)
|
force_https = features_config.get(FEATURES_FORCE_HTTPS, False)
|
||||||
|
default_redirect = features_config.get(FEATURES_DEFAULT_REDIRECT, False)
|
||||||
|
|
||||||
await hass.http.async_register_static_paths(
|
await hass.http.async_register_static_paths(
|
||||||
[
|
[
|
||||||
@@ -158,7 +161,15 @@ async def _setup_oidc_provider(hass: HomeAssistant, my_config: dict, display_nam
|
|||||||
)
|
)
|
||||||
|
|
||||||
hass.http.register_view(
|
hass.http.register_view(
|
||||||
OIDCWelcomeView(provider, name, force_https, has_other_auth_providers)
|
OIDCWelcomeView(
|
||||||
|
provider,
|
||||||
|
OIDCWelcomeOptions(
|
||||||
|
name=name,
|
||||||
|
force_https=force_https,
|
||||||
|
has_other_auth_providers=has_other_auth_providers,
|
||||||
|
prefers_skipping=default_redirect,
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
hass.http.register_view(OIDCDeviceSSE(provider))
|
hass.http.register_view(OIDCDeviceSSE(provider))
|
||||||
hass.http.register_view(OIDCRedirectView(oidc_client, provider, force_https))
|
hass.http.register_view(OIDCRedirectView(oidc_client, provider, force_https))
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ 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"
|
||||||
FEATURES_FORCE_HTTPS = "force_https"
|
FEATURES_FORCE_HTTPS = "force_https"
|
||||||
|
FEATURES_DEFAULT_REDIRECT = "default_redirect"
|
||||||
CLAIMS = "claims"
|
CLAIMS = "claims"
|
||||||
CLAIMS_DISPLAY_NAME = "display_name"
|
CLAIMS_DISPLAY_NAME = "display_name"
|
||||||
CLAIMS_USERNAME = "username"
|
CLAIMS_USERNAME = "username"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from .const import (
|
|||||||
FEATURES_DISABLE_PKCE,
|
FEATURES_DISABLE_PKCE,
|
||||||
FEATURES_INCLUDE_GROUPS_SCOPE,
|
FEATURES_INCLUDE_GROUPS_SCOPE,
|
||||||
FEATURES_FORCE_HTTPS,
|
FEATURES_FORCE_HTTPS,
|
||||||
|
FEATURES_DEFAULT_REDIRECT,
|
||||||
CLAIMS,
|
CLAIMS,
|
||||||
CLAIMS_DISPLAY_NAME,
|
CLAIMS_DISPLAY_NAME,
|
||||||
CLAIMS_USERNAME,
|
CLAIMS_USERNAME,
|
||||||
@@ -75,6 +76,13 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(FEATURES_FORCE_HTTPS, default=False): vol.Coerce(
|
vol.Optional(FEATURES_FORCE_HTTPS, default=False): vol.Coerce(
|
||||||
bool
|
bool
|
||||||
),
|
),
|
||||||
|
# Welcome page will be skipped automatically if there are no
|
||||||
|
# other auth providers.
|
||||||
|
# This flag enables this behavior regardless of the amount
|
||||||
|
# of other auth providers.
|
||||||
|
vol.Optional(
|
||||||
|
FEATURES_DEFAULT_REDIRECT, 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
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from aiohttp import web
|
|||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from ..tools.helpers import error_response, get_url, template_response
|
from ..tools.helpers import error_response, get_url, template_response
|
||||||
from ..provider import OpenIDAuthProvider
|
from ..provider import OpenIDAuthProvider
|
||||||
|
from ..tools.types import OIDCWelcomeOptions
|
||||||
|
|
||||||
PATH = "/auth/oidc/welcome"
|
PATH = "/auth/oidc/welcome"
|
||||||
|
|
||||||
@@ -20,16 +21,13 @@ class OIDCWelcomeView(HomeAssistantView):
|
|||||||
name = "auth:oidc:welcome"
|
name = "auth:oidc:welcome"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, oidc_provider: OpenIDAuthProvider, options: OIDCWelcomeOptions
|
||||||
oidc_provider: OpenIDAuthProvider,
|
|
||||||
name: str,
|
|
||||||
force_https: bool,
|
|
||||||
has_other_auth_providers: bool,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self.oidc_provider = oidc_provider
|
self.oidc_provider = oidc_provider
|
||||||
self.name = name
|
self.name = options.get("name")
|
||||||
self.force_https = force_https
|
self.force_https = options.get("force_https")
|
||||||
self.has_other_auth_providers = has_other_auth_providers
|
self.has_other_auth_providers = options.get("has_other_auth_providers")
|
||||||
|
self.prefers_skipping = options.get("prefers_skipping")
|
||||||
|
|
||||||
async def _process_url(self, redirect_uri: str) -> List[str, bool]:
|
async def _process_url(self, redirect_uri: str) -> List[str, bool]:
|
||||||
"""Processes the redirect URI to determine if we need setTokens and if this is mobile."""
|
"""Processes the redirect URI to determine if we need setTokens and if this is mobile."""
|
||||||
@@ -108,7 +106,9 @@ class OIDCWelcomeView(HomeAssistantView):
|
|||||||
|
|
||||||
# If this is the only provider and we are on desktop,
|
# If this is the only provider and we are on desktop,
|
||||||
# automatically go through the OIDC login
|
# automatically go through the OIDC login
|
||||||
if not is_mobile and not self.has_other_auth_providers:
|
if not is_mobile and (
|
||||||
|
not self.has_other_auth_providers or self.prefers_skipping
|
||||||
|
):
|
||||||
raise web.HTTPFound(
|
raise web.HTTPFound(
|
||||||
location=get_url("/auth/oidc/redirect", self.force_https),
|
location=get_url("/auth/oidc/redirect", self.force_https),
|
||||||
headers=cookie_header,
|
headers=cookie_header,
|
||||||
|
|||||||
@@ -39,3 +39,19 @@ class OIDCState(dict):
|
|||||||
|
|
||||||
# IP address
|
# IP address
|
||||||
ip_address: str | None
|
ip_address: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class OIDCWelcomeOptions(dict):
|
||||||
|
"""Options for the welcome screen"""
|
||||||
|
|
||||||
|
# User friendly SSO name to display
|
||||||
|
name: str
|
||||||
|
|
||||||
|
# Does the user force HTTPS on all generated URLs?
|
||||||
|
force_https: bool
|
||||||
|
|
||||||
|
# Has the user registered any other auth providers?
|
||||||
|
has_other_auth_providers: bool
|
||||||
|
|
||||||
|
# Does the user prefer to skip the welcome screen?
|
||||||
|
prefers_skipping: bool
|
||||||
|
|||||||
@@ -78,6 +78,19 @@ auth_oidc:
|
|||||||
|
|
||||||
This will show the provider on the login screen as: "Login with Example".
|
This will show the provider on the login screen as: "Login with Example".
|
||||||
|
|
||||||
|
### Skipping the welcome screen
|
||||||
|
If you would like to skip the welcome screen, you can either enable the `features.default_redirect` feature, or [disable the Home Assistant auth provider](https://github.com/christiaangoossens/hass-oidc-auth/discussions/67).
|
||||||
|
|
||||||
|
If you want to keep the default login (backup login) enabled, but still skip the welcome screen by default, you can configure the following yaml:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth_oidc:
|
||||||
|
features:
|
||||||
|
default_redirect: true
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have this feature enabled and you would like to use the backup login, make sure to append `?skip_oidc_redirect=true` to your login URL. For example, if your HA is at `https://ha.example.com`, you can go to `https://ha.example.com/?skip_oidc_redirect=true` to see the HA username/password login screen.
|
||||||
|
|
||||||
### Forcing HTTPS
|
### 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:
|
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:
|
||||||
|
|
||||||
@@ -161,6 +174,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.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`. |
|
| `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`. |
|
||||||
|
| `features.default_redirect` | `boolean` | No | `false` | Set to `true` to always skip the welcome screen (on desktop), regardless of if there are any other auth providers registered. |
|
||||||
| `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). |
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import base64
|
|||||||
import os
|
import os
|
||||||
from urllib.parse import parse_qs, quote, unquote, urlparse, urlencode
|
from urllib.parse import parse_qs, quote, unquote, urlparse, urlencode
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
from auth_oidc.config.const import DISCOVERY_URL, CLIENT_ID
|
from auth_oidc.config.const import (
|
||||||
|
DISCOVERY_URL,
|
||||||
|
CLIENT_ID,
|
||||||
|
FEATURES,
|
||||||
|
FEATURES_DEFAULT_REDIRECT,
|
||||||
|
)
|
||||||
|
|
||||||
from pytest_homeassistant_custom_component.typing import ClientSessionGenerator
|
from pytest_homeassistant_custom_component.typing import ClientSessionGenerator
|
||||||
import pytest
|
import pytest
|
||||||
@@ -98,6 +103,28 @@ async def test_redirect_page_registration(
|
|||||||
assert resp2.status == 302
|
assert resp2.status == 302
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_welcome_page_default_redirect(
|
||||||
|
hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
|
):
|
||||||
|
"""Test that the welcome page returns a redirect when default_redirect is preferred."""
|
||||||
|
|
||||||
|
mock_config = {
|
||||||
|
DOMAIN: {
|
||||||
|
CLIENT_ID: "dummy",
|
||||||
|
DISCOVERY_URL: "https://example.com/.well-known/openid-configuration",
|
||||||
|
FEATURES: {FEATURES_DEFAULT_REDIRECT: True},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await async_setup_component(hass, DOMAIN, mock_config)
|
||||||
|
assert result
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get("/auth/oidc/welcome", allow_redirects=False)
|
||||||
|
assert resp.status == 302
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_welcome_rejects_invalid_encoded_redirect_uri(
|
async def test_welcome_rejects_invalid_encoded_redirect_uri(
|
||||||
hass: HomeAssistant, hass_client: ClientSessionGenerator
|
hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
|
|||||||
Reference in New Issue
Block a user