ReactからCognitoオーソライザーを使ったAPI Gatewayをコールする

2023.12.25

情報システム室 進地 です。

ReactからCognitoオーソライザーを設定したAPI Gateway + Lambdaをコールしてみました。 XHRのライブラリにはaxiosを使っています。

Backend

AWS CDKコードサンプル

lib/someStack.ts

import { aws_lambda, Stack, StackProps, Duration } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha';
import { ManagedPolicy, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
import {
  Cors,
  EndpointType,
  LambdaIntegration,
  MethodLoggingLevel,
  RestApi,
  CognitoUserPoolsAuthorizer
} from "aws-cdk-lib/aws-apigateway";
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as dotenv from 'dotenv';

dotenv.config();

export class SomeStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const lambdaRole = new Role(this, "SomeLambdaRole", {
      assumedBy: new ServicePrincipal("lambda.amazonaws.com"),
      managedPolicies: [
        ManagedPolicy.fromManagedPolicyArn(
          this,
          "lambda",
          "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
        )
      ],
      description: "Basic Lambda Role"
    });

    const some = new PythonFunction(this, 'SomeHandler', {
      functionName: 'SomeHandler',
      runtime: aws_lambda.Runtime.PYTHON_3_11,
      entry: 'lambda/some',
      handler: 'lambda_handler',
      role: lambdaRole,
      timeout: Duration.seconds(30)
    });
    const someIntegration = new LambdaIntegration(some);

    const apigw = new RestApi(this, "apigw", {
      restApiName: "apigw",
      deployOptions: {
        loggingLevel: MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
        metricsEnabled: true,
      },
      defaultCorsPreflightOptions: {
        allowOrigins: Cors.ALL_ORIGINS,
        allowMethods: Cors.ALL_METHODS,
        allowHeaders: Cors.DEFAULT_HEADERS,
        statusCode: 200,
      },
      endpointTypes: [EndpointType.REGIONAL],
      cloudWatchRole: true
    });

    // 既存のユーザープールを使う
    const userPool = cognito.UserPool.fromUserPoolArn(
      this,
      'ExistingUserPool',
      `arn:aws:cognito-idp:${process.env.AWS_REGION}:${process.env.AWS_ACCOUNT_ID}:userpool/${process.env.COGNITO_USER_POOL_ID}`
    );

    // Cognitoのユーザープールにクライアントを追加
    const userPoolClientName = 'SomeUserPoolClient';
    userPool.addClient(userPoolClientName, {
      userPoolClientName: userPoolClientName,
      // パスワード認証を有効にするためには下記の設定が必要
      authFlows: { userPassword: true },
    });

    // CognitoのAuthorizerの作成
    const cognitoAuthorizer = new CognitoUserPoolsAuthorizer(
      this,
      'cognitoAuthorizer',
      {
        cognitoUserPools: [userPool],
      }
    );

    const someEndpoint = apigw.root.addResource("some");
    someEndpoint.addMethod(
      "POST",
      someIntegration,
      {
        authorizer: cognitoAuthorizer
      }
    );
  }
}

LambdaはPythonを使っています。

Cognitoユーザプールクライアントを作成する際に、パスワード認証を有効にするためにauthFlows: { userPassword: true }を設定しています(ハイライト部)。

.envファイルは下記のように設定しています。

.env

COGNITO_USER_POOL_ID=<接続するCognitoユーザプールIDを設定する>
AWS_ACCOUNT_ID=<AWSアカウントIDを設定する>
AWS_REGION=ap-northeast-1

Lambdaコードサンプル

someParameterというキーで受け取ったパラメータをそのまま返すLambdaとします。

lambda/some/index.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import logging
import json

# ロギング用
logging.basicConfig(format='%(asctime)s - %(threadName)s - %(module)s:%(funcName)s(%(lineno)d) - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

def lambda_handler(
    event: dict,
    context: dict
) -> dict:
    """
    実質メイン処理
    """
    logger.info("処理開始 [__PROGRESS__]")

    param = json.loads(event["body"])

    statusCode = 200
    body = {
        "someParameter": param.get("someParameter")
    }

    logger.info("処理終了 [__PROGRESS__]")

    return {
        "statusCode": statusCode,
        "headers": {"Access-Control-Allow-Origin": "*"},
        "body": json.dumps(body)
    }

if __name__ == "__main__":
    event = {
    }
    lambda_handler(event, {})

Frontend

Reactコードサンプル

src/App.js

  /* Cognitoオーソライザーを設定したAPI Gateway + Lambdaをコールする */
  const callAPI = async () => {
    try {
      /* トークンを取得 */
      const token = await getCognitoAccessToken(
        process.env.REACT_APP_COGNITO_URL,
        'USER_PASSWORD_AUTH',
        process.env.REACT_APP_COGNITO_USER_POOL_CLIENT_ID,
        process.env.REACT_APP_COGNITO_USERNAME,
        process.env.REACT_APP_COGNITO_PASSWORD
      );

      /* APIに渡したいパラメータを設定 */
      const requestPayload = {
        'someParameter': '<適当な値を渡す>'
      };
      /* ヘッダ設定 */
      const requestHeaders = {
        'Content-Type': 'application/json',
        'Authorization': token
      };
      const requestOptions = {
        'headers' : requestHeaders
      };

      /* APIを呼び出し */
      const requestUrl = '<APIのエンドポイントのURL>';
      try {
        const response = await axios.post(requestUrl, JSON.stringify(requestPayload), requestOptions);
        const content = response.data;
        console.log(content.someParameter);
      } catch(err) {
        console.error(err);
      }
    } catch(err) {
      console.error(err);
    }
  };

  /* アクセストークン取得用 */
  const getCognitoAccessToken = async (cognitoUrl, authFlow, clientId, username, password) => {
    // リクエストボディ
    const payload = {
      AuthFlow: authFlow,
      ClientId: clientId,
      AuthParameters: {
        USERNAME: username,
        PASSWORD: password
      }
    };
    // リクエストヘッダー
    const headers = {
      'Content-Type': 'application/x-amz-json-1.1',
      'X-Amz-Target': 'AWSCognitoIdentityProviderService.InitiateAuth'
    }
    // リクエストオプション
    const options = {
      'headers' : headers
    }

    const response = await axios.post(cognitoUrl, JSON.stringify(payload), options);
    const content = response.data;
    return content.AuthenticationResult.IdToken;
  }

Cognito接続用のユーザをあらかじめユーザプールに作成して、.envに設定しておきます。 Frontendの.envは次のように設定しています。

.env

REACT_APP_COGNITO_USER_POOL_CLIENT_ID=<CognitoユーザプールクライアントID>
REACT_APP_COGNITO_URL=https://cognito-idp.ap-northeast-1.amazonaws.com/
REACT_APP_COGNITO_USERNAME=<Cognito接続用ユーザのユーザ名>
REACT_APP_COGNITO_PASSWORD=<Cognito接続用ユーザのパスワード>

callAPI関数を呼び出せば、Backendで作成したAPI Gateway + Lambdaをコールして、レスポンスを取得できます。

留意事項

今回のサンプルでは、Cognito接続用のユーザパスワードをReact側に設定していますが、実際に運用に載せる場合はパスワードなどCredentialな情報は.envに記載しないでください。

具体的には、Proxy ServerをVercelなどで作って、FrontendでCredentialを持たないようにしてください。