# Authenticate your web app using AWS Cognito

In this article, you will find out how to integrate AWS Cognito into NextJs and understand the different authentication types that Cognito supports.

I had a use case where I wanted to integrate Cognito into a web app. For reasons I will explain later, I needed to use the OAuth `Authorization Code Grant`. However, when reading the documentation, I found it difficult to understand how to integrate Cognito into the web app. Hence I decided to write this article.

Before you start, **you will need an AWS account**.

# Who is this article for?

This article is for anyone who wants to integrate with Cognito for authentication, or is simply curious, but doesn't know where to start.

I have implemented this in NextJs because that's what I'm familiar with, however, there is no reason you can't implement the whole process in any other language or framework.

# What is AWS Cognito?

Cognito is a managed AWS service that provides authentication, authorization and user management for your web and mobile apps.

# Why would you want to use Cognito?

Essentially, Cognito will allow you to quickly implement authentication into your application without needing to spend time building it yourself. Cognito is great if you just want to focus on implementing user features in your application. Perhaps you are building a prototype application, and want to get the application running as quickly as possible to see if it is viable.

# What is OAuth?

OAuth (Open Authentication) is an open standard for authentication that apps can use to provide client applications with 'secure delegated access'. The simplest example of this would be any website that lets you log in with Google or Github, rather than logging into the application directly.

I won't explain OAuth in too much detail because there are already many articles about this. I found this article titled ['What the Hack is OAuth?`](https://developer.okta.com/blog/2017/06/21/what-the-heck-is-oauth) pretty helpful.

## Authorization code grant vs Implicit grant

### Implicit grant

The implicit grant is the easiest to implement. Using the implicit grant will return the access token directly to the client (browser or mobile app), which is recommended for a single-page application without any backend. 

The main downside to implicit grant is security. As I mentioned above, the access token is returned to the client, usually through a URL parameter. This means there is a risk that the access token could be taken via an XSS attack or even just server logs.

### Authorization code grant

The authorization code grant is similar to the implicit grant, however, it adds a few additional steps. Instead of the authorization code being returned directly to the browser, a code is returned. The code can then be sent to the client backend and using the `client_id` and `client_secret` can be exchanged for the access token.

The benefit to this approach is although the code could be intercepted, only the client backend can use it to get the access token because the `client_secret` is required.

# What is the hosted UI?

The hosted UI provided by Cognito is as it sounds, an out-of-the-box user interface with allows a user to do any authentication-related tasks,  such as logging in, creating an account etc. 

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664805519341/QRe19GPLs.png align="left")

The hosted UI is the fasted way to get started with authentication for your application. Additionally, you can [customise Cognito UI](https://medium.com/@andrew.buntsev/aws-cognito-ui-customization-bc39afd5b2f2) to some extent.

Alternatively, you could build the login/register forms directly into the application. But obviously, that's going to take some time. You can find out more by reading the [Cognito documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-ui-customization.html). 

# Setting up Cognito

Before you start, you will need an AWS account to follow this guide. Check out this article by amazon on [how to register an AWS account](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/). 

First, create a new user pool and select your sign-in options. You can select any combination of username, email or password. I have selected email because I only want users to log in with their email.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664806649213/hma4FQ7Nr.png align="left")

For the security requirements page, I will keep everything as default, except for multi-factor authentication. For this article, I have set MFA as optional. This makes it easier to sign up new users without implementing MFA. However, I would recommend making MFA mandatory for production applications. 

You will need to select at least one of the MFA methods. I have chosen only Authenticator apps.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664806755364/cRBo3BWDD.png align="left")

I will not change any of the settings in the Configure sign-up experience page

For message delivery, I suggest selecting 'Send email with Cognito' as this is the easiest.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664806990668/K_yRxcrIR.png align="left")

You must also come up with a pool name. The name doesn't matter too much, just try and keep it consistent. 

Select 'Use the Cognito Hosted UI', and enter a similar Cognito domain name. I have used 'BlogPost' and 'blog-post'.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664807209412/TqBBdOS1v.png align="left")

In the Initial app client section, select 'Confidential client' as the app type. This is essentially the Authorization code grant flow.

The name of the app client doesn't matter too much, so I have called mine NextJs because that is the app that will use it. Then add `http://localhost:3000` and `http://localhost:3000/api/auth` to the allowed callback URLs. This is important so that the Hosted UI can redirect back to the local project.

You will also need to generate a client secret. This is required for the Authorization Code grant flow, but it should be enabled already since you selected 'Confidential client' as the app type.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664828502764/h37hiI6ur.png align="left")

Finally, check all of the options in the review step and click create.

# Setting up NextJs

## Scaffold the NextJS app

```bash
npx create-next-app@latest
# or
yarn create next-app
```

## Install Dependencies

```
npm install aws-jwt-verify cookies-next
#  or
yarn add aws-jwt-verify cookies-next
```

## Cleanup the homepage
Replace the homepage with the following.

`./pages/index.js`
```javascript
export default function Home() {
  return (
    <div>
      <h1>Protected Homepage</h1>

      <div>
        <button>Logout</button>
      </div>
    </div>
  );
}
```

## Add the login page
Create the file `./pages/login.js`
```javascript
export default function Login() {
  return (
    <div>
      <h1>Login Page</h1>

      <p>
        <a href="">Login</a>
      </p>
    </div>
  );
}

```

## Add and populate the environment file

Create the file `.env.local`:
```env
CLIENT_SECRET=

NEXT_PUBLIC_CLIENT_ID=
NEXT_PUBLIC_USER_POOL_ID=
NEXT_PUBLIC_REGION=
NEXT_PUBLIC_DOMAIN=http://localhost:3000
NEXT_PUBLIC_COGNITO_URI=
```

The region will be whatever region you have already selected in the AWS console.

The User pool Id can be found in the overview section.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664830517811/IwMQKXKMq.png align="left")

Next, open the 'App integration' tab, and scroll to the bottom of the page.


![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664830567826/DSnIeam3G.png align="left")

You can grab your clientId here. Navigate to your app client.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664830613716/7dM-H69rF.png align="left")

You can view the client secret after clicking 'show client secret'

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664830683873/vxvJvweqD.png align="left")

If you then scroll down, you can view the hosted UI.


![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664830716257/lhsYdw7xi.png align="left")

Copy the domain of the URL, this is the `COGNITO_URL` variable. For example, mine is `https://blog-post.auth.eu-west-2.amazoncognito.com/`.

# Adding login functionality

Now that everything is set up, we can get started integrating with the login page.

Add the following line to the login page component. 

```javascript
  const loginUrl = `${process.env.NEXT_PUBLIC_COGNITO_URI}/login?client_id=${process.env.NEXT_PUBLIC_CLIENT_ID}&response_type=code&scope=email+openid+phone&redirect_uri=${process.env.NEXT_PUBLIC_DOMAIN}`;

```

and update the link to navigate to that link.
```html
<a href={loginUrl}>Login</a>
```

If you navigate to the link, you will be presented with the Cognito hosted UI. Once you successfully log in, you will be redirected back to the provided redirect_uri. 

## Testing the login page

Now, run the following command to start nextJs:

```bash
npm run dev
# or
yarn dev
```

Once the terminal is running, visit `http://localhost:3000/login` in your browser. You should see a login page with a button.


![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664994869091/vZW-Xm2Ef.png align="left")

Next, click on the Login button. You will be redirected to the URL of the Hosted UI. You will need to sign up. 

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664995006524/TVcwPycXH.png align="left")

Once you have signed up, you should have been redirected to the homepage, since the redirectUrl was `http://localhost:3000`. Notice that in the URL, there is now a code parameter. We will need to use this code to get the access token.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664995136233/X_66xQpUT.png align="left")

## Fetching the token

Before we start, update the file `pages/login.js` and replace the login URL with the following:
```javascript
  const loginUrl = `${process.env.NEXT_PUBLIC_COGNITO_URI}/login?client_id=${process.env.NEXT_PUBLIC_CLIENT_ID}&response_type=code&scope=email+openid+phone&redirect_uri=${process.env.NEXT_PUBLIC_DOMAIN}/api/auth`;
```

Instead of the hosted UI redirecting back to the homepage, we can redirect to an API route `/api/auth`, and then use the code to get the access token.

Create the file `pages/api/auth.js` with the following code.

```javascript
import { setCookie as nextSetCookie } from "cookies-next";

const COOKIE_NAME = "ACCESS_TOKEN";
const REDIRECT_URL = process.env.NEXT_PUBLIC_DOMAIN;

export default function handler(req, res) {
  if (req.method !== "GET") return;

  const { code } = req.query;

  return new Promise((resolve) => {
    verifyToken(code)
      .then((body) => {
        const { access_token } = JSON.parse(body);

        nextSetCookie(COOKIE_NAME, access_token, {
          req,
          res,
          path: "/",
          sameSite: "lax",
          httpOnly: true,
        });

        res.writeHead(302, {
          Location: REDIRECT_URL,
        });

        res.end();
        resolve();
      })
      .catch((error) => {
        res.status(500).end();
        resolve();
      });
  });
}

const verifyToken = (code) => {
  const clientId = process.env.NEXT_PUBLIC_CLIENT_ID;
  const clientSecret = process.env.CLIENT_SECRET;
  const authEndpoint = `${process.env.NEXT_PUBLIC_DOMAIN}/api/auth`;

  var myHeaders = new Headers();
  myHeaders.append("Content-Type", "application/x-www-form-urlencoded");

  var urlencoded = new URLSearchParams();
  urlencoded.append("grant_type", "authorization_code");
  urlencoded.append("code", code);
  urlencoded.append("client_id", clientId);
  urlencoded.append("redirect_uri", authEndpoint);
  urlencoded.append("client_secret", clientSecret);

  var requestOptions = {
    method: "POST",
    headers: myHeaders,
    body: urlencoded,
    redirect: "follow",
  };

  return new Promise((resolve) => {
    fetch(`${process.env.NEXT_PUBLIC_COGNITO_URI}/token`, requestOptions)
      .then((response) => response.text())
      .then((response) => {
        resolve(response);
      })
      .catch((error) => {
        reject(error);
      });
  });
};

```

Essentially, this endpoint is getting the code, and sending a request to the Cognito token endpoint. The Cognito endpoint then returns an access token, we can then set it as an HTTP cookie.

You can now view the token by navigating to Chrome Dev tools, `Application`, `Cookies`, `http://localhost:3000` then `ACCESS_TOKEN`.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1664997669940/kbO2ehg1F.png align="left")

Here is a link to [read more about the Cognito token endpoint](https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html).

## Cleanup

You can also delete `pages/api/hello.js`.

# Adding protected routes

Now that we have the login mechanism working, we need some way to check if the user is authenticated. 

The authentication will need to be server-side. So add the following to the `pages/_app.js` file.

```javascript
import "../styles/globals.css";

import Router from "next/router";
import { CognitoJwtVerifier } from "aws-jwt-verify";
import { getCookie as nextGetCookie } from "cookies-next";

const COOKIE_NAME = "ACCESS_TOKEN";

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

MyApp.getInitialProps = async ({ ctx, router }) => {
  if (!!ctx.req) {
    // Server side check
    if (router.pathname === "/login") return {};
  } else {
    // Client side check
    if (ctx.pathname === "/login") return {};
  }

  if (!(await isAuthorised(ctx))) {
    redirect(ctx.req, ctx.res, "/login");
    return {};
  }

  return {};
};

const isAuthorised = async ({ req, res }) => {
  const token = nextGetCookie(COOKIE_NAME, { req, res });

  if (!token) {
    return null;
  }

  const verifier = createVerifier();
  const payload = await tryParseToken(token, verifier);

  return payload;
};

const tryParseToken = async (token, verifier) => {
  try {
    const payload = await verifier.verify(token);

    return payload;
  } catch {
    return null;
  }
};

const createVerifier = () => {
  return CognitoJwtVerifier.create({
    userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID,
    tokenUse: "access",
    clientId: process.env.NEXT_PUBLIC_CLIENT_ID,
  });
};

const redirect = (req, res, path) => {
  if (!req) {
    Router.push("/login");
    return {};
  }

  res.writeHead(302, {
    Location: path,
  });
  res.end();
};

export default MyApp;
```

In this file, were using the NextJs [`getInitialProps`](https://nextjs.org/docs/api-reference/data-fetching/get-initial-props) hook to check if the token is valid. I have used the library `aws-jwt-verify` to make this process easier. If the token is invalid, the user will be redirected to the login page.

Try this out locally. Check that you are redirected to the login page, log in and then delete the token to test if you are redirected to the login page again.

# Using refresh tokens

Refresh tokens are returned when the user is first authenticated alongside the access token. When the access token expires, you can make a request to the Cognito refresh endpoint, pass the clientId and clientSecret, and get a new access token.

The idea behind refresh tokens is you give access tokens a much shorter lifespan, let's say 5 mins to an hour. However, you give the refresh tokens a much longer lifespan, such as 1 day to 30 days. Due to the way JWT tokens work, you can't really make them expire, or 'logout a user', without saving a copy of the logged-out token on the server.

Instead, you can revoke the refresh token to log out. The access token will still be valid, but hopefully not for long if you have set a short lifespan. But then when you try to refresh the token, you won't be able to.

## Updating the token lifespan

Navigate to the edit page of your app client in the AWS console.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1665172289444/7zcxwxgI8.png align="left")

Update the access token expiration to 5 minutes. I don't know what the optimal timespan for an access token is, but let's set it to the minimum for the purpose of the is the article. Doing so should make it easier to view the refresh token being used.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1665172398861/LDSXzpLoY.png align="left")

## Updating the auth API route

First, we need to save the refresh token when authenticating. Navigate to the file `pages/api/auth.js`. Add the following line to the top:

```javascript
const REFRESH_COOKIE_NAME = "REFRESH_TOKEN";
```

And update the `verifyToken` callback to create a new cookie:
```javascript
verifyToken(code)
      .then((body) => {
        const { access_token, refresh_token } = JSON.parse(body);

        nextSetCookie(COOKIE_NAME, access_token, {
          req,
          res,
          path: "/",
          sameSite: "lax",
          httpOnly: true,
        });
        
        nextSetCookie(REFRESH_COOKIE_NAME, refresh_token, {
          req,
          res,
          path: "/",
          sameSite: "lax",
          httpOnly: true,
        });

        ...
      })
```

Next, navigate to the file `pages/_app.js`, and replace it with the following:

```javascript
import "../styles/globals.css";

import Router from "next/router";
import { CognitoJwtVerifier } from "aws-jwt-verify";
import {
  getCookie as nextGetCookie,
  setCookie as nextSetCookie,
} from "cookies-next";

const COOKIE_NAME = "ACCESS_TOKEN";
const REFRESH_COOKIE_NAME = "REFRESH_TOKEN";

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

MyApp.getInitialProps = async ({ ctx, router }) => {
  if (!!ctx.req) {
    // Server side check
    if (router.pathname === "/login") return {};
  } else {
    // Client side check
    if (ctx.pathname === "/login") return {};
  }

  if (!(await isAuthorised(ctx))) {
    redirect(ctx.req, ctx.res, "/login");
    return {};
  }

  return {};
};

const isAuthorised = async ({ req, res }) => {
  const token = nextGetCookie(COOKIE_NAME, { req, res });
  const refreshToken = nextGetCookie(REFRESH_COOKIE_NAME, { req, res });

  if (!token) {
    return null;
  }

  const verifier = createVerifier();
  const payload = await tryParseToken(token, verifier);

  if (payload !== null) return payload;

  if (refreshToken === null) return null;

  const newTokens = await tryRefreshTokens(refreshToken);

  if (newTokens !== null) {
    console.log("Access token refreshed");

    nextSetCookie(COOKIE_NAME, newTokens.access_token, {
      req,
      res,
      path: "/",
      sameSite: "lax",
      httpOnly: true,
    });

    const payload = await tryParseToken(newTokens.access_token, verifier);

    return payload;
  }

  return payload;
};

const tryParseToken = async (token, verifier) => {
  try {
    const payload = await verifier.verify(token);

    return payload;
  } catch {
    return null;
  }
};

const createVerifier = () => {
  return CognitoJwtVerifier.create({
    userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID,
    tokenUse: "access",
    clientId: process.env.NEXT_PUBLIC_CLIENT_ID,
  });
};

const redirect = (req, res, path) => {
  if (!req) {
    Router.push("/login");
    return {};
  }

  res.writeHead(302, {
    Location: path,
  });
  res.end();
};

const tryRefreshTokens = async (refresh_token) => {
  const refreshUrl = `${process.env.NEXT_PUBLIC_COGNITO_URI}/oauth2/token`;
  const clientId = process.env.NEXT_PUBLIC_CLIENT_ID;
  const redirectUri = process.env.NEXT_PUBLIC_DOMAIN;
  const secret = process.env.CLIENT_SECRET;

  return new Promise(async (resolve) => {
    const res = await fetch(refreshUrl, {
      method: "POST",
      headers: new Headers({
        "content-type": "application/x-www-form-urlencoded",
      }),
      body: Object.entries({
        grant_type: "refresh_token",
        client_id: clientId,
        redirect_uri: redirectUri,
        refresh_token: refresh_token,
        client_secret: secret,
      })
        .map(([k, v]) => `${k}=${v}`)
        .join("&"),
    });

    if (!res.ok) {
      resolve(null);
      return;
    }

    const newTokens = await res.json();

    resolve(newTokens);
  });
};

export default MyApp;
```

To test that the code is working, log in, wait five minutes, and refresh the page. You should see a log message containing `Access token refreshed` to indicate that the access token has been refreshed


# Revoking the refresh token

As mentioned above, technically you cannot 'log out' when using JWT because there is no session. However, if you set the token lifespan very short, and revoke the refresh token (prevent a new access token from being generated), it will do the job.

## Implementing the logout button

Update the file `pages/index.js` with the following.

```javascript
import Router from "next/router";

export default function Home() {
  const handleLogout = () => {
    const url = `/api/revoke`;

    var requestOptions = {
      method: "GET",
      headers: [],
      redirect: "follow",
    };

    fetch(url, requestOptions)
      .then((response) => response.text())
      .then(() => {
        Router.push("/login");
      })
      .catch((error) => console.log("error", error));
  };

  return (
    <div>
      <h1>Protected Homepage</h1>

      <div>
        <button onClick={handleLogout}>Logout</button>
      </div>
    </div>
  );
}
```

The method will make a request to the API route `/api/revoke`, which we will implement next.

## Add API route

Next, create the file `pages/api/revoke.js`

```javascript
import { CognitoJwtVerifier } from "aws-jwt-verify";
import {
  getCookie as nextGetCookie,
  setCookie as nextSetCookie,
  deleteCookie as nextDeleteCookie,
} from "cookies-next";

const COOKIE_NAME = "ACCESS_TOKEN";
const REFRESH_COOKIE_NAME = "REFRESH_TOKEN";

export default function handler(req, res) {
  if (req.method !== "GET") return;

  return new Promise((resolve, reject) => {
    const refreshToken = nextGetCookie(REFRESH_COOKIE_NAME, { req, res });

    revokeToken(refreshToken)
      .then(() => {
        // nextDeleteCookie(COOKIE_NAME, {
        //   req,
        //   res,
        //   path: "/",
        //   sameSite: "lax",
        //   httpOnly: true,
        // });

        // nextDeleteCookie(REFRESH_COOKIE_NAME, {
        //   req,
        //   res,
        //   path: "/",
        //   sameSite: "lax",
        //   httpOnly: true,
        // });

        res.status(200).send();
        resolve();
      })
      .catch((error) => {
        res.status(500).end();
        resolve();
      });
  });
}

const revokeToken = (refreshToken) => {
  const clientId = process.env.NEXT_PUBLIC_CLIENT_ID;
  const clientSecret = process.env.CLIENT_SECRET;

  const credentials = `Basic ${Buffer.from(
    clientId + ":" + clientSecret
  ).toString("base64")}`;

  var myHeaders = new Headers();

  myHeaders.append("Authorization", credentials);
  myHeaders.append("Content-Type", "application/x-www-form-urlencoded");

  var urlencoded = new URLSearchParams();
  urlencoded.append("client_id", clientId);
  urlencoded.append("token", refreshToken);

  var requestOptions = {
    method: "POST",
    headers: myHeaders,
    body: urlencoded,
    redirect: "follow",
  };

  return new Promise((resolve, reject) => {
    fetch(
      `${process.env.NEXT_PUBLIC_COGNITO_URI}/oauth2/revoke`,
      requestOptions
    )
      .then((response) => response.text())
      .then(() => {
        resolve();
      })
      .catch((error) => {
        reject(error);
      });
  });
};
```

The code above gets the refresh token, and then calls the revoke endpoint. 

I have commented out the delete cookies part so you can see the token being revoked. Login, click the logout button and then wait 5 minutes. Once the authentication token has expired, you should be logged out, and you won't be able to navigate to the homepage.

Finally, uncomment the code that deletes the cookies. This will ensure that access is revoked locally.

Here is a link to [read more about the Cognito Revoke endpoint](https://docs.aws.amazon.com/cognito/latest/developerguide/revocation-endpoint.html).

# Conclusion

You should now have a practical understanding and a working example of using Cognito to secure a nextJs app. 

Your next steps should be to build something yourself and read through the [Cognito documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html). 
