Skip to content

Commit

Permalink
Add more cookie options
Browse files Browse the repository at this point in the history
  • Loading branch information
ahopkins committed Jun 10, 2020
1 parent 47399fc commit a21f7d3
Show file tree
Hide file tree
Showing 24 changed files with 145 additions and 47 deletions.
1 change: 1 addition & 0 deletions example/basic_with_user_secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ async def retrieve_user_secret(user_id):
print(f"{user_id=}")
return f"user_id|{user_id}"


app = Sanic(__name__)
Initialize(
app,
Expand Down
8 changes: 6 additions & 2 deletions example/inline_tokens_and_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ async def run():

payload = await app.auth.verify_token(token, return_payload=True)
try:
is_verified = await app.auth.verify_token(token, custom_claims=[UserIsPrime])
is_verified = await app.auth.verify_token(
token, custom_claims=[UserIsPrime]
)
except exceptions.InvalidCustomClaimError:
is_verified = False
finally:
Expand All @@ -50,7 +52,9 @@ async def run():

payload = await app.auth.verify_token(token, return_payload=True)
try:
is_verified = await app.auth.verify_token(token, custom_claims=[UserIsPrime])
is_verified = await app.auth.verify_token(
token, custom_claims=[UserIsPrime]
)
except exceptions.InvalidCustomClaimError:
is_verified = False
finally:
Expand Down
25 changes: 17 additions & 8 deletions sanic_jwt/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
import jwt

from . import exceptions, utils
from .exceptions import (InvalidCustomClaimError, InvalidVerification,
InvalidVerificationError, SanicJWTException)
from .exceptions import (
InvalidCustomClaimError,
InvalidVerification,
InvalidVerificationError,
SanicJWTException
)

logger = logging.getLogger(__name__)
claim_label = {"iss": "issuer", "iat": "iat", "nbf": "nbf", "aud": "audience"}
Expand Down Expand Up @@ -125,7 +129,9 @@ async def retrieve_user(self, *args, **kwargs):


class Authentication(BaseAuthentication):
async def _check_authentication(self, request, request_args, request_kwargs):
async def _check_authentication(
self, request, request_args, request_kwargs
):
"""
Checks a request object to determine if that request contains a valid,
and authenticated JWT.
Expand Down Expand Up @@ -247,13 +253,14 @@ async def _get_secret(self, token=None, payload=None, encode=False):
if self.config.user_secret_enabled():
if not payload:
algorithm = self._get_algorithm()
payload = jwt.decode(token, verify=False,
algorithms=[algorithm])
payload = jwt.decode(
token, verify=False, algorithms=[algorithm]
)
user_id = payload.get("user_id")
return await utils.call(
self.retrieve_user_secret,
user_id=user_id,
encode=self._is_asymmetric and encode
encode=self._is_asymmetric and encode,
)

if self._is_asymmetric and encode:
Expand All @@ -273,7 +280,7 @@ def _get_token_from_cookies(self, request, refresh_token):
token = request.cookies.get(cookie_token_name(), None)
if not refresh_token and self.config.cookie_split() and token:
signature_name = self.config.cookie_split_signature_name()
token += "." + request.cookies.get(signature_name, '')
token += "." + request.cookies.get(signature_name, "")
return token

def _get_token_from_headers(self, request, refresh_token):
Expand Down Expand Up @@ -516,7 +523,9 @@ async def is_authenticated(self, request):
async def retrieve_refresh_token_from_request(self, request):
return await self._get_refresh_token(request)

async def verify_token(self, token, return_payload=False, custom_claims=None):
async def verify_token(
self, token, return_payload=False, custom_claims=None
):
"""
Perform an inline verification of a token.
"""
Expand Down
3 changes: 3 additions & 0 deletions sanic_jwt/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
"claim_nbf_delta": 0,
"cookie_access_token_name": "access_token",
"cookie_domain": "",
"cookie_expires": None,
"cookie_httponly": True,
"cookie_max_age": 0,
"cookie_path": "/",
"cookie_refresh_token_name": "refresh_token",
"cookie_secure": False,
"cookie_set": False,
"cookie_split": False,
"cookie_split_signature_name": "access_token_signature",
Expand Down
4 changes: 3 additions & 1 deletion sanic_jwt/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,9 @@ async def decorated_function(request, *args, **kwargs):
f, request, *args, **kwargs
) # noqa

payload = await instance.auth.extract_payload(request, verify=False)
payload = await instance.auth.extract_payload(
request, verify=False
)
user = await utils.call(
instance.auth.retrieve_user, request, payload
)
Expand Down
4 changes: 3 additions & 1 deletion sanic_jwt/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ async def post(self, request, *args, **kwargs):

# TODO:
# - Add more exceptions
payload = await self.instance.auth.extract_payload(request, verify=False)
payload = await self.instance.auth.extract_payload(
request, verify=False
)

try:
user = await utils.call(
Expand Down
4 changes: 1 addition & 3 deletions sanic_jwt/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ class UserSecretNotImplemented(SanicJWTException):
status_code = 500

def __init__(
self,
message="User secrets have not been enabled.",
**kwargs
self, message="User secrets have not been enabled.", **kwargs
):
super().__init__(message, **kwargs)

Expand Down
34 changes: 27 additions & 7 deletions sanic_jwt/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@

from .base import BaseDerivative

COOKIE_OPTIONS = (
("domain", "cookie_domain"),
("expires", "cookie_expires"),
("max-age", "cookie_max_age"),
("secure", "cookie_secure"),
)


def _set_cookie(response, key, value, config, force_httponly=None):
response.cookies[key] = value
response.cookies[key]["httponly"] = config.cookie_httponly() if force_httponly is None else force_httponly
response.cookies[key]["httponly"] = (
config.cookie_httponly() if force_httponly is None else force_httponly
)
response.cookies[key]["path"] = config.cookie_path()

domain = config.cookie_domain()
if domain:
response.cookies[key]["domain"] = domain
for item, option in COOKIE_OPTIONS:
value = getattr(config, option)()
if value:
response.cookies[key][item] = value


class Responses(BaseDerivative):
Expand All @@ -33,9 +43,19 @@ def get_token_response(

if config.cookie_split():
signature_name = config.cookie_split_signature_name()
header_payload, signature = access_token.rsplit('.', maxsplit=1)
_set_cookie(response, key, header_payload, config, force_httponly=False)
_set_cookie(response, signature_name, signature, config, force_httponly=True)
header_payload, signature = access_token.rsplit(
".", maxsplit=1
)
_set_cookie(
response, key, header_payload, config, force_httponly=False
)
_set_cookie(
response,
signature_name,
signature,
config,
force_httponly=True,
)
else:
_set_cookie(response, key, access_token, config)

Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ async def retrieve_user(request, payload, *args, **kwargs):
def retrieve_user_secret():
async def retrieve_user_secret(user_id, **kwargs):
return f"foobar<{user_id}>"

yield retrieve_user_secret


Expand Down
2 changes: 1 addition & 1 deletion tests/test_async_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
"""


import jwt
import pytest
from sanic import Blueprint, Sanic
from sanic.response import text
from sanic.views import HTTPMethodView

import jwt
from sanic_jwt import Authentication, initialize, protected

ALL_METHODS = ["GET", "OPTIONS"]
Expand Down
1 change: 1 addition & 0 deletions tests/test_claims.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import datetime, timedelta

import jwt

from freezegun import freeze_time


Expand Down
2 changes: 1 addition & 1 deletion tests/test_complete_authentication.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from datetime import datetime, timedelta

import jwt
import pytest
from sanic import Sanic
from sanic.response import json

import jwt
from freezegun import freeze_time
from sanic_jwt import Authentication, exceptions, Initialize, protected

Expand Down
2 changes: 1 addition & 1 deletion tests/test_custom_claims.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import jwt
import pytest
from sanic import Sanic

import jwt
from sanic_jwt import Claim, exceptions, Initialize


Expand Down
4 changes: 2 additions & 2 deletions tests/test_decorators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from sanic import Sanic
from sanic.blueprints import Blueprint
from sanic.response import json, text, html
from sanic.response import html, json, text

from sanic_jwt import Initialize
from sanic_jwt.decorators import inject_user, protected, scoped
Expand Down Expand Up @@ -233,7 +233,7 @@ async def my_protected_static(request):
request, response = sanic_app.test_client.get("/protected/static")

assert response.status == 200
assert response.body == b'<html><body>Home</body></html>'
assert response.body == b"<html><body>Home</body></html>"
assert response.history
assert response.history[0].status_code == 302

Expand Down
2 changes: 1 addition & 1 deletion tests/test_decorators_override_config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from datetime import datetime, timedelta

import jwt
from sanic import Sanic
from sanic.blueprints import Blueprint
from sanic.response import json

import jwt
from freezegun import freeze_time
from sanic_jwt import Initialize
from sanic_jwt.decorators import protected
Expand Down
3 changes: 1 addition & 2 deletions tests/test_endpoints_auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import pytest

import jwt
import pytest


@pytest.fixture
Expand Down
3 changes: 1 addition & 2 deletions tests/test_endpoints_basic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import pytest

import jwt
import pytest


def test_unprotected(app):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_endpoints_cbv.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import jwt
from sanic import Sanic
from sanic.response import json
from sanic.views import HTTPMethodView

import jwt
from sanic_jwt import exceptions, Initialize
from sanic_jwt.decorators import protected

Expand Down
65 changes: 60 additions & 5 deletions tests/test_endpoints_cookies.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import binascii
import os
from datetime import datetime

import jwt
import pytest

from sanic import Sanic
from sanic.response import json

from sanic_jwt import Initialize, protected


Expand Down Expand Up @@ -426,6 +427,7 @@ def test_config_with_cookie_path(users, authenticate):
cookie = response.raw_cookies.get("access_token")
assert cookie.path == path


def test_with_split_cookie(app):
sanic_app, sanicjwt = app
sanicjwt.config.cookie_set.update(True)
Expand All @@ -444,13 +446,17 @@ def test_with_split_cookie(app):
assert token_cookie
assert signature_cookie

raw_token_cookie, raw_signature_cookie = [value.decode(response.headers.encoding) for key, value in response.headers.raw if key.lower() == b'set-cookie']

raw_token_cookie, raw_signature_cookie = [
value.decode(response.headers.encoding)
for key, value in response.headers.raw
if key.lower() == b"set-cookie"
]

assert raw_token_cookie
assert raw_signature_cookie

assert token_cookie.value.count('.') == 1
assert signature_cookie.value.count('.') == 0
assert token_cookie.value.count(".") == 1
assert signature_cookie.value.count(".") == 0
assert "HttpOnly" not in raw_token_cookie
assert "HttpOnly" in raw_signature_cookie

Expand All @@ -475,3 +481,52 @@ def test_with_split_cookie(app):

assert response.status == 200
assert response.json.get("valid") == True


def test_with_cookie_normal(app):
sanic_app, sanicjwt = app
sanicjwt.config.cookie_set.update(True)

_, response = sanic_app.test_client.post(
"/auth",
json={"username": "user1", "password": "abcxyz"},
raw_cookies=True,
)

raw_token_cookie = [
value.decode(response.headers.encoding)
for key, value in response.headers.raw
if key.lower() == b"set-cookie"
][0]

assert raw_token_cookie
assert "httponly" in raw_token_cookie.lower()
assert "expired" not in raw_token_cookie.lower()
assert "secure" not in raw_token_cookie.lower()
assert "max-age" not in raw_token_cookie.lower()


def test_with_cookie_config(app):
sanic_app, sanicjwt = app
sanicjwt.config.cookie_set.update(True)
sanicjwt.config.cookie_httponly.update(False)
sanicjwt.config.cookie_expires.update(datetime(2100, 1, 1))
sanicjwt.config.cookie_secure.update(True)
sanicjwt.config.cookie_max_age.update(10)

_, response = sanic_app.test_client.post(
"/auth",
json={"username": "user1", "password": "abcxyz"},
raw_cookies=True,
)

raw_token_cookie = [
value.decode(response.headers.encoding)
for key, value in response.headers.raw
if key.lower() == b"set-cookie"
][0]
assert raw_token_cookie
assert "httponly" not in raw_token_cookie.lower()
assert "expires=fri, 01-jan-2100 00:00:00 gmt" in raw_token_cookie.lower()
assert "secure" in raw_token_cookie.lower()
assert "max-age=10" in raw_token_cookie.lower()
Loading

0 comments on commit a21f7d3

Please sign in to comment.