ion
ion copied to clipboard
Example: NextAuth.js
I wanted to write this up somewhere as adding NextAuth has been a bit of a pigs ear (at least at time of writing). I've been dealing with some other deficiencies in SST at the same time which just muddied the waters, and its not been helped by NextAuth rebranding to Auth.js and guiding people into the beta which still has some issues.
I ultimately got this working with NextAuth.js v4.24.7, and for this example I'm only using the Github Provider. I am also using NextJs 14.2.3 and the App Router. You should be able to use the following as a skeleton and amend it depending on your Auth Provider needs.
Obstacles to overcome:
- In a plain Nextjs app, you usually use
.env
files to configure the auth secrets, but SST somehow breaks (#507) the loading of .env even though the Nextjs build step says its loading .env files. SST itself loads.env
during processing of thesst.config.ts
file, but only for.env
, and not.env.<stage>
variants. - If you know what your application origin is going to be (
https://<my domain>
) then life is somewhat easier, but I wanted to start developing and testing auth before I got to the point of deploying it (after all, who wants to deploy and unsecure app!). If you want to work with the auto generated domain from CloudFront (e.g.{random}.cloudfront.net
then you'll have to do more work to wire things up. That's the point of this guide. - SST doesn't export the application origin (
https://<my domain>
) to Next (#733), and NextAuth needs this. All well and good if you know what its going to be, as you can hardcode it or pass it as an environment variable.- v4 docs consider this 'advanced' usage, and the implementation below is in effect a form of 'lazy' loading, contrary to the NextAuth docs which assume you can prepare auth from the outset.
- v5-beta is adding lazy loading of the AuthOptions but there are still some bugs in this that i couldn't figure out how to workaround for now
- Github app's need hardcoded callback URL's which means that the dynamic URL generated by sst/aws can change, requiring you to edit the app configuration should that occur. I decided to bite the bullet and setup a sub-domain for my app, based on my existing company domain, but this requires AWS certificates and custom DNS entries. It should be easier if you can use a root domain and AWS Route53 to managed the DNS setup.
- You will likely need two SST stages, one for development and one for deployment. You may then needs a Github app for each, as the hardcoded URL's means you can't share.
- My Nextjs app is in a subdirectory of a larger CDK project, and #530 covers how type generation for the
Resource
import is broken. Instead of using the import, I useJSON.parse(process.env.SST_RESOURCE_<name> ?? '{}')?.value
as a workaround. - NEXTAUTH_SECRET environment variable seems to be mandatory despite
secret
being a member of theAuthOptions
object. These routines patch the environment prior to NextAuth to make sure everything is setup.
- Auth.js v5 is adding 'lazy' loading of the auth adapter middleware/route handlers which helps to simplify this, but there are still some problems that get in the way of using it
NextAuth Installation
npm install next-auth@4
sst.config.ts
You need to define placeholders for secrets which will be associated with your auth provider (in my case Github), and for NextAuth itself.
These are then link
ed to the Nextjs application so they will be available for use in the middleware and NextAuth route handlers.
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
app(input) {
return {
// up to you
};
},
async run() {
const GitHubId = new sst.Secret('GitHubId', 'placeholder-value')
const GitHubSecret = new sst.Secret('GitHubSecret', 'placeholder-value')
const NextAuthSecret = new sst.Secret('NextAuthSecret', 'placeholder-value')
new sst.aws.Nextjs('Dashboard', {
domain: {
name: 'your-domain-here',
dns: false, // if you are using your own DNS like I am
cert: 'arn:aws:acm:us-east-1:<REDACTED account-number>:certificate/<REDACTED certificate ID>,
},
link: [GitHubId, GitHubSecret, NextAuthSecret],
})
},
});
Github App
This provides an overview of creating a Github App to work as an Auth Provider. If you don't want to use Github, choose your own provider(s) and adapt the rest of the instructions to suit.
IMPORTANT - If you want to test auth in your local dev stage/environment, you'll need to set the {my-domain}
part below to localhost:3000
(or whatever port its running on) or it won't redirect back to you. This is why you may need multiple Github apps, one for dev, and one for production (or other public facing stages).
Go to Github > Settings > Developer Settings > Github Apps > New Github App
- App Name - might be advisable to include the stage name in this if you need to distinguish
- Homepage URL - The origin for your domain, e.g.
https://<my-domain>
. If you are setting up an AWS Certificate for your (sub)domain you will need to usehttps
. Your DNS provider might offer their own certificates, but you need the AWS certificate to validate your domain ownership and it includes the necessary SSL certificate to allow forhttps
. - Callback URL -
https://<my-domain>/api/auth/callback/github
. The/api/auth
part of the route we will create in a bit, and you can customise this if you want or need to avoid existing route configuration. - Webhook - DISABLE
- Permissions - can be left no access unless you specifically need them.
- Where can this app be installed? I chose 'Only this account' as I'm currently testing, if you are moving to production then you will probably want this enabled for 'any account' for your production stage.
- Other options can be disabled or left default
IMPORTANT - When you choose 'Create this App' you will be given the client secret so be sure to note it before navigating away (or you'll need to generate another secret and delete the initial one).
sst secret set...
You now need to provide stage specific values to override the placeholders. Note that for Github, the GitHubSecret is ONLY shown at the point of creation so you'll need to make sure you note it down. The clientId is the app's Id. You can create an additional secret if you somehow forget it, without deleting the Github app itself.
sst secret set GitHubId "<clientId>" --stage <stagename>
sst secret set GitHubSecret "<clientSecret>" --stage <stagename>
sst secret set NextAuthSecret "<some random string>" --stage <stagename>
AWS Certificate Manager
IMPORTANT: You MUST switch to 'us-east-1' (N.Virginia) when creating certificates.
Go to Certificate Manager > Request Certificate
- Request a public certificate
- Fully qualified domain name e.g.
example.com
. If you are adding targetting a subdomain, you should set the root domain as the FQD - For a sub-domain, choose 'Add another name to this certificate' and enter the subdomain including the root, e.g.
myapp.example.com
. Repeat as necessary if you are creating multiple stages for deployment. - Validation method - DNS validation if you are able to amend your DNS records, which you will need to do to redirect traffic to the AWS CloudFront distribution that SST will create.
- Key algorithm - I went with RSA 2048
- Tags - as per your preference for AWS resource tagging
Go to List Certificates, wait for the request to be satisfied, then view the certificate.
- Under 'Domains' you will see CNAME entries for each root and subdomain created. The status will be 'pending' until you amend your DNS entries and AWS validates that the domains belong to you.
- Each DNS providers has their own way of amending DNS entries, but you will need to add the root domain and any subdomain entries using the provided details.
IMPORTANT
You will also need to set a CNAME entry in your DNS for the domain/subdomain to redirect web requests to CloudFront, but we haven't go there yet. You may also need to remove any default DNS entries (A/AAAA/ALIAS/CNAME) that your DNS provider automatically created for your domain, such as for parking/hosting.
Don't forget to come back to this, and I will note it later in the instructions.
Next Auth prerequisites for SST
The following assumes that your Nextjs project was configured to use a src
directory, but if not, omit the src
prefix from the following.
Also, note that I use as @/
path alias in my tsconfig.json
file to provide a root for my application sources. You'll see this in some of the import
statements.
- Add
/src/nextAuth
folder to your project - Create
/src/nextAuth/GetAuthOptions.ts
, containingimport { AuthOptions } from 'next-auth' import { NextRequest } from 'next/server' export type GetAuthOptions = (url: NextRequest) => AuthOptions
- Create
/src/nextAuth/createSSTNextAuthMiddleware.ts
, containingimport NextAuth from 'next-auth' import { NextRequest } from 'next/server' import { GetAuthOptions } from '@/nextAuth/GetAuthOptions' export function createSSTNextAuthHandlers(getAuthOptions: GetAuthOptions) { const handler = (req: NextRequest, res: any) => { const withAuth = NextAuth(getAuthOptions(req)) return withAuth(req, res) } return ['GET', 'POST'].reduce((handlers, method) => { handlers[method] = handler return handlers }, {} as Record<string, any>) }
- Create
src/auth.ts
NB: If you want to modify theimport { GetAuthOptions } from '@/nextAuth/GetAuthOptions' import { AuthOptions } from 'next-auth' import GitHubProvider from 'next-auth/providers/github' import { NextURL } from 'next/dist/server/web/next-url' import { Resource } from 'sst' export const getAuthOptions: GetAuthOptions = (req) => { process.env.NEXTAUTH_URL = new NextURL('/api/auth', req.nextUrl).toString() process.env.NEXTAUTH_SECRET = Resource.NextAuthSecret return { providers: [ GitHubProvider({ clientId: Resource.GitHubId, clientSecret: Resource.GitHubSecret, }) ] } satisfies AuthOptions }
/api/auth
route you will need to do so in this file. If you are using other Auth Providers, you can expand on this.
NextJs App Auth Middleware
In your Nextjs project, you will need to add middleware to allow NextAuth to validate the session for each request made to the server. This DOES NOT actually prevent access, it only determines if the session is valid, and there will be additional steps depending on how you app is constructed, especially with regards to React Server Components.
- Create
/src/middleware.ts
, containingimport { createSSTNextAuthMiddleware } from '@/nextAuth/createSSTNextAuthMiddleware' import { getAuthOptions } from '@/auth' export default createSSTNextAuthMiddleware(getAuthOptions)
NextJs App Auth Route Handler
The auth route handler is responsible for dealing with sign in, sign out and provider callbacks. Note that if you want to modify the /api/auth
root for these, here is where you also need to do that.
NB: The [...nextauth]
in the following path is the actual directory name and is how Nextjs handles dynamic route segements.
- Create
/src/api/auth/[...nextauth]/route.ts
, containingimport { createSSTNextAuthHandlers } from '@/nextAuth/createSSTNextAuthHandlers' import { getAuthOptions } from '@/auth' export const { GET, POST } = createSSTNextAuthHandlers(getAuthOptions)
Deploy
This will deploy the changes for the given stage(s) that align with your domain/subdomain name(s).
IMPORTANT: Don't forget the following CNAME step to point the domain to the CloudFront distribution
sst deploy --stage <stagename>
Add domain CNAME redirection to CloudFront
Go to your AWS Console > CloudFront > Distributions
- Identify which entry belongs to the domain/subdomain that you are working with by the 'Alternate Domain name'
- Copy the 'Domain name' value, which looks like
{random}.cloudfront.net
- Go to your DNS provider for the domain/subdomain
- Add an additional CNAME entry with a name of
@
and a value of{random}.cloudfront.net
- Save and allow for the DNS propogation (this can take hours depending on your DNS provider).
Protecting routes from unauthorised access
There is seemingly an error in the docs for how to do wildcard matching.
In your middleware.ts
file, you will want to add:
export const config = { matcher: ["/dashboard/(.*)"] }
Note that the docs contains :part*
as the wildcard, but this fails to match anything. The matcher strings are effectively regular expressions but you need to wrap the wildcard in brackets for it to accept it (https://github.com/nextauthjs/next-auth/issues/11412).
There are alternatives to how to protect routes, for example on an individual route/page handler basis, but that's a bit beyond the scope here.
Where to put SessionProvider in a React Server Component app?
NextAuth provides this client side component to inject the session into the component hierarchy which can then be consumed with the useSession()
hook. If you are using RSC and want to access the session from a server component, you will need to use getServerSession()
instead. You'll still need the SessionProvider
wrapped around client components which is a bit of a pain if you've got lots of client leaf nodes, but I'm not sure there is much alternative to this.
Auth.js v5 simplifies this with an agnostic auth()
API.