By KFSys
System Administrator
A complete guide to implementing JWT authentication with Django REST Framework backend and Next.js frontend.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Accepted Answer
# Create and activate virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install django djangorestframework djangorestframework-simplejwt django-cors-headers
# Create project
django-admin startproject backend
cd backend
python manage.py startapp accounts
Edit backend/settings.py
:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_simplejwt',
'corsheaders',
'accounts',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# CORS Settings
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
]
CORS_ALLOW_CREDENTIALS = True
# REST Framework Settings
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
}
# JWT Settings
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': True,
}
Create accounts/serializers.py
:
from rest_framework import serializers
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'email', 'first_name', 'last_name')
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
password2 = serializers.CharField(write_only=True, required=True)
class Meta:
model = User
fields = ('username', 'password', 'password2', 'email', 'first_name', 'last_name')
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):
validated_data.pop('password2')
user = User.objects.create_user(**validated_data)
return user
Create accounts/views.py
:
from rest_framework import generics, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from django.contrib.auth.models import User
from .serializers import RegisterSerializer, UserSerializer
class RegisterView(generics.CreateAPIView):
queryset = User.objects.all()
permission_classes = (AllowAny,)
serializer_class = RegisterSerializer
class UserDetailView(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
Create accounts/urls.py
:
from django.urls import path
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from .views import RegisterView, UserDetailView
urlpatterns = [
path('register/', RegisterView.as_view(), name='register'),
path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('user/', UserDetailView.as_view(), name='user_detail'),
]
Update backend/urls.py
:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/auth/', include('accounts.urls')),
]
python manage.py makemigrations
python manage.py migrate
python manage.py runserver
npx create-next-app@latest frontend
cd frontend
npm install axios js-cookie
When prompted, choose:
Create src/context/AuthContext.tsx
:
'use client';
import React, { createContext, useState, useContext, useEffect } from 'react';
import axios from 'axios';
import Cookies from 'js-cookie';
interface User {
id: number;
username: string;
email: string;
first_name: string;
last_name: string;
}
interface AuthContextType {
user: User | null;
loading: boolean;
login: (username: string, password: string) => Promise<void>;
register: (username: string, email: string, password: string, password2: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const api = axios.create({
baseURL: 'http://localhost:8000/api/auth',
headers: {
'Content-Type': 'application/json',
},
});
// Add token to requests
api.interceptors.request.use((config) => {
const token = Cookies.get('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Handle token refresh
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = Cookies.get('refresh_token');
const response = await axios.post('http://localhost:8000/api/auth/token/refresh/', {
refresh: refreshToken,
});
const { access } = response.data;
Cookies.set('access_token', access);
originalRequest.headers.Authorization = `Bearer ${access}`;
return api(originalRequest);
} catch (refreshError) {
logout();
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
useEffect(() => {
const loadUser = async () => {
const token = Cookies.get('access_token');
if (token) {
try {
const response = await api.get('/user/');
setUser(response.data);
} catch (error) {
console.error('Failed to load user', error);
Cookies.remove('access_token');
Cookies.remove('refresh_token');
}
}
setLoading(false);
};
loadUser();
}, []);
const login = async (username: string, password: string) => {
const response = await api.post('/login/', { username, password });
const { access, refresh } = response.data;
Cookies.set('access_token', access);
Cookies.set('refresh_token', refresh);
const userResponse = await api.get('/user/');
setUser(userResponse.data);
};
const register = async (username: string, email: string, password: string, password2: string) => {
await api.post('/register/', { username, email, password, password2 });
await login(username, password);
};
const logout = () => {
Cookies.remove('access_token');
Cookies.remove('refresh_token');
setUser(null);
};
return (
<AuthContext.Provider value={{ user, loading, login, register, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
Update src/app/layout.tsx
:
import { AuthProvider } from '@/context/AuthContext';
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}
Create src/app/login/page.tsx
:
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/context/AuthContext';
import Link from 'next/link';
export default function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { login } = useAuth();
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
try {
await login(username, password);
router.push('/dashboard');
} catch (err: any) {
setError(err.response?.data?.detail || 'Invalid credentials');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
<h2 className="text-3xl font-bold text-center">Sign In</h2>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700">
Username
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<button
type="submit"
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700"
>
Sign In
</button>
</form>
<p className="text-center text-sm text-gray-600">
Don't have an account?{' '}
<Link href="/register" className="text-blue-600 hover:underline">
Sign up
</Link>
</p>
</div>
</div>
);
}
Create src/app/register/page.tsx
:
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/context/AuthContext';
import Link from 'next/link';
export default function RegisterPage() {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [password2, setPassword2] = useState('');
const [error, setError] = useState('');
const { register } = useAuth();
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (password !== password2) {
setError('Passwords do not match');
return;
}
try {
await register(username, email, password, password2);
router.push('/dashboard');
} catch (err: any) {
setError(err.response?.data?.username?.[0] || 'Registration failed');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
<h2 className="text-3xl font-bold text-center">Create Account</h2>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700">
Username
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Email
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Confirm Password
</label>
<input
type="password"
value={password2}
onChange={(e) => setPassword2(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<button
type="submit"
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700"
>
Sign Up
</button>
</form>
<p className="text-center text-sm text-gray-600">
Already have an account?{' '}
<Link href="/login" className="text-blue-600 hover:underline">
Sign in
</Link>
</p>
</div>
</div>
);
}
Create src/app/dashboard/page.tsx
:
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/context/AuthContext';
export default function DashboardPage() {
const { user, loading, logout } = useAuth();
const router = useRouter();
useEffect(() => {
if (!loading && !user) {
router.push('/login');
}
}, [user, loading, router]);
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-xl">Loading...</div>
</div>
);
}
if (!user) {
return null;
}
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold">Dashboard</h1>
<button
onClick={logout}
className="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700"
>
Logout
</button>
</div>
</nav>
<div className="max-w-7xl mx-auto px-4 py-8">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold mb-4">Welcome back, {user.username}!</h2>
<div className="space-y-2">
<p><span className="font-semibold">Email:</span> {user.email}</p>
<p><span className="font-semibold">Username:</span> {user.username}</p>
<p><span className="font-semibold">User ID:</span> {user.id}</p>
</div>
</div>
</div>
</div>
);
}
Update src/app/page.tsx
:
'use client';
import { useAuth } from '@/context/AuthContext';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import Link from 'next/link';
export default function Home() {
const { user, loading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!loading && user) {
router.push('/dashboard');
}
}, [user, loading, router]);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center space-y-6">
<h1 className="text-4xl font-bold">Welcome to JWT Auth Demo</h1>
<p className="text-gray-600">Django + Next.js Authentication</p>
<div className="space-x-4">
<Link
href="/login"
className="inline-block bg-blue-600 text-white px-6 py-3 rounded-md hover:bg-blue-700"
>
Sign In
</Link>
<Link
href="/register"
className="inline-block bg-gray-600 text-white px-6 py-3 rounded-md hover:bg-gray-700"
>
Sign Up
</Link>
</div>
</div>
</div>
);
}
npm run dev
http://localhost:3000
/dashboard
directlysecure: true, httpOnly: true, sameSite: 'strict'
in production.env
filesROTATE_REFRESH_TOKENS: True
djangorestframework-simplejwt[blacklist]
CORS errors: Verify CORS_ALLOWED_ORIGINS
includes your frontend URL
401 errors: Check token expiration times and refresh logic
Cookie issues: Ensure cookies are being set correctly with proper domain/path
Token not refreshing: Verify interceptor logic and refresh endpoint
You now have a fully functional JWT authentication system with Django and Next.js! This setup provides secure, scalable authentication with automatic token refresh and proper separation of concerns between frontend and backend.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.