Next-auth App Router Credentials - An Annotated Guide
Tags: beginners, javascript, nextjs, webdev
Published: 2024-06-04 07:28:56

This is an annotated guide on implementing Auth.js (NextAuth 5) on NextJS (app router). We will follow official steps outlined in respective docs.

We want to implement 
- a email+password based login system,
- set custom login pages
- protect routes,  and
- add roles (ie extend user objects for additional vaues)

Tldr: Git repo - https://github.com/kaizenworks/nextjs-authjs-example

## NextJS Install

Source - https://nextjs.org/docs/getting-started/installation

> Create project
```bash
npx create-next-app@latest
```
> Project name and options
```bash
What is your project named? my-app
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias (@/*)? No
```

On fresh install (ts version), every page inside `/src/app` will throw `module not found` errors on imports.
This is probably because in `tsconfig.json`, `moduleResoultion` is set to `bundler`. More here - https://github.com/vercel/next.js/discussions/41189

Setting   `moduleResoultion: "node"` solves the issue.

## Auth.js / NextAuth install

Source - https://authjs.dev/getting-started/installation


> Package install

```bash
npm install next-auth@beta
```

> Setup enironment

```bash
npx auth secret
```

Supposed to create a `.env.local` file with `AUTH_SECRET=` value, but didn't. So had to manullay save the outputted value to `.env.local`.

> Create auth.js at the root of the project (ie `/src`)

```ts
//  path: ./src/auth.ts

import NextAuth from "next-auth"
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [],
})
```

> Set auth related route handlers
```ts
// path: ./src/app/api/auth/[...nextauth]/route.ts

import { handlers } from "@/auth" // Referring to the auth.ts we just created
export const { GET, POST } = handlers 
```

> Add middleware

```ts
//path: ./src/middleware.ts

export { auth as middleware } from "@/auth"
```

I expected it would protect all the routes by default. But you would have to define guard rules separately. This version will keep the session values updated.

## Credential (Email+Password) Authentication Setup

Source - https://authjs.dev/getting-started/authentication/credentials

> Setup Credential Provider in `auth.ts` file
```ts
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
	Credentials({
		name: "Email",
		credentials: {
			email: { label: "Email", type: "email " },
			password: { label: "Password", type: "password" },
		},
		authorize: async (credentials) => {

			let {email,password} = credentials;

			if(email==process.env.ADMIN_EMAIL
				&& password==process.env.ADMIN_PASS) {
					return {
						id: '1',
						name: 'Admin'
					}
				}
			
			return null;
		},
	  }),
  ],
})
```

Define database lookup and password matching inside the authorize callback. I have used static env variables to keep things simple.

NextAuth uses a default user type, but you can extend it (more on that later)

## Signin and Signout

Source - https://authjs.dev/getting-started/session-management/login

> Create signin button

```tsx

'use client'

import { signIn } from "next-auth/react"

export const SignInButton = ({children}:any) => {

	return (
		<button className="[redacted]." type="button" onClick={()=>signIn()}>
			{children}
		</button>
	)
}
```
Clicking this button takes user to default sign in page.

Note there are two ways of importin signin/out functions 
- Your `auth.js` file or `@/auth`  for server-side use
- `next-auth/react` for client-side use

Define component render boundaries with `use client` and `use server` 

> Create signout button

Signout button is the same, but using `signOut` method.

> Add buttons to Home

```tsx

//.... imports

export default async function Home() {

    const session = await auth();

    return (
        
        ...
        
        <div>
        { session
            ? (
                <>
                    <h1>{session.user?.name}</h1>
                    <SignOutButton>Sign Out</SignOutButton>
                </>
            )
             : (
                <>
                    <h1>Guest</h1>
                    <SignInButton>Sign In</SignInButton>
                </>
              )}
        </div>

        ...
        
    )
}
```

## Protecting routes

Source - https://authjs.dev/getting-started/session-management/protecting

To demonstrate this, we will add another route `/dashboard` and make it protected (ie only visible to logged in users)

> Add middleware with routh matcher config

```ts
export { auth as middleware } from "@/auth"

export const config = {
	matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
```
This did not work out for me.

But defining it inside a custom auth function works:

```ts
import { auth } from "@/auth"
 
export default auth((req) => {
  if (!req.auth) {
    const url = new URL( "/login", req.url );
    // here "/login" is a custom login page, we havent defined it yet
    return Response.redirect(url)
  }
})
```

Another way is to define in the page.tsx of the specific route, in our case `/dashboard`

```tsx
import { auth, signIn } from "@/auth";

export default async function Dashboard() {

  const session = await auth();

  if (!session) return signIn();

  return (
    ...
  );
}

```

## Custom Login Page

Source - https://authjs.dev/getting-started/session-management/custom-pages

> Define custom signIn path in `auth.ts`
```ts
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  pages: {
    signIn: "/login",
  },
  ...
})
```

> Create a Login page and implement signin()

We will do this using server actions - 

1. create signin request action
2. create signin form component (client side). Bind reqeuestSignIn using `useFormState`
3.  create login page on `login/page.tsx` that holds the form component

`@/actions/request-sign-in.ts`
```ts
"use server"
import { signIn } from "@/auth";
import { redirect } from "next/navigation";

type FormState = {
	error: string;
};
  
export async function requestSignIn(formState:FormState,formData: FormData) {
	try{
		const email = formData.get('email') as string;
		const password = formData.get('password') as string;
		await signIn("credentials", {email,password, redirect:false});
	} catch(error){
		return {error: "Invalid login"};
	}

	redirect('http://localhost:3000/');
}
````

When called from `useFormState` the first param is the state and the second on is FormData. Got it mixed up.

Its weird that you have to pass `reidirect` or `reidrectTo` inside the data param in `signIn`.

But it throws a `NEXT_REDIRECT` error. Keeping `redirect` out of try/catch works somehow.

*@/components/sign-in-form.client.tsx*
 

```jsx
"use client"

import { requestSignIn } from "@/actions/request-sign-in";
import { useEffect } from "react";
import { useFormState } from "react-dom";

const initState = { error: '' };

export const SignInForm = () => {
    const [fstate, action] = useFormState(requestSignIn, initState);

    useEffect(() => {
        let { error } = fstate;
        if (error) alert(error);
    }, [fstate])


    return (

        <form
			action={action}
		>
			<label>
				Email
				<input name="email" type="email" />
			</label>
			<br />
			<label>
				Password
				<input name="password" type="password" />
			</label>
			<br />
			<button>Sign In</button>
		</form>
    )
}
```

*@/app/login/page.tsx*

```tsx
import { SignInForm } from "@/components/sign-in-form.client";

export default function SignInPage() {

	return (
		<div className="flex flex-col gap-2">
			<SignInForm />
		</div>
	)
}

```

## Session Data

Source : Extending Session - https://authjs.dev/guides/extending-the-session

Source: Role Based Access Example - https://authjs.dev/guides/role-based-access-control

*@/auth.ts*

```ts
export const { handlers, signIn, signOut, auth } = NextAuth({
	providers: [
		Credentials({
			...
			authorize: async (credentials) => {

				let { email, password } = credentials;

				if (email == process.env.ADMIN_EMAIL
					&& password == process.env.ADMIN_PASS) {
					return {
						id: '1',
						name: 'Admin',
						role: 'staff'
					}
				}

				return null;
			},
		}),
	],
	callbacks: {
		jwt({ token, user }) {
			if (user) {
				token.role = user.role;
			}
			return token;
		},
		session({ session, token }) {
			session.user.role = token.role
			return session
		},
	}
})
```

We defined two callbacks here

`jwt` 

> During sign-in, the jwt callback exposes the user’s profile information coming from the provider. You can leverage this to add the user’s id to the JWT token.

Here will will add the user role to token data

`session`

> This callback is called whenever a session is checked. (i.e. when invoking the /api/session endpoint, using useSession or getSession). The return value will be exposed to the client, so be careful what you return here! If you want to make anything available to the client which you've added to the token through the JWT callback, you have to explicitly return it here as well.

 In our case we are exposing role in session.

### Typescript error

When extending user with role field, there will be a type error in the callbacks.

> Property `role` does not exist on type `User | AdapterUser`

Solution is to extend the User and Session tyoes from Next Auth

`@/types/next-auth.d.ts`

```ts
import { DefaultUser } from 'next-auth';
declare module 'next-auth' {
    interface Session {
        user?: DefaultUser & { id: string; role: string };
    }
    interface User extends DefaultUser {
        role: string;
    }
}
```

Source - https://stackoverflow.com/questions/74425533/property-role-does-not-exist-on-type-user-adapteruser-in-nextauth

## Additional resources - 

1. (Youtube) Next.js App Router Authenication - https://www.youtube.com/watch?v=DJvM2lSPn6w
2. Practical overview of Nextjs forms and server actions - https://www.robinwieruch.de/next-forms/