9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

NestJSでAWS Cognitoの認証APIを一通り作ってみた

Last updated at Posted at 2022-11-30

認証基盤にAWS Cognitoを採用し、それらをバックエンドAPI(今回はNestJS)に実装してみました。

基本的なユースケースに対応したコードを紹介します。
(メールアドレスの変更は現在は問題があるためやりません)

前提

  • Cognitoのユーザープールが作成されていること
  • NestJS v9を使っています

使用するパッケージ

npm install amazon-cognito-identity-js aws-jwt-verify class-validator dotenv

class-validator(リクエストバリデーション時に使う)とdotenvはお好みで。

新規登録

ロジック部分はauth.service.tsに集約します。

Cognitoはメールアドレスが一意かどうかをチェックしてくれないので、他のユーザーと登録されるメールアドレスが重複することが可能となっています。
なので、他のユーザーと重複がないようにチェックするようにしています。

auth.service.ts
import { Injectable } from '@nestjs/common';
import { CognitoUserAttribute, CognitoUserPool } from 'amazon-cognito-identity-js';
import { ConfigService } from '@nestjs/config';
import { RegisterRequestDto } from '@/dto/register.request.dto';
import { fetchListUsers } from '@/helpers/utils.helper';

@Injectable()
export class AuthService {
  private userPool: CognitoUserPool;

  constructor(private configService: ConfigService) {
    this.userPool = new CognitoUserPool({
      UserPoolId: this.configService.get<string>('COGNITO_USER_POOL_ID'),
      ClientId: this.configService.get<string>('COGNITO_CLIENT_ID'),
    });
  }

  async register(authRegisterRequest: RegisterRequestDto) {
    const { name, email, password } = authRegisterRequest;
    // メールアドレスの重複がないかチェックする
    const existedUser = await fetchListUsers(email);
    if (existedUser.length > 0) throw new Error('The email is duplicated.');

    return new Promise((resolve, reject) => {
      return this.userPool.signUp(
        name,
        password,
        [new CognitoUserAttribute({ Name: 'email', Value: email })],
        null,
        (err, result) => {
          if (!result) {
            reject(err);
          } else {
            resolve(result.user);
          }
        },
      );
    });
  }
}

ちなみに、特定のEmailを持ったユーザーを取得する関数はこれ。

@aws-sdk/client-cognito-identity-provider を使うとユーザープールを操作できます。

helper.ts
import { CognitoIdentityProviderClient, ListUsersCommand } from '@aws-sdk/client-cognito-identity-provider';

const cognitoClient = new CognitoIdentityProviderClient({});

/** CognitoのUserPoolの特定のEmailを持ったユーザーを取得する */
export const fetchListUsers = async (email: string) => {
  const command = new ListUsersCommand({
    UserPoolId: process.env.COGNITO_USER_POOL_ID,
    Filter: `email = "${email}"`,
  });
  const { Users: users } = await cognitoClient.send(command);
  return users;
};

controllerは呼び出すのと、エラーハンドリングをするだけ。なので、次項から省略します。

auth.controller.ts
import { RegisterRequestDto } from '@/dto/register.request.dto';
import { BadRequestException, Body, Controller, Post } from '@nestjs/common';
import { AuthService } from '@services/auth.service';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('register')
  async register(@Body() registerRequest: RegisterRequestDto) {
    try {
      return await this.authService.register(registerRequest);
    } catch (e) {
      throw new BadRequestException(e.message);
    }
  }
}

e.g. 動作確認用リクエスト

curl --location --request POST 'http://localhost:3000/auth/register' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name":"user_1",
    "email":"hogehoge@example.com",
    "password":"P@ssw0rd"
}'

確認コードの検証

メールアドレスを検証される際に必要になる確認コードを検証します。

auth.service.ts
import { Injectable } from '@nestjs/common';
import { CognitoUser, CognitoUserPool } from 'amazon-cognito-identity-js';
import { ConfigService } from '@nestjs/config';
import { VerifyCodeRequestDto } from '@/dto/verifyCode.request.dto';

@Injectable()
export class AuthService {
  private userPool: CognitoUserPool;

  constructor(private configService: ConfigService) {
    this.userPool = new CognitoUserPool({
      UserPoolId: this.configService.get<string>('COGNITO_USER_POOL_ID'),
      ClientId: this.configService.get<string>('COGNITO_CLIENT_ID'),
    });
  }

  async verifyEmail(user: VerifyCodeRequestDto) {
    const { name, code } = user;
    const userData = {
      Username: name,
      Pool: this.userPool,
    };
    const cognitoUser = new CognitoUser(userData);
    return new Promise((resolve, reject) => {
      return cognitoUser.confirmRegistration(
        code.toString(),
        true,
        function (error, result) {
          if (error) reject(error);
          resolve(result);
        },
      );
    });
  }
}

e.g. 動作確認用リクエスト

curl --location --request POST 'http://localhost:3000/auth/verify' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "user_1",
    "code": "494769"
}'

ログイン

上記で作ったアカウント情報を使ってログインします。

auth.service.ts
import { Injectable } from '@nestjs/common';
import { AuthenticationDetails, CognitoUser, CognitoUserPool } from 'amazon-cognito-identity-js';
import { ConfigService } from '@nestjs/config';
import { AuthenticateRequestDto } from '@/dto/authenticate.request.dto';

@Injectable()
export class AuthService {
  private userPool: CognitoUserPool;

  constructor(private configService: ConfigService) {
    this.userPool = new CognitoUserPool({
      UserPoolId: this.configService.get<string>('COGNITO_USER_POOL_ID'),
      ClientId: this.configService.get<string>('COGNITO_CLIENT_ID'),
    });
  }

  async authenticate(user: AuthenticateRequestDto) {
    const { name, password } = user;
    const authenticationDetails = new AuthenticationDetails({
      Username: name,
      Password: password,
    });
    const userData = {
      Username: name,
      Pool: this.userPool,
    };
    const newUser = new CognitoUser(userData);
    return new Promise((resolve, reject) => {
      return newUser.authenticateUser(authenticationDetails, {
        onSuccess: (result) => {
          resolve(result);
        },
        onFailure: (err) => {
          reject(err);
        },
      });
    });
  }
}

e.g. 動作確認用リクエスト

curl --location --request POST 'http://localhost:3000/auth/authenticate' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name":"user_1",
    "password":"P@ssw0rd"
}'

パスワードを変更する

パスワードを変更したいとき

auth.service.ts
import { Injectable } from '@nestjs/common';
import { AuthenticationDetails, CognitoUser, CognitoUserPool } from 'amazon-cognito-identity-js';
import { ConfigService } from '@nestjs/config';
import { ChangePasswordRequestDto } from '@/dto/changePassword.request.dto';
@Injectable()
export class AuthService {
  private userPool: CognitoUserPool;

  constructor(private configService: ConfigService) {
    this.userPool = new CognitoUserPool({
      UserPoolId: this.configService.get<string>('COGNITO_USER_POOL_ID'),
      ClientId: this.configService.get<string>('COGNITO_CLIENT_ID'),
    });
  }

  async changePassword(user: ChangePasswordRequestDto) {
    const { name, old_password, password } = user;
    const userData = {
      Username: name,
      Pool: this.userPool,
    };
    const cognitoUser = new CognitoUser(userData);
    const authenticationDetails = new AuthenticationDetails({
      Username: name,
      Password: old_password,
    });
    return new Promise((resolve, reject) => {
      cognitoUser.authenticateUser(authenticationDetails, {
        onSuccess: () => {
          cognitoUser.changePassword(
            old_password,
            password,
            function (error, result) {
              if (error) reject(error);
              resolve(result);
            },
          );
        },
        onFailure: (err) => {
          reject(err);
        },
      });
    });
  }
}

e.g. 動作確認用リクエスト

curl --location --request POST 'http://localhost:3000/auth/changePassword' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "user_1",
    "old_password": "P@ssw0rd",
    "password": "new_P@ssw0rd"
}'

パスワードを忘れたとき(パスワードリセット)

パスワードを忘れた際はこのようなフローになります(シーケンス図慣れてないので見づらかったらすみません)
forgotPasswordで確認コードを発行→resetPasswordでパスワードのリセットです。

スクリーンショット 2022-11-30 19.51.49.png

auth.service.ts
import { Injectable } from '@nestjs/common';
import { CognitoUser, CognitoUserPool } from 'amazon-cognito-identity-js';
import { ConfigService } from '@nestjs/config';
import { ForgotPasswordRequestDto } from '@/dto/forgotPassword.dto';

@Injectable()
export class AuthService {
  private userPool: CognitoUserPool;

  constructor(private configService: ConfigService) {
    this.userPool = new CognitoUserPool({
      UserPoolId: this.configService.get<string>('COGNITO_USER_POOL_ID'),
      ClientId: this.configService.get<string>('COGNITO_CLIENT_ID'),
    });
  }

  async forgotPassword(user: ForgotPasswordRequestDto) {
    const { name } = user;
    const userData = {
      Username: name,
      Pool: this.userPool,
    };
    const cognitoUser = new CognitoUser(userData);
    return new Promise((resolve, reject) => {
      cognitoUser.forgotPassword({
        onSuccess: (result) => {
          resolve(result);
        },
        onFailure: (err) => {
          reject(err);
        },
      });
    });
  }

  async resetPassword(user: ForgotPasswordRequestDto) {
    const { name, code, password } = user;
    const userData = {
      Username: name,
      Pool: this.userPool,
    };
    const cognitoUser = new CognitoUser(userData);
    return new Promise((resolve, reject) => {
      cognitoUser.confirmPassword(code.toString(), password, {
        onSuccess: (result) => {
          resolve(result);
        },
        onFailure: (err) => {
          reject(err);
        },
      });
    });
  }
}

JWTを検証する(Guardを実装する)

Cognitoで作成されたユーザーがログインしたときに取得されるアクセストークン(=JWT)を検証することで、ユーザーからのリクエストを検証できます。
ここで使われるのがaws-jwt-verifyです。これのおかげで簡潔に書けます。

ここで作ったGuardを任意のControllerに使用するだけです。

jwt-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { CognitoJwtVerifier } from 'aws-jwt-verify';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  private jwtVerifier = CognitoJwtVerifier.create({
    userPoolId: process.env.COGNITO_USER_POOL_ID,
    tokenUse: 'access',
    clientId: process.env.COGNITO_CLIENT_ID,
    scope: 'aws.cognito.signin.user.admin',
  });
  async canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    try {
      const result = await this.jwtVerifier.verify(
        request.header('authorization'),
      );
      return !!result;
    } catch (error) {
      return false;
    }
  }
}

以上で、

  • ユーザープールへのユーザーの作成
  • メールアドレスの検証
  • ログイン
  • パスワードの変更
  • パスワードのリセット
  • リクエストの検証

の要件は対応できるかと思います。

もっとこうした方がいいよ、などありましたら教えてください。

9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?