Next.js + Django SSO

For information about authentication in a Next.js app, refer to this page: https://nextjs.org/learn/dashboard-app/adding-authentication this implementation of authentication is based on auth.js: https://authjs.dev/

Django SSO is a python package for Single Sign-On: https://github.com/davidhaker/django-sso

My project is a Next.js app with a Python Flask backend, and I use a Django SSO server for authentication.

config in Next.js app

auth.config.js

import type { NextAuthConfig } from "next-auth";

export const authConfig = {
  pages: {
    signIn: "/login",
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      return isLoggedIn;
    },
    async session({ session, token }) {
      // @ts-ignore
      session.user = { ...token.user };
      return session;
    },
    async jwt({ token, user }) {
      if (user) {
        token.user = user;
      }
      return token;
    },
  },
  providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;

auth.ts

import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { authConfig } from "./auth.config";
import { fetchUser } from "@/app/lib/actions";

export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      async authorize(credentials) {
        if (credentials.authenticated) {
          const userInfo = await fetchUser(credentials.user_identy as string);
          return {
            ...userInfo,
            name: credentials.user_identy,
          };
        }
        return null;
      },
    }),
  ],
});

middleware.ts

import NextAuth from "next-auth";
import { authConfig } from "./auth.config";

export default NextAuth(authConfig).auth;

export const config = {
  // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
  matcher: ["/((?!api|_next/static|_next/image|.*\\.png$|sso).*)"],
};

Route in app.js

app/login/route.tsx

import { redirect } from "next/navigation";
import { fetchToken } from "@/app/lib/actions";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const url = new URL(request.url);
  const searchParams = url.searchParams;
  const nextUrl = searchParams.get("callbackUrl") || "";
  const ssoTokenResponse = await fetchToken(nextUrl);
  const token = ssoTokenResponse.token;

  if (token) {
    cookies().set("auth_token", token);
    redirect(`${process.env.NEXT_PUBLIC_SSO_AUTH_LOGIN}?sso=${token}`);
  } else {
    return NextResponse.json(
      { message: "Invalid credentials" },
      { status: 401 }
    );
  }
}

app/sso/accept/route.tsx

import { fetchAuthInfo, confirmAuth } from "@/app/lib/actions";
import { cookies } from "next/headers";
import { signIn } from "@/auth";
import { NextResponse } from "next/server";

export async function GET() {
  const cookieStore = cookies();
  const token = cookieStore.get("auth_token");
  if (!token) {
    return NextResponse.json(
      { message: "Invalid credentials" },
      { status: 401 }
    );
  }
  const authInfo = await fetchAuthInfo(token.value);
  await confirmAuth(token.value);
  await signIn("credentials", authInfo);
  return NextResponse.json({ message: "Login successful" }, { status: 200 });
}

app/sso/event/route.tsx

import { NextResponse } from "next/server";
import { postSsoEvent } from "@/app/lib/data";

export async function POST(request: Request) {
  const body = await request.json();
  await postSsoEvent(body);
  return NextResponse.json({ message: "POST request" });
}

actions in next.js app

app/lib/actions.ts

"use server";

import { redirect } from "next/navigation";
import { VersionPostData } from "./definitions";
import { signOut } from "@/auth";

export async function fetchToken(nextUrl: string) {
  const formData = new FormData();
  formData.append("token", `${process.env.NEXT_PUBLIC_SSO_AUTH_SECRET}`);
  formData.append("next_url", nextUrl);
  const response = await fetch(`${process.env.NEXT_PUBLIC_SSO_AUTH_OBTAIN}`, {
    method: "POST",
    body: formData,
  }).catch((error) => {
    throw new Error(`认证服务连接失败, 请联系管理员: ${error}`);
  });
  return response.json();
}

export async function fetchAuthInfo(authToken: string) {
  let formData = new FormData();
  formData.append("token", `${process.env.NEXT_PUBLIC_SSO_AUTH_SECRET}`);
  formData.append("authentication_token", authToken);
  const response = await fetch(`${process.env.NEXT_PUBLIC_SSO_AUTH_GET}`, {
    method: "POST",
    body: formData,
  });
  return response.json();
}

export async function confirmAuth(authToken: string) {
  let formData = new FormData();
  formData.append("token", `${process.env.NEXT_PUBLIC_SSO_AUTH_SECRET}`);
  formData.append("authentication_token", authToken);
  const response = await fetch(`${process.env.NEXT_PUBLIC_SSO_AUTH_CONFIRM}`, {
    method: "POST",
    body: formData,
  });
  return response.json();
}

export async function logout(user_identy: string) {
  let formData = new FormData();
  formData.append("token", `${process.env.NEXT_PUBLIC_SSO_AUTH_SECRET}`);
  formData.append("user_identy", user_identy);
  const response = await fetch(`${process.env.NEXT_PUBLIC_SSO_LOGOUT}`, {
    method: "POST",
    body: formData,
  });
  await signOut();
  return response.json();
}

export async function postSsoEvent(data: any) {
  const response = await fetch(`${process.env.SSO_EVENT_URL}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(data),
  });
  return response.json();
}

export async function fetchUser(username: string) {
  const response = await fetch(`${process.env.VERMAN_API}/users/${username}`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  });
  if (response.status !== 200) {
    return null;
  }
  const data = await response.json();
  return data;
}

env config

.env

AUTH_SECRET="HrYDel8Bs8qA2QbYlxX96Nq1bqm/7/g1zbTD413HHgY="
AUTH_TRUST_HOST=true
NEXT_PUBLIC_SSO_AUTH_SECRET="Ri7rndq7LFD2MEi9xysU3Lu728FoJMOXHBe45UVR8za4YyxolD9XPklQogloaL91kjLjrG9H9wcp01voDvOKNWvIJ7XihMa8K03Chm5pv7tmwxflZHpeUPpooi6XG353"
NEXT_PUBLIC_SSO_AUTH_OBTAIN=http://localhost:8807/sso/obtain/
NEXT_PUBLIC_SSO_AUTH_LOGIN=http://localhost:8807/login/
NEXT_PUBLIC_SSO_AUTH_GET=http://localhost:8807/sso/get/
NEXT_PUBLIC_SSO_AUTH_CONFIRM=http://localhost:8807/sso/make_used/
NEXT_PUBLIC_SSO_LOGOUT=http://localhost:8807/sso/deauthenticate/