# The lion's den: NestJS and authentication with AWS Cognito

> Read how we complete the full stack circle and integrate AWS Cognito authentication with NestJS.

- Date: 2024-06-10T00:00:00.000Z
- Authors: Yuri Mikhin, Travis Turner
- Categories: 
- URL: https://evilmartians.com/chronicles/the-lions-den-nest-js-and-authentication-with-aws-cognito

---

I’ve heard of many cases where developers (specifically, those with Node.js experience) have reported that NestJS is very much a stable and solid solution ready for enterprise-level production. But then, what if we want to add AWS Cognito to the mix? Let's cover the lion’s share of the steps you’ll need to take to make it all work.

In [a previous article](https://evilmartians.com/chronicles/how-to-manifest-streamlined-authentication-aws-cognito-in-a-react-app), we demonstrated how to streamline authentication with React and AWS Cognito.

Read that post here:
*Read also Evil Martians' article*: https://evilmartians.com/chronicles/how-to-manifest-streamlined-authentication-aws-cognito-in-a-react-app

That can be tricky, but doable. However, our frontend team wanted to stretch their paws and show that we're also down for full stack projects. This article has its origins in our real experiences. And luckily, for those developers who want to complete the full-stack circle, integrating AWS Cognito Authentication into a NestJS TypeScript application is quite straightforward by comparison.

If you've got to integrate AWS Cognito with NestJS, following this journey will be of particular value to you.

NestJS will give us a solid foundation for implementing AWS Cognito, and we'll show how NestJS applications can be further fortified against unauthorized access, enhancing security (and maintaining user trust).

---

*Hire us to build a fullstack JS/TS application* [Contact Evil Martians](https://evilmartians.com/contact-us)

---

By the end of the road, you'll have a secure authentication system configured and ready, and a full stack application that's ready to roll.

## Kickstarting NestJS Integration with AWS Cognito

We begin by defining some essential environment variables:

```bash
COGNITO_CLIENT_ID=exampleClientIdABC123
COGNITO_USER_POOL_ID=us-fake-1_abCDeFgHi
COGNITO_ADMINS_GROUP_NAME=demoAdmins
COGNITO_MAIN_REGION=us-fake-1
```

Let's linger for a moment on two of them:

- `COGNITO_CLIENT_ID` (`exampleClientIdABC123`): This is the application client's Cognito identifier; it's used to make authenticated requests to Cognito.
- `COGNITO_USER_POOL_ID` (`us-fake-1_abCDeFgHi`): This identifier represents the user pool in Cognito, a user directory that facilitates sign-ups and sign-ins.

You'll find info on getting these values in the AWS Cognito docs: [Amazon Cognito user pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) and [user pool app clients](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html)

Next, consider the administrative group and the AWS region where the Cognito instance will reside:

- `COGNITO_ADMINS_GROUP_NAME` (`demoAdmins`): This defines a user group within the Cognito user pool, and is typically used to separate different types of users (such as administrators).
- `COGNITO_MAIN_REGION` (`us-fake-1`): AWS services are regional, so this indicates the region where a Cognito user pool is hosted.

## NestJS: strongly-typed configuration

Schema validation is critical for directly avoiding the use of raw values from `process.env`. Further, it also guarantees the presence of all the necessary configuration settings.

We start by defining our `ApplicationConfig` interface, which will represent the structure of our application's configuration (including both production and testing credentials for AWS Cognito):

```ts
import * as process from 'process';

export interface ApplicationConfig {
  cognitoClientID: string;
  cognitoUserPoolID: string;
  cognitoAdminsGroupName: string;
  cognitoMainRegion: string;
}
```

Next, we gather our configuration from environment variables, bridging our `.env` settings with our application logic:

```ts
export const appConfig = (): ApplicationConfig => {
  const config = {
    cognitoClientID: process.env.COGNITO_CLIENT_ID,
    cognitoUserPoolID: process.env.COGNITO_USER_POOL_ID,
    cognitoAdminsGroupName: process.env.COGNITO_ADMINS_GROUP_NAME,
    cognitoMainRegion: process.env.COGNITO_MAIN_REGION,
  };
}
```

To ensure our application runs with the correct and complete configuration, we're using data validation [via `Joi`](https://www.npmjs.com/package/joi); let's make sure all the necessary environment variables are present:

```ts
import * as Joi from 'joi';

const validationSchema = Joi.object({
  cognitoClientID: Joi.string().required(),
  cognitoUserPoolID: Joi.string().required(),
  cognitoAdminsGroupName: Joi.string().required(),
  cognitoMainRegion: Joi.string().required(),
});

const { error, value } = validationSchema.validate(config, { abortEarly: true });

if (error) {
  throw new Error(
    `[Application Config]: Environments validation failed. Please check .env file.
    Error message: ${error.message}`,
  );
}
```

## Using @nestjs-cognito for authentication and authorization

[`@nestjs-cognito`](https://www.npmjs.com/package/@nestjs-cognito/auth) is a comprehensive NestJS library designed for seamless integration with AWS Cognito. Translation: you get secure authentication and authorization in your NestJS applications with minimal effort, easily connected AWS Cognito features like user management, authentication, and security.

This library is part of a suite that encompasses core functionality, [GraphQL support](https://www.npmjs.com/package/@nestjs-cognito/graphql), and [testing utilities](https://www.npmjs.com/package/@nestjs-cognito/testing).

## Configuring AppModule

Typically, NestJS applications are structured around modules. These modules encapsulate related functionality, and `AppModule` is the root module. Specifically, it orchestrates the application's components, including controllers, services, and other modules.

Within the `AppModule`, we'll register the `CognitoAuthModule` asynchronously, taking advantage of the existing configuration module:

```ts
import { CognitoAuthModule } from '@nestjs-cognito/auth';
import { CognitoConfig } from './config/cognito.config';

@Module({
  imports: [
    CognitoAuthModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useClass: CognitoConfig,
    }),
  ],
  providers: [CognitoConfig],
})
export class AppModule {}
```

This code dynamically configures the `CognitoAuthModule` using a custom `CognitoConfig`. It specifies that the `ConfigModule` is a dependency, and the `ConfigService` will be injected into the fetch configuration details. The `useClass` method then leverages `CognitoConfig` for specific Cognito settings.

Moving on to the bit of code below, dependency injection provides the `ConfigService` with our application's configuration context, enabling access to the necessary Cognito parameters. The foundation of our Cognito integration is the `CognitoConfig` service, as it implements `CognitoModuleOptionsFactory` from the `@nestjs-cognito/core` package. Additionally, via `ConfigService`, the `CognitoConfig` service accesses application-specific settings defined in `ApplicationConfig`.

```ts
// cognito.config.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
  CognitoModuleOptions,
  CognitoModuleOptionsFactory,
} from '@nestjs-cognito/core';
import { ApplicationConfig } from './app.config';

@Injectable()
export class CognitoConfig implements CognitoModuleOptionsFactory {
  constructor(
    private readonly configService: ConfigService,
  ) {}
}
```

Next up from there, the core functionality of `CognitoConfig` is encapsulated in the `createCognitoModuleOptions` method. It meticulously assembles the needed configuration so that Cognito can work within our application.

By extracting environment-specific values (like `accessKeyId`, `clientId`, and `userPoolId`), we can customize Cognito's setup to our application's needs, so we get both the desired functionality and security.

And then, with all necessary details in place, the service finalizes the Cognito module options, focusing on the identity provider credentials and JWT verification settings:

```ts
createCognitoModuleOptions(): CognitoModuleOptions {
  return {
    jwtVerifier: {
      clientId: this.configService.get('cognitoClientID'),
      tokenUse: 'id',
      userPoolId: this.configService.get('cognitoUserPoolID'),
    },
  };
}
```

## Understanding the JWT Verifier

An ID token in OAuth 2.0 and OpenID Connect is a JSON Web Token (JWT) that contains claims about the authentication of an end-user by the authorization server.

`tokenUse: 'id'` is specified to tell the system that ID tokens are being used for user authentication and identity verification.

On the other hand, `tokenUse: 'access'`, denotes access tokens. Access tokens are used to authorize API requests on behalf of a user; they do not contain information about the user's identity but rather specify permissions granted.

Choosing `tokenUse: 'id'` over `tokenUse: 'access'` is essential for scenarios where an application needs to know the identity of the user, for example, to display user-specific data or make decisions based on the user's role or attributes. Access tokens, being more about granting access to resources rather than conveying user identity information, are less suitable for these purposes.

This concludes configuration of the Cognito module. From here, we'll see how to use it in an application!

## Securing routes with authentication and authorization

To protect your routes against unauthorized access, let's start with the `@Authentication` decorator from [@nestjs-cognito/auth](https://www.npmjs.com/package/@nestjs-cognito/auth) library. Simply tag it above your controller class and your route is secure. (For a more granular approach, `@UseGuards(AuthenticationGuard)` allows us to protect specific endpoints.)

```ts
import { Authentication } from "@nestjs-cognito/auth";
import { Controller } from "@nestjs/common";

@Controller("secure-route")
@Authentication()
export class SecureController {}
```

While authentication confirms identity, authorization defines what an authenticated user can do. Here, the `@Authorization` decorator comes into play, allowing us to specify the user groups which can access your controller or route:

```ts
@Controller("admin-area")
@Authorization({
  allowedGroups: [appConfig().cognitoAdminsGroupName],
  requiredGroups: [appConfig().cognitoAdminsGroupName],
})
export class AdminController {}
```

Beyond that, understanding who is making a request is crucial: the `@CognitoUser` decorator injects user details directly into your route handlers, offering a straightforward way to access user information without boilerplate code:

```ts
@Get()
findUserData(@CognitoUser("email") userEmail: string) {
  console.log(`User email: ${userEmail}`);
}
```

## Testing controllers

For testing purposes we'll simplify the process of integration using login/password methods as if from one real user. This approach allows for easier testing and debugging, and it gives us a more straightforward authentication mechanism for development cycles (without compromising the integrity of our production security posture):

```
COGNITO_TEST_CLIENT_ID=demoTestClientIdXYZ789
COGNITO_TEST_USER_PASSWORD=Pass!word123
COGNITO_TEST_USER_EMAIL=demo@example.com
COGNITO_TEST_MAIN_REGION=us-mock-2
COGNITO_TEST_USER_POOL_ID=us-mock-2_jKlMnOpQr
```

Let's take a look at these items a little more closely:

- `COGNITO_TEST_CLIENT_ID` (`demoTestClientIdXYZ789`): similar to `COGNITO_CLIENT_ID`, but for the test environment
- `COGNITO_TEST_USER_POOL_ID` (`us-mock-2_jKlMnOpQr`): represents the user pool used for testing
- `COGNITO_TEST_USER_EMAIL` (`demo@example.com`) and `COGNITO_TEST_USER_PASSWORD` (`Pass!word123`): these are credentials for a test user account
- `COGNITO_TEST_MAIN_REGION` (`us-mock-2`): Tte AWS region for the test Cognito user pool

We utilize AWS Cognito for user authentication to make sure we have secure access in place.

This step involves setting up the `CognitoTestingModule`, which mocks the authentication process for testing purposes:

```ts
import { CognitoTestingModule } from '@nestjs-cognito/testing';
```

We begin by mocking the authentication process to simulate user login via AWS Cognito:

```ts
describe('AppController (e2e)', () => {
  let app: INestApplication;
  let config: ConfigService;
  let token: string;
  let apiKeysRepository: ApiKeysRepository;

  beforeAll(async () => {
    const mockCognitoClient = {
      initiateAuth: async () => {
        return request(app.getHttpServer()).post('/cognito-testing-login').send({
          clientId: config.get('cognitoTestClientId'),
          password: config.get('cognitoTestUserPassword'),
          username: config.get('cognitoTestUserEmail'),
        });
      },
    };
  });
})
```

With our mock authentication client ready, let's move on and configure the testing environment. This involves setting up `CognitoTestingModule` with the specific configurations for our AWS Cognito instance. This includes the regions and client IDs, and makes sure our tests communicate with AWS Cognito as they would in a live environment:

```ts
const moduleRef = await Test.createTestingModule({
  imports: [
    CognitoTestingModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (
        configService: ConfigService,
      ) => {
        return {
          identityProvider: {
            region: configService.get('cognitoTestMainRegion'),
          },
          jwtVerifier: {
            clientId: configService.get('cognitoTestClientId'),
            tokenUse: 'id',
            userPoolId: configService.get(
              'cognitoTestUserPoolId',
            ),
          },
        };
      },
    }),
    AppModule,
  ],
}).compile();
```

With our environment configured, it's time to initiate our NestJS application within the testing framework. This boots up our application with the mock Cognito client so we can test our system under simulated authentication scenarios:

```ts
app = moduleRef.createNestApplication();
config = moduleRef.get(ConfigService);

await app.init();

const authenticationResult = await mockCognitoClient.initiateAuth();
token = authenticationResult?.body.IdToken;
```

The first scenario tests the process for authenticated users, and by sending a POST request with a valid authentication token, we can simulate an authorized user's attempt to post some data:

```ts
it('/POST create with token should return data', () => {
  return request(app.getHttpServer())
    .post('/controller-route')
    .set({ Authorization: `Bearer ${token}` })
    .expect(201)
});
```

This test verifies that whenever an authenticated request is made, our system actually processes it.

Then, conversely, let's also simulate an _unauthenticated_ user's attempt to post some data by leaving out the request's authentication token. This test ensures our system can effectively defend against unauthorized access:

```ts
it('/POST create without token should return 403', () => {
  return request(app.getHttpServer())
    .post('/controller-route')
    .expect(403);
});
```

Security first!

Finally, as we wrap up our testing procedures, let's go ahead and be sure we'll have a clean, stable testing environment for future tests; that means making sure our application is properly shut down:

```ts
afterAll(async () => {
  await app.close();
});
```

**

That concludes our tale of bringing NestJS and AWS Cognito together. Hopefully this sheds some light on the nuances of authentication and authorization when working with them! Consider this beast tamed.

---

**Hire us to build a fullstack JS/TS application** Jump start product development, ship new integrations and periphery, or accelerate delivery with our JS/TS experts. [Contact Evil Martians](https://evilmartians.com/contact-us)
