Skip to content

feat: Introduce header based auth along with existing cookie based auth #262

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a25373a
feat: Introduce header based auth along with existing cookie based auth
KShivendu Dec 5, 2022
ef4b24e
fix: Pass partial functions to session object for modifying response …
KShivendu Dec 13, 2022
9d8aa9e
fix: Remove repeated headers in flask auth and refactor logic
KShivendu Dec 14, 2022
50a6448
fix: Fix lint errors and refactor logic
KShivendu Dec 14, 2022
6826ff1
refactor: Improve variables names and code quality
KShivendu Dec 16, 2022
13a3da7
Improve header based auth logic
KShivendu Dec 22, 2022
bb46a5e
fix: Type errors in tests because of signature updates
KShivendu Dec 23, 2022
f7af9d8
refactor: Minor changes in session recipe suggested in feedback
KShivendu Dec 26, 2022
f1657ff
refactor: Use response mutators everywhere instead of new tokens info
KShivendu Dec 28, 2022
cfa29ac
refactor: Minor changes in header based auth logic
KShivendu Dec 29, 2022
8cde45b
fix: Fix mistake in create_new_session that failed tests
KShivendu Dec 29, 2022
8f8c496
test: Use cookie as token transfer method for existing tests
KShivendu Dec 29, 2022
0b06480
refactor: Changes for header based auth
KShivendu Dec 30, 2022
87cb08d
test: Fix failing tests because of IdRefreshToken and get_token_trans…
KShivendu Jan 2, 2023
107ea0e
test: Remove IdRefreshToken from all the tests
KShivendu Jan 2, 2023
33e3362
fix: Missing use of unquote func for parsing cookie value
KShivendu Jan 2, 2023
63f0b63
test: Fix test failures and add more tests in test_auth_mode
KShivendu Jan 2, 2023
3352bdc
tests: Fix and add new auth mode tests
KShivendu Jan 3, 2023
8581d84
test: Fix failing tests for revoke session during refresh
KShivendu Jan 3, 2023
185c950
refactor: Strongly type response mutators
KShivendu Jan 5, 2023
5048d89
refactor: Improve response mutators usage in handle_error
KShivendu Jan 5, 2023
b4809f2
refactor: Fix cyclic import error
KShivendu Jan 5, 2023
7bbcb53
Merge branch '0.11' into feat/header-based-auth
KShivendu Jan 5, 2023
ed905b8
Merge branch '0.11' into feat/header-based-auth
KShivendu Jan 10, 2023
1f161ef
fix: Clear session changes for edge cases
KShivendu Jan 11, 2023
fa22498
chores: Update changelog to mention header based auth changes
KShivendu Jan 11, 2023
11fca6d
test: Fix issues with tests
KShivendu Jan 11, 2023
168450c
chores: Run formatter
KShivendu Jan 18, 2023
6f1262f
test: Restore get_st_init_args func params
KShivendu Jan 18, 2023
e7f89c0
test: Fix failing tests and use url decode for token cookies
KShivendu Jan 19, 2023
92a1104
chores: Bump version to 0.12.0 with other relevant changes
KShivendu Jan 19, 2023
4f2cccc
Merge pull request #275 from supertokens/fix/clear-sesion
porcellus Jan 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion supertokens_python/framework/django/framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@
from supertokens_python.framework.types import Framework

if TYPE_CHECKING:
from django.http import HttpRequest
from django.http import HttpRequest, HttpResponse


class DjangoFramework(Framework):
def wrap_request(self, unwrapped: HttpRequest):
from supertokens_python.framework.django.django_request import DjangoRequest

return DjangoRequest(unwrapped)

def wrap_response(self, unwrapped: HttpResponse):
from supertokens_python.framework.django.django_response import DjangoResponse

return DjangoResponse(unwrapped)
9 changes: 8 additions & 1 deletion supertokens_python/framework/fastapi/framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@
from supertokens_python.framework.types import Framework

if TYPE_CHECKING:
from fastapi import Request
from fastapi import Request, Response


class FastapiFramework(Framework):
def wrap_request(self, unwrapped: Request):
from supertokens_python.framework.fastapi.fastapi_request import FastApiRequest

return FastApiRequest(unwrapped)

def wrap_response(self, unwrapped: Response):
from supertokens_python.framework.fastapi.fastapi_response import (
FastApiResponse,
)

return FastApiResponse(unwrapped)
7 changes: 6 additions & 1 deletion supertokens_python/framework/flask/framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@
from supertokens_python.framework.types import Framework

if TYPE_CHECKING:
from flask.wrappers import Request
from flask.wrappers import Request, Response


class FlaskFramework(Framework):
def wrap_request(self, unwrapped: Request):
from supertokens_python.framework.flask.flask_request import FlaskRequest

return FlaskRequest(unwrapped)

def wrap_response(self, unwrapped: Response):
from supertokens_python.framework.flask.flask_response import FlaskResponse

