【AWSで構築するサーバレスWebSocket②】 WebSocketAPIにオーソライザーを設定する


※ 当ページには【広告/PR】を含む場合があります。
2021/07/25
【AWSで構築するサーバレスWebSocket①】 AWS APIGatewayからWebSocket APIを試してみる
蛸壺の技術ブログ|WebSocketAPIにオーソライザーを設定する
前回 の内容では、手動でAWSダッシュボードからWebSocket APIを構築する方法を解説しました。
今回は次なる段階として、作成したWebSocket APIにLambdaオーソライザーを設定して、アクセス制限をする方法を検討してみたいと思います。

合同会社タコスキングダム|蛸壺の技術ブログ
【AWSで構築するサーバレスWebSocket①】 AWS APIGatewayからWebSocket APIを試してみる

もっとも簡単なWebSocket APIの作成にチャレンジして、その作成過程を詳しく解説します。


合同会社タコスキングダム|蛸壺の技術ブログ【AWS独習術】AWSをじっくり独学したい人のためのオススメ書籍&教材特集
図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書

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ページのアクセスを
Cognitoオーソライザ付きのRest API(かHttp API) で制限し、そのページ内でしか提供していないWebSocketアプリ用のアクセストークンやCookie情報などを利用して、WebSocket APIのアクセスを許可するような 二段重ね のセキュリティの仕組みを採用してみましょう。

合同会社タコスキングダム|蛸壺の技術ブログ


この構成の内、第一段階の
Cognitoオーソライザの付け方 は既に説明していましたので、今回は第二段目のLambdaオーソライザの話をしていきます。


合同会社タコスキングダム|蛸壺の技術ブログ【AWS独習術】AWSをじっくり独学したい人のためのオススメ書籍&教材特集
図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書

Websocket APIのオーソライザ



Websocket APIは通常のRest APIと違い、Lambdaオーソライザを設定する作法が少々異なります。
REST APIとは違う点として、

            1. WebSocketにはHTTPメソッド(GET/POST/...)が無い

        

これは
前回の冒頭 でも説明したとおり、WebSocket APIのハンドラ関数は全て ルート選択式 と呼ばれるrouteKeyの値を判定してそれに応じた処理が実行される仕組みです。
よって、Websocket APIのオーソライザーでは接続前処理である
$connect ルートをターゲットに設定する必要があります。
例えば$connectルートで固定の際に使うARN値は
arn:aws:execute-api:<リージョン>:<アカウント名>:<APIのID>/<ステージ>/$connect となります。

            2. パス変数 (event.pathParameters) を使用できない

        

Rest APIではお馴染みのハンドラ関数からパス変数を読み取って、
api1/get などのようにパスごとの処理を行わせることはできません。 原則としてWebSocket APIのルート名は固定です。

            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 をそのまま利用します。
まずwsMyAppを選択して
オーソライザー の設定ページを表示します。

[新しいオーソライザーの作成] ボタンを押し、オーソライザーを作成します。
名前も適当に決めても良いのですが、ここでは
ws-app-authorizer で、Lambda関数には先程作成していた ws-myapp-auth を指定しておきます。

合同会社タコスキングダム|蛸壺の技術ブログ


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

合同会社タコスキングダム|蛸壺の技術ブログ


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

合同会社タコスキングダム|蛸壺の技術ブログ


次に
ルート のページに戻り、 $connect ルートを選択し、 ルートリクエスト をクリックします。

合同会社タコスキングダム|蛸壺の技術ブログ


ここでアクセス設定の
認可 の鉛筆のアイコンをクリックします。

合同会社タコスキングダム|蛸壺の技術ブログ


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

合同会社タコスキングダム|蛸壺の技術ブログ


もしも選択候補にリクエストオーソライザーが表示されていない場合には、リージョン内でのオーソライザーのインデックス登録が完了していないかも知れませんので少し待って時間を置いてみてください。
これで$connectルートにLambdaオーソライザーが紐付けすることができました。 このオーソライザの設定を有効にするために、再度デプロイさせることを忘れずにおこないます。


合同会社タコスキングダム|蛸壺の技術ブログ【AWS独習術】AWSをじっくり独学したい人のためのオススメ書籍&教材特集
図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書

オーソライザを使った認証テスト



前回と同様に
wscatコマンド で動作確認を行います。
通常どおりのアクセスをしてみましょう。

            $ 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の接続を制御することに成功しています。


合同会社タコスキングダム|蛸壺の技術ブログ【AWS独習術】AWSをじっくり独学したい人のためのオススメ書籍&教材特集
図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書

まとめ



今回はWebSocket APIへのLambdaオーソライザを手動で設定してみる方法を解説してみました。 これで不特定多数のユーザーから直接エンドポイントを踏まれた場合でも、WebSocketアプリへのアクセスを拒否できるようになります。
もっと安全面で堅牢なシステムにしたければ、HTMLページにAWS WAFとCookie認証を組み合わせたファイヤーウォールを設定するなどが検討できます。
今後もよりセキュアなバックエンドの開発にも触れる機会があればこのブログで解説していきたいと思いますが、今回のWebSocketアプリの認証系の話はこの辺で一旦止めておいて、次回はServerless Frameworkで、WebSocketアプリの自動構築を特集していく予定です。

参考サイト

Lambda REQUEST オーソライザーの関数の作成API Gateway WebSocket API mapping template reference
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。

合同会社タコスキングダム|蛸壺の技術ブログ【AWS独習術】AWSをじっくり独学したい人のためのオススメ書籍&教材特集