npm package: @nodelabs/nest-aws-cognito
Description: NestJS module for AWS Cognito (authentication & user management)
Author: Abiral Neupane
Latest version: 0.0.1

# @nodelabs/nest-aws-cognito

A NestJS dynamic module for **AWS Cognito** — user authentication, registration, admin user management, password flows, group assignment, and email template customisation. Supports both static (`forRoot`) and async (`forRootAsync`) configuration, with optional global registration.

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
  - [forRoot (static)](#forroot-static)
  - [forRootAsync (factory)](#forrootasync-factory)
  - [isGlobal](#isglobal)
  - [All Options](#all-options)
- [AWS Cognito Setup Requirements](#aws-cognito-setup-requirements)
- [Available Services](#available-services)
  - [AwsCognitoService](#awscognitoservice)
- [API Reference](#api-reference)
  - [ICognitoModuleOptions](#icognitomoduleoptions)
  - [IRegisterUserPayload](#iregisteruserpayload)
  - [ICreateUserPayload](#icreateuseripayload)
  - [IUpdateUserPayload](#iupdateuserpayload)
  - [IUpdateCognitoEmailTemplateOptions](#iupdatecognitoemailtemplateoptions)
- [Exports](#exports)

---

## Installation

```bash
npm install @nodelabs/nest-aws-cognito @aws-sdk/client-cognito-identity-provider
```

### Peer Dependencies

```bash
npm install @nestjs/common @nestjs/config reflect-metadata rxjs
```

---

## Quick Start

```typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { CognitoModule } from '@nodelabs/nest-aws-cognito';

@Module({
  imports: [
    CognitoModule.forRoot({
      region: 'us-east-1',
      accessKeyId: 'YOUR_ACCESS_KEY_ID',
      secretAccessKey: 'YOUR_SECRET_ACCESS_KEY',
      clientId: 'YOUR_COGNITO_APP_CLIENT_ID',
      clientSecret: 'YOUR_COGNITO_APP_CLIENT_SECRET',
      userPoolId: 'us-east-1_XXXXXXXXX',
      appName: 'My App',
    }),
  ],
})
export class AppModule {}
```

---

## Configuration

### `forRoot` (static)

```typescript
CognitoModule.forRoot({
  region: 'us-east-1',
  accessKeyId: 'YOUR_ACCESS_KEY_ID',
  secretAccessKey: 'YOUR_SECRET_ACCESS_KEY',
  clientId: 'YOUR_COGNITO_APP_CLIENT_ID',
  clientSecret: 'YOUR_COGNITO_APP_CLIENT_SECRET',
  userPoolId: 'us-east-1_XXXXXXXXX',
  appName: 'My App',     // optional — used in default email subjects
  isGlobal: true,        // optional
})
```

---

### `forRootAsync` (factory)

```typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { CognitoModule } from '@nodelabs/nest-aws-cognito';

@Module({
  imports: [
    ConfigModule.forRoot(),
    CognitoModule.forRootAsync({
      isGlobal: true,
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        region: config.get<string>('AWS_REGION'),
        accessKeyId: config.get<string>('AWS_ACCESS_KEY_ID'),
        secretAccessKey: config.get<string>('AWS_SECRET_ACCESS_KEY'),
        clientId: config.get<string>('COGNITO_CLIENT_ID'),
        clientSecret: config.get<string>('COGNITO_CLIENT_SECRET'),
        userPoolId: config.get<string>('COGNITO_USER_POOL_ID'),
        appName: config.get<string>('APP_NAME'),
      }),
    }),
  ],
})
export class AppModule {}
```

#### Environment variables (example `.env`)

```env
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
COGNITO_CLIENT_ID=your_cognito_client_id
COGNITO_CLIENT_SECRET=your_cognito_client_secret
COGNITO_USER_POOL_ID=us-east-1_XXXXXXXXX
APP_NAME=My App
```

---

### `isGlobal`

Setting `isGlobal: true` registers the module globally so `AwsCognitoService` is available across all feature modules without re-importing `CognitoModule`.

---

### All Options

| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| `region` | `string` | ✅ | — | AWS region of the User Pool |
| `accessKeyId` | `string` | ✅ | — | IAM Access Key ID |
| `secretAccessKey` | `string` | ✅ | — | IAM Secret Access Key |
| `clientId` | `string` | ✅ | — | Cognito App Client ID |
| `clientSecret` | `string` | ✅ | — | Cognito App Client Secret |
| `userPoolId` | `string` | ✅ | — | Cognito User Pool ID |
| `appName` | `string` | ❌ | `'My App'` | Used in default email template subjects |
| `isGlobal` | `boolean` | ❌ | `false` | Register module globally |

---

## AWS Cognito Setup Requirements

- Your Cognito App Client must have **`ALLOW_ADMIN_USER_PASSWORD_AUTH`** enabled (for `login` and `respondToNewPasswordChallenge`).
- Your App Client must have a **client secret** (required for HMAC secret hash computation).
- To use `updateCognitoEmailTemplate`, your User Pool's **Email Sending Account** must be set to **`DEVELOPER`** (custom SES integration).
- The IAM user/role must have appropriate Cognito permissions (e.g. `cognito-idp:AdminInitiateAuth`, `cognito-idp:AdminCreateUser`, etc.).

---

## Available Services

### `AwsCognitoService`

Inject `AwsCognitoService` into any provider after importing `CognitoModule`.

```typescript
import { Injectable } from '@nestjs/common';
import { AwsCognitoService } from '@nodelabs/nest-aws-cognito';

@Injectable()
export class AuthService {
  constructor(private readonly cognitoService: AwsCognitoService) {}
}
```

#### Methods

---

##### `login(username, password)`

Authenticates a user via `ADMIN_USER_PASSWORD_AUTH` flow.

```typescript
const result = await cognitoService.login('user@example.com', 'P@ssword123');
```

**Returns** on success:
```typescript
{
  accessToken: string;
  refreshToken: string;
  idToken: string;
  expiresIn: number;
  tokenType: string;
}
```

**Returns** when `NEW_PASSWORD_REQUIRED` challenge is raised:
```typescript
{
  challengeName: 'NEW_PASSWORD_REQUIRED';
  session: string;
}
```

Returns `null` on any other failure.

---

##### `respondToNewPasswordChallenge(username, newPassword, session)`

Completes the `NEW_PASSWORD_REQUIRED` challenge returned by `login`.

```typescript
const result = await cognitoService.respondToNewPasswordChallenge(
  'user@example.com',
  'NewP@ssword123',
  session, // from login() response
);
```

**Returns** the same token shape as `login` on success.

---

##### `register(body)`

Self-service user registration via `SignUp`. Requires unauthenticated access to be enabled on the App Client.

```typescript
await cognitoService.register({
  email: 'user@example.com',
  password: 'P@ssword123',
  firstName: 'Alice',
  lastName: 'Smith',
});
```

**Payload:** [`IRegisterUserPayload`](#iregisteruserpayload)

---

##### `createUser(body)`

Admin-side user creation via `AdminCreateUser`. Sets `email_verified: true` and suppresses the default invitation message (use `updateCognitoEmailTemplate` for custom invitations).

```typescript
await cognitoService.createUser({
  email: 'admin-created@example.com',
  password: 'TempP@ss123',
  firstName: 'Bob',
  lastName: 'Jones',
});
```

**Payload:** [`ICreateUserPayload`](#icreateuseripayload)

---

##### `updateUser(cognitoId, body)`

Updates user attributes for an existing user.

```typescript
await cognitoService.updateUser('sub-uuid-here', {
  firstName: 'Bobby',
  customAttributes: {
    'custom:role': 'admin',
    'custom:tenantId': 'tenant-123',
  },
});
```

**Payload:** [`IUpdateUserPayload`](#iupdateuserpayload)

---

##### `forgotPassword(email)`

Initiates the forgot-password flow, sending a confirmation code to the user.

```typescript
await cognitoService.forgotPassword('user@example.com');
```

---

##### `confirmForgotPassword(email, confirmationCode, newPassword)`

Completes the forgot-password flow using the code sent by `forgotPassword`.

```typescript
await cognitoService.confirmForgotPassword(
  'user@example.com',
  '123456',
  'NewP@ssword123',
);
```

---

##### `addToGroup(cognitoId, roles)`

Adds a user to one or more Cognito groups in parallel.

```typescript
await cognitoService.addToGroup('sub-uuid-here', ['admin', 'editor']);
```

---

##### `updateCognitoEmailTemplate(options)`

Updates the User Pool's verification and invitation email templates. The caller is responsible for rendering the HTML strings (e.g. via Handlebars, EJS, or any template engine) before passing them in — keeping this package free of file-system or template-engine dependencies.

> Requires `EmailSendingAccount` to be set to `DEVELOPER` in the User Pool.

```typescript
import Handlebars from 'handlebars';
import * as fs from 'fs';

const verificationTemplate = Handlebars.compile(
  fs.readFileSync('templates/verification.hbs', 'utf8'),
);
const invitationTemplate = Handlebars.compile(
  fs.readFileSync('templates/invitation.hbs', 'utf8'),
);

await cognitoService.updateCognitoEmailTemplate({
  verificationEmailHtml: verificationTemplate({ appName: 'My App' }),
  verificationEmailSubject: 'Verify your email — My App',
  invitationEmailHtml: invitationTemplate({ appName: 'My App' }),
  invitationEmailSubject: 'Your temporary password — My App',
});
```

> **Note:** `verificationEmailHtml` must include the `{####}` placeholder so Cognito can inject the verification code. `invitationEmailHtml` must include `{####}` for the temporary password.

**Payload:** [`IUpdateCognitoEmailTemplateOptions`](#iupdatecognitoemailtemplateoptions)

---

#### Methods Summary

| Method | Signature | Description |
|--------|-----------|-------------|
| `login` | `login(username: string, password: string): Promise<TokenResult \| ChallengeResult \| null>` | Admin password auth |
| `respondToNewPasswordChallenge` | `respondToNewPasswordChallenge(username: string, newPassword: string, session: string): Promise<TokenResult \| undefined>` | Complete NEW_PASSWORD_REQUIRED challenge |
| `register` | `register(body: IRegisterUserPayload): Promise<any \| null>` | Self-service sign-up |
| `createUser` | `createUser(body: ICreateUserPayload): Promise<any \| null>` | Admin user creation |
| `updateUser` | `updateUser(cognitoId: string, body: IUpdateUserPayload): Promise<any \| null>` | Update user attributes |
| `forgotPassword` | `forgotPassword(email: string): Promise<ForgotPasswordCommandOutput \| null>` | Initiate forgot-password flow |
| `confirmForgotPassword` | `confirmForgotPassword(email: string, confirmationCode: string, newPassword: string): Promise<ConfirmForgotPasswordCommandOutput \| null>` | Complete forgot-password flow |
| `addToGroup` | `addToGroup(cognitoId: string, roles: string[]): Promise<any[]>` | Add user to Cognito groups |
| `updateCognitoEmailTemplate` | `updateCognitoEmailTemplate(options: IUpdateCognitoEmailTemplateOptions): Promise<any>` | Update User Pool email templates |

---

## API Reference

### `ICognitoModuleOptions`

```typescript
interface ICognitoModuleOptions {
  /** AWS region (e.g. 'us-east-1') */
  region: string;

  /** IAM Access Key ID */
  accessKeyId: string;

  /** IAM Secret Access Key */
  secretAccessKey: string;

  /** Cognito App Client ID */
  clientId: string;

  /** Cognito App Client Secret */
  clientSecret: string;

  /** Cognito User Pool ID (e.g. 'us-east-1_XXXXXXXXX') */
  userPoolId: string;

  /** Application name used in default email subjects (default: 'My App') */
  appName?: string;
}
```

### `ICognitoModuleAsyncOptions`

```typescript
interface ICognitoModuleAsyncOptions {
  isGlobal?: boolean;
  imports?: any[];
  inject?: any[];
  useFactory: (...args: any[]) => ICognitoModuleOptions | Promise<ICognitoModuleOptions>;
}
```

### `IRegisterUserPayload`

```typescript
interface IRegisterUserPayload {
  email: string;
  password: string;
  firstName?: string;
  lastName?: string;
}
```

### `ICreateUserPayload`

```typescript
interface ICreateUserPayload {
  email: string;
  password: string;
  firstName?: string;
  lastName?: string;
}
```

### `IUpdateUserPayload`

```typescript
interface IUpdateUserPayload {
  email?: string;
  firstName?: string;
  lastName?: string;
  /** Key-value map for custom Cognito attributes (e.g. 'custom:role') */
  customAttributes?: Record<string, string>;
}
```

### `IUpdateCognitoEmailTemplateOptions`

```typescript
interface IUpdateCognitoEmailTemplateOptions {
  /**
   * Pre-rendered HTML for the verification email.
   * Must include {####} placeholder for the verification code.
   */
  verificationEmailHtml: string;

  /** Subject line for the verification email (default: 'Your verification code - <appName>') */
  verificationEmailSubject?: string;

  /**
   * Pre-rendered HTML for the admin invitation email.
   * Must include {####} placeholder for the temporary password.
   */
  invitationEmailHtml: string;

  /** Subject line for the invitation email (default: 'Your temporary password - <appName>') */
  invitationEmailSubject?: string;
}
```

---

## Exports

| Symbol | Description |
|--------|-------------|
| `CognitoModule` | Dynamic NestJS module (`forRoot` / `forRootAsync`) |
| `AwsCognitoService` | Injectable Cognito service |
| `COGNITO_MODULE_OPTIONS` | Injection token for options object |
| `ICognitoModuleOptions` | TypeScript interface for module options |
| `ICognitoModuleAsyncOptions` | TypeScript interface for async options |
| `IRegisterUserPayload` | Interface for self-service registration payload |
| `ICreateUserPayload` | Interface for admin user creation payload |
| `IUpdateUserPayload` | Interface for user attribute update payload |

---

## License

MIT