カテゴリー
【AWSで構築するサーバレスWebSocket②】 WebSocketAPIにオーソライザーを設定する
※ 当ページには【広告/PR】を含む場合があります。
2021/07/25

今回は次なる段階として、作成したWebSocket APIにLambdaオーソライザーを設定して、アクセス制限をする方法を検討してみたいと思います。
WebSocket APIのアクセス保護って?
前回の話まででは、WebSocket APIのURLを知っていれば、不特定多数のクライアントからアクセスすることが可能です。
$ wscat -c wss://[APIのドメインID].[APIのリージョン名].amazonaws.com/[APIをデプロイしたステージ名]
#👆誰でもアクセス可能!
確かにWebSocket APIのURLエンドポイントさえ情報漏洩しなければ不特定多数の人間からは使えないようにすることは出来るのですが、せめてサービス提供側から与えられたパスワードのような利用者しか知らない認証情報などが無いとそういった運用法は安全面で限りなく不安です。
さすがに素のWebSocketアプリのまま使うことはセキュリティ上宜しくありませんので、何かしらの認証機能をWebSocket APIに施す必要があります。 そこでWebSocket APIのオーソライザの利用を検討しましょう。
現在、WebSocket APIのオーソライザはLambda型のみしか対応していません。 今後もしかしたら直接的にCognitoオーソライザによる認証機能が追加されるかも知れませんが、基本的にLambdaによる認証ハンドラによって操作を実現するしかありません。
よって、WebページなどでSPAとしてWebSocketアプリを組み込んで利用するということになると、まずWebSocketアプリのHTMLページのアクセスを
二段重ね

この構成の内、第一段階の
Websocket APIのオーソライザ
Websocket APIは通常のRest APIと違い、Lambdaオーソライザを設定する作法が少々異なります。
REST APIとは違う点として、
1. WebSocketにはHTTPメソッド(GET/POST/...)が無い
これは
ルート選択式
よって、Websocket APIのオーソライザーでは接続前処理である
$connect
例えば$connectルートで固定の際に使うARN値は
arn:aws:execute-api:<リージョン>:<アカウント名>:<APIのID>/<ステージ>/$connect
2. パス変数 (event.pathParameters) を使用できない
Rest APIではお馴染みのハンドラ関数からパス変数を読み取って、
api1/get
3. event.requestContextのコンテキスト構造が異なる
基本的にRest APIのrequestContextのオブジェクトの中身がWebSocket APIで違うということで、Rest API用に設定していたLambdaオーソライザをそのまま使うことができません。
では早速WebSocket用のオーソライザーの一例を与えます。
まずがオーソライザー用のLambdaハンドラ関数を先に準備しましょう。
お好みでいいのですが、関数名は
ws-myapp-auth
nodejs12.x
AWSLambdaBasicExecutionRole

以下のindex.jsのコードを書き換えます。
exports.handler = async (event, context, callback) => {
//👇イベントJSON全体を確認したい時に利用
//console.log('Received event:', JSON.stringify(event, null, 2));
const requestContext = event.requestContext;
//👇認証用のヘッダー情報を含む
const headers = event.headers;
//👇認証用のクエリ文字列を含む
const queryStringParameters = event.queryStringParameters;
const tmp = event.methodArn.split(':');
const accountId = tmp[4];
const apiGatewayArnTmp = tmp[5].split('/');
const stage = apiGatewayArnTmp[1];
const authResponse = {};
const condition = {
IpAddress: {}
};
if (headers.HeaderAuth1 === "headerValue1" &&
queryStringParameters.QueryString1 === "queryValue1" &&
stage === "[WebSocket APIのデプロイ先ステージ]" &&
accountId=== "[AWSアカウントID]"
) {
callback(null, generateAllow('me', event.methodArn));
} else {
callback("Unauthorized");
}
};
//👇認証した場合IAMポリシーを付与する
const generatePolicy = (principalId, effect, resource) => {
const authResponse = {
principalId,
//レスポンスに何かキーを付ける場合(オプション)
context: {
"stringKey": "stringval",
"numberKey": 123,
"booleanKey": true
}
};
if (effect && resource) {
const policyDocument = {
Version: '2012-10-17',
Statement: []
};
const statementOne = {
Action: 'execute-api:Invoke',
Effect: effect,
Resource: resource
};
policyDocument.Statement[0] = statementOne;
authResponse.policyDocument = policyDocument;
}
return authResponse;
};
//👇トークンを判定し特定のユーザーを承認する
const generateAllow = (principalId, resource) => {
return generatePolicy(principalId, 'Allow', resource);
};
これでひとまずlambdaを保存して、デプロイしておきます。
次にAPI Gatewayのダッシュボードで作業していきます。
前回作成した
まずwsMyAppを選択して
オーソライザー
[新しいオーソライザーの作成]
名前も適当に決めても良いのですが、ここでは
ws-app-authorizer
ws-myapp-auth

