You asked, we answered. In this exciting tutorial, we’re going to implement Descope authentication to our Next.js 13 app using NextAuth v4. To demonstrate, we’re going to build a hackathon template!
If you’re a hacker, developer, or organizer looking for a website for your own hackathon, this tutorial is for you! The hackathon template features:
Descope NextAuth authentication. 🔐
Protected pages & API routes with NextAuth.
The latest Next.js app router, server, and client components.
Fully customizable Home screen which features an About, Speakers, Sponsors, and FAQ section.
A dedicated Team page to showcase all contributors.
A Dashboard page for hackers to complete onboarding forms, see their acceptance status, and read hackathon announcements.
Airtable backend for hackers to sign up and view hackathon details.
Fully responsive UI (mobile, tablet, computer).
Prerequisites
All the code for the tutorial will be in the GitHub Repository at next-hackathon-template. Instructions on the installation are on the README.md file.
If you’re a beginner or intermediate with Next.js 12 or 13, we recommend you check out our blog here to learn more about Next and the differences between the two versions. The blog covers rendering, API and page routes, server components, and more.
Overview
The tutorial will cover these core topics:
Descope + NextAuth
Sign-in page
Dashboard
Setting up our API
Demo
Let's start with the most fundamental concepts of Next.js.
Descope + NextAuth
NextAuth.js is a great choice for implementing authentication for several reasons:
It's a popular open-source authentication solution for Next.js applications.
It comes built-in with multiple authentication providers.
It supports various OAuth protocols such as OAuth 1.0, 2.0, and OIDC.
To use Descope, we’re going to implement a custom provider in NextAuth.
To start, create a route.ts file for your API endpoint in the following directory: app/api/auth/[...nextauth]
.
In route.ts
, we will include the following code:
import NextAuth from "next-auth/next";
import { authOptions } from "../../../_utils/options";
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
Code block: app/api/auth/[...nextauth]/route.ts
The [...nextauth]
directory is a catch-all route, denoted by the three dots. A catch-all route means that all routes that begin with /api/auth/
will be handled by the route.ts
. In the route.ts
file, we initialize our NextAuth
handler using our authentication options.
In the last line, the handler is exported as a GET and POST because a Route Handler file in Next 13 expects a web request and response.
Next, we need to actually build our authentication options.
import { NextAuthOptions } from "next-auth"
export const authOptions: NextAuthOptions = {
providers: [
{
id: "descope",
name: "Descope",
type: "oauth",
wellKnown: `https://api.descope.com/${process.env.DESCOPE_PROJECT_ID}/.well-known/openid-configuration`,
authorization: { params: { scope: "openid email profile" } },
idToken: true,
clientId: process.env.DESCOPE_PROJECT_ID,
clientSecret: process.env.DESCOPE_ACCESS_KEY,
checks: ["pkce", "state"],
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
}
},
},
]
}
Code block: app/_utils/options.ts
Here’s what we’re doing:
authOptions
: stores our NextAuth configurations such as our custom provider or other authentication providers.Within the provider's array, we define our Descope custom provider object.
Within the custom provider, let's see what the attributes are doing:
id & name: This identifiers for our custom provider.
type: OAuth is the authentication protocol.
wellKnown: Since Descope is an OpenID Connect Provider, we use the
wellKnown
attribute to point to a discovery URL which is the entry point for the authentication process. The discovery URL will contain the OIDC Configuration in the form of a JSON.authorization: This is where we provide the scope of permissions we are requesting from the authorization server.
idToken: This is set to true since OIDC uses JWTs.
clientId: This is used to identify the client with authorization server.
clientSecret: This is used to authenticate to the authorization server and get the access token.
checks: PKCE and state are two different OIDC flows. Both should be included.
profile: When the authentication is successful and the callback occurs, the user object is returned.
Next, let’s create a dashboard for our hackers.
NOTE: To learn more about how OIDC works, refer to our blog on What is OpenID Connect (OIDC)?.
Sign-in page
In order to sign in with Descope, we have a button in our Navbar component with NextAuth’s signIn method.
'use client'
...
import { useSession, signIn } from "next-auth/react"
export default function Navbar({ Logo }: { Logo: string }) {
...
return (
...
<button onClick={() => signIn("descope", { callbackUrl: "/dashboard" })}
className="text-[#e9e9e9] bg-[#262d3b] py-2 px-7 border-[#45546e] border-4">Apply</button>
...
)
}
Code block: app/_components/Navbar.tsx
The signIn
method has two parameters:
“descope”: This is the authentication provider ID.
callbackUrl: The URL we want to redirect back to once authenticated.
Our callbackUrl
is set to the dashboard page. Let’s check that out!
Dashboard
In the Dashboard component, we have two functions:
getData: This fetches data from the Airtable API endpoint (which we will create).
Dashboard: This is the main component of the page.
For the hackathon template, we’re going to use Airtable as our database of choice to store the list of user responses and their application statuses. Airtable is simple to use and comes with additional features such as form creation.
...
const getData = async () => {
const session = await getServerSession(authOptions)
const email = encodeURIComponent(session?.user?.email || "")
const res = await fetch(`${process.env.NEXTAUTH_URL}/api/airtable?email=${email}&secret=${process.env.SECRET_TOKEN}`)
const data = await res.json()
return data.body
}
export default async function Dashboard() {
const session = await getServerSession(authOptions)
if (!session) {
redirect("/api/auth/signin?callbackUrl=/dashboard")
}
const airtableRecord = await getData()
return (
<div className='page space'>
<div className="w-[90%]">
<Header />
{airtableRecord ?
<>
<Status accepted={airtableRecord['Accepted']} />
{airtableRecord['Accepted'] &&
<Info data={AnnouncementsList} />
}
<Application application={airtableRecord} />
</>
:
<Form />
}
</div>
</div>
)
}
Code block: app/dashboard/page.tsx
Within the fetch request of the getData
function are two parameters:
email: We get the user’s email from the
getServerSession
above. The email is passed in the query to identify the user data we are fetching.secret: The secret token acts as the API key that we get from our environment variables as a way for the API to validate the request. Here’s an example from the Next.js docs.
The Dashboard component is made up of three key parts:
To protect the Dashboard page, we use
getServerSession
to get the session and check if it exists. If not, we redirect to the sign-in page with the callback URL set to the Dashboard page.The
getData
function is called and the response is an object that can contain a field of “Accepted” to signify the hacker’s acceptance into the hackathon.In the return statement, we first check if user data exists in our Airtable. If it doesn’t, we display the Form component. If a user record exists, we display the Status component and Application Component. If the user is accepted, we display the Info component.
NOTE: Since the Dashboard component is a server component, useSession or getSession cannot be used since it uses hooks. Instead, we will use getServerSession to access the session and user email.
Setting up and protecting our API
In this final step, let’s set up our API to validate requests and get data from Airtable.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import Airtable from 'airtable'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const email = searchParams.get('email')
if (searchParams.get('secret') !== process.env.SECRET_TOKEN) {
return NextResponse.json("Unauthorized", { status: 401 })
}
...
const res = [{
"fields": {
"Name": 'Descope',
"University": 'University of Waterloo',
"What year are you?": 'First year',
"Email": 'example@descope.com',
'Why AuthHacks?': "Authentication is a fundamental part of any startup, SaaS, or business. The workshops and connections I'll make will profoundly broaden my knowledge of good security practices and industry leaders.",
'Accepted': true
}
}]
return NextResponse.json(
{
body: res[0].fields,
},
{
status: 200,
},
);
}
Code block: app/api/airtable/route.ts
Here’s a list of steps we take to protect and get data:
Next.js 13 comes with HTTP methods as the route identifier. The GET method will be triggered when we call a GET request to
/api/airtable
.We can use JavaScript’s built-in URL class to parse the incoming request and get the query parameters: email and secret.
We get the secret from the
searchParams
and check it against our secret token that we have stored as an environment variable. If it does not match, we return an unauthorized response.The
res
variable contains our hackathon dummy data which we send to the client in the response body.
And that’s it! Let’s see the hackathon template in action.
Demo day
Here’s are some screenshots from the hackathon template. You can also check out the live preview here: https://nextjs-hackathon-template.descope.com/
As a reminder, all the code can be found in the repository.
AuthHacks Hackathon
The theme and name for the hackathon template “AuthHacks” was coined after our mission here at Descope: passwordless authentication in just a few lines of code.
What will your hackathon be?
To learn more about Descope and showcase your creations, join hundreds of other Descopers on our AuthTown community.
Descope offers full support of Next.js with many sample apps and guides on how to get started. If you’re curious to start your Descope journey, sign up for a Free Forever account and build something auth-some!