Skip to content

Commit bace82a

Browse files
committed
improve: granular optional auth
1 parent 445e3cd commit bace82a

File tree

3 files changed

+81
-69
lines changed

3 files changed

+81
-69
lines changed

example/main.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from fastapi import Depends
55
from fastapi import FastAPI
66
from fastapi import HTTPException
7+
from fastapi import Security
78
from fastapi import status
89
from fastapi.middleware.cors import CORSMiddleware
910
from starlette.responses import RedirectResponse
@@ -14,12 +15,15 @@
1415
auth = Auth(
1516
openid_connect_url="http://localhost:8080/auth/realms/my-realm/.well-known/openid-configuration",
1617
issuer="http://localhost:8080/auth/realms/my-realm", # optional, verification only
17-
audience="my-audience", # optional, verification only
18-
auto_error=False, # optional
18+
# audience="my-audience", # optional, verification only
1919
idtoken_model=KeycloakIDToken, # optional
2020
)
2121

22-
app = FastAPI(title="Example", version="dev")
22+
app = FastAPI(
23+
title="Example",
24+
version="dev",
25+
dependencies=[Depends(auth.oidc_scheme)],
26+
)
2327

2428
# CORS errors instead of seeing internal exceptions
2529
# https://stackoverflow.com/questions/63606055/why-do-i-get-cors-error-reason-cors-request-did-not-succeed
@@ -37,16 +41,21 @@ def redirect_to_docs():
3741
return RedirectResponse(url="/docs")
3842

3943

40-
@app.get("/protected", dependencies=[Depends(auth.oidc_scheme)])
41-
def protected(id_token: Optional[KeycloakIDToken] = Depends(auth.authenticate_user)):
44+
@app.get("/protected")
45+
def protected(id_token: Optional[KeycloakIDToken] = Security(auth.authenticate_user())):
46+
print(id_token)
4247
if id_token is None:
4348
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
4449

4550
return dict(message=f"You are {id_token.email}")
4651

4752

48-
@app.get("/mixed", dependencies=[Depends(auth.oidc_scheme)])
49-
def mixed(id_token: Optional[KeycloakIDToken] = Depends(auth.authenticate_user)):
53+
@app.get("/mixed")
54+
def mixed(
55+
id_token: Optional[KeycloakIDToken] = Security(
56+
auth.authenticate_user(auto_error=False)
57+
),
58+
):
5059
if id_token is None:
5160
return dict(message="You are not authenticated")
5261
else:

fastapi_oidc/auth.py

