本記事は、「Keycloakを使ってAPI GatewayでAPIをアクセス制限する」の記事を元に、Amazon API Gatewayを使ったアクセス制限(認可)として、クライアントアプリ向けのAPIサーバーをAmazon API GatewayとAWS lamdaで構築して、どのようにアクセス制限が行えるのか検証する。
本記事でやること
- Keycloakサーバ設定
- Lambda関数作成
- API Gateway作成
- 動作検証
Amazon API Gatewayとは
Amazon API Gatewayは、様々な規模の REST、HTTPなどのAPIを作成、公開、維持、モニタリング、セキュア化できる AWS のフルマネージドサービスのことである。
世間一般的なAPI Gatewayとは何か、についてはこちらの記事で説明されているので本記事では割愛する。
今回の構築する構成
以下のような環境を構築する。
認証についてはKeycloakを利用して、
APIのアクセス制御に関してはAPI GatewayのLambda オーソライザー機能を利用してトークン(JWT)の検証処理を行う。
Keycloakサーバ設定
レルムを作成
Realm nameをapi-gateway
としてレルムを作成する。
ユーザー作成
作成したレルムに所属するユーザーとして、今回は検証用に以下の2つのユーザーを作成する。
ユーザー名 | 用途 |
---|---|
testuser1 | ロール未所属のユーザー |
testuser2 |
api-gateway-role のロールに所属するユーザー |
ロール作成&ユーザー追加
ロール名をapi-gateway-role
としてロールを作成し、作成済みのtestuser2
ユーザーを作成したロールに所属させる。
Keycloakの公開鍵情報取得
左のメニューからレルムの設定
→鍵
タブを押下して、公開鍵情報を確認する。RS256
の公開
ボタンを押下することで、公開鍵情報を表示される。
公開鍵はLambdaオーソライザーのトークン検証処理の実装時に利用するため、後で確認できるようにメモしておく。
Lambda関数作成
AWSのLambdaサービスの画面を開き、関数の作成
ボタンから以下の2つのLambda関数を作成する。ランタイムなどは何でもよいが、今回はNode.jsで作成した。
API業務処理関数作成
業務用の関数はほぼデフォルトのままとし、以下の通り返却するJson文字列だけ変更した。
export const handler = async(event) => {
const response = {
statusCode: 200,
body: JSON.stringify('Get resource!!!'),
};
return response;
};
オーソライザー用関数作成
オーソライザー関数についてはこちらをベースに、トークン検証用にjsonwebtokenを利用する形で実装した。
import * as jsonwebtoken from 'jsonwebtoken';
export const handler = function(event, context, callback) {
// トークンを取得
var token = event.authorizationToken.replace("Bearer ", "");
// Keycloak側の公開鍵
const publicKey = "-----BEGIN PUBLIC KEY-----\r\n"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..."
+ "\r\n-----END PUBLIC KEY-----";
// トークンの検証
jsonwebtoken.verify(token, publicKey, (err, decoded) => {
if (err) {
// 検証NG (403 Error)
console.log(err);
callback(null, generatePolicy('user', 'Deny', event.methodArn));
} else {
// 検証OK
var roles = decoded.realm_access.roles
if(roles.includes('api_gateway_test_role')) {
callback(null, generatePolicy('user', 'Allow', event.methodArn));
} else {
// 検証NG (403 Error)
console.log('権限なし');
callback(null, generatePolicy('user', 'Deny', event.methodArn));
}
}
});
};
// ポリシーを作成
var generatePolicy = (principalId, effect, resource) => {
var authResponse = {};
authResponse.principalId = principalId;
if (effect && resource) {
var policyDocument = {};
policyDocument.Version = '2012-10-17';
policyDocument.Statement = [];
var statementOne = {};
statementOne.Action = 'execute-api:Invoke';
statementOne.Effect = effect;
statementOne.Resource = resource;
policyDocument.Statement[0] = statementOne;
authResponse.policyDocument = policyDocument;
}
return authResponse;
}
オーソライザー関数では以下のような処理を行っている。
- リクエストヘッダから
Bearer
を除去したトークンを取得 - Keycloakの公開鍵を用いてトークンを検証
- 検証成功時、デコードしたトークンからroleの情報を取り出して
api_gateway_test_role
のロールを持っているか確認
※トークン検証NGの時などは、エラーとして403応答が返却 - ロール保持時、Invokeの権限を付与したポリシーを作成してコールバック
※ロール非保持の場合、権限をDenyに設定してコールバック
本記事では公開鍵を直接Lambda関数に埋め込んだが、JWKsエンドをKeycloakで設定することで都度参照するようにすることも可能
API Gateway作成
API作成
REST API
を選択してAPIを作成する。API名以外はデフォルトの設定値のままとする。
リソース&メソッド作成
API一覧から作成したAPIを選択して、アクション
ボタン → リソース作成
ボタンを押下してリソースの作成画面を開く。
リソース名を入力して、リソース作成
ボタンを押下すると、以下のように作成したリソースがAPIに追加される。
次にメソッドを作成する。
作成したリソースを選択した状態で、アクション
ボタン → メソッドの作成
ボタンを押下すると、リソースの下にメソッドのセレクトボックスが表示される。GET
を選択してリストの右にある✓ボタンを押下するとメソッドの設定画面が開かれるので作成したLambda関数等を設定していく。
Lamda関数の欄に事前に作成済みの関数名を入力して保存ボタンを押下する。
これでAPIのtest_resourceパスのGETメソッドに作成したLambda関数が統合された。
APIデプロイ
作成したAPIを外部からアクセスできるようにデプロイを行う。
アクション
ボタン → メソッドの作成
ボタンを押下してデプロイするステージを選択する。ステージが未作成の場合は、新規作成選択およびステージ名を入力してからデプロイボタンを押下する。
デプロイすると以下のようにデプロイしたリソースがステージに含まれる。
テスト実行
ここで作成したAPIを実行して動作確認してみる。現時点ではアクセス制限をしていない状態のため、API Gatewayは単純なプロキシーをする動きのみとなっている。
APIを実行するクライアントとしてPostmanを利用して、以下のようにtest_resourceパスのGETでリクエストを送信する。
リクエストにはアクセストークンなど一切設定していないがひとまずAPIの正常疎通が確認できた。
オーソライザー作成
最後にAPI Gatewayにオーソライザーを作成する。
API Gatewayの左側のメニューからオーソライザー
を選択して、新しいオーソライザーの作成
を押下してオーソライザーを作成する。
設定項目 | 設定内容 |
---|---|
名前 | 任意でOK |
Lambda関数 | 事前に作成したオーソライザー用Lambda関数 |
Lambda呼び出しロール | 未設定でOK |
トークンのソース | トークンとして取得するHTTPヘッダの名称 |
トークン検証 | トークンのバリデーション |
認可のキャッシュ | 任意で設定 |
トークンソースのソースは実際のHTTPヘッダの名称を指定するが、Lambda関数内で取得するときはauthorizationTokenとなる。
続けて作成したオーソライザーをAPIに設定する。
リソースからオーソライザーを設定するメソッドを選択して。メソッドリクエスト
の部分を押下する。
メソッドリクエストの設定画面が開くので、認可
の部分で作成したオーソライザーを設定する。
ここまで完了したらオーソライザーを適用したAPIを再度デプロイして準備完了。
動作検証
今回作成したAPIはapi_gateway_test_role
を持っているユーザーのみアクセスを許可するようにオーソライザーを設定しているので、対象のロールを持っていないユーザー、持っているユーザーのトークンを用いてAPIを実行し、期待通りに制限できているか検証する。
まず、対象のロールを持っていないtestuser1
で取得したトークンでAPIを実行する。
トークン取得についてはKeycloak標準であるaccount_connsoleなどでログインして取得し、PostmanのAuthorization
ヘッダに付与して実行する。
その結果、以下のように必要なロールを保持していないため認可されず、403応答でレスポンスが返却される。
Lambdaのログを確認するとしっかり検証失敗時に埋め込んだログが出力されているため、オーソライザーとして作成したLambda関数で検証されていることが確認できる。
次に、対象のロールを持っているtestuser2
で取得したトークンでAPIを実行する。
先ほど同様に取得したトークンをPostmanのAuthorization
ヘッダに付与して実行すると、以下のように必要なロールを保持しているため認可され、200応答でレスポンスが返却される。
以上でAmazon API GatewayとKeycloakを使ってAPIのアクセス制限を確認することができた。
最後に
今回は簡易的な検証として、トークン(JWT)にある属性値の一致をみるだけのアクセス制限(認可)でした。実際にOIDCとしての認可ではトークン・インストロスペクション・エンドポイントへトークンを送り、現在アクティブかどうかや権限情報を取得するような動きとすべきかと思います。そのあたりについては機会があればまた検証してみたいと思います。