Skip to content

Commit ab80c48

Browse files
committed
Add tests for auto_error=False
1 parent 68d2686 commit ab80c48

File tree

11 files changed

+67
-41
lines changed

11 files changed

+67
-41
lines changed

demo_project/api/dependencies.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from fastapi import Depends
77
from fastapi.security.api_key import APIKeyHeader
88

9-
from fastapi_azure_auth import SingleTenantAzureAuthorizationCodeBearer
9+
from fastapi_azure_auth import MultiTenantAzureAuthorizationCodeBearer, SingleTenantAzureAuthorizationCodeBearer
1010
from fastapi_azure_auth.exceptions import InvalidAuth
1111
from fastapi_azure_auth.user import User
1212

@@ -50,7 +50,7 @@ async def __call__(self, tid: str) -> str:
5050
# logic to find your allowed tenants and it's issuers here
5151
# (This example cache in memory for 1 hour)
5252
self.tid_to_iss = {
53-
'intility_tenant': 'intility_tenant',
53+
'intility_tenant_id': 'https://login.microsoftonline.com/intility_tenant/v2.0',
5454
}
5555
try:
5656
return self.tid_to_iss[tid]
@@ -59,12 +59,15 @@ async def __call__(self, tid: str) -> str:
5959
raise InvalidAuth('You must be an Intility customer to access this resource')
6060

6161

