React | NextJS

14.2 Authentication

Module Still Under Development

# Authentication in NextJS

There are three concepts connected to authentication.

  • Authentication verifies if the user is who they say they are. It requires the user to prove their identity with something they have, such as a username and password.
  • Session Management tracks the user's state (e.g. logged in) across multiple requests.
  • Authorization decides what parts of the application the user is allowed to access.

When you are authenticating users and tracking sessions you are often using cookies to do that. Cookies can be set on either the client or server side. We will primarily try to do this on the server side so that the cookies are set before they get to the browser.

Cookies are set via an HTTP header - set-cookie. On the server-side, the cookies must be set before any content is sent to the browser. On the client-side, you can always read the cookies through document.cookie. If you are setting a cookie on the client-side, then you are doing this so that it will be sent along with the next HTTP Request to the server. The same cookie will come back in the set-cookie header on the next Response, unless removed or changed on the server-side.

In NextJS, on the server-side you can set and read cookies from inside a Server Action (think actions.js) or inside a route.js file, or in the middleware.js file. They can only be set before any streaming of content to the server starts.

For the client-side, in a page.js we will just be reading cookie values like this:

// any page.js
import { cookies } from 'next/headers';

export default function Page() {
  const cookieStore = cookies();
  const theme = cookieStore.get('theme');
  return '...';
}
1
2
3
4
5
6
7
8

In an actions.js Server Action function we can read AND write cookies. The set() and delete() methods do not work in a page.js or a component.

// app/actions.js
'use server';

import { cookies } from 'next/headers';

async function someFunc(data) {
  //non-exported function
  //read a cookie
  cookies().get('thing');
  // setting a basic cookie
  cookies().set('name', 'steve');
  // or setting one with options
  cookies().set({
    name: 'token',
    secure: process.env.NODE_ENV === 'production',
    value: 'kjshdfkjhsdkfhskjfhskdhfsdkhfk',
    httpOnly: true,
    path: '/',
  });
  //redirect or revalidatePath
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

On the client-side, in a use client component, you can use plain vanilla JS and set the cookie with document.cookie="name=steve;httpOnly=true;path=/;". If you need the value from a cookie on a page you can use the import { cookies } from 'next/headers'; in your page.js file. Then you can call the cookies() methods:

// page.js
import SomeComponent from '@/app/components/SomeComponent'; // a client-side component
import { cookies } from 'next/headers'; // SERVER SIDE ONLY

export default function Page() {
  let token = cookies().get('token');
  //returns an object with that name {name:'token', value:'something'}

  return (
    <div>
      <h1>{token?.value}</h1>
      <SomeComponent tk={token} />
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

You can use the token.value as the value anywhere in your page or pass the cookie object or value through props down to a client-side component.

Cookie Tips

You can READ the value of a cookie from the request or response at any time from anywhere in your code (client or server).

cookies().get('name') or request.cookie.get('name') or response.cookie.get('name')

However, to SET or DELETE a cookie can only be done on the server side in a route.js, actions.js, or middleware.js file BEFORE the headers are streamed to the browser and the HTTP response body is started.

# External Authentication

When working with Google Auth, like in your final project api, we redirect the user to the Google login page. We send a redirect_url through the querystring. This way, after the Authentication is completed, Google can send the user to the redirect_url value. This redirect_url will be the home page for your website.

After a successful login, when Google redirects the user, they will also send a JWT token through the querystring.

In your final project you will get that token from the querystring back in the NextJS website. The token can be set as a cookie or saved on the clientside in sessionStorage. To access the token from sessionStorage, it means this can only be done from inside a use client component. If a cookie is set with the token then it can be passed with every request navigation from page to page within the website.

To use the token in calls to your API on the Render site, you will need to place the token in the HTTP Request headers.

const url = 'https://www.example.com/api';
const token = 'khsdkfhsdjfskhfskdhfkjshfh';

fetch(url, {
  method: 'GET',
  headers: {
    accept: 'application/json',
    Authorization: `Bearer ${token}`, //to send the JWT token to the
  },
});
1
2
3
4
5
6
7
8
9
10

You can review the videos about JWT Authentication in Module 5.1 (opens new window).

HTTP Request Body

If you are making a GET request, you cannot add a body in the fetch.

If you are adding a body to a POST, PUT, PATCH, or DELETE, then it can be a FormData object, a JSON string, or a plain string. FormData objects can include File objects.

# Import Reference

With NextJS there is a long list of methods that you will be importing from lots of different packages. So, this is a quick reference list.

//server side imports
import { redirect } from 'next/navigation'; // inside route.js, page.js, and actions.js
import { revalidatePath } from 'next/cache'; // inside route.js and actions.js
import { cookies } from 'next/headers'; // inside page.js, route.js
import { NextResponse } from 'next/server'; //inside actions.js and middleware.js

// used with 'use client' in components
import { useRouter } from 'next/navigation';

// in either page.js or any components
import Link from 'next/link';
1
2
3
4
5
6
7
8
9
10
11

# Middleware

In a Next.js app we can create middleware functions just like with Express. You need to create a middleware.js file and place it at the top of the project, at the same level as the app folder (not inside it).

The middleware.js will have one exported function and an exported config object. The config object contains an array of all the routes which you want to have the middleware function run first.

//middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  //request.nextUrl - same as new URL(request.url)
  if (request.nextUrl.pathname === '/') {
    let response = NextResponse.next();
    //you can get/set/delete cookies or headers or redirect
    return response;
  }
  if (request.nextUrl.pathname === '/secret') {
    let response = NextResponse.next();
    //you can get/set/delete cookies or headers or redirect
    return response;
  }
  if (request.nextUrl.pathname === '/logout') {
    let response = NextResponse.next();
    //you can get/set/delete cookies or headers or redirect
    //maybe redirect based on something;
    return NextResponse.redirect(new URL('/new', request.url));
  }
}

export const config = {
  matcher: ['/', '/logout', '/secret', '/anyone'],
};
//without config the middleware function runs for every page
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

Inside the middleware function you can handle any route that you want. You can examine the request object that is being passed through the middleware function by using request.nextUrl. The request.nextUrl object is the same as turning the url from the request into a new URL object.

With the request.nextUrl you can read the .pathname, .basePath, .cookies, .searchParams, or .headers properties. From those properties you can make decisions about whether you need edit cookies or headers or if you want to do a redirect to a whole new url. newUrl reference (opens new window)

Once you have a middleware.js file and function, a good way to stay organized is through calling server-side functions in your actions.js file.

Here are a couple repos that you can clone and run from different instances of VS Code. One is a server that acts like a fake Google Auth endpoint. The other is the website that manages sessions through JWT-ish tokens.

Fake Google Auth endpoint (opens new window)

Website with Auth (opens new window)

# Extracting User Id From Token

The JWT token is a base-64 string that contains information about the user.

We are saving this token as a cookie in our application. This let's us figure out whether or not the user has logged in. Whether or not the token is valid is determined on the server before any data gets accessed.

On the client-side there will be times when you want to find the actual user id so you can include it as part of a fetch call to the server.

The JavaScript function to convert from a base-64 string back to a basic utf-8 string is atob(). The token is made up of three parts separated by a period. The middle part is the object that includes an id property. The value returned from the atob function is a JSON string. This is just like the result from a fetch to an API. You will need to convert the value from a string into a JS object to get the id value.

export default function Page(){
  const token = cookies().get('token')?.value;
  let json = atob(token.split('.')[1]);
  let user = JSON.parse(json);
  console.log(user.id); // the actual id from the logged in user

  return ...
}
1
2
3
4
5
6
7
8

The user object that you extract from the token will look something like this:

{ "id": "6611d3ef19ce24e6e7ce03bd", "iat": 1712807730, "exp": 1713412530 }
1

Convert that to an object, then you will have the id to pass to any fetch call.

# What to do this week

TODO Things to do before next week.

  • Read all the content from Modules 14.1 and 14.2.
  • Work on your Final Project with your partner.
Last Updated: 5/16/2024, 3:45:15 PM