mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add websocket commands for refresh tokens (#16559)
* Add websocket commands for refresh tokens * Comment
This commit is contained in:
parent
4e3faf6108
commit
0db13a99aa
@ -156,6 +156,19 @@ SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = \
|
||||
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,
|
||||
})
|
||||
|
||||
RESULT_TYPE_CREDENTIALS = 'credentials'
|
||||
RESULT_TYPE_USER = 'user'
|
||||
|
||||
@ -178,6 +191,16 @@ async def async_setup(hass, config):
|
||||
websocket_create_long_lived_access_token,
|
||||
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_REFRESH_TOKENS,
|
||||
websocket_refresh_tokens,
|
||||
SCHEMA_WS_REFRESH_TOKENS
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_DELETE_REFRESH_TOKEN,
|
||||
websocket_delete_refresh_token,
|
||||
SCHEMA_WS_DELETE_REFRESH_TOKEN
|
||||
)
|
||||
|
||||
await login_flow.async_setup(hass, store_result)
|
||||
await mfa_setup_flow.async_setup(hass)
|
||||
@ -445,3 +468,40 @@ def websocket_create_long_lived_access_token(
|
||||
|
||||
hass.async_create_task(
|
||||
async_create_long_lived_access_token(connection.user))
|
||||
|
||||
|
||||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_refresh_tokens(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
"""Return metadata of users refresh tokens."""
|
||||
connection.to_write.put_nowait(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,
|
||||
} for refresh in connection.user.refresh_tokens.values()]))
|
||||
|
||||
|
||||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_delete_refresh_token(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
"""Handle a delete refresh token request."""
|
||||
async def async_delete_refresh_token(user, refresh_token_id):
|
||||
"""Delete a refresh token."""
|
||||
refresh_token = connection.user.refresh_tokens.get(refresh_token_id)
|
||||
|
||||
if refresh_token is None:
|
||||
return websocket_api.error_message(
|
||||
msg['id'], 'invalid_token_id', 'Received invalid token')
|
||||
|
||||
await hass.auth.async_remove_refresh_token(refresh_token)
|
||||
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(msg['id'], {}))
|
||||
|
||||
hass.async_create_task(
|
||||
async_delete_refresh_token(connection.user, msg['refresh_token_id']))
|
||||
|
@ -2,18 +2,15 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import const
|
||||
from homeassistant.auth import auth_manager_from_config
|
||||
from homeassistant.auth.models import Credentials
|
||||
from homeassistant.components.auth import RESULT_TYPE_USER
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.components import auth
|
||||
|
||||
from . import async_setup_auth
|
||||
from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser
|
||||
|
||||
from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser, \
|
||||
ensure_auth_manager_loaded
|
||||
from . import async_setup_auth
|
||||
|
||||
|
||||
async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client):
|
||||
@ -272,28 +269,12 @@ async def test_revoking_refresh_token(hass, aiohttp_client):
|
||||
assert resp.status == 400
|
||||
|
||||
|
||||
async def test_ws_long_lived_access_token(hass, hass_ws_client):
|
||||
async def test_ws_long_lived_access_token(hass, hass_ws_client,
|
||||
hass_access_token):
|
||||
"""Test generate long-lived access token."""
|
||||
hass.auth = await auth_manager_from_config(
|
||||
hass, provider_configs=[{
|
||||
'type': 'insecure_example',
|
||||
'users': [{
|
||||
'username': 'test-user',
|
||||
'password': 'test-pass',
|
||||
'name': 'Test Name',
|
||||
}]
|
||||
}], module_configs=[])
|
||||
ensure_auth_manager_loaded(hass.auth)
|
||||
assert await async_setup_component(hass, 'auth', {'http': {}})
|
||||
assert await async_setup_component(hass, 'api', {'http': {}})
|
||||
|
||||
user = MockUser(id='mock-user').add_to_hass(hass)
|
||||
cred = await hass.auth.auth_providers[0].async_get_or_create_credentials(
|
||||
{'username': 'test-user'})
|
||||
await hass.auth.async_link_user(user, cred)
|
||||
|
||||
ws_client = await hass_ws_client(hass, hass.auth.async_create_access_token(
|
||||
await hass.auth.async_create_refresh_token(user, CLIENT_ID)))
|
||||
ws_client = await hass_ws_client(hass, hass_access_token)
|
||||
|
||||
# verify create long-lived access token
|
||||
await ws_client.send_json({
|
||||
@ -315,12 +296,51 @@ async def test_ws_long_lived_access_token(hass, hass_ws_client):
|
||||
assert refresh_token.client_name == 'GPS Logger'
|
||||
assert refresh_token.client_icon is None
|
||||
|
||||
# verify long-lived access token can be used as bearer token
|
||||
api_client = ws_client.client
|
||||
resp = await api_client.get(const.URL_API)
|
||||
assert resp.status == 401
|
||||
|
||||
resp = await api_client.get(const.URL_API, headers={
|
||||
'Authorization': 'Bearer {}'.format(long_lived_access_token)
|
||||
async def test_ws_refresh_tokens(hass, hass_ws_client, hass_access_token):
|
||||
"""Test fetching refresh token metadata."""
|
||||
assert await async_setup_component(hass, 'auth', {'http': {}})
|
||||
|
||||
ws_client = await hass_ws_client(hass, hass_access_token)
|
||||
|
||||
await ws_client.send_json({
|
||||
'id': 5,
|
||||
'type': auth.WS_TYPE_REFRESH_TOKENS,
|
||||
})
|
||||
assert resp.status == 200
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result['success'], result
|
||||
assert len(result['result']) == 1
|
||||
token = result['result'][0]
|
||||
refresh_token = await hass.auth.async_validate_access_token(
|
||||
hass_access_token)
|
||||
assert token['id'] == refresh_token.id
|
||||
assert token['type'] == refresh_token.token_type
|
||||
assert token['client_id'] == refresh_token.client_id
|
||||
assert token['client_name'] == refresh_token.client_name
|
||||
assert token['client_icon'] == refresh_token.client_icon
|
||||
assert token['created_at'] == refresh_token.created_at.isoformat()
|
||||
|
||||
|
||||
async def test_ws_delete_refresh_token(hass, hass_ws_client,
|
||||
hass_access_token):
|
||||
"""Test deleting a refresh token."""
|
||||
assert await async_setup_component(hass, 'auth', {'http': {}})
|
||||
|
||||
refresh_token = await hass.auth.async_validate_access_token(
|
||||
hass_access_token)
|
||||
|
||||
ws_client = await hass_ws_client(hass, hass_access_token)
|
||||
|
||||
# verify create long-lived access token
|
||||
await ws_client.send_json({
|
||||
'id': 5,
|
||||
'type': auth.WS_TYPE_DELETE_REFRESH_TOKEN,
|
||||
'refresh_token_id': refresh_token.id
|
||||
})
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result['success'], result
|
||||
refresh_token = await hass.auth.async_validate_access_token(
|
||||
hass_access_token)
|
||||
assert refresh_token is None
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Fixtures for component testing."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.setup import async_setup_component
|
||||
@ -16,23 +18,37 @@ def hass_ws_client(aiohttp_client):
|
||||
assert await async_setup_component(hass, 'websocket_api')
|
||||
|
||||
client = await aiohttp_client(hass.http.app)
|
||||
websocket = await client.ws_connect(wapi.URL)
|
||||
auth_resp = await websocket.receive_json()
|
||||
|
||||
if auth_resp['type'] == wapi.TYPE_AUTH_OK:
|
||||
assert access_token is None, \
|
||||
'Access token given but no auth required'
|
||||
return websocket
|
||||
patching = None
|
||||
|
||||
assert access_token is not None, 'Access token required for fixture'
|
||||
if access_token is not None:
|
||||
patching = patch('homeassistant.auth.AuthManager.active',
|
||||
return_value=True)
|
||||
patching.start()
|
||||
|
||||
await websocket.send_json({
|
||||
'type': websocket_api.TYPE_AUTH,
|
||||
'access_token': access_token
|
||||
})
|
||||
try:
|
||||
websocket = await client.ws_connect(wapi.URL)
|
||||
auth_resp = await websocket.receive_json()
|
||||
|
||||
auth_ok = await websocket.receive_json()
|
||||
assert auth_ok['type'] == wapi.TYPE_AUTH_OK
|
||||
if auth_resp['type'] == wapi.TYPE_AUTH_OK:
|
||||
assert access_token is None, \
|
||||
'Access token given but no auth required'
|
||||
return websocket
|
||||
|
||||
assert access_token is not None, \
|
||||
'Access token required for fixture'
|
||||
|
||||
await websocket.send_json({
|
||||
'type': websocket_api.TYPE_AUTH,
|
||||
'access_token': access_token
|
||||
})
|
||||
|
||||
auth_ok = await websocket.receive_json()
|
||||
assert auth_ok['type'] == wapi.TYPE_AUTH_OK
|
||||
|
||||
finally:
|
||||
if patching is not None:
|
||||
patching.stop()
|
||||
|
||||
# wrap in client
|
||||
websocket.client = client
|
||||
|
Loading…
x
Reference in New Issue
Block a user