Skip to content

Commit

Permalink
Document JWT generation using AuthTokenManager (#156)
Browse files Browse the repository at this point in the history
  • Loading branch information
apragacz committed Mar 26, 2024
1 parent 5c0b6ee commit cab1f0a
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 20 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ Full documentation for the project is available at [https://django-rest-registra

## Current limitations

* Supports only one email per user (as model field)
* No JWT support (but you can use it along libraries like [django-rest-framework-simplejwt](https://github.com/davesque/django-rest-framework-simplejwt))
* Supports only one email per user (as model field)
* No JWT support (but you can easily
[implement one](https://django-rest-registration.readthedocs.io/en/latest/cookbook/jwt.html)
or use Django REST Registration along libraries like
[django-rest-framework-simplejwt](https://github.com/davesque/django-rest-framework-simplejwt))


## Installation & Configuration
Expand Down
7 changes: 7 additions & 0 deletions docs/cookbook/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Cookbook
========

.. toctree::
:maxdepth: 2

jwt
85 changes: 85 additions & 0 deletions docs/cookbook/jwt.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
.. _generating-and-authenticating-using-jwt:

Generating and Authenticating using JWT
=======================================

One can implement generating and authenticating using JWT. Below is the
AuthTokenManager implementation for JWT::

class AuthJWTManager(AbstractAuthTokenManager):

def get_authentication_class(self) -> Type[BaseAuthentication]:
return JWTAuthentication

def get_app_names(self) -> Sequence[str]:
return [
'tests.testapps.custom_authtokens', # update with your Django app
]

def provide_token(self, user: 'AbstractBaseUser') -> AuthToken:
encoded_jwt = jwt.encode(
{"user_id": user.pk},
settings.SECRET_KEY,
algorithm=JWTAuthentication.ALGORITHM,
)
return AuthToken(encoded_jwt)

def revoke_token(
self, user: 'AbstractBaseUser', *,
token: Optional[AuthToken] = None) -> None:
raise AuthTokenNotRevoked()

And here is the Django REST Framework authentication class::

class JWTAuthentication(BaseAuthentication):
ALGORITHM = "HS256"

def authenticate(self, request):
"""
Returns a `User` if a correct username and password have been supplied
using HTTP Basic authentication. Otherwise returns `None`.
"""
auth = get_authorization_header(request).split()

if not auth or auth[0].lower() != b'bearer':
return None

if len(auth) == 1:
msg = _('Invalid authorization header. No credentials provided.')
raise AuthenticationFailed(msg)
if len(auth) > 2:
msg = _(
'Invalid authorization header. Credentials string should not'
' contain spaces.'
)
raise AuthenticationFailed(msg)

encoded_jwt = auth[1]

try:
jwt_data = jwt.decode(
encoded_jwt,
settings.SECRET_KEY,
algorithms=[self.ALGORITHM],
)
except jwt.ExpiredSignatureError:
msg = _('Expired JWT.')
raise AuthenticationFailed(msg) from None
except jwt.InvalidTokenError:
msg = _('Invalid JWT payload.')
raise AuthenticationFailed(msg) from None

try:
user_id = jwt_data["user_id"]
except KeyError:
msg = _('Missing user info in JWT.')
raise AuthenticationFailed(msg) from None

user_class = get_user_model()
try:
user = user_class.objects.get(pk=user_id)
except user_class.DoesNotExist:
msg = _('User not found.')
raise AuthenticationFailed(msg) from None

return (user, encoded_jwt)
5 changes: 4 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ User registration REST API, based on Django-REST-Framework.
install
quickstart
detailed_configuration/index
cookbook/index


Requirements
Expand Down Expand Up @@ -51,7 +52,9 @@ Current limitations
-------------------

- Supports only one email per user (as user model field)
- No JWT support (but you can use it along libraries like
- No built-it JWT support (but you can easily
:ref:`implement one <generating-and-authenticating-using-jwt>` or
use Django REST Registration along libraries like
`django-rest-framework-simplejwt <https://github.com/davesque/django-rest-framework-simplejwt>`__)


Expand Down
2 changes: 2 additions & 0 deletions requirements/requirements-dev.lock.txt
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ pygments==2.17.2
# readme-renderer
# rich
# sphinx
pyjwt==2.8.0
# via -r requirements/requirements-test.lock.txt
pylint==3.0.3
# via -r requirements/requirements-test.lock.txt
pyproject-api==1.6.1
Expand Down
3 changes: 3 additions & 0 deletions requirements/requirements-test.in
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ sphinx_rtd_theme
sphinx-autobuild
sphinx-jinja
doc8

# cookbook
pyjwt
2 changes: 2 additions & 0 deletions requirements/requirements-test.lock.txt
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ pygments==2.17.2
# readme-renderer
# rich
# sphinx
pyjwt==2.8.0
# via -r requirements/requirements-test.in
pylint==3.0.3
# via -r requirements/requirements-test.in
pyproject-api==1.6.1
Expand Down
67 changes: 59 additions & 8 deletions tests/helpers/settings.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,100 @@
from typing import Callable
from typing import Any, Callable, Dict, Optional

from django.conf import settings
from django.conf import LazySettings, settings
from django.test.utils import TestContextDecorator, override_settings
from rest_framework.settings import api_settings
from rest_framework.views import APIView

from tests.helpers.common import shallow_merge_dicts

_API_VIEW_FIELDS = [
"renderer_classes",
"parser_classes",
"authentication_classes",
"throttle_classes",
"permission_classes",
"content_negotiation_class",
"metadata_class",
"versioning_class",
]

def override_rest_registration_settings(new_settings: dict = None):

def _noop():
pass


def _api_view_fields_patcher():
for field_name in _API_VIEW_FIELDS:
api_settings_name = f"DEFAULT_{field_name.upper()}"
api_setting_value = getattr(api_settings, api_settings_name)
setattr(APIView, field_name, api_setting_value)


def override_rest_registration_settings(
new_settings: Optional[Dict[str, Any]] = None,
) -> TestContextDecorator:
if new_settings is None:
new_settings = {}

def processor(current_settings):
def processor(current_settings: LazySettings) -> Dict[str, Any]:
return {
'REST_REGISTRATION': shallow_merge_dicts(
current_settings.REST_REGISTRATION, new_settings),
}

return OverrideSettingsDecorator(processor)
return OverrideSettingsDecorator(processor, _noop)


def override_rest_framework_settings(
new_settings: Optional[Dict[str, Any]] = None,
) -> TestContextDecorator:
if new_settings is None:
new_settings = {}

def processor(current_settings: LazySettings) -> Dict[str, Any]:
current_rest_framework_settings = getattr(
current_settings, "REST_FRAMEWORK", {},
)
return {
"REST_FRAMEWORK": shallow_merge_dicts(
current_rest_framework_settings, new_settings),
}

return OverrideSettingsDecorator(processor, _api_view_fields_patcher)


def override_auth_model_settings(new_auth_model):

def processor(current_settings):
return {'AUTH_USER_MODEL': new_auth_model}

return OverrideSettingsDecorator(processor)
return OverrideSettingsDecorator(processor, _noop)


class OverrideSettingsDecorator(TestContextDecorator):

def __init__(self, settings_processor: Callable[[dict], dict]):
def __init__(
self,
settings_processor: Callable[[LazySettings], Dict[str, Any]],
patcher: Callable[[], None],
) -> None:
super().__init__()
self._settings_processor = settings_processor
self._patcher = patcher
# Parent decorator will be created on-demand.
self._parent_decorator = None

def enable(self):
self._parent_decorator = self._build_parent_decorator()
return self._parent_decorator.enable()
self._parent_decorator.enable()
self._patcher()

def disable(self):
try:
return self._parent_decorator.disable()
finally:
self._parent_decorator = None
self._patcher()

def decorate_class(self, cls):
return self._build_parent_decorator().decorate_class(cls)
Expand Down
17 changes: 12 additions & 5 deletions tests/helpers/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,31 @@

class ViewProvider:

def __init__(self, view_name, app_name='rest_registration'):
def __init__(self, view_name, app_name='rest_registration', view_cls=None):
self.view_name = view_name
self.app_name = app_name
self.view_cls = view_cls
self._view_url = None
self._view_func = None

@property
def full_view_name(self):
def full_view_name(self) -> str:
return f"{self.app_name}:{self.view_name}"

@property
def view_url(self):
def view_url(self) -> str:
if self._view_url is None:
self._view_url = reverse(self.full_view_name)
return self._view_url

def view_func(self, *args, **kwargs):
def get_view_func(self):
if self.view_cls:
return self.view_cls.as_view()
if self._view_func is None:
match = resolve(self.view_url)
self._view_func = match.func
return self._view_func(*args, **kwargs)
return self._view_func

def view_func(self, request, *args, **kwargs):
view_f = self.get_view_func()
return view_f(request, *args, **kwargs)
Loading

0 comments on commit cab1f0a

Please sign in to comment.