django-easyjwt


Namedjango-easyjwt JSON
Version 0.6.2 PyPI version JSON
download
home_pagehttps://github.com/garrethcain/django-easyjwt
SummaryA convenient package for building both client and server implementations for JWTs or Json Web Tokens. It allows you to build either side by only including the module you need and includes an optional custom user model.
upload_time2025-02-14 22:37:29
maintainerNone
docs_urlNone
authorGarreth Cain
requires_python>=3.8
licenseMIT
keywords
VCS
bugtrack_url
requirements asgiref build certifi cfgv charset-normalizer distlib Django django-easyjwt djangorestframework filelock identify idna mypy mypy-extensions nodeenv packaging platformdirs pre-commit pyproject_hooks pytz PyYAML requests ruamel.yaml ruamel.yaml.clib ruff sqlparse tomli types-requests typing_extensions urllib3 virtualenv
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Django-EasyJWT

This is a package for the implementation of a remote authentication backend
for Django apps, primarily meant for use with JWTs, also supporting sessions as
well. Ie. elevated users could log into the Django `/admin/` once authenticated.
The target is microservice ecosystems, with several independant services all
authenticating against a central authentication services, or CAS with permissions
and other custom user data able to be used in the authentication and Authorization
process.

### Why does this exist?

I once had a several services with the same users; so I created a a centralised authentication
service to avoid the confusion of different passwords on, what to the outside, appeared to be
the same service. Then we added more services, but not all users were allowed access to
these, which is where the extra data came from.

Then some of our customer decided they wanted to also have this notion of users, staff and
admins; but we'd already used Django's built in solution for our staff. So we extended the
extra data to include permissions too.

The PyPi package can be found here: https://pypi.org/project/django-easyjwt

ACKNOWLEDGEMENTS
This package is heavily based on Djangorestframework_simplejwt and heavily influenced by SimpleJWT.

This package is used in the example
[auth-client-service-example](https://github.com/garrethcain/django-easyjwt-example)
project.

# Change Log

2024-03-19 - 4.0.2
    - removed dependency from easyjwt_user migrations.
    - added raw_data to the context var of the custom serializer so custom data stripped by the serializer
      can still be accessed.
    - Changed the exception raise for a remote user model to show text and not try parse to json.

# How To

There are, at a minimum, two components required for this to work;

1. An Auth-Service, to authenticate against,
2. A Client-Service, users want to use.

This package is made of of three sub-packages; easyjwt-Auth, RemoteJWT-Client, and RemoteJWT-User.
With the Auth & Client used in the separate Auth and Client services and the User being used in both,
or neither.

The idea being that you can have any number of client-services using the same
auth-service to validate login requests and your auth-service is behind some
kind of private network and not public facing.

This package is a wrapper for all the main components of the auth-service and client-service; eg.

* /token/ to obtain an access and a refresh token,
    and create/update the local instance.
* /token/refresh/ to refresh an expired access token.
* /token/verify/ to confirm if a token is valid or not.

The above urls in the client-service just proxy requests to the remote
Auth-Service, configured in `settings.py` `EASY_JWT` dict, but creating a
local user object if required.

All that is needed is to add the Django-EasyJWT URLs to your client-service. The
auth-service remains mostly vanilla aside from maybe using a custom User model,
include in this package as well, for convenience.

You can't create users in the local client-service!
If you retrieve a user from the auth-service with the same ID you will overwrite
the local record with data from the auth-service.

Your project can use HMAC by implementing some HMAC backend locally. The HMAC
keys will be kept local to the service and not centralised in the Auth-Service.
The Auth-Service is intentionally kept lean and only handles "users".


# Get Started

What we'll be doing;

1. Create an Auth-Service
2. Create a Client-Service



## Create an Auth-Service

Always upgrade pip first.
```
$ pip install --upgrade pip
```

Create a temporary virtual environment to install django so we can create a
project.
```
$ python -m virtualenv .venv
```

Activate the virtual environment.
```
$ source .venv/bin/activate
```

Your terminal should look something like this;

`(.venv)  user@domain  ~/Code/ `

With the `(.venv)` part implying you're currently inside a virtual environment.

Install Django so we can create our app.
```
$ pip install django
```

Create our project with the name `config` so the nested directory is named more
conveniently.
```PYTHON
$ django-admin startproject config
```

Rename the outer directory because I personally like having the project's config
kept in a directory called `config` with the outer directory the name of
project.
```PYTHON
$ mv config auth-service
```

You should have a directory structure similar to this.
```
─── auth-service
    ├── config
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    └── manage.py
```

Now let's remove the temp virtual environment by deactivating, deleting the old
one, then creating a new on in the right place.
```
$ deactivate
```

Your terminal should be back to something like this;
`user@domain  ~/Code/ `

Delete the files.
```
$ rm -Rf .venv
```

```
$ cd auth-service
```

Create another virtual environment.
```
$ python -m virtualenv .venv
```

Activate the virtual environment, again. We'll keep this one this time.
```
$ source .venv/bin/activate
```

Again, your terminal should look something like this;
`(.venv)  user@domain  ~/Code/auth-service/ `

Install the packages we're going to use.
```
$ pip install django django_rest_framework django_easyjwt
```

Open `config/settings.py` and add the apps we'll be using to `INSTALLED_APPS`;

```PY
INSTALLED_APPS = [
    ...
    'rest_framework',
    'easyjwt_auth',
    'easyjwt_user',
]
```

We've included `easyjwt_user` so we can use the same User model between
all services. It also includes some convieniences such as update forms and
changes the Username field from `username` to `email`.

Tell Django about the new user model by adding;
```py
AUTH_USER_MODEL = "easyjwt_user.User"
```

Next we need to configure Django Rest Framework. For this example you need just
the following;
```PYTHON
REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework.authentication.SessionAuthentication",
        "easyjwt_auth.authentication.JWTAuthentication",
    ),
}
```

In order to use easyjwt-Auth you will also need to add some configuration for it.

```PY
EASY_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
    "ROTATE_REFRESH_TOKENS": False,
    "BLACKLIST_AFTER_ROTATION": False,
    "UPDATE_LAST_LOGIN": False,
    "ALGORITHM": "HS256",
    "SIGNING_KEY": "d577273ff885c3f84dadb8578bb40000", # You must set this correctly for Production.
    "VERIFYING_KEY": None,
    "AUDIENCE": None,
    "ISSUER": None,
    "JWK_URL": None,
    "LEEWAY": 0,
    "USER_ID_FIELD": "id",
    "USER_ID_CLAIM": "user_id",
    "USER_AUTHENTICATION_RULE": "easyjwt_auth.authentication.default_user_authentication_rule",
    "AUTH_TOKEN_CLASSES": ("easyjwt_auth.tokens.AccessToken",),
    "TOKEN_TYPE_CLAIM": "token_type",
    "TOKEN_USER_CLASS": "easyjwt_auth.models.TokenUser",
    "JTI_CLAIM": "jti",
    "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
    "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
    "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
}
```

In the JWT configuration we use `timedelta` so you need to import `timedelta` at
the top of `config/settings.py`.

`from datetime import timedelta`

Make migrations so we can migrate.
```
$ python manage.py makemigrations
```

Migrate to create the database. An SQLite db is fine for the example. In a
production environment you'd use something a bit more appropriate like PostreSQL or DynamoDB.
```
$ python manage.py migrate
```

And finally you can stand up the auth-service with;
```
$ python manage.py runserver
```

Which should get you something like;
```
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
May 02, 2023 - 10:50:48
Django version 4.0.2, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
```

Hit `^C` so we can create a few users for testing with later on.

We're going to create three test users as below.
* admin | admin@test.com | admin-pass
* staff | staff@test.com | staff-pass
* user | user@test.com | user-pass

Eg.
```
export DJANGO_SUPERUSER_EMAIL=admin@test.com
export DJANGO_SUPERUSER_USERNAME=admin
export DJANGO_SUPERUSER_PASSWORD=admin-pass
python manage.py createsuperuser --noinput
```
or
```
$ python manage.py createsuperuser
```
Then stand up the auth-service and log into `http://127.0.0.1:8000/admin/` login
with the superuser you created above and create the other two users, setting
`is_staff=True` for the staff user.
Log out once you're done and terminate the instance that's running with `^C` and
deactivate this virtual environment.
```
$ deactivate
```

The final step is to configure the Urls. So open `config/urls.py` and add the
following.

Then expost the paths to the JWT endpoints and a user view which is where the
client-service will download the user from.
```py
urlpatterns = [
    ...
    path('auth/', include('easyjwt_auth.urls')),  # gives us access to the auth views.
    path('auth/', include('easyjwt_user.urls')),  # gives us access to the users views.
]
```

And that's it for the Auth-Service. It doesn't need any views or serializers.
Everything is handled by easyjwt-Auth and Django's OEM methods. This service is
super light to run and will handle many requests with ease.

---
## Create a Client-Service

Now we need a client-service that will authenticate against the Auth-Service to
complete the example.

Go up one directory;
```
cd ..
```

We're going to perform the same steps for the client-service that we did for the
auth-service. You should recognise most of this. The main difference is that
this time we'll be using `django_easyjwt` and not creating any local users.


Create a temporary virtual environment to install django so we can create a
project.
```
$ python -m virtualenv .venv
```

Activate the virtual environment.
```
$ source .venv/bin/activate
```

Your terminal should look something like this;

`(.venv)  user@domain  ~/Code/ `

With the `(.venv)` part implying you're currently inside a virtual environment.

Install Django so we can create our app.
```
$ pip install django
```

Create our project with the name `config` so the nested directory is named more
conveniently.
```PYTHON
$ django-admin startproject config
```

Rename the outer directory because I personally like having the project's config
kept in a directory called `config` with the outer directory the name of
project.
```PYTHON
$ mv config client-service
```

You should have a directory structure similar to this.
```
├── auth-service
│   ├── config
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── db.sqlite3
│   └── manage.py
└── client-service
    ├── config
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    └── manage.py
```

Now let's remove the temp virtual environment by deactivating, deleting the old
one, then creating a new on in the right place.
```
$ deactivate
```

Your terminal should be back to something like this;
`user@domain  ~/Code/ `

Delete the files.
```
$ rm -Rf .venv
```

```
$ cd client-service
```

Create another virtual environment.
```
$ python -m virtualenv .venv
```

Activate the virtual environment, again. We'll keep this one this time.
```
$ source .venv/bin/activate
```

Again, your terminal should look something like this;
`(.venv)  user@domain  ~/Code/auth-service/ `

Install the packages we're going to use. We don't need `SimpleJWT` this time
because authentication is handled by the remote auth-service.
```
$ pip install django django_rest_framework django_easyjwt
```

We need to let Django know about the apps we'll be using, so open `settings.py`
and add the following lines to `INSTALLED_APPS`;
```PY
INSTALLED_APPS = [
    ...
    'rest_framework',
    'easyjwt_client',
    'easyjwt_user',
]
```

We need to use the same User model as the auth-service, otherwise the user
returned by the auth-service will cause an integrity error.

```py
AUTH_USER_MODEL = "easyjwt_user.User"
```

Add some configuration for Djang Rest Framework. Change the default behaviour
for all endpoints to require authentication. Then we override the default
authentication classes with the ones from `Django-EasyJWT`.
```py
REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": (
        "rest_framework.permissions.IsAuthenticated",
    ),
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "easyjwt_client.authentication.RemoteJWTAuthentication",     # Use our service
        "rest_framework.authentication.SessionAuthentication",  # Maybe the user has a session...
    ),
}
```

Let Django know that we want to use a custom authentication backend.
```py
# implement out or custom backend for Admin and other views.
AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',    # Default, check the local DB.
    'easyjwt_client.authentication.ModelBackend'         # Our override to check the remote service.
]
```

Time to configure `Django-EasyJWT`. For this example example we're going to run
the auth-server on `:8000` and the client-service on `:8001`. Most of this conf
should be handled through environmental variables in a real project. But we're
just aiming for the absolute minimal working example.

```py
EASY_JWT = {
    "AUTH_HEADER_TYPES": ("Bearer", ),
    "AUTH_HEADER_NAME": "Authorization",
    "REMOTE_AUTH_SERVICE_URL": "http://127.0.0.1:8000", # Where do we reach the Auth-Service
    "REMOTE_AUTH_SERVICE_TOKEN_PATH": "/auth/token/", # The path to login and retrieve a token
    "REMOTE_AUTH_SERVICE_REFRESH_PATH": "/auth/token/refresh/", # The path to refresh a token
    "REMOTE_AUTH_SERVICE_VERIFY_PATH": "/auth/token/verify/", # The path to verify a token
    "REMOTE_AUTH_SERVICE_USER_PATH": "/auth/user/", # The path to get the user object
    "USER_ID_FIELD": "id",
    "USER_ID_CLAIM": "user_id",
}
```

Open `config/urls.py` and add the URLs from easyjwt that will be passed
through to the auth-service.

```py
from django.urls import path, include


urlpatterns = [
    path('admin/', admin.site.urls),
    path('auth/', include("easyjwt_client.urls"))
]
```
Don't forget the `include` import.

We'll add our test view to the urls as well shortly.

All the client-service needs now is an endpoint to prove it's alive. So let's
add a Django app with a view that requires authentication we use to test.
```py
$ django-admin startapp test_app
```

Your directory structure should now look something along the lines of;
```
.
├── config
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
└── test_app
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── migrations
    │   └── __init__.py
    ├── models.py
    ├── tests.py
    └── views.py
```

You can see the new app called `test_app` has been added.

Open `test_app/views.py` and add the following view. Because we changed the
Rest Framework config to use a `DEFAULT_PERMISSION_CLASSES` of `IsAuthenticated`
all views will require authentication.
```py
from rest_framework import generics
from rest_framework.response import Response

class TestView(generics.GenericAPIView):
    def get(self, request):
        return Response("success", status=200)
```

There are no models or serializers, it's the absolute least we can do to get a
success. There is no need to add the `test_app` to the `INSTALLED_APPS` because
it has no models that need migrating.


The absolute final step before we can run some tests is to add the `TestView` to
the client-service's Urls.py so it knows where to send an incoming request.

```py
from django.urls import path, include
from test_app.views import TestView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('auth/', include("easyjwt_client.urls")),
    path('api/test/', TestView.as_view()),
]
```

Don't forget it include the `include` import at the end of
`from django.urls import path` at the top of the `urls.py` file.

Modfiy the `config/urls.py` so it looks like the above.
First we import the `TestView` from `test_app` and then we give it a path, in
this case `/api/test/`.

Let's migrate the client-service so it has a database to write the user to.
```
$ python manage.py makemigrations
```
```
$ python manage.py migrate
```



## Standing up the Services

As mentioned earlier, we have two services and auth-service and a client-service
. We want the auth-service to be on `:8000` and the client-service to be at
`:8001`.

**This is important because it's how we configure the easyjwt's configuration
in the auth-service and client-service.**

You'll need two terminals. One in auth-service/ and one in client-service/ both
with the respective virtual environments loaded and then a third one to execute
the requests from using `curl`.

In auth-service, stand up on port `:8000` like;
```
(.venv)  user@domain > ~/Code/auth-service $ python manage.py runserver 0.0.0.0:8000
```

And then stand up the client-service on port `:8001` like this;
```
(.venv)  user@domain > ~/Code/client-service $ python manage.py runserver 0.0.0.0:8001
```

---

# How to test the API

In the below examples we're mking requests to super simple API  (client-service)
which will reach out to the auth-service to retrieve, verify, and if needed
refresh the tokens.

You can check the client-service's db.sqlite3 database before making any
requests to confirm the `user` table is empty. After making a few successful
requests there will be some users there.

Remember the users added to the auth-service further back?
You'll need those email and passwords shortly.

Also remember that the auth-service is at `:8000` and the client-service is at
`:8001`. As a client-service user, we should never interact with the
auth-service directly. It shouldn't even be accessible to the public in a normal
production environment.

## Authorise and obtain a token pair

```
curl \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"email": "user@test.com", "password": "user-pass"}' \
  http://127.0.0.1:8001/auth/token/
```

Will give you a response like;
```JSON
{
    "refresh":" ... ",
    "access":" ... "
}
```

(I removed the tokens above for brevity.)


## Perform a generic API requst

Export the access token from the previous response to an envar.
eg.
 `export ACCESS_TOKEN={paste_token_here}`

 Should return 'success'.

```
curl \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  http://127.0.0.1:8001/api/test/
```

The response should be;

```JSON
    "success"
```


## Refresh an expired token

```
curl \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"refresh": "${REFRESH_TOKEN"}}' \
  http://127.0.0.1:8001/auth/token/refresh/
```


## Verify the token is correct
 Performed by the client-service against the auth-service with every single
 JWT API request.

```
curl \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"token": "${REFRESH_TOKEN}"}' \
  http://127.0.0.1:8001/auth/token/verify/
```


## Get the user details
This would be done inside the Auth handler when the user doesn't exist. There
needs to be a valid user_id for the user associated with the access token being
used. Ie. you can't view other user objectss by guessing an ID. Only your own.

```
curl \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  http://localhost:8001/auth/users/{user_id}/
```


# Extra Data

There may be scenarios where you want the Auth-Service to include additional User information that is passed
down to the Client-Services. One scenario for this may be having a centralised auth service, but customer's
get access to the client services individually; Ie. a user may have access to service A but not service B.
Think of this as maybe an Access Group with a access level attribute.

This can achieved by specifying and creating custom User Model Serializers. The config key for this
is "USER_MODEL_SERIALIZER" where you specify a serializer to use when parsing the User data returned from the
Auth-Service, eg. "custom.serializers.CustomUserModelSerialzer" or whatever you name the app and file.

The Auth-Service serializer needs to expose ALL the data that will be needed by ALL Client-services. A Client-
service needs to only parse the data relevant to it.
I.e. it is possible to Client-services to have different custom serializers only recording the fields they
individually care about.

### NB

You cannot use exactly the same serializer between the two services as the Client Service is required to
override the create & update methods on the custom user Serializer.


### Auth Service Config

Create a new app, or prepare an existing one to hold your custom user data.

`models.py`
```python
class AccessGroup(models.Model):

    user = models.OneToOneField(
        User, related_name="accessgroup", on_delete=models.CASCADE
    )
    user_type = models.TextField()  # use a choices/enum field here.
```

Now we add the serializers to expose this data. Two are used because one represents the standard User object
and the next is the serializer for the related AccessGroup model.

`serializers.py`
```
class AccessGroupSerializer(serializers.ModelSerializer):

    class Meta:
        model = AccessGroup
        fields = (
            "user_type",
        )


class TokenUserSerializer(serializers.ModelSerializer):
    accessgroup = AccessGroupSerializer()

    class Meta:
        model = User
        fields = (
            "id",
            "first_name",
            "email",
            "last_name",
            "date_joined",
            "last_login",
            "is_active",
            "is_staff",
            "is_superuser",
            "accessgroup",
        )
```

Remember to change the Serializer the Django-EasyJWT will use to parse the user data by adding the following
to the EASY_JWT dict in settings.py.

```
    "USER_MODEL_SERIALIZER": "userdata.serializer.TokenUserSerializer",
```

### Client Service Config

Add the same AccessGroup modes from the auth service into an appropriate app. Create a new one if required.

`models.py`
```
class AccessGroup(models.Model):

    user = models.OneToOneField(
        User, related_name="accessgroup", on_delete=models.CASCADE
    )
    user_type = models.TextField()  # use choices here in production.
```

Now we add the Serializers that will parse the info overriding the create and update methods so we can save
the AccessGroup data cleanly.

`serializers.py`
```
class AccessGroupSerializer(serializers.ModelSerializer):

    class Meta:
        model = AccessGroup
        fields = (
            "user_type",
        )


class TokenUserSerializer(serializers.ModelSerializer):
    accessgroup = AccessGroupSerializer()

    class Meta:
        model = User
        fields = (
            "id",
            "first_name",
            "email",
            "last_name",
            "date_joined",
            "last_login",
            "is_active",
            "is_staff",
            "is_superuser",
            "accessgroup",
        )

    def create(self, validated_data):
        """
        DRF doesn't support writable nested serializers so we need to create the nested
        objects manually.
        """

        user_id = validated_data.pop(get_user_model().USERNAME_FIELD)
        accessgroup = validated_data.pop("accessgroup")
        user, _ = User.objects.get_or_create(email=user_id, defaults=validated_data)
        # Delete stale data.
        # It's stale because this payload is the latest truth.
        try:
            user.accessgroup.delete()
        except AccessGroup.DoesNotExist:
            pass

        serializer = AccessGroupSerializer(data=accessgroup)
        serializer.is_valid(raise_exception=True)
        serializer.save(user=user)

        return user

    def update(self, instance, validated_data):
        accessgroup = validated_data.pop("accessgroup")
        # Delete stale data.
        # It's stale because auth service is the source of truth.
        try:
            instance.accessgroup.delete()
        except AccessGroup.DoesNotExist:
            pass

        instance.first_name = validated_data["first_name"]
        instance.last_name = validated_data["last_name"]
        # Do no update email, its a primary key.
        instance.last_login = validated_data["last_login"]
        instance.is_active = validated_data["is_active"]
        instance.is_staff = validated_data["is_staff"]
        instance.is_superuser = validated_data["is_superuser"]
        instance.save()

        serializer = AccessGroupSerializer(data=accessgroup)
        serializer.is_valid(raise_exception=True)
        serializer.save(user=instance)

        return instance
```

Again, we need to let the Django-EasyJWT know ho to parse the user data.

```
    "USER_MODEL_SERIALIZER": "userdata.serializer.TokenUserSerializer",
```

Because, in this example, the extra data is being exposed as a nested serializer, you are required to override
the serializers .create() method and handle the nested data yourself. This give the flexability to only record
the data relevant to the service.


# Permissions
Now that some custom data is being sent along witht the login request, we need to do something with it that
allows a user to use one service and not another. The most straight forward solution would be to name the
fields being passed as the services needing access and then use a permission class (for DRF only) to reject
access if they're not authed for this particular service.


```python
from rest_framework import permissions


class AccessGroupPermission(permissions.BasePermission):
    message = "You do not have permission to this service"

    def has_object_permission(self, request, view, obj):
        return request.user is not None

    def has_permission(self, request, view):
        return (
            not request.user.is_anonymous
            and view.access_level.startswith(request.user.accessgroup.access_level)
```

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/garrethcain/django-easyjwt",
    "name": "django-easyjwt",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": null,
    "author": "Garreth Cain",
    "author_email": "garrethccain@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/4f/79/d0fea1eaeebbb9338645fe455db6d8b7a907f1fde4bc527cb422c7fe82d5/django_easyjwt-0.6.2.tar.gz",
    "platform": null,
    "description": "# Django-EasyJWT\n\nThis is a package for the implementation of a remote authentication backend\nfor Django apps, primarily meant for use with JWTs, also supporting sessions as\nwell. Ie. elevated users could log into the Django `/admin/` once authenticated.\nThe target is microservice ecosystems, with several independant services all\nauthenticating against a central authentication services, or CAS with permissions\nand other custom user data able to be used in the authentication and Authorization\nprocess.\n\n### Why does this exist?\n\nI once had a several services with the same users; so I created a a centralised authentication\nservice to avoid the confusion of different passwords on, what to the outside, appeared to be\nthe same service. Then we added more services, but not all users were allowed access to\nthese, which is where the extra data came from.\n\nThen some of our customer decided they wanted to also have this notion of users, staff and\nadmins; but we'd already used Django's built in solution for our staff. So we extended the\nextra data to include permissions too.\n\nThe PyPi package can be found here: https://pypi.org/project/django-easyjwt\n\nACKNOWLEDGEMENTS\nThis package is heavily based on Djangorestframework_simplejwt and heavily influenced by SimpleJWT.\n\nThis package is used in the example\n[auth-client-service-example](https://github.com/garrethcain/django-easyjwt-example)\nproject.\n\n# Change Log\n\n2024-03-19 - 4.0.2\n    - removed dependency from easyjwt_user migrations.\n    - added raw_data to the context var of the custom serializer so custom data stripped by the serializer\n      can still be accessed.\n    - Changed the exception raise for a remote user model to show text and not try parse to json.\n\n# How To\n\nThere are, at a minimum, two components required for this to work;\n\n1. An Auth-Service, to authenticate against,\n2. A Client-Service, users want to use.\n\nThis package is made of of three sub-packages; easyjwt-Auth, RemoteJWT-Client, and RemoteJWT-User.\nWith the Auth & Client used in the separate Auth and Client services and the User being used in both,\nor neither.\n\nThe idea being that you can have any number of client-services using the same\nauth-service to validate login requests and your auth-service is behind some\nkind of private network and not public facing.\n\nThis package is a wrapper for all the main components of the auth-service and client-service; eg.\n\n* /token/ to obtain an access and a refresh token,\n    and create/update the local instance.\n* /token/refresh/ to refresh an expired access token.\n* /token/verify/ to confirm if a token is valid or not.\n\nThe above urls in the client-service just proxy requests to the remote\nAuth-Service, configured in `settings.py` `EASY_JWT` dict, but creating a\nlocal user object if required.\n\nAll that is needed is to add the Django-EasyJWT URLs to your client-service. The\nauth-service remains mostly vanilla aside from maybe using a custom User model,\ninclude in this package as well, for convenience.\n\nYou can't create users in the local client-service!\nIf you retrieve a user from the auth-service with the same ID you will overwrite\nthe local record with data from the auth-service.\n\nYour project can use HMAC by implementing some HMAC backend locally. The HMAC\nkeys will be kept local to the service and not centralised in the Auth-Service.\nThe Auth-Service is intentionally kept lean and only handles \"users\".\n\n\n# Get Started\n\nWhat we'll be doing;\n\n1. Create an Auth-Service\n2. Create a Client-Service\n\n\n\n## Create an Auth-Service\n\nAlways upgrade pip first.\n```\n$ pip install --upgrade pip\n```\n\nCreate a temporary virtual environment to install django so we can create a\nproject.\n```\n$ python -m virtualenv .venv\n```\n\nActivate the virtual environment.\n```\n$ source .venv/bin/activate\n```\n\nYour terminal should look something like this;\n\n`(.venv)  user@domain \ue0b0 ~/Code/ \ue0b0`\n\nWith the `(.venv)` part implying you're currently inside a virtual environment.\n\nInstall Django so we can create our app.\n```\n$ pip install django\n```\n\nCreate our project with the name `config` so the nested directory is named more\nconveniently.\n```PYTHON\n$ django-admin startproject config\n```\n\nRename the outer directory because I personally like having the project's config\nkept in a directory called `config` with the outer directory the name of\nproject.\n```PYTHON\n$ mv config auth-service\n```\n\nYou should have a directory structure similar to this.\n```\n\u2500\u2500\u2500 auth-service\n    \u251c\u2500\u2500 config\n    \u2502\u00a0\u00a0 \u251c\u2500\u2500 __init__.py\n    \u2502\u00a0\u00a0 \u251c\u2500\u2500 asgi.py\n    \u2502\u00a0\u00a0 \u251c\u2500\u2500 settings.py\n    \u2502\u00a0\u00a0 \u251c\u2500\u2500 urls.py\n    \u2502\u00a0\u00a0 \u2514\u2500\u2500 wsgi.py\n    \u2514\u2500\u2500 manage.py\n```\n\nNow let's remove the temp virtual environment by deactivating, deleting the old\none, then creating a new on in the right place.\n```\n$ deactivate\n```\n\nYour terminal should be back to something like this;\n`user@domain \ue0b0 ~/Code/ \ue0b0`\n\nDelete the files.\n```\n$ rm -Rf .venv\n```\n\n```\n$ cd auth-service\n```\n\nCreate another virtual environment.\n```\n$ python -m virtualenv .venv\n```\n\nActivate the virtual environment, again. We'll keep this one this time.\n```\n$ source .venv/bin/activate\n```\n\nAgain, your terminal should look something like this;\n`(.venv)  user@domain \ue0b0 ~/Code/auth-service/ \ue0b0`\n\nInstall the packages we're going to use.\n```\n$ pip install django django_rest_framework django_easyjwt\n```\n\nOpen `config/settings.py` and add the apps we'll be using to `INSTALLED_APPS`;\n\n```PY\nINSTALLED_APPS = [\n    ...\n    'rest_framework',\n    'easyjwt_auth',\n    'easyjwt_user',\n]\n```\n\nWe've included `easyjwt_user` so we can use the same User model between\nall services. It also includes some convieniences such as update forms and\nchanges the Username field from `username` to `email`.\n\nTell Django about the new user model by adding;\n```py\nAUTH_USER_MODEL = \"easyjwt_user.User\"\n```\n\nNext we need to configure Django Rest Framework. For this example you need just\nthe following;\n```PYTHON\nREST_FRAMEWORK = {\n    \"DEFAULT_PERMISSION_CLASSES\": (\"rest_framework.permissions.IsAuthenticated\",),\n    \"DEFAULT_AUTHENTICATION_CLASSES\": (\n        \"rest_framework.authentication.SessionAuthentication\",\n        \"easyjwt_auth.authentication.JWTAuthentication\",\n    ),\n}\n```\n\nIn order to use easyjwt-Auth you will also need to add some configuration for it.\n\n```PY\nEASY_JWT = {\n    \"ACCESS_TOKEN_LIFETIME\": timedelta(minutes=5),\n    \"REFRESH_TOKEN_LIFETIME\": timedelta(days=1),\n    \"ROTATE_REFRESH_TOKENS\": False,\n    \"BLACKLIST_AFTER_ROTATION\": False,\n    \"UPDATE_LAST_LOGIN\": False,\n    \"ALGORITHM\": \"HS256\",\n    \"SIGNING_KEY\": \"d577273ff885c3f84dadb8578bb40000\", # You must set this correctly for Production.\n    \"VERIFYING_KEY\": None,\n    \"AUDIENCE\": None,\n    \"ISSUER\": None,\n    \"JWK_URL\": None,\n    \"LEEWAY\": 0,\n    \"USER_ID_FIELD\": \"id\",\n    \"USER_ID_CLAIM\": \"user_id\",\n    \"USER_AUTHENTICATION_RULE\": \"easyjwt_auth.authentication.default_user_authentication_rule\",\n    \"AUTH_TOKEN_CLASSES\": (\"easyjwt_auth.tokens.AccessToken\",),\n    \"TOKEN_TYPE_CLAIM\": \"token_type\",\n    \"TOKEN_USER_CLASS\": \"easyjwt_auth.models.TokenUser\",\n    \"JTI_CLAIM\": \"jti\",\n    \"SLIDING_TOKEN_REFRESH_EXP_CLAIM\": \"refresh_exp\",\n    \"SLIDING_TOKEN_LIFETIME\": timedelta(minutes=5),\n    \"SLIDING_TOKEN_REFRESH_LIFETIME\": timedelta(days=1),\n}\n```\n\nIn the JWT configuration we use `timedelta` so you need to import `timedelta` at\nthe top of `config/settings.py`.\n\n`from datetime import timedelta`\n\nMake migrations so we can migrate.\n```\n$ python manage.py makemigrations\n```\n\nMigrate to create the database. An SQLite db is fine for the example. In a\nproduction environment you'd use something a bit more appropriate like PostreSQL or DynamoDB.\n```\n$ python manage.py migrate\n```\n\nAnd finally you can stand up the auth-service with;\n```\n$ python manage.py runserver\n```\n\nWhich should get you something like;\n```\nWatching for file changes with StatReloader\nPerforming system checks...\n\nSystem check identified no issues (0 silenced).\nMay 02, 2023 - 10:50:48\nDjango version 4.0.2, using settings 'config.settings'\nStarting development server at http://127.0.0.1:8000/\nQuit the server with CONTROL-C.\n```\n\nHit `^C` so we can create a few users for testing with later on.\n\nWe're going to create three test users as below.\n* admin | admin@test.com | admin-pass\n* staff | staff@test.com | staff-pass\n* user | user@test.com | user-pass\n\nEg.\n```\nexport DJANGO_SUPERUSER_EMAIL=admin@test.com\nexport DJANGO_SUPERUSER_USERNAME=admin\nexport DJANGO_SUPERUSER_PASSWORD=admin-pass\npython manage.py createsuperuser --noinput\n```\nor\n```\n$ python manage.py createsuperuser\n```\nThen stand up the auth-service and log into `http://127.0.0.1:8000/admin/` login\nwith the superuser you created above and create the other two users, setting\n`is_staff=True` for the staff user.\nLog out once you're done and terminate the instance that's running with `^C` and\ndeactivate this virtual environment.\n```\n$ deactivate\n```\n\nThe final step is to configure the Urls. So open `config/urls.py` and add the\nfollowing.\n\nThen expost the paths to the JWT endpoints and a user view which is where the\nclient-service will download the user from.\n```py\nurlpatterns = [\n    ...\n    path('auth/', include('easyjwt_auth.urls')),  # gives us access to the auth views.\n    path('auth/', include('easyjwt_user.urls')),  # gives us access to the users views.\n]\n```\n\nAnd that's it for the Auth-Service. It doesn't need any views or serializers.\nEverything is handled by easyjwt-Auth and Django's OEM methods. This service is\nsuper light to run and will handle many requests with ease.\n\n---\n## Create a Client-Service\n\nNow we need a client-service that will authenticate against the Auth-Service to\ncomplete the example.\n\nGo up one directory;\n```\ncd ..\n```\n\nWe're going to perform the same steps for the client-service that we did for the\nauth-service. You should recognise most of this. The main difference is that\nthis time we'll be using `django_easyjwt` and not creating any local users.\n\n\nCreate a temporary virtual environment to install django so we can create a\nproject.\n```\n$ python -m virtualenv .venv\n```\n\nActivate the virtual environment.\n```\n$ source .venv/bin/activate\n```\n\nYour terminal should look something like this;\n\n`(.venv)  user@domain \ue0b0 ~/Code/ \ue0b0`\n\nWith the `(.venv)` part implying you're currently inside a virtual environment.\n\nInstall Django so we can create our app.\n```\n$ pip install django\n```\n\nCreate our project with the name `config` so the nested directory is named more\nconveniently.\n```PYTHON\n$ django-admin startproject config\n```\n\nRename the outer directory because I personally like having the project's config\nkept in a directory called `config` with the outer directory the name of\nproject.\n```PYTHON\n$ mv config client-service\n```\n\nYou should have a directory structure similar to this.\n```\n\u251c\u2500\u2500 auth-service\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 config\n\u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u251c\u2500\u2500 __init__.py\n\u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u251c\u2500\u2500 asgi.py\n\u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u251c\u2500\u2500 settings.py\n\u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u251c\u2500\u2500 urls.py\n\u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 wsgi.py\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 db.sqlite3\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 manage.py\n\u2514\u2500\u2500 client-service\n    \u251c\u2500\u2500 config\n    \u2502\u00a0\u00a0 \u251c\u2500\u2500 __init__.py\n    \u2502\u00a0\u00a0 \u251c\u2500\u2500 asgi.py\n    \u2502\u00a0\u00a0 \u251c\u2500\u2500 settings.py\n    \u2502\u00a0\u00a0 \u251c\u2500\u2500 urls.py\n    \u2502\u00a0\u00a0 \u2514\u2500\u2500 wsgi.py\n    \u2514\u2500\u2500 manage.py\n```\n\nNow let's remove the temp virtual environment by deactivating, deleting the old\none, then creating a new on in the right place.\n```\n$ deactivate\n```\n\nYour terminal should be back to something like this;\n`user@domain \ue0b0 ~/Code/ \ue0b0`\n\nDelete the files.\n```\n$ rm -Rf .venv\n```\n\n```\n$ cd client-service\n```\n\nCreate another virtual environment.\n```\n$ python -m virtualenv .venv\n```\n\nActivate the virtual environment, again. We'll keep this one this time.\n```\n$ source .venv/bin/activate\n```\n\nAgain, your terminal should look something like this;\n`(.venv)  user@domain \ue0b0 ~/Code/auth-service/ \ue0b0`\n\nInstall the packages we're going to use. We don't need `SimpleJWT` this time\nbecause authentication is handled by the remote auth-service.\n```\n$ pip install django django_rest_framework django_easyjwt\n```\n\nWe need to let Django know about the apps we'll be using, so open `settings.py`\nand add the following lines to `INSTALLED_APPS`;\n```PY\nINSTALLED_APPS = [\n    ...\n    'rest_framework',\n    'easyjwt_client',\n    'easyjwt_user',\n]\n```\n\nWe need to use the same User model as the auth-service, otherwise the user\nreturned by the auth-service will cause an integrity error.\n\n```py\nAUTH_USER_MODEL = \"easyjwt_user.User\"\n```\n\nAdd some configuration for Djang Rest Framework. Change the default behaviour\nfor all endpoints to require authentication. Then we override the default\nauthentication classes with the ones from `Django-EasyJWT`.\n```py\nREST_FRAMEWORK = {\n    \"DEFAULT_PERMISSION_CLASSES\": (\n        \"rest_framework.permissions.IsAuthenticated\",\n    ),\n    \"DEFAULT_AUTHENTICATION_CLASSES\": (\n        \"easyjwt_client.authentication.RemoteJWTAuthentication\",     # Use our service\n        \"rest_framework.authentication.SessionAuthentication\",  # Maybe the user has a session...\n    ),\n}\n```\n\nLet Django know that we want to use a custom authentication backend.\n```py\n# implement out or custom backend for Admin and other views.\nAUTHENTICATION_BACKENDS = [\n    'django.contrib.auth.backends.ModelBackend',    # Default, check the local DB.\n    'easyjwt_client.authentication.ModelBackend'         # Our override to check the remote service.\n]\n```\n\nTime to configure `Django-EasyJWT`. For this example example we're going to run\nthe auth-server on `:8000` and the client-service on `:8001`. Most of this conf\nshould be handled through environmental variables in a real project. But we're\njust aiming for the absolute minimal working example.\n\n```py\nEASY_JWT = {\n    \"AUTH_HEADER_TYPES\": (\"Bearer\", ),\n    \"AUTH_HEADER_NAME\": \"Authorization\",\n    \"REMOTE_AUTH_SERVICE_URL\": \"http://127.0.0.1:8000\", # Where do we reach the Auth-Service\n    \"REMOTE_AUTH_SERVICE_TOKEN_PATH\": \"/auth/token/\", # The path to login and retrieve a token\n    \"REMOTE_AUTH_SERVICE_REFRESH_PATH\": \"/auth/token/refresh/\", # The path to refresh a token\n    \"REMOTE_AUTH_SERVICE_VERIFY_PATH\": \"/auth/token/verify/\", # The path to verify a token\n    \"REMOTE_AUTH_SERVICE_USER_PATH\": \"/auth/user/\", # The path to get the user object\n    \"USER_ID_FIELD\": \"id\",\n    \"USER_ID_CLAIM\": \"user_id\",\n}\n```\n\nOpen `config/urls.py` and add the URLs from easyjwt that will be passed\nthrough to the auth-service.\n\n```py\nfrom django.urls import path, include\n\n\nurlpatterns = [\n    path('admin/', admin.site.urls),\n    path('auth/', include(\"easyjwt_client.urls\"))\n]\n```\nDon't forget the `include` import.\n\nWe'll add our test view to the urls as well shortly.\n\nAll the client-service needs now is an endpoint to prove it's alive. So let's\nadd a Django app with a view that requires authentication we use to test.\n```py\n$ django-admin startapp test_app\n```\n\nYour directory structure should now look something along the lines of;\n```\n.\n\u251c\u2500\u2500 config\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 __init__.py\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 asgi.py\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 settings.py\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 urls.py\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 wsgi.py\n\u251c\u2500\u2500 manage.py\n\u2514\u2500\u2500 test_app\n    \u251c\u2500\u2500 __init__.py\n    \u251c\u2500\u2500 admin.py\n    \u251c\u2500\u2500 apps.py\n    \u251c\u2500\u2500 migrations\n    \u2502\u00a0\u00a0 \u2514\u2500\u2500 __init__.py\n    \u251c\u2500\u2500 models.py\n    \u251c\u2500\u2500 tests.py\n    \u2514\u2500\u2500 views.py\n```\n\nYou can see the new app called `test_app` has been added.\n\nOpen `test_app/views.py` and add the following view. Because we changed the\nRest Framework config to use a `DEFAULT_PERMISSION_CLASSES` of `IsAuthenticated`\nall views will require authentication.\n```py\nfrom rest_framework import generics\nfrom rest_framework.response import Response\n\nclass TestView(generics.GenericAPIView):\n    def get(self, request):\n        return Response(\"success\", status=200)\n```\n\nThere are no models or serializers, it's the absolute least we can do to get a\nsuccess. There is no need to add the `test_app` to the `INSTALLED_APPS` because\nit has no models that need migrating.\n\n\nThe absolute final step before we can run some tests is to add the `TestView` to\nthe client-service's Urls.py so it knows where to send an incoming request.\n\n```py\nfrom django.urls import path, include\nfrom test_app.views import TestView\n\nurlpatterns = [\n    path('admin/', admin.site.urls),\n    path('auth/', include(\"easyjwt_client.urls\")),\n    path('api/test/', TestView.as_view()),\n]\n```\n\nDon't forget it include the `include` import at the end of\n`from django.urls import path` at the top of the `urls.py` file.\n\nModfiy the `config/urls.py` so it looks like the above.\nFirst we import the `TestView` from `test_app` and then we give it a path, in\nthis case `/api/test/`.\n\nLet's migrate the client-service so it has a database to write the user to.\n```\n$ python manage.py makemigrations\n```\n```\n$ python manage.py migrate\n```\n\n\n\n## Standing up the Services\n\nAs mentioned earlier, we have two services and auth-service and a client-service\n. We want the auth-service to be on `:8000` and the client-service to be at\n`:8001`.\n\n**This is important because it's how we configure the easyjwt's configuration\nin the auth-service and client-service.**\n\nYou'll need two terminals. One in auth-service/ and one in client-service/ both\nwith the respective virtual environments loaded and then a third one to execute\nthe requests from using `curl`.\n\nIn auth-service, stand up on port `:8000` like;\n```\n(.venv)  user@domain > ~/Code/auth-service $ python manage.py runserver 0.0.0.0:8000\n```\n\nAnd then stand up the client-service on port `:8001` like this;\n```\n(.venv)  user@domain > ~/Code/client-service $ python manage.py runserver 0.0.0.0:8001\n```\n\n---\n\n# How to test the API\n\nIn the below examples we're mking requests to super simple API  (client-service)\nwhich will reach out to the auth-service to retrieve, verify, and if needed\nrefresh the tokens.\n\nYou can check the client-service's db.sqlite3 database before making any\nrequests to confirm the `user` table is empty. After making a few successful\nrequests there will be some users there.\n\nRemember the users added to the auth-service further back?\nYou'll need those email and passwords shortly.\n\nAlso remember that the auth-service is at `:8000` and the client-service is at\n`:8001`. As a client-service user, we should never interact with the\nauth-service directly. It shouldn't even be accessible to the public in a normal\nproduction environment.\n\n## Authorise and obtain a token pair\n\n```\ncurl \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\": \"user@test.com\", \"password\": \"user-pass\"}' \\\n  http://127.0.0.1:8001/auth/token/\n```\n\nWill give you a response like;\n```JSON\n{\n    \"refresh\":\" ... \",\n    \"access\":\" ... \"\n}\n```\n\n(I removed the tokens above for brevity.)\n\n\n## Perform a generic API requst\n\nExport the access token from the previous response to an envar.\neg.\n `export ACCESS_TOKEN={paste_token_here}`\n\n Should return 'success'.\n\n```\ncurl \\\n  -H \"Authorization: Bearer ${ACCESS_TOKEN}\" \\\n  http://127.0.0.1:8001/api/test/\n```\n\nThe response should be;\n\n```JSON\n    \"success\"\n```\n\n\n## Refresh an expired token\n\n```\ncurl \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"refresh\": \"${REFRESH_TOKEN\"}}' \\\n  http://127.0.0.1:8001/auth/token/refresh/\n```\n\n\n## Verify the token is correct\n Performed by the client-service against the auth-service with every single\n JWT API request.\n\n```\ncurl \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"token\": \"${REFRESH_TOKEN}\"}' \\\n  http://127.0.0.1:8001/auth/token/verify/\n```\n\n\n## Get the user details\nThis would be done inside the Auth handler when the user doesn't exist. There\nneeds to be a valid user_id for the user associated with the access token being\nused. Ie. you can't view other user objectss by guessing an ID. Only your own.\n\n```\ncurl \\\n  -H \"Authorization: Bearer ${ACCESS_TOKEN}\" \\\n  http://localhost:8001/auth/users/{user_id}/\n```\n\n\n# Extra Data\n\nThere may be scenarios where you want the Auth-Service to include additional User information that is passed\ndown to the Client-Services. One scenario for this may be having a centralised auth service, but customer's\nget access to the client services individually; Ie. a user may have access to service A but not service B.\nThink of this as maybe an Access Group with a access level attribute.\n\nThis can achieved by specifying and creating custom User Model Serializers. The config key for this\nis \"USER_MODEL_SERIALIZER\" where you specify a serializer to use when parsing the User data returned from the\nAuth-Service, eg. \"custom.serializers.CustomUserModelSerialzer\" or whatever you name the app and file.\n\nThe Auth-Service serializer needs to expose ALL the data that will be needed by ALL Client-services. A Client-\nservice needs to only parse the data relevant to it.\nI.e. it is possible to Client-services to have different custom serializers only recording the fields they\nindividually care about.\n\n### NB\n\nYou cannot use exactly the same serializer between the two services as the Client Service is required to\noverride the create & update methods on the custom user Serializer.\n\n\n### Auth Service Config\n\nCreate a new app, or prepare an existing one to hold your custom user data.\n\n`models.py`\n```python\nclass AccessGroup(models.Model):\n\n    user = models.OneToOneField(\n        User, related_name=\"accessgroup\", on_delete=models.CASCADE\n    )\n    user_type = models.TextField()  # use a choices/enum field here.\n```\n\nNow we add the serializers to expose this data. Two are used because one represents the standard User object\nand the next is the serializer for the related AccessGroup model.\n\n`serializers.py`\n```\nclass AccessGroupSerializer(serializers.ModelSerializer):\n\n    class Meta:\n        model = AccessGroup\n        fields = (\n            \"user_type\",\n        )\n\n\nclass TokenUserSerializer(serializers.ModelSerializer):\n    accessgroup = AccessGroupSerializer()\n\n    class Meta:\n        model = User\n        fields = (\n            \"id\",\n            \"first_name\",\n            \"email\",\n            \"last_name\",\n            \"date_joined\",\n            \"last_login\",\n            \"is_active\",\n            \"is_staff\",\n            \"is_superuser\",\n            \"accessgroup\",\n        )\n```\n\nRemember to change the Serializer the Django-EasyJWT will use to parse the user data by adding the following\nto the EASY_JWT dict in settings.py.\n\n```\n    \"USER_MODEL_SERIALIZER\": \"userdata.serializer.TokenUserSerializer\",\n```\n\n### Client Service Config\n\nAdd the same AccessGroup modes from the auth service into an appropriate app. Create a new one if required.\n\n`models.py`\n```\nclass AccessGroup(models.Model):\n\n    user = models.OneToOneField(\n        User, related_name=\"accessgroup\", on_delete=models.CASCADE\n    )\n    user_type = models.TextField()  # use choices here in production.\n```\n\nNow we add the Serializers that will parse the info overriding the create and update methods so we can save\nthe AccessGroup data cleanly.\n\n`serializers.py`\n```\nclass AccessGroupSerializer(serializers.ModelSerializer):\n\n    class Meta:\n        model = AccessGroup\n        fields = (\n            \"user_type\",\n        )\n\n\nclass TokenUserSerializer(serializers.ModelSerializer):\n    accessgroup = AccessGroupSerializer()\n\n    class Meta:\n        model = User\n        fields = (\n            \"id\",\n            \"first_name\",\n            \"email\",\n            \"last_name\",\n            \"date_joined\",\n            \"last_login\",\n            \"is_active\",\n            \"is_staff\",\n            \"is_superuser\",\n            \"accessgroup\",\n        )\n\n    def create(self, validated_data):\n        \"\"\"\n        DRF doesn't support writable nested serializers so we need to create the nested\n        objects manually.\n        \"\"\"\n\n        user_id = validated_data.pop(get_user_model().USERNAME_FIELD)\n        accessgroup = validated_data.pop(\"accessgroup\")\n        user, _ = User.objects.get_or_create(email=user_id, defaults=validated_data)\n        # Delete stale data.\n        # It's stale because this payload is the latest truth.\n        try:\n            user.accessgroup.delete()\n        except AccessGroup.DoesNotExist:\n            pass\n\n        serializer = AccessGroupSerializer(data=accessgroup)\n        serializer.is_valid(raise_exception=True)\n        serializer.save(user=user)\n\n        return user\n\n    def update(self, instance, validated_data):\n        accessgroup = validated_data.pop(\"accessgroup\")\n        # Delete stale data.\n        # It's stale because auth service is the source of truth.\n        try:\n            instance.accessgroup.delete()\n        except AccessGroup.DoesNotExist:\n            pass\n\n        instance.first_name = validated_data[\"first_name\"]\n        instance.last_name = validated_data[\"last_name\"]\n        # Do no update email, its a primary key.\n        instance.last_login = validated_data[\"last_login\"]\n        instance.is_active = validated_data[\"is_active\"]\n        instance.is_staff = validated_data[\"is_staff\"]\n        instance.is_superuser = validated_data[\"is_superuser\"]\n        instance.save()\n\n        serializer = AccessGroupSerializer(data=accessgroup)\n        serializer.is_valid(raise_exception=True)\n        serializer.save(user=instance)\n\n        return instance\n```\n\nAgain, we need to let the Django-EasyJWT know ho to parse the user data.\n\n```\n    \"USER_MODEL_SERIALIZER\": \"userdata.serializer.TokenUserSerializer\",\n```\n\nBecause, in this example, the extra data is being exposed as a nested serializer, you are required to override\nthe serializers .create() method and handle the nested data yourself. This give the flexability to only record\nthe data relevant to the service.\n\n\n# Permissions\nNow that some custom data is being sent along witht the login request, we need to do something with it that\nallows a user to use one service and not another. The most straight forward solution would be to name the\nfields being passed as the services needing access and then use a permission class (for DRF only) to reject\naccess if they're not authed for this particular service.\n\n\n```python\nfrom rest_framework import permissions\n\n\nclass AccessGroupPermission(permissions.BasePermission):\n    message = \"You do not have permission to this service\"\n\n    def has_object_permission(self, request, view, obj):\n        return request.user is not None\n\n    def has_permission(self, request, view):\n        return (\n            not request.user.is_anonymous\n            and view.access_level.startswith(request.user.accessgroup.access_level)\n```\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "A convenient package for building both client and server implementations for JWTs or Json Web Tokens. It allows you to build either side by only including the module you need and includes an optional custom user model.",
    "version": "0.6.2",
    "project_urls": {
        "Homepage": "https://github.com/garrethcain/django-easyjwt"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "474c4a26cc4f492d6d2b4db13762dfa63b7eb986614554da5ec746dbc85a5c0a",
                "md5": "dbccbb218bf82a8d82b71b7d2064c18f",
                "sha256": "ba2de5eac7662f2f1737ca880d3412ca37a752d5cd9a6ef1094be9457dfb8623"
            },
            "downloads": -1,
            "filename": "django_easyjwt-0.6.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "dbccbb218bf82a8d82b71b7d2064c18f",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 46587,
            "upload_time": "2025-02-14T22:37:27",
            "upload_time_iso_8601": "2025-02-14T22:37:27.215939Z",
            "url": "https://files.pythonhosted.org/packages/47/4c/4a26cc4f492d6d2b4db13762dfa63b7eb986614554da5ec746dbc85a5c0a/django_easyjwt-0.6.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "4f79d0fea1eaeebbb9338645fe455db6d8b7a907f1fde4bc527cb422c7fe82d5",
                "md5": "46483b5d66c5fbe5573b8890da4bf482",
                "sha256": "d4b075a79b266801459c5fb96dba06eed5243234105a9370e60bdbb3b4473148"
            },
            "downloads": -1,
            "filename": "django_easyjwt-0.6.2.tar.gz",
            "has_sig": false,
            "md5_digest": "46483b5d66c5fbe5573b8890da4bf482",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 36750,
            "upload_time": "2025-02-14T22:37:29",
            "upload_time_iso_8601": "2025-02-14T22:37:29.307786Z",
            "url": "https://files.pythonhosted.org/packages/4f/79/d0fea1eaeebbb9338645fe455db6d8b7a907f1fde4bc527cb422c7fe82d5/django_easyjwt-0.6.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-02-14 22:37:29",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "garrethcain",
    "github_project": "django-easyjwt",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "requirements": [
        {
            "name": "asgiref",
            "specs": [
                [
                    "==",
                    "3.7.2"
                ]
            ]
        },
        {
            "name": "build",
            "specs": [
                [
                    "==",
                    "1.1.1"
                ]
            ]
        },
        {
            "name": "certifi",
            "specs": [
                [
                    "==",
                    "2023.11.17"
                ]
            ]
        },
        {
            "name": "cfgv",
            "specs": [
                [
                    "==",
                    "3.4.0"
                ]
            ]
        },
        {
            "name": "charset-normalizer",
            "specs": [
                [
                    "==",
                    "3.3.2"
                ]
            ]
        },
        {
            "name": "distlib",
            "specs": [
                [
                    "==",
                    "0.3.8"
                ]
            ]
        },
        {
            "name": "Django",
            "specs": [
                [
                    "==",
                    "5.0"
                ]
            ]
        },
        {
            "name": "django-easyjwt",
            "specs": [
                [
                    "==",
                    "0.4.1"
                ]
            ]
        },
        {
            "name": "djangorestframework",
            "specs": [
                [
                    "==",
                    "3.14.0"
                ]
            ]
        },
        {
            "name": "filelock",
            "specs": [
                [
                    "==",
                    "3.13.1"
                ]
            ]
        },
        {
            "name": "identify",
            "specs": [
                [
                    "==",
                    "2.5.35"
                ]
            ]
        },
        {
            "name": "idna",
            "specs": [
                [
                    "==",
                    "3.6"
                ]
            ]
        },
        {
            "name": "mypy",
            "specs": [
                [
                    "==",
                    "1.9.0"
                ]
            ]
        },
        {
            "name": "mypy-extensions",
            "specs": [
                [
                    "==",
                    "1.0.0"
                ]
            ]
        },
        {
            "name": "nodeenv",
            "specs": [
                [
                    "==",
                    "1.8.0"
                ]
            ]
        },
        {
            "name": "packaging",
            "specs": [
                [
                    "==",
                    "24.0"
                ]
            ]
        },
        {
            "name": "platformdirs",
            "specs": [
                [
                    "==",
                    "4.2.0"
                ]
            ]
        },
        {
            "name": "pre-commit",
            "specs": [
                [
                    "==",
                    "3.6.2"
                ]
            ]
        },
        {
            "name": "pyproject_hooks",
            "specs": [
                [
                    "==",
                    "1.0.0"
                ]
            ]
        },
        {
            "name": "pytz",
            "specs": [
                [
                    "==",
                    "2023.3.post1"
                ]
            ]
        },
        {
            "name": "PyYAML",
            "specs": [
                [
                    "==",
                    "6.0.1"
                ]
            ]
        },
        {
            "name": "requests",
            "specs": [
                [
                    "==",
                    "2.31.0"
                ]
            ]
        },
        {
            "name": "ruamel.yaml",
            "specs": [
                [
                    "==",
                    "0.18.6"
                ]
            ]
        },
        {
            "name": "ruamel.yaml.clib",
            "specs": [
                [
                    "==",
                    "0.2.8"
                ]
            ]
        },
        {
            "name": "ruff",
            "specs": [
                [
                    "==",
                    "0.3.3"
                ]
            ]
        },
        {
            "name": "sqlparse",
            "specs": [
                [
                    "==",
                    "0.4.4"
                ]
            ]
        },
        {
            "name": "tomli",
            "specs": [
                [
                    "==",
                    "2.0.1"
                ]
            ]
        },
        {
            "name": "types-requests",
            "specs": [
                [
                    "==",
                    "2.31.0.20240311"
                ]
            ]
        },
        {
            "name": "typing_extensions",
            "specs": [
                [
                    "==",
                    "4.9.0"
                ]
            ]
        },
        {
            "name": "urllib3",
            "specs": [
                [
                    "==",
                    "2.1.0"
                ]
            ]
        },
        {
            "name": "virtualenv",
            "specs": [
                [
                    "==",
                    "20.25.1"
                ]
            ]
        }
    ],
    "lcname": "django-easyjwt"
}
        
Elapsed time: 2.43806s