mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Prefer external URL in WWW-Authenticate header for RFC 9728 (#169658)
This commit is contained in:
@@ -59,13 +59,17 @@ def request_handler_factory(
|
||||
# Import here to avoid circular dependency with network.py
|
||||
from .network import NoURLAvailableError, get_url # noqa: PLC0415
|
||||
|
||||
# Get the current request header to include as resource metadata
|
||||
# endpoint for RFC9728. We currently prefer external since this
|
||||
# is likely most used by remote OAuth clients
|
||||
try:
|
||||
url_prefix = get_url(hass, require_current_request=True)
|
||||
url_prefix = get_url(
|
||||
hass, require_current_request=True, prefer_external=True
|
||||
)
|
||||
except NoURLAvailableError:
|
||||
# Omit header to avoid leaking configured URLs
|
||||
raise HTTPUnauthorized from None
|
||||
raise HTTPUnauthorized(
|
||||
# Include resource metadata endpoint for RFC9728
|
||||
headers={
|
||||
"WWW-Authenticate": (
|
||||
f'Bearer resource_metadata="{url_prefix}'
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""Tests for Home Assistant View."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from decimal import Decimal
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import math
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from aiohttp import hdrs
|
||||
from aiohttp.web_exceptions import (
|
||||
HTTPBadRequest,
|
||||
HTTPInternalServerError,
|
||||
@@ -15,10 +17,12 @@ import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import KEY_HASS
|
||||
from homeassistant.components.http.request_context import current_request
|
||||
from homeassistant.components.http.view import (
|
||||
HomeAssistantView,
|
||||
request_handler_factory,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceNotFound, Unauthorized
|
||||
from homeassistant.helpers.network import NoURLAvailableError
|
||||
|
||||
@@ -143,3 +147,76 @@ async def test_requires_auth_omits_www_authenticate_without_url(
|
||||
AsyncMock(),
|
||||
)(mock_request)
|
||||
assert "WWW-Authenticate" not in exc_info.value.headers
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_current_request(
|
||||
mock_request: Mock, request_host: str, hass: HomeAssistant
|
||||
) -> Generator[Mock]:
|
||||
"""Set the current request context."""
|
||||
mock_request.get = Mock(return_value=False)
|
||||
mock_request.headers = {hdrs.HOST: request_host}
|
||||
mock_request.app = {KEY_HASS: hass}
|
||||
|
||||
token = current_request.set(mock_request)
|
||||
yield mock_request
|
||||
current_request.reset(token)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("internal_url", "external_url", "request_host", "expected_url"),
|
||||
[
|
||||
# Match either internal or external
|
||||
("https://foo.com", "https://example.com", "foo.com:18123", "https://foo.com"),
|
||||
("https://example.com", "https://foo.com", "foo.com:18123", "https://foo.com"),
|
||||
# Requests have a port and match external url
|
||||
# Note: We currently do not fully properly handle port matching for
|
||||
# internal urls. The tests here work because of prefer_external=True. We
|
||||
# can improve get_url so that additional cases where the internal url
|
||||
# have the same hostname work in future:
|
||||
# - Match request to internal url when external url has a port
|
||||
# - Match request to external url when internal url has a port
|
||||
(
|
||||
"https://foo.com",
|
||||
"https://foo.com:18123",
|
||||
"foo.com:18123",
|
||||
"https://foo.com:18123",
|
||||
),
|
||||
("https://foo.com:18123", "https://foo.com", "foo.com", "https://foo.com"),
|
||||
(
|
||||
"http://192.168.1.2:8123",
|
||||
"https://foo.com:18123",
|
||||
"192.168.1.2:8123",
|
||||
"http://192.168.1.2:8123",
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"request_host_matches_internal",
|
||||
"request_host_matches_external",
|
||||
"internal_no_port_request_external",
|
||||
"internal_port_request_external",
|
||||
"request_internal_distinct_host",
|
||||
],
|
||||
)
|
||||
async def test_requires_auth_www_authenticate_prefer_external(
|
||||
mock_current_request: Mock,
|
||||
hass: HomeAssistant,
|
||||
internal_url: str,
|
||||
external_url: str,
|
||||
expected_url: str,
|
||||
) -> None:
|
||||
"""Test that 401 responses include WWW-Authenticate header matching the requested URL."""
|
||||
hass.config.internal_url = internal_url
|
||||
hass.config.external_url = external_url
|
||||
|
||||
with pytest.raises(HTTPUnauthorized) as exc_info:
|
||||
await request_handler_factory(
|
||||
hass,
|
||||
Mock(requires_auth=True),
|
||||
AsyncMock(),
|
||||
)(mock_current_request)
|
||||
|
||||
assert exc_info.value.headers["WWW-Authenticate"] == (
|
||||
"Bearer resource_metadata="
|
||||
f'"{expected_url}/.well-known/oauth-protected-resource"'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user