2018-07-13 09:43:08 +00:00
|
|
|
"""Provide an authentication layer for Home Assistant."""
|
2021-02-12 09:58:20 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2018-07-13 09:43:08 +00:00
|
|
|
import asyncio
|
|
|
|
from collections import OrderedDict
|
2022-01-12 06:56:35 +00:00
|
|
|
from collections.abc import Mapping
|
2018-09-11 10:05:15 +00:00
|
|
|
from datetime import timedelta
|
2023-01-23 08:56:10 +00:00
|
|
|
from typing import Any, cast
|
2018-07-13 09:43:08 +00:00
|
|
|
|
2018-08-14 19:14:12 +00:00
|
|
|
import jwt
|
|
|
|
|
2018-07-13 09:43:08 +00:00
|
|
|
from homeassistant import data_entry_flow
|
2021-10-08 16:38:22 +00:00
|
|
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
2021-04-29 11:40:51 +00:00
|
|
|
from homeassistant.data_entry_flow import FlowResult
|
2018-08-14 19:14:12 +00:00
|
|
|
from homeassistant.util import dt as dt_util
|
2018-07-13 09:43:08 +00:00
|
|
|
|
2023-03-23 02:03:41 +00:00
|
|
|
from . import auth_store, jwt_wrapper, models
|
2021-05-22 08:15:30 +00:00
|
|
|
from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN
|
2019-12-09 15:42:10 +00:00
|
|
|
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
|
|
|
|
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
|
2018-07-13 09:43:08 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
EVENT_USER_ADDED = "user_added"
|
2022-06-10 03:49:02 +00:00
|
|
|
EVENT_USER_UPDATED = "user_updated"
|
2019-07-31 19:25:30 +00:00
|
|
|
EVENT_USER_REMOVED = "user_removed"
|
2018-10-11 15:06:51 +00:00
|
|
|
|
2022-01-11 20:26:03 +00:00
|
|
|
_MfaModuleDict = dict[str, MultiFactorAuthModule]
|
2023-01-23 08:56:10 +00:00
|
|
|
_ProviderKey = tuple[str, str | None]
|
2022-01-11 20:26:03 +00:00
|
|
|
_ProviderDict = dict[_ProviderKey, AuthProvider]
|
2018-07-13 09:43:08 +00:00
|
|
|
|
|
|
|
|
2021-01-28 11:06:20 +00:00
|
|
|
class InvalidAuthError(Exception):
|
|
|
|
"""Raised when a authentication error occurs."""
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidProvider(Exception):
|
|
|
|
"""Authentication provider not found."""
|
|
|
|
|
|
|
|
|
2018-07-31 14:00:17 +00:00
|
|
|
async def auth_manager_from_config(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass: HomeAssistant,
|
2021-03-17 20:46:07 +00:00
|
|
|
provider_configs: list[dict[str, Any]],
|
|
|
|
module_configs: list[dict[str, Any]],
|
2021-02-12 09:58:20 +00:00
|
|
|
) -> AuthManager:
|
2018-08-28 18:54:01 +00:00
|
|
|
"""Initialize an auth manager from config.
|
|
|
|
|
|
|
|
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
|
|
|
|
mfa modules exist in configs.
|
|
|
|
"""
|
2018-07-13 09:43:08 +00:00
|
|
|
store = auth_store.AuthStore(hass)
|
|
|
|
if provider_configs:
|
|
|
|
providers = await asyncio.gather(
|
2019-07-31 19:25:30 +00:00
|
|
|
*(
|
|
|
|
auth_provider_from_config(hass, store, config)
|
|
|
|
for config in provider_configs
|
|
|
|
)
|
|
|
|
)
|
2018-07-13 09:43:08 +00:00
|
|
|
else:
|
2019-10-19 18:35:57 +00:00
|
|
|
providers = []
|
2018-07-13 09:43:08 +00:00
|
|
|
# So returned auth providers are in same order as config
|
2019-09-04 03:36:04 +00:00
|
|
|
provider_hash: _ProviderDict = OrderedDict()
|
2018-07-13 09:43:08 +00:00
|
|
|
for provider in providers:
|
|
|
|
key = (provider.type, provider.id)
|
|
|
|
provider_hash[key] = provider
|
2018-08-22 07:52:34 +00:00
|
|
|
|
|
|
|
if module_configs:
|
|
|
|
modules = await asyncio.gather(
|
2019-07-31 19:25:30 +00:00
|
|
|
*(auth_mfa_module_from_config(hass, config) for config in module_configs)
|
|
|
|
)
|
2018-08-22 07:52:34 +00:00
|
|
|
else:
|
2019-10-19 18:35:57 +00:00
|
|
|
modules = []
|
2018-08-22 07:52:34 +00:00
|
|
|
# So returned auth modules are in same order as config
|
2019-09-04 03:36:04 +00:00
|
|
|
module_hash: _MfaModuleDict = OrderedDict()
|
2018-08-22 07:52:34 +00:00
|
|
|
for module in modules:
|
|
|
|
module_hash[module.id] = module
|
|
|
|
|
|
|
|
manager = AuthManager(hass, store, provider_hash, module_hash)
|
2018-07-13 09:43:08 +00:00
|
|
|
return manager
|
|
|
|
|
|
|
|
|
2020-01-03 10:52:01 +00:00
|
|
|
class AuthManagerFlowManager(data_entry_flow.FlowManager):
|
|
|
|
"""Manage authentication flows."""
|
|
|
|
|
2021-05-20 15:53:29 +00:00
|
|
|
def __init__(self, hass: HomeAssistant, auth_manager: AuthManager) -> None:
|
2020-01-03 10:52:01 +00:00
|
|
|
"""Init auth manager flows."""
|
|
|
|
super().__init__(hass)
|
|
|
|
self.auth_manager = auth_manager
|
|
|
|
|
|
|
|
async def async_create_flow(
|
|
|
|
self,
|
2022-12-15 11:41:20 +00:00
|
|
|
handler_key: str,
|
2020-01-03 10:52:01 +00:00
|
|
|
*,
|
2021-03-17 20:46:07 +00:00
|
|
|
context: dict[str, Any] | None = None,
|
|
|
|
data: dict[str, Any] | None = None,
|
2020-01-03 10:52:01 +00:00
|
|
|
) -> data_entry_flow.FlowHandler:
|
|
|
|
"""Create a login flow."""
|
|
|
|
auth_provider = self.auth_manager.get_auth_provider(*handler_key)
|
|
|
|
if not auth_provider:
|
|
|
|
raise KeyError(f"Unknown auth provider {handler_key}")
|
|
|
|
return await auth_provider.async_login_flow(context)
|
|
|
|
|
|
|
|
async def async_finish_flow(
|
2021-04-29 11:40:51 +00:00
|
|
|
self, flow: data_entry_flow.FlowHandler, result: FlowResult
|
|
|
|
) -> FlowResult:
|
2020-01-03 10:52:01 +00:00
|
|
|
"""Return a user as result of login flow."""
|
|
|
|
flow = cast(LoginFlow, flow)
|
|
|
|
|
2022-06-08 05:02:44 +00:00
|
|
|
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
2020-01-03 10:52:01 +00:00
|
|
|
return result
|
|
|
|
|
|
|
|
# we got final result
|
2021-01-28 11:06:20 +00:00
|
|
|
if isinstance(result["data"], models.Credentials):
|
2020-01-03 10:52:01 +00:00
|
|
|
result["result"] = result["data"]
|
|
|
|
return result
|
|
|
|
|
|
|
|
auth_provider = self.auth_manager.get_auth_provider(*result["handler"])
|
|
|
|
if not auth_provider:
|
|
|
|
raise KeyError(f"Unknown auth provider {result['handler']}")
|
|
|
|
|
|
|
|
credentials = await auth_provider.async_get_or_create_credentials(
|
2021-04-15 17:17:07 +00:00
|
|
|
cast(Mapping[str, str], result["data"]),
|
2020-01-03 10:52:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
if flow.context.get("credential_only"):
|
|
|
|
result["result"] = credentials
|
|
|
|
return result
|
|
|
|
|
|
|
|
# multi-factor module cannot enabled for new credential
|
|
|
|
# which has not linked to a user yet
|
|
|
|
if auth_provider.support_mfa and not credentials.is_new:
|
|
|
|
user = await self.auth_manager.async_get_user_by_credentials(credentials)
|
|
|
|
if user is not None:
|
|
|
|
modules = await self.auth_manager.async_get_enabled_mfa(user)
|
|
|
|
|
|
|
|
if modules:
|
2021-01-28 11:06:20 +00:00
|
|
|
flow.credential = credentials
|
2020-01-03 10:52:01 +00:00
|
|
|
flow.user = user
|
|
|
|
flow.available_mfa_modules = modules
|
|
|
|
return await flow.async_step_select_mfa_module()
|
|
|
|
|
2021-01-28 11:06:20 +00:00
|
|
|
result["result"] = credentials
|
2020-01-03 10:52:01 +00:00
|
|
|
return result
|
|
|
|
|
|
|
|
|
2018-07-13 09:43:08 +00:00
|
|
|
class AuthManager:
|
|
|
|
"""Manage the authentication for Home Assistant."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
hass: HomeAssistant,
|
|
|
|
store: auth_store.AuthStore,
|
|
|
|
providers: _ProviderDict,
|
|
|
|
mfa_modules: _MfaModuleDict,
|
|
|
|
) -> None:
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Initialize the auth manager."""
|
2018-08-22 07:52:34 +00:00
|
|
|
self.hass = hass
|
2018-07-13 09:43:08 +00:00
|
|
|
self._store = store
|
|
|
|
self._providers = providers
|
2018-08-22 07:52:34 +00:00
|
|
|
self._mfa_modules = mfa_modules
|
2020-01-03 10:52:01 +00:00
|
|
|
self.login_flow = AuthManagerFlowManager(hass, self)
|
2021-10-08 16:38:22 +00:00
|
|
|
self._revoke_callbacks: dict[str, list[CALLBACK_TYPE]] = {}
|
2018-07-13 09:43:08 +00:00
|
|
|
|
|
|
|
@property
|
2021-03-17 20:46:07 +00:00
|
|
|
def auth_providers(self) -> list[AuthProvider]:
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Return a list of available auth providers."""
|
2018-07-13 13:31:20 +00:00
|
|
|
return list(self._providers.values())
|
|
|
|
|
2018-08-22 07:52:34 +00:00
|
|
|
@property
|
2021-03-17 20:46:07 +00:00
|
|
|
def auth_mfa_modules(self) -> list[MultiFactorAuthModule]:
|
2018-08-22 07:52:34 +00:00
|
|
|
"""Return a list of available auth modules."""
|
|
|
|
return list(self._mfa_modules.values())
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def get_auth_provider(
|
2021-03-17 20:46:07 +00:00
|
|
|
self, provider_type: str, provider_id: str | None
|
|
|
|
) -> AuthProvider | None:
|
2019-03-11 02:55:36 +00:00
|
|
|
"""Return an auth provider, None if not found."""
|
|
|
|
return self._providers.get((provider_type, provider_id))
|
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
def get_auth_providers(self, provider_type: str) -> list[AuthProvider]:
|
2019-03-11 02:55:36 +00:00
|
|
|
"""Return a List of auth provider of one type, Empty if not found."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return [
|
|
|
|
provider
|
|
|
|
for (p_type, _), provider in self._providers.items()
|
|
|
|
if p_type == provider_type
|
|
|
|
]
|
2019-03-11 02:55:36 +00:00
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
def get_auth_mfa_module(self, module_id: str) -> MultiFactorAuthModule | None:
|
2019-03-11 02:55:36 +00:00
|
|
|
"""Return a multi-factor auth module, None if not found."""
|
2018-08-22 07:52:34 +00:00
|
|
|
return self._mfa_modules.get(module_id)
|
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
async def async_get_users(self) -> list[models.User]:
|
2018-07-13 13:31:20 +00:00
|
|
|
"""Retrieve all users."""
|
|
|
|
return await self._store.async_get_users()
|
2018-07-13 09:43:08 +00:00
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
async def async_get_user(self, user_id: str) -> models.User | None:
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Retrieve a user."""
|
|
|
|
return await self._store.async_get_user(user_id)
|
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
async def async_get_owner(self) -> models.User | None:
|
2019-03-11 02:55:36 +00:00
|
|
|
"""Retrieve the owner."""
|
|
|
|
users = await self.async_get_users()
|
|
|
|
return next((user for user in users if user.is_owner), None)
|
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
async def async_get_group(self, group_id: str) -> models.Group | None:
|
2018-11-21 11:26:08 +00:00
|
|
|
"""Retrieve all groups."""
|
|
|
|
return await self._store.async_get_group(group_id)
|
|
|
|
|
2018-08-22 07:52:34 +00:00
|
|
|
async def async_get_user_by_credentials(
|
2019-07-31 19:25:30 +00:00
|
|
|
self, credentials: models.Credentials
|
2021-03-17 20:46:07 +00:00
|
|
|
) -> models.User | None:
|
2018-08-22 07:52:34 +00:00
|
|
|
"""Get a user by credential, return None if not found."""
|
|
|
|
for user in await self.async_get_users():
|
|
|
|
for creds in user.credentials:
|
|
|
|
if creds.id == credentials.id:
|
|
|
|
return user
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
2018-11-25 17:04:48 +00:00
|
|
|
async def async_create_system_user(
|
2021-11-29 22:01:03 +00:00
|
|
|
self,
|
|
|
|
name: str,
|
|
|
|
*,
|
|
|
|
group_ids: list[str] | None = None,
|
|
|
|
local_only: bool | None = None,
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> models.User:
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Create a system user."""
|
2018-10-11 15:06:51 +00:00
|
|
|
user = await self._store.async_create_user(
|
2021-11-29 22:01:03 +00:00
|
|
|
name=name,
|
|
|
|
system_generated=True,
|
|
|
|
is_active=True,
|
|
|
|
group_ids=group_ids or [],
|
|
|
|
local_only=local_only,
|
2018-07-13 09:43:08 +00:00
|
|
|
)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id})
|
2018-10-11 15:06:51 +00:00
|
|
|
|
|
|
|
return user
|
|
|
|
|
2020-03-30 18:33:43 +00:00
|
|
|
async def async_create_user(
|
2021-11-29 22:01:03 +00:00
|
|
|
self,
|
|
|
|
name: str,
|
|
|
|
*,
|
|
|
|
group_ids: list[str] | None = None,
|
|
|
|
local_only: bool | None = None,
|
2020-03-30 18:33:43 +00:00
|
|
|
) -> models.User:
|
2018-07-13 13:31:20 +00:00
|
|
|
"""Create a user."""
|
2021-03-17 20:46:07 +00:00
|
|
|
kwargs: dict[str, Any] = {
|
2019-07-31 19:25:30 +00:00
|
|
|
"name": name,
|
|
|
|
"is_active": True,
|
2020-03-30 18:33:43 +00:00
|
|
|
"group_ids": group_ids or [],
|
2021-11-29 22:01:03 +00:00
|
|
|
"local_only": local_only,
|
2019-09-04 03:36:04 +00:00
|
|
|
}
|
2018-07-15 18:46:15 +00:00
|
|
|
|
|
|
|
if await self._user_should_be_owner():
|
2019-07-31 19:25:30 +00:00
|
|
|
kwargs["is_owner"] = True
|
2018-07-15 18:46:15 +00:00
|
|
|
|
2018-10-11 15:06:51 +00:00
|
|
|
user = await self._store.async_create_user(**kwargs)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id})
|
2018-10-11 15:06:51 +00:00
|
|
|
|
|
|
|
return user
|
2018-07-13 13:31:20 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_get_or_create_user(
|
|
|
|
self, credentials: models.Credentials
|
|
|
|
) -> models.User:
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Get or create a user."""
|
|
|
|
if not credentials.is_new:
|
2018-08-22 07:52:34 +00:00
|
|
|
user = await self.async_get_user_by_credentials(credentials)
|
|
|
|
if user is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise ValueError("Unable to find the user.")
|
2019-02-27 21:10:40 +00:00
|
|
|
return user
|
2018-07-13 09:43:08 +00:00
|
|
|
|
|
|
|
auth_provider = self._async_get_auth_provider(credentials)
|
2018-07-13 13:31:20 +00:00
|
|
|
|
|
|
|
if auth_provider is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise RuntimeError("Credential with unknown provider encountered")
|
2018-07-13 13:31:20 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
info = await auth_provider.async_user_meta_for_credentials(credentials)
|
2018-07-13 09:43:08 +00:00
|
|
|
|
2018-10-11 15:06:51 +00:00
|
|
|
user = await self._store.async_create_user(
|
2018-07-15 18:46:15 +00:00
|
|
|
credentials=credentials,
|
2018-08-16 20:25:41 +00:00
|
|
|
name=info.name,
|
|
|
|
is_active=info.is_active,
|
2018-12-03 10:34:01 +00:00
|
|
|
group_ids=[GROUP_ID_ADMIN],
|
2018-07-15 18:46:15 +00:00
|
|
|
)
|
2018-07-13 09:43:08 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id})
|
2018-10-11 15:06:51 +00:00
|
|
|
|
|
|
|
return user
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_link_user(
|
|
|
|
self, user: models.User, credentials: models.Credentials
|
|
|
|
) -> None:
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Link credentials to an existing user."""
|
2021-10-13 15:36:31 +00:00
|
|
|
linked_user = await self.async_get_user_by_credentials(credentials)
|
|
|
|
if linked_user == user:
|
|
|
|
return
|
|
|
|
if linked_user is not None:
|
|
|
|
raise ValueError("Credential is already linked to a user")
|
|
|
|
|
2018-07-13 09:43:08 +00:00
|
|
|
await self._store.async_link_user(user, credentials)
|
|
|
|
|
2018-08-16 20:25:41 +00:00
|
|
|
async def async_remove_user(self, user: models.User) -> None:
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Remove a user."""
|
2018-07-13 13:31:20 +00:00
|
|
|
tasks = [
|
|
|
|
self.async_remove_credentials(credentials)
|
|
|
|
for credentials in user.credentials
|
|
|
|
]
|
|
|
|
|
|
|
|
if tasks:
|
2021-10-13 15:34:57 +00:00
|
|
|
await asyncio.gather(*tasks)
|
2018-07-13 13:31:20 +00:00
|
|
|
|
2018-07-13 09:43:08 +00:00
|
|
|
await self._store.async_remove_user(user)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass.bus.async_fire(EVENT_USER_REMOVED, {"user_id": user.id})
|
2018-10-11 15:06:51 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_update_user(
|
|
|
|
self,
|
|
|
|
user: models.User,
|
2021-03-17 20:46:07 +00:00
|
|
|
name: str | None = None,
|
|
|
|
is_active: bool | None = None,
|
|
|
|
group_ids: list[str] | None = None,
|
2021-11-29 22:01:03 +00:00
|
|
|
local_only: bool | None = None,
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> None:
|
2018-11-25 17:04:48 +00:00
|
|
|
"""Update a user."""
|
2021-03-17 20:46:07 +00:00
|
|
|
kwargs: dict[str, Any] = {}
|
2021-11-29 22:01:03 +00:00
|
|
|
|
|
|
|
for attr_name, value in (
|
|
|
|
("name", name),
|
|
|
|
("group_ids", group_ids),
|
|
|
|
("local_only", local_only),
|
|
|
|
):
|
|
|
|
if value is not None:
|
|
|
|
kwargs[attr_name] = value
|
2018-11-25 17:04:48 +00:00
|
|
|
await self._store.async_update_user(user, **kwargs)
|
|
|
|
|
2020-11-27 08:13:16 +00:00
|
|
|
if is_active is not None:
|
|
|
|
if is_active is True:
|
|
|
|
await self.async_activate_user(user)
|
|
|
|
else:
|
|
|
|
await self.async_deactivate_user(user)
|
|
|
|
|
2022-06-10 03:49:02 +00:00
|
|
|
self.hass.bus.async_fire(EVENT_USER_UPDATED, {"user_id": user.id})
|
|
|
|
|
2018-08-16 20:25:41 +00:00
|
|
|
async def async_activate_user(self, user: models.User) -> None:
|
2018-07-15 18:46:15 +00:00
|
|
|
"""Activate a user."""
|
|
|
|
await self._store.async_activate_user(user)
|
|
|
|
|
2018-08-16 20:25:41 +00:00
|
|
|
async def async_deactivate_user(self, user: models.User) -> None:
|
2018-07-15 18:46:15 +00:00
|
|
|
"""Deactivate a user."""
|
2018-07-15 21:09:05 +00:00
|
|
|
if user.is_owner:
|
2020-01-31 16:33:00 +00:00
|
|
|
raise ValueError("Unable to deactivate the owner")
|
2018-07-15 18:46:15 +00:00
|
|
|
await self._store.async_deactivate_user(user)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
|
2018-07-13 13:31:20 +00:00
|
|
|
"""Remove credentials."""
|
|
|
|
provider = self._async_get_auth_provider(credentials)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if provider is not None and hasattr(provider, "async_will_remove_credentials"):
|
2022-11-08 13:41:39 +00:00
|
|
|
await provider.async_will_remove_credentials(credentials)
|
2018-07-13 13:31:20 +00:00
|
|
|
|
|
|
|
await self._store.async_remove_credentials(credentials)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_enable_user_mfa(
|
|
|
|
self, user: models.User, mfa_module_id: str, data: Any
|
|
|
|
) -> None:
|
2018-08-22 07:52:34 +00:00
|
|
|
"""Enable a multi-factor auth module for user."""
|
|
|
|
if user.system_generated:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise ValueError(
|
2019-10-29 06:47:31 +00:00
|
|
|
"System generated users cannot enable multi-factor auth module."
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-08-22 07:52:34 +00:00
|
|
|
|
2021-09-18 23:31:35 +00:00
|
|
|
if (module := self.get_auth_mfa_module(mfa_module_id)) is None:
|
2019-08-23 16:53:33 +00:00
|
|
|
raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}")
|
2018-08-22 07:52:34 +00:00
|
|
|
|
|
|
|
await module.async_setup_user(user.id, data)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_disable_user_mfa(
|
|
|
|
self, user: models.User, mfa_module_id: str
|
|
|
|
) -> None:
|
2018-08-22 07:52:34 +00:00
|
|
|
"""Disable a multi-factor auth module for user."""
|
|
|
|
if user.system_generated:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise ValueError(
|
2019-10-29 06:47:31 +00:00
|
|
|
"System generated users cannot disable multi-factor auth module."
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-08-22 07:52:34 +00:00
|
|
|
|
2021-09-18 23:31:35 +00:00
|
|
|
if (module := self.get_auth_mfa_module(mfa_module_id)) is None:
|
2019-08-23 16:53:33 +00:00
|
|
|
raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}")
|
2018-08-22 07:52:34 +00:00
|
|
|
|
|
|
|
await module.async_depose_user(user.id)
|
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
async def async_get_enabled_mfa(self, user: models.User) -> dict[str, str]:
|
2018-08-22 07:52:34 +00:00
|
|
|
"""List enabled mfa modules for user."""
|
2021-03-17 20:46:07 +00:00
|
|
|
modules: dict[str, str] = OrderedDict()
|
2018-08-22 07:52:34 +00:00
|
|
|
for module_id, module in self._mfa_modules.items():
|
|
|
|
if await module.async_is_user_setup(user.id):
|
2018-08-26 20:38:52 +00:00
|
|
|
modules[module_id] = module.name
|
|
|
|
return modules
|
2018-08-22 07:52:34 +00:00
|
|
|
|
2018-09-11 10:05:15 +00:00
|
|
|
async def async_create_refresh_token(
|
2019-07-31 19:25:30 +00:00
|
|
|
self,
|
|
|
|
user: models.User,
|
2021-03-17 20:46:07 +00:00
|
|
|
client_id: str | None = None,
|
|
|
|
client_name: str | None = None,
|
|
|
|
client_icon: str | None = None,
|
|
|
|
token_type: str | None = None,
|
2019-07-31 19:25:30 +00:00
|
|
|
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
|
2021-03-17 20:46:07 +00:00
|
|
|
credential: models.Credentials | None = None,
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> models.RefreshToken:
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Create a new refresh token for a user."""
|
|
|
|
if not user.is_active:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise ValueError("User is not active")
|
2018-07-13 09:43:08 +00:00
|
|
|
|
|
|
|
if user.system_generated and client_id is not None:
|
|
|
|
raise ValueError(
|
2019-07-31 19:25:30 +00:00
|
|
|
"System generated users cannot have refresh tokens connected "
|
|
|
|
"to a client."
|
|
|
|
)
|
2018-07-13 09:43:08 +00:00
|
|
|
|
2018-09-11 10:05:15 +00:00
|
|
|
if token_type is None:
|
|
|
|
if user.system_generated:
|
|
|
|
token_type = models.TOKEN_TYPE_SYSTEM
|
|
|
|
else:
|
|
|
|
token_type = models.TOKEN_TYPE_NORMAL
|
|
|
|
|
|
|
|
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
|
|
|
|
raise ValueError(
|
2019-10-29 06:47:31 +00:00
|
|
|
"System generated users can only have system type refresh tokens"
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-09-11 10:05:15 +00:00
|
|
|
|
|
|
|
if token_type == models.TOKEN_TYPE_NORMAL and client_id is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise ValueError("Client is required to generate a refresh token.")
|
2018-07-13 09:43:08 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if (
|
|
|
|
token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
|
|
|
|
and client_name is None
|
|
|
|
):
|
2019-10-29 06:47:31 +00:00
|
|
|
raise ValueError("Client_name is required for long-lived access token")
|
2018-09-11 10:05:15 +00:00
|
|
|
|
|
|
|
if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN:
|
|
|
|
for token in user.refresh_tokens.values():
|
2019-07-31 19:25:30 +00:00
|
|
|
if (
|
|
|
|
token.client_name == client_name
|
|
|
|
and token.token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
|
|
|
|
):
|
2018-09-11 10:05:15 +00:00
|
|
|
# Each client_name can only have one
|
|
|
|
# long_lived_access_token type of refresh token
|
2019-08-23 16:53:33 +00:00
|
|
|
raise ValueError(f"{client_name} already exists")
|
2018-09-11 10:05:15 +00:00
|
|
|
|
|
|
|
return await self._store.async_create_refresh_token(
|
2019-07-31 19:25:30 +00:00
|
|
|
user,
|
|
|
|
client_id,
|
|
|
|
client_name,
|
|
|
|
client_icon,
|
|
|
|
token_type,
|
|
|
|
access_token_expiration,
|
2021-01-28 11:06:20 +00:00
|
|
|
credential,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-07-13 09:43:08 +00:00
|
|
|
|
2018-08-16 20:25:41 +00:00
|
|
|
async def async_get_refresh_token(
|
2019-07-31 19:25:30 +00:00
|
|
|
self, token_id: str
|
2021-03-17 20:46:07 +00:00
|
|
|
) -> models.RefreshToken | None:
|
2018-08-14 19:14:12 +00:00
|
|
|
"""Get refresh token by id."""
|
|
|
|
return await self._store.async_get_refresh_token(token_id)
|
|
|
|
|
2018-08-16 20:25:41 +00:00
|
|
|
async def async_get_refresh_token_by_token(
|
2019-07-31 19:25:30 +00:00
|
|
|
self, token: str
|
2021-03-17 20:46:07 +00:00
|
|
|
) -> models.RefreshToken | None:
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Get refresh token by token."""
|
2018-08-14 19:14:12 +00:00
|
|
|
return await self._store.async_get_refresh_token_by_token(token)
|
2018-07-13 09:43:08 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_remove_refresh_token(
|
|
|
|
self, refresh_token: models.RefreshToken
|
|
|
|
) -> None:
|
2018-08-21 18:02:55 +00:00
|
|
|
"""Delete a refresh token."""
|
|
|
|
await self._store.async_remove_refresh_token(refresh_token)
|
|
|
|
|
2021-10-08 16:38:22 +00:00
|
|
|
callbacks = self._revoke_callbacks.pop(refresh_token.id, [])
|
|
|
|
for revoke_callback in callbacks:
|
|
|
|
revoke_callback()
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_register_revoke_token_callback(
|
|
|
|
self, refresh_token_id: str, revoke_callback: CALLBACK_TYPE
|
|
|
|
) -> CALLBACK_TYPE:
|
|
|
|
"""Register a callback to be called when the refresh token id is revoked."""
|
|
|
|
if refresh_token_id not in self._revoke_callbacks:
|
|
|
|
self._revoke_callbacks[refresh_token_id] = []
|
|
|
|
|
|
|
|
callbacks = self._revoke_callbacks[refresh_token_id]
|
|
|
|
callbacks.append(revoke_callback)
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def unregister() -> None:
|
|
|
|
if revoke_callback in callbacks:
|
|
|
|
callbacks.remove(revoke_callback)
|
|
|
|
|
|
|
|
return unregister
|
|
|
|
|
2018-07-13 09:43:08 +00:00
|
|
|
@callback
|
2019-07-31 19:25:30 +00:00
|
|
|
def async_create_access_token(
|
2021-03-17 20:46:07 +00:00
|
|
|
self, refresh_token: models.RefreshToken, remote_ip: str | None = None
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> str:
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Create a new access token."""
|
2021-01-28 11:06:20 +00:00
|
|
|
self.async_validate_refresh_token(refresh_token, remote_ip)
|
|
|
|
|
2018-09-12 11:24:16 +00:00
|
|
|
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
|
|
|
|
|
2018-09-11 10:05:15 +00:00
|
|
|
now = dt_util.utcnow()
|
2019-07-31 19:25:30 +00:00
|
|
|
return jwt.encode(
|
|
|
|
{
|
|
|
|
"iss": refresh_token.id,
|
|
|
|
"iat": now,
|
|
|
|
"exp": now + refresh_token.access_token_expiration,
|
|
|
|
},
|
|
|
|
refresh_token.jwt_key,
|
|
|
|
algorithm="HS256",
|
2021-09-08 03:59:02 +00:00
|
|
|
)
|
2018-08-14 19:14:12 +00:00
|
|
|
|
2021-01-28 11:06:20 +00:00
|
|
|
@callback
|
|
|
|
def _async_resolve_provider(
|
|
|
|
self, refresh_token: models.RefreshToken
|
2021-03-17 20:46:07 +00:00
|
|
|
) -> AuthProvider | None:
|
2021-01-28 11:06:20 +00:00
|
|
|
"""Get the auth provider for the given refresh token.
|
|
|
|
|
|
|
|
Raises an exception if the expected provider is no longer available or return
|
|
|
|
None if no provider was expected for this refresh token.
|
|
|
|
"""
|
|
|
|
if refresh_token.credential is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
provider = self.get_auth_provider(
|
|
|
|
refresh_token.credential.auth_provider_type,
|
|
|
|
refresh_token.credential.auth_provider_id,
|
|
|
|
)
|
|
|
|
if provider is None:
|
|
|
|
raise InvalidProvider(
|
2022-12-22 09:12:50 +00:00
|
|
|
f"Auth provider {refresh_token.credential.auth_provider_type},"
|
|
|
|
f" {refresh_token.credential.auth_provider_id} not available"
|
2021-01-28 11:06:20 +00:00
|
|
|
)
|
|
|
|
return provider
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_validate_refresh_token(
|
2021-03-17 20:46:07 +00:00
|
|
|
self, refresh_token: models.RefreshToken, remote_ip: str | None = None
|
2021-01-28 11:06:20 +00:00
|
|
|
) -> None:
|
|
|
|
"""Validate that a refresh token is usable.
|
|
|
|
|
|
|
|
Will raise InvalidAuthError on errors.
|
|
|
|
"""
|
2021-09-18 23:31:35 +00:00
|
|
|
if provider := self._async_resolve_provider(refresh_token):
|
2021-01-28 11:06:20 +00:00
|
|
|
provider.async_validate_refresh_token(refresh_token, remote_ip)
|
|
|
|
|
2018-08-16 20:25:41 +00:00
|
|
|
async def async_validate_access_token(
|
2019-07-31 19:25:30 +00:00
|
|
|
self, token: str
|
2021-03-17 20:46:07 +00:00
|
|
|
) -> models.RefreshToken | None:
|
2018-08-21 18:02:55 +00:00
|
|
|
"""Return refresh token if an access token is valid."""
|
2018-08-14 19:14:12 +00:00
|
|
|
try:
|
2023-03-23 02:03:41 +00:00
|
|
|
unverif_claims = jwt_wrapper.unverified_hs256_token_decode(token)
|
2018-08-14 19:14:12 +00:00
|
|
|
except jwt.InvalidTokenError:
|
|
|
|
return None
|
2018-07-13 09:43:08 +00:00
|
|
|
|
2018-08-14 19:14:12 +00:00
|
|
|
refresh_token = await self.async_get_refresh_token(
|
2019-07-31 19:25:30 +00:00
|
|
|
cast(str, unverif_claims.get("iss"))
|
|
|
|
)
|
2018-08-14 19:14:12 +00:00
|
|
|
|
|
|
|
if refresh_token is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
jwt_key = ""
|
|
|
|
issuer = ""
|
2018-08-14 19:14:12 +00:00
|
|
|
else:
|
|
|
|
jwt_key = refresh_token.jwt_key
|
|
|
|
issuer = refresh_token.id
|
|
|
|
|
|
|
|
try:
|
2023-03-23 02:03:41 +00:00
|
|
|
jwt_wrapper.verify_and_decode(
|
|
|
|
token, jwt_key, leeway=10, issuer=issuer, algorithms=["HS256"]
|
|
|
|
)
|
2018-08-14 19:14:12 +00:00
|
|
|
except jwt.InvalidTokenError:
|
2018-07-13 09:43:08 +00:00
|
|
|
return None
|
|
|
|
|
2018-08-16 20:25:41 +00:00
|
|
|
if refresh_token is None or not refresh_token.user.is_active:
|
2018-07-13 09:43:08 +00:00
|
|
|
return None
|
|
|
|
|
2018-08-14 19:14:12 +00:00
|
|
|
return refresh_token
|
2018-07-13 09:43:08 +00:00
|
|
|
|
|
|
|
@callback
|
2018-08-16 20:25:41 +00:00
|
|
|
def _async_get_auth_provider(
|
2019-07-31 19:25:30 +00:00
|
|
|
self, credentials: models.Credentials
|
2021-03-17 20:46:07 +00:00
|
|
|
) -> AuthProvider | None:
|
2018-08-24 08:28:43 +00:00
|
|
|
"""Get auth provider from a set of credentials."""
|
2019-07-31 19:25:30 +00:00
|
|
|
auth_provider_key = (
|
|
|
|
credentials.auth_provider_type,
|
|
|
|
credentials.auth_provider_id,
|
|
|
|
)
|
2018-07-13 13:31:20 +00:00
|
|
|
return self._providers.get(auth_provider_key)
|
2018-07-15 18:46:15 +00:00
|
|
|
|
2018-08-16 20:25:41 +00:00
|
|
|
async def _user_should_be_owner(self) -> bool:
|
2018-07-15 18:46:15 +00:00
|
|
|
"""Determine if user should be owner.
|
|
|
|
|
|
|
|
A user should be an owner if it is the first non-system user that is
|
|
|
|
being created.
|
|
|
|
"""
|
|
|
|
for user in await self._store.async_get_users():
|
|
|
|
if not user.system_generated:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|