Skip to content
This repository was archived by the owner on Apr 26, 2025. It is now read-only.

Commit ee71a93

Browse files
committed
Fix #16: handle sub-tenant prefix correctly
1 parent f4a5213 commit ee71a93

File tree

3 files changed

+112
-24
lines changed

3 files changed

+112
-24
lines changed

fief_client/client.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import uuid
44
from enum import Enum
55
from typing import Any, Dict, List, Mapping, Optional, Tuple, TypedDict, Union
6-
from urllib.parse import urlencode, urlsplit
6+
from urllib.parse import urlencode
77

88
import httpx
99
from httpx._types import CertTypes, VerifyTypes
@@ -253,8 +253,7 @@ def _get_endpoint_url(
253253
rather stick to the host specified on the client configuration.
254254
"""
255255
if not absolute:
256-
splitted_url = urlsplit(openid_configuration[field])
257-
return splitted_url.path
256+
return openid_configuration[field].split(self.base_url)[1]
258257
return openid_configuration[field]
259258

260259
def _auth_url(

tests/conftest.py

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import contextlib
12
import uuid
23
from datetime import datetime, timezone
34
from os import path
4-
from typing import Callable, Generator, List
5+
from typing import Callable, ContextManager, Generator, List, Protocol
56

67
import pytest
78
import pytest_asyncio
@@ -98,27 +99,47 @@ def encrypted_id_token(generate_token: Callable[..., str]) -> str:
9899
return generate_token(encrypt=True)
99100

100101

102+
class GetAPIRequestsMock(Protocol):
103+
def __call__(
104+
self, *, hostname: str = "https://bretagne.fief.dev", path_prefix: str = ""
105+
) -> ContextManager[respx.MockRouter]:
106+
...
107+
108+
109+
@pytest_asyncio.fixture(scope="module")
110+
def get_api_requests_mock(signature_key: jwk.JWK) -> GetAPIRequestsMock:
111+
@contextlib.contextmanager
112+
def _get_api_requests_mock(
113+
*, hostname: str = "https://bretagne.fief.dev", path_prefix: str = ""
114+
) -> Generator[respx.MockRouter, None, None]:
115+
with respx.mock(assert_all_mocked=True, assert_all_called=False) as respx_mock:
116+
openid_configuration_route = respx_mock.get(
117+
f"{path_prefix}/.well-known/openid-configuration"
118+
)
119+
openid_configuration_route.return_value = Response(
120+
200,
121+
json={
122+
"authorization_endpoint": f"{hostname}{path_prefix}/authorize",
123+
"token_endpoint": f"{hostname}{path_prefix}/token",
124+
"userinfo_endpoint": f"{hostname}{path_prefix}/userinfo",
125+
"jwks_uri": f"{hostname}{path_prefix}/.well-known/jwks.json",
126+
},
127+
)
128+
129+
jwks_route = respx_mock.get(f"{path_prefix}/.well-known/jwks.json")
130+
jwks_route.return_value = Response(
131+
200,
132+
json={"keys": [signature_key.export(private_key=False, as_dict=True)]},
133+
)
134+
135+
yield respx_mock
136+
137+
return _get_api_requests_mock
138+
139+
101140
@pytest_asyncio.fixture(scope="module", autouse=True)
102141
def mock_api_requests(
103-
signature_key: jwk.JWK,
142+
get_api_requests_mock: GetAPIRequestsMock,
104143
) -> Generator[respx.MockRouter, None, None]:
105-
HOSTNAME = "https://bretagne.fief.dev"
106-
107-
with respx.mock(assert_all_mocked=True, assert_all_called=False) as respx_mock:
108-
openid_configuration_route = respx_mock.get("/.well-known/openid-configuration")
109-
openid_configuration_route.return_value = Response(
110-
200,
111-
json={
112-
"authorization_endpoint": f"{HOSTNAME}/authorize",
113-
"token_endpoint": f"{HOSTNAME}/token",
114-
"userinfo_endpoint": f"{HOSTNAME}/userinfo",
115-
"jwks_uri": f"{HOSTNAME}/.well-known/jwks.json",
116-
},
117-
)
118-
119-
jwks_route = respx_mock.get("/.well-known/jwks.json")
120-
jwks_route.return_value = Response(
121-
200, json={"keys": [signature_key.export(private_key=False, as_dict=True)]}
122-
)
123-
144+
with get_api_requests_mock() as respx_mock:
124145
yield respx_mock

tests/test_client.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,19 @@
2424
FiefTokenResponse,
2525
)
2626
from fief_client.crypto import get_validation_hash
27+
from tests.conftest import GetAPIRequestsMock
2728

2829

2930
@pytest.fixture(scope="module")
3031
def fief_client() -> Fief:
3132
return Fief("https://bretagne.fief.dev", "CLIENT_ID", "CLIENT_SECRET")
3233

3334

35+
@pytest.fixture(scope="module")
36+
def fief_client_tenant() -> Fief:
37+
return Fief("https://bretagne.fief.dev/secondary", "CLIENT_ID", "CLIENT_SECRET")
38+
39+
3440
@pytest.fixture(scope="module")
3541
def fief_client_encryption_key(encryption_key: jwk.JWK) -> Fief:
3642
return Fief(
@@ -235,6 +241,30 @@ async def test_authorization_url_async(
235241

236242
assert request.url.host == request.headers["Host"]
237243

244+
def test_authorization_url_tenant(
245+
self, fief_client_tenant: Fief, mock_api_requests: respx.MockRouter
246+
):
247+
openid_configuration_route = mock_api_requests.get(
248+
"/secondary/.well-known/openid-configuration"
249+
)
250+
openid_configuration_route.return_value = Response(
251+
200,
252+
json={
253+
"authorization_endpoint": "https://bretagne.fief.dev/secondary/authorize",
254+
"token_endpoint": "https://bretagne.fief.dev/secondary/token",
255+
"userinfo_endpoint": "https://bretagne.fief.dev/secondary/userinfo",
256+
"jwks_uri": "https://bretagne.fief.dev/secondary/.well-known/jwks.json",
257+
},
258+
)
259+
260+
authorize_url = fief_client_tenant.auth_url(
261+
"https://www.bretagne.duchy/callback"
262+
)
263+
assert (
264+
authorize_url
265+
== "https://bretagne.fief.dev/secondary/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=https%3A%2F%2Fwww.bretagne.duchy%2Fcallback"
266+
)
267+
238268

239269
class TestAuthCallback:
240270
def test_error_response(
@@ -339,6 +369,44 @@ async def test_valid_response_async(
339369
assert isinstance(userinfo, dict)
340370
assert userinfo["sub"] == user_id
341371

372+
def test_valid_response_tenant(
373+
self,
374+
fief_client_tenant: Fief,
375+
get_api_requests_mock: GetAPIRequestsMock,
376+
access_token: str,
377+
signed_id_token: str,
378+
user_id: str,
379+
):
380+
with get_api_requests_mock(path_prefix="/secondary") as mock_api_requests:
381+
token_route = mock_api_requests.post("/secondary/token")
382+
token_route.return_value = Response(
383+
200,
384+
json={
385+
"access_token": access_token,
386+
"id_token": signed_id_token,
387+
"token_type": "bearer",
388+
},
389+
)
390+
391+
token_response, userinfo = fief_client_tenant.auth_callback(
392+
"CODE",
393+
"https://www.bretagne.duchy/callback",
394+
code_verifier="CODE_VERIFIER",
395+
)
396+
397+
token_route_call = token_route.calls.last
398+
assert token_route_call is not None
399+
400+
request_data = token_route_call.request.content.decode("utf-8")
401+
assert "client_id" in request_data
402+
assert "client_secret" in request_data
403+
404+
assert token_response["access_token"] == access_token
405+
assert token_response["id_token"] == signed_id_token
406+
407+
assert isinstance(userinfo, dict)
408+
assert userinfo["sub"] == user_id
409+
342410

343411
class TestAuthRefreshToken:
344412
def test_error_response(

0 commit comments

Comments
 (0)