Authentication with NextAuth - vonschappler/Ultimate-React GitHub Wiki
The Auth.js library (formely known as NextAuth) is a library that simplificates the process of authentication and authorization on your Next.js applications.
It's possible that this library can be used on any applications / frameworks, but here we are going to work with this library inside Next.js, while also allowing the creation of new users accounts on our supabase database.
In our case, we are going to work with Google Provider in order to authenticate users, so the following steps are required:
-
Set up google to enable the auth api, in order to make it usable on your application - for this, any usable resources to learn how this process is done, because both the UI and way of creating an application can change from time to time.
-
Then we need to set up (or add) the environment variables in our
.env.local
:NEXTAUTH_URL=your_developer_base_url # as it's diplayed on the web browser NEXTAUTH_SECRET=your_secret # it's a secret created by you or generated with the help of any secret generation application AUTH_GOOGLE_ID=google_application_client_id # the value provided by the application after creating it AUTH_GOOGLE_SECRET=google_application_client_secret # the value proved by the application after creating it
-
Install the library
npm i next-auth@beta
NOTE:
For this project we are going to use the @beta tag, as indicated on the official documentation
-
After setting up everything, the next step is to add a new file inside our
_lib
folder, which in hereimport NextAuth from 'next-auth'; import Google from 'next-auth/providers/google'; const authConfig = { providers: [ Google({ clientId: process.env.AUTH_GOOGLE_ID, clientSecret: process.env.AUTH_GOOGLE_SECRET, }), ], }; export const { auth, handlers: { GET, POST }, } = NextAuth(authConfig);
-
Now we need to create a new api to handle the authentication part of the application, using another convention for Next.js, the catch all router, by (in this case) creating a nested folder with the following relative path (from /app):
api/auth/[...nextauth]
, while adding to this new folder the fileroute.js
with the following code:export { GET, POST } from '@/app/_lib/auth';
-
To test if everything is working, read to the url
yourHost/api/auth/signup
and if all the setup process was done correctly you should be able to see the sign up with google button.
When we completed with the whole setup proccess above, one of the functions that we exported was the auth()
, inside the file auth.js
.
This function can be then called inside any server component so we can have access to the information provided to the application about the current logged in user.
One of the ways we can use the auth() function is displayed on the code below:
import { auth } from 'path/to/auth';
export default async function ServerComponent() {
const session = await auth();
console.log(session);
// do something with the session object returned by auth()
}
One important thing to consider here is that using this auth()
function will make all the "children routes and components" dynamic, because this auth function reads cookies and as alrady discussed, cookies automatically convert and route/component to a dynamic component, because cookies can only be known are runtime, not at build time.
Authentication and authorization are close siblings. Authentication is the procces on which we create a session for logged in users, while authorization is the proccess on which we set up access to routes on our application based on the session object created during the authentication proccess.
In Next.js the access to routes based on sessions is done by middlewares.
In Next.js, a middleware as a function that "sits" between a request and a response. In other words, a Next.js middleware allows us to run some code after an income request and before we can return any response to the client.
Well, this concept is not really "that simple". The fact is that middlewares, by default run before every app route. If we want a middleware to run just on specific routes, we can specify these by using a matcher.
So, with this new concept, we can go into more detail and say that a middleware is a function that runs right after any request is received, but before the app route is rendered and the results are sent to the client.
We can make an analogy to this as a chunk of code that's passed to the client into every page.js
component. But to keep our component clean and all the common logic into a central place, we make use of those middlewares.
Keep in mind that for defining middlewares on Next.js a few conventions must be followed:
- A file with the name
middleware.js
needs to be placed on the project root folder (not the /app folder) - ONLY ONE function can be exported from this file, and this function HAS TO BE NAMED
middleware
, which has access to the request object
We use middlewares for:
- Reading and setting cookies and headers
- Enabling authentication and authorization
- Enabling server-side analytics
- Enabling redirects based on geolocation
- Enabling A/B testing
Middlewares always need to produce a response, which can be done in two different ways:
- Re-render (redirect) to some route in our application
- Sending a json response to the client - in this case, we can still keep all functionality of the middlewares, but without necesseraly reaching a route to re-render, which is useful to send some data as part of an api.
Let's remember what was discussed above and then start the implementation of NextAuth inside a middleware to add some access policies to parts of our application.
Let suppose that we wish to redirect a visitor from one page to another, we can do it like so:
import { NextResponse } from 'next/server';
export function middleware(request) {
return NextResponse.redirect(new URL('/destiny_route', request.url));
}
// this prevents infinite redirect loops
export const config = {
matcher: ['/origin_route'],
};
Below there is a basic snippet of code which can be used as a skeleton for this use case (authorization):
import { auth } from 'path/to/_lib/auth';
export const middleware = auth;
export const config = {
matcher: ['/route_to_be_protected'],
};
We also need to (for this use case) add a callback object in our auth.js
file:
import NextAuth from 'next-auth';
import Google from 'next-auth/providers/google';
const authConfig = {
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
}),
],
callbacks: {
authorized({ auth, request }) {
return !!auth?.user;
},
},
pages: {
signIn: '/login',
},
};
export const {
auth,
handlers: { GET, POST },
signIn,
signOut,
} = NextAuth(authConfig);
Also in order to make this work, since we are redirecting users to the /login
route (with a login button) in case they reach the protected route defined in the middleware, we need to make use of server actions as a way to provide some functionality to server components, only because we want the login part of our application to be rendered as a server component, so either the skeleton code and the explanation for this procces will be added to this.
For more about server actions check this page.
A similar proccess as described above (the use of server actions) can be done to create a logout process, but again, this proccess will not be detailed here.
Let's remember that when we first created the internal application for The Wild Oasis, we created a guests
table. This is the table that we'll be using for storing the new guests information to our application, so those can create new reservations of cabins and also make changes on theor profiles.
So to do so, we need to connect those two pieces, just like shown in snippet of code below:
// auth.js
import NextAuth from 'next-auth';
import Google from 'next-auth/providers/google';
import { createGuest, getGuest } from './data-service';
const authConfig = {
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
}),
],
callbacks: {
authorized({ auth, request }) {
return !!auth?.user;
},
async signIn({ user, account, profile }) {
try {
const existingGuest = await getGuest(user.email);
console.log(existingGuest);
if (!existingGuest)
await createGuest({ email: user.email, fullName: user.name });
return true;
} catch {
return false;
}
},
async session({ session, user }) {
const guest = await getGuest(session.user.email);
session.user.gestId = guest.id;
return session;
},
},
pages: {
signIn: '/login',
},
};
export const {
auth,
handlers: { GET, POST },
signIn,
signOut,
} = NextAuth(authConfig);