DEV Community

SJ W
SJ W

Posted on • Edited on

Adding Social Authentication to Django app using django-allauth and DRF-SimpleJWT

Hello! This is going to be the first post of my blog I will get to work on, and I am excited to write about something I am passionate about and interested in. With the first post, we are going to explore the topic of how to implement the social authentication of your Django app and log in using your GitHub account by using the followings: DRF, django-allauth, and DRF-SimpleJWT!

Here is a problem I had: a user could not log in to my website using social authentication. My web app consists of frontend - ReactJS - and backend - Django. What I wanted to achieve was to let a user log in to my web app with a GitHub account from frontend, and send JWT tokens in return. I had no idea how to make backend pass JWT tokens to a user, and eventually, redirect a user back to frontend upon successful social authentication. That's what I set out to do: coming up with a method for a user to receive JWT tokens and be redirected back to frontend upon successful social authentication.

I decided to make a post about my journey with implementation, because I couldn't necessarily find clear instructions on the way of implementing this - particularly the one using DRF, so I did have to find a way for myself by scouring the source code of libraries mentioned above, and somehow came up with the working implementation of what I was looking for in the first place by borrowing and tweaking various bits of code.

As a preface to this post, after some time of implementing this method, I came across the library called dj-rest-auth, which basically does try to implement something similar to what I am going to demonstrate, but in a much clearer and better way I suppose, so you might want to check that library out to see whether it has something you need before proceeding with this blog post. This is strictly about how I implemented social authentication, so it is expected that you will see some inefficiency and flaws in implementation. I would like to apologize in advance, if you have troubles understanding or following some of the things that I will be subsequently mentioning since I am fairly a beginner myself in developing web apps and might not be too proficient in explaining how things work. I would be more than happy to answer your inquiries if you decide to contact me via comment section or DMs. Also, I don't ever want to pretend like I know more than people regarding this, because I am merely a web developer who strives to learn and get better every day about programming! Please don't hesitate to point out things that are wrong or incorrectly explained in this post. I would really appreciate your help!

What is Social Authentication?

Social Authentication is basically a way of logging in to your website using accounts of the third-party services without having to register a new account specifically for your web application. Using your various existing accounts of providers, such as Google, Twitter, etc., you are able to log in using the third-party website seamlessly. Please check out the Wikipedia page here to learn more about various facts of social authentication.

What is django-allauth and djangorestframework-simplejwt?

Django-allauth is the library that allows for the registration, authentication, and management of accounts by offering various bits of tools for developers to integrate social authentication into your project. Using this library, you can allow users to log in to your website with their numerous social accounts on various websites.

djangorestframework-simplejwt is the library that allows developers to implement the JSON Web Token backend for django-rest-framework. Combining this and django-rest-framework provides a way for the users to access service using JWT tokens. Essentially, the backend - which is your web app - issues two tokens - access and refresh tokens - which a user sends along with requests to backend for authentication.

I am sure the most of you reading this post has dealt with django-rest-framework (I will call it DRF from here) at this point, and stumbled upon this post to figure out a way to combine DRF and social authentication to allow users to log in to your web app with existing social accounts. So for those of you who are not familiar with DRF yet, I highly suggest you google it to learn and understand the basics of it.

At this point, I will assume that readers have basic knowledge on Django and django-rest-framework - like you know how to create a Django project and implement basic APIs.

What are we trying to accomplish by combining those three libraries?

Our goal in this post is to log in to our web application with our GitHub accounts. Our web application will issue JWT tokens to a user, and a user will use tokens to access our service.

Unfortunately, the current implementation of django-allauth doesn't allow for the issuance of JWT tokens upon social authentication, so I took upon finding a way to solve this issue by looking up on the Internet, but to no avail. Without further ado, let's delve into the very topic of how I integrated social authentication into my web app.

Install the following and follow the instruction on how to go about installing these if you would like to continue with this post:

  1. django-allauth
  2. djangorestframework-simplejwt
  3. django-rest-framework

How does this work?

(What I am going to write below is purely what I learned from going through the source code of django-allauth, so there is a high likelihood that there are some inaccurate information on the overview of the mechanism of django-allauth. I apologize in advance, if I get it wrong.)

I would like to start by describing some of the bits and parts of django-allauth that I eventually got to mess with or customize, in order to get this thing working. There are only a few things you have to learn before this.

A brief overview of the whole thing from the beginning to the end is this:

  1. A user tries to log in to your website using their social accounts, and we redirect them to the login page of third party providers.
  2. A third-party provider performs authentication, and upon successfully doing so, redirects a user back to the URL of a callback view.
  3. The callback view receives, extracts and processes information necessary for a user to register and log in to its new account that gets created as a result.
  4. Upon successful creation of the new account, the web app returns to a user a set of JSON Web Tokens used to authorize its use of services.

Here are things that I eventually had to fix or modify:

  1. Adapter - From what I have read, at the time of writing this post, the adapter is basically to bridge the difference between two incompatible systems to make them work together. I suppose it makes sense since the authentication of your app and that of other third-party providers work in a totally or partially different way. Adapter will receive and extract all the information from the provider - in our example, GitHub - and pass the parsed information along to the rest of the process for authentication.

  2. Provider - GitHub, Facebook, Google are all examples of provider. You have to explicitly add the provider you would like the users to log in from to INSTALLED_APPS in your Django app.

  3. Callback view - This is a Django view that the providers make a request to, upon successful authentication.

  4. Various code of utility - Various functions and variables that django-allauth uses to finalize the process of social authentication.

No worries if you have no idea how it works for now. I will try to explain to you as much as possible and answer your questions throughout the future. Let's dive right into this!

Tutorial on implementing GitHub login

1. Generate your web app

https://github.com/settings/applications/new

Using the link above, we have to create a new OAuth application that we are going to use to redirect users to GitHub for log-in and fetch their information for our web app.

Image description

In the image above, there are three required fields that have to be filled out, in order to generate a new application.

The first required field, Application Name, can be anything I guess.

The second required field - Homepage URL - should contain the very URL of your web app - since our web app is currently in development, I can access my backend at "http://127.0.0.1:8000/".

For the third field, you have to type in the exact URL that the callback view is going to be associated with. By default, when you install django-allauth and follow through installation according to the documentation, the format for every default callback URL goes exactly like this: "accounts/[provider]/login/callback/". There are numerous providers you can choose to log in to your website with, and all you got to do is to replace the name of the provider with the one you like - if I would like to log in with Google, it would be "accounts/google/login/callback". However, as you can see in the screenshot above, I have appended the link with the underscore letter, because we are going to modify and use our new callback view. Since I can't pair the original callback URL for GitHub with my soon-to-be-created new callback view, I thought the best thing I could do was to append the "_" at the end. So, the callback URL of our customized callback view will be "http://127.0.0.1:8000/accounts/github/login/callback_/". Of course, it means that once we finish modifying the callback view to our liking, we are going to pair it with the exact callback URL.

Image description

Once we proceed with the creation of our new app, you will be presented with a page with a following box, which contains information you need, in order for social authentication to work. As you can see in the picture above, there are two values - Client ID and Client secrets - that need to be copied over to your Django application after this very step. Client ID, by default, is generated, once the app is generated, whereas the secret code must be generated, by clicking on the button "Generate a new client secret". When you are done with the generation of both values, it's time to move on to registering your new OAuth application to your Django application.

2. Register your OAuth2 app to Your Django Project

Image description

Let's log-in to the default admin page of your Django project!

Image description

The very first thing we have to do at this point is to add our domain to "Sites". At the very bottom of the picture, you can see the box that allows admins to edit objects of the type Sites. Let's add our domain by clicking the "add" button.

Image description

After clicking the "add" button, we are presented with a HTML form like a screenshot right above, which requires us to register our domain and its display name. At the time of writing this, I can access my Django application at the following domain: http://127.0.0.1:8000. In this case, I will add the domain of my Django application, which is "127.0.0.1" without the port 8000 at the end. If your Django application uses a different domain, such as "localhost" or something else, then you have to input that, instead of what I plugged in
You may input the same value for the display name part of the form, and at last, you may click "SAVE" at the bottom of the form to continue to next step.

Image description

In addition to adding the domain to Site, we have to specify the ID of the domain we just added in the settings file of your Django project. Open "settings.py", and add the following and save the file:

Image description

In "settings.py", the value of the variable of "SITE_ID" is set to 1, because there exists two domains at the moment for my site - "example.com" and "127.0.0.1". As you can see in the screenshot before the screenshot right above, and the one we just added was the second one that got registered - the very first domain that existed before, example.com, has an ID of 0, whereas the one we added gets assigned the ID of 1. The point is to have the variable "SITE_ID" point to the ID of the domain of your Django application - "127.0.0.1" is the current domain of my Django application.

Image description

If you are still unsure about the ID of the domain, the sure way of knowing is opening a shell prompt for your Django application, and query what its ID is, by following an example in a screenshot above. All you have to do is to replace "127.0.0.1" with whatever your domain is, and Django will present an ID you need to set to the variable "SITE_ID" in "settings.py".

Now, let's move on to adding our GitHub application we created not too long ago to our Django project!

Image description

Now, we are back to where we first were, right after the successful log-in to Django Admin Panel. In the category that says "SOCIAL ACCOUNTS", you will see the subcategory "Social applications". Click the "Add" button to register our GitHub application.

Image description

Image description

Let's first check out the second screenshot to see what needs to be filled out. The very first field - Providers - will be GitHub, because we want to log in to our website with GitHub. The second field - Name - can be whatever value you think is appropriate in this situation. The third field - "Client id" - is where you plug in the value of "Client id" of your GitHub OAuth2 Application you generated - in this case, it would be the very first censored value in the first screenshot. The fourth field - "Secret Key" - is where you plug in the value of Client Secrets you generated. Let's skip the field "Key" and move right on to the field "Sites". For the field "Sites", add the domain we added prior to this - I added "127.0.0.1" previously. Once you are done, click the "SAVE" button, and we are officially done with the setup of the Social Application! After this, We are going to jump right into coding our callback view and various codes needed!

3. Copying and modifying bits of django-allauth

Intro

We have finally reached the point where the provider - in this case, GitHub - and our Django project comes in contact with one another to facilitate the real process of social authentication! At this point, it's entirely up to us to decide what to do with the information of a user provided by the provider. A brief summary of what we are going to do in this part of the tutorial is to register and redirect a new user to the front-end website, by utilizing the very information provided by the provider. The information contains the various things related to a user, such as the UID, username, etc. Check out the link below to see what bits of information GitHub provides to your Django application via a callback view.

https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user

Modification of the original callback view

Image description

Original callback view

Image description

The modified version of a callback view

The screenshot right above shows modification done to the callback view "OAuth2CallbackView" from django-allauth. What we are trying to achieve here is, upon successful log-in, to redirect a user to the frontend part of the web application. The provider, after successful authentication, will pass the information of a user to backend by sending a request to the URL of the callback view, and we will register a user, if needed, and redirect a user to frontend, with the JWT token. In order to make this happen, the original callback view needs to be modified so that it actually redirects a user to frontend with the JWT token a user has requested. In one of the applications of the Django project, I have created a file named "allauth_views.py" and copied the original version of callback view - a class named OAuth2CallbackView - over to the file.

The First Red Box

The very first red box in the screenshot right above this paragraph redirects a user to the home page of frontend upon unsuccessful authentication - in our case, a user will be redirected to the frontend part of the web app if it has failed to log in to GitHub for one of many reasons. That was pretty easy to implement. Maybe, you can even customize it to prompt an error message indicating the failure to log in to the provider of their choice? It's all up to you to change the code to your liking.

The Second Red Box

Please SKIP this section if you haven't extend the User model in your Django application. Scroll all the way down to "The Third Red Box"

The next red box contains a function called "self.adapter.complete_login", which accepts four arguments - request, app, token, access_token. This is probably the trickiest part of implementation, due to having to copy from the source code of django-allauth and modify about three different functions. The very first function at this stage that we are going to modify is the function "complete_login" of the adapter for GitHub.

Image description

Image description

Image description

Create a new file called "adapters.py" in the very same directory that you created "allauth_view.py", and let's import the "GitHubOAuth2Adapter" class from "allauth.socialaccount.providers.github.views" and create a new subclass called "CustomGitHubOAuth2Adapter" inheriting from the "GitHubOAuth2Adapter" class. And once you are done creating the subclass, override its method called "complete_login" by copying the source code of it from here to the subclass "CustomGitHubOAuth2Adapter". In django-allauth, every provider has its own adapter to facilitate the process of the log-in, however, since we are in the process of trying to log in with a GitHub account, we get to work with an adapter for GitHub. Anyway, let's briefly go over the function of "complete_login" and delve into the red box within the last screenshot above.

The main purpose of the function "complete_login" is to generate an instance of the class "SocialLogin", which is a class for temporarily retaining the information of a user received from the provider to ease and facilitate the social login process. Its main role is to register and authenticate a user using the information from the provider. The function requests and receives the general information of a user from GitHub, using the very token obtained from the successful login of the user, and use it to generate the instance of the class "SocialLogin".

If you look at a red box in the last screenshot, you see that I have implemented the customized version of the provider class "GitHubProvider" named "CustomGitHubProvider" whose function "sociallogin_from_response" is called. The main task we need to do is to reimplement our version of the class "SocialLogin" and modify the function "sociallogin_from_response" to make it use our "SocialLogin" class we have created, for the rest of the duration of the login process. Why do we need to modify the class "SocialLogin"? We will see why soon.

Image description

Actually, you can skip this part if you haven't extended the default User model, because that's what we are basically going to deal with in this bit. Skip to the next screenshot below if you have a default User model provided by Django.

Let's create our "SocialLogin" class which inherits from the original version of "SocialLogin" in a new file or whatever file that exists. I gave our class a name "ModifiedSocialLogin", and it's neatly placed right in an existing "allauth_views.py" file because why not?

The function we are going to mess with in the class "ModifiedSocialLogin" is a function named "save". What the function does is to create a new user upon learning that a user doesn't exist. It checks whether the social user already exists by querying the database for checking the existence of the unique ID for a given provider, and if it exists, then it decides that the user is already registered. The reason why I placed a set of custom code into the function "save" is that, at the outset of the project, I extended the User model to have additional fields - I added the second password and a nickname field to the model for whatever reason. Basically, backend can't register a user without a nickname and the second password, and the original "save" function of the "SocialLogin" class doesn't have the functionality to assign a new user a nickname and the second password. So what I did was to place various bits of code in the "save" function of the class "ModifiedSocialLogin" for assigning the unusuable second password and a random nickname to a new user. You can customize it to your own liking based on the overall structure of the User model of your project. Now, let's move on to reimplementing the function "sociallogin_from_response" of "GitHubProvider" class.

Image description

An original "GitHubProvider" class will always use an original "SocialLogin" class, and we don't want that. So it's our job to make sure that the "GitHubProvider" class uses our "ModifiedSocialLogin" class we have just created a moment ago. To do that, I basically created a new class "CustomGitHubProvider" which inherits from the "GitHubProvider" class, and overrode its function "sociallogin_from_response". I added a line in a red box inside a screenshot above to make sure that "CustomGitHubProvider" uses an instance of the "ModifiedSocialLogin" class. That's all we got to do with the newly created "CustomGitHubProvider" class, and all we got to do now is to go back to the "CustomGitHubOAuth2Adapter" class and let its function "complete_login" return an instance of the class "ModifiedSocialLogin" we created!

I neatly placed a newly created provider in a red box

We have overridden the "complete_login" function to return the instance of the "ModifiedSocialLogin" by calling the function "sociallogin_from_response" of the "CustomGitHubProvider".

The Third Red Box

Image description

The third red box contains the function called "complete_social_login". The function accepts two arguments - request and login (SocialLogin we get from the second red box). Being located right at the end of the "dispatch" function of the callback view, you can infer from the "return" statement the fact that it probably has something to do with creating and returning the JWT tokens to the user. Let's break down what the function does.

Image description

Copy the function "complete_social_login" from the official source code of django-allauth to any file in the same directory as "adapters.py" and "allauth_view.py" we created prior. I personally created a file called "utils.py", and placed the function there. We aren't going to modify anything in the function, but the reason why we need to copy this function to a file is because the function "complete_social_login" is going to call the modified version of the function "_complete_social_login". Let's copy the function "_complete_social_login" as well to "utils.py", because we are going to modify this to make it return a response object with JWT token!

Prior to focusing on the red box in the screenshot above, let's dive into what we are looking at in the function. The function starts with checking whether the user exists, by calling the "lookup" function of the class "ModifiedSocialLogin". And eventually, it checks the value of the "process" key of the state of the instance of "ModifiedSocialLogin". As far as I know, the log-in system we are implementing doesn't set any state that results in the first two expressions being evaluated to True. So, the else condition ends up being evaluated to True in our case, and what we face is the function
"_complete_social_login". This function is a highlight of our journey, because this is where the real magic of giving the JWT tokens to users happens. Let's see what goes on in the function.

Image description

If...else statement, at the very beginning, evaluates whether the user is already registered in the system. If it evaluates to False, then it saves a new user by calling the "save" function of "ModifiedSocialLogin" we created before. A user is saved to DB, and with an instance of our User model of our Django web app, we generate both access and refresh tokens used for authentication throughout the duration of using the web application. I decided to store the refresh token in the HttpOnly cookie of the response, for the duration of about a day, so that a user doesn't have to log-in as often. And finally, we return the response object, whose cookie contains the JWT refresh token we just generated, and a user gets redirected to the frontend, with the refresh token it requested.

Image description

You can check out both "get_tokens_for_user" and "set_tokens_in_cookie" functions used in "_complete_social_login".

Set a URL to a newly created callback function

Now, the hardest part is done, and all we are left with is some easy stuffs that must be done to ensure that everything works as intended!

Remember the "OAuth2CallbackView" class we made in "allauth_view.py"? We want to make sure that our provider - GitHub - needs to call the view, right after determining whether a user has successfully or unsuccessfully logged in. In order to do that, we have to pair the callback view with the URL!

Image description

I will assume you are aware of how to set a view to a certain URL, based on the premise I mentioned that you have a decent experience in Django. But the most important thing here is to create a new class method of "OAuth2CallbackView" and assign it to a variable "github_login_callback" by plugging "CustomGitHubOAuth2Adapter" into "OAuth2CallbackView.adapter_view", which returns a view method that can be used to set to a URL. The URL for a callback function for every provider of django-allauth goes like this: "accounts/[provider's id]/login/callback/". We replace the very "[provider's id]" with an id of a provider - GitHub, in our case. Import the class "CustomGitHubProvider" from "adapters.py" in the same directory, and fetch its ID, by typing in "CustomGitHubProvider.id". Set the variable "github_login_callback" to a parameter "view" of a "path" function, and neatly place the path function into the variable "urlpatterns"!

4. Logging in with GitHub from frontend

Image description

Image description

Image description

Here is a frontend part of my web application, which I made with ReactJS. As you can see in the login box, I have added a button for logging in with my GitHub account. Check out the screenshot right above this, and you see that clicking a button will simply redirect us to the page "http://127.0.0.1:8000/accounts/github/login/".

Image description

Upon clicking a button, we get redirected to the page with a button that allows us to log in to GitHub. All we have to do is to click that very button "Continue" to successfully log in and get redirected back to frontend!

Image description

We get redirected to GitHub and greeted with a page for logging in with our GitHub account. Simply plug in your GitHub account and log in.

Image description

I was redirected to the frontend part of the web application upon successful log-in, and was able to confirm that I logged in!

Conclusion

Thanks for reading my first ever write-up. It was an amazing learning experience, trying to understand the inner-workings of django-allauth and come up with a novel solution for the problem. Even after finishing writing this, I still feel like the write-up is largely inadequate in many aspects, so I will continuously update this write-up throughout the foreseeable future.

Check out the code here to see what's created and fixed

adapters.py

Image description

allauth_views.py

Image description

Image description

Image description

utils.py

Image description

Image description

urls.py

Image description

Top comments (0)