【Express.js】AWS SDKを使用したCognito認証処理(ログイン、トークン更新)

Express.jsアプリケーションにて、「AWS SDK for JavaScript V3」を使用して認証処理を実装する方法を記載する。今回は、usernameとpasswordを使用したログイン処理を事例に解説する。

環境

{
  "dependencies": {
    "@aws-sdk/client-cognito-identity-provider": "^3.568.0",
    "dotenv": "^16.4.5",
    "express": "^4.19.2",
  },
"devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^20.12.7",
    "ts-node": "^10.9.2",
    "typescript": "^5.4.5"
  }
}

aws-sdkはv3を使用。また、TypeScriptで実装しているのでパッケージをインストールする。

AWS SDK を使用して認証を開始する

サンプル実装のGitHubリポジトリ

ログイン処理

ファイル構成は以下の通り。

// アプリケーションルート
src/index.ts

// ルートハンドラ
src/routes/auth.ts

// AWS Cognitoの接続情報を定義
src/config/aws/cognito.ts

// ビジネスロジック
src/controllers/authController.ts

src/index.ts

コメント// 認証処理のルートにて、ルートハンドラを定義。

import { Application, Request, Response } from "express";
import express from "express";
import authRouter from "./routes/auth";
import { connectDB } from "./config/db";
import { errorHandler } from "./middleware/errorHandler";

require("dotenv").config();

const app: Application = express();
const PORT = process.env.PORT || 8080;

app.use(express.json());
// 認証処理のルート
app.use("/api/auth", authRouter);

// サーバー起動
const startServer = async () => {
  try {
    await connectDB();
    // DB接続を待機してからサーバーを起動する
    app.listen(PORT, () => {
      console.log(`App listening on port ${PORT}.`);
    });
  } catch (error) {
    console.error("Failed to connect to MongoDB:", error);
  }
};
startServer();

// エラーハンドリングミドルウェア
app.use(errorHandler);

app.get("/", (req: Request, res: Response) => {
  res.send(`Hello ECS! Mongo ${process.env.DB_USER}`);
});

src/routes/auth.ts

ルートハンドラ。パス/loginにアクセスした際、コントローラーのlogin関数を実行する。

import express from "express";
import { login } from "../controllers/authController";

const router = express.Router();

router.post("/login", login);

export default router;

src/config/aws/cognito.ts

AWSCognitoConfigというインスタンスを定義し、getterを通じて各値を返す。

export class AWSCognitoConfig {
  private static instance: AWSCognitoConfig;

  private readonly region: string;
  private readonly userPoolId: string;
  private readonly appClientId: string;

  private constructor() {
    // リージョン
    this.region = "ap-northeast-1";
    // ユーザープールID
    this.userPoolId = "xxxxxxxxxxxxxx";
    // アプリクライアントID
    this.appClientId = "xxxxxxxxxxxxxx";
  }

  public static getInstance(): AWSCognitoConfig {
    if (!AWSCognitoConfig.instance) {
      AWSCognitoConfig.instance = new AWSCognitoConfig();
    }
    return AWSCognitoConfig.instance;
  }

  public get getRegion(): string {
    return this.region;
  }

  public get getUserPoolId(): string {
    return this.userPoolId;
  }

  public get getAppClientId(): string {
    return this.appClientId;
  }
}

src/controllers/authController.ts

コントローラーのlogin関数。
AWS Cognitoの接続情報からCognitoに接続。
専用のインスタンスとリクエストボディのusername, passwordを使用してPOSTする。
成功した場合、フロントエンドで使用する以下の情報を返す。
idToken、accessToken、refreshToken、expiresIn、refreshTokenExpiresIn

import { Request, Response } from "express";
import {
  AuthFlowType,
  CognitoIdentityProvider,
  InitiateAuthCommand,
} from "@aws-sdk/client-cognito-identity-provider";
import { NextFunction } from "express";
import { AWSCognitoConfig } from "../config/aws/cognito";

/**
 * Cognito リフレッシュトークンの有効期限
 * 更新トークンの有効期限: 30 日で設定
 */
const LOGIN_REFRESH_TOKEN_EXPIRY_DAYS = 30;

// インスタンスに接続
const cognitoConfig = AWSCognitoConfig.getInstance();
const client = new CognitoIdentityProvider({ region: cognitoConfig.getRegion });

export const login = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const { username, password } = req.body;
  const command = new InitiateAuthCommand({
    // 認証タイプでパスワード方式を指定
    AuthFlow: AuthFlowType.USER_PASSWORD_AUTH,
    // アプリクライアントID
    ClientId: cognitoConfig.getAppClientId,
    AuthParameters: {
      USERNAME: username,
      PASSWORD: password,
    },
  });

  try {
    const response = await client.send(command);

    const currentTime = Math.floor(Date.now() / 1000);
    const expiresIn =
      currentTime + (response.AuthenticationResult?.ExpiresIn ?? 0);
    // リフレッシュトークンの有効期限を秒で計算: 現在時刻から30日後
    const refreshTokenExpiresIn =
      currentTime + LOGIN_REFRESH_TOKEN_EXPIRY_DAYS * 86400;

    res.json({
      idToken: response.AuthenticationResult?.IdToken,
      accessToken: response.AuthenticationResult?.AccessToken,
      refreshToken: response.AuthenticationResult?.RefreshToken,
      // フロントエンドへ渡すアクセストークンの有効期限
      expiresIn,
      // フロントエンドへ渡すリフレッシュトークンの有効期限
      refreshTokenExpiresIn,
    });
  } catch (error) {
    console.error(error);
    next(error);
  }
};

レスポンスの説明

idToken: ユーザーの認証情報がエンコードされたJWT(JSON Web Token)であり、ユーザーの属性情報(プロフィール情報やグループメンバーシップなど)を含んでいる。アプリケーションがユーザーを特定し、ユーザーが誰であるかを確認するために使用される。

accessToken: ユーザーがAWS Cognitoが管理するリソースにアクセスするための権限を持つJWT。このトークンはAPIへのアクセス許可を委譲するときに使われ、特定のAPI操作が許可された範囲内でのみ実行可能であることを保証する。

refreshToken: 有効期限が比較的長く設定されており、idTokenaccessTokenが失効した際にこれらを新しく発行するために使用されるトークン。refreshToken自体が失効するまでは、新しいidTokenaccessTokenを取得することができる。そのため、ユーザーが毎度ログイン処理をしなくても、システム側でrefreshTokenを使用して認証情報を更新することができる。

expiresIn: idTokenとaccessTokenの有効期限。

refreshTokenExpiresIn: refreshTokenの有効期限。

認証と認可の違いについて

流れとしては、ユーザーが最初にシステムにログインするときに認証が行われ、その後の各アクションやリクエストに対して認可が適用される。

認証(Authentication)

認証は、ユーザー情報(通常はユーザー名やメールアドレス)の正当性を確認するプロセス。サービスを利用しているユーザーが本人であることを証明する手段。指紋や顔認識など、ユーザーの身体的特徴を使用する「生体認証」、IDパスワード認証、スマートカードやモバイルデバイスなどの所有アイテムを通じた認証など様々。

認可(Authorization)

認可は、認証されたユーザーがシステム内で行うことが許可されているアクションを決定するプロセス。つまり、認証されたユーザーがどのリソースにアクセスできるか、どのような操作を行えるかを制御する。ユーザーにロールを割り当て、そのロールに基づいてアクセス権を設定する「ロールベースのアクセス制御(RBAC)」など。

トークンリフレッシュ

src/routes/auth.ts

ルートハンドラ。パス/token-refreshにアクセスした際、コントローラーのrefreshTokens関数を実行する。

import express from "express";
import { login } from "../controllers/authController";

const router = express.Router();

router.post("/token-refresh", refreshTokens);

export default router;

src/controllers/authController.ts

コントローラーのrefreshTokens関数。
AWS Cognitoの接続情報からCognitoに接続。
専用のインスタンスとリクエストボディのrefreshTokenを使用してPOSTする。
成功した場合、更新されたidToken、accessToken、新しいexpiresInを返す。

import { Request, Response } from "express";
import {
  AuthFlowType,
  CognitoIdentityProvider,
  InitiateAuthCommand,
} from "@aws-sdk/client-cognito-identity-provider";
import { NextFunction } from "express";
import { AWSCognitoConfig } from "../config/aws/cognito";

/**
 * Cognito リフレッシュトークンの有効期限
 * 更新トークンの有効期限: 30 日で設定
 */
const LOGIN_REFRESH_TOKEN_EXPIRY_DAYS = 30;

// インスタンスに接続
const cognitoConfig = AWSCognitoConfig.getInstance();
const client = new CognitoIdentityProvider({ region: cognitoConfig.getRegion });

/**
 * トークンリフレッシュ
 */
export const refreshTokens = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const { refreshToken } = req.body;
  const command = new InitiateAuthCommand({
    AuthFlow: AuthFlowType.REFRESH_TOKEN_AUTH,
    ClientId: cognitoConfig.getAppClientId,
    AuthParameters: {
      REFRESH_TOKEN: refreshToken,
    },
  });

  try {
    const response = await client.send(command);
    const currentTime = Math.floor(Date.now() / 1000);
    res.json({
      idToken: response.AuthenticationResult?.IdToken,
      accessToken: response.AuthenticationResult?.AccessToken,
      // アクセストークンの新しい有効期限を追加
      expiresIn: currentTime + (response.AuthenticationResult?.ExpiresIn ?? 0),
    });
  } catch (error) {
    console.error(error);
    next(error);
  }
};

関連記事

コメント

この記事へのコメントはありません。