Loading...

Postulate is the best way to take and share notes for classes, research, and other learning.

More info

How to Set Up Authentication in Next.js (as a Noob)

Profile picture of Samson ZhangSamson Zhang
May 30, 2021Last updated May 30, 20216 min read

Authentication may take 5 months to learn, but with the beauty of NextAuth, you don't need to learn the ins and outs of auth to add sign in features to your Next.js app. A noob like me was able to set up authentication in something that looks closer to 5 hours, and even then, I had wasted a lot of time debugging noob errors. However, we can do better than that - today, I will show you how get authentication fully up and running in Next.js in 5 minutes. (And how to avoid the noob errors I made.)

To start off, npm i next-auth @types/next-auth . Create an API route at pages/api/auth/[...nextauth].ts - here is where we'll be configuring our providers. You can add as many as you like, but I'm just going to use Google.

import NextAuth from "next-auth";

import Providers from "next-auth/providers";

import {NextApiRequest, NextApiResponse} from "next";



const options = {

providers: [

Providers.Google({

clientId: process.env.GOOGLE_CLIENT_ID,

clientSecret: process.env.GOOGLE_CLIENT_SECRET

}),

]

};



export default (req: NextApiRequest, res: NextApiResponse) => NextAuth(req, res, options);

You get your client ID and client secret by going to your Google Cloud Console. Create a new project, then go to APIs and services > Credentials. Hit "Create Credentials," select "OAuth client ID," and enter "http://localhost:3000" and your domain as Authorised JavaScript origins and "http://localhost:3000/api/auth/callback/google" and your domain with the /api/auth/callback/google path as Authorised redirect URIs. Google will then provide you with your client ID and client secret, which can be added as environment variables.

In order for the app to know that the providers are there (and to avoid my noob error of providers being null), wrap your pages in the <Provider> tag and setting the session page props. Here's my _app.tsx :

import '../styles/globals.css'

import {Provider} from "next-auth/client";



function MyApp({ Component, pageProps }) {

return (

<Provider session={pageProps.session}>

<Component {...pageProps} />

</Provider>

)

}



export default MyApp

For auth flow, we're going to have a sign in page at pages/auth/sign-in.tsx . For our auth purposes, it's going to be plain and unstyled - just a basic button that calls NextAuth's built in sign in callback.

import {signIn, useSession, getSession} from "next-auth/client"

import {GetServerSideProps} from "next";



const SignIn = () => {

const [session, loading] = useSession();

return (

<div>

{!session && (

<>

Not signed in <br />

<button onClick={() => signIn('google')}>Sign in</button>

</>

)}

{session && (

<>

Signed in as {session.user.email} <br />

<div>You can now access our super secret pages</div>

</>

)}

</div>

)

}



export default SignIn

The useSession() hook can be accessed from any page and allows us to check whether the user is logged in.

At this point, authentication already works. Try logging in!



However, we have no way to do any long term user data processing as we're not storing these accounts in a database. Let's change that.

If the user is already signed in, we will redirect them to create a new account via server side props, which we put in the file underneath the main component function.

export const getServerSideProps: GetServerSideProps = async (context) => {

const session = await getSession(context);



if (session) {

console.log(session);

context.res.setHeader("location", "/auth/new-account");

context.res.statusCode = 302;

context.res.end();

}



return {props: {}};

};

At the page where a new account is created, we will connect to MongoDB via a pages/api/accounts.ts API route. If this is a new account, we create a new account and store the data in MongoDB. Here is pages/api/accounts.ts :

import {NextApiRequest, NextApiResponse} from "next";

import {getSession} from "next-auth/client";

import dbConnect from "../../utils/dbConnect";

import {AccountModel} from "../../models/account";

import {AccountObj} from "../../utils/types";



export default async function account(req: NextApiRequest, res: NextApiResponse) {

if (req.method !== "POST") return res.status(405);

const session = await getSession({req});

if (!session) {

return res.status(403).json({message: "You must have an active session to create an account."});

}



try {

await dbConnect();

const emailUser = await AccountModel.findOne({ "email": session.user.email });

if (emailUser) {

return res.status(200).json({message: "Account already exists."});

}



const newAccount: AccountObj = {

email: session.user.email,

name: session.user.name,

image: session.user.image,

trialExpDate: "2021-06-28T18:46:41.984Z" // this is a random date, change later to today + x days

};



await AccountModel.create(newAccount);

res.status(200).json({message: "Account successfully created."});

return;

} catch (e) {

return res.status(500).json({message: e});

}

}

At pages/auth/new-account.tsx , we will send a post request to the above API route.

import React, {useState} from "react";

import {GetServerSideProps} from "next";

import {getSession, signIn, useSession} from "next-auth/client";

import axios from "axios";

import {useRouter} from "next/router";

import Skeleton from "react-loading-skeleton";

import SpinnerButton from "../../components/spinner-button";

import { getCurrUserRequest } from "../../utils/requests"



export default function NewAccount() {

const router = useRouter();

const [session, loading] = useSession();

const [error, setError] = useState<string>(null);



function onSubmit() {

axios.post("/api/account").then(res => {

if (res.data.error) {

setError(res.data.error);

} else {

console.log(res.data.message)

console.log("redirecting...");

router.push("/").catch(e => console.log(e));

}

}).catch(e => {

setError("An unknown error occurred.");

console.log(e.response.data);

});

}



return (

<div className="max-w-sm mx-auto px-4">

<h1 className="up-h1">Welcome to Beta Testing Manager</h1>

<hr className="my-8"/>

<p className="up-ui-title">Creating new account as:</p>

{loading ? (

<div className="my-4">

<Skeleton count={2}/>

</div>

) : (

<div className="flex items-center my-4">

<img

src={session.user.image}

alt={`Profile picture of ${session.user.name}`}

className="rounded-full h-12 h-12 mr-4"

/>

<div>

<p className="up-ui-title">{session.user.name}</p>

<p>{session.user.email}</p>

</div>

</div>

)}

<hr className="my-8"/>

{error && (

<p className="text-red-500">{error}</p>

)}

<Button

onClick={onSubmit}

>

Let's get started!

</Button>

</div>

);

}

Session objects allow you to retrieve the user's name, email, and image associated with their Google account through session.user.name etc, which we can play around with. If the post request goes through, we redirect the user to the homepage with router.push("/") .

Now, I can try logging in again! Seeing my data get created in MongoDB as a new user makes me feel like a 5 year old on Christmas morning.



One last step: if /auth/new-account is accessed when an account already exists, the user will be redirect to homepage. If this page is accessed while not logged in, the user will be redirected to sign in. Once again, this is done with server side rendering.

export const getServerSideProps: GetServerSideProps = async (context) => {

const session = await getSession(context);

const user = await getCurrUserRequest(session.user.email);



if (!session || user) {

context.res.setHeader("location", !session ? "/auth/sign-in" : "/");

context.res.statusCode = 302;

context.res.end();

}



return {props: {}};

};

With that, we have set up our full auth flow!




Comments (loading...)

Sign in to comment

Beta Testing Manager

Building a beta testing manager app with Laura