Getting started with next-auth beta

Posted on July 7, 2024

At the time of writing this article follows the latest version of next-auth which is version 5.0.0-beta.19.

Whilst working on a project that required authentication, I evaluated a range of libraries. Auth.js (formerly next-auth) stood out as it allows user's to own their data in addition to having community packages for other frameworks such as hono.js.

Generally I was impressed with the library, despite being in a beta state it provides a significant amount of functionality with the ability to extend core features.

Unfortunately I faced some challenges when implementing a Credentials' authentication method (e.g. username and password) primarily because the documentation is outdated.

The following is my journey to setup credentials authentication using Auth.js beta.

Install dependencies

pnpm add next-auth@beta

Generate encryption key

The key is used in order to encrypt tokens and verification hashes

npx auth secret

The above command with add an AUTH_SECRET= entry to the .env.local file.

Create auth.ts file

In my project I don't use the src folder so placed the file auth.ts at the root of the application; however if you had a src folder then you'd likely want to nest it under this directory.

// auth.ts
import NextAuth from 'next-auth'
export const { handlers, signIn, signOut, auth } = NextAuth({
secret: process.env.AUTH_SECRET!,
providers: []
})

Note: if receive issues for auth and signin methods then it's likely because you need to disable compilerOptions.declaration and compilerOptions.declarationMap in the .tsconfig file.

If you setup your project with turborepo then it's likely you will need to set the above settings to false.

{
"compilerOptions": {
"declaration": false,
"declarationMap": false
}
}

Add authentication routes

We want to allow next-auth to handle sign-in requests; in order to do this we declare a dynamic route. The next-auth docs typically places this under the /api route however I prefer to place this at the root. For example http://localhost:3000/auth/sigin.

At the following location /app/auth/[...nextAuth] we want to create the following file routes.ts

// app/auth/[...nextAuth]/route.ts
import { handlers } from '@/auth'
export const { GET, POST } = handlers

Note: that my project has an alias to resolve @/ to the root of the project.

We will then want to update auth.ts to define a baseUrl of '/auth'

Local credentials' provider

Inside out auth.ts file we want to add an import to the Credentials provider

import Credential from 'next-auth/providers/credentials'

Invalid credential error object

We will need to define an error type to handle an invalid credential error. It's important to define a kind on the error. Otherwise, the built-in error state won't work.

class InvalidCredentialError extends AuthError {
public readonly kind = 'signIn'
constructor() {
super('Invalid credentials')
this.type = 'CredentialsSignin'
}
}

Credentials authentication configuration

Once we have the error object defined we will want to create a provider in order to authenticate a user.

We define a credentials object that defines a text field for each of the required inputs.

Following this we define a authorize method in order to handle the authentication request. It's important to note that if we are unable to authorize a user, we need to throw the error defined in the previous step.

The documentation says you can return null however this causes issues with the auth flow of next-auth, similarly if your error doesn't include a kind property that has a value of signin.

Credential({
credentials: {
email: { label: 'Username' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
if (credentials.email === 'someone@example.com') {
return { id: 'xxx', email: credentials.email }
}
throw new InvalidCredentialError()
},
name: 'credentials',
type: 'credentials',
}),

Final result

Once we've performed all these steps, you should expect to have the following files and their contents:

// auth.ts
import NextAuth from 'next-auth'
import Credential from 'next-auth/providers/credentials'
class InvalidCredentialError extends AuthError {
public readonly kind = 'signIn'
constructor() {
super('Invalid credentials')
this.type = 'CredentialsSignin'
}
}
export const { handlers, signIn, signOut, auth } = NextAuth({
secret: process.env.AUTH_SECRET!,
basePath: '/auth',
providers: [
Credential({
credentials: {
email: { label: 'Username' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
if (credentials.email === 'someone@example.com') {
return { id: 'xxx', email: credentials.email }
}
throw new InvalidCredentialError()
},
name: 'credentials',
type: 'credentials',
}),
]
})
// app/auth/[...nextAuth]/route.ts
import { handlers } from '@/auth'
export const { GET, POST } = handlers

The official setup guide can be found at Auth.js | InstallationGitHubVercel