62-
azure_scheme_auto_error_false = SingleTenantAzureAuthorizationCodeBearer(
62+
issuer_fetcher = IssuerFetcher()
63+
64+
azure_scheme_auto_error_false = MultiTenantAzureAuthorizationCodeBearer(
6365
app_client_id=settings.APP_CLIENT_ID,
6466
scopes={
65-
f'api://{settings.APP_CLIENT_ID}/user_impersonation': '**No client secret needed, leave blank**',
67+
f'api://{settings.APP_CLIENT_ID}/user_impersonation': 'User impersonation',
6668
},
67-
tenant_id=settings.TENANT_ID,
69+
validate_iss=True,
70+
iss_callable=issuer_fetcher,
6871
auto_error=False,
6972
)
7073

demo_project/core/config.py

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import List, Optional, Union
22

3-
from pydantic import AnyHttpUrl, BaseSettings, Field, HttpUrl, validator
3+
from pydantic import AnyHttpUrl, BaseSettings, Field, HttpUrl
44

55

66
class AzureActiveDirectory(BaseSettings):
@@ -18,29 +18,9 @@ class Settings(AzureActiveDirectory):
1818
# "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'
1919
BACKEND_CORS_ORIGINS: List[Union[str, AnyHttpUrl]] = ['http://localhost:8000']
2020

21-
@validator('BACKEND_CORS_ORIGINS', pre=True)
22-
def assemble_cors_origins(cls, value: Union[str, List[str]]) -> Union[List[str], str]: # pragma: no cover
23-
"""
24-
Validate cors list
25-
"""
26-
if isinstance(value, str) and not value.startswith('['):
27-
return [i.strip() for i in value.split(',')]
28-
elif isinstance(value, (list, str)):
29-
return value
30-
raise ValueError(value)
31-
3221
PROJECT_NAME: str = 'My Project'
3322
SENTRY_DSN: Optional[HttpUrl] = None
3423

35-
@validator('SENTRY_DSN', pre=True)
36-
def sentry_dsn_can_be_blank(cls, value: str) -> Optional[str]: # pragma: no cover
37-
"""
38-
Validate sentry DSN
39-
"""
40-
if not value:
41-
return None
42-
return value
43-
4424
class Config: # noqa
4525
env_file = 'demo_project/.env'
4626
env_file_encoding = 'utf-8'

fastapi_azure_auth/auth.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import asyncio
21
import inspect
32
import logging
43
from collections.abc import Callable
5-
from typing import Any, Literal, Optional
4+
from typing import Any, Awaitable, Literal, Optional
65

76
from fastapi.exceptions import HTTPException
87
from fastapi.security import OAuth2AuthorizationCodeBearer, SecurityScopes
@@ -27,7 +26,7 @@ def __init__(
2726
scopes: Optional[dict[str, str]] = None,
2827
multi_tenant: bool = False,
2928
validate_iss: bool = True,
30-
iss_callable: Optional[Callable[..., Any]] = None,
29+
iss_callable: Optional[Callable[[str], Awaitable[str]]] = None,
3130
token_version: Literal[1, 2] = 2,
3231
openid_config_use_app_id: bool = False,
3332
openapi_authorization_url: Optional[str] = None,
@@ -55,7 +54,7 @@ def __init__(
5554
:param validate_iss: bool
5655
**Only used for multi-tenant applications**
5756
Whether to validate the token `iss` (issuer) or not. This can be skipped to allow anyone to log in.
58-
:param iss_callable: Callable
57+
:param iss_callable: Async Callable
5958
**Only used for multi-tenant application**
6059
Async function that has to accept a `tid` (tenant ID) and return a `iss` (issuer) or
6160
raise an InvalidIssuer exception
@@ -81,8 +80,6 @@ def __init__(
8180
if multi_tenant:
8281
if validate_iss and not callable(iss_callable):
8382
raise RuntimeError('`validate_iss` is enabled, so you must provide an `iss_callable`')
84-
elif iss_callable and not asyncio.iscoroutinefunction(iss_callable):
85-
raise RuntimeError('`iss_callable` must be a coroutine')
8683
elif iss_callable and 'tid' not in inspect.signature(iss_callable).parameters.keys():
8784
raise RuntimeError('`iss_callable` must accept `tid` as an argument')
8885

@@ -277,7 +274,7 @@ def __init__(
277274
auto_error: bool = True,
278275
scopes: Optional[dict[str, str]] = None,
279276
validate_iss: bool = True,
280-
iss_callable: Optional[Callable[..., Any]] = None,
277+
iss_callable: Optional[Callable[[str], Awaitable[str]]] = None,
281278
openid_config_use_app_id: bool = False,
282279
openapi_authorization_url: Optional[str] = None,
283280
openapi_token_url: Optional[str] = None,
@@ -299,7 +296,7 @@ def __init__(
299296
300297
:param validate_iss: bool
301298
Whether to validate the token `iss` (issuer) or not. This can be skipped to allow anyone to log in.
302-
:param iss_callable: Callable
299+
:param iss_callable: Async Callable
303300
Async function that has to accept a `tid` (tenant ID) and return a `iss` (issuer) or
304301
raise an InvalidIssuer exception
305302
This is required when validate_iss is set to `True`.

tests/multi_tenant/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async def issuer_fetcher(tid):
2323
iss_callable=issuer_fetcher,
2424
)
2525
app.dependency_overrides[azure_scheme] = azure_scheme_overrides
26-
yield azure_scheme
26+
yield
2727

2828

2929
@pytest.fixture
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This folder contains tests to ensure that multiple authentication schemes will work together.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import pytest
2+
from demo_project.main import app
3+
from httpx import AsyncClient
4+
from tests.utils import build_access_token, build_access_token_expired
5+
6+
7+
@pytest.mark.anyio
8+
async def test_normal_azure_user_valid_token(multi_tenant_app, mock_openid_and_keys, freezer):
9+
access_token = build_access_token(version=2)
10+
async with AsyncClient(app=app, base_url='http://test', headers={'Authorization': 'Bearer ' + access_token}) as ac:
11+
response = await ac.get('api/v1/hello-multi-auth')
12+
assert response.json() == {'api_key': False, 'azure_auth': True}
13+
14+
15+
@pytest.mark.anyio
16+
async def test_api_key_valid_key(multi_tenant_app, mock_openid_and_keys, freezer):
17+
async with AsyncClient(app=app, base_url='http://test', headers={'TEST-API-KEY': 'JonasIsCool'}) as ac:
18+
response = await ac.get('api/v1/hello-multi-auth')
19+
assert response.json() == {'api_key': True, 'azure_auth': False}
20+
21+
22+
@pytest.mark.anyio
23+
async def test_normal_azure_user_but_invalid_token(multi_tenant_app, mock_openid_and_keys, freezer):
24+
access_token = build_access_token_expired(version=2)
25+
async with AsyncClient(app=app, base_url='http://test', headers={'Authorization': 'Bearer ' + access_token}) as ac:
26+
response = await ac.get('api/v1/hello-multi-auth')
27+
assert response.json() == {'detail': 'You must either provide a valid bearer token or API key'}
28+
29+
30+
@pytest.mark.anyio
31+
async def test_api_key_but_invalid_key(multi_tenant_app, mock_openid_and_keys, freezer):
32+
async with AsyncClient(app=app, base_url='http://test', headers={'TEST-API-KEY': 'JonasIsNotCool'}) as ac:
33+
response = await ac.get('api/v1/hello-multi-auth')
34+
assert response.json() == {'detail': 'You must either provide a valid bearer token or API key'}

tests/multi_tenant/test_settings.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ async def iss_callable_do_not_accept_tid_argument(t):
77
pass
88

99

10-
@pytest.mark.parametrize(
11-
'iss_callable', [None, '', True, False, 1, lambda tid: True, iss_callable_do_not_accept_tid_argument]
12-
)
10+
@pytest.mark.parametrize('iss_callable', [None, '', True, False, 1, iss_callable_do_not_accept_tid_argument])
1311
def test_non_accepted_issue_fetcher_given(iss_callable):
1412
with pytest.raises(RuntimeError):
1513
MultiTenantAzureAuthorizationCodeBearer(app_client_id='some id', validate_iss=True, iss_callable=iss_callable)

tests/single_tenant/conftest.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ def single_tenant_app(token_version):
3434
token_version=1,
3535
)
3636
app.dependency_overrides[azure_scheme] = azure_scheme_overrides
37-
yield azure_scheme
3837
elif token_version == 2:
3938
azure_scheme_overrides = SingleTenantAzureAuthorizationCodeBearer(
4039
app_client_id=settings.APP_CLIENT_ID,
@@ -45,7 +44,7 @@ def single_tenant_app(token_version):
4544
token_version=2,
4645
)
4746
app.dependency_overrides[azure_scheme] = azure_scheme_overrides
48-
yield azure_scheme
47+
yield
4948

5049

5150
@pytest.fixture

0 commit comments

Comments
 (0)