NextAuth.js is a popular authentication library for Next.js that comes with built-in support for dozens of providers (including GitHub, Twitter, Google, and more).
In this guide we will look at adding NextAuth.js and Grafbase to an existing Next.js application. We will use the JWT generated by NextAuth.js to pass to Grafbase to authorize requests.
If you don't already have an application with Next.js, you can follow the getting started guide.
Inside of your project root, run the following command to initialize a new GraphQL backend with the Grafbase CLI:
npx grafbase init
Then inside of the file that was created at grafbase/schema.graphql
up it to contain the following schema:
type Message @model {
author: String!
body: String!
}
Grafbase provides an agnostic authentication layer that lets you bring your own auth provider. Since NextAuth.js generated JWTs for users when they sign-in, we'll use the JWT provider.
Create the file grafbase/.env
and add the following:
NEXTAUTH_SECRET=YourSuperSecret
ISSUER_URL=https://grafbase.com
You will want to generate a random secret for NEXTAUTH_SECRET
. You can run the following command to generate one:
openssl rand -base64 32
The ISSUER_URL
can be any valid URL you like.
Now inside grafbase/schema.graphql
we can configure the JWT provider using the @auth
directive. We will also set the rule { allow: private }
that requires users to be signed-in to make requests.
schema
@auth(
providers: [
{
type: jwt
issuer: "{{ env.ISSUER_URL }}"
secret: "{{ env.NEXTAUTH_SECRET }}"
}
]
rules: [{ allow: private }]
) {
query: Query
}
You'll notice we use the special {{ env. }}
syntax so we can use those environment variables created above.
Make sure to add these environment variables to your Grafbase project when you deploy to production.
Now all that's left to do is run your backend locally:
npx grafbase dev
At this point you can visit the playground at localhost:4000
to execute a GraphQL mutation to create a new Message
. It will look something like this:
mutation {
messageCreate(
input: { author: "Grafbase Admin", body: "Grafbase is awesome!" }
) {
message {
id
}
}
}
Now let's move to installing NextAuth.js and the jsonwebtoken
package:
npm install next-auth jsonwebtoken
If you're using TypeScript you will want to install the @types/jsonwebtoken
package as a dev dependency:
npm install @types/jsonwebtoken
Now create the file pages/api/auth/[...nextauth].ts
inside of your project. If you're using the new Next.js 13 app
directory, you will still want to create this file (for now):
import NextAuth, { NextAuthOptions } from 'next-auth'
export const authOptions: NextAuthOptions = {}
export default NextAuth(authOptions)
Next let's update the root pages/_app.tsx
file to include the SessionProvider
by NextAuth.js:
import { SessionProvider } from 'next-auth/react'
import type { AppProps } from 'next/app'
export default function App({
Component,
pageProps: { session, ...pageProps },
}: AppProps) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
Now open or create the file .env
in the root of your project and add the following:
NEXT_PUBLIC_GRAFBASE_API_URL=http://localhost:4000/graphql
NEXTAUTH_SECRET=
ISSUER_URL=https://grafbase.com
The NEXT_PUBLIC_GRAFBASE_API_URL
is prefixed with NEXT_PUBLIC_
so it can be used client-side.
The NEXTAUTH_SECRET
and ISSUER_URL
should match that set inside grafbase/.env
.
Let's move onto configuring the Login with GitHub functionality. You should create a new app inside of your GitHub developer settings.
The callback URL should be http://localhost:3000/api/auth/callback/github
.
Then update the .env
file to contain the Client ID and Client Secret provided by GitHub:
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
Next we can use these environment variables by importing next-auth/providers/github
and adding it to the providers
array in our authOptions
object:
import NextAuth, { NextAuthOptions } from 'next-auth'
import GitHubProvider from 'next-auth/providers/github'
export const authOptions: NextAuthOptions = {
providers: [
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
],
}
export default NextAuth(authOptions)
Make sure to add a button in your application to login and you will be taken to the NextAuth.js login screen that will show the button to Login with GitHub:
// pages/index.tsx
import { signIn } from 'next-auth/react'
export default function Home() {
return (
<>
<p>Not signed in<p>
<button onClick={() => signIn()}>Sign in</button>
</>
)
}
Right now when users sign in they will be generated a generic JWT that is encoded by NextAuth.js but we would like to customize the token so it works with the Grafbase JWT provider.
To do this we will provide our own encode
and decode
methods for the jwt
property of authOptions
.
We will use the library jsonwebtoken
that we installed previously to sign
and verify
tokens. When we sign
a new token we will pass the iss
and exp
values.
import jsonwebtoken from 'jsonwebtoken'
import NextAuth, { NextAuthOptions } from 'next-auth'
import { JWT } from 'next-auth/jwt'
import GitHubProvider from 'next-auth/providers/github'
export const authOptions: NextAuthOptions = {
providers: [
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
],
jwt: {
encode: ({ secret, token }) => {
const encodedToken = jsonwebtoken.sign(
{
...token,
iss: process.env.ISSUER_URL,
exp: Math.floor(Date.now() / 1000) + 60 * 60,
},
secret,
)
return encodedToken
},
decode: async ({ secret, token }) => {
const decodedToken = jsonwebtoken.verify(token!, secret)
return decodedToken as JWT
},
},
}
export default NextAuth(authOptions)
Now NextAuth.js knows how to encode
and decode
JWTs in the same format Grafbase is expecting we can now use that token to send with each GraphQL request.
One of the benefits of using NextAuth.js is how it handles storing JWTs. The raw
JWT isn't something that's saved in your browser that can be used over and over as NextAuth.js securely hides and handles those for us.
Instead we will create a Next.js API route that returns the token
we can then pass on. We must hit this endpoint before every request to get a new valid JWT using the getToken
method from NextAuth.js.
Create the file pages/api/auth/token.ts
and add the following:
import { NextApiRequest, NextApiResponse } from 'next'
import { getToken } from 'next-auth/jwt'
const secret = process.env.NEXTAUTH_SECRET
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const token = await getToken({ req, secret, raw: true })
res.json({ token })
}
Now all that's left to do is hook up your GraphQL client to invoke the new /api/auth/token
endpoint before every operation.
We'll use fetch
in this example and create the helper fetchToken
for the purposes of this guide:
const fetchToken = async () =>
await fetch('/api/auth/token').then(res => res.json())
Then wherever you want to query you can invoke fetchToken
and pass the value of that to your fetch
. We will use the query automatically generated by Grafbase to fetch all Message
's:
const fetchMessages = async () => {
const { token } = await fetchToken()
return fetch(process.env.NEXT_PUBLIC_GRAFBASE_API_URL!, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
query: /* GraphQL */ `
{
messageCollection(first: 100) {
edges {
node {
id
}
}
}
}
`,
}),
})
.then(res => res.json())
.catch(err => console.log(err))
}
That's it! All requests will now be authenticated using the JWT generated by NextAuth.js after you successfully login.
Unauthenticated requests will not be permitted because of the auth rule we configured with Grafbase.