NextAuth not only does handle most of your client and server-side logic with ease, but you can also create multiple authentication providers. You will understand how NextAuth works and how to use the credentials provider
to create a JWT accessToken and refresh it. So, let's dive in and use t3-app and MongoDB to build a full-stack authentication application with NextAuth.
1) Create a t3 app project
use npm:
npm create t3-app@latest
or yarn:
yarn create t3-app@latest
2) Setup a MongoDB in Prisma
To make Prisma work in MongoDB , we need to get a Replica Set on MongoDB Atlas
Edit .env file in root directory, and change DATABASE_URL
:
DATABASE_URL="mongodb+srv://YOUR_DB_URL"
Make sure to replace special characters to ASCII value e.g. "?"
replace to "%3F"
To store user data, we need to create a User schema. We'll need to replace the current schema located at ./prisma/schema.prisma with the following:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid()) @map("_id")
name String
password String
}
Then run npx prisma db push
command to sync our changes with MongoDB.
3) Setup a NextAuth
If you're new to JWT authentication, you can watch this video for a more detailed explanation of the process:
In NextAuth, we can choice JWT strategy or Database strategy. In this demo, we will use JWT strategy
for setup NextAuth.
Add JWT_SECRET
and NEXTAUTH_SECRET
in .env
file. The secret is a hash key to sign and verify JWT.
...
DATABASE_URL="mongodb+srv://your db url"
+ JWT_SECRET="your secret"
+ NEXTAUTH_SECRET="your secret"
If you're using a Mac OS or Linux system, you can generate secret using the following command:
openssl rand -base64 32
Next, we need to install bcrypt in order to hash the user's password:
use npm:
npm install bcrypt -D @types/bcrypt
or yarn:
yarn add bcrypt --dev @types/bcrypt
Let's take a closer look at the main steps involved in how NextAuth works within our app:
Providers: When a user initiates the authentication process, it will send authentication providers and their configuration options to the client-side. We will use
credentials
providerUsername
andPassword
for user login/signup. In addition, you can add addition provider to your nextAuth.If the authentication process is successful, the function will return {user:{id: string, name: string}}. If authentication fails, it will return null. In either case, the data is passed to the JWT callback function as{user}
Callback - JWT: This function is called whenever a request is made to
/api/auth/signin
, as well as after thesignIn
callback function has been executed. And pass{token}
to session callback function. After the user has successfully logged in, we can retrieve their data from the params{user}
. If the user exists, we can then proceed to create both theaccessToken
andrefreshToken
. Subsequently, the callback function is also called during requests made togetSession()
,unstable_getServerSession()
, anduseSession()
. During each of these requests, the callback function is the first to check whether the JWT access token has expired. If it has, the JWT callback will attempt to refresh the token using the refresh token. For a more detailed look at how to implement this process๏ผ refresh token rotation.Callback - session: The session callback will return session object that can be used to display the user's name or any other user data that's relevant to the application. The callback function will be called when
getSession()
,useSession()
,/api/auth/session
To create and verify JWT tokens, we need to create a function within the src/utils/
directory called jwtHelper.ts
. This function will handle the creation and verification of the JWT tokens that are used throughout our app.
import { encode, decode } from "next-auth/jwt";
import { env } from "@/env.mjs";
import { User } from "@prisma/client";
export interface AuthUser extends Omit<User, "Password">{}
export const tokenOneDay = 24 * 60 * 60;
export const tokenOnWeek = tokenOneDay * 7
const craeteJWT = (token:AuthUser, duration: number) => encode({token, secret: env.JWT_SECRET, maxAge: duration})
export const jwtHelper = {
createAcessToken: (token:AuthUser) => craeteJWT(token, tokenOneDay),
createRefreshToken: (token:AuthUser) => craeteJWT(token, tokenOnWeek),
verifyToken: (token:string) => decode({token, secret: env.JWT_SECRET})
}
We need to define the NextAuthType type in order to integrate it properly within our app. To do so, we'll create a new file called next-auth.d.ts
within the src/pages/types/ directory
. For more information on how to extend default interface properties and properly define this type.
import NextAuth from "next-auth/next";
import { JWT } from "next-auth/jwt"
import { AuthUser } from "@/utils/jwtHelper";
declare module "next-auth" {
interface User {
userId?: string,
name?: string
}
interface Session {
user: {
userId?: string,
name?: string
},
error?: "RefreshAccessTokenError"
}
}
declare module "next-auth/jwt" {
interface JWT{
user: AuthUser
accessToken: string,
refreshToken: string,
accessTokenExpired: number,
refreshTokenExpired: number,
error?: "RefreshAccessTokenError"
}
}
Change src/pages/api/auth/[...nextauth].ts
as follow:
import { type GetServerSidePropsContext } from "next";
import {
getServerSession,
type NextAuthOptions,
} from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { prisma } from "@/server/db";
import bcrypt from 'bcrypt';
import { AuthUser, jwtHelper, tokenOneDay, tokenOnWeek } from "@/utils/jwtHelper";
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
session: {
strategy: "jwt",
maxAge: 60 * 60
},
providers: [
CredentialsProvider({
id: "next-auth",
name: "Login with email",
async authorize(credentials, req) {
try {
const user = await prisma.user.findFirst({
where: {
name: credentials?.username
}
});
if (user && credentials){
const validPassword = await bcrypt.compare(credentials?.password, user.password);
if (validPassword){
return {
id: user.id,
name: user.name,
}
}
}
} catch(error){
console.log(error)
}
return null
},
credentials: {
username: {
label: "Username",
type: "text",
placeholder: "jsmith",
},
password: {
label: "Password",
type: "password",
},
},
})
],
callbacks: {
async jwt({token, user, profile, account, isNewUser}){
// credentials provider: Save the access token and refresh token in the JWT on the initial login
if (user){
const authUser = {id: user.id, name: user.name} as AuthUser;
const accessToken = await jwtHelper.createAcessToken(authUser);
const refreshToken = await jwtHelper.createRefreshToken(authUser);
const accessTokenExpired = Date.now() /1000 + tokenOneDay;
const refreshTokenExpired = Date.now() /1000 + tokenOnWeek;
return {
...token, accessToken, refreshToken, accessTokenExpired, refreshTokenExpired,
user: authUser
}
} else {
if (token){
// In subsequent requests, check access token has expired, try to refresh it
if (Date.now() /1000 > token.accessTokenExpired){
const verifyToken = await jwtHelper.verifyToken(token.refreshToken);
if (verifyToken){
const user = await prisma.user.findFirst({
where: {
name: token.user.name
}
});
if (user){
const accessToken = await jwtHelper.createAcessToken(token.user);
const accessTokenExpired = Date.now() /1000 + tokenOneDay;
return {...token, accessToken, accessTokenExpired}
}
}
return {...token, error: "RefreshAccessTokenError"}
}
}
}
return token
},
async session({ session, token }){
if (token){
session.user = {
name: token.user.name,
userId: token.user.id
}
}
session.error = token.error;
return session;
}
}
};
Now that we've implemented NextAuth, we can easily protect unauthorized access from both the front-end and back-end of our application.
In t3-app, we can use the protectedProcedure
to protect our API from unauthorized access by checking for a valid session. Additionally, we can use the useSession()
hook to check whether a client is authorized to access certain features of the app.
I hope you found this tutorial helpful, the full source code is available on GitHub, so feel free to check it out and customize it to fit your needs. In the next tutorial, we'll explore how to build a multi-factor authentication to further bolster your app's security. Stay tuned! ๐พ๐พ๐พ๐พ
Full code on GitHub
Top comments (0)