Lines changed: 64 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,12 @@ def test_auth(authenticated_user: AuthenticatedUser = Depends(authenticate_user)
4040
from fastapi_oidc import discovery
4141
from fastapi_oidc.types import IDToken
4242

43+
# class AuthBearer(HTTPBearer):
44+
# async def __call__(self, request: Request):
45+
# return await super().__call__(request)
4346

44-
class AuthBearer(HTTPBearer):
45-
async def __call__(self, request: Request):
46-
return await super().__call__(request)
4747

48-
49-
class EmptyOAuth2(OAuth2):
48+
class OAuth2Facade(OAuth2):
5049
async def __call__(self, request: Request) -> Optional[str]:
5150
return None
5251

@@ -92,13 +91,15 @@ def __init__(
9291
)
9392

9493
self.oidc_scheme = OpenIdConnect(
95-
openIdConnectUrl=openid_connect_url, auto_error=auto_error
94+
openIdConnectUrl=openid_connect_url,
95+
auto_error=False,
9696
)
9797
self.password_scheme = OAuth2PasswordBearer(
9898
tokenUrl=self.discover.token_url(oidc_discoveries),
9999
scopes=scopes,
100+
auto_error=False,
100101
)
101-
self.implicit_scheme = EmptyOAuth2(
102+
self.implicit_scheme = OAuth2Facade(
102103
flows=OAuthFlows(
103104
implicit={
104105
"authorizationUrl": self.discover.authorization_url(
@@ -108,22 +109,17 @@ def __init__(
108109
}
109110
),
110111
scheme_name="OAuth2ImplicitBearer",
111-
auto_error=auto_error,
112+
auto_error=False,
112113
)
113114
self.authcode_scheme = OAuth2AuthorizationCodeBearer(
114115
authorizationUrl=self.discover.authorization_url(oidc_discoveries),
115116
tokenUrl=self.discover.token_url(oidc_discoveries),
116117
# refreshUrl=self.discover.refresh_url(oidc_discoveries),
117118
scopes=scopes,
119+
auto_error=False,
118120
)
119121

120-
def authenticate_user(
121-
self,
122-
security_scopes: SecurityScopes,
123-
authorization_credentials: Optional[HTTPAuthorizationCredentials] = Depends(
124-
AuthBearer(auto_error=False)
125-
),
126-
) -> Optional[IDToken]:
122+
def authenticate_user(self, auto_error=None):
127123
"""Validate and parse OIDC ID token against issuer in config.
128124
Note this function caches the signatures and algorithms of the issuing server
129125
for signature_cache_ttl seconds.
@@ -133,53 +129,64 @@ def authenticate_user(
133129
scenes by Depends.
134130
135131
Return:
136-
Dict: Dictionary with IDToken information
132+
IDToken: Dictionary with IDToken information
137133
138134
raises:
139135
HTTPException(status_code=401, detail=f"Unauthorized: {err}")
140136
"""
141137

142-
if authorization_credentials is None:
143-
if self.auto_error:
144-
raise HTTPException(
145-
status.HTTP_401_UNAUTHORIZED, detail="Missing bearer token"
146-
)
147-
else:
148-
return None
138+
if auto_error is None:
139+
auto_error = self.auto_error
149140

150-
oidc_discoveries = self.discover.auth_server(
151-
openid_connect_url=self.openid_connect_url
152-
)
153-
key = self.discover.public_keys(oidc_discoveries)
154-
algorithms = self.discover.signing_algos(oidc_discoveries)
155-
156-
try:
157-
id_token = jwt.decode(
158-
authorization_credentials.credentials,
159-
key,
160-
algorithms,
161-
audience=self.audience,
162-
issuer=self.issuer,
163-
options={
164-
# Disabled at_hash check since we aren't using the access token
165-
"verify_at_hash": False,
166-
"verify_iss": self.issuer is not None,
167-
"verify_aud": self.audience is not None,
168-
},
141+
def authenticate_user_(
142+
security_scopes: SecurityScopes,
143+
authorization_credentials: Optional[HTTPAuthorizationCredentials] = Depends(
144+
HTTPBearer(auto_error=auto_error)
145+
),
146+
) -> Optional[IDToken]:
147+
if authorization_credentials is None:
148+
if auto_error:
149+
raise HTTPException(
150+
status.HTTP_401_UNAUTHORIZED, detail="Missing bearer token"
151+
)
152+
else:
153+
return None
154+
155+
oidc_discoveries = self.discover.auth_server(
156+
openid_connect_url=self.openid_connect_url
169157
)
170-
except (ExpiredSignatureError, JWTError, JWTClaimsError) as err:
171-
if self.auto_error:
172-
raise HTTPException(status_code=401, detail=f"Unauthorized: {err}")
173-
else:
174-
return None
175-
176-
if not set(security_scopes.scopes).issubset(id_token["scope"].split(" ")):
177-
if self.auto_error:
178-
raise HTTPException(
179-
status.HTTP_401_UNAUTHORIZED,
180-
detail=f"""Missing scope token, only have {id_token["scopes"]}""",
158+
key = self.discover.public_keys(oidc_discoveries)
159+
algorithms = self.discover.signing_algos(oidc_discoveries)
160+
161+
try:
162+
id_token = jwt.decode(
163+
authorization_credentials.credentials,
164+
key,
165+
algorithms,
166+
audience=self.audience,
167+
issuer=self.issuer,
168+
options={
169+
# Disabled at_hash check since we aren't using the access token
170+
"verify_at_hash": False,
171+
"verify_iss": self.issuer is not None,
172+
"verify_aud": self.audience is not None,
173+
},
181174
)
182-
else:
183-
return None
184-
185-
return self.idtoken_model(**id_token)
175+
except (ExpiredSignatureError, JWTError, JWTClaimsError) as err:
176+
if auto_error:
177+
raise HTTPException(status_code=401, detail=f"Unauthorized: {err}")
178+
else:
179+
return None
180+
181+
if not set(security_scopes.scopes).issubset(id_token["scope"].split(" ")):
182+
if auto_error:
183+
raise HTTPException(
184+
status.HTTP_401_UNAUTHORIZED,
185+
detail=f"""Missing scope token, only have {id_token["scopes"]}""",
186+
)
187+
else:
188+
return None
189+
190+
return self.idtoken_model(**id_token)
191+
192+
return authenticate_user_

fastapi_oidc/types.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,8 @@ class KeycloakIDToken(IDToken):
5151
"""Pydantic Model for the IDToken returned by Keycloak's OIDC implementation."""
5252

5353
auth_time: int
54-
ver: int
5554
jti: str
56-
amr: List[str]
57-
idp: str
58-
nonce: str
59-
at_hash: str
6055
name: str
6156
email: str
57+
email_verified: bool
6258
preferred_username: str

0 commit comments

Comments
 (0)