Skip to content

Commit 51a0afa

Browse files
authored
Merge pull request #44 from bkmetzler/auto_error
Implement `auto_error` flag - Resolves #43
2 parents 80cc7aa + b98bd11 commit 51a0afa

File tree

23 files changed

+804
-502
lines changed

23 files changed

+804
-502
lines changed

.github/workflows/testing.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
fail-fast: false
2929
matrix:
3030
python-version: [ "3.9", "3.10" ]
31-
fastapi-version: [ "0.68.0", "0.70.0" ]
31+
fastapi-version: [ "0.68.0", "0.70.0", "0.71.0" ]
3232
steps:
3333
- name: Check out repository
3434
uses: actions/checkout@v2

.pre-commit-config.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ repos:
4141
hooks:
4242
- id: isort
4343
- repo: https://github.com/pre-commit/mirrors-mypy
44-
rev: v0.910
44+
rev: "v0.930"
4545
hooks:
4646
- id: mypy
47+
exclude: "test_*"
48+
additional_dependencies:
49+
[
50+
fastapi,
51+
pydantic,
52+
starlette,
53+
]

demo_project/api/api_v1/api.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from demo_project.api.api_v1.endpoints import hello_world
1+
from demo_project.api.api_v1.endpoints import hello_world, hello_world_multi_auth
22
from fastapi import APIRouter
33

4-
api_router = APIRouter(tags=['hello'])
5-
api_router.include_router(hello_world.router)
4+
api_router_azure_auth = APIRouter(tags=['hello'])
5+
api_router_azure_auth.include_router(hello_world.router)
6+
api_router_multi_auth = APIRouter(tags=['hello'])
7+
api_router_multi_auth.include_router(hello_world_multi_auth.router)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from typing import Union
2+
3+
from demo_project.api.dependencies import multi_auth
4+
from demo_project.schemas.hello_world import TokenType
5+
from fastapi import APIRouter, Depends, Request
6+
7+
from fastapi_azure_auth.user import User
8+
9+
router = APIRouter()
10+
11+
12+
@router.get(
13+
'/hello-multi-auth',
14+
response_model=TokenType,
15+
summary='Say hello with an API key',
16+
name='hello_world_api_key',
17+
operation_id='helloWorldApiKey',
18+
)
19+
async def world(request: Request, auth: Union[str, User] = Depends(multi_auth)) -> dict[str, bool]:
20+
"""
21+
Wonder how this auth is done?
22+
"""
23+
if isinstance(auth, str):
24+
# An API key was used
25+
return {'api_key': True, 'azure_auth': False}
26+
return {'api_key': False, 'azure_auth': True}

demo_project/api/dependencies.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import logging
22
from datetime import datetime, timedelta
3-
from typing import Optional
3+
from typing import Optional, Union
44

55
from demo_project.core.config import settings
66
from fastapi import Depends
7+
from fastapi.security.api_key import APIKeyHeader
78

8-
from fastapi_azure_auth import SingleTenantAzureAuthorizationCodeBearer
9+
from fastapi_azure_auth import MultiTenantAzureAuthorizationCodeBearer, SingleTenantAzureAuthorizationCodeBearer
910
from fastapi_azure_auth.exceptions import InvalidAuth
1011
from fastapi_azure_auth.user import User
1112

@@ -49,10 +50,40 @@ async def __call__(self, tid: str) -> str:
4950
# logic to find your allowed tenants and it's issuers here
5051
# (This example cache in memory for 1 hour)
5152
self.tid_to_iss = {
52-
'intility_tenant': 'intility_tenant',
53+
'intility_tenant_id': 'https://login.microsoftonline.com/intility_tenant/v2.0',
5354
}
5455
try:
5556
return self.tid_to_iss[tid]
5657
except Exception as error:
5758
log.exception('`iss` not found for `tid` %s. Error %s', tid, error)
5859
raise InvalidAuth('You must be an Intility customer to access this resource')
60+
61+
62+
issuer_fetcher = IssuerFetcher()
63+
64+
azure_scheme_auto_error_false = MultiTenantAzureAuthorizationCodeBearer(
65+
app_client_id=settings.APP_CLIENT_ID,
66+
scopes={
67+
f'api://{settings.APP_CLIENT_ID}/user_impersonation': 'User impersonation',
68+
},
69+
validate_iss=True,
70+
iss_callable=issuer_fetcher,
71+
auto_error=False,
72+
)
73+
74+
75+
api_key_auth_auto_error_false = APIKeyHeader(name='TEST-API-KEY', auto_error=False)
76+
77+
78+
async def multi_auth(
79+
azure_auth: Optional[User] = Depends(azure_scheme_auto_error_false),
80+
api_key: Optional[str] = Depends(api_key_auth_auto_error_false),
81+
) -> Union[User, str]:
82+
"""
83+
Example implementation.
84+
"""
85+
if azure_auth:
86+
return azure_auth
87+
if api_key == 'JonasIsCool':
88+
return api_key
89+
raise InvalidAuth('You must either provide a valid bearer token or API key')

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'

demo_project/main.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from argparse import ArgumentParser
33

44
import uvicorn
5-
from demo_project.api.api_v1.api import api_router
5+
from demo_project.api.api_v1.api import api_router_azure_auth, api_router_multi_auth
66
from demo_project.api.dependencies import azure_scheme
77
from demo_project.core.config import settings
88
from fastapi import FastAPI, Security
@@ -44,10 +44,15 @@ async def load_config() -> None:
4444

4545

4646
app.include_router(
47-
api_router,
47+
api_router_azure_auth,
4848
prefix=settings.API_V1_STR,
4949
dependencies=[Security(azure_scheme, scopes=['user_impersonation'])],
5050
)
51+
app.include_router(
52+
api_router_multi_auth,
53+
prefix=settings.API_V1_STR,
54+
# Dependencies specified on the API itself
55+
)
5156

5257

5358
if __name__ == '__main__':

demo_project/schemas/hello_world.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@
66
class HelloWorldResponse(BaseModel):
77
hello: str = Field(..., description='What we\'re saying hello to')
88
user: User = Field(..., description='The user object')
9+
10+
11+
class TokenType(BaseModel):
12+
api_key: bool = Field(..., description='API key was used')
13+
azure_auth: bool = Field(..., description='Azure auth was used')

docs/docs/settings/multi_tenant.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,11 @@ Override OpenAPI token URL
6363
**Default:** `None`
6464

6565
Override OpenAPI description
66+
67+
-----------------
68+
69+
### auto_error: `bool`
70+
**Default:** `True`
71+
72+
Set this to False if you are using multiple authentication libraries. This will return rather than
73+
throwing authentication exceptions.

docs/docs/settings/single_tenant.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,11 @@ Override OpenAPI token URL
6363
**Default:** `None`
6464

6565
Override OpenAPI description
66+
67+
-----------------
68+
69+
### auto_error: `bool`
70+
**Default:** `True`
71+
72+
Set this to False if you are using multiple authentication libraries. This will return rather than
73+
throwing authentication exceptions.

0 commit comments

Comments
 (0)