async def test_async_user_not_allowed_do_auth(hass, app): """Test for not allowing auth.""" user = await hass.auth.async_create_user("Hello") user.is_active = False # User not active assert async_user_not_allowed_do_auth(hass, user) == "User is not active" user.is_active = True user.local_only = True # No current request assert (async_user_not_allowed_do_auth( hass, user) == "No request available to validate local access") trusted_request = Mock(remote="192.168.1.123") untrusted_request = Mock(remote=UNTRUSTED_ADDRESSES[0]) # Is Remote IP and local only (cloud not loaded) assert async_user_not_allowed_do_auth(hass, user, trusted_request) is None assert (async_user_not_allowed_do_auth( hass, user, untrusted_request) == "User cannot authenticate remotely") # Mimic cloud loaded and validate local IP again hass.config.components.add("cloud") assert async_user_not_allowed_do_auth(hass, user, trusted_request) is None assert (async_user_not_allowed_do_auth( hass, user, untrusted_request) == "User cannot authenticate remotely") # Is Cloud request and local only, even a local IP will fail with patch("hass_nabucasa.remote.is_cloud_request", Mock(get=Mock(return_value=True))): assert (async_user_not_allowed_do_auth( hass, user, trusted_request) == "User is local only")
class LoginFlowBaseView(HomeAssistantView): """Base class for the login views.""" requires_auth = False def __init__( self, flow_mgr: AuthManagerFlowManager, store_result: StoreResultType, ) -> None: """Initialize the flow manager index view.""" self._flow_mgr = flow_mgr self._store_result = store_result async def _async_flow_result_to_response( self, request: web.Request, client_id: str, result: data_entry_flow.FlowResult, ) -> web.Response: """Convert the flow result to a response.""" if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: # @log_invalid_auth does not work here since it returns HTTP 200. # We need to manually log failed login attempts. if (result["type"] == data_entry_flow.FlowResultType.FORM and (errors := result.get("errors")) and errors.get("base") in ( "invalid_auth", "invalid_code", )): await process_wrong_login(request) return self.json(_prepare_result_json(result)) hass: HomeAssistant = request.app["hass"] if not await indieauth.verify_redirect_uri( hass, client_id, result["context"]["redirect_uri"]): return self.json_message("Invalid redirect URI", HTTPStatus.FORBIDDEN) result.pop("data") result.pop("context") result_obj: Credentials = result.pop("result") # Result can be None if credential was never linked to a user before. user = await hass.auth.async_get_user_by_credentials(result_obj) if user is not None and (user_access_error := async_user_not_allowed_do_auth(hass, user)): return self.json_message(f"Login blocked: {user_access_error}", HTTPStatus.FORBIDDEN)
) credential = self._retrieve_auth(client_id, code) if credential is None or not isinstance(credential, Credentials): return self.json( { "error": "invalid_request", "error_description": "Invalid code" }, status_code=HTTPStatus.BAD_REQUEST, ) user = await hass.auth.async_get_or_create_user(credential) if user_access_error := async_user_not_allowed_do_auth(hass, user): return self.json( { "error": "access_denied", "error_description": user_access_error, }, status_code=HTTPStatus.FORBIDDEN, ) 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(
class TokenView(HomeAssistantView): """View to issue tokens.""" url = "/auth/token" name = "api:auth:token" requires_auth = False cors_allowed = True def __init__(self, retrieve_auth: RetrieveResultType) -> None: """Initialize the token view.""" self._retrieve_auth = retrieve_auth @log_invalid_auth async def post(self, request: web.Request) -> web.Response: """Grant a token.""" hass: HomeAssistant = request.app["hass"] data = cast(MultiDictProxy[str], await request.post()) grant_type = data.get("grant_type") # IndieAuth 6.3.5 # The revocation endpoint is the same as the token endpoint. # The revocation request includes an additional parameter, # action=revoke. if data.get("action") == "revoke": # action=revoke is deprecated. Use /auth/revoke instead. # Keep here for backwards compat return await RevokeTokenView.post(self, request ) # type: ignore[arg-type] if grant_type == "authorization_code": return await self._async_handle_auth_code(hass, data, request.remote) if grant_type == "refresh_token": return await self._async_handle_refresh_token( hass, data, request.remote) return self.json({"error": "unsupported_grant_type"}, status_code=HTTPStatus.BAD_REQUEST) async def _async_handle_auth_code( self, hass: HomeAssistant, data: MultiDictProxy[str], remote_addr: str | None, ) -> web.Response: """Handle authorization code request.""" client_id = data.get("client_id") if client_id is None or not indieauth.verify_client_id(client_id): return self.json( { "error": "invalid_request", "error_description": "Invalid client id" }, status_code=HTTPStatus.BAD_REQUEST, ) if (code := data.get("code")) is None: return self.json( { "error": "invalid_request", "error_description": "Invalid code" }, status_code=HTTPStatus.BAD_REQUEST, ) credential = self._retrieve_auth(client_id, code) if credential is None or not isinstance(credential, Credentials): return self.json( { "error": "invalid_request", "error_description": "Invalid code" }, status_code=HTTPStatus.BAD_REQUEST, ) user = await hass.auth.async_get_or_create_user(credential) if user_access_error := async_user_not_allowed_do_auth(hass, user): return self.json( { "error": "access_denied", "error_description": user_access_error, }, status_code=HTTPStatus.FORBIDDEN, )