Skip to content

Commit

Permalink
Big refactor. add more claims, token invalidation and user HttpOnly
Browse files Browse the repository at this point in the history
Cookie with token
  • Loading branch information
pbgc committed Jul 23, 2020
1 parent 9f6581b commit 0ec7d6b
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 187 deletions.
25 changes: 0 additions & 25 deletions jwt_auth/compat.py

This file was deleted.

3 changes: 3 additions & 0 deletions jwt_auth/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib.auth import get_user_model

User = get_user_model()
33 changes: 4 additions & 29 deletions jwt_auth/forms.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
from calendar import timegm
from datetime import datetime

from django import forms
from django.contrib.auth import authenticate
from django.utils.translation import ugettext as _

from jwt_auth import settings
from jwt_auth.compat import User


jwt_payload_handler = settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = settings.JWT_ENCODE_HANDLER
jwt_decode_handler = settings.JWT_DECODE_HANDLER
jwt_get_user_id_from_payload = settings.JWT_PAYLOAD_GET_USER_ID_HANDLER
from jwt_auth.core import User


class JSONWebTokenForm(forms.Form):
password = forms.CharField()

def __init__(self, *args, **kwargs):
super(JSONWebTokenForm, self).__init__(*args, **kwargs)

super().__init__(*args, **kwargs)
# Dynamically add the USERNAME_FIELD to self.fields.
self.fields[self.username_field] = forms.CharField()

Expand All @@ -32,34 +24,17 @@ def username_field(self):
return 'username'

def clean(self):
cleaned_data = super(JSONWebTokenForm, self).clean()
cleaned_data = super().clean()
credentials = {
self.username_field: cleaned_data.get(self.username_field),
'password': cleaned_data.get('password')
}

if all(credentials.values()):
user = authenticate(**credentials)

if user:
if not user.is_active:
raise forms.ValidationError(_('User account is disabled.'))

payload = jwt_payload_handler(user)

# Include original issued at time for a brand new token,
# to allow token refresh
if settings.JWT_ALLOW_REFRESH:
payload['iat'] = timegm(
datetime.utcnow().utctimetuple()
)

self.object = {
'token': jwt_encode_handler(payload)
}

self.user = user

cleaned_data["user"] = user
else:
raise forms.ValidationError(_('Unable to login with provided credentials.'))
else:
Expand Down
46 changes: 27 additions & 19 deletions jwt_auth/mixins.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from django.http import HttpResponse
import json

from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.utils.translation import ugettext as _

import jwt
from jwt_auth import settings, exceptions
from jwt_auth.core import User
from jwt_auth.utils import get_authorization_header
from jwt_auth.compat import json, smart_text, User
from jwt_auth.utils import is_token_blacklisted


