Skip to content

Commit 9fefd8f

Browse files
authored
Merge pull request #305 from supertokens/session-no-req-res
feat: Introduce session recipe functions without request response
2 parents dd26224 + 282aebc commit 9fefd8f

20 files changed

+1220
-1071
lines changed

supertokens_python/recipe/emailpassword/api/implementation.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ async def sign_in_post(
171171
session = await create_new_session(
172172
api_options.request,
173173
user.user_id,
174+
access_token_payload={},
175+
session_data_in_database={},
174176
user_context=user_context,
175177
)
176178
return SignInPostOkResult(user, session)
@@ -208,6 +210,8 @@ async def sign_up_post(
208210
session = await create_new_session(
209211
api_options.request,
210212
user.user_id,
213+
access_token_payload={},
214+
session_data_in_database={},
211215
user_context=user_context,
212216
)
213217
return SignUpPostOkResult(user, session)

supertokens_python/recipe/session/api/implementation.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,27 @@
2424
from supertokens_python.types import MaybeAwaitable
2525
from supertokens_python.utils import normalise_http_method
2626

27-
from ..utils import get_required_claim_validators
28-
2927
if TYPE_CHECKING:
3028
from supertokens_python.recipe.session.interfaces import APIOptions
3129
from ..interfaces import SessionContainer
3230

3331
from typing import Any, Dict
3432

33+
from supertokens_python.recipe.session.session_request_functions import (
34+
get_session_from_request,
35+
refresh_session_in_request,
36+
)
37+
3538

3639
class APIImplementation(APIInterface):
3740
async def refresh_post(
3841
self, api_options: APIOptions, user_context: Dict[str, Any]
3942
) -> SessionContainer:
40-
return await api_options.recipe_implementation.refresh_session(
41-
api_options.request, user_context
43+
return await refresh_session_in_request(
44+
api_options.request,
45+
user_context,
46+
api_options.config,
47+
api_options.recipe_implementation,
4248
)
4349

4450
async def signout_post(
@@ -69,23 +75,18 @@ async def verify_session(
6975
return None
7076
incoming_path = NormalisedURLPath(api_options.request.get_path())
7177
refresh_token_path = api_options.config.refresh_token_path
72-
if incoming_path.equals(refresh_token_path) and method == "post":
73-
return await api_options.recipe_implementation.refresh_session(
74-
api_options.request, user_context
75-
)
76-
session = await api_options.recipe_implementation.get_session(
77-
api_options.request,
78-
anti_csrf_check,
79-
session_required,
80-
user_context,
81-
)
8278

83-
if session is not None:
84-
claim_validators = await get_required_claim_validators(
85-
session,
86-
override_global_claim_validators,
79+
if incoming_path.equals(refresh_token_path) and method == "post":
80+
return await refresh_session_in_request(
81+
api_options.request,
8782
user_context,
83+
api_options.config,
84+
api_options.recipe_implementation,
8885
)
89-
await session.assert_claims(claim_validators, user_context)
9086

91-
return session
87+
return await get_session_from_request(
88+
api_options.request,
89+
api_options.config,
90+
api_options.recipe_implementation,
91+
user_context=user_context,
92+
)

supertokens_python/recipe/session/api/signout.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@
1515

1616
from typing import TYPE_CHECKING
1717

18+
from supertokens_python.recipe.session.session_request_functions import (
19+
get_session_from_request,
20+
)
21+
1822
if TYPE_CHECKING:
19-
from supertokens_python.recipe.session.interfaces import APIInterface, APIOptions
23+
from supertokens_python.recipe.session.interfaces import (
24+
APIInterface,
25+
APIOptions,
26+
)
2027

2128
from supertokens_python.utils import default_user_context, send_200_response
2229

@@ -27,12 +34,15 @@ async def handle_signout_api(api_implementation: APIInterface, api_options: APIO
2734
or api_implementation.signout_post is None
2835
):
2936
return None
37+
3038
user_context = default_user_context(api_options.request)
3139

32-
session = await api_options.recipe_implementation.get_session(
33-
request=api_options.request,
34-
anti_csrf_check=None,
40+
session = await get_session_from_request(
41+
api_options.request,
42+
api_options.config,
43+
api_options.recipe_implementation,
3544
session_required=False,
45+
override_global_claim_validators=lambda _, __, ___: [],
3646
user_context=user_context,
3747
)
3848

supertokens_python/recipe/session/asyncio/__init__.py

Lines changed: 160 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
# under the License.
1414
from typing import Any, Dict, List, Union, TypeVar, Callable, Optional
1515

16+
from supertokens_python.exceptions import SuperTokensError
17+
from supertokens_python.framework.request import BaseRequest
1618
from supertokens_python.recipe.openid.interfaces import (
1719
GetOpenIdDiscoveryConfigurationResult,
1820
)
@@ -26,10 +28,25 @@
2628
ClaimsValidationResult,
2729
JSONObject,
2830
GetClaimValueOkResult,
31+
GetSessionUnauthorizedErrorResult,
32+
GetSessionTryRefreshTokenErrorResult,
33+
GetSessionClaimValidationErrorResult,
34+
GetSessionClaimValidationErrorResponseObject,
35+
CreateNewSessionResult,
36+
GetSessionOkResult,
37+
RefreshSessionOkResult,
38+
RefreshSessionUnauthorizedResult,
39+
RefreshSessionTokenTheftErrorResult,
2940
)
3041
from supertokens_python.recipe.session.recipe import SessionRecipe
42+
from supertokens_python.recipe.session.session_request_functions import (
43+
get_session_from_request,
44+
create_new_session_in_request,
45+
refresh_session_in_request,
46+
)
3147
from supertokens_python.types import MaybeAwaitable
3248
from supertokens_python.utils import FRAMEWORKS, resolve, deprecated_warn
49+
from ..exceptions import InvalidClaimsError
3350
from ..utils import get_required_claim_validators
3451
from ...jwt.interfaces import (
3552
CreateJwtOkResult,
@@ -54,6 +71,36 @@ async def create_new_session(
5471
if access_token_payload is None:
5572
access_token_payload = {}
5673

74+
recipe_instance = SessionRecipe.get_instance()
75+
config = recipe_instance.config
76+
app_info = recipe_instance.app_info
77+
78+
return await create_new_session_in_request(
79+
request,
80+
user_context,
81+
recipe_instance,
82+
access_token_payload,
83+
user_id,
84+
config,
85+
app_info,
86+
session_data_in_database,
87+
)
88+
89+
90+
async def create_new_session_without_request_response(
91+
user_id: str,
92+
access_token_payload: Union[Dict[str, Any], None] = None,
93+
session_data_in_database: Union[Dict[str, Any], None] = None,
94+
disable_anti_csrf: bool = False,
95+
user_context: Union[None, Dict[str, Any]] = None,
96+
) -> CreateNewSessionResult:
97+
if user_context is None:
98+
user_context = {}
99+
if session_data_in_database is None:
100+
session_data_in_database = {}
101+
if access_token_payload is None:
102+
access_token_payload = {}
103+
57104
claims_added_by_other_recipes = (
58105
SessionRecipe.get_instance().get_claims_added_by_other_recipes()
59106
)
@@ -63,16 +110,11 @@ async def create_new_session(
63110
update = await claim.build(user_id, user_context)
64111
final_access_token_payload = {**final_access_token_payload, **update}
65112

66-
if not hasattr(request, "wrapper_used") or not request.wrapper_used:
67-
request = FRAMEWORKS[
68-
SessionRecipe.get_instance().app_info.framework
69-
].wrap_request(request)
70-
71113
return await SessionRecipe.get_instance().recipe_implementation.create_new_session(
72-
request,
73114
user_id,
74115
final_access_token_payload,
75116
session_data_in_database,
117+
disable_anti_csrf,
76118
user_context=user_context,
77119
)
78120

@@ -236,9 +278,10 @@ async def remove_claim(
236278

237279

238280
async def get_session(
239-
request: Any,
240-
anti_csrf_check: Union[bool, None] = None,
241-
session_required: bool = True,
281+
request: BaseRequest,
282+
session_required: Optional[bool] = None,
283+
anti_csrf_check: Optional[bool] = None,
284+
check_database: Optional[bool] = None,
242285
override_global_claim_validators: Optional[
243286
Callable[
244287
[List[SessionClaimValidator], SessionContainer, Dict[str, Any]],
@@ -249,40 +292,135 @@ async def get_session(
249292
) -> Union[SessionContainer, None]:
250293
if user_context is None:
251294
user_context = {}
252-
if not hasattr(request, "wrapper_used") or not request.wrapper_used:
253-
request = FRAMEWORKS[
254-
SessionRecipe.get_instance().app_info.framework
255-
].wrap_request(request)
256295

257-
session_recipe_impl = SessionRecipe.get_instance().recipe_implementation
258-
session = await session_recipe_impl.get_session(
296+
recipe_instance = SessionRecipe.get_instance()
297+
recipe_interface_impl = recipe_instance.recipe_implementation
298+
config = recipe_instance.config
299+
300+
return await get_session_from_request(
259301
request,
302+
config,
303+
recipe_interface_impl,
304+
session_required=session_required,
305+
anti_csrf_check=anti_csrf_check,
306+
check_database=check_database,
307+
override_global_claim_validators=override_global_claim_validators,
308+
user_context=user_context,
309+
)
310+
311+
312+
async def get_session_without_request_response(
313+
access_token: str,
314+
anti_csrf_token: Optional[str] = None,
315+
anti_csrf_check: Optional[bool] = None,
316+
check_database: Optional[bool] = None,
317+
override_global_claim_validators: Optional[
318+
Callable[
319+
[List[SessionClaimValidator], SessionContainer, Dict[str, Any]],
320+
MaybeAwaitable[List[SessionClaimValidator]],
321+
]
322+
] = None,
323+
user_context: Union[None, Dict[str, Any]] = None,
324+
) -> Union[
325+
GetSessionOkResult,
326+
GetSessionUnauthorizedErrorResult,
327+
GetSessionTryRefreshTokenErrorResult,
328+
GetSessionClaimValidationErrorResult,
329+
]:
330+
"""Tries to validate an access token and build a Session object from it.
331+
332+
Notes about anti-csrf checking:
333+
- if the `antiCsrf` is set to VIA_HEADER in the Session recipe config you have to handle anti-csrf checking before calling this function and set antiCsrfCheck to false in the options.
334+
- you can disable anti-csrf checks by setting antiCsrf to NONE in the Session recipe config. We only recommend this if you are always getting the access-token from the Authorization header.
335+
- if the antiCsrf check fails the returned status will be TRY_REFRESH_TOKEN_ERROR
336+
337+
Args:
338+
- access_token: The access token extracted from the authorization header or cookies
339+
- anti_csrf_token: The anti-csrf token extracted from the authorization header or cookies. Can be undefined if antiCsrfCheck is false
340+
- anti_csrf_check: If true, anti-csrf checking will be done. If false, it will be skipped. Defaults behaviour to check.
341+
- check_database: If true, the session will be checked in the database. If false, it will be skipped. Defaults behaviour to skip.
342+
- override_global_claim_validators: Alter the
343+
- user_context: user context
344+
345+
Returned values:
346+
- GetSessionOkResult: The session was successfully validated, including claim validation
347+
- GetSessionClaimValidationErrorResult: While the access token is valid, one or more claim validators have failed. Our frontend SDKs expect a 403 response the contents matching the value returned from this function.
348+
- GetSessionTryRefreshTokenErrorResult: This means, that the access token structure was valid, but it didn't pass validation for some reason and the user should call the refresh API.
349+
- You can send a 401 response to trigger this behaviour if you are using our frontend SDKs
350+
- GetSessionUnauthorizedErrorResult: This means that the access token likely doesn't belong to a SuperTokens session. If this is unexpected, it's best handled by sending a 401 response.
351+
"""
352+
if user_context is None:
353+
user_context = {}
354+
355+
recipe_interface_impl = SessionRecipe.get_instance().recipe_implementation
356+
357+
res = await recipe_interface_impl.get_session(
358+
access_token,
359+
anti_csrf_token,
260360
anti_csrf_check,
261-
session_required,
361+
check_database,
362+
override_global_claim_validators,
262363
user_context,
263364
)
264365

265-
if session is not None:
366+
if isinstance(res, GetSessionOkResult):
266367
claim_validators = await get_required_claim_validators(
267-
session, override_global_claim_validators, user_context
368+
res.session, override_global_claim_validators, user_context
268369
)
269-
await session.assert_claims(claim_validators, user_context)
370+
try:
371+
await res.session.assert_claims(claim_validators, user_context)
372+
except SuperTokensError as e:
373+
if isinstance(e, InvalidClaimsError):
374+
return GetSessionClaimValidationErrorResult(
375+
error=e,
376+
response=GetSessionClaimValidationErrorResponseObject(
377+
message="invalid claim", claim_validation_errors=e.payload
378+
),
379+
)
380+
raise e
270381

271-
return session
382+
return res
272383

273384

274385
async def refresh_session(
275-
request: Any, user_context: Union[None, Dict[str, Any]] = None
386+
request: Any,
387+
user_context: Union[None, Dict[str, Any]] = None,
276388
) -> SessionContainer:
277389
if user_context is None:
278390
user_context = {}
391+
279392
if not hasattr(request, "wrapper_used") or not request.wrapper_used:
280393
request = FRAMEWORKS[
281394
SessionRecipe.get_instance().app_info.framework
282395
].wrap_request(request)
283396

397+
recipe_instance = SessionRecipe.get_instance()
398+
config = recipe_instance.config
399+
recipe_interface_impl = recipe_instance.recipe_implementation
400+
401+
return await refresh_session_in_request(
402+
request,
403+
user_context,
404+
config,
405+
recipe_interface_impl,
406+
)
407+
408+
409+
async def refresh_session_without_request_response(
410+
refresh_token: str,
411+
disable_anti_csrf: bool = False,
412+
anti_csrf_token: Optional[str] = None,
413+
user_context: Optional[Dict[str, Any]] = None,
414+
) -> Union[
415+
RefreshSessionOkResult,
416+
RefreshSessionUnauthorizedResult,
417+
RefreshSessionTokenTheftErrorResult,
418+
]:
419+
if user_context is None:
420+
user_context = {}
421+
284422
return await SessionRecipe.get_instance().recipe_implementation.refresh_session(
285-
request, user_context
423+
refresh_token, anti_csrf_token, disable_anti_csrf, user_context
286424
)
287425

288426

0 commit comments

Comments
 (0)