Username and Password authentication with NextAuth.js

NextAuth.js is a popular authentication library for Next.js that is easy to setup, secure, and highly flexible. If you need to secure server rendered pages, authenticate users via middleware, or API routes, NextAuth.js has it all!

The package comes with everything you need to authenticate with OAuth 1.0 and 2.0 services, as well as username and password credentials — which we will explore in this guide.

This guide will focus on using Grafbase as your backend for storing users, creating users, and fetching users by username and validating if the supplied password is correct.

Run the following command at the root of your Next.js project:

npx grafbase init

This will create the folder grafbase and in it schema.graphql — this schema is where we will configure the User model.

Replace the contents of grafbase/schema.graphql with the following:

type User @model {
  id: ID!
  username: String! @unique
  passwordHash: String!
}

The Grafbase CLI can generate and run locally a GraphQL API with one command:

npx grafbase dev

You should see the API Playground is running on http://localhost:4000 if all went well.

Inside of your Next.js project you will want to add the following dependencies:

npm install next-auth graphql-request graphql bcrypt

You should also install the types for bcrypt:

npm install --save-dev @types/bcrypt

The first thing you'll need to do is wrap your application inside _app.tsx with the SessionProvider exported from next-auth/react.

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>
  )
}

We'll begin by creating the file pages/api/auth/[...nextauth].ts in our project and import some dependencies:

import { compare, hash } from 'bcrypt'
import { GraphQLClient, gql } from 'graphql-request'
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'

We'll now create a new instance of GraphQLClient and pass it the Grafbase API URL and API Key from your project dashboard.

In development we can forgo the API Key, and use http://localhost:4000/graphql as our API URL.

const grafbase = new GraphQLClient(process.env.GRAFBASE_API_URL as string, {
  headers: {
    'x-api-key': process.env.GRAFBASE_API_KEY as string
  }
})

So this works locally, create the file .env.local and add the following:

GRAFBASE_API_URL=http://localhost:4000/graphql
GRAFBASE_API_KEY=

We need to create a query to fetch users by username. We can do this using the GraphQL query Grafbase generates automatically from our schema:

const GetUserByUsername = gql`
  query GetUserByUsername($username: String!) {
    user(by: { username: $username }) {
      id
      passwordHash
    }
  }
`

If users don't exist, we need a way to create them. We can use a GraphQL mutation to do this:

const CreateUserByUsername = gql`
  mutation CreateUserByUsername($username: String!, $passwordHash: String!) {
    userCreate(input: { username: $username, passwordHash: $passwordHash }) {
      user {
        id
        username
      }
    }
  }
`

Next inside of pages/api/auth/[...nextauth].ts you will want to create the const authOptions and pass it to the default export:

export const authOptions = {
  providers: []
}

export default NextAuth(authOptions)

NextAuth.js supports multiple provider types but we will use the Credentials provider we imported previously and pass it to the providers array.

We will also pass it the fields username and password with their type and placeholder values. These will automatically create fields inside of the hosted NextAuth.js authentication page!

export const authOptions = {
  providers: [
    Credentials({
      name: 'Credentials',
      credentials: {
        username: {
          label: 'Username',
          type: 'text',
          placeholder: 'grafbase'
        },
        password: { label: 'Password', type: 'password' }
      }
    })
  ]
}

We must next implement the authorize method. This is where we will make a GraphQL request to our Grafbase backend to check if the User exists.

export const authOptions = {
  providers: [
    Credentials({
      name: 'Credentials',
      credentials: {
        username: {
          label: 'Username',
          type: 'text',
          placeholder: 'grafbase'
        },
        password: { label: 'Password', type: 'password' }
      },
      async authorize(credentials) {
        const { username, password } = credentials as {
          username: string
          password: string
        }

        const { user } = await grafbase.request(GetUserByUsername, {
          username
        })
      }
    })
  ]
}

If the User does not exist, we'll create one with the username and passwordHash. Instead of passing the password as plain text, we'll store it as a hash.

Once the user has been created we can simply return and NextAuth.js will take over to finish authenticating that new user.

export const authOptions = {
  providers: [
    Credentials({
      // ...
      async authorize(credentials) {
        const { username, password } = credentials as {
          username: string
          password: string
        }

        const { user } = await grafbase.request(GetUserByUsername, {
          username
        })

        if (!user) {
          const { userCreate } = await grafbase.request(CreateUserByUsername, {
            username,
            passwordHash: await hash(password, 12)
          })

          return {
            id: userCreate.id,
            username
          }
        }
      }
    })
  ]
}

All we need to do to finish implementing the authorize method is to provide the happy path for users who do exist.

Outside of the if statement for users who do not exist, we'll next check the password is valid for the user:

export const authOptions = {
  providers: [
    Credentials({
      // ...
      async authorize(credentials) {
        const { username, password } = credentials as {
          username: string
          password: string
        }

        const { user } = await grafbase.request(GetUserByUsername, {
          username
        })

        if (!user) {
          const { userCreate } = await grafbase.request(CreateUserByUsername, {
            username,
            passwordHash: await hash(password, 12)
          })

          return {
            id: userCreate.id,
            username
          }
        }

        const isValid = await compare(password, user.passwordHash)
      }
    })
  ]
}

Instead of returning an error that the password is invalid, you will want to throw a generic error to confuse those trying to brute force their way through username/password combinations!

If the pass isValid, you can safely return the user that was previously queried from Grafbase and pass it to NextAuth.js to finish authenticating the user:

export const authOptions = {
  providers: [
    Credentials({
      // ...
      async authorize(credentials) {
        const { username, password } = credentials as {
          username: string
          password: string
        }

        const { user } = await grafbase.request(GetUserByUsername, {
          username
        })

        if (!user) {
          const { userCreate } = await grafbase.request(CreateUserByUsername, {
            username,
            passwordHash: await hash(password, 12)
          })

          return {
            id: userCreate.id,
            username
          }
        }

        const isValid = await compare(password, user.passwordHash)

        if (!isValid) {
          throw new Error('Wrong credentials. Try again.')
        }

        return user
      }
    })
  ]
}

Now you can continue using NextAuth.js in the same way you are used to by invoking the methods to signIn, signOut, useSession, and more.

We'll toggle based on the whether we have an active which button to show:

import { signIn, signOut, useSession } from 'next-auth/react'

export default function Home() {
  const { data: session } = useSession()

  if (session) {
    return (
      <>
        Signed in
        <br />
        <button onClick={() => signOut()}>Sign out</button>
      </>
    )
  }

  return (
    <>
      Not signed in <br />
      <button onClick={() => signIn()}>Sign in</button>
    </>
  )
}

That's it! You now have successfully added username/password authentication to your Next.js application with NextAuth.js. All user data is stored in Grafbase.

Users who click the button to Sign in will be redirected to the NextAuth.js page to enter their username and password. If you want to customize this page, or work with the API directly inside of your own page component, read the docs to learn more.

Note: NextAuth.js prefers user data is managed by a OAuth 1.0 or 2.0 service but provides this credentials provider for those who have an existing user database, like Grafbase!

Build your Next.js backend with Grafbase

Get early access to the Grafbase beta.