Adding Next-Auth Authentication to Django, FastAPI, and Next.js Stack

Integrate Next-Auth authentication with Django backend, FastAPI API, and Next.js frontend for secure user authentication.

Engineering

This tutorial builds upon the Creating a Full Stack Application with Django, FastAPI, and Next.js guide. We'll integrate Next-Auth (Auth.js) authentication into our existing full-stack application.

1. Initial Setup

If you haven't completed the first tutorial, start by cloning the repository and setting up the initial project.

1.1 Clone the Repository

git clone [email protected]:damianhodgkiss/next-django-fastapi-fullstack-tutorial.git

1.2 Start the Docker Stack

docker compose up --build -d

1.3 Run Database Migrations

docker compose exec api python manage.py migrate

1.4 Create a Superuser

docker compose exec api python manage.py createsuperuser

Follow the prompts to create your superuser account.

2. Install and Configure Next-Auth

Now, let's set up Next-Auth in our Next.js frontend.

2.1 Install Next-Auth

docker compose exec frontend yarn add next-auth@beta

2.2 Create Next-Auth Configuration

Create a new file src/auth.ts in your frontend directory:

import NextAuth from "next-auth"
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  secret: process.env.NEXTAUTH_SECRET || "secret",
  providers: [],
})

2.3 Set Up Next-Auth API Routes

Create a new file src/app/api/auth/[...nextauth]/route.ts:

import { handlers } from "@/auth";

export const { GET, POST } = handlers;

2.4 Add Next-Auth Middleware

Edit src/middleware.ts:

export { auth as middleware } from "@/auth";

3. Create Sign In and Sign Out Components

Let's create components for signing in and out, and update our home page.

3.1 Create Sign In Button Component

Create a new file src/app/components/sign-in.tsx:

import { signIn } from "next-auth/react";

export function SignInButton() {
  return <button className="bg-blue-500 py-2 px-4 rounded text-white" onClick={() => signIn()}>Sign in</button>
}

3.2 Create Sign Out Button Component

Create a new file src/app/components/sign-out.tsx:

import { signOut } from "next-auth/react";

export function SignOutButton() {
  return <button className="bg-blue-500 py-2 px-4 rounded text-white" onClick={() => signOut()}>Sign out</button>
}

3.3 Update Home Page

Edit src/app/page.tsx:

import { SignInButton } from "@/components/sign-in";
import { SignOutButton } from "@/components/sign-out";
import { auth } from "@/auth";
import { Sign } from "crypto";

export default async function Home() {
  const session = await auth();
  const { user } = session || {};
  const isSignedIn = !!user;

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      {
        isSignedIn ?
          <div>
            <pre>{JSON.stringify(session, null, 2)}</pre>
            <SignOutButton />
          </div>
          : <SignInButton />
      }
    </main>
  );
}

4. Configure Next-Auth for Django Credentials

Now, we'll set up Next-Auth to use Django credentials for authentication.

4.1 Set Environment Variables

Create a frontend/.env.local file:

NEXTAUTH_SECRET=secret
INTERNAL_API_URL=http://api:8000

4.2 Create Next-Auth Type Declarations

Create a new file frontend/src/types/next-auth.d.ts:

import NextAuth from "next-auth";
import { JWT } from "next-auth/jwt"

declare module 'next-auth' {
  interface User {
    first_name: string;
    last_name?: string;
    is_staff: boolean;
    is_active: boolean;
    is_superuser: boolean;
    last_login: string;
    date_joined: string;
  }

  interface Session {
    user: User;
    access_token: string;
    token_type: 'Bearer';
  }
}

declare module 'next-auth/jwt' {
  interface JWT {
    user: User;
    access_token: string;
    token_type: 'Bearer';
  }
}

4.3 Update Next-Auth Configuration

Update frontend/src/auth.ts:

import NextAuth, { type User } from "next-auth";
import { CredentialsSignin } from '@auth/core/errors';
import CredentialsProvider from 'next-auth/providers/credentials';

export const { handlers, signIn, signOut, auth } = NextAuth({
  secret: process.env.NEXTAUTH_SECRET || "secret",
  providers: [
    CredentialsProvider({
      id: 'django',
      name: 'Django',
      credentials: {
        username: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        const response = await fetch(`${process.env.INTERNAL_API_URL}/users/login/`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(credentials),
        });
        const json = await response.json();

        if (!response.ok) throw new CredentialsSignin(json.detail);

        return json;
      }
    })
  ],
  callbacks: {
    async jwt({ token, user, account }) {
      switch (account?.provider) {
        case 'django':
          const credentialUser = user as any;
          token.access_token = credentialUser?.access_token;
          token.token_type = credentialUser?.token_type;
          token.user = credentialUser?.user;
          break;
      }
      return token;
    },
    async session({ session, token }) {
      const accessToken = token?.access_token;
      const expireSession = {
        expires: new Date().toISOString(),
      };

      if (!accessToken) {
        return expireSession;
      }

      const response = await fetch(`${process.env.INTERNAL_API_URL}/users/session/`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${accessToken}`,
        },
      });

      if (response.status !== 200) {
        return expireSession;
      }

      const json = await response.json();

      return {
        ...session,
        access_token: json.access_token,
        token_type: json.token_type,
        user: json.user,
      };
    }
  }
})

4.4 Update Docker Compose Configuration

Edit docker-compose.yml:

frontend:
    env_file:
      - ./frontend/.env.local

4.5 Restart Frontend Container

docker compose up frontend -d

5. Create API Routes for Django Authentication

Now, we'll create the necessary API routes in our Django backend.

5.1 Update Backend Requirements

Edit backend/requirements.txt:

python-jose==3.3.0

5.2 Create User Schemas

Create a new file backend/users/schemas.py:

from pydantic import BaseModel
from pydantic import ConfigDict
from typing import Optional
from datetime import datetime

class LoginRequest(BaseModel):
    username: str
    password: str

class UserSchema(BaseModel):
    id: int
    email: str
    first_name: Optional[str] = None
    last_name: Optional[str] = None
    name: Optional[str] = None
    is_staff: bool
    is_active: bool
    is_superuser: bool
    last_login: datetime | None
    date_joined: datetime

    model_config = ConfigDict(from_attributes=True)

class Token(BaseModel):
    access_token: str
    token_type: str
    user: UserSchema

5.3 Create Authentication Utilities

Create a new file backend/users/utils.py:

from fastapi import HTTPException, status, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError
from django.conf import settings
from django.contrib.auth import get_user_model
from typing import Any, Annotated
from .models import User

SECRET_KEY = settings.SECRET_KEY
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 30
ACCESS_TOKEN_VALID_MINUTES = 1

security = HTTPBearer()
optional_security = HTTPBearer(auto_error=False)
credentials_exception = HTTPException(
    status_code=status.HTTP_401_UNAUTHORIZED,
    detail="Could not validate credentials",
    headers={"WWW-Authenticate": "Bearer"},
)

def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    token = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return token

async def get_access_token(
    credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]
) -> str:
    if not credentials:
        raise credentials_exception
    try:
        return credentials.credentials
    except JWTError:
        raise credentials_exception

async def get_token_payload(
    token: Annotated[str, Depends(get_access_token)]
) -> dict[str, Any]:
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        id: str | None = payload.get("sub")
        if id is None:
            raise credentials_exception
        return payload
    except JWTError:
        raise credentials_exception

async def get_current_user(
    payload: Annotated[dict[str, Any], Depends(get_token_payload)]
) -> User:
    try:
        id: str | None = payload.get("sub")
        if id is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    user = await get_user_model().objects.filter(id=id, is_active=True).afirst()
    if user is None:
        raise credentials_exception
    return user

async def get_optional_current_user(
    credentials: Annotated[HTTPAuthorizationCredentials, Depends(optional_security)]
) -> User | None:
    if not credentials:
        return None
    try:
        payload = await get_token_payload(credentials.credentials)
        return await get_current_user(payload)
    except HTTPException:
        return None

async def get_current_active_user(
    current_user: Annotated[User, Depends(get_current_user)],
) -> User:
    if not current_user.is_active:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user

5.4 Create Authentication Routers

Create a new file backend/users/routers.py:

from fastapi import FastAPI, APIRouter, Depends
from django.contrib.auth import authenticate
from typing import Annotated, Any
from datetime import timedelta
from .schemas import LoginRequest, Token
from datetime import datetime
from .utils import (
    create_access_token,
    get_access_token,
    get_token_payload,
    get_current_active_user,
    credentials_exception,
    ACCESS_TOKEN_EXPIRE_MINUTES,
    ACCESS_TOKEN_VALID_MINUTES,
)
from .models import User

router = APIRouter(prefix="/users", tags=["users"])

@router.post("/login/")
def login(login: LoginRequest):
    user = authenticate(email=login.username, password=login.password)
    if not user:
        raise credentials_exception

    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": str(user.id)}, expires_delta=access_token_expires
    )

    return Token(access_token=access_token, token_type="Bearer", user=user)

@router.post("/session/")
async def check_session(
    token: Annotated[str, Depends(get_access_token)],
    payload: Annotated[dict[str, Any], Depends(get_token_payload)],
    current_user: Annotated[User, Depends(get_current_active_user)],
) -> Token:
    exp_time = datetime.fromtimestamp(payload["exp"])
    current_time = datetime.now()
    time_difference = exp_time - current_time
    difference_in_minutes = time_difference.total_seconds() / 60

    if difference_in_minutes >= ACCESS_TOKEN_VALID_MINUTES:
        return Token(access_token=token, token_type="Bearer", user=current_user)

    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": str(current_user.id)}, expires_delta=access_token_expires
    )
    return Token(access_token=access_token, token_type="bearer", user=current_user)

def register_routers(app: FastAPI):
    app.include_router(router)

5.5 Update ASGI Configuration

Update backend/mysaas/asgi.py:

import os
from django.core.asgi import get_asgi_application
from fastapi import FastAPI

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysaas.settings")

application = get_asgi_application()
fastapp = FastAPI(
    servers=[
        {
            "url": "/api/v1",
            "description": "V1",
        }
    ]
)

def init(app: FastAPI):
    from users.routers import register_routers as register_user_routers

    register_user_routers(app)

    @app.get("/health")
    def health_check():
        return {"status": "ok"}

init(fastapp)

6. Verify the Setup

To verify that everything is set up correctly:

  1. Visit http://localhost/ - You should see a sign-in button.
  2. Visit http://localhost/docs - You should see the /users/login/ endpoint in the FastAPI documentation.
  3. Visit http://localhost/admin - You should be able to log in to the Django admin site using the superuser credentials you created earlier.

Certainly! Here's Part 7 of the tutorial focusing on protecting API routes:

7. Protecting API Routes

To protect API routes, use the get_current_active_user function in your FastAPI route definitions:

@router.post("/my-api-route/")
async def my_api_route(
    current_user: Annotated[User, Depends(get_current_active_user)],
):
    # user is authenticated as current_user
    ...

When making requests to protected routes from the frontend, include the access token in the Authorization header:

import { auth } from "@/auth";

const session = await auth();
// ...check session.access_token is not null
const response = await fetch(`${process.env.INTERNAL_API_URL}/.../my-api-route/`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${session.access_token}`,
  },
  body: JSON.stringify(data),
});
// ...check response.ok or response.status

This setup ensures that the access token from the Next-Auth session is passed to the API route, allowing the backend to authenticate the user. The get_current_active_user function will verify the token and retrieve the corresponding user, making it available in your route handler.

By using this approach, you can easily secure any API route that requires authentication, ensuring that only authenticated users can access these endpoints.

Conclusion

You have now successfully integrated Next-Auth authentication into your Django, FastAPI, and Next.js stack. This setup allows users to authenticate using Django credentials through a Next.js frontend, with FastAPI handling the API requests. Remember to implement additional security measures and features like password reset or email verification as needed for your specific application.