Full Stack Personal Blog 09: Authentication
cz2025.06.03 15:03

Auth0 is an authentication and authorization platform. A free account supports up to 25,000 user logins. It supports one-click login with multiple social platforms, such as Google, Github, etc. You can also customize hooks to communicate with the host platform after successful login or registration.

1. Create an Application

After registration, create a web application and select the type Regular Web Application. You will get the following credentials, which should be pasted into your .env file:

// .env
AUTH0_DOMAIN='dev-xxxx.auth0.com'
AUTH0_CLIENT_ID='xxxx'
AUTH0_CLIENT_SECRET='xxxx_xxxx'

2. Configure the Application

  • Application Login URI:

    Set this to the login page route of your project, e.g. https://thisiscz.vercel.app/en/login. Add the returnTo parameter to redirect to a specified address after successful login.

// login.tsx
import { Locale } from 'next-intl'
import { redirect } from 'next/navigation'

type Props = {
  params: Promise<{ locale: Locale }>
  searchParams: Promise<{ returnTo?: string }>
}

export default async function LoginPage({ params, searchParams }: Props) {
  const { returnTo } = await searchParams

  redirect(
    returnTo
      ? `/auth/login?returnTo=${encodeURIComponent(returnTo)}`
      : '/auth/login',
  )
}
  • Allowed Callback URLs:

After successful login, Auth0 will automatically callback the following routes:

http://localhost:3000/auth/callback, https://thisiscz.vercel.app/auth/callback
  • Allowed Logout URLs:

After logout, the default is to return to the homepage. Multiple URLs can be separated by commas.

http://localhost:3000, https://thisiscz.vercel.app

3. Configure Auth0 SDK Client

pnpm add @auth0/nextjs-auth0

// lib/auth0.js
import { Auth0Client } from '@auth0/nextjs-auth0/server'
// Initialize the Auth0 client
export const auth0 = new Auth0Client({
  // Options are loaded from environment variables by default
  // Ensure necessary environment variables are properly set
  domain: process.env.AUTH0_DOMAIN,
  clientId: process.env.AUTH0_CLIENT_ID,
  clientSecret: process.env.AUTH0_CLIENT_SECRET,
  appBaseUrl: process.env.APP_BASE_URL,
  secret: process.env.AUTH0_SECRET,

  authorizationParameters: {
    // In v4, the AUTH0_SCOPE and AUTH0_AUDIENCE environment variables for API authorized applications are no longer automatically picked up by the SDK.
    // Instead, we need to provide the values explicitly.
    scope: process.env.AUTH0_SCOPE,
    audience: process.env.AUTH0_AUDIENCE,
  },
})

4. Use Auth0 Middleware

Routes starting with /auth are handled by Auth0, others are handled by the internationalization middleware.

import createMiddleware from 'next-intl/middleware'
import { routing } from '@/i18n/routing'

import type { NextRequest } from 'next/server'
import { auth0 } from '@/lib/auth0'

export default async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/auth')) {
    return await auth0.middleware(request)
  } else {
    const intlMiddleware = createMiddleware(routing)
    return await intlMiddleware(request)
  }
}

export const config = {
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
}

5. Sync Auth0 Login to Database

When Auth0 enables Google one-click login, you can get basic information such as email and nickname. At this point, you need to seed the user data into the User table in your database.

Auth0 supports writing custom scripts after successful login to send user data back to your project API, creating a User record. This only syncs to the database on the first login. Example code:

const fetch = require('node-fetch')

exports.onExecutePostLogin = async (event, api) => {
  const SECRET = event.secrets.AUTH0_HOOK_SECRET

  if (event.user.app_metadata.localUserCreated) {
    return
  }

  const email = event.user.email
  const nickname = event.user.nickname || event.user.name

  const request = await fetch('https://thisiscz.vercel.app/api/auth/hook', {
    method: 'post',
    body: JSON.stringify({ email, nickname, secret: SECRET }),
    headers: { 'Content-Type': 'application/json' },
  })
  const response = await request.json()

  api.user.setAppMetadata('localUserCreated', true)
}

Expose a service endpoint /api/auth/hook in your project, with secret key validation:

import prisma from '@/lib/prisma'

export async function POST(request: Request) {
  const { email, nickname, secret } = await request.json()

  if (secret !== process.env.AUTH0_HOOK_SECRET) {
    return Response.json(
      { message: `You must provide the secret 🤫` },
      { status: 403 },
    )
  }

  if (email) {
    await prisma.user.create({
      data: { email, nickname },
    })
    return Response.json(
      { message: `User with email: ${email} has been created successfully!` },
      { status: 200 },
    )
  }
}

6. Get User Information

  • Get the user's email from Auth0, then use the email to find the corresponding data in the local User table. You can encapsulate the following function, which supports use in server components:
// /lib/getServerUser.ts

import { auth0 } from './auth0'
import prisma from './prisma'

export async function getServerUser() {
  const session = (await auth0.getSession()) ?? {}
  const email = (session as any)?.user?.email
  let user = null
  if (email) {
    user = await prisma.user.findUnique({
      where: {
        email,
      },
    })
  }
  return user
}
  • To get user data in client components, you can use Zustand to pass the user data obtained on the server to the client component, then set the global state with useUserStore.setState({ user }).

Zustand cannot synchronize initial data from the server to the client, otherwise it would be simpler. For details, see: Zustand outside of components

Usage example:

export default async function LocaleLayout({ children, params }: Props) {
  //...
  const user = await getServerUser()
  return (
    <html lang={locale} suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          <NextIntlClientProvider>
            <ApolloWrapper>
              <AppInit user={user} />
            </ApolloWrapper>
            <Toaster />
          </NextIntlClientProvider>
        </ThemeProvider>
      </body>
    </html>
  )
}
// AppInit.tsx
'use client'
import { useUserStore } from '@/store/userStore'
import { useEffect } from 'react'

export default function AppInit({ user }: { user: any }) {
  useEffect(() => {
    if (user) {
      useUserStore.setState({ user })
    }
  }, [user])

  return null
}

7. Next.js Route Authorization

Only ADMIN users are allowed to access the /admin route.

// admin/layout.tsx

export default async function AdminLayout({ children, params }: Props) {
  const { locale } = await params
  // Enable static rendering
  setRequestLocale(locale)
  const user = await getServerUser()

  if (!user || user.role !== 'ADMIN') {
    return redirect('/')
  }

  return <div className="page-wrapper py-6">{children}</div>
}

8. GraphQL API Authorization

When the user has ADMIN permissions, they are allowed to publish blog posts.

// graphql/context.ts
import { getServerUser } from '@/lib/getServerUser'
import prisma from '@/lib/prisma'
import type { NextApiRequest, NextApiResponse } from 'next'

export async function createContext({
  req,
  res,
}: {
  req: NextApiRequest
  res: NextApiResponse
}) {
  const user = await getServerUser()
  return {
    prisma,
    user,
  }
}

Add context to the api/graphql endpoint:

// api/graphql
import { createYoga } from 'graphql-yoga'
import { schema } from '../../../../graphql/schema'
import { createContext } from '../../../../graphql/context'
import { NextRequest } from 'next/server'

const { handleRequest } = createYoga({
  schema,
  context: createContext,
})

export async function GET(request: NextRequest) {
  return handleRequest(request, {} as any)
}

export async function POST(request: NextRequest) {
  return handleRequest(request, {} as any)
}

Add permission check to the post publishing mutation:

// graphql/types/Post.ts

// Define mutation type for adding a post
builder.mutationType({
  fields: (t: any) => ({
    addPost: t.field({
      type: 'Post' as any,
      args: {
        title: t.arg.string(),
        summary: t.arg.string(),
        content: t.arg.string(),
      },
      resolve: async (_root: any, args: any, ctx: any) => {
        if (ctx.user?.role !== 'ADMIN') {
          throw new Error('you are not admin')
        }
        const { title, summary, content } = args

        const post = await ctx.prisma.post.create({
          data: { title, summary, content, createdById: ctx.user.id },
        })

        return post
      },
    }),
  }),
} as any)

Comments