2018-05-01 16:20:41 +00:00
|
|
|
"""Component to allow users to login and get tokens.
|
|
|
|
|
|
|
|
# POST /auth/token
|
|
|
|
|
|
|
|
This is an OAuth2 endpoint for granting tokens. We currently support the grant
|
|
|
|
types "authorization_code" and "refresh_token". Because we follow the OAuth2
|
|
|
|
spec, data should be send in formatted as x-www-form-urlencoded. Examples will
|
|
|
|
be in JSON as it's more readable.
|
|
|
|
|
|
|
|
## Grant type authorization_code
|
|
|
|
|
|
|
|
Exchange the authorization code retrieved from the login flow for tokens.
|
|
|
|
|
|
|
|
{
|
2018-09-11 10:05:15 +00:00
|
|
|
"client_id": "https://hassbian.local:8123/",
|
2018-05-01 16:20:41 +00:00
|
|
|
"grant_type": "authorization_code",
|
|
|
|
"code": "411ee2f916e648d691e937ae9344681e"
|
|
|
|
}
|
|
|
|
|
|
|
|
Return value will be the access and refresh tokens. The access token will have
|
|
|
|
a limited expiration. New access tokens can be requested using the refresh
|
|
|
|
token.
|
|
|
|
|
|
|
|
{
|
|
|
|
"access_token": "ABCDEFGH",
|
|
|
|
"expires_in": 1800,
|
|
|
|
"refresh_token": "IJKLMNOPQRST",
|
|
|
|
"token_type": "Bearer"
|
|
|
|
}
|
|
|
|
|
|
|
|
## Grant type refresh_token
|
|
|
|
|
|
|
|
Request a new access token using a refresh token.
|
|
|
|
|
|
|
|
{
|
2018-09-11 10:05:15 +00:00
|
|
|
"client_id": "https://hassbian.local:8123/",
|
2018-05-01 16:20:41 +00:00
|
|
|
"grant_type": "refresh_token",
|
|
|
|
"refresh_token": "IJKLMNOPQRST"
|
|
|
|
}
|
|
|
|
|
|
|
|
Return value will be a new access token. The access token will have
|
|
|
|
a limited expiration.
|
|
|
|
|
|
|
|
{
|
|
|
|
"access_token": "ABCDEFGH",
|
|
|
|
"expires_in": 1800,
|
|
|
|
"token_type": "Bearer"
|
|
|
|
}
|
2018-08-21 18:02:55 +00:00
|
|
|
|
|
|
|
## Revoking a refresh token
|
|
|
|
|
|
|
|
It is also possible to revoke a refresh token and all access tokens that have
|
|
|
|
ever been granted by that refresh token. Response code will ALWAYS be 200.
|
|
|
|
|
|
|
|
{
|
|
|
|
"token": "IJKLMNOPQRST",
|
|
|
|
"action": "revoke"
|
|
|
|
}
|
|
|
|
|
2018-09-11 10:05:15 +00:00
|
|
|
# Websocket API
|
|
|
|
|
|
|
|
## Get current user
|
|
|
|
|
|
|
|
Send websocket command `auth/current_user` will return current user of the
|
|
|
|
active websocket connection.
|
|
|
|
|
|
|
|
{
|
|
|
|
"id": 10,
|
|
|
|
"type": "auth/current_user",
|
|
|
|
}
|
|
|
|
|
|
|
|
The result payload likes
|
|
|
|
|
|
|
|
{
|
|
|
|
"id": 10,
|
|
|
|
"type": "result",
|
|
|
|
"success": true,
|
|
|
|
"result": {
|
|
|
|
"id": "USER_ID",
|
|
|
|
"name": "John Doe",
|
2019-04-11 04:58:50 +00:00
|
|
|
"is_owner": true,
|
|
|
|
"credentials": [{
|
|
|
|
"auth_provider_type": "homeassistant",
|
|
|
|
"auth_provider_id": null
|
|
|
|
}],
|
|
|
|
"mfa_modules": [{
|
|
|
|
"id": "totp",
|
|
|
|
"name": "TOTP",
|
|
|
|
"enabled": true
|
|
|
|
}]
|
2018-09-11 10:05:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
## Create a long-lived access token
|
|
|
|
|
|
|
|
Send websocket command `auth/long_lived_access_token` will create
|
|
|
|
a long-lived access token for current user. Access token will not be saved in
|
|
|
|
Home Assistant. User need to record the token in secure place.
|
|
|
|
|
|
|
|
{
|
|
|
|
"id": 11,
|
|
|
|
"type": "auth/long_lived_access_token",
|
|
|
|
"client_name": "GPS Logger",
|
|
|
|
"lifespan": 365
|
|
|
|
}
|
|
|
|
|
|
|
|
Result will be a long-lived access token:
|
|
|
|
|
|
|
|
{
|
|
|
|
"id": 11,
|
|
|
|
"type": "result",
|
|
|
|
"success": true,
|
|
|
|
"result": "ABCDEFGH"
|
|
|
|
}
|
|
|
|
|
2018-05-01 16:20:41 +00:00
|
|
|
"""
|
2021-03-17 22:34:25 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2019-12-08 13:55:23 +00:00
|
|
|
from datetime import timedelta
|
2021-09-22 18:59:52 +00:00
|
|
|
from http import HTTPStatus
|
2018-05-01 16:20:41 +00:00
|
|
|
import uuid
|
|
|
|
|
2018-08-21 18:02:55 +00:00
|
|
|
from aiohttp import web
|
2018-05-01 16:20:41 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2021-01-28 11:06:20 +00:00
|
|
|
from homeassistant.auth import InvalidAuthError
|
2021-11-28 13:14:52 +00:00
|
|
|
from homeassistant.auth.models import TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, Credentials
|
2018-07-17 07:24:51 +00:00
|
|
|
from homeassistant.components import websocket_api
|
2021-11-29 22:01:03 +00:00
|
|
|
from homeassistant.components.http.auth import (
|
|
|
|
async_sign_path,
|
|
|
|
async_user_not_allowed_do_auth,
|
|
|
|
)
|
2018-07-29 00:54:26 +00:00
|
|
|
from homeassistant.components.http.ban import log_invalid_auth
|
2018-05-01 16:20:41 +00:00
|
|
|
from homeassistant.components.http.data_validator import RequestDataValidator
|
2018-07-29 00:54:26 +00:00
|
|
|
from homeassistant.components.http.view import HomeAssistantView
|
2019-12-08 13:55:23 +00:00
|
|
|
from homeassistant.core import HomeAssistant, callback
|
2021-12-31 05:17:05 +00:00
|
|
|
from homeassistant.helpers.typing import ConfigType
|
2019-12-08 13:55:23 +00:00
|
|
|
from homeassistant.loader import bind_hass
|
2018-07-10 09:20:22 +00:00
|
|
|
from homeassistant.util import dt as dt_util
|
2018-08-24 17:17:43 +00:00
|
|
|
|
2019-12-08 13:55:23 +00:00
|
|
|
from . import indieauth, login_flow, mfa_setup_flow
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN = "auth"
|
|
|
|
WS_TYPE_CURRENT_USER = "auth/current_user"
|
|
|
|
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
|
|
|
{vol.Required("type"): WS_TYPE_CURRENT_USER}
|
|
|
|
)
|
|
|
|
|
|
|
|
WS_TYPE_LONG_LIVED_ACCESS_TOKEN = "auth/long_lived_access_token"
|
|
|
|
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Required("type"): WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
|
|
|
vol.Required("lifespan"): int, # days
|
|
|
|
vol.Required("client_name"): str,
|
|
|
|
vol.Optional("client_icon"): str,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
WS_TYPE_REFRESH_TOKENS = "auth/refresh_tokens"
|
|
|
|
SCHEMA_WS_REFRESH_TOKENS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
|
|
|
{vol.Required("type"): WS_TYPE_REFRESH_TOKENS}
|
|
|
|
)
|
|
|
|
|
|
|
|
WS_TYPE_DELETE_REFRESH_TOKEN = "auth/delete_refresh_token"
|
|
|
|
SCHEMA_WS_DELETE_REFRESH_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Required("type"): WS_TYPE_DELETE_REFRESH_TOKEN,
|
|
|
|
vol.Required("refresh_token_id"): str,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
WS_TYPE_SIGN_PATH = "auth/sign_path"
|
|
|
|
SCHEMA_WS_SIGN_PATH = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Required("type"): WS_TYPE_SIGN_PATH,
|
|
|
|
vol.Required("path"): str,
|
|
|
|
vol.Optional("expires", default=30): int,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
RESULT_TYPE_CREDENTIALS = "credentials"
|
2018-08-21 08:18:04 +00:00
|
|
|
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2019-03-08 21:51:42 +00:00
|
|
|
@bind_hass
|
2021-11-28 13:14:52 +00:00
|
|
|
def create_auth_code(hass, client_id: str, credential: Credentials) -> str:
|
2019-03-08 21:51:42 +00:00
|
|
|
"""Create an authorization code to fetch tokens."""
|
2021-11-28 13:14:52 +00:00
|
|
|
return hass.data[DOMAIN](client_id, credential)
|
2019-03-08 21:51:42 +00:00
|
|
|
|
|
|
|
|
2021-12-31 05:17:05 +00:00
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
2018-05-01 16:20:41 +00:00
|
|
|
"""Component to allow users to login."""
|
2018-08-21 08:18:04 +00:00
|
|
|
store_result, retrieve_result = _create_auth_code_store()
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2019-03-08 21:51:42 +00:00
|
|
|
hass.data[DOMAIN] = store_result
|
|
|
|
|
2018-08-21 18:02:55 +00:00
|
|
|
hass.http.register_view(TokenView(retrieve_result))
|
2018-08-21 08:18:04 +00:00
|
|
|
hass.http.register_view(LinkUserView(retrieve_result))
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2022-01-11 17:24:08 +00:00
|
|
|
websocket_api.async_register_command(
|
|
|
|
hass, WS_TYPE_CURRENT_USER, websocket_current_user, SCHEMA_WS_CURRENT_USER
|
2018-07-17 07:24:51 +00:00
|
|
|
)
|
2022-01-11 17:24:08 +00:00
|
|
|
websocket_api.async_register_command(
|
|
|
|
hass,
|
2018-09-11 10:05:15 +00:00
|
|
|
WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
|
|
|
websocket_create_long_lived_access_token,
|
2019-07-31 19:25:30 +00:00
|
|
|
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN,
|
2018-09-11 10:05:15 +00:00
|
|
|
)
|
2022-01-11 17:24:08 +00:00
|
|
|
websocket_api.async_register_command(
|
|
|
|
hass, WS_TYPE_REFRESH_TOKENS, websocket_refresh_tokens, SCHEMA_WS_REFRESH_TOKENS
|
2018-09-11 16:08:03 +00:00
|
|
|
)
|
2022-01-11 17:24:08 +00:00
|
|
|
websocket_api.async_register_command(
|
|
|
|
hass,
|
2018-09-11 16:08:03 +00:00
|
|
|
WS_TYPE_DELETE_REFRESH_TOKEN,
|
|
|
|
websocket_delete_refresh_token,
|
2019-07-31 19:25:30 +00:00
|
|
|
SCHEMA_WS_DELETE_REFRESH_TOKEN,
|
2018-09-11 16:08:03 +00:00
|
|
|
)
|
2022-01-11 17:24:08 +00:00
|
|
|
websocket_api.async_register_command(
|
|
|
|
hass, WS_TYPE_SIGN_PATH, websocket_sign_path, SCHEMA_WS_SIGN_PATH
|
2018-10-25 14:44:57 +00:00
|
|
|
)
|
2018-07-17 07:24:51 +00:00
|
|
|
|
2018-08-21 08:18:04 +00:00
|
|
|
await login_flow.async_setup(hass, store_result)
|
2018-08-24 17:17:43 +00:00
|
|
|
await mfa_setup_flow.async_setup(hass)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2018-07-29 00:54:26 +00:00
|
|
|
return True
|
2018-05-01 16:20:41 +00:00
|
|
|
|
|
|
|
|
2018-08-21 18:02:55 +00:00
|
|
|
class TokenView(HomeAssistantView):
|
|
|
|
"""View to issue or revoke tokens."""
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = "/auth/token"
|
|
|
|
name = "api:auth:token"
|
2018-05-01 16:20:41 +00:00
|
|
|
requires_auth = False
|
2018-07-19 06:37:00 +00:00
|
|
|
cors_allowed = True
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2021-01-28 11:06:20 +00:00
|
|
|
def __init__(self, retrieve_auth):
|
2018-08-21 18:02:55 +00:00
|
|
|
"""Initialize the token view."""
|
2021-01-28 11:06:20 +00:00
|
|
|
self._retrieve_auth = retrieve_auth
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2018-07-24 08:09:52 +00:00
|
|
|
@log_invalid_auth
|
2018-07-09 16:24:46 +00:00
|
|
|
async def post(self, request):
|
2018-05-01 16:20:41 +00:00
|
|
|
"""Grant a token."""
|
2019-07-31 19:25:30 +00:00
|
|
|
hass = request.app["hass"]
|
2018-05-01 16:20:41 +00:00
|
|
|
data = await request.post()
|
2018-07-09 16:24:46 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
grant_type = data.get("grant_type")
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2018-08-21 18:02:55 +00:00
|
|
|
# IndieAuth 6.3.5
|
|
|
|
# The revocation endpoint is the same as the token endpoint.
|
|
|
|
# The revocation request includes an additional parameter,
|
|
|
|
# action=revoke.
|
2019-07-31 19:25:30 +00:00
|
|
|
if data.get("action") == "revoke":
|
2018-08-21 18:02:55 +00:00
|
|
|
return await self._async_handle_revoke_token(hass, data)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if grant_type == "authorization_code":
|
2020-08-11 20:57:50 +00:00
|
|
|
return await self._async_handle_auth_code(hass, data, request.remote)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if grant_type == "refresh_token":
|
2020-08-11 20:57:50 +00:00
|
|
|
return await self._async_handle_refresh_token(hass, data, request.remote)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2020-04-09 19:43:42 +00:00
|
|
|
return self.json(
|
2021-09-22 18:59:52 +00:00
|
|
|
{"error": "unsupported_grant_type"}, status_code=HTTPStatus.BAD_REQUEST
|
2020-04-09 19:43:42 +00:00
|
|
|
)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2018-08-21 18:02:55 +00:00
|
|
|
async def _async_handle_revoke_token(self, hass, data):
|
|
|
|
"""Handle revoke token request."""
|
2021-09-18 11:52:59 +00:00
|
|
|
# pylint: disable=no-self-use
|
|
|
|
|
2018-08-21 18:02:55 +00:00
|
|
|
# OAuth 2.0 Token Revocation [RFC7009]
|
|
|
|
# 2.2 The authorization server responds with HTTP status code 200
|
|
|
|
# if the token has been revoked successfully or if the client
|
|
|
|
# submitted an invalid token.
|
2021-10-17 18:19:56 +00:00
|
|
|
if (token := data.get("token")) is None:
|
2021-10-01 16:27:44 +00:00
|
|
|
return web.Response(status=HTTPStatus.OK)
|
2018-08-21 18:02:55 +00:00
|
|
|
|
|
|
|
refresh_token = await hass.auth.async_get_refresh_token_by_token(token)
|
|
|
|
|
|
|
|
if refresh_token is None:
|
2021-10-01 16:27:44 +00:00
|
|
|
return web.Response(status=HTTPStatus.OK)
|
2018-08-21 18:02:55 +00:00
|
|
|
|
|
|
|
await hass.auth.async_remove_refresh_token(refresh_token)
|
2021-10-01 16:27:44 +00:00
|
|
|
return web.Response(status=HTTPStatus.OK)
|
2018-08-21 18:02:55 +00:00
|
|
|
|
2018-09-12 11:24:16 +00:00
|
|
|
async def _async_handle_auth_code(self, hass, data, remote_addr):
|
2018-05-01 16:20:41 +00:00
|
|
|
"""Handle authorization code request."""
|
2019-07-31 19:25:30 +00:00
|
|
|
client_id = data.get("client_id")
|
2018-07-23 12:06:09 +00:00
|
|
|
if client_id is None or not indieauth.verify_client_id(client_id):
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json(
|
|
|
|
{"error": "invalid_request", "error_description": "Invalid client id"},
|
2021-09-22 18:59:52 +00:00
|
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-07-23 12:06:09 +00:00
|
|
|
|
2021-10-17 18:19:56 +00:00
|
|
|
if (code := data.get("code")) is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json(
|
|
|
|
{"error": "invalid_request", "error_description": "Invalid code"},
|
2021-09-22 18:59:52 +00:00
|
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2021-11-28 13:14:52 +00:00
|
|
|
credential = self._retrieve_auth(client_id, code)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2021-01-28 11:06:20 +00:00
|
|
|
if credential is None or not isinstance(credential, Credentials):
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json(
|
|
|
|
{"error": "invalid_request", "error_description": "Invalid code"},
|
2021-09-22 18:59:52 +00:00
|
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2021-01-28 11:06:20 +00:00
|
|
|
user = await hass.auth.async_get_or_create_user(credential)
|
2018-07-15 18:46:15 +00:00
|
|
|
|
2021-11-29 22:01:03 +00:00
|
|
|
if user_access_error := async_user_not_allowed_do_auth(hass, user):
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json(
|
2021-11-29 22:01:03 +00:00
|
|
|
{
|
|
|
|
"error": "access_denied",
|
|
|
|
"error_description": user_access_error,
|
|
|
|
},
|
2021-09-22 18:59:52 +00:00
|
|
|
status_code=HTTPStatus.FORBIDDEN,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
|
|
|
|
2021-01-28 11:06:20 +00:00
|
|
|
refresh_token = await hass.auth.async_create_refresh_token(
|
|
|
|
user, client_id, credential=credential
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
access_token = hass.auth.async_create_access_token(
|
|
|
|
refresh_token, remote_addr
|
|
|
|
)
|
|
|
|
except InvalidAuthError as exc:
|
|
|
|
return self.json(
|
|
|
|
{"error": "access_denied", "error_description": str(exc)},
|
2021-09-22 18:59:52 +00:00
|
|
|
status_code=HTTPStatus.FORBIDDEN,
|
2021-01-28 11:06:20 +00:00
|
|
|
)
|
2019-07-31 19:25:30 +00:00
|
|
|
|
|
|
|
return self.json(
|
|
|
|
{
|
|
|
|
"access_token": access_token,
|
|
|
|
"token_type": "Bearer",
|
|
|
|
"refresh_token": refresh_token.token,
|
|
|
|
"expires_in": int(
|
|
|
|
refresh_token.access_token_expiration.total_seconds()
|
|
|
|
),
|
|
|
|
}
|
|
|
|
)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2018-09-12 11:24:16 +00:00
|
|
|
async def _async_handle_refresh_token(self, hass, data, remote_addr):
|
2018-05-01 16:20:41 +00:00
|
|
|
"""Handle authorization code request."""
|
2019-07-31 19:25:30 +00:00
|
|
|
client_id = data.get("client_id")
|
2018-07-23 12:06:09 +00:00
|
|
|
if client_id is not None and not indieauth.verify_client_id(client_id):
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json(
|
|
|
|
{"error": "invalid_request", "error_description": "Invalid client id"},
|
2021-09-22 18:59:52 +00:00
|
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-07-23 12:06:09 +00:00
|
|
|
|
2021-10-17 18:19:56 +00:00
|
|
|
if (token := data.get("refresh_token")) is None:
|
2021-09-22 18:59:52 +00:00
|
|
|
return self.json(
|
|
|
|
{"error": "invalid_request"}, status_code=HTTPStatus.BAD_REQUEST
|
|
|
|
)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2018-08-14 19:14:12 +00:00
|
|
|
refresh_token = await hass.auth.async_get_refresh_token_by_token(token)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2018-07-23 12:06:09 +00:00
|
|
|
if refresh_token is None:
|
2021-09-22 18:59:52 +00:00
|
|
|
return self.json(
|
|
|
|
{"error": "invalid_grant"}, status_code=HTTPStatus.BAD_REQUEST
|
|
|
|
)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2018-07-23 12:06:09 +00:00
|
|
|
if refresh_token.client_id != client_id:
|
2021-09-22 18:59:52 +00:00
|
|
|
return self.json(
|
|
|
|
{"error": "invalid_request"}, status_code=HTTPStatus.BAD_REQUEST
|
|
|
|
)
|
2018-07-23 12:06:09 +00:00
|
|
|
|
2021-11-29 22:01:03 +00:00
|
|
|
if user_access_error := async_user_not_allowed_do_auth(
|
|
|
|
hass, refresh_token.user
|
|
|
|
):
|
|
|
|
return self.json(
|
|
|
|
{
|
|
|
|
"error": "access_denied",
|
|
|
|
"error_description": user_access_error,
|
|
|
|
},
|
|
|
|
status_code=HTTPStatus.FORBIDDEN,
|
|
|
|
)
|
|
|
|
|
2021-01-28 11:06:20 +00:00
|
|
|
try:
|
|
|
|
access_token = hass.auth.async_create_access_token(
|
|
|
|
refresh_token, remote_addr
|
|
|
|
)
|
|
|
|
except InvalidAuthError as exc:
|
|
|
|
return self.json(
|
|
|
|
{"error": "access_denied", "error_description": str(exc)},
|
2021-09-22 18:59:52 +00:00
|
|
|
status_code=HTTPStatus.FORBIDDEN,
|
2021-01-28 11:06:20 +00:00
|
|
|
)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json(
|
|
|
|
{
|
|
|
|
"access_token": access_token,
|
|
|
|
"token_type": "Bearer",
|
|
|
|
"expires_in": int(
|
|
|
|
refresh_token.access_token_expiration.total_seconds()
|
|
|
|
),
|
|
|
|
}
|
|
|
|
)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
class LinkUserView(HomeAssistantView):
|
|
|
|
"""View to link existing users to new credentials."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = "/auth/link_user"
|
|
|
|
name = "api:auth:link_user"
|
2018-05-01 16:20:41 +00:00
|
|
|
|
|
|
|
def __init__(self, retrieve_credentials):
|
|
|
|
"""Initialize the link user view."""
|
|
|
|
self._retrieve_credentials = retrieve_credentials
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
@RequestDataValidator(vol.Schema({"code": str, "client_id": str}))
|
2018-05-01 16:20:41 +00:00
|
|
|
async def post(self, request, data):
|
|
|
|
"""Link a user."""
|
2019-07-31 19:25:30 +00:00
|
|
|
hass = request.app["hass"]
|
|
|
|
user = request["hass_user"]
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2021-11-28 13:14:52 +00:00
|
|
|
credentials = self._retrieve_credentials(data["client_id"], data["code"])
|
2018-05-01 16:20:41 +00:00
|
|
|
|
|
|
|
if credentials is None:
|
2021-09-22 18:59:52 +00:00
|
|
|
return self.json_message("Invalid code", status_code=HTTPStatus.BAD_REQUEST)
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2021-10-13 15:36:31 +00:00
|
|
|
linked_user = await hass.auth.async_get_user_by_credentials(credentials)
|
|
|
|
if linked_user != user and linked_user is not None:
|
|
|
|
return self.json_message(
|
|
|
|
"Credential already linked", status_code=HTTPStatus.BAD_REQUEST
|
|
|
|
)
|
|
|
|
|
|
|
|
# No-op if credential is already linked to the user it will be linked to
|
|
|
|
if linked_user != user:
|
|
|
|
await hass.auth.async_link_user(user, credentials)
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json_message("User linked")
|
2018-05-01 16:20:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2018-08-21 08:18:04 +00:00
|
|
|
def _create_auth_code_store():
|
|
|
|
"""Create an in memory store."""
|
|
|
|
temp_results = {}
|
2018-05-01 16:20:41 +00:00
|
|
|
|
|
|
|
@callback
|
2018-08-21 08:18:04 +00:00
|
|
|
def store_result(client_id, result):
|
|
|
|
"""Store flow result and return a code to retrieve it."""
|
2021-11-28 13:14:52 +00:00
|
|
|
if not isinstance(result, Credentials):
|
|
|
|
raise ValueError("result has to be a Credentials instance")
|
2018-08-21 08:18:04 +00:00
|
|
|
|
2018-05-01 16:20:41 +00:00
|
|
|
code = uuid.uuid4().hex
|
2021-11-28 13:14:52 +00:00
|
|
|
temp_results[(client_id, code)] = (
|
2019-07-31 19:25:30 +00:00
|
|
|
dt_util.utcnow(),
|
|
|
|
result,
|
|
|
|
)
|
2018-05-01 16:20:41 +00:00
|
|
|
return code
|
|
|
|
|
|
|
|
@callback
|
2021-11-28 13:14:52 +00:00
|
|
|
def retrieve_result(client_id, code):
|
2018-08-21 08:18:04 +00:00
|
|
|
"""Retrieve flow result."""
|
2021-11-28 13:14:52 +00:00
|
|
|
key = (client_id, code)
|
2018-07-10 09:20:22 +00:00
|
|
|
|
2018-08-21 08:18:04 +00:00
|
|
|
if key not in temp_results:
|
2018-07-10 09:20:22 +00:00
|
|
|
return None
|
|
|
|
|
2021-11-28 13:14:52 +00:00
|
|
|
created, result = temp_results.pop(key)
|
2018-07-10 09:20:22 +00:00
|
|
|
|
|
|
|
# OAuth 4.2.1
|
|
|
|
# The authorization code MUST expire shortly after it is issued to
|
|
|
|
# mitigate the risk of leaks. A maximum authorization code lifetime of
|
|
|
|
# 10 minutes is RECOMMENDED.
|
|
|
|
if dt_util.utcnow() - created < timedelta(minutes=10):
|
2018-08-21 08:18:04 +00:00
|
|
|
return result
|
2018-07-10 09:20:22 +00:00
|
|
|
|
|
|
|
return None
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2018-08-21 08:18:04 +00:00
|
|
|
return store_result, retrieve_result
|
2018-07-17 07:24:51 +00:00
|
|
|
|
|
|
|
|
2018-08-24 17:17:43 +00:00
|
|
|
@websocket_api.ws_require_user()
|
2018-10-24 13:23:09 +00:00
|
|
|
@websocket_api.async_response
|
|
|
|
async def websocket_current_user(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
|
|
|
):
|
2018-07-17 07:24:51 +00:00
|
|
|
"""Return the current user."""
|
2018-10-24 13:23:09 +00:00
|
|
|
user = connection.user
|
|
|
|
enabled_modules = await hass.auth.async_get_enabled_mfa(user)
|
|
|
|
|
|
|
|
connection.send_message(
|
2019-07-31 19:25:30 +00:00
|
|
|
websocket_api.result_message(
|
|
|
|
msg["id"],
|
|
|
|
{
|
|
|
|
"id": user.id,
|
|
|
|
"name": user.name,
|
|
|
|
"is_owner": user.is_owner,
|
|
|
|
"is_admin": user.is_admin,
|
|
|
|
"credentials": [
|
|
|
|
{
|
|
|
|
"auth_provider_type": c.auth_provider_type,
|
|
|
|
"auth_provider_id": c.auth_provider_id,
|
|
|
|
}
|
|
|
|
for c in user.credentials
|
|
|
|
],
|
|
|
|
"mfa_modules": [
|
|
|
|
{
|
|
|
|
"id": module.id,
|
|
|
|
"name": module.name,
|
|
|
|
"enabled": module.id in enabled_modules,
|
|
|
|
}
|
|
|
|
for module in hass.auth.auth_mfa_modules
|
|
|
|
],
|
|
|
|
},
|
|
|
|
)
|
|
|
|
)
|
2018-09-11 10:05:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.ws_require_user()
|
2018-10-24 13:23:09 +00:00
|
|
|
@websocket_api.async_response
|
|
|
|
async def websocket_create_long_lived_access_token(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
|
|
|
):
|
2018-09-11 10:05:15 +00:00
|
|
|
"""Create or a long-lived access token."""
|
2018-10-24 13:23:09 +00:00
|
|
|
refresh_token = await hass.auth.async_create_refresh_token(
|
|
|
|
connection.user,
|
2019-07-31 19:25:30 +00:00
|
|
|
client_name=msg["client_name"],
|
|
|
|
client_icon=msg.get("client_icon"),
|
2018-10-24 13:23:09 +00:00
|
|
|
token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
2019-07-31 19:25:30 +00:00
|
|
|
access_token_expiration=timedelta(days=msg["lifespan"]),
|
|
|
|
)
|
2018-09-11 10:05:15 +00:00
|
|
|
|
2021-01-28 11:06:20 +00:00
|
|
|
try:
|
|
|
|
access_token = hass.auth.async_create_access_token(refresh_token)
|
|
|
|
except InvalidAuthError as exc:
|
|
|
|
return websocket_api.error_message(
|
|
|
|
msg["id"], websocket_api.const.ERR_UNAUTHORIZED, str(exc)
|
|
|
|
)
|
2018-09-11 10:05:15 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_message(websocket_api.result_message(msg["id"], access_token))
|
2018-09-11 16:08:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.ws_require_user()
|
|
|
|
@callback
|
|
|
|
def websocket_refresh_tokens(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
|
|
|
):
|
2018-09-11 16:08:03 +00:00
|
|
|
"""Return metadata of users refresh tokens."""
|
2018-10-01 14:09:31 +00:00
|
|
|
current_id = connection.refresh_token_id
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_message(
|
|
|
|
websocket_api.result_message(
|
|
|
|
msg["id"],
|
|
|
|
[
|
|
|
|
{
|
|
|
|
"id": refresh.id,
|
|
|
|
"client_id": refresh.client_id,
|
|
|
|
"client_name": refresh.client_name,
|
|
|
|
"client_icon": refresh.client_icon,
|
|
|
|
"type": refresh.token_type,
|
|
|
|
"created_at": refresh.created_at,
|
|
|
|
"is_current": refresh.id == current_id,
|
|
|
|
"last_used_at": refresh.last_used_at,
|
|
|
|
"last_used_ip": refresh.last_used_ip,
|
|
|
|
}
|
|
|
|
for refresh in connection.user.refresh_tokens.values()
|
|
|
|
],
|
|
|
|
)
|
|
|
|
)
|
2018-09-11 16:08:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.ws_require_user()
|
2018-10-24 13:23:09 +00:00
|
|
|
@websocket_api.async_response
|
|
|
|
async def websocket_delete_refresh_token(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
|
|
|
):
|
2018-09-11 16:08:03 +00:00
|
|
|
"""Handle a delete refresh token request."""
|
2019-07-31 19:25:30 +00:00
|
|
|
refresh_token = connection.user.refresh_tokens.get(msg["refresh_token_id"])
|
2018-09-11 16:08:03 +00:00
|
|
|
|
2018-10-24 13:23:09 +00:00
|
|
|
if refresh_token is None:
|
|
|
|
return websocket_api.error_message(
|
2019-07-31 19:25:30 +00:00
|
|
|
msg["id"], "invalid_token_id", "Received invalid token"
|
|
|
|
)
|
2018-09-11 16:08:03 +00:00
|
|
|
|
2018-10-24 13:23:09 +00:00
|
|
|
await hass.auth.async_remove_refresh_token(refresh_token)
|
2018-09-11 16:08:03 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_message(websocket_api.result_message(msg["id"], {}))
|
2018-10-25 14:44:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.ws_require_user()
|
|
|
|
@callback
|
|
|
|
def websocket_sign_path(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
|
|
|
):
|
2018-10-25 14:44:57 +00:00
|
|
|
"""Handle a sign path request."""
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_message(
|
|
|
|
websocket_api.result_message(
|
|
|
|
msg["id"],
|
|
|
|
{
|
|
|
|
"path": async_sign_path(
|
|
|
|
hass,
|
|
|
|
msg["path"],
|
|
|
|
timedelta(seconds=msg["expires"]),
|
|
|
|
)
|
|
|
|
},
|
|
|
|
)
|
|
|
|
)
|