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',
]
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),
}
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'),
]
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:
- 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**
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)
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.
- 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
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
class ProfileSerializer(serializers.ModelSerializer):
notes = NoteSerializer(many=True, read_only=True)
class Meta:
model = CustomUser
fields = '__all__'
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'),
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.
- Registering Our User
we make a POST request to 'api/register/' to create a new user
2.Logging in
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':
thus getting the user profile that we created:
4.Get your Notes
A user can now access their notes when authenticated through the GET 'api/notes' endpoint:
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)
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?
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.
Hello, let me know what happened to your github.
Github link you provided seems incorrect.
Please update the article.
Thank you.
I have updated it
Had to signup on this platform after reading this post, great workšš½
Thank you for the kind words
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
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?