return FlaskResponse(unwrapped)
10 changes: 8 additions & 2 deletions supertokens_python/recipe/emailpassword/api/implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ async def sign_in_post(

user = result.user
session = await create_new_session(
api_options.request, user.user_id, user_context=user_context
api_options.request,
api_options.response,
user.user_id,
user_context=user_context,
)
return SignInPostOkResult(user, session)

Expand Down Expand Up @@ -204,6 +207,9 @@ async def sign_up_post(

user = result.user
session = await create_new_session(
api_options.request, user.user_id, user_context=user_context
api_options.request,
api_options.response,
user.user_id,
user_context=user_context,
)
return SignUpPostOkResult(user, session)
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ async def handle_email_verify_api(

session = await get_session(
api_options.request,
api_options.response,
session_required=False,
override_global_claim_validators=lambda _, __, ___: [],
user_context=user_context,
Expand All @@ -60,6 +61,7 @@ async def handle_email_verify_api(

session = await get_session(
api_options.request,
api_options.response,
override_global_claim_validators=lambda _, __, ___: [],
user_context=user_context,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async def handle_generate_email_verify_token_api(
user_context = default_user_context(api_options.request)
session = await get_session(
api_options.request,
api_options.response,
override_global_claim_validators=lambda _, __, ___: [],
user_context=user_context,
)
Expand Down
7 changes: 6 additions & 1 deletion supertokens_python/recipe/passwordless/api/implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,12 @@ async def consume_code_post(
)

session = await create_new_session(
api_options.request, user.user_id, {}, {}, user_context=user_context
api_options.request,
api_options.response,
user.user_id,
{},
{},
user_context=user_context,
)

return ConsumeCodePostOkResult(
Expand Down
16 changes: 12 additions & 4 deletions supertokens_python/recipe/session/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@
# under the License.
from __future__ import annotations

from typing import TYPE_CHECKING, Callable, Union
from typing import TYPE_CHECKING, Any, Callable, Dict, Union

from typing_extensions import Literal

if TYPE_CHECKING:
from ...recipe_module import RecipeModule
from supertokens_python.supertokens import AppInfo
from supertokens_python.supertokens import AppInfo, BaseRequest
from .utils import TokenTransferMethod

from . import exceptions as ex
from . import interfaces, utils
from .recipe import SessionRecipe
from . import utils
from . import interfaces

InputErrorHandlers = utils.InputErrorHandlers
InputOverrideConfig = utils.InputOverrideConfig
Expand All @@ -39,6 +39,13 @@ def init(
cookie_same_site: Union[Literal["lax", "none", "strict"], None] = None,
session_expired_status_code: Union[int, None] = None,
anti_csrf: Union[Literal["VIA_TOKEN", "VIA_CUSTOM_HEADER", "NONE"], None] = None,
get_token_transfer_method: Union[
Callable[
[BaseRequest, bool, Dict[str, Any]],
Union[TokenTransferMethod, Literal["any"]],
],
None,
] = None,
error_handlers: Union[InputErrorHandlers, None] = None,
override: Union[InputOverrideConfig, None] = None,
jwt: Union[JWTConfig, None] = None,
Expand All @@ -50,6 +57,7 @@ def init(
cookie_same_site,
session_expired_status_code,
anti_csrf,
get_token_transfer_method,
error_handlers,
override,
jwt,
Expand Down
43 changes: 27 additions & 16 deletions supertokens_python/recipe/session/access_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
# under the License.
from __future__ import annotations

from typing import Any, Union
from typing import Any, Dict, Union

from supertokens_python.logger import log_debug_message
from supertokens_python.utils import get_timestamp_ms

from .exceptions import raise_try_refresh_token_exception
from .jwt import get_payload
from .jwt import ParsedJWTInfo, verify_jwt


def sanitize_string(s: Any) -> Union[str, None]:
Expand All @@ -41,10 +41,15 @@ def sanitize_number(n: Any) -> Union[Union[int, float], None]:


def get_info_from_access_token(
token: str, jwt_signing_public_key: str, do_anti_csrf_check: bool
jwt_info: ParsedJWTInfo, jwt_signing_public_key: str, do_anti_csrf_check: bool
):
try:
payload = get_payload(token, jwt_signing_public_key)
verify_jwt(jwt_info, jwt_signing_public_key)
payload = jwt_info.payload

validate_access_token_structure(payload)
# FIXME: Find equivalent of ! in nodejs that is cleaner than asserts?

session_handle = sanitize_string(payload.get("sessionHandle"))
user_id = sanitize_string(payload.get("userId"))
refresh_token_hash_1 = sanitize_string(payload.get("refreshTokenHash1"))
Expand All @@ -56,18 +61,11 @@ def get_info_from_access_token(
expiry_time = sanitize_number(payload.get("expiryTime"))
time_created = sanitize_number(payload.get("timeCreated"))

if (
(session_handle is None)
or (user_data is None)
or (refresh_token_hash_1 is None)
or (user_data is None)
or (anti_csrf_token is None and do_anti_csrf_check)
or (expiry_time is None)
or (time_created is None)
):
raise Exception(
"Access token does not contain all the information. Maybe the structure has changed?"
)
if anti_csrf_token is None and do_anti_csrf_check:
raise Exception("Access token does not contain the anti-csrf token")

# FIXME: Find equivalent of ! from nodejs (cleaner than assert)
assert isinstance(expiry_time, int)

if expiry_time < get_timestamp_ms():
raise Exception("Access token expired")
Expand All @@ -87,3 +85,16 @@ def get_info_from_access_token(
"getSession: Returning TRY_REFRESH_TOKEN because failed to decode access token"
)
raise_try_refresh_token_exception(e)


def validate_access_token_structure(payload: Dict[str, Any]) -> None:
if (
not isinstance(payload.get("sessionHandle"), str)
or not isinstance(payload.get("userData"), str)
or not isinstance(payload.get("refreshTokenHash1"), str)
or not isinstance(payload.get("expiryTime"), int)
or not isinstance(payload.get("timeCreated"), int)
):
raise Exception(
"Access token does not contain all the information. Maybe the structure has changed?"
)
5 changes: 3 additions & 2 deletions supertokens_python/recipe/session/api/implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async def refresh_post(
self, api_options: APIOptions, user_context: Dict[str, Any]
) -> SessionContainer:
return await api_options.recipe_implementation.refresh_session(
api_options.request, user_context
api_options.request, api_options.response, user_context
)

async def signout_post(
Expand Down Expand Up @@ -71,10 +71,11 @@ async def verify_session(
refresh_token_path = api_options.config.refresh_token_path
if incoming_path.equals(refresh_token_path) and method == "post":
return await api_options.recipe_implementation.refresh_session(
api_options.request, user_context
api_options.request, api_options.response, user_context
)
session = await api_options.recipe_implementation.get_session(
api_options.request,
api_options.response,
anti_csrf_check,
session_required,
user_context,
Expand Down
1 change: 1 addition & 0 deletions supertokens_python/recipe/session/api/signout.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ async def handle_signout_api(api_implementation: APIInterface, api_options: APIO

session = await api_options.recipe_implementation.get_session(
request=api_options.request,
response=api_options.response,
anti_csrf_check=None,
session_required=False,
user_context=user_context,
Expand Down
18 changes: 16 additions & 2 deletions supertokens_python/recipe/session/asyncio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

async def create_new_session(
request: Any,
response: Any,
user_id: str,
access_token_payload: Union[Dict[str, Any], None] = None,
session_data: Union[Dict[str, Any], None] = None,
Expand All @@ -68,8 +69,14 @@ async def create_new_session(
SessionRecipe.get_instance().app_info.framework
].wrap_request(request)

if not hasattr(response, "wrapper_used") or not response.wrapper_used:
response = FRAMEWORKS[
SessionRecipe.get_instance().app_info.framework
].wrap_response(response)

return await SessionRecipe.get_instance().recipe_implementation.create_new_session(
request,
response,
user_id,
final_access_token_payload,
session_data,
Expand Down Expand Up @@ -237,6 +244,7 @@ async def remove_claim(

async def get_session(
request: Any,
response: Any,
anti_csrf_check: Union[bool, None] = None,
session_required: bool = True,
override_global_claim_validators: Optional[
Expand All @@ -257,6 +265,7 @@ async def get_session(
session_recipe_impl = SessionRecipe.get_instance().recipe_implementation
session = await session_recipe_impl.get_session(
request,
response,
anti_csrf_check,
session_required,
user_context,
Expand All @@ -272,16 +281,21 @@ async def get_session(


async def refresh_session(
request: Any, user_context: Union[None, Dict[str, Any]] = None
request: Any, response: Any, user_context: Union[None, Dict[str, Any]] = None
) -> SessionContainer:
if user_context is None:
user_context = {}
if not hasattr(request, "wrapper_used") or not request.wrapper_used:
request = FRAMEWORKS[
SessionRecipe.get_instance().app_info.framework
].wrap_request(request)
if not hasattr(response, "wrapper_used") or not response.wrapper_used:
response = FRAMEWORKS[
SessionRecipe.get_instance().app_info.framework
].wrap_response(response)

return await SessionRecipe.get_instance().recipe_implementation.refresh_session(
request, user_context
request, response, user_context
)


Expand Down
3 changes: 3 additions & 0 deletions supertokens_python/recipe/session/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

SESSION_REFRESH = "/session/refresh"
SIGNOUT = "/signout"
ACCESS_TOKEN_COOKIE_KEY = "sAccessToken"
Expand All @@ -22,3 +23,5 @@
ID_REFRESH_TOKEN_HEADER_SET_KEY = "id-refresh-token"
ID_REFRESH_TOKEN_HEADER_GET_KEY = "id-refresh-token"
ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"

available_token_transfer_methods = ["cookie", "header"]
Loading