jwt_decode_handler = settings.JWT_DECODE_HANDLER
Expand All @@ -21,7 +24,7 @@ class JSONWebTokenAuthMixin(object):
HTTP header, prepended with the string specified in the setting
`JWT_AUTH_HEADER_PREFIX`. For example:
Authorization: JWT eyJhbGciOiAiSFMyNTYiLCAidHlwIj
Authorization: Bearer eyJhbGciOiAiSFMyNTYiLCAidHlwIj
"""
www_authenticate_realm = 'api'
payload = None
Expand All @@ -31,24 +34,22 @@ def dispatch(self, request, *args, **kwargs):
try:
request.user, request.token = self.authenticate(request)
except exceptions.AuthenticationFailed as e:
response = HttpResponse(
json.dumps({'errors': [str(e)]}),
status=401,
content_type='application/json'
)

response = JsonResponse({'errors': [str(e)]}, status=401)
response['WWW-Authenticate'] = self.authenticate_header(request)

return response
return super().dispatch(request, *args, **kwargs)

return super(JSONWebTokenAuthMixin, self).dispatch(
request, *args, **kwargs)

def authenticate(self, request):
@staticmethod
def get_jwt_value(request):
auth = get_authorization_header(request).split()
auth_header_prefix = settings.JWT_AUTH_HEADER_PREFIX.lower()

if not auth or smart_text(auth[0].lower()) != auth_header_prefix:
if not auth:
if settings.JWT_AUTH_COOKIE:
return request.COOKIES.get(settings.JWT_AUTH_COOKIE)
raise exceptions.AuthenticationFailed()

if auth[0].lower().decode("utf-8") != auth_header_prefix:
raise exceptions.AuthenticationFailed()

if len(auth) == 1:
Expand All @@ -57,25 +58,32 @@ def authenticate(self, request):
)
elif len(auth) > 2:
raise exceptions.AuthenticationFailed(
_('Invalid Authorization header. Credentials string should not contain spaces.'))
_('Invalid Authorization header. Credentials string should not contain spaces.')
)

return auth[1]

def authenticate(self, request):
jwt_value = JSONWebTokenAuthMixin.get_jwt_value(request)
if is_token_blacklisted(jwt_value):
raise exceptions.AuthenticationFailed(_('Invalid Token!'))
try:
self.payload = jwt_decode_handler(auth[1])
self.payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
raise exceptions.AuthenticationFailed(_('Signature has expired.'))
except jwt.DecodeError:
raise exceptions.AuthenticationFailed(_('Error decoding signature.'))

user = self.authenticate_credentials(self.payload)

return (user, auth[1])
return (user, jwt_value)

def authenticate_credentials(self, payload):
"""
Returns an active user that matches the payload's user id and email.
"""
try:
user_id = jwt_get_user_id_from_payload(payload)

if user_id:
user = User.objects.get(pk=user_id, is_active=True)
else:
Expand Down
56 changes: 25 additions & 31 deletions jwt_auth/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,27 @@
from jwt_auth.utils import import_from_string


JWT_ENCODE_HANDLER = getattr(
settings,
'JWT_ENCODE_HANDLER',
import_from_string('jwt_auth.utils.jwt_encode_handler')
JWT_ENCODE_HANDLER = import_from_string(
getattr(settings, 'JWT_ENCODE_HANDLER', 'jwt_auth.utils.jwt_encode_handler')
)

JWT_DECODE_HANDLER = getattr(
settings,
'JWT_DECODE_HANDLER',
import_from_string('jwt_auth.utils.jwt_decode_handler')
JWT_DECODE_HANDLER = import_from_string(
getattr(settings, 'JWT_DECODE_HANDLER', 'jwt_auth.utils.jwt_decode_handler')
)

JWT_PAYLOAD_HANDLER = getattr(
settings,
'JWT_PAYLOAD_HANDLER',
import_from_string('jwt_auth.utils.jwt_payload_handler')
JWT_PAYLOAD_HANDLER = import_from_string(
getattr(settings, 'JWT_PAYLOAD_HANDLER', 'jwt_auth.utils.jwt_payload_handler')
)

JWT_PAYLOAD_GET_USER_ID_HANDLER = getattr(
settings,
'JWT_PAYLOAD_GET_USER_ID_HANDLER',
import_from_string('jwt_auth.utils.jwt_get_user_id_from_payload_handler')
JWT_PAYLOAD_GET_USER_ID_HANDLER = import_from_string(
getattr(settings, 'JWT_PAYLOAD_GET_USER_ID_HANDLER', 'jwt_auth.utils.jwt_get_user_id_from_payload_handler')
)

JWT_SECRET_KEY = getattr(
settings,
'JWT_SECRET_KEY',
settings.SECRET_KEY
)
JWT_PRIVATE_KEY = getattr(settings, 'JWT_PRIVATE_KEY', None)

JWT_PUBLIC_KEY = getattr(settings, 'JWT_PUBLIC_KEY', None)

JWT_SECRET_KEY = getattr(settings, 'JWT_SECRET_KEY', settings.SECRET_KEY)

JWT_ALGORITHM = getattr(settings, 'JWT_ALGORITHM', 'HS256')

Expand All @@ -43,18 +35,20 @@

JWT_LEEWAY = getattr(settings, 'JWT_LEEWAY', 0)

JWT_EXPIRATION_DELTA = getattr(
settings,
'JWT_EXPIRATION_DELTA',
datetime.timedelta(seconds=300)
)
JWT_EXPIRATION_DELTA = getattr(settings, 'JWT_EXPIRATION_DELTA', datetime.timedelta(seconds=300))

JWT_ALLOW_REFRESH = getattr(settings, 'JWT_ALLOW_REFRESH', False)

JWT_REFRESH_EXPIRATION_DELTA = getattr(
settings,
'JWT_REFRESH_EXPIRATION_DELTA',
datetime.timedelta(seconds=300)
)
JWT_REFRESH_EXPIRATION_DELTA = getattr(settings, 'JWT_REFRESH_EXPIRATION_DELTA', datetime.timedelta(seconds=300))

JWT_AUTH_HEADER_PREFIX = getattr(settings, 'JWT_AUTH_HEADER_PREFIX', 'Bearer')

JWT_AUDIENCE = getattr(settings, 'JWT_AUDIENCE', None)

JWT_ISSUER = getattr(settings, 'JWT_ISSUER', None)

JWT_AUTH_COOKIE = getattr(settings, 'JWT_AUTH_COOKIE', None)

JWT_REDIS_HOST = getattr(settings, 'JWT_REDIS_HOST', "localhost")
JWT_REDIS_PORT = getattr(settings, 'JWT_REDIS_HOST', 6379)
JWT_REDIS_DB = getattr(settings, 'JWT_REDIS_DB', 0)
82 changes: 68 additions & 14 deletions jwt_auth/utils.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,78 @@
from __future__ import unicode_literals
from datetime import datetime
import importlib

from django.contrib.auth import get_user_model

try:
import redis
except ImportError:
redis = None

import jwt

from . import settings


def blacklist_token(token):
if redis is not None:
try:
r = redis.Redis(
host=settings.JWT_REDIS_HOST,
port=settings.JWT_REDIS_PORT,
db=settings.JWT_REDIS_DB
)
r.setex(
token,
settings.JWT_EXPIRATION_DELTA,
value="EXPIRED"
)
except Exception as e:
print(e)


def is_token_blacklisted(token):
if redis is not None:
try:
r = redis.Redis(
host=settings.JWT_REDIS_HOST,
port=settings.JWT_REDIS_PORT,
db=settings.JWT_REDIS_DB
)
return r.get(token) is not None
except Exception as e:
print(e)
return False


def jwt_payload_handler(user):
from jwt_auth import settings

try:
username = user.get_username()
except AttributeError:
username = user.username

return {
'user_id': user.pk,
'email': user.email,
'username': username,
'exp': datetime.utcnow() + settings.JWT_EXPIRATION_DELTA
try:
username_field = get_user_model().USERNAME_FIELD
except AttributeError:
username_field = 'username'

payload = {
"user_id": user.pk,
username_field: username,
"exp": datetime.utcnow() + settings.JWT_EXPIRATION_DELTA
}

if hasattr(user, 'email'):
payload['email'] = user.email

if settings.JWT_AUDIENCE is not None:
payload['aud'] = settings.JWT_AUDIENCE

if settings.JWT_ISSUER is not None:
payload['iss'] = settings.JWT_ISSUER

return payload


def jwt_get_user_id_from_payload_handler(payload):
"""
Expand All @@ -30,28 +83,29 @@ def jwt_get_user_id_from_payload_handler(payload):


def jwt_encode_handler(payload):
from jwt_auth import settings

key = settings.JWT_PRIVATE_KEY or settings.JWT_SECRET_KEY
return jwt.encode(
payload,
settings.JWT_SECRET_KEY,
key,
settings.JWT_ALGORITHM
).decode('utf-8')


def jwt_decode_handler(token):
from jwt_auth import settings

options = {
'verify_exp': settings.JWT_VERIFY_EXPIRATION,
}

key = settings.JWT_PUBLIC_KEY or settings.JWT_SECRET_KEY
return jwt.decode(
token,
settings.JWT_SECRET_KEY,
key,
settings.JWT_VERIFY,
options=options,
leeway=settings.JWT_LEEWAY
leeway=settings.JWT_LEEWAY,
audience=settings.JWT_AUDIENCE,
issuer=settings.JWT_ISSUER,
algorithms=[settings.JWT_ALGORITHM]
)


Expand Down
Loading

0 comments on commit 0ec7d6b

Please sign in to comment.