-
-
Notifications
You must be signed in to change notification settings - Fork 31.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
210 additions
and
163 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
"""HTTP views handle login flow. | ||
# GET /auth/providers | ||
Return a list of auth providers. Example: | ||
[ | ||
{ | ||
"name": "Local", | ||
"id": null, | ||
"type": "local_provider", | ||
} | ||
] | ||
# POST /auth/login_flow | ||
Create a login flow. Will return the first step of the flow. | ||
Pass in parameter 'client_id' and 'redirect_url' validate by indieauth. | ||
Pass in parameter 'handler' to specify the auth provider to use. Auth providers | ||
are identified by type and id. | ||
{ | ||
"client_id": "https://hassbian.local:8123/", | ||
"handler": ["local_provider", null], | ||
"redirect_url": "https://hassbian.local:8123/" | ||
} | ||
Return value will be a step in a data entry flow. See the docs for data entry | ||
flow for details. | ||
{ | ||
"data_schema": [ | ||
{"name": "username", "type": "string"}, | ||
{"name": "password", "type": "string"} | ||
], | ||
"errors": {}, | ||
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58", | ||
"handler": ["insecure_example", null], | ||
"step_id": "init", | ||
"type": "form" | ||
} | ||
# POST /auth/login_flow/{flow_id} | ||
Progress the flow. Most flows will be 1 page, but could optionally add extra | ||
login challenges, like TFA. Once the flow has finished, the returned step will | ||
have type "create_entry" and "result" key will contain an authorization code. | ||
{ | ||
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58", | ||
"handler": ["insecure_example", null], | ||
"result": "411ee2f916e648d691e937ae9344681e", | ||
"source": "user", | ||
"title": "Example", | ||
"type": "create_entry", | ||
"version": 1 | ||
} | ||
""" | ||
import aiohttp.web | ||
import voluptuous as vol | ||
|
||
from homeassistant import data_entry_flow | ||
from homeassistant.components.http.ban import process_wrong_login, \ | ||
log_invalid_auth | ||
from homeassistant.components.http.data_validator import RequestDataValidator | ||
from homeassistant.components.http.view import HomeAssistantView | ||
from homeassistant.helpers.data_entry_flow import ( | ||
FlowManagerIndexView, FlowManagerResourceView) | ||
from . import indieauth | ||
|
||
|
||
async def async_setup(hass, store_credentials): | ||
"""Component to allow users to login.""" | ||
hass.http.register_view(AuthProvidersView) | ||
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow)) | ||
hass.http.register_view( | ||
LoginFlowResourceView(hass.auth.login_flow, store_credentials)) | ||
|
||
|
||
class AuthProvidersView(HomeAssistantView): | ||
"""View to get available auth providers.""" | ||
|
||
url = '/auth/providers' | ||
name = 'api:auth:providers' | ||
requires_auth = False | ||
|
||
async def get(self, request): | ||
"""Get available auth providers.""" | ||
return self.json([{ | ||
'name': provider.name, | ||
'id': provider.id, | ||
'type': provider.type, | ||
} for provider in request.app['hass'].auth.auth_providers]) | ||
|
||
|
||
class LoginFlowIndexView(FlowManagerIndexView): | ||
"""View to create a config flow.""" | ||
|
||
url = '/auth/login_flow' | ||
name = 'api:auth:login_flow' | ||
requires_auth = False | ||
|
||
async def get(self, request): | ||
"""Do not allow index of flows in progress.""" | ||
return aiohttp.web.Response(status=405) | ||
|
||
@RequestDataValidator(vol.Schema({ | ||
vol.Required('client_id'): str, | ||
vol.Required('handler'): vol.Any(str, list), | ||
vol.Required('redirect_uri'): str, | ||
})) | ||
@log_invalid_auth | ||
async def post(self, request, data): | ||
"""Create a new login flow.""" | ||
if not indieauth.verify_redirect_uri(data['client_id'], | ||
data['redirect_uri']): | ||
return self.json_message('invalid client id or redirect uri', 400) | ||
|
||
# pylint: disable=no-value-for-parameter | ||
return await super().post(request) | ||
|
||
|
||
class LoginFlowResourceView(FlowManagerResourceView): | ||
"""View to interact with the flow manager.""" | ||
|
||
url = '/auth/login_flow/{flow_id}' | ||
name = 'api:auth:login_flow:resource' | ||
requires_auth = False | ||
|
||
def __init__(self, flow_mgr, store_credentials): | ||
"""Initialize the login flow resource view.""" | ||
super().__init__(flow_mgr) | ||
self._store_credentials = store_credentials | ||
|
||
async def get(self, request, flow_id): | ||
"""Do not allow getting status of a flow in progress.""" | ||
return self.json_message('Invalid flow specified', 404) | ||
|
||
@RequestDataValidator(vol.Schema({ | ||
'client_id': str | ||
}, extra=vol.ALLOW_EXTRA)) | ||
@log_invalid_auth | ||
async def post(self, request, flow_id, data): | ||
"""Handle progressing a login flow request.""" | ||
client_id = data.pop('client_id') | ||
|
||
if not indieauth.verify_client_id(client_id): | ||
return self.json_message('Invalid client id', 400) | ||
|
||
try: | ||
result = await self._flow_mgr.async_configure(flow_id, data) | ||
except data_entry_flow.UnknownFlow: | ||
return self.json_message('Invalid flow specified', 404) | ||
except vol.Invalid: | ||
return self.json_message('User input malformed', 400) | ||
|
||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: | ||
# @log_invalid_auth does not work here since it returns HTTP 200 | ||
# need manually log failed login attempts | ||
if result['errors'] is not None and \ | ||
result['errors'].get('base') == 'invalid_auth': | ||
await process_wrong_login(request) | ||
return self.json(self._prepare_result_json(result)) | ||
|
||
result.pop('data') | ||
result['result'] = self._store_credentials(client_id, result['result']) | ||
|
||
return self.json(result) | ||
|
||
|
Oops, something went wrong.