Add basic provider to test frontend parts
This commit is contained in:
@@ -4,28 +4,35 @@ from typing import OrderedDict
|
||||
import voluptuous as vol
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .example import ExampleAuthProvider
|
||||
|
||||
DOMAIN = "auth_oidc"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
from .provider import OpenIDAuthProvider
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config):
|
||||
"""TODO"""
|
||||
# Inject Auth-Header provider.
|
||||
"""Add the OIDC Auth Provider to the providers in Home Assistant"""
|
||||
providers = OrderedDict()
|
||||
provider = ExampleAuthProvider(
|
||||
|
||||
provider = OpenIDAuthProvider(
|
||||
hass,
|
||||
hass.auth._store,
|
||||
config[DOMAIN],
|
||||
)
|
||||
|
||||
providers[(provider.type, provider.id)] = provider
|
||||
providers.update(hass.auth._providers)
|
||||
hass.auth._providers = providers
|
||||
_LOGGER.debug("Injected example provider")
|
||||
return True
|
||||
|
||||
_LOGGER.debug("Added OIDC provider")
|
||||
return True
|
||||
|
||||
41
custom_components/auth_oidc/callback.py
Normal file
41
custom_components/auth_oidc/callback.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from aiohttp import web
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import logging
|
||||
|
||||
DATA_VIEW_REGISTERED = "oauth2_view_reg"
|
||||
AUTH_CALLBACK_PATH = "/auth/oidc/callback"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@callback
|
||||
def async_register_view(hass: HomeAssistant) -> None:
|
||||
"""Make sure callback view is registered."""
|
||||
if not hass.data.get(DATA_VIEW_REGISTERED, False):
|
||||
hass.http.register_view(OAuth2AuthorizeCallbackView()) # type: ignore
|
||||
hass.data[DATA_VIEW_REGISTERED] = True
|
||||
|
||||
|
||||
class OAuth2AuthorizeCallbackView(HomeAssistantView):
|
||||
"""OAuth2 Authorization Callback View."""
|
||||
|
||||
requires_auth = False
|
||||
url = AUTH_CALLBACK_PATH
|
||||
name = "auth:oidc:callback"
|
||||
|
||||
async def get(self, request: web.Request) -> web.Response:
|
||||
"""Receive response."""
|
||||
|
||||
_LOGGER.debug(request.query)
|
||||
|
||||
hass = request.app["hass"]
|
||||
flow_mgr = hass.auth.login_flow
|
||||
|
||||
await flow_mgr.async_configure(
|
||||
flow_id=request.query["flow_id"], user_input=request.query["test"]
|
||||
)
|
||||
|
||||
return web.Response(
|
||||
headers={"content-type": "text/html"},
|
||||
text="<script>if (window.opener) { window.opener.postMessage({type: 'externalCallback'}); } window.close();</script>",
|
||||
)
|
||||
@@ -1,112 +0,0 @@
|
||||
"""OIDC Provider"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import hmac
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from homeassistant.auth.providers import (
|
||||
AUTH_PROVIDERS,
|
||||
AuthProvider,
|
||||
LoginFlow,
|
||||
)
|
||||
|
||||
from homeassistant.auth.models import Credentials, UserMeta
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
@AUTH_PROVIDERS.register("insecure_example_2")
|
||||
class ExampleAuthProvider(AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
|
||||
DEFAULT_TITLE = "OpenID Connect (SSO)"
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
return "auth_oidc"
|
||||
|
||||
@property
|
||||
def support_mfa(self) -> bool:
|
||||
"""OIDC Authentication Provider does not support MFA in Home Assistant, only external."""
|
||||
return False
|
||||
|
||||
async def async_login_flow(self) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
return ExampleLoginFlow(self)
|
||||
|
||||
@callback
|
||||
def async_validate_login(self, input: str) -> None:
|
||||
"""Validate a username and password."""
|
||||
|
||||
if input is "example":
|
||||
return
|
||||
else:
|
||||
raise InvalidAuthError
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Mapping[str, str]
|
||||
) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result["input"]
|
||||
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data["input"] == username:
|
||||
return credential
|
||||
|
||||
# Create new credentials.
|
||||
return self.async_create_credentials({"username": username})
|
||||
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials
|
||||
) -> UserMeta:
|
||||
"""Return extra user metadata for credentials.
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
username = credentials.data["username"]
|
||||
name = None
|
||||
|
||||
for user in self.config["users"]:
|
||||
if user["username"] == username:
|
||||
name = user.get("name")
|
||||
break
|
||||
|
||||
return UserMeta(name=name, is_active=True)
|
||||
|
||||
|
||||
class ExampleLoginFlow(LoginFlow):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the step of the form."""
|
||||
errors = None
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
cast(ExampleAuthProvider, self._auth_provider).async_validate_login(
|
||||
user_input["input"]
|
||||
)
|
||||
except InvalidAuthError:
|
||||
errors = {"base": "invalid_auth"}
|
||||
|
||||
if not errors:
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required("input"): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
73
custom_components/auth_oidc/provider.py
Normal file
73
custom_components/auth_oidc/provider.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""OIDC Authentication provider.
|
||||
Allow access to users based on login with an external OpenID Connect Identity Provider (IdP).
|
||||
"""
|
||||
import logging
|
||||
from secrets import token_hex
|
||||
from typing import Any, Dict, Optional, cast
|
||||
from homeassistant.auth.providers import (
|
||||
AUTH_PROVIDERS,
|
||||
AuthProvider,
|
||||
LoginFlow,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers.network import get_url
|
||||
|
||||
from .callback import async_register_view, AUTH_CALLBACK_PATH
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
@AUTH_PROVIDERS.register("oidc")
|
||||
class OpenIDAuthProvider(AuthProvider):
|
||||
"""Allow access to users based on login with an external OpenID Connect Identity Provider (IdP)."""
|
||||
|
||||
DEFAULT_TITLE = "OpenID Connect"
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
return "auth_oidc"
|
||||
|
||||
@property
|
||||
def support_mfa(self) -> bool:
|
||||
return False
|
||||
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
|
||||
async_register_view(self.hass)
|
||||
return OpenIdLoginFlow(self)
|
||||
|
||||
|
||||
class OpenIdLoginFlow(LoginFlow):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
return await self.async_step_authenticate()
|
||||
|
||||
def redirect_uri(self) -> str:
|
||||
"""Return the redirect uri."""
|
||||
return f"{get_url(self.hass, require_current_request=True)}{AUTH_CALLBACK_PATH}?test=value&flow_id={self.flow_id}"
|
||||
|
||||
async def async_step_authenticate(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Authenticate user using external step."""
|
||||
|
||||
if user_input:
|
||||
self.external_data = str(user_input)
|
||||
return self.async_external_step_done(next_step_id="authorize")
|
||||
|
||||
return self.async_external_step(step_id="authenticate", url=self.redirect_uri())
|
||||
|
||||
async def async_step_authorize(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Authorize user received from external step."""
|
||||
_LOGGER.log(user_input)
|
||||
return self.async_abort(reason="invalid_auth")
|
||||
Reference in New Issue
Block a user