今回もっとも重要なポイントとして、オーソライザーの使う認証情報をIDソースであらかじめ設定することで、API Gateway側からカスタム定義されたトークンがリクエストのペイロードとしてオーソライザーに渡されます。
指定できるIDソースはいくつか種類がありますが、今回は
ヘッダー
クエリ文字列
HeaderAuth1
QueryString1
全ての項目を設定し、
保存
なお
Lambda呼び出しロール
自動で紐付けされる際に、LambdaのトリガーにAPIのパスが無いとの警告が出ていますが、WebSocket APIの
$connect

上手く設定がなされていれば以下のようにWebSocket APi用のオーソライザーが作成されていると思います。

次に
ルート
$connect
ルートリクエスト

ここでアクセス設定の
認可

するとコンボボックス内に、先程作成したリクエストオーソライザーが選択できるようになっていると思います。

もしも選択候補にリクエストオーソライザーが表示されていない場合には、リージョン内でのオーソライザーのインデックス登録が完了していないかも知れませんので少し待って時間を置いてみてください。
これで$connectルートにLambdaオーソライザーが紐付けすることができました。 このオーソライザの設定を有効にするために、再度デプロイさせることを忘れずにおこないます。
オーソライザを使った認証テスト
前回と同様に
通常どおりのアクセスをしてみましょう。
$ wscat -c wss://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/wsDev
error: Unexpected server response: 401

なんの認証情報も与えていないので、期待の通りにステータスコード401が跳ね返りアクセスが拒否されていることが確認できます。
ではお待ちかねのLambdaオーソライザー付きWebSocket APIにアクセスさせてみましょう。
#👇認証ヘッダ+認証クエリともに正しい
$ wscat \
-c 'wss://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/wsDev?QueryString1=queryValue1' \
-H 'HeaderAuth1:headerValue1'
Connected (press CTRL+C to quit)
#👇認証クエリが間違い
$ wscat \
-c 'wss://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/wsDev?QueryString1=hogehoge' \
-H 'HeaderAuth1:headerValue1'
error: Unexpected server response: 401
#👇認証ヘッダーが間違い
$ wscat \
-c 'wss://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/wsDev?QueryString1=queryValue1' \
-H 'HeaderAuth1:hogehoge'
error: Unexpected server response: 401

ということで期待通りにカスタマイズした認証パラメータを使ってWebSocket APIの接続を制御することに成功しています。
まとめ
今回はWebSocket APIへのLambdaオーソライザを手動で設定してみる方法を解説してみました。 これで不特定多数のユーザーから直接エンドポイントを踏まれた場合でも、WebSocketアプリへのアクセスを拒否できるようになります。
もっと安全面で堅牢なシステムにしたければ、HTMLページにAWS WAFとCookie認証を組み合わせたファイヤーウォールを設定するなどが検討できます。
今後もよりセキュアなバックエンドの開発にも触れる機会があればこのブログで解説していきたいと思いますが、今回のWebSocketアプリの認証系の話はこの辺で一旦止めておいて、次回はServerless Frameworkで、WebSocketアプリの自動構築を特集していく予定です。
参考サイト
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー