Episyche
NextJS/Single Sign On(SSO)/Discord/
Published on
Single Sign On (SSO) gives your users convenient but secure access to all their web applications with a single set of credentials. This guide explains how we can configure Discord SSO(Social Login) in the Django Rest Framework backend with Nextjs.
Django REST framework (DRF) is a powerful and flexible toolkit for building Web APIs. Its main benefit is that it makes serialization much easier. Django REST framework is based on Django's class-based views, so it's an excellent option if you're familiar with Django. To know more about relative links click here
Next.js is a React framework. Next.js is constitutionally an excellent tool to achieve great SEO performance. building super fast static websites that behave dynamically. Next.js when building UI and UX there is flexibility. and the important great community support. To know more about relative links click here
NextAuth. js is a completely secured authentication solution for implementing authentication in Next. js applications. It is a flexible authentication library designed to sync with any OAuth service, with full support for passwordless sign-in.To know more about relative links click here
Single sign-on (SSO) is a time-saving and highly secure user authentication process. SSO lets users access multiple applications with a single account and sign out instantly with one click.
If you don't have a Discord account (Discord Developer Portal — API Docs for Bots and Developers ), please proceed with the signup option. otherwise, log in with your existing credentials.
In the Discord developer dashboard go to Applications and click New Application
It prompts a popup, In that popup Fill your application name and click Create
Then, click OAuth2 > General, In the Client information section, find the CLIENT ID and CLIENT SECRET. Copy them and paste them into a local text file. These keys will be used to interact with Discord.
Then, click Add Redirect button and add redirect URI’s
Create a Django project named discord_sso using the following command.
django-admin startproject discord_sso
Navigate to the project directory which you have created in the above step and create an app named accounts using the following command.
cd discord_sso
python manage.py startapp accounts
The project structure is given below
In this blog, we are using the Django Rest framework Python library to create APIs, therefore please install the same to proceed further.
pip install djangorestframework
django-cors-headers
library needs to establish a connection from the React frontend to Django API, hence please install the same using the below command.
pip install django-cors-headers
To Install all required python packages for this project by executing the following command.
pip install -r requirements.txt
A sample requirements.txt file can be found in the following git repository file.
Update the django settings.py file with the following items.
In INSTALLED_APPS
section add your app name (i.e. accounts) , rest_framework
and rest_framework.authtoken
#settings.py
INSTALLED_APPS = [
.....
'accounts',
'rest_framework',
'rest_framework.authtoken',
]
also add the following piece of code:
CORS_ORIGIN_ALLOW_ALL = True
AUTH_USER_MODEL = 'accounts.User'
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware', # <<< newly added line
]
A sample settings.py file can be found in the following git repository file.
In your project, go to your /accounts
directory and create a file called serializers.py
In /accounts/models.py
, add User model.
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
email = models.EmailField(unique=False)
username = models.CharField(max_length=30, unique=True)
first_name = models.CharField(max_length=30, blank=True)
last_name = models.CharField(max_length=50, blank=True)
date_joined = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=False)
is_deleted = models.BooleanField(default=False)
created_date = models.DateTimeField(default=timezone.now)
modified_date = models.DateTimeField(auto_now=True)
created_by = models.DateTimeField(default=timezone.now)
modified_by = models.DateTimeField(auto_now=True)
auth_provider = models.CharField(max_length=50, blank=True, default='email')
In /accounts/serializers.py
, add the following code.
from rest_framework import serializers
from lib.social_account import discord
from lib.register.register import register_social_user
class DiscordAuthSerializer(serializers.Serializer):
"""Handles serialization of discord related data"""
auth_token = serializers.CharField()
def validate_auth_token(self, auth_token):
user_data = discord.Discord.validate(auth_token)
try:
email = user_data['email']
provider = 'discord'
except:
raise serializers.ValidationError(
'The token is invalid or expired. Please login again.'
)
return register_social_user(
provider=provider, user_id=None, email=email, name=None)
Sample code for the /accounts/serializers.py
can be found in the following Github URL.
Create the lib directory in the following hierarchy.
lib → social_account
lib → register.
After that, please create a discord.py
file inside the lib/social_account
directory
In lib/social_account/discord.py
, add the following snippet.
import requests
class Discord:
"""
Discord class to fetch the user info and return it
"""
@staticmethod
def validate(auth_token):
"""
validate method Queries the discord url to fetch the user info
"""
try:
access_token = auth_token
headers = {'Authorization': 'Bearer %s' %access_token }
resp = requests.get('https://discord.com/api/v6/users/@me' , headers=headers)
user_info = resp.json()
return user_info
except:
return "The token is either invalid or has expired""
Sample code for the lib/social_account/discord.py
file can be found in the following Github URL.
Navigate to the lib/register
directory and create a file called register.py
In lib/register/register.py
, add the following code.
from rest_framework.authtoken.models import Token
from accounts.models import User
from django.conf import settings
from rest_framework.exceptions import AuthenticationFailed
def register_social_user(provider, user_id, email, name):
filtered_user_by_email = User.objects.filter(email=email)
if filtered_user_by_email.exists():
if provider == filtered_user_by_email[0].auth_provider:
new_user = User.objects.get(email=email)
registered_user = User.objects.get(email=email)
registered_user.check_password(settings.SOCIAL_SECRET)
Token.objects.filter(user=registered_user).delete()
Token.objects.create(user=registered_user)
new_token = list(Token.objects.filter(
user_id=registered_user).values("key"))
return {
'username': registered_user.username,
'email': registered_user.email,
'token': str(new_token[0]['key'])}
else:
raise AuthenticationFailed(
detail='Please continue your login using ' + filtered_user_by_email[0].auth_provider)
else:
user = {
'username': email, 'email': email,
'password': settings.SOCIAL_SECRET
}
user = User.objects.create_user(**user)
user.is_active = True
user.auth_provider = provider
user.save()
new_user = User.objects.get(email=email)
new_user.check_password(settings.SOCIAL_SECRET)
Token.objects.create(user=new_user)
new_token = list(Token.objects.filter(user_id=new_user).values("key"))
return {
'email': new_user.email,
'username': new_user.username,
'token': str(new_token[0]['key']),
}
Sample code for the lib/register/register.py
file can be found in the following Github URL.
Navigate to accounts django app directory, and create a DiscordSocialAuthView class in views.py.
from django.shortcuts import render
from accounts.serializer import *
from rest_framework import status
from rest_framework.response import Response
from rest_framework.generics import GenericAPIView
from rest_framework.decorators import permission_classes
from rest_framework.permissions import AllowAny
@permission_classes((AllowAny, ))
class DiscordSocialAuthView(GenericAPIView):
serializer_class = DiscordAuthSerializer
def post(self, request):
"""
POST with "auth_token"
Send an accesstoken as from discord to get user information
"""
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
data = ((serializer.validated_data)['auth_token'])
return Response(data, status=status.HTTP_200_OK)
Sample code for the /accounts/views.py
can be found in the following Github URL.
After creating the class add the urls.py file under your accounts django app.
In your core folder navigate to urls.py(core folder) and add your path with <appname> and <url_filename>(i.e. created in the above step). Your core urls.py settings are like the below.
from django.contrib import admin
from django.urls import path , include
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('<your_app_name>.<url_filename>')),
]
For example
from django.contrib import admin
from django.urls import path , include
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('accounts.urls')),
]
A sample urls.py(core folder) file can be found in the following git repository file.
Now, navigate to the Django app directory (i.e accounts ) and add the DiscordSocialAuthView Class API path in urls.py
from .views import*
from django.urls import path
urlpatterns = [
path('discord/', DiscordSocialAuthView.as_view()),
]
A sample urls.py(accounts) file can be found in the following git repository file.
Before run the django project . Run the following commands to migrate the models
python manage.py makemigrations
python manage.py migrate
To run the Django project . Run the following command
python manage.py runserver
Test the API using postman or any other API Client tool.
Before we create Next App we need to install node.js.
If you already installed node.js please proceed to next step
After installing Node.js open the terminal or command prompt and create the next.js app with the following commands
npx create-next-app frontend
After creating the Nextjs app then install next-auth using the following command.
npm i next-auth
In your project, go to your /pages/api/
directory and create a directory auth . under auth directory create file called [...nextauth].js
. This means that all of the routes starting with /api/auth
will be handled by the [...nextauth].js
file.
In /pages/api/auth/[...nextauth].js
, write the following piece of code:
Also update clientId
and clientSecret
with your credentials
import NextAuth from "next-auth"
import DiscordProvider from "next-auth/providers/discord";
var userCredential = []
export const authOptions = {
providers: [
DiscordProvider({
clientId: "<YOUR_CLIENT_ID>",
clientSecret: "<YOUR_CLIENT_SECRET>",
})
],
callbacks: {
async jwt(token, user, account, profile, isNewUser) {
var condition = token.account?.provider
if (condition === "discord") {
userCredential = { auth_token: token.account.access_token }
userCredential['provider']='discord'
}
},
async session({ session, token, user }) {
return userCredential
}
},
}
export default NextAuth(authOptions)
Sample code for the /pages/api/[...nextauth].js
file can be found in the following Github URL.
To make use of Next-auth in the app, please update the pages/_app.js
file with the below snippet.
import { SessionProvider } from "next-auth/react"
import "../styles/globals.css"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
Sample code for the pages/_app.js
file can be found in the following Github URL.
With Next-auth added to our application, we can now build our sign-in page. First, we will set up our page to display the Sign in to Discord
button if the user is not authenticated, else it returns our application. To do this, we modify the index.js
file as shown below:
import React, { useEffect } from "react";
import { useSession, signIn } from "next-auth/react"
export default function Home({ providers }) {
const { data: session, loading } = useSession()
useEffect(() => {
if (session) {
if (session.provider === "discord") {
var auth_token = session.auth_token
backendapi(auth_token)
}
}
}, [session])
function backendapi(auth_token) {
fetch(`http://127.0.0.1:8000/accounts/discord/`, {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ "auth_token": auth_token }),
}).then((data) => data.json())
.then((res) => {
if (res.token) {
document.getElementById("email_id").innerText = res.email
document.getElementById("token").innerText = res.token
}
})
}
return (
<>
<div id='discord-container'>
<div type="submit" id='discordlogin' onClick={() => signIn("discord")}>
<div className='discord-button'>
<h1>
<img className="discord-img" src="https://cdn3.iconfinder.com/data/icons/popular-services-brands-vol-2/512/discord-512.png" ></img>
</h1>
<button className="discord-login-btn">Sign in to Discord</button>
</div>
</div>
</div>
<div className='new_text'>
<div >
<label>Email Id : </label>
<label id='email_id'></label>
</div>
<div >
<label>Auth token : </label>
<label id='token'></label>
</div>
</div>
</>
)
}
Sample code for the index.js
file can be found in the following Github URL.
You can run your app via CLI with the following command and view it in your browser:
npm run dev
After successfully running both the backend and frontend apps. If you open the Nextjs URL (i.e localhost:3000) in your browser the following page will appear.
Click Sign in to Discord button , it will redirect to discord authorize page . click Authorize
If authorization is success, then you will get the email and token .
Comments