I have struggled for quite some time with setting up Cognito and integrating it into a web application as an authentication service. The documentation from AWS is not very clear on how to set it up and how the different pieces fit together. This post explains how I did it.

The setup

Client side

1. Create user pool

A user pool is like your users’ table. It will contain user information such as email, username, password, etc. The first step with Cognito is to create a user pool.

AWS has a step-by-step guide on setting up a user pool at Tutorial: create user pool

2. Create an app client

An app client specifies how your app will authenticate with your user pool. You can choose a client → Cognito authentication flow, client → server → Cognito flow, or even a custom flow.

For more detail, refer to this guide: Configuring a user pool app client

In this post, I’ve choosen client → cognito flow(no need to implement a server).

3. Create hosted UI

Hosted UI is AWS’s provided login page, if we don’t want to create a login page, we can use hosted UI. We can make some simple modification to the look of hosted UI by provide our own css file. Here is what we can custom the hosted UI: Customizing the built-in sign-in and sign-up webpages

4. Client’s code

4.1. Redirect to hosted UI

After creating a hosted UI, Cognito will provide an endpoint for us to redirect users. They will go to this endpoint, login(or register) and after a success login will be redirected back to our application. Our app will receive a code parameter that we’ll use to exchange for access token.

4.2. Callback endpoint

After user has successfully login, they will be redirected back to callback endpoint with code parameter. We will use this code to exchange for access_token.

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("client_id", process.env.AWS_COGNITO_APP_CLIENT_ID);
urlencoded.append("code", code);
urlencoded.append("redirect_uri", "https://localhost:1234/callback");

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

const response = await fetch("https://sakura-vinh.auth.ap-northeast-1.amazoncognito.com/oauth2/token", requestOptions)
const body = await response.json();

5. Server side

Now let’s say we have an API server and we need to protect it using Cogito user’s pool. I’ll use express and cognito-express package.

const cognitoExpress = new CognitoExpress({
  region: "ap-northeast-1",
  cognitoUserPoolId: process.env.AWS_COGNITO_USERPOOL_ID,
  tokenUse: "access",
  tokenExpiration: 3_600_000
});

API with authentication

We’ll add a middleware to extract access_token from Authorization header and check it against our user’s pool on Cognito

authenticatedRoute = express.Router();
app.use("/api", authenticatedRoute);
authenticatedRoute.use(function(req, res, next) {
  let accessTokenFromClient = extractToken(req.headers.authorization);
  if (!accessTokenFromClient) return res.status(401).send("Access Token missing from header");
  cognitoExpress.validate(accessTokenFromClient, function(err, response) {
    if (err) return res.status(401).send(err);
    res.locals.user = response;
    next();
  });
});

const extractToken = (authHeader) => {
  if (authHeader.startsWith("Bearer ")) {
    token = authHeader.substring(7, authHeader.length);
    return token;
  } else {
    throw Error("invalid header");
  }
}

after that we can put an API endpoint behind the middleware

authenticatedRoute.get("/hello", function(_req, res, _next) {
  res.send(`Hi ${res.locals.user.username}, your API call is authenticated!`);
});

6. Cognito’s identity pool

One interesting feature of Cognito is that we can allow our users to have temporary access to AWS. For example, an authenticated user can call a Lambda function from the front-end.

To do this, we need to create an identity pool (also known as Federated Identities). With an identity pool, we can use Cognito’s user pool as an identity source, or we can use other OAuth services such as Facebook, SAML, etc.

I’ll use my user pool as the identity source.

  1. Create an identity pool.
  2. Create a role with appropriate permissions.
  3. Client code.

From 4.2 above we can get access_token, id_token and refresh_token from Cognito. We will use this id_token to exchange for an Identity ID using GetID command

import { CognitoIdentityClient, GetIdCommand, GetCredentialsForIdentityCommand } from "@aws-sdk/client-cognito-identity";
const client = new CognitoIdentityClient({ region: process.env.AWS_DEFAULT_REGION });
const logins = {
  [`cognito-idp.ap-northeast-1.amazonaws.com/${process.env.AWS_COGNITO_USERPOOL_ID}`]: info.id_token
}
const getIdParams = {
  IdentityPoolId: process.env.AWS_COGNITO_IDENTITY_POOL_ID,
  Logins: logins
}
const command = new GetIdCommand(getIdParams);
const getIdResponse = await client.send(command);

Then using Identity ID to exchange for AWS’s access_token by calling GetCredentialsForIdentity

const getCredsParams = {
  IdentityId: getIdResponse.IdentityId,
  Logins: logins
}
const credCommand = new GetCredentialsForIdentityCommand(getCredsParams);
const credsResp = await client.send(credCommand);
return credsResp.Credentials;

We can use this Credentials to call other AWS service, such as

const getSTSInfo = async (credential) => {
  const { AccessKeyId, Expiration, SecretKey, SessionToken } = credential;
  const credentials = {
    accessKeyId: AccessKeyId,
    expiration: Expiration,
    secretAccessKey: SecretKey,
    sessionToken: SessionToken
  }
  const client = new STSClient({ credentials, region: process.env.AWS_DEFAULT_REGION });
  const command = new GetCallerIdentityCommand({});
  try {
    const response = await client.send(command);
    console.log(response);
  } catch (error) {
    console.log(error);
  }
}