APIGatewayにCognitoを連携して認証機能を設定する

awsアプリケーション開発

本記事では、Amazon API Gatewayで公開しているAPIへのアクセスを認証付きで公開する方法についてご紹介します。

Z-A-K-I
Z-A-K-I

API Gatewayを認証付きで公開したい!

API GatewayのAPI呼び出し時の認証方法が知りたい!

と、思ってる方におすすめです。

この記事では、以下のことを紹介してます。

  • API GatewayとCognitoを連携した認証設定
  • Reactから認証情報付きでAPI を呼び出す方法

Amazon API GatewayでAPIを公開すると、グローバルに公開され誰でもアクセスが可能となります。

API GatewayのリソースポリシーでIPアドレスや特定のAWSアカウントからのアクセスのみに制限することも可能ですが、特定の利用者へ公開したい場合、この制限では要件を満たせません。

API Gatewayに認証設定を行うことで、特定の利用者のみへのアクセス制限が可能となります。

API GatewayとAmazon Cognitoを連携して認証機能を設定する方法を、手順を追って紹介していきます。

認証付きAPIGatewayの全体構成

ますは、認証機能設定前と設定後で、どのように構成が異なるか紹介します。

変更前の構成

元々の構成は以下です。

開発者のPCにあるHTML、JSから、APIGatewayを介してLambdaのプログラムを呼び出しています。LambdaはDynamoDBから要求に応じたデータを取得し、クライアントに返却しています。

APIGatewayでLambdaを公開すると、グローバルに公開されるので、その対策としてAPIGatewayのリソースポリシーの設定で特定のIPアドレスからのみアクセスできるように設定しています。

API Gateway、LambdaともにCloudWatchにログを出力しています。

認証設定後の構成

API GatewatyとAmazon Cognitoを連携させて、IPアドレスで制限していたところを特定の利用者のみアクセスできるように変更します。

本記事の設定作業後は、以下の構成になります。CognitoとSESのサービスが増えてます。

APIGatewayとは

AWSが提供するAPI管理サービスです。このサービスを利用することで開発者は簡単にAPIの作成、公開、管理を行うことが可能となります。Lambdaで作成したプログラムなどを公開する場合に利用します。

LambdaとAPI Gatewayを連携させて外部に公開する手順は以下で紹介してます。詳しく知りたい方はこちらを参照ください。

Amazon Cognitoとは

AWSが提供する認証とアクセス管理を提供するサービスです。

このサービスを利用することで、ウェブアプリやモバイルアプリのユーザー認証を実現することができます。

Cognitoは、ユーザープール、IDプールの二つの主要なコンポーネントがあります。認証を行う上で、必要不可欠な知識となるため簡単に説明します。

ユーザープールとは

アプリケーションのユーザー認証とユーザー情報を行うためのコンポーネントです。

GoogleやFacebookなどのソーシャルログインをサポートしています。認証後、JSONウェブトークン(JWT)が発行され、アプリケーションの認証に利用します。

ウェブアプリケーションの認証のみで、AWSリソースへのアクセスはLambdaなどで行い、クライアントアプリからAWSリソースを利用しない場合は、こちらを利用します。

IDプールとは

認証されたユーザにAWSリソースへのアクセス権限を提供します。

これにより、ユーザーはS3やDynamoDBのリソースにアクセスすることができます。クライアントアプリからAWSリソースにアクセスする場合はこちらを利用します。

Amazon SESとは

AWSが提供するEメール送信サービスです。

アプリケーションやウェブサイトからEメールを送信する際に利用します。Cognitoからメール配信が必要となるため、このサービスを利用します。

API Gatewayの認証設定

API Gatewayに認証機能を設定していきます。

以下のステップで作成していきます。

  1. Amazon SESの作成
  2. Amazon Coginitoの作成(ユーザープールの作成)
  3. API GatewayとCognitoの連携設定
  4. 設定した認証のテスト
  5. Amazon Coginitoの作成(IDプールの作成)
  6. 設定した認可のテスト
  7. API Gatewayにオーソライザーの設定

Amazon SESの作成

Cognitoの設定で必要となるので作成しておきます。

マネジメントコンソールでAmazonSESの機能に移動します。IDの作成ボタンからEメールアドレスを選択し作成します。

作成後は、以下の状態でステータスは「検証保留中」となります。

AWSからメールが送信されているので、本文内のリンクをクリックします。

リンクをクリックし、以下ページが表示されれば検証は成功です。

Amazon Cognitoの作成(ユーザープール)

マネジメントコンソールでCognitoの機能に移動し、ユーザープールを作成していきます。

プロバイダーの設定です。作業をシンプルにするため、SNSでもログインできる設定の「フェデレーテッドアイデンティプロバイダー」はオフにしておきます。

セキュリティの設定です。こちらも作業をシンプルにするため、まずはMFAの認証設定なしで作成してみます。

サインアップの設定です。検証のため作成するので、「自己登録の有効化」のチェックは外しておきます。ここ以外は全てデフォルト設定です。

メール配信の設定です。先ほど設定したメールアドレスを送信元として設定します。

IAMロールの設定です。今回は「test-web-app-role」とし新規に作成します。

アプリケーションクライアントの設定です。ユーザープール名にはわかりやすい名称を設定します。

アプリケーションクライアント名に任意の名前を入力します。

ユーザーID、パスワードで認証を行うので、認証フローを以下に設定します。

設定内容に間違いがないことを確認し、「ユーザープールを作成」ボタンをクリックします。

ユーザープールが作成されました。

テスト用にユーザーを作成しておきます。

作成直後は、仮パスワードなため、今作成したユーザーのパスワードを変更します。作成したユーザープールを選択し、「ホストされたUIを表示」ボタンをクリックします。

以下画面が表示されるので、ログインします。

認証が成功するとパスワード変更画面が表示されるので、変更します。

コールバックURLに指定した画面が存在しないので、以下画面が表示されますが問題ありません。

API GatewayとCognitoの連携設定

APIGatewayのカスタムオーソライザーという機能を使って、 Cognitoと連携します。

マネジメントコンソールからAPI Gatewayの機能に移動します。
オーソライザーを選択し、作成していきます。

以下の設定でオーソライザーを作成します。オーソライザー名は任意の名前を設定します。Authorizationにトークンが設定されるので、トークンのソースに追加します。

設定した認証のテスト

オーソライザーが正しく作成されたかテストしてみます。

認証フローは、 ユーザーID/パスワードが正しい場合、IDトークンが払い出されます。

払い出されたIDトークンを他のリクエスト情報と一緒にAPIGatewayに送信することで、APIGatewayにアクセスすることができます。

テストはマネジメントコンソール上で行います。API Gatewayの「オーソライザーをテスト」ボタンをクリックします。間違ったトークンを設定すると以下のように401(認証エラー)が返却されます。

認証を成功させるためには、IDトークンが必要となります。AWS CLIを利用しIDトークンを取得します。

認証を行うCognitoのユーザープールのID、クライアントアプリのID、アクセスするユーザーのID/パスワードを設定し、以下のコマンドを実行します。

aws cognito-idp admin-initiate-auth --user-pool-id 「CognitoのユーザープールID」 —client-id 「アプリクライアントID」 --auth-flow ADMIN_NO_SRP_AUTH --auth-parameters USERNAME=「ユーザ名」,PASSWORD=「パスワード」

実行後、以下の結果が返却されますので「IDToken」をコピーします。

AWSマネジメントコンソールで、再度テストしてみます。
以下のように変わっていれば認証の設定は成功です。

※403でもOKでも問題ありません。この後の認可の設定後、200の結果となります。

Amazon Cognitoの作成(IDプールの作成)

次は認可の設定をしていきます。APIGatewayを実行する権限を設定していきます。

Amazon Cognitoの機能から、IDプールを作成していきます。

ユーザープールで認証されたユーザーにのみアクセスできるように設定します。

「基本フローをアクティブ化」にチェックしておきます。

確認画面が表示されるので、設定値に問題がなければ作成します。
作成後以下の画面に移動します。

作成したIDプールのロールにAPIGatewayを実行できる権限を付与します。
今回は検証なので、フルアクセスを設定します。

設定した認可のテスト

再度オーソライザーでテストし、以下のように結果が返却されれば成功です。

API Gatewayにオーソライザーの設定

作成したオーソライザーをAPIに設定していきます。
オーソライザーはリソース単位、メソッド単位で設定することができます。データ取得用のAPIは認証なし、データ更新用のAPIは認証あり、のような設定が可能です。

設定は以下画面で行います。「認可」の部分を作成したCognitoのユーザープールを設定します。

正しく設定されたかテストしてみます。

リクエストヘッダーにトークンIDを設定しないとアクセスできないことを確認します。

以下の記事で作成したアプリケーションを起動し確認します。変更前は以下のようにアクセスできてできてます。コンソールにもエラーは表示されてません。

データ取得用のリソースに認可としてCognitoを設定し、デプロイします。

デプロイ後、再度リクエストを実行すると今回は401(認証エラー)が返却されるようになりました。正しく認証認可の設定がされています。

ソースコードの修正

APIの呼び出しには、IDトークンが必要となるため、IDトークンを取得し、そのIDトークンを利用して、APIを呼び出すように修正します。

AWS SDKライブラリの導入

Cognitoと連携するため、以下コマンドを実行しライブラリを導入します。

npm install amazon-cognito-identity-js

ソースコードの修正

Cognitoへ認証を行い、返却されたIDトークンを使ってAPIGatewayを呼び出すように修正します。
変更後のコードは以下となります。APIへのリクエスト実行前にCognitoから取得し、API実行時に取得したIDトークンをheaderに設定するよう変更しています。

変更前のコードは以下記事を参照ください。

import React, { useState } from 'react';
import axios from 'axios';
import './App.css';
import Box from '@mui/material/Box';

import EditIcon from '@mui/icons-material/Edit';

import Button from '@mui/material/Button';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
import SaveIcon from '@mui/icons-material/Save';
import CancelIcon from '@mui/icons-material/Close';
import {
  DataGrid,
  GridRowModes,
  GridToolbarContainer,
  GridActionsCellItem,
  GridRowEditStopReasons,
} from '@mui/x-data-grid';
import {
  randomId,
} from '@mui/x-data-grid-generator';

import TextField from '@mui/material/TextField';

import { CognitoUserPool, CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js';


function App() {

  const [name, setName] = useState('');

  const getIdToken = async () =>{
    // CognitoのUser Poolの設定
    const poolData = {
      UserPoolId: 'ap-northeast-xxxxx', // あなたのCognito User Pool ID
      ClientId: 'xxxxxxx' // あなたのCognito User PoolのクライアントID
    };

    const userPool = new CognitoUserPool(poolData);
    const username = 'xxxx';
    const password = 'xxxxx';
    const authenticationData = { Username: username, Password: password };
    const authenticationDetails = new AuthenticationDetails(authenticationData);
    const userData = { Username: username, Pool: userPool };
    const cognitoUser = new CognitoUser(userData);

    return new Promise((resolve, reject) => {
      cognitoUser.authenticateUser(authenticationDetails, {
        onSuccess: (session) => {
          const idToken = session.getIdToken().getJwtToken();
          resolve(idToken);
        },
        onFailure: (err) => {
          reject(err);
        }
      });
    });
  }


  const handleSearch = async () => {

    // CognitoからIDトークンを取得
    let idToken = await getIdToken();

    try {

      const response = await axios({
        method: 'post',
        url: 'https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/test/get-user-data',
        data: {
          queryStringParameters: {
            name: name
          }
        },
        headers: {
          Authorization: idToken
        },
      });
      const usersData = JSON.parse(response.data.body);
      const editableUsersData = usersData.map(user => ({
        ...user,
        editId: user.id,
        editName: user.name,
        editAge: user.age,
        editEmail: user.email
      }));
      setRows(editableUsersData);
    } catch (error) {
      console.error('検索中にエラーが発生しました:', error);
    }
  };

  // 更新ボタンの処理
  const handleUpdate = async (updUser) => {

    // CognitoからIDトークンを取得
    let idToken = await getIdToken();

    try {
      const response = await axios({
        method: 'post',
        url: 'https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/test/update-user-data', // 実際のエンドポイントURLに置き換えてください
        data: {
          queryStringParameters: {
            id: updUser.id === undefined ? "" : updUser.id,
            name: updUser.name === undefined ? "" : updUser.name,
            age: updUser.age === undefined ? "" : updUser.age,
            email: updUser.email === undefined ? "" : updUser.email
          }
        },
        headers: {
          Authorization: idToken
        }
      });
      // 処理結果を状態にセット
      // setUpdateStatus({ ...updateStatus, [index]: '処理が正常終了しました' });
    } catch (error) {
      // エラー処理
      // setUpdateStatus({ ...updateStatus, [index]: 'エラーが発生しました' });
    }
  };

  // 新しいデータを登録する関数
  const handleRegister = async (newUser) => {
    
    // CognitoからIDトークンを取得
    let idToken = await getIdToken();

    try {
      const response = await axios({
        method: 'post',
        url: 'https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/test/update-user-data', // 実際のエンドポイントURLに置き換えてください
        data: {
          queryStringParameters: {
            id: newUser.id === undefined ? "" : newUser.id,
            name: newUser.name === undefined ? "" : newUser.name,
            age: newUser.age === undefined ? "" : newUser.age,
            email: newUser.email === undefined ? "" : newUser.email
          }
        },
        headers: {
          Authorization: idToken
        },
      });
      // 登録成功時の処理
      console.log('データが正常に登録されました。', response);
      
    } catch (error) {
      // 登録失敗時のエラー処理
      console.error('データ登録中にエラーが発生しました。', error);
    }
  };

  // ツールバーコンポーネント
  function EditToolbar(props) {
    const { setRows, setRowModesModel } = props;
  
    const handleClick = () => {
      const id = randomId();
      setRows((oldRows) => [...oldRows, { id, name: '', age: '', isNew: true }]);
      setRowModesModel((oldModel) => ({
        ...oldModel,
        [id]: { mode: GridRowModes.Edit, fieldToFocus: 'name' },
      }));
    };
  
    return (
      <GridToolbarContainer>
        <Button color="primary" startIcon={<AddIcon />} onClick={handleClick}>
          Add record
        </Button>
      </GridToolbarContainer>
    );
  }

  const [rows, setRows] = React.useState([]);
  const [rowModesModel, setRowModesModel] = React.useState({});

  const handleRowEditStop = (params, event) => {
    if (params.reason === GridRowEditStopReasons.rowFocusOut) {
      event.defaultMuiPrevented = true;
    }
  };
  //  編集ボタンクリック
  const handleEditClick = (id) => () => {
    setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
  };
  // 編集ボタンクリック
  const handleSaveClick = (id) => () => {
    setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
  };
  // 削除ボタンクリック
  const handleDeleteClick = (id) => () => {
    setRows(rows.filter((row) => row.id !== id));
  };
  // キャンセルボタンクリック
  const handleCancelClick = (id) => () => {
    setRowModesModel({
      ...rowModesModel,
      [id]: { mode: GridRowModes.View, ignoreModifications: true },
    });
    const editedRow = rows.find((row) => row.id === id);
    if (editedRow !== undefined && editedRow.isNew) {
      setRows(rows.filter((row) => row.id !== id));
    }
  };

    // 更新処理
  const processRowUpdate = (newRow) => {
    const updatedRow = { ...newRow, isNew: false };
    if (newRow.isNew){
      handleRegister(newRow);
    } else {
      handleUpdate(newRow);
    }
    setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row)));

    return updatedRow;
  };

  const handleRowModesModelChange = (newRowModesModel) => {
    setRowModesModel(newRowModesModel);
  };

  // グリッドの項目定義
  const columns = [
    { field: 'id', headerName: 'ID', width: 100, editable: true },
    { field: 'name', headerName: '名前', width: 180, editable: true },
    {
      field: 'age',
      headerName: '年齢',
      width: 80,
      align: 'left',
      headerAlign: 'left',
      editable: true,
    },
    {
      field: 'email',
      headerName: 'メール',
      width: 220,
      editable: true,
     },
    {
      field: 'actions',
      type: 'actions',
      headerName: 'Actions',
      width: 100,
      cellClassName: 'actions',
      getActions: ({ id }) => {
        const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;

        if (isInEditMode) {
          return [
            <GridActionsCellItem
              icon={<SaveIcon />}
              label="Save"
              sx={{
                color: 'primary.main',
              }}
              onClick={handleSaveClick(id)}
            />,
            <GridActionsCellItem
              icon={<CancelIcon />}
              label="Cancel"
              className="textPrimary"
              onClick={handleCancelClick(id)}
              color="inherit"
            />,
          ];
        }

        return [
          <GridActionsCellItem
            icon={<EditIcon />}
            label="Edit"
            className="textPrimary"
            onClick={handleEditClick(id)}
            color="inherit"
          />,
          <GridActionsCellItem
            icon={<DeleteIcon />}
            label="Delete"
            onClick={handleDeleteClick(id)}
            color="inherit"
          />,
        ];
      },
    },
  ];

  return (
    <Box m={2}>
      <div>
        <h3>検索条件</h3>
        <TextField id="standard-basic" label="名前" variant="standard"
        onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
        value={name}
            onChange={(e) => setName(e.target.value)} />
      </div>
      <div>
        <h3>検索結果</h3>
        {/* グリッド部分 */}
        <Box
          sx={{
            height: 500,
            width: '100%',
            '& .actions': {
              color: 'text.secondary',
            },
            '& .textPrimary': {
              color: 'text.primary',
            },
          }}
        >
        <DataGrid
          rows={rows}
          columns={columns}
          editMode="row"
          rowModesModel={rowModesModel}
          onRowModesModelChange={handleRowModesModelChange}
          onRowEditStop={handleRowEditStop}
          processRowUpdate={processRowUpdate}
          slots={{
            toolbar: EditToolbar,
          }}
          slotProps={{
            toolbar: { setRows, setRowModesModel },
          }}
        />
      </Box>
    </div>
  </Box>
  );
}

export default App;

まとめ

API Gatewayに認証機能の設定方法まとめ

本記事では、AmazonCognitoをAPI Gatewayに設定して、Cognitoで認証されたユーザーのみAPIを実行できるようにしました。

AWS Cognitoを利用すると簡単に認証・認可機能を設定することができます。本記事ではユーザーID、パスワードをコード内で設定しましたが、今後は、利用者がログイン画面などで入力した情報を利用できるように変更していきます。

皆様の参考になりましたら幸いです。

コメント

タイトルとURLをコピーしました