Skip to content

Commit 683cdf0

Browse files
committed
Allow openid for integration tests
1 parent 8941286 commit 683cdf0

3 files changed

Lines changed: 112 additions & 6 deletions

File tree

src/isar/apis/security/authentication.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from fastapi import Depends
55
from fastapi.security.base import SecurityBase
66
from fastapi_azure_auth import SingleTenantAzureAuthorizationCodeBearer
7+
from fastapi_azure_auth.auth import AzureAuthorizationCodeBearerBase
78
from fastapi_azure_auth.exceptions import InvalidAuthHttp
89
from fastapi_azure_auth.user import User
910
from pydantic import BaseModel
@@ -21,13 +22,47 @@ def __init__(self) -> None:
2122
self.scheme_name = "No Security"
2223

2324

24-
azure_scheme = SingleTenantAzureAuthorizationCodeBearer(
25-
app_client_id=settings.AZURE_CLIENT_ID,
26-
tenant_id=settings.AZURE_TENANT_ID,
27-
scopes={
25+
def build_azure_scheme() -> AzureAuthorizationCodeBearerBase:
26+
"""
27+
Build the security scheme used to validate access tokens.
28+
29+
By default this is a single tenant Azure Entra ID scheme. If
30+
``settings.OPENID_CONFIG_URL`` is set, the OpenID Connect discovery document is
31+
read from that URL instead, which allows ISAR to be pointed at a different
32+
OpenID provider such as a local mock issuer used by the integration tests.
33+
34+
``SingleTenantAzureAuthorizationCodeBearer`` does not accept an
35+
``openid_config_url`` argument, so the base class is used directly in that case.
36+
Issuer validation remains enabled either way; the expected issuer is taken from
37+
the discovery document.
38+
39+
Returns
40+
-------
41+
AzureAuthorizationCodeBearerBase
42+
The configured security scheme.
43+
"""
44+
scopes: dict[str, str] = {
2845
f"api://{settings.AZURE_CLIENT_ID}/user_impersonation": "user_impersonation",
29-
},
30-
)
46+
}
47+
48+
if settings.OPENID_CONFIG_URL:
49+
return AzureAuthorizationCodeBearerBase(
50+
app_client_id=settings.AZURE_CLIENT_ID,
51+
tenant_id=settings.AZURE_TENANT_ID,
52+
scopes=scopes,
53+
openid_config_url=settings.OPENID_CONFIG_URL,
54+
openapi_authorization_url=settings.OPENAPI_AUTHORIZATION_URL,
55+
openapi_token_url=settings.OPENAPI_TOKEN_URL,
56+
)
57+
58+
return SingleTenantAzureAuthorizationCodeBearer(
59+
app_client_id=settings.AZURE_CLIENT_ID,
60+
tenant_id=settings.AZURE_TENANT_ID,
61+
scopes=scopes,
62+
)
63+
64+
65+
azure_scheme: AzureAuthorizationCodeBearerBase = build_azure_scheme()
3166

3267

3368
async def validate_has_role(user: User = Depends(azure_scheme)) -> None:

src/isar/config/settings.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,19 @@ class Settings(BaseSettings):
125125
# ChainedTokenCredential (e.g. "WorkloadIdentity,ClientSecret").
126126
ALLOWED_AUTH_METHODS: str = Field(default="ClientSecret")
127127

128+
# Override the OpenID Connect discovery document URL used to validate access
129+
# tokens. When unset (the default), the URL is derived from AZURE_TENANT_ID and
130+
# points at Azure Entra ID. Set this to point ISAR at a different OpenID
131+
# provider, such as a local mock issuer used by the integration tests.
132+
OPENID_CONFIG_URL: str | None = Field(default=None)
133+
134+
# Override the authorization and token URLs advertised in the generated OpenAPI
135+
# document, so that Swagger's "Authorize" button does not point at
136+
# login.microsoftonline.com when a non-Azure issuer is configured. Only affect
137+
# the API documentation; token validation is governed by OPENID_CONFIG_URL.
138+
OPENAPI_AUTHORIZATION_URL: str | None = Field(default=None)
139+
OPENAPI_TOKEN_URL: str | None = Field(default=None)
140+
128141
# MQTT username
129142
# The username and password is set by the MQTT broker and must be known in advance
130143
# The password should be set as an environment variable "MQTT_PASSWORD"

tests/isar/apis/security/test_authentication.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
import jwt
44
import pytest
55
from fastapi.testclient import TestClient
6+
from fastapi_azure_auth import SingleTenantAzureAuthorizationCodeBearer
7+
from fastapi_azure_auth.auth import AzureAuthorizationCodeBearerBase
8+
from pytest import MonkeyPatch
9+
10+
from isar.apis.security.authentication import build_azure_scheme
11+
from isar.config.settings import settings
612

713

814
def stub_access_token() -> str:
@@ -30,3 +36,55 @@ def test_authentication(
3036
)
3137

3238
assert response.status_code == expected_status_code
39+
40+
41+
class TestBuildAzureScheme:
42+
def test_defaults_to_single_tenant_azure_scheme(
43+
self, monkeypatch: MonkeyPatch
44+
) -> None:
45+
monkeypatch.setattr(settings, "OPENID_CONFIG_URL", None)
46+
47+
scheme = build_azure_scheme()
48+
49+
assert isinstance(scheme, SingleTenantAzureAuthorizationCodeBearer)
50+
# No override, so the discovery document URL is derived from the tenant ID
51+
# and points at Azure Entra ID.
52+
assert scheme.openid_config.config_url is None
53+
assert scheme.openid_config.tenant_id == settings.AZURE_TENANT_ID
54+
assert scheme.app_client_id == settings.AZURE_CLIENT_ID
55+
56+
def test_openid_config_url_is_honoured(self, monkeypatch: MonkeyPatch) -> None:
57+
config_url = "http://oauth-mock:8080/.well-known/openid-configuration"
58+
authorization_url = "http://oauth-mock:8080/authorize"
59+
token_url = "http://oauth-mock:8080/token"
60+
61+
monkeypatch.setattr(settings, "OPENID_CONFIG_URL", config_url)
62+
monkeypatch.setattr(settings, "OPENAPI_AUTHORIZATION_URL", authorization_url)
63+
monkeypatch.setattr(settings, "OPENAPI_TOKEN_URL", token_url)
64+
65+
scheme = build_azure_scheme()
66+
67+
assert not isinstance(scheme, SingleTenantAzureAuthorizationCodeBearer)
68+
assert isinstance(scheme, AzureAuthorizationCodeBearerBase)
69+
assert scheme.openid_config.config_url == config_url
70+
assert scheme.authorization_url == authorization_url
71+
assert scheme.token_url == token_url
72+
# The audience is still ISAR's own client ID, and issuer validation stays on.
73+
assert scheme.app_client_id == settings.AZURE_CLIENT_ID
74+
assert scheme.validate_iss is True
75+
76+
def test_openapi_urls_fall_back_to_azure_when_unset(
77+
self, monkeypatch: MonkeyPatch
78+
) -> None:
79+
monkeypatch.setattr(
80+
settings,
81+
"OPENID_CONFIG_URL",
82+
"http://oauth-mock:8080/.well-known/openid-configuration",
83+
)
84+
monkeypatch.setattr(settings, "OPENAPI_AUTHORIZATION_URL", None)
85+
monkeypatch.setattr(settings, "OPENAPI_TOKEN_URL", None)
86+
87+
scheme = build_azure_scheme()
88+
89+
assert scheme.authorization_url is not None
90+
assert settings.AZURE_TENANT_ID in scheme.authorization_url

0 commit comments

Comments
 (0)