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
Github
, etc. You can also customize hooks to communicate with the host platform after successful login or registration.
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'
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',
)
}
After successful login, Auth0 will automatically callback the following routes:
http://localhost:3000/auth/callback, https://thisiscz.vercel.app/auth/callback
After logout, the default is to return to the homepage. Multiple URLs can be separated by commas.
http://localhost:3000, https://thisiscz.vercel.app
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,
},
})
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|.*\\..*).*)',
}
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 },
)
}
}
// /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
}
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
}
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>
}
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)