DEV Community

Cover image for Implementing JWT Authentication and User Profile with Django Rest API Part 3
Kenneth Kimani
Kenneth Kimani

Posted on • Edited on

Implementing JWT Authentication and User Profile with Django Rest API Part 3

Introduction

Welcome back to part three of our React and Django series, where we're creating a Notes app from scratch. As we know, security is of utmost importance in any web application, and authentication is a crucial aspect of it. This is where JWT comes in. JSON Web Tokens (JWT) is a widely used standard for securely transmitting information between parties as a JSON object. JWT allows us to authenticate users and secure our application by encrypting and transmitting user data in the form of a token. It's a great option for authentication because it allows us to store user information directly in the token, making it easy to verify the user's identity with every subsequent request. In this post, we'll be implementing JWT authentication to ensure that our users' data stays safe and secure. So, let's dive in and learn how to secure our Notes app with JWT authentication

Installing Neccessary Packages

Before we start off we need to install the necessary django packages required for JWT these are:

pip install djangorestframework-simplejwt
The djangorestframework-simplejwt package provides a simple way to implement JWT authentication in Django REST framework applications. It includes views and serializers for generating and refreshing JWT tokens, as well as a built-in token authentication backend for validating tokens.

Next we can then include it in the installed apps and add it to the settings:



INSTALLED_APPS = [


    'rest_framework_simplejwt.token_blacklist',
]


Enter fullscreen mode Exit fullscreen mode

Token blacklisting involves maintaining a list of tokens that have been revoked or expired, and checking each incoming token against this list to ensure that it is still valid. This can help to prevent security vulnerabilities, such as token theft or replay attacks.

Next,we setup the JWT:



SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=180),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=50),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'UPDATE_LAST_LOGIN': False,

    'ALGORITHM': 'HS256',

    'VERIFYING_KEY': None,
    'AUDIENCE': None,
    'ISSUER': None,
    'JWK_URL': None,
    'LEEWAY': 0,

    'AUTH_HEADER_TYPES': ('Bearer',),
    'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
    'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',

    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
    'TOKEN_TYPE_CLAIM': 'token_type',
    'TOKEN_USER_CLASS': 'rest_framework_simplejwt.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),
}


Enter fullscreen mode Exit fullscreen mode

Here is an explanation of some of the key settings:

ACCESS_TOKEN_LIFETIME: This sets the lifetime of the access token, which is the token that grants access to protected resources. In this case, it is set to 180 minutes (or 3 hours).

REFRESH_TOKEN_LIFETIME: This sets the lifetime of the refresh token, which is used to obtain a new access token after the original token expires. In this case, it is set to 50 days.

ROTATE_REFRESH_TOKENS: This setting controls whether or not refresh tokens are rotated when a new access token is issued. If it is set to True, a new refresh token will be issued with each new access token.

BLACKLIST_AFTER_ROTATION: This setting controls whether or not refresh tokens that have been rotated are blacklisted. If it is set to True, any previous refresh tokens will be invalidated when a new one is issued.

ALGORITHM: This sets the algorithm used to sign the JWT tokens. In this case, it is set to HS256, which is a symmetric-key algorithm that uses a shared secret key to sign and verify tokens.

AUTH_HEADER_TYPES: This specifies the types of authentication headers that can be used to send JWT tokens. In this case, it is set to ('Bearer',) which is the most commonly used type.

AUTH_HEADER_NAME: This sets the name of the HTTP header that will be used to send the authentication token. In this case, it is set to 'HTTP_AUTHORIZATION'.

USER_ID_FIELD: This sets the name of the field that will be used to identify the user in the JWT token. In this case, it is set to 'id'.

USER_ID_CLAIM: This sets the name of the claim that will be used to store the user ID in the JWT token. In this case, it is set to 'user_id'.

AUTH_TOKEN_CLASSES: This sets the classes of authentication tokens that will be accepted by the authentication system. In this case, it is set to ('rest_framework_simplejwt.tokens.AccessToken',) which is the default token class.

JTI_CLAIM: This sets the name of the claim that will be used to store the unique identifier of the JWT token. In this case, it is set to 'jti'.

Configuring the urls

Now that we have configured the settings, we can now move to configuring our authentication urls, where we will we will create the URL for access token and refresh token in the app level since we are only authenticating one app:



urlpatterns = [
    #Authentication
    path('token/', views.MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]


Enter fullscreen mode Exit fullscreen mode

Great now that we have that setup we can create a Profile for our Notes app users such that only authenticated users have access to the app.We can do that by first:

  1. Creating User Model ```python

from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
bio = models.CharField(max_length=255, blank=True)
cover_photo = models.ImageField(upload_to='covers/', null=True, blank=True)

By creating a custom user model that extends AbstractUser, you can add fields that are specific to your application and provide additional functionality beyond what is available in the default User model.

2.**Updating our original Note Model**
Enter fullscreen mode Exit fullscreen mode

user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, null=True, blank=True, related_name='notes')

This line of code defines a foreign key relationship between the Note model and the CustomUser model. The ForeignKey field creates a many-to-one relationship between Note and CustomUser, meaning that each Note is associated with a single CustomUser instance, but a CustomUser can have many Note instances.

3.**Creating Views for UserProfile, Register and Login
```python


#Login User
class MyTokenObtainPairView(TokenObtainPairView):
    serializer_class = MyTokenObtainPairSerializer

#Register User
class RegisterView(generics.CreateAPIView):
    queryset = CustomUser.objects.all()
    permission_classes = (AllowAny,)
    serializer_class = RegisterSerializer

#api/profile  and api/profile/update
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def getProfile(request):
    user = request.user
    serializer = ProfileSerializer(user, many=False)
    return Response(serializer.data)

@api_view(['PUT'])
@permission_classes([IsAuthenticated])
def updateProfile(request):
    user = request.user
    serializer = ProfileSerializer(user, data=request.data, partial=True)
    if serializer.is_valid():
        serializer.save()
    return Response(serializer.data)

#api/notes
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def getNotes(request):
    public_notes = Note.objects.filter(is_public=True).order_by('-updated')[:10]
    user_notes = request.user.notes.all().order_by('-updated')[:10]
    notes = public_notes | user_notes
    serializer = NoteSerializer(notes, many=True)
    return Response(serializer.data)



Enter fullscreen mode Exit fullscreen mode

The first code defines a custom token obtain view that uses a custom serializer for obtaining a JSON Web Token (JWT) pair for a user.

The second code defines a view for registering a new user. It uses the built-in Django CreateAPIView and sets the permission class to AllowAny, which means anyone can access this view.

The next two codes define views for getting and updating the user's profile. The getProfile view returns the serialized data for the currently authenticated user's profile, while the updateProfile view updates the profile with the data in the request. Both views require the user to be authenticated with the IsAuthenticated permission class.

Finally, the last code defines a view for getting the user's notes. It first filters public notes and then the user's notes before combining them and returning them in serialized formThe first code defines a custom token obtain view that uses a custom serializer for obtaining a JSON Web Token (JWT) pair for a user.
The second code defines a view for registering a new user. It uses the built-in Django CreateAPIView and sets the permission class to AllowAny, which means anyone can access this view.
The next two codes define views for getting and updating the user's profile. The getProfile view returns the serialized data for the currently authenticated user's profile, while the updateProfile view updates the profile with the data in the request. Both views require the user to be authenticated with the IsAuthenticated permission class.
Finally, the last code defines a view for getting the user's notes. It first filters public notes and then the user's notes before combining them and returning them in serialized form. This view also requires the user to be authenticated with the IsAuthenticated permission class.. This view also requires the user to be authenticated with the IsAuthenticated permission class.

  1. Adding our Serializers ```python

class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)

    # Add custom claims
    token['username'] = user.username
    token['email'] = user.email
    # ...

    return token
Enter fullscreen mode Exit fullscreen mode

class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(
write_only=True, required=True, validators=[validate_password])
password2 = serializers.CharField(write_only=True, required=True)
email = serializers.EmailField(
required=True,
validators=[UniqueValidator(queryset=CustomUser.objects.all())]
)

class Meta:
    model = CustomUser
    fields = ('username', 'email', 'password', 'password2', 'bio', 'cover_photo')

def validate(self, attrs):
    if attrs['password'] != attrs['password2']:
        raise serializers.ValidationError(
            {"password": "Password fields didn't match."})

    return attrs

def create(self, validated_data):
    user = CustomUser.objects.create(
        username=validated_data['username'],
        email=validated_data['email'],
        bio=validated_data['bio'],
        cover_photo=validated_data['cover_photo']
    )

    user.set_password(validated_data['password'])
    user.save()

    return user
Enter fullscreen mode Exit fullscreen mode

class ProfileSerializer(serializers.ModelSerializer):
notes = NoteSerializer(many=True, read_only=True)

class Meta:
    model = CustomUser
    fields = '__all__'
Enter fullscreen mode Exit fullscreen mode
MyTokenObtainPairSerializer: a subclass of TokenObtainPairSerializer that adds custom claims (username, email, etc.) to the token payload.
RegisterSerializer: a serializer for the user registration process. It validates the password fields, checks for duplicate emails, and creates a new CustomUser instance if all fields are valid.
ProfileSerializer: a serializer for the user profile data. It includes all fields from the CustomUser model and also serializes the related notes using NoteSerializer.

5. ** Configuring URL's **
```python


#Authentication
path('token/', views.MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('register/', views.RegisterView.as_view(), name='auth_register'),

#Profile
path('profile/', views.getProfile, name='profile'),
path('profile/update/', views.updateProfile, name='update-profile'),


Enter fullscreen mode Exit fullscreen mode

NB: I haven't included the Note url because I explained it in part one of the series

Okay Great with this steps done, we can now test whether our back-end is working as expected but first we need to apply migrations as we have created and modified our tables:

python manage.py makemigrations
python manage.py migrate

Testing our Backend with Postman

Lets now test our backend using Postman,Postman is a popular software application that allows developers to test and debug APIs. With Postman, developers can send HTTP requests to a web server and view the response.

  1. Registering Our User

Register User

we make a POST request to 'api/register/' to create a new user

2.Logging in

login

For logging in our new user we will use use the POST 'api/token' endpoint to generate our access and refresh tokens to allow authentication, we will can manually copy them as we are using postman to test and are yet to configure the endpoint with our react frontend

3.Checking our UserProfile
In-order as to process an authenticated request using postman we have to paste in the access token we got when we logged in to the bearer token in authorization like so GET 'api/profile':

bearer

thus getting the user profile that we created:

User Profile

4.Get your Notes
A user can now access their notes when authenticated through the GET 'api/notes' endpoint:

notes

Note that the list is empty as the user has not created a note yet

Conclusion

In conclusion, I want to extend my heartfelt thanks to you for taking the time to read this article on JWT authentication in the Django backend of our React Django notes app. I hope that this article has been helpful and informative as we work towards creating a fully authenticated app. In the next part of this series, we will dive deeper into integrating authentication with the React frontend so that we can see the app in its full authenticated glory. If you're interested in exploring the code, you can find the link to the app on Github(ReactDjango Notes App). Once again, thank you for reading and I look forward to sharing the next part of this series with you soon.

Top comments (8)

Collapse
 
louien profile image
louien

Great post, but I wish you included how you tied it up all together at the end. in your testing, you only testing the login from the backend (127.0.0.1:8000) and not from the frontend. how do you pass your JWT tokens to the frontend and how do you handle subsequent requests?

Collapse
 
ki3ani profile image
Kenneth Kimani

To handle this, you can use Axios for making HTTP requests from the frontend. You can find the complete implementation in my frontend repository github.com/ki3ani/my-notes-frontend
Additionally, I explained the integration in detail in my previous part 2 post, which you can refer to for more context.

Collapse
 
nemanja333 profile image
Nemanja Djordjevic

Hello, let me know what happened to your github.
Github link you provided seems incorrect.
Please update the article.
Thank you.

Collapse
 
ki3ani profile image
Kenneth Kimani

I have updated it

Collapse
 
unubi_opaluwa_f3029f48673 profile image
Unubi Opaluwa

Had to signup on this platform after reading this post, great workšŸ‘šŸ½

Collapse
 
ki3ani profile image
Kenneth Kimani

Thank you for the kind words

Collapse
 
mosesmbadi profile image
mosesmbadi

Did you include the function to only allow authenticated users to access the createnote endpoint? I couldn't find it. I'm working on something similar and I'm stuck

Collapse
 
serg_sergov_0d5eac674952e profile image
Serg Sergov • Edited

Hi!
I'm new to Django and I had an idea about Serializers that they're not involved in direct data manipulation within the database, I thought it's handled in Views - can you please comment on that?