Skip to content
Snippets Groups Projects
Commit 0db13a99 authored by Paulus Schoutsen's avatar Paulus Schoutsen Committed by Jason Hu
Browse files

Add websocket commands for refresh tokens (#16559)

* Add websocket commands for refresh tokens

* Comment
parent 4e3faf61
No related branches found
No related tags found
No related merge requests found
......@@ -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
"""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
if access_token is not None:
patching = patch('homeassistant.auth.AuthManager.active',
return_value=True)
patching.start()
try:
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
assert access_token is not None, \
'Access token required for fixture'
assert access_token is not None, 'Access token required for fixture'
await websocket.send_json({
'type': websocket_api.TYPE_AUTH,
'access_token': access_token
})
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
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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment