Max Next – Section 13 – Adding Authentication


Courses

Updated Feb 1st, 2022

Table of Contents

Chapter Details

Ch. 213 – Module Introduction:

Ch. 214 – Starting Project

As of 1/31/22, this is now chapter 216…

Simple “NavBar” component with text-based logo and Login, Profile and Logout buttons. Purple UI color scheme. Only authenticated users can see profile link and navigate to that page. Only logged in users see logout link.

There is a button at the bottom of the form that toggles between login and sign-up. Uses conditional code like:

function AuthForm() {
  const [isLogin, setIsLogin] = useState(true)

  function switchAuthModeHandler() {
    setIsLogin((prevState) => !prevState)
  }

  return (
    <section className={classes.auth}>
     <h1>{isLogin ? 'Login' : 'Sign Up'}</h1>
     <form>
      // Some more conditional code here
     </form>
    </section>
  )
}

Ch. 215 – How Authorization Works in React and NextJS Apps

User fills out a form to try and become authenticated and the client browser sends a JavaScript request with user credentials to the server and the server sends back a response with a yes or no.

We want certain aspects of our application to only be accessed or performed by authenticated users. So when a user sends a request to attempt to perform a protected action or access a protected area we also need to send additional data that tells the server whether the user is authenticated or not.

A request could come in to a protected API route saying “hey I’m authenticated” but we can’t just believe that, because a “fake yes” could be sent to the server requesting protected data. We need proof, which can’t be faked, and we have two main methods: server-side sessions and authentication tokens.

Server-side Sessions Overview: Store unique identifier on server, send same identifier to client. Client sends identifier along with requests to protected resources.

Authentication Tokens Overview: Create (but not store) “permission” token on server, send token to the client. Client sends token along with requests to protected resources.

Single page applications work with tokens instead of sessions. Pages are served directly and populated with logic without hitting the server. Backend APIs work in a “stateless” way (they don’t care about connected clients) with a detached front-end and back-end. Servers don’t save information about authenticated clients. Instead, clients should get information that allows them to prove their authentication. JSON Web Tokens are the most common form of authorization token.

A JWT is comprised of: Issuer Data, Custom Data (user data), and Secret Signing Key. Note that it is signed, NOT encrypted, (can be parsed and read by anyone). Only the signing server is able to verify and incoming token.

For example, if you wanted to update a password then the request would have the old password, the new password, and the token.

Ch. 216 – Using “next-auth” Library

NextAuth.js is a package that makes authentication with Next JS simple. We can implement one of the many different providers or we can go with our own. Docs are the place to go. Building our own in this section.

To get started “npm install next-auth” which has both client-side and server-side validation capabilities. Need to bring in our own logic to create users, (With our own “/api/signup” api route).

Important: This Course Uses Next-Auth Version 3!

Currently, the latest version is v4. It generally works the same as shown in the lectures but some imports and method names changed. There is an official V4 migration guide here. To install older version use this npm command:

npm install --save-exact next-auth@3

Ch 217 – Adding a User Signup API Route

In the “pages” folder create an “api” folder and inside of that create an “auth” folder and inside of that create a “signup.js” file. Connect to the database and store user data.

To share database connection logic, since you will need to use this in multiple files, in the root directory create a “lib” folder and inside it create a “db.js” file (to create reusable database connection).

In the “lib/db.js” file export an async function called “connectToDatabase” that creates a constant variable named client that is set to await MongoClient.connect() and returns a client.

In the “signup.js” file, in a handler function, extract incoming data from the request object by creating a constant variable named data set to req.body and then extract email and password from this new data constant. Run some simple validation. Import and call the “connectToDatabase” function. Get access to the “users” collection and store data using the “.insertOne()” method but be sure to store a hashed/encrypted password.

In the “lib” folder create an “auth.js” file, (to hold authorization-related utility methods like password hashing) and import {hash, compare} from “bcryptjs,” a 3rd party package. Export an async function named hashPassword that takes in the plain text password as an input parameter. In the function body call the hash() method where the first argument is password, and the second argument is the number of salting rounds. The hash method returns a promise which will give you the resulting hashed password so await the hash method and set hashedPassword as a constant variable. Return hashedPassword.

Back in “signup.js” create a hashedPassword const by awaiting and calling this newly created hashPassword function (be sure to import this at the top of signup.js file) and pass it the plain text password. Now in insertOne() set the password key to hashedPassword.

With that you are storing the user in the database and now you want to send back a success response. We are not implementing error handling here since the lesson is focusing on authentication.

Ch. 218 – Sending Signup Requests from the Front-End

Go back to “auth-form.js” file and make sure we talk to the API route created in previous lesson when the user authenticates. You want to listen to the form submission and if you’re not in the logged-in-mode, which means you are in create-mode, you want to send a request to your backend to create a new user.

Create a new “submitHandler” function where we take in the event and prevent the default and check if we are in login-mode in which we would log the user in, else, we want to send a request that creates the user.

Create anew async function called “createUser” that awaits the sending of a fetch request (as post request, with body sending email and password, and header as ‘Content-Type’: ‘application/json’) and stores in a constant named response.

Handle the response by getting the data out of the response with response.json() and awaiting that since it also returns a promise and then we check if (!response.ok) throw a new error with data or message just saying “something went wrong!” Ultimately return data if we make it passed the if check.

Now in the else case of the submitHandler function call createUser and pass in the email and password. Now for this we need to get access to the email and password in which we can use useState with two-way binding or we can use useRef for quick and easy access which is what we will do here.

Import useRef from “react” and in the component function just create two refs for the two inputs we have. Wire them up by setting, on the email input, ref={emailInputRef} and do the same for the password input.

Now get the data in the submitHandler function by setting const enteredEmail equal to emailInputRef.current.value and do the same for password.

We could add validation here but we’re skipping.

Now just pass enteredEmail as the first argument and enteredPassword as the second argument to createUser. Turn submitHandler function into an async function and then await the response from createUser by storing in a constant named result. You can also wrap this in a try/catch block to catch any errors and handle the errors.

Connect form to the submitHandler function with the onSubmit prop.

And then on the API route we still need to do something. We are sending a POST request and only a POST request should trigger this user creation. So in async function handler you want to check if (request.method === POST) and only if that is the case cut and paste in all the code to execute it otherwise you don’t do anything. Note that you can also reverse the logic too say: if ( request.method !== POST) return and then below have code. This is the approach Max takes.

Now if you go back to the auth form in the browser and open up your console and do a test sign up with a very short password you will get an error in console. But if you enter a password which is long enough you will get a success message in the console and in your MongoDB database you will see a new user.

Ch. 219 – Improve Signup with Unique Email Addresses

Creating user works but to this point you can create multiple users with the same email address which is obviously not what we want. To avoid this, in “signup.js” file in the handler function, see if a user with a given email already exists.

So after we connect to the database but before we hash the password check if the user already exist by creating an existingUser constant and setting to await db.collection(“users”) and call findOne() and pass an object where we describe how to search using key value pairs. In this case the key will be email and the value (the values we want to search for) will also be email. And this will be either “undefined” if we did not find a user or it is the user object. So if (existingUser) then send back a 422 status code with the JSON message “user already exists”, and run db.close, and then return to stop execution.

Ch 220 – Adding the “Credentials Auth Provider” and User Login Logic

Create a new dynamic catch-all API route by creating, in the “pages/api/auth” folder, a new “[…nextauth].js” file that will catch all routes that begin with “api/auth/.”

We need this catch-all route because “next-auth” package will, behind the scenes, expose multiple routes for user login and user logout for example and a couple other routes as well. In order for “next-auth” to set up its own routes, and handle its own routes, we need to have a catch-all route so all those special requests to these special routes are automatically handled by the “next-auth” package. We can still define our own routes, like the “/signup” route in addition, as long as we don’t override one of the built-in routes “next-auth” exposes.

If you want to find out what these built-in routes are, visit the next-auth.js.org website to see the documentation and click on Rest API to see the route names you should not clash with.

In this new “[…nextauth].js file import “NextAuth” from “next-auth.” At the bottom export default “NextAuth()” and notice that we actually execute it here. When we execute “NextAuth” it returns a new function, a handler function. It needs to because this is still an API route and therefore an API route still needs to return a function and it needs to export a function. But this exported handler function is created by “next-auth” behind the scenes by calling “NextAuth().”

When we call NextAuth we can pass a configuration object to it, which allows us to configure NextAuth’s behavior. You can dive into the official docs in configuration options to learn about all the options you can and must set here.

In our case we want to set a “providers” key, which is set to an array. Import “Providers” from “next-auth” and then set the value for the “providers” key to “Providers.Credentials,” which means that we bring our own credentials.

import NextAuth from 'next-auth'
import Providers form 'next-auth/providers'

export default NextAuth({
  providers: [
    Providers.Credentials
  ]
})

Credentials takes a configuration object itself in which we can 1.) set a “credentials” key and set to what your credentials are (email and password) and let NextAuth create a form for us, we will not take this option here, or 2.) Call the “authorize()” method, which NextJS will call for us when it receives an incoming login request. It’s an async function that returns a promise and, as an argument we get the credentials that were submitted, and that is an object with the data we submitted (email/password).

Inside bring our own authorization logic to check if the credentials are valid and tell the user if that’s not the case (for example throw an error). So in here connect to database (import from lib folder) and close db at the bottom of the function. If we have a user for entered email and we have a user with that correct password. Get access to the users collections and use findOne() to check for email field. Await this task since it’s an async task returning a promise and set result to a constant named user.

If (!user) there is no user with that email address so “throw new Error,” where we say “no user found.” When you throw an error inside of “authorize” it will reject promise which it generates and then, by default, redirect the client to another page, (we will override this so we stay on login page and maybe just show error here).

import NextAuth from 'next-auth'
import Providers form 'next-auth/providers'

export default NextAuth({
  providers: [
    Providers.Credentials({
      async authorize(credentials) {
        const client = await connectToDatabase()
        const usersCollection = client.db().collection('users')
        const user = await usersCollection.findOne({email: credentials.email})

        if (!user) {
          client.close()
          throw new Error('No user found.')
        }

        // find out if password is correct 

        client.close()
      }
    })
  ]
})

If we make it passed this “if check” we have found a user for this email address and now we need to check if the password is correct. The password is hashed so we cannot just do a simple compare. The “bcrypt” package has a “compare” method/function (need to import this) that allows us to see if plain text password matches a hashed password.

In the “lib/auth.js” export a new async function called “verifyPassword” and pass the plain-text password as the first argument and the second password pass the “hashedPassword.” And then run the “compare()” function and pass in the plain text password and the “hashedPassword.” Await this and store result as a constant named “isValid” because “compare()” returns a promise that will be a boolean (true if equals and false if otherwise) and return that boolean (isValid).

// in "lib/auth.js" file

import {hash, compare} from 'bcryptjs'

export async function verifyPassword(password, hashedPassword) {
  const isValid = await compare(password, hashedPassword)
  return isValid
}

Now back in the catch-all, “[…nextauth].js” file in your function call the “verifyPassword” function and pass in credentials.password (submitted password which user tries to login) and the second argument is the password stored in database (user.password). Need to await this because “verifyPassword” is async function and hence returns a promise and then we get a “isValid” result to store in constant. If (!isValid) then throw new Error and say “could not log you in.”

So back in the “[…nextauth].js” file under the “if check”

const isValid = await verifyPassword(credentials.password, user.password)

if (!isValid) {
  client.close()
  throw new Error('Could not log you in.') 
}

client.close()
return {email: user.email}

If you make it passed the “isValid if check” we know we have a user for a given email and the password so we return an object. If you return an object inside of “authorize” we are letting “next-auth” know that authorization succeeded so that the user is logged in.

This object will actually be encoded into the JSON web token. So we can include the user email but we should not pass the entire user object because we don’t want to include the user password, exposing to the client, even if it hashed.

To ensure the JSON web token is created go to the “nextAuth” config object and, besides setting up our providers, also add the “session” option here. This is an object where you can configure how the session for an authenticated user should be managed and here you have a “jwt” key that should be set to true so JSON web tokens will be used.

export default NextAuth({
  session: {
    jwt: true
  },
  providers: []
})

For some other authentication providers you have other ways of managing this (say store in database) but for credential-based authentication, for this provider, you must set to “jwt.” This would be set to true by default if you don’t specify a database, which we do not, but it’s still not a bad idea to set “jwt” to true explicitly anyway. For example, as a side note, if you use the email authentication provider, where you get an email magically sent to you, you must provide a database and you don’t use “jwt.”

Make sure “client.close” is called before return and also if you throw an error

Ch 221 – Sending a Sign-in Request from the Front-end

We also need front-end logic in the “auth-form.js” component. There we have this check, if the user is logged in but we’re not doing anything if they are logged in. So to sign the user in we don’t need to send our own HTTP request. We don’t need to configure it ourselves and we shouldn’t.

Instead we will import {signIn} function from “next-auth/client.” We can call the “signIn()” function in our component to send a sign-in request and the request will be sent automatically. So we just call the “signIn()” function here and pass in some data. The first argument describes the provider with which we want to sign in, because we could have multiple providers in the same application. Here it is the credentials provider.

Then pass the second argument which is a configuration object where we can configure how the sign-in process should work. Specifically we can add a redirect key and set it to false. This is because if you remember on the back-end when you throw an error because authentication failed by default Next JS will redirect to an error page. Now when setting redirect to false the “signIn()” function will return a promise so you can await that and store the result in a constant named result. This promise will always resolve and never reject. If we have an error in the back end authorization code it will resolve with an object containing information about the error. If it resolves without an error it will still have an object but without the error information.

const result = await signIn('credentials', {
  redirect: false
})

Now besides passing redirect and setting it to false, we also want to pass our credentials data to the back-end. We do this in the second argument (configuration object) as well. Add an email key and set to enteredEmail and a password key set to enteredPassword. Now if you submit a correct username but a bad password you’ll get a result object which has an error value. If you enter an incorrect user you’ll get a result object which has an error value of “error: no user found.” If you submit correct info you will get a result object with the error set to null.

const result = await signIn('credentials', {
  redirect: false,
  email: enteredEmail,
  password: enteredPassword,
})

So this is how we sign a user in and we get this yes/no response.

But to this point we are not working with the token. In the front-end components when we get the results we could now check if result error is falsy (!result.error) which means we have no error or succeeded and we could “set some auth state” maybe also with the help of React’s useContext API, or redux, and use that state to change what we see. For example, change the options in the header. But whenever we reload that state would be lost because we start a brand new single page application when we reload all the state which was only stored in memory from the last visit and will be lost and that’s not what we want.

That’s also not what we have to accept because this is exactly why we have this token concept where we can store the token in more permanent storage, and not just memory, and we can use the token to send requests to potentially protected API routes like a “/change-password” API route.

Ch 222 – Managing Active Session on the Front-end

So let’s start utilizing the answer to the question whether the user is authenticated or not. For example we want to make sure that the options in the header change based on whether we are authenticated or not and again NextJS makes is pretty easy for us.

It is worth noting that after we logged in successfully, NextJS added a cookie and we can see this in the browser dev tools and application and select our domain. These cookies are generated and managed by next JS and set when we logged in successfully. And it’ll also uses token automatically for us when we try to change what we see on the screen or when we try to send requests to protected resources.

For example we can get the answer to whether the user is logged in or not by finding whether or not such a valid token exists and we could come up with some code that allows us to do that manually though that code would involve that we send that cookie to the server and let the server decide whether it is valid or not but it’s good that we don’t have to do that.

Instead if you want to find out if the user using this page at the moment is authenticated or not Next JS gives us a convenient way of doing that. In the “layout/main-navigation.js” component import {useSession} from “next-auth/client.” This “useSession” is a hook that we can use in any React functional component.

So call “useSession” which will return an array with two elements. The first element is the session object describing the active session. The second element is “loading” which tells us if NextJS is still figuring out whether we are logged in or not. The line of code will look like const [session, loading] = useSession().

import {useSession} from 'next-auth/client'

const [session, loading] = useSession()

console.log(loading)
console.log(session)

When you log these both individually and go back to the browser dev tools console and reload we see true, undefined, true, some object, false, and some object.

true // loading state finding out if authenticated or not
undefined // no session yet
true // still loading
{} // still loading but get object
false // loading has completed
{} // we have session object with user data which we encoded into token

First log is always the loading state. So initially we’re loading, we are finding out whether the user is authenticator or not. Then hence we get no section object yet and then at some point we are still loading but we do have the object and hence then loading is false and in the session object we get the user data which we encoded into our token and we get some expiration date which tells us when the session will expire automatically though it is worth noting that this session will be prolonged automatically if the user is active. Now that we know what’s inside session and loading, we can change what we see based on these two pieces of information (session and loading).

We can make sure that the profile link is only showing up if we have a session.

{session && (
  <li>
    <Link href="/profile">Profile</Link>
  </li>
)}

This could creates some brief flashing that is unpleasant and we’ll come back later to optimize this, but this generally is how we can find out if we are authenticated or not.

We can utilize this to only show the “login” link if we don’t have a session and we’re are not loading. For the “logout” button we only want to show this if we do have a session, {session && (-li Link li-)}

{session && !loading && (
  <li>
    <Link href="/auth">Login</Link>
  </li>
)}

{session && (
  <li>
    <Link href="/profile">Profile</Link>
  </li>
)}

{session && (
  <li>
    <button>Logout</button>
  </li>
)}

We now at least know how we can control what we see on the user interface and that is an important first step.

Ch 223 – Adding User Logout

Give the “logout” button an “onClick” prop that triggers a “logoutHandler” function defined at the top of the file. Now import {signOut} from “next-auth/client.” In the “logoutHandler” function call “signOut()” function and that also returns a promise, which tells us when it’s done, but here we don’t care about that since we are using “useSession” and so this component will be updated automatically anyways as soon as the active session changes, and it will change when we sign out.

NextJS will then clear that cookie and clear that information that the active user is logged in. Hence if we save and reload and still logged in and click “logout” the UI changes, and you can no longer see the “profile” link. If you go to application in the dev tools again and you inspect your cookies you also see that one very important cookie is missing and that’s the session token cookie. That’s missing because NextJS cleared that when we sent the sign out request. Now of course we can sign in again though and then our UI updates appropriately. So that is how we can log out and update the UI.

Ch. 224 – Adding Client Side Page Guards (Route Protection)

Quick Note: This Client Side Page Guard (Route Protection Strategy) is shown for educational purposes but in the next chapter we learn of the better server-side approach that prevents a brief flashing while the authentication logic runs.

So of course if we are not logged in you shouldn’t be able to see a profile page and if we were on that page and then we click “log out” we should be redirected.

Implementing this is rather straightforward with “next-auth.” All we have to do is go to the pages which we want to protect or restrict in some way, like “profile.js” page, and also use some “next-auth” functionality either directly in the page or in a component of that page, like “user-profile.js”.

Here we want to redirect away if we’re not authenticated. To implement this, in the “user-profile.js” file, import “useSession” from “next-auth/client” to get access to your session data. Just inside the component’s main function:

const [session, loading] = useSession()

if (loading) {
  return <p className={classes.profile}>Loading...</p>
}

And now you will see loading and, at this point, it never changes and indeed that is how “useSession” behaves, at least at the moment. So “useSession” returns the session and loading but there is this scenario in which you load a page, but loading stays true and it does not update. Note that the author is not sure if this is a bug or a feature but it is what it is and thankfully there is a way around this.

Besides “useSession” there is “getSession” and the difference is that with “useSession” we immediately get the session and loading and then both session and loading could change if session data was fetched. If we have no session because we’re logged out session will never change and, as we see, loading unfortunately also does not change but instead it stays true.

The “getSession” function works differently. It sends a new request and gets the latest session data and then we can react to the answer, tp the response for that request, and that allows us to manage our own loading state while we’re getting this session and act accordingly. It’s a bit more cumbersome, requires more work from our end, but it is the way around the issue we have with loading not updating with “useSession.”

It’s not too difficult to implement. We just need to manage our own state with “useState” so import this and set “isLoading” and “setIsLoading” with “useState” having an initial value set to true. We can do the same thing for “loadedSession” and “setLoadedSession” with no initial value.

Now we can import and use the “useEffect” hook, with an empty dependency array, to get the session when this component is rendered. Inside the useEffect function call and execute “getSession” which will then return a promise so use .then().catch() or async/await.

Note: If we used “async/await” here we would need to wrap in an extra function since we shouldn’t make “useEffect” an async function. So we just use “.then().catch()” here instead so it’s easier.

Inside the .then() create an arrow function that has “session” as a parameter which we will later receive as an argument, (this may be null if we don’t have a session). In the arrow function’s body call “setLoadedSession(session)” and “setIsLoading(false).”

import {getSession} for "next-auth/client'
import {useEffect, useState} from 'react'

function UserProfile() {
  const [isLoading, setIsLoading] = useState(false)
  const [loadedSession, setLoadedSession] = useState()

  useEffect(() => {
    getSession().then((session) => {
      setLoadedSession(session)
      setIsLoading(false)
    })
  }, [])
}

Now we can remove the “useSession” line and import since we are no longer using this, and swapped out for “getSession.”

Now what is the benefit of this approach? Well now we can use our own “isLoading” state to show the loading message and we can now react to “isLoading” being false and us not having a session. We can redirect away then. And actually if that’s our goal instead of managing that extra session state if we don’t need the session for anything else then navigating away then we can delete “setLoadedSession(session)” from the code above and replace it with:

if (!session) {
  window.location.href = '/auth'
}

Move the “setIsLoading(false)” line to above the “if check” logic. We will move into an else block in a second.

Now if we don’t find a session we navigate away with this code. Otherwise, eventually loading will be false and we do have a session and we render the content.

If we reload the page loads and if you log out you are redirected. But we see a flash for a brief moment and the reason for that is Max is setting “setIsLoading” to false in the “getSession” no matter what. So cut this line and put it in an “else” block so if we do have a session we stay on the page and only then do you want to set is loading to false. Code should now look like this:

function UserProfile() {
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    getSession().then((session) => {
      if (!session) {
        window.location.href = '/auth'
      } else {
        setIsLoading(false)
      }
    })
  }, [])

Ch. 225 – Adding Server Side Page Guards (And when to use which approach):

So we are now protecting the profile page in that the user is redirected away if they are not authenticated. Also redirected if an authenticated user clicks the logout button. But there’s still a brief moment we see “loading…” when we enter the “/profile” URL directly in the browser address bar when not authenticated and this is something we want to get rid of.

We can’t really get rid of it with just client side code. Because if we visit this page and then use client side JavaScript code to determine whether we are authenticated or not, then we will always need to wait that fraction of a second to find out if we are or not.

But we must not forget that Next JS blends server-side and client-side code. So we can use server-side code to determine whether the user that sent the request is authenticated or not and return different page content and possible a redirect if the user is not authenticated.

For this we need to go to the “profile” page and add an extra function. The question now is just which function?

We could add the “getStaticProps” function, (this would allow us to fetch data in advance). But this is wrong the wrong function for us. Why? Because “getStaticProps” runs during build time (yes revalidate could be set, to run thereafter, but it mainly runs during build time) and absolutely does not run for every incoming request.

Here in this case every incoming request matters because we need to find out if the user is logged in or not. So we should use “getServerSideProps.” That will then get a “context” object where we get access to the incoming request. The good thing about “getSession” from “next-auth/client,” which we are using in the “user-profile” component, is that you’re not limited to using it on client-side code only.

You can also use “getSession” on the server-side, even though it is imported from “next-auth/client.” So we can copy the “getSession” import and paste it into “profile.js” and in a “getServerSideProps” function we can also call “getSession” and actually pass in an object where we set a request key to the incoming request {req: context.req}.

Don’t forget that we have access to the request object through “context” when using “getServerSideProps.” And we pass this request which we get here into the “getSession” function through the “req” key in the configuration object. Then “getSession” will automatically look into that request and extract the data it needs, the session token cookie to be exact, and see if that is valid, and if that user is authenticated, and to see if the cookie even exists to begin with.

And this will all happen behind the scenes. Convert “getServerSideProps” function to be an async function and then await “getSession,” since it will return a promise, and set it in a constant variable named “session.” And this will be null if user is not authenticated and it will be a valid session object if the user is authenticated.

Check if not session, (!session), so if it’s null and we don’t have a session, in that case return an object where we set redirect key to an object where you redirect the user. As you learned earlier, the object you return in GSP or GSSP, you typically set props but you are not limited to just props. Could also set a “notFound” key to true to show the 404 page. Or set a “redirect” key to an object where you describe the destination redirect you want to issue. This will redirect the user to a different page without the unwanted flashing. Give this “redirect” an object with a “destination” key and set a value of “/auth.” Also add to “redirect” a key of “permanent” and set to false (to indicate if this is permanent or temporary). Permanent here because the user is not logged in.

export async function getServerSideProps(context) {
  const session = await getSession({req: context.req})

  if (!session) {
    return {
      redirect: {
        destination: "/auth",
        permanent: false
      }
    }
  }

  return {
    props: {session},
  }
}

Note that if we don’t make it into the “if block”, we know there is a session, and we can set return to props: {session} to provide the session.

We are now using “getServerSideProps” for server-side authorization protection (page-guards).

In the “user-profile.js” file we can now get rid of our client-side code and imports, or comment it out, because this “<UserProfile />” component will only be rendered if that page renders, and that page will only render if we are authenticated because of our “getServerSideProps” logic.

Using “getServerSideProps” for server-side page-guarding (authorization protection) is the more elegant way of handling (Compared with client-side page-guarding). If a user is not logged in and visit the “/profile” page they do not see any flashing and are instantly redirected. If the user is logged in with valid credentials, you can still go to profile page, again with no flashing.

Ch 226 – Protecting the “auth” Page:

We want to redirect the user from the “auth” page after login when sign-in is succeeded. We want to leverage an “if check” that we don’t have an error on “signIn.” We could use “window.location.href” but this resets the entire application and you would lose all of your state. This approach is fine for an initial page load but if we already have application going on you don’t want to do this. So instead import and leverage useRouter from “next/router.”

// in "auth-form.js"

const router = useRouter()

if (!result.error) {
  router.replace('/profile')
  // instead of window.location.href
}

We also want to make sure a user cannot visit the “auth” page when the user is already logged in. For this we can use “getServerSideProps” to protect the page as we just learned. Alternatively we will use the client-side workaround for practice. We can do this with useEffect, getSession, and useRouter.

const router = useRouter()

useEffect(() => {
  getSession().then(session => {
    if (session) {
      router.replace('/')
    }
  })
}, [router])

We don’t want to show the “<AuthForm />” component while NextJS is still figuring out if the user is authenticated or not so also bring in useState.

function AuthPage() {
  const [isLoading, setIsLoading] = useState(true)
  const router = useRouter()

  useEffect(() => {
    getSession().then(session => {
      if (session) {
        router.replace('/')
      } else {
        setIsLoading(false)
      }
    })
  }, [router])

  if (isLoading) {return <p>Loading...</p>}

  return <AuthForm />
}

export default AuthPage

If we visit “/auth” we see loading briefly (due to this client-side page-guard approach) and then you are re-directed. As mentioned earlier, you can use “getServerSideProps” with “getSession” to implement the server-side page guard approach to avoid the flashing.

Ch. 227 – Using the “next-auth” Session Provider Component:

The “next-auth” Session Provider Component is an optimization to consider implementing, especially for a bigger application.

At the moment we are using this session functionality in a couple of different components, (“main-navigation.js”). The main problem we have with this is if a user is authenticated and reloads while on the profile page we see if user is authorized or not using the “getServerSideProps” function in “profile.js,” and then after the page is loaded, with the useSession hook, we use in “main-navigation.js” will then also run some logic to find out if we have a session or not.

We can see this in the network tab in browser tool we see there is a request being sent to “/api/auth.session.” This is sent by the useSession hook to see if the cookie is valid. There is nothing wrong with this check it’s just that it is a redundant request since we already checked our session in “profiles.js.” We can avoid this duplication by using an extra component offered by “next-auth/client,” the special Provider component.

In the “_app.js” file, that wraps all page components, we can import {Provider} from “next-auth/client.” This is a wrapper which we can wrap around all of our other components. On the Provider component we can then set a “session” prop and set it to any session data which you already have.

In “_app.js,” in the root component (MyApp), we don’t just get the page components that should be loaded but also the props provided for that page. These are the props provided by “getStaticProps” or “getServerSideProps.” At least for our “profile” page these props will contain a session key with the session we validated inside of “getServerSideProps.”

So we are setting this session prop on the profile page and we are setting it to this session which we already loaded and validated with that server-side code. So not all of our pages, but some pages, in this case the “priofile.js” page, will have a session prop with session data. So we can set the “session” prop provided to the Provider component to “pageProps.session.”

In most cases this will be “undefined” because most pages don’t have this prop. But our profile page does have it, so the session is already set, and this allows “next-auth” to skip the extra session check that is a redundant request. Preventing unnecessary http requests is never a bad thing.

If we loaded another component where this is not set, and “session” is “undefined,” the useSession hook will still do its thing and check it manually.

Using the session Provider component is a recommended pattern.

import {Provider} from "next-auth/client"
// other imports

function MyApp({Component, pageProps}) {
  return (
    <Provider session={pageProps.session}>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </Provider>
  )
}

Ch. 228 – Analyzing Further Authentication Requirements

To this point one main feature is missing, one main reason for adding authentication. Client-side page protection is nice and good but what really matters is what a user can do as it relates to API routes. When a user hits API routes which triggers certain operations, like create and delete products for example, we need to know if they are authorized to do that. Requests can also come from the command line or an application like postman, so we need protection on the API (back-end/server-side) as well.

Ch. 229 – Protecting API Routes:

Protect API routes not only on front-end but on the back-end (for example: to change password).

An example is letting a user change passwords. Add a new folder in “api/user” folder, to prove it doesn’t need to be in “api/auth” folder, and add a “change-password.js” file inside. We can reach this API route by sending a request to “/api/user/change-password.”

In this file we are not going to use “NextAuth” as we used in “[…nextauth].js” file. Instead we will create a handler function which gets a request and a response and we will export this handler function as a default. In the handler function we want to extract the old and new password, verify request is coming from an authenticated user and deny further action if it’s not, get email, look into the database, see if the old password matches the current password and, if so, replace the old password with the new password.

Check if incoming request has the right method (POST, PUT, or PATCH could all be valid) but we are using PATCH.

function handler (req, res) {
  if (req.method !== PATCH) {
    return // to not continue at all
  }
}

export default handler

import getSession from next-auth.client and store session in a constant by calling getSession({req: req}). Turn handler into async function and await getSession.

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

async function handler (req, res) {
  if (req.method !== PATCH) {
    return // to not continue at all
  }

  const session = await getSession({req: req})

  if (!session) {
    res.status(401).json({message: "Not authenticated."})
    return // to stop execution
  }
}

export default handler

This “session and if !session” code is the key code to restrict API routes.

Ch. 230 – Adding the Change Password Logic:

Now that we have the protection code in place, the next step is to extract data from the incoming data (old and new password and email). Fortunately, we encode email in the token in the “[…nextauth].js” file so it is in the token and ends up on the session we get. Access by:

Import “connectToDatabase” from “lib/db.js” and call. Await this and store result in a constant variable named “client.” Once we have this “client,” reach out to the database using client.db().collection(‘users’) and store in “usersCollection” constant variable.

Leverage “usersCollection” to “findOne({email: useEmail})” and await this since “findOne()” returns a promise, storing the result in a “user” contant variable. Have a conditional “if (!user) {res.status(404).json({message: “user not found”})” and “client.close()” and “return” (to stop execution)}

Set a constant of “currentPassword” to “user.password.”

Import and call the “verifyPassword” function passing it “oldPassword and currentPassword” as arguments. Await this and store in a constant variable named “passwordsAreEqual.” Set logic that if (!passwordsAreEqual) {res.status(403).json({message: “Invalid Password.”}) and client.close() and “return” to stop execution}

We can add update with the code of usersCollection.updateOne({email: userEmail}, {$set: {password: newPassword}}) but we need to hash newPassword first.

Import from the “lib” folder and above the “updateOne()” line create a constant variable named “hashedPassword” and set to await “hashPassword(newPassword)” and change “newPassword” in the “$set” key to “hashedPassword.” Await “updateOne()” action and store in a constant variable named “result.”

You can add error handling with “try/catch” but ignore this for now and close database and send back a status code of success and message of password updated.

// in "change-password.js" under if checks

const userEmail = session.user.email
const oldPassword = req.body.oldPassword
const newPassword = req.body.newPassword

const client = await connectToDatabase()

const usersCollection = client.db().collection('users')

const user = await usersCollection.findOne({email: userEmail})

if (!user) {
  res.status(404).json({message: "User not found."})
  client.close()
  return // stops execution
}

currentPassword = user.password

const passwordsAreEqual = await verifyPassword(oldPassword, currentPassword)

if (!passwordsAreEqual) {
  res.status(403).json({message:"Invalid password."})
  client.close()
  return
}

const hashedPassword = await hashPassword(newPassword)

const result = await usersCollection.updateOne({user: userEmail}, {$set: {password: hashedPassword}})

client.close()
res.status(200).json({message: "Password updated."})

Ch. 231 – Sending “Change Password Request’ from the Front-End:

In the “profile-form.js” file we want to handle form submission, extract entered values, and either 1.) send request or 2.) send extracted data to a parent component “user-profile.js” so the request can be sent from there. We will implement the second, “send to parent,” option.

Create a “submitHandler” function that takes in the event and, in the function block, prevent default. Add an “onSubmit” prop to the form that points to the “submitHandler” function.

For extracting the current values we could 1.) use useState and listen to changes to these inputs with every keystroke with onChange event or 2.) leverage useRefs to extract the values just when we need them. Here we will take useRef approach.

Import useRef from ‘react.’ Set up reference for “oldPasswordRef” set equal to useRef() and “newPasswordRef” set equal to useRef() and connect to the inputs by adding ref={oldPassword} and do the same for the “newPassword” input. Now we can use this in the “submitHandler” by creating new constant variables there “enteredOldPassword = oldPasswordRef.current.value” and the same for “enteredNewPassword.”

We could add validation here but skipping for now.

We could send request here but sending via parent so pass props up to the “ProfileForm” component which lives in the “user-profile.js” file. Start by adding props to profileForm component function. Then under the varibales set to “.current.value” add:

const enteredOldPassword = oldPasswordRef.current.value
const enteredNewPassword = newPasswordRef.current.value 

props.onChangePassword({
  oldPassword: enteredOldPassword,
  newPassword: enteredNewPassword
})

These property keys need to be written like this because this is what the back-end API is expecting.

Now we need to set the “onChangePassword” prop on the “ProfileForm” component when we use it, and so in the “user-profile.js” file, find the self-closing “ProfileForm” component in the return JSX area and add an “onChangePassword={changePasswordHandler}” prop, pointing to a function we create now just above the JSX.

In the “changePasswordHandler” function take in “passwordData” and then in function block send the http request. We do this with fetch sending to “/api/user/change-password” and configuring the request to be a PATCH request and set body: JSON.stringify(passwordData) and set header as usual.

Convert “changePasswordHandler” to an async function and await fetch request and store in constant variable named response. Underneath this, create a constant variable named data set to await response.json() and then log this data to the console.

// In user-profile.js in the ProfileForm main component function

async function changePasswordHandler() {
  const response = await fetch('/api/user/change-password', {
    method: 'PATCH',
    body: JSON.stringify(passwordData), // Make sure field names match
    headers: {'Content-Type': 'application/json'}
  }) 

  const data = await response.json()

  console.log(data)
}

return (
  <section className={classes.profile}>
    <h1>Your User Profile</h1>
    <ProfileForm onChangePassword={changePasswordHandler} />
  </section>
)

Test that this works for both a valid and invalid current password.

Ch. 232 – Module Summary & Final Steps

NEXTAUTH_URL environment variable needed when deploying to production, can inject from the hosting environment dashboard. Set to the domain in which you are going to host your application in production.

NEXTAUTH_URL=https://example.com

Remember their are lots of Providers besides the “credentials” provider we used to manage our own users with email/password or whatever your credentials are. You can use third-party providers like Apple, Amazon, Facebook, Google, Github, etc. Check out the “next-auth” docs to see how to configure. Also remember you can have multiple log-in alternatives since in your “[…nextauth].js” file, “providers” is an array.

Ch. 233 – Module Resources