はじめに
lambda や API の実態と認証のロジックを切り離したい。
そしたらもっとシンプルにバックエンド開発ができる。
API はすべて API Gateway を通る構成なので、API Gateway をリバースプロキシのように扱い、そこでアクセス制御をしたい。
結論
IAM ロールに紐付けている IAM ポリシーで各エンドポイントへのアクセス許可を制御したかっのですが、今回の方法では要件をクリアできませんでした。
今回の方法は Cognito オーソライザー に設定しているユーザープールへの認証が通れば、IDプールのロール設定に関係なくすべてのAPIが実行できてしまいます。
AWS に慣れている人からしたら当たり前の事かもしれないですね( そもそもリクエスト時にIDプール指定してないですね…😅 )
IAM ポリシーで制御するためには 一時的な認証情報を使用してリクエストヘッダに署名をする必要があり、それを行うと要件を満たすことができました。
準備
1. オーソライザーの作成
API Gateway のメニューからオーソライザーを新規に作成します。
- 名前 (今回は cognito-auth にしました)
- タイプ 「Cognito」
- ユーザープールを選択
- トークンの格納フィールドを 「Authorization」 とする
2. アクセス制御の設定
アクセス制御を設定したいメソッドに対して先程作成した 「cognito-auth」 を指定します。
デプロイ後、トークンが付与されていないリクエストは受け付けません。
$ curl https://38npzjahsi.execute-api.ap-northeast-1.amazonaws.com/prod/cognito
{"message":"Unauthorized"}
実装
import axios from "axios";
import CognitoIdp from "aws-sdk/clients/cognitoidentityserviceprovider";
const COGNITO_API_VERSION = "2016-04-18";
const REGION = "ap-northeast-1";
const COGNITO_USER_POOL_ID = "ユーザープールID";
const COGNITO_CLIENT_ID = "アプリクライアントID";
const USERNAME = "ユーザー名";
const PASSWORD = "パスワード";
const IAM_ACCESS_KEY_ID = "クレデンシャルのためのアクセスキー(マシンで設定済みのものを使用する場合は不要)";
const IAM_SECRET_ACCESS_KEY_ID = "クレデンシャルのためのシークレットキー(マシンで設定済みのものを使用する場合は不要)";
export const cognitoIdpClient = new CognitoIdp({
  apiVersion: COGNITO_API_VERSION,
  region: REGION,
  credentials: { // adminInitiateAuth と adminRespondToAuthChallenge の実行権限を持っているクレデンシャルを使う
    accessKeyId: IAM_ACCESS_KEY_ID,
    secretAccessKey: IAM_SECRET_ACCESS_KEY_ID,
  },
});
// IDトークンを取得
async function getIdToken() {
  const getIdTokenCore = async () => {
    const params: CognitoIdp.AdminInitiateAuthRequest = {
      UserPoolId: COGNITO_USER_POOL_ID,
      ClientId: COGNITO_CLIENT_ID,
      AuthFlow: "ADMIN_USER_PASSWORD_AUTH",
      AuthParameters: {
        USERNAME: USERNAME,
        PASSWORD: PASSWORD,
      },
    };
    const response = await cognitoIdpClient.adminInitiateAuth(params).promise();
    return response;
  };
  let response = await getIdTokenCore();
  // 初めてのログインの場合
  if (response.ChallengeName === "NEW_PASSWORD_REQUIRED") {
    const params: CognitoIdp.AdminRespondToAuthChallengeRequest = {
      ChallengeName: "NEW_PASSWORD_REQUIRED",
      UserPoolId: COGNITO_USER_POOL_ID,
      ClientId: COGNITO_CLIENT_ID,
      ChallengeResponses: {
        USERNAME: USERNAME,
        NEW_PASSWORD: PASSWORD,
      },
      Session: response.Session,
    };
    // パスワードの再設定が面倒なので同じパスワードで状態を「CONFIRMED」に更新
    await cognitoIdpClient.adminRespondToAuthChallenge(params).promise();
    // 更新後 IDトークンを再取得
    response = await getIdTokenCore();
  }
  return response.AuthenticationResult?.IdToken!;
}
async function main() {
  const idToken = await getIdToken();
  const resp = await axios.get(
    `https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/cognito`,
    {
      headers: {
        Authorization: idToken, // ここ
      },
    }
  );
  console.dir(resp.data);
}
main();
まとめ
ユーザープールだけで制御したいという場合は有効な手段だと思います 😇


