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

Commit b43fc6c

Browse files
committed
Add ACR support to integrations
1 parent bc371f4 commit b43fc6c

File tree

7 files changed

+328
-7
lines changed

7 files changed

+328
-7
lines changed

fief_client/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"Fief client for Python."
22
from fief_client.client import (
33
Fief,
4+
FiefAccessTokenACRTooLow,
45
FiefAccessTokenExpired,
56
FiefAccessTokenInfo,
67
FiefAccessTokenInvalid,
@@ -25,6 +26,7 @@
2526
"FiefAccessTokenInfo",
2627
"FiefUserInfo",
2728
"FiefError",
29+
"FiefAccessTokenACRTooLow",
2830
"FiefAccessTokenExpired",
2931
"FiefAccessTokenMissingPermission",
3032
"FiefAccessTokenMissingScope",

fief_client/client.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,32 @@ class FiefACR(str, Enum):
2424
LEVEL_ONE = "1"
2525
"""Level 1. Password authentication was performed."""
2626

27+
def __lt__(self, other: object) -> bool:
28+
return self._compare(other, True, True)
29+
30+
def __le__(self, other: object) -> bool:
31+
return self._compare(other, False, True)
32+
33+
def __gt__(self, other: object) -> bool:
34+
return self._compare(other, True, False)
35+
36+
def __ge__(self, other: object) -> bool:
37+
return self._compare(other, False, False)
38+
39+
def _compare(self, other: object, strict: bool, asc: bool) -> bool:
40+
if not isinstance(other, FiefACR):
41+
return NotImplemented # pragma: no cover
42+
43+
if self == other:
44+
return not strict
45+
46+
for elem in FiefACR:
47+
if self == elem:
48+
return asc
49+
elif other == elem:
50+
return not asc
51+
raise RuntimeError() # pragma: no cover
52+
2753

2854
class FiefTokenResponse(TypedDict):
2955
"""
@@ -134,6 +160,10 @@ class FiefAccessTokenMissingScope(FiefError):
134160
"""The access token is missing a required scope."""
135161

136162

163+
class FiefAccessTokenACRTooLow(FiefError):
164+
"""The access token doesn't meet the minimum ACR level."""
165+
166+
137167
class FiefAccessTokenMissingPermission(FiefError):
138168
"""The access token is missing a required permission."""
139169

@@ -254,6 +284,7 @@ def _validate_access_token(
254284
jwks: jwk.JWKSet,
255285
*,
256286
required_scope: Optional[List[str]] = None,
287+
required_acr: Optional[FiefACR] = None,
257288
required_permissions: Optional[List[str]] = None,
258289
) -> FiefAccessTokenInfo:
259290
try:
@@ -265,6 +296,15 @@ def _validate_access_token(
265296
if scope not in access_token_scope:
266297
raise FiefAccessTokenMissingScope()
267298

299+
try:
300+
acr = FiefACR(claims["acr"])
301+
except ValueError as e:
302+
raise FiefAccessTokenInvalid() from e
303+
304+
if required_acr is not None:
305+
if acr < required_acr:
306+
raise FiefAccessTokenACRTooLow()
307+
268308
permissions: List[str] = claims["permissions"]
269309
if required_permissions is not None:
270310
for required_permission in required_permissions:
@@ -274,7 +314,7 @@ def _validate_access_token(
274314
return {
275315
"id": uuid.UUID(claims["sub"]),
276316
"scope": access_token_scope,
277-
"acr": claims["acr"],
317+
"acr": acr,
278318
"permissions": permissions,
279319
"access_token": access_token,
280320
}
@@ -574,6 +614,7 @@ def validate_access_token(
574614
access_token: str,
575615
*,
576616
required_scope: Optional[List[str]] = None,
617+
required_acr: Optional[FiefACR] = None,
577618
required_permissions: Optional[List[str]] = None,
578619
) -> FiefAccessTokenInfo:
579620
"""
@@ -583,6 +624,8 @@ def validate_access_token(
583624
584625
:param access_token: The access token to validate.
585626
:param required_scope: Optional list of scopes to check for.
627+
:param required_acr: Optional minimum ACR level required.
628+
Read more: https://docs.fief.dev/going-further/acr/
586629
:param required_permissions: Optional list of permissions to check for.
587630
588631
**Example: Validate access token with required scopes**
@@ -600,6 +643,21 @@ def validate_access_token(
600643
print(access_token_info)
601644
```
602645
646+
**Example: Validate access token with minimum ACR level**
647+
648+
```py
649+
try:
650+
access_token_info = fief.validate_access_token("ACCESS_TOKEN", required_acr=FiefACR.LEVEL_ONE)
651+
except FiefAccessTokenInvalid:
652+
print("Invalid access token")
653+
except FiefAccessTokenExpired:
654+
print("Expired access token")
655+
except FiefAccessTokenACRTooLow:
656+
print("ACR too low")
657+
658+
print(access_token_info)
659+
```
660+
603661
**Example: Validate access token with required permissions**
604662
605663
```py
@@ -620,6 +678,7 @@ def validate_access_token(
620678
access_token,
621679
jwks,
622680
required_scope=required_scope,
681+
required_acr=required_acr,
623682
required_permissions=required_permissions,
624683
)
625684

@@ -983,6 +1042,7 @@ async def validate_access_token(
9831042
access_token: str,
9841043
*,
9851044
required_scope: Optional[List[str]] = None,
1045+
required_acr: Optional[FiefACR] = None,
9861046
required_permissions: Optional[List[str]] = None,
9871047
) -> FiefAccessTokenInfo:
9881048
"""
@@ -992,6 +1052,8 @@ async def validate_access_token(
9921052
9931053
:param access_token: The access token to validate.
9941054
:param required_scope: Optional list of scopes to check for.
1055+
:param required_acr: Optional minimum ACR level required.
1056+
Read more: https://docs.fief.dev/going-further/acr/
9951057
:param required_permissions: Optional list of permissions to check for.
9961058
9971059
**Example: Validate access token with required scopes**
@@ -1009,6 +1071,21 @@ async def validate_access_token(
10091071
print(access_token_info)
10101072
```
10111073
1074+
**Example: Validate access token with minimum ACR level**
1075+
1076+
```py
1077+
try:
1078+
access_token_info = await fief.validate_access_token("ACCESS_TOKEN", required_acr=FiefACR.LEVEL_ONE)
1079+
except FiefAccessTokenInvalid:
1080+
print("Invalid access token")
1081+
except FiefAccessTokenExpired:
1082+
print("Expired access token")
1083+
except FiefAccessTokenACRTooLow:
1084+
print("ACR too low")
1085+
1086+
print(access_token_info)
1087+
```
1088+
10121089
**Example: Validate access token with required permissions**
10131090
10141091
```py
@@ -1029,6 +1106,7 @@ async def validate_access_token(
10291106
access_token,
10301107
jwks,
10311108
required_scope=required_scope,
1109+
required_acr=required_acr,
10321110
required_permissions=required_permissions,
10331111
)
10341112

fief_client/integrations/fastapi.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121

2222
from fief_client import (
2323
Fief,
24+
FiefAccessTokenACRTooLow,
2425
FiefAccessTokenExpired,
2526
FiefAccessTokenInfo,
2627
FiefAccessTokenInvalid,
2728
FiefAccessTokenMissingPermission,
2829
FiefAccessTokenMissingScope,
30+
FiefACR,
2931
FiefAsync,
3032
FiefUserInfo,
3133
)
@@ -124,6 +126,7 @@ def authenticated(
124126
self,
125127
optional: bool = False,
126128
scope: Optional[List[str]] = None,
129+
acr: Optional[FiefACR] = None,
127130
permissions: Optional[List[str]] = None,
128131
):
129132
"""
@@ -135,6 +138,9 @@ def authenticated(
135138
an unauthorized response will be raised.
136139
:param scope: Optional list of scopes required.
137140
If the access token lacks one of the required scope, a forbidden response will be raised.
141+
:param acr: Optional minimum ACR level required.
142+
If the access token doesn't meet the minimum level, a forbidden response will be raised.
143+
Read more: https://docs.fief.dev/going-further/acr/
138144
:param permissions: Optional list of permissions required.
139145
If the access token lacks one of the required permission, a forbidden response will be raised.
140146
@@ -164,7 +170,10 @@ async def _authenticated(
164170

165171
try:
166172
result = self.client.validate_access_token(
167-
token, required_scope=scope, required_permissions=permissions
173+
token,
174+
required_scope=scope,
175+
required_acr=acr,
176+
required_permissions=permissions,
168177
)
169178
if isawaitable(result):
170179
info = await result
@@ -174,7 +183,11 @@ async def _authenticated(
174183
if optional:
175184
return None
176185
return await self.get_unauthorized_response(request, response)
177-
except (FiefAccessTokenMissingScope, FiefAccessTokenMissingPermission):
186+
except (
187+
FiefAccessTokenMissingScope,
188+
FiefAccessTokenACRTooLow,
189+
FiefAccessTokenMissingPermission,
190+
):
178191
return await self.get_forbidden_response(request, response)
179192

180193
return info
@@ -185,6 +198,7 @@ def current_user(
185198
self,
186199
optional: bool = False,
187200
scope: Optional[List[str]] = None,
201+
acr: Optional[FiefACR] = None,
188202
permissions: Optional[List[str]] = None,
189203
refresh: bool = False,
190204
):
@@ -199,6 +213,9 @@ def current_user(
199213
an unauthorized response will be raised.
200214
:param scope: Optional list of scopes required.
201215
If the access token lacks one of the required scope, a forbidden response will be raised.
216+
:param acr: Optional minimum ACR level required.
217+
If the access token doesn't meet the minimum level, a forbidden response will be raised.
218+
Read more: https://docs.fief.dev/going-further/acr/
202219
:param permissions: Optional list of permissions required.
203220
If the access token lacks one of the required permission, a forbidden response will be raised.
204221
:param refresh: If `True`, the user information will be refreshed from the Fief API.
@@ -215,7 +232,7 @@ async def get_current_user(
215232
```
216233
"""
217234
signature = self._get_current_user_call_signature(
218-
self.authenticated(optional, scope, permissions)
235+
self.authenticated(optional, scope, acr, permissions)
219236
)
220237

221238
@with_signature(signature)

fief_client/integrations/flask.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77

88
from fief_client import (
99
Fief,
10+
FiefAccessTokenACRTooLow,
1011
FiefAccessTokenExpired,
1112
FiefAccessTokenInfo,
1213
FiefAccessTokenInvalid,
1314
FiefAccessTokenMissingScope,
15+
FiefACR,
1416
FiefUserInfo,
1517
)
1618
from fief_client.client import FiefAccessTokenMissingPermission
@@ -47,7 +49,7 @@ class FiefAuthForbidden(FiefAuthError):
4749
Request forbidden error.
4850
4951
This error is raised when using the `authenticated` or `current_user` decorator
50-
but the access token doesn't match the list of scopes or permissions.
52+
but the access token doesn't match the list of scopes, permissions or minimum ACR level.
5153
5254
You should implement an `errorhandler` to define the behavior of your server when
5355
this happens.
@@ -166,6 +168,7 @@ def authenticated(
166168
*,
167169
optional: bool = False,
168170
scope: Optional[List[str]] = None,
171+
acr: Optional[FiefACR] = None,
169172
permissions: Optional[List[str]] = None,
170173
):
171174
"""
@@ -178,6 +181,9 @@ def authenticated(
178181
a `FiefAuthUnauthorized` error will be raised.
179182
:param scope: Optional list of scopes required.
180183
If the access token lacks one of the required scope, a `FiefAuthForbidden` error will be raised.
184+
:param acr: Optional minimum ACR level required.
185+
If the access token doesn't meet the minimum level, a `FiefAuthForbidden` error will be raised.
186+
Read more: https://docs.fief.dev/going-further/acr/
181187
:param permissions: Optional list of permissions required.
182188
If the access token lacks one of the required permission, a `FiefAuthForbidden` error will be raised.
183189
@@ -203,7 +209,10 @@ def decorated_function(*args, **kwargs):
203209

204210
try:
205211
info = self.client.validate_access_token(
206-
token, required_scope=scope, required_permissions=permissions
212+
token,
213+
required_scope=scope,
214+
required_acr=acr,
215+
required_permissions=permissions,
207216
)
208217
except (FiefAccessTokenInvalid, FiefAccessTokenExpired) as e:
209218
if optional:
@@ -212,6 +221,7 @@ def decorated_function(*args, **kwargs):
212221
raise FiefAuthUnauthorized() from e
213222
except (
214223
FiefAccessTokenMissingScope,
224+
FiefAccessTokenACRTooLow,
215225
FiefAccessTokenMissingPermission,
216226
) as e:
217227
raise FiefAuthForbidden() from e
@@ -229,6 +239,7 @@ def current_user(
229239
*,
230240
optional: bool = False,
231241
scope: Optional[List[str]] = None,
242+
acr: Optional[FiefACR] = None,
232243
permissions: Optional[List[str]] = None,
233244
refresh: bool = False,
234245
):
@@ -242,6 +253,9 @@ def current_user(
242253
a `FiefAuthUnauthorized` error will be raised.
243254
:param scope: Optional list of scopes required.
244255
If the access token lacks one of the required scope, a `FiefAuthForbidden` error will be raised.
256+
:param acr: Optional minimum ACR level required.
257+
If the access token doesn't meet the minimum level, a `FiefAuthForbidden` error will be raised.
258+
Read more: https://docs.fief.dev/going-further/acr/
245259
:param permissions: Optional list of permissions required.
246260
If the access token lacks one of the required permission, a `FiefAuthForbidden` error will be raised.
247261
:param refresh: If `True`, the user information will be refreshed from the Fief API.
@@ -260,7 +274,9 @@ def get_current_user():
260274

261275
def _current_user(f):
262276
@wraps(f)
263-
@self.authenticated(optional=optional, scope=scope, permissions=permissions)
277+
@self.authenticated(
278+
optional=optional, scope=scope, acr=acr, permissions=permissions
279+
)
264280
def decorated_function(*args, **kwargs):
265281
access_token_info: Optional[FiefAccessTokenInfo] = g.access_token_info
266282

0 commit comments

Comments
 (0)