11import contextlib
22import json
33import uuid
4+ from enum import Enum
45from typing import Any , Dict , List , Mapping , Optional , Tuple , TypedDict , Union
56from urllib .parse import urlencode
67
1314HTTPXClient = Union [httpx .Client , httpx .AsyncClient ]
1415
1516
17+ class FiefACR (str , Enum ):
18+ """
19+ List of defined Authentication Context Class Reference.
20+ """
21+
22+ LEVEL_ZERO = "0"
23+ """Level 0. No authentication was performed, a previous session was used."""
24+ LEVEL_ONE = "1"
25+ """Level 1. Password authentication was performed."""
26+
27+
1628class FiefTokenResponse (TypedDict ):
1729 """
1830 Typed dictionary containing the tokens and related information returned by Fief after a successful authentication.
@@ -40,6 +52,7 @@ class FiefAccessTokenInfo(TypedDict):
4052 {
4153 "id": "aeeb8bfa-e8f4-4724-9427-c3d5af66190e",
4254 "scope": ["openid", "required_scope"],
55+ "acr": "1",
4356 "permissions": ["castles:read", "castles:create", "castles:update", "castles:delete"],
4457 "access_token": "ACCESS_TOKEN",
4558 }
@@ -50,6 +63,8 @@ class FiefAccessTokenInfo(TypedDict):
5063 """ID of the user."""
5164 scope : List [str ]
5265 """List of granted scopes for this access token."""
66+ acr : FiefACR
67+ """Level of Authentication Context class Reference."""
5368 permissions : List [str ]
5469 """List of [granted permissions](https://docs.fief.dev/getting-started/access-control/) for this user."""
5570 access_token : str
@@ -259,6 +274,7 @@ def _validate_access_token(
259274 return {
260275 "id" : uuid .UUID (claims ["sub" ]),
261276 "scope" : access_token_scope ,
277+ "acr" : claims ["acr" ],
262278 "permissions" : permissions ,
263279 "access_token" : access_token ,
264280 }
@@ -367,6 +383,51 @@ def _get_update_profile_request(
367383 json = data ,
368384 )
369385
386+ def _get_change_password_request (
387+ self ,
388+ client : HTTPXClient ,
389+ * ,
390+ endpoint : str ,
391+ access_token : str ,
392+ new_password : str ,
393+ ) -> httpx .Request :
394+ return client .build_request (
395+ "PATCH" ,
396+ endpoint ,
397+ headers = {"Authorization" : f"Bearer { access_token } " },
398+ json = {"password" : new_password },
399+ )
400+
401+ def _get_email_change_request (
402+ self ,
403+ client : HTTPXClient ,
404+ * ,
405+ endpoint : str ,
406+ access_token : str ,
407+ email : str ,
408+ ) -> httpx .Request :
409+ return client .build_request (
410+ "PATCH" ,
411+ endpoint ,
412+ headers = {"Authorization" : f"Bearer { access_token } " },
413+ json = {"email" : email },
414+ )
415+
416+ def _get_email_verify_request (
417+ self ,
418+ client : HTTPXClient ,
419+ * ,
420+ endpoint : str ,
421+ access_token : str ,
422+ code : str ,
423+ ) -> httpx .Request :
424+ return client .build_request (
425+ "PATCH" ,
426+ endpoint ,
427+ headers = {"Authorization" : f"Bearer { access_token } " },
428+ json = {"code" : code },
429+ )
430+
370431 def _handle_request_error (self , response : httpx .Response ):
371432 if response .is_error :
372433 raise FiefRequestError (response .status_code , response .text )
@@ -594,34 +655,115 @@ def update_profile(self, access_token: str, data: Dict[str, Any]) -> FiefUserInf
594655 :param access_token: A valid access token.
595656 :param data: A dictionary containing the data to update.
596657
597- **Example: Update email address**
658+ **Example: Update user field**
659+
660+ To update [user field](https://docs.fief.dev/getting-started/user-fields/) values, you need to nest them into a `fields` dictionary, indexed by their slug.
598661
599662 ```py
600- userinfo = fief.update_profile("ACCESS_TOKEN", { "email ": "anne@nantes.city" })
663+ userinfo = fief.update_profile("ACCESS_TOKEN", { "fields ": { "first_name": "Anne" } })
601664 ```
665+ """
666+ update_profile_endpoint = f"{ self .base_url } /api/profile"
602667
603- **Example: Update password**
668+ with self ._get_httpx_client () as client :
669+ request = self ._get_update_profile_request (
670+ client ,
671+ endpoint = update_profile_endpoint ,
672+ access_token = access_token ,
673+ data = data ,
674+ )
675+ response = client .send (request )
676+
677+ self ._handle_request_error (response )
678+
679+ return response .json ()
680+
681+ def change_password (self , access_token : str , new_password : str ) -> FiefUserInfo :
682+ """
683+ Change the user password with the Fief API using a valid access token.
684+
685+ **An access token with an ACR of at least level 1 is required.**
686+
687+ :param access_token: A valid access token.
688+ :param new_password: The new password.
689+
690+ **Example**
604691
605692 ```py
606- userinfo = fief.update_profile ("ACCESS_TOKEN", { "password": " herminetincture" } )
693+ userinfo = fief.change_password ("ACCESS_TOKEN", " herminetincture")
607694 ```
695+ """
696+ change_password_profile_endpoint = f"{ self .base_url } /api/password"
608697
609- **Example: Update user field**
698+ with self ._get_httpx_client () as client :
699+ request = self ._get_change_password_request (
700+ client ,
701+ endpoint = change_password_profile_endpoint ,
702+ access_token = access_token ,
703+ new_password = new_password ,
704+ )
705+ response = client .send (request )
610706
611- To update [user field](https://docs.fief.dev/getting-started/user-fields/) values, you need to nest them into a `fields` dictionary, indexed by their slug.
707+ self ._handle_request_error (response )
708+
709+ return response .json ()
710+
711+ def email_change (self , access_token : str , email : str ) -> FiefUserInfo :
712+ """
713+ Request an email change with the Fief API using a valid access token.
714+
715+ The user will receive a verification code on this new email address.
716+ It shall be used with the method `email_verify` to complete the modification.
717+
718+ **An access token with an ACR of at least level 1 is required.**
719+
720+ :param access_token: A valid access token.
721+ :param email: The new email address.
722+
723+ **Example**
612724
613725 ```py
614- userinfo = fief.update_profile ("ACCESS_TOKEN", { "fields": { "first_name": "Anne" } } )
726+ userinfo = fief.email_change ("ACCESS_TOKEN", "anne@nantes.city" )
615727 ```
616728 """
617- update_profile_endpoint = f"{ self .base_url } /api/profile "
729+ email_change_endpoint = f"{ self .base_url } /api/email/change "
618730
619731 with self ._get_httpx_client () as client :
620- request = self ._get_update_profile_request (
732+ request = self ._get_email_change_request (
621733 client ,
622- endpoint = update_profile_endpoint ,
734+ endpoint = email_change_endpoint ,
623735 access_token = access_token ,
624- data = data ,
736+ email = email ,
737+ )
738+ response = client .send (request )
739+
740+ self ._handle_request_error (response )
741+
742+ return response .json ()
743+
744+ def email_verify (self , access_token : str , code : str ) -> FiefUserInfo :
745+ """
746+ Verify the user email with the Fief API using a valid access token and verification code.
747+
748+ **An access token with an ACR of at least level 1 is required.**
749+
750+ :param access_token: A valid access token.
751+ :param code: The verification code received by email.
752+
753+ **Example**
754+
755+ ```py
756+ userinfo = fief.email_verify("ACCESS_TOKEN", "ABCDE")
757+ ```
758+ """
759+ email_verify_endpoint = f"{ self .base_url } /api/email/verify"
760+
761+ with self ._get_httpx_client () as client :
762+ request = self ._get_email_verify_request (
763+ client ,
764+ endpoint = email_verify_endpoint ,
765+ access_token = access_token ,
766+ code = code ,
625767 )
626768 response = client .send (request )
627769
@@ -924,34 +1066,117 @@ async def update_profile(
9241066 :param access_token: A valid access token.
9251067 :param data: A dictionary containing the data to update.
9261068
927- **Example: Update email address**
1069+ **Example: Update user field**
1070+
1071+ To update [user field](https://docs.fief.dev/getting-started/user-fields/) values, you need to nest them into a `fields` dictionary, indexed by their slug.
9281072
9291073 ```py
930- userinfo = await fief.update_profile("ACCESS_TOKEN", { "email ": "anne@nantes.city" })
1074+ userinfo = await fief.update_profile("ACCESS_TOKEN", { "fields ": { "first_name": "Anne" } })
9311075 ```
1076+ """
1077+ update_profile_endpoint = f"{ self .base_url } /api/profile"
1078+
1079+ async with self ._get_httpx_client () as client :
1080+ request = self ._get_update_profile_request (
1081+ client ,
1082+ endpoint = update_profile_endpoint ,
1083+ access_token = access_token ,
1084+ data = data ,
1085+ )
1086+ response = await client .send (request )
1087+
1088+ self ._handle_request_error (response )
1089+
1090+ return response .json ()
1091+
1092+ async def change_password (
1093+ self , access_token : str , new_password : str
1094+ ) -> FiefUserInfo :
1095+ """
1096+ Change the user password with the Fief API using a valid access token.
1097+
1098+ **An access token with an ACR of at least level 1 is required.**
1099+
1100+ :param access_token: A valid access token.
1101+ :param new_password: The new password.
9321102
933- **Example: Update password **
1103+ **Example**
9341104
9351105 ```py
936- userinfo = await fief.update_profile ("ACCESS_TOKEN", { "password": " herminetincture" } )
1106+ userinfo = await fief.change_password ("ACCESS_TOKEN", " herminetincture")
9371107 ```
1108+ """
1109+ change_password_profile_endpoint = f"{ self .base_url } /api/password"
9381110
939- **Example: Update user field**
1111+ async with self ._get_httpx_client () as client :
1112+ request = self ._get_change_password_request (
1113+ client ,
1114+ endpoint = change_password_profile_endpoint ,
1115+ access_token = access_token ,
1116+ new_password = new_password ,
1117+ )
1118+ response = await client .send (request )
9401119
941- To update [user field](https://docs.fief.dev/getting-started/user-fields/) values, you need to nest them into a `fields` dictionary, indexed by their slug.
1120+ self ._handle_request_error (response )
1121+
1122+ return response .json ()
1123+
1124+ async def email_change (self , access_token : str , email : str ) -> FiefUserInfo :
1125+ """
1126+ Request an email change with the Fief API using a valid access token.
1127+
1128+ The user will receive a verification code on this new email address.
1129+ It shall be used with the method `email_verify` to complete the modification.
1130+
1131+ **An access token with an ACR of at least level 1 is required.**
1132+
1133+ :param access_token: A valid access token.
1134+ :param email: The new email address.
1135+
1136+ **Example**
9421137
9431138 ```py
944- userinfo = await fief.update_profile ("ACCESS_TOKEN", { "fields": { "first_name": "Anne" } } )
1139+ userinfo = await fief.email_change ("ACCESS_TOKEN", "anne@nantes.city" )
9451140 ```
9461141 """
947- update_profile_endpoint = f"{ self .base_url } /api/profile "
1142+ email_change_endpoint = f"{ self .base_url } /api/email/change "
9481143
9491144 async with self ._get_httpx_client () as client :
950- request = self ._get_update_profile_request (
1145+ request = self ._get_email_change_request (
9511146 client ,
952- endpoint = update_profile_endpoint ,
1147+ endpoint = email_change_endpoint ,
9531148 access_token = access_token ,
954- data = data ,
1149+ email = email ,
1150+ )
1151+ response = await client .send (request )
1152+
1153+ self ._handle_request_error (response )
1154+
1155+ return response .json ()
1156+
1157+ async def email_verify (self , access_token : str , code : str ) -> FiefUserInfo :
1158+ """
1159+ Verify the user email with the Fief API using a valid access token and verification code.
1160+
1161+ **An access token with an ACR of at least level 1 is required.**
1162+
1163+ :param access_token: A valid access token.
1164+ :param code: The verification code received by email.
1165+
1166+ **Example**
1167+
1168+ ```py
1169+ userinfo = fief.email_verify("ACCESS_TOKEN", "ABCDE")
1170+ ```
1171+ """
1172+ email_verify_endpoint = f"{ self .base_url } /api/email/verify"
1173+
1174+ async with self ._get_httpx_client () as client :
1175+ request = self ._get_email_verify_request (
1176+ client ,
1177+ endpoint = email_verify_endpoint ,
1178+ access_token = access_token ,
1179+ code = code ,
9551180 )
9561181 response = await client .send (request )
9571182
0 commit comments