この記事では、API GatewayのWebAPIに認証機能を設定する方法を紹介します。
AWSのAPI Gatewayは、作成したWebAPIを簡単に外部公開できて便利なサービスです。
一方で、クラウドサービスであるため、基本的には公開したWebAPIが誰でもアクセス可能となります。
接続元を制限するために、API Gatewayにはリソースポリシーという機能があり、特定のIPアドレスやAWSアカウントからのみにアクセスを制限できます。
しかし、リソースポリシーを利用した方法では、接続元を制限できても、特定の利用者のみに公開する制限はできません。
ではどのように利用者を制限するのでしょうか。
この記事では、API Gatewayで公開しているWebAPIに認証機能を設定し、特定の利用者のみにアクセスを制限する方法を紹介します。
API Gatewayに認証機能を設定する具体的な手順から、認証情報付きでWebAPIを呼び出すソースコードを紹介しています。
ぜひ最後までご覧ください。
全体構成
API Gatewayに認証機能を設定する前後で、どのように構成が異なるか紹介します。
認証機能み設定の構成
認証機能を設定しない場合の構成はこちらです。
こちらの図では、
「クライアントPCから、WebAPIを呼び出してユーザーデータを取得するアプリケーション」
を表しています。
Lambdaで作成したWebAPIを、API Gatewayで公開しています。
ユーザーデータはDynamoDBに格納用に利用、CloudWatchはログ出力用に利用しています。
前述しましたが、この構成だとAPIがグローバルに公開される状態です。
認証機能設定時の構成
認証機能設定後の構成はこちらです。
認証機能を設定していない構成とくらべて「Amazon Cognito」、「Amazon SES」 というサービスが追加されています。
「Amazon Cognito」は認証とアクセス管理を行うサービスです。
Amazon CognitoをAPI Gatewayに設定することで、認証付きのWebAPIが公開できます。
「Amazon SES」はメール送信用のサービスです。
認証機能の設定に直接的な関係はありませんが、Amazon Cognitoを設定するにあたってメールサービスが必要となるので、追加しています。
利用するAWSサービスの紹介
今回利用するAWSサービスを簡単に紹介します。
API Gateway
AWSが提供しているAPI管理サービスです。
このサービスを利用することで開発者は簡単にAPIの作成、公開、管理を行うことが可能となります。
Lambdaで作成したプログラムなどを公開する場合に利用します。
Amazon Cognito
AWSが提供している、認証とアクセス管理を行うサービスです。
ウェブアプリやモバイルアプリのユーザー認証を実現することができます。
Amazon Cognitoでは、ユーザープール、IDプールの二つの主要なコンポーネントがあります。
この後の作業で、必要な知識となるため簡単に説明します。
ユーザープール
アプリケーションのユーザー認証とユーザー情報を行うためのコンポーネントです。
GoogleやFacebookなどのソーシャルログインをサポートしています。認証後、JSONウェブトークン(JWT)が発行され、アプリケーションの認証に利用します。
ウェブアプリケーションの認証のみで、AWSリソースへのアクセスはLambdaなどで行い、クライアントアプリからAWSリソースを利用しない場合は、こちらを利用します。
IDプール
認証されたユーザにAWSリソースへのアクセス権限を提供します。
これにより、ユーザーはS3やDynamoDBのリソースにアクセスすることができます。クライアントアプリからAWSリソースにアクセスする場合はこちらを利用します。
公式ドキュメントはこちらです。
Amazon SES
AWSが提供しているEメール送信サービスです。
アプリケーションやウェブサイトからEメールを送信するときに利用します。
Amazon Cognitoを利用する時にメール配信設定が必要となるため、このサービスを利用します。
APIGatewayの認証設定
API Gatewayに認証機能を設定していきます。
- Step1Amazon SESの作成
- Step2Amazon Coginitoの作成(ユーザープールの作成)
- Step3API GatewayとCognitoの連携設定
- Step4設定した認証のテスト
- Step5Amazon Coginitoの作成(IDプールの作成)
- Step6設定した認可のテスト
- Step7API Gatewayにオーソライザーの設定
Amazon SESの作成
Amazon Cognitoの利用には、利用者へ通知するためにEメールを利用します。
手順の中でEメールサービスが必要となるので、Amazon SES で作成しておきます。
マネジメントコンソールでAmazon SESの機能に移動します。
IDの作成ボタンからEメールアドレスを選択し作成します。
作成後は、以下の状態でステータスは「検証保留中」となります。
AWSからメールが送信されているので、本文内のリンクをクリックします。
リンクをクリックし、以下のページが表示されれば検証は成功です。
Amazon Cognitoの作成(ユーザープール)
アプリケーションの認証に利用するための「ユーザープール」を作成します。
ユーザープールの作成
マネジメントコンソールでCognitoの機能に移動し、ユーザープールを作成していきます。
「ユーザープールを作成」ボタンをクリックし、プロバイダーの設定画面を表示します。
Amazon CognitoではSNSを使ったログインを設定できます。
サインインエクスエクスペリエンスの設定
サインインの設定です。
GoogleなどのSNSを使ったログインを行う場合は「フェデレーテッドアイデンティプロバイダー」をオンにします。
今回の検証では、SNSでのログインは設定しないので、「フェデレーテッドアイデンティプロバイダー」はオフにしておきます。
セキュリティ要件の設定
セキュリティの設定です。
「Amazon Cognito」では、MFA認証を設定することができます。
今回は作業をシンプルにするため、MFAの認証設定はなしで作成してみます。
ユーザーアカウントの復旧方法の設定
ユーザーアカウントの復旧方法を設定します。
Eメールで復旧できるようにします。
サインアップエクスペリエンスの設定
サインアップの設定です。
「Amazon Cognito」では、自分でユーザーを登録できる設定(自己登録の有効化)もあります。
今回は検証なので、「自己登録の有効化」のチェックは外しておきます。他は全てデフォルト設定です。
メール配信の設定
メール配信の設定です。
利用者にどのメールサービスをつかって配信するかを設定します。
ここで先ほど設定したメールアドレスを送信元として設定します。
IAMロールの設定
IAMロールの設定です。
IAMロール名は任意の名称を設定してください。
わたしはアプリケーション名を「test-web-app」とするので、ロール名は分かりやすく「test-web-app-role」としました。
アプリケーション統合の設定
アプリケーション統合の設定です。
ユーザープール名には任意のわかりやすい名称を設定します。
「CognitoのホストされたUIを使用」をオンにすることで、Cognitoが提供しているログイン画面が利用できます。
ログイン画面を自前で作る必要がなく便利なので、オンにします。
ドメインの設定
ドメインの設定です。
自前で用意したドメインを設定する場合は、「カスタムドメインを使用」をオンにします。
今回はCognitoが用意するドメインを利用するので、「Cognitoドメインを使用する」をオンにします。
アプリケーションクライアントの設定
アプリケーションクライアント名に任意の名前を入力します。
今回は「test-web-app」という名称で設定します。
認証方法の設定
認証方法の設定です。
今回は、ユーザーID、パスワードで認証を行う設定にします。
「ALLOW_ADMIN_USER_PASSWORD_AUTH」と「ALLOW_USER_SRP_AUTH」にチェックを入れます。
「許可されているコールバックURL」にログイン成功後に表示するURLを設定します。
設定内容に間違いがないことを確認し、「ユーザープールを作成」ボタンをクリックします。
これでユーザープールが作成されました。
Amazon Cognitoの動作確認
認証の動作確認用にユーザーを作成します。
初回のみ利用できる、仮パスワードを設定します。初回ログイン時に利用者自身でパスワードの変更が必要になります。
動作確認
Amazon Cognito でログインできるか確認してみます。
作成したユーザープールを選択し、「ホストされたUIを表示」ボタンをクリックします。
ログイン画面が表示されるので、上で作成したユーザー情報でログインします。
ログインに成功するとパスワードの変更が求められます。
パスワード変更後、こちらのように「サーバが見つかりません」が表示されますが問題ありません。
コールバックURLに指定した画面が存在しないため、表示されます。
API GatewayとCognitoの連携設定
API Gatewayに作成したAmazon Cognitoを設定します。
この設定をすることで認証機能付きでWebAPIが公開できます。
カスタムオーソライザーの作成
Amazon Cognitoとの連携には、API Gatewayのカスタムオーソライザーという機能を使います。
マネジメントコンソールからAPI Gatewayの機能に移動し、オーソライザーを作成していきます。
オーソライザーの作成はシンプルです。
オーソライザー名は任意の名前を設定し、Cognitoのどのユーザープールで認証するかを設定します。
Amazon Cognitoでは、「Authorization」という項目にトークンが設定されるので、トークンのソースに「Authorization」を追加します。
設定した認証のテスト
オーソライザーが正しく作成されたかテストしてみます。
認証の流れはこちらです。
- Step1ユーザーID/パスワードの検証
- Step2正しい場合、CognitoからIDトークンの払い出し
- Step3払い出されたIDトークンをリクエスト情報に追加しWebAPI呼び出し
- Step4正しいIDトークンであればWebAPIが実行される
マネジメントコンソール上でテストします。
API Gatewayの「オーソライザーをテスト」ボタンをクリックします。
「Authorization」項目に払い出されたIDトークンを設定します。
IDトークンの払い出し
認証を成功させるためには、IDトークンが必要となります。
AWS CLIを利用し、以下コマンドを実行します。ユーザー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」部分が払い出されたIDトークンです。こちらをコピーしておきます。
AWSマネジメントコンソールで、テストしてみます。
ステータスコードが「200」、「403」となっていれば、認証成功です。
※403でも問題ありません。この後の認可の設定後、200の結果となります。
認証失敗時の場合は、401エラー(認証エラー)となります。
トークンのコピー範囲が間違っていないか確認してください。
Amazon Cognitoの作成(IDプールの作成)
次は認可の設定です。
API Gatewayを実行可能とする権限を設定します。
Amazon Cognitoの機能から、IDプールを作成していきます。
ユーザープールで認証されたユーザーにのみアクセスできるように設定します。
「基本フローをアクティブ化」にチェックしておきます。
確認画面が表示されるので、設定値に問題がなければ作成します。
作成後、こちらの画面に移動します。
作成したIDプールのロールにAPIGatewayを実行できる権限を付与します。
今回は検証なので、フルアクセスを設定します。
設定した認可のテスト
再度オーソライザーでテストし、以下のように結果が返却されれば成功です。
API GatewayのWebAPIにオーソライザーの設定
作成したオーソライザーを認証機能をつけたいWebAPIに設定していきます。
オーソライザーはリソース単位、メソッド単位で設定することができるので認証範囲を細かくコントロールが可能です。
「データ取得用のAPIは認証なし」、「データ更新用のAPIは認証あり」のような設定が可能です。
「認可」の部分を作成したCognitoのユーザープールを設定します。
以上で設定は完了です。
認証機能付きのWebAPIになっているか確認してみます。
リクエストヘッダーにトークンIDを設定しないとアクセスできないことを確認します。
こちらのアプリケーションは、認証機能設定前にWebAPIを呼び出しているアプリケーションです。
今回の設定前は以下のようにアクセスできており、コンソールにエラーは表示されてません。
データ取得用のリソースに、Cognitoを設定したAPI Gatewayをデプロイします。
デプロイ後、再度リクエストを実行すると401(認証エラー)が返却されるようになりました。
正しく認証認可の設定がされています。
ソースコードの修正
ここではWebAPI呼び出ししている部分を認証情報付きで呼び出すように修正していきます。
Amazon CognitoからIDトークンを取得し、IDトークンを含めてWebAPIを呼び出すようにします。
ソースコードはReactで作成されています。
AWS SDKライブラリの導入
Amazon Cognitoと連携するためのライブラリを導入します。
以下のコマンドでプロジェクトにライブラリをインストールします。
npm install amazon-cognito-identity-js
認証機能付きAPIを呼び出すソースコード
Cognitoへ認証を行い、返却されたIDトークンを使ってAPIGatewayを呼び出すソースコードを紹介します。
APIへのリクエスト実行前にCognitoからIDトークンを取得。WebAPI実行時に取得したIDトークンをheaderに設定することで認証付きWebAPIを呼び出すことができます。
Amazon Cognito用のライブラリのインポート
Amazon Cognitoを利用するための機能をインポートします。
import { CognitoUserPool, CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js';
IDトークンを取得するメソッドの作成
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);
}
});
});
}
Cognitoと連携して、IDトークンを取得する「getIdToken」メソッドです。
UserPoolID、ClientID、username、passwordは自身の環境のものを設定ください。
リクエストのヘッダーにIDトークンを設定
// 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
},
});
headersの「Authorization」にCognitoから取得したIDトークンを設定します。
urlは自身の環境のものに変更してください。
ソース全体
ソース全体はこちらのようになります。
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;
APIGatewayに認証機能を設定する方法まとめ
この記事では、Amazon Cognitoを利用して、認証機能付きのWebAPIを公開する方法を紹介しました。
Amazon CognitoをAPI Gatewayに設定することで、Amazon Cognitoで認証されたユーザーのみがAPIを実行できるようになります。
Amazon Cognitoを利用することで認証・認可機能を簡単に実現できます。ぜひ利用して開発の生産性を向上させてみてください。
本記事が皆様の参考になりましたら幸いです。
コメント