Use NextAuth to authenticate API requests
Tags: javascript, nextjs, node, api
Published: 2022-05-24 14:19:11

NextAuth is a great way to authenticate users for your Next.js app. However, Next.js API Routes are not a good substitute for a full backend framework. In this article, we'll address all the setup required to use NextAuth's cookies to authenticate against a separate backend. While I am using NestJS as my backend framework, the general logic/flow is portable to almost any backend framework.
> **Important Note:** You can avoid a lot of this by proxying your API requests through Next.js. This tutorial is specifically for an API server on a separate, **sub**domain.  

## The Basic Idea 
When a user logs in successfully, NextAuth issues an `HttpOnly` cookie that contains either a JWT or a session token. While we could have an API route that issues a different token for accessing the API server, this adds complexity. What if, instead, we could use NextAuth's cookie as our token for the API? This is a good solution because it allows us to reduce complexity, leaving the management of the token to NextAuth. Our backend can then just validate it and move on. This is also assuming that **your backend is on a subdomain relative to the domain of your frontend; e.g. your frontend is at `example.com` and your backend is at `api.example.com`.**

## The Frontend
#### NextAuth Config  
No matter what your backend is, you'll have to set a [custom configuration for the session cookie in NextAuth.](https://next-auth.js.org/configuration/options#cookies) By default, NextAuth does not specify a domain for the cookie. This results in a cookie whose domain is that of the current page (i.e. `example.com`), not including subdomains. Other than that, all other defaults are fine.   
 
Here's how to set it:
```ts
// Inside your NextAuth config object
cookies: {
  sessionToken: {
    name: `__Secure-next-auth.session-token`, // Make sure to add conditional logic so that the name of the cookie does not include `__Secure-` on localhost
    options: { // All of these options must be specified, even if you're not changing them
      httpOnly: true,
      sameSite: 'lax',
      path: '/',
      secure: true,
      domain: `example.com` // Ideally, you should use an environment variable for this
    }
  },
}
```   
You might be looking at that domain wondering "*How is that different?* ". When you specify the domain, the cookie's domain value will get set with an extra period in front (i.e. `.example.com` for this) which tells the browser that it's fine to send to subdomains.

#### Making requests  
When you make requests from your frontend, you also need to add an option to the request to send the cookie(s).  
- `fetch` requests: specify `credentials: "include"` in the request config   
[MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
- `XHR` requests (e.g. Axios): `withCredentials = true`    
[MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials)
  
You'll also want to make sure that your chosen method of making requests is able to make CORS preflight requests for some HTTP methods.

## The Backend
#### Headers  
For this to work, we need to set up some CORS headers. Here they are:
- `Access-Control-Allow-Origin`: You must specify the domains; a wildcard (`*`) will not allow access to request credentials.   
[MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-origin)
- `Access-Control-Allow-Credentials`: This must be set to `true`.  
[MDN Reference](Link)
- `Access-Control-Allow-Headers`: Make sure that the `Cookies` header is included.  
[MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-methods)
- `Vary`: Must be set to `Origin`. Most frameworks/libraries set this automatically.  

#### Authentication/Validation
In your backend of choice, you'll need similar conditional logic to above. Assuming that you use the default cookie names, you'll access a cookie called `__Secure-next-auth.session-token` or `next-auth.session-token`. If this is a JWT, you'll validate it and extract the encoded information - make sure your server uses the same JWT signing secret that you provided to NextAuth. If it's a session token, you'll look it up in your database and make sure it exists + is not expired.  
  
#### NestJS - Passport  
Here's specifically how I implemented this in NestJS, using Passport. While I am still using Nest's Express platform, this should be mostly compatible with the Fastify platform. First, you'll need the `cors` package, configured as followed: 
```ts
app.enableCors({
    origin: [
      'http://localhost:3000',
      'http://127.0.0.1:3000',
      // add your other urls here
    ],
    allowedHeaders: ['Cookie', 'Content-Type'],
    methods: ['GET', 'POST', 'PUT', 'PATCH', 'OPTIONS'],
    credentials: true, // This is what sets the `Access-Control-Allow-Credentials` header.
  });
```  
Secondly, you'll need the `cookie-parser` package.
```ts
import cookieParser from 'cookie-parser';

// ... other code

app.use(cookieParser());
```  
Finally, I used the `passport-custom` package to set up a custom strategy. I implemented it as follows:  
``` ts
const cookieName = // This turnary statement is the conditional logic I mentioned previously
  process.env.NODE_ENV === 'production'
    ? '__Secure-next-auth.session-token'
    : 'next-auth.session-token';

@Injectable()
export class NextAuthSession extends PassportStrategy(
  Strategy,
  'nextauth-session',
) {
  constructor(private authService: AuthService) {
    super();
  }
// The Request type is imported from Express
  async validate(req: Request): Promise<User | null> {
    const sessionToken = req.cookies[cookieName];
    if (!sessionToken) {
      throw new UnauthorizedException({ message: 'No session token' });
    }

// authService.verifySession does a database lookup with Prisma
    const session = await this.authService.verifySession(sessionToken);
    if (!session) {
      throw new UnauthorizedException({
        statusCode: 401,
        message: 'Invalid Session',
      });
    }
// Whatever you return gets added to the request object as `req.user`
    return session.user; 
  }
}
```

## Conclusion  
This was something that took me a bit to figure out properly, especially figuring out how to use Passport. I hope others come across this guide and find it useful.