Skip to content
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

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

Merged
merged 32 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def get_website_domain():
framework="django",
mode="wsgi",
recipe_list=[
session.init(),
session.init(get_token_transfer_method=lambda _, __, ___: "cookie"),
emailverification.init("REQUIRED"),
thirdpartyemailpassword.init(
providers=[
Expand Down
2 changes: 1 addition & 1 deletion examples/with-fastapi/with-thirdpartyemailpassword/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def get_website_domain():
),
framework="fastapi",
recipe_list=[
session.init(),
session.init(get_token_transfer_method=lambda _, __, ___: "cookie"),
emailverification.init("REQUIRED"),
thirdpartyemailpassword.init(
providers=[
Expand Down
2 changes: 1 addition & 1 deletion examples/with-flask/with-thirdpartyemailpassword/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def get_website_domain():
),
framework="flask",
recipe_list=[
session.init(),
session.init(get_token_transfer_method=lambda _, __, ___: "cookie"),
emailverification.init("REQUIRED"),
thirdpartyemailpassword.init(
providers=[
Expand Down
6 changes: 4 additions & 2 deletions supertokens_python/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
# under the License.
from __future__ import annotations

from typing import Union, NoReturn
from typing import Union, NoReturn, Callable, List

from supertokens_python.framework import BaseResponse


def raise_general_exception(
Expand All @@ -31,7 +33,7 @@ def raise_bad_input_exception(msg: str) -> NoReturn:


class SuperTokensError(Exception):
pass
response_mutators: List[Callable[[BaseResponse], None]] = []


class GeneralError(SuperTokensError):
Expand Down
6 changes: 3 additions & 3 deletions supertokens_python/framework/django/django_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def middleware(get_response: Any):
from supertokens_python.framework.django.django_request import DjangoRequest
from supertokens_python.framework.django.django_response import DjangoResponse
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.supertokens import manage_cookies_post_response
from supertokens_python.supertokens import manage_session_post_response

from django.http import HttpRequest

Expand All @@ -45,7 +45,7 @@ async def __asyncMiddleware(request: HttpRequest):
if hasattr(request, "supertokens") and isinstance(
request.supertokens, SessionContainer # type: ignore
):
manage_cookies_post_response(
manage_session_post_response(
request.supertokens, result # type: ignore
)
if isinstance(result, DjangoResponse):
Expand Down Expand Up @@ -79,7 +79,7 @@ def __syncMiddleware(request: HttpRequest):
if hasattr(request, "supertokens") and isinstance(
request.supertokens, SessionContainer # type: ignore
):
manage_cookies_post_response(
manage_session_post_response(
request.supertokens, result # type: ignore
)
return result.response
Expand Down
4 changes: 2 additions & 2 deletions supertokens_python/framework/fastapi/fastapi_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
FastApiResponse,
)
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.supertokens import manage_cookies_post_response
from supertokens_python.supertokens import manage_session_post_response

st = Supertokens.get_instance()
from fastapi.responses import Response
Expand All @@ -56,7 +56,7 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
if hasattr(request.state, "supertokens") and isinstance(
request.state.supertokens, SessionContainer
):
manage_cookies_post_response(request.state.supertokens, result)
manage_session_post_response(request.state.supertokens, result)
if isinstance(result, FastApiResponse):
return result.response
except SuperTokensError as e:
Expand Down
4 changes: 2 additions & 2 deletions supertokens_python/framework/flask/flask_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def set_before_after_request(self):
app = self.app
from supertokens_python.framework.flask.flask_request import FlaskRequest
from supertokens_python.framework.flask.flask_response import FlaskResponse
from supertokens_python.supertokens import manage_cookies_post_response
from supertokens_python.supertokens import manage_session_post_response

from flask.wrappers import Response

Expand Down Expand Up @@ -65,7 +65,7 @@ def _(response: Response):

response_ = FlaskResponse(response)
if hasattr(g, "supertokens") and g.supertokens is not None:
manage_cookies_post_response(g.supertokens, response_)
manage_session_post_response(g.supertokens, response_)

return response_.response

Expand Down
2 changes: 1 addition & 1 deletion supertokens_python/framework/flask/flask_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def set_header(self, key: str, value: str):
)
self.headers.append((key, value))
else:
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
self.response.headers.add(key, value)
self.response.headers[key] = value
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved

def get_header(self, key: str) -> Union[None, str]:
if self.response is not None:
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
8 changes: 6 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,9 @@ 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,
user.user_id,
user_context=user_context,
)
return SignInPostOkResult(user, session)

Expand Down Expand Up @@ -204,6 +206,8 @@ 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,
user.user_id,
user_context=user_context,
)
return SignUpPostOkResult(user, session)
6 changes: 5 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,11 @@ async def consume_code_post(
)

session = await create_new_session(
api_options.request, user.user_id, {}, {}, user_context=user_context
api_options.request,
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[
porcellus marked this conversation as resolved.
Show resolved Hide resolved
[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
41 changes: 25 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,14 @@ 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)

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 +60,10 @@ 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")

assert isinstance(expiry_time, int)

if expiry_time < get_timestamp_ms():
raise Exception("Access token expired")
Expand All @@ -87,3 +83,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 payload.get("userData") is None
or not isinstance(payload.get("refreshTokenHash1"), str)
or not isinstance(payload.get("expiryTime"), int)
or not isinstance(payload.get("timeCreated"), int)
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
):
raise Exception(
"Access token does not contain all the information. Maybe the structure has changed?"
)
1 change: 1 addition & 0 deletions supertokens_python/recipe/session/asyncio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ async def refresh_session(
request = FRAMEWORKS[
SessionRecipe.get_instance().app_info.framework
].wrap_request(request)

return await SessionRecipe.get_instance().recipe_implementation.refresh_session(
request, user_context
)
Expand Down
15 changes: 12 additions & 3 deletions supertokens_python/recipe/session/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,23 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from typing import TYPE_CHECKING, List

if TYPE_CHECKING:
from .utils import TokenTransferMethod

SESSION_REFRESH = "/session/refresh"
SIGNOUT = "/signout"
ACCESS_TOKEN_COOKIE_KEY = "sAccessToken"
REFRESH_TOKEN_COOKIE_KEY = "sRefreshToken"
ID_REFRESH_TOKEN_COOKIE_KEY = "sIdRefreshToken"
FRONT_TOKEN_HEADER_SET_KEY = "front-token"
ANTI_CSRF_HEADER_KEY = "anti-csrf"
RID_HEADER_KEY = "rid"
ID_REFRESH_TOKEN_HEADER_SET_KEY = "id-refresh-token"
ID_REFRESH_TOKEN_HEADER_GET_KEY = "id-refresh-token"
AUTH_MODE_HEADER_KEY = "st-auth-mode"
AUTHORIZATION_HEADER_KEY = "authorization"
ACCESS_TOKEN_HEADER_KEY = "st-access-token"
REFRESH_TOKEN_HEADER_KEY = "st-refresh-token"
ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"

available_token_transfer_methods: List[TokenTransferMethod] = ["cookie", "header"]
Loading