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


2021/07/25
蛸壺の技術ブログ|WebSocketAPIにオーソライザーを設定する

前回の内容では、手動でAWSダッシュボードからWebSocket APIを構築する方法を解説しました。

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

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

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


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オーソライザーが紐付けすることができました。このオーソライザの設定を有効にするために、再度デプロイさせることを忘れずにおこないます。


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

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


まとめ

今回はWebSocket APIへのLambdaオーソライザを手動で設定してみる方法を解説してみました。これで不特定多数のユーザーから直接エンドポイントを踏まれた場合でも、WebSocketアプリへのアクセスを拒否できるようになります。

もっと安全面で堅牢なシステムにしたければ、HTMLページにAWS WAFとCookie認証を組み合わせたファイヤーウォールを設定するなどが検討できます。

今後もよりセキュアなバックエンドの開発にも触れる機会があればこのブログで解説していきたいと思いますが、今回のWebSocketアプリの認証系の話はこの辺で一旦止めておいて、次回はServerless Frameworkで、WebSocketアプリの自動構築を特集していく予定です。


参考サイト

Lambda REQUEST オーソライザーの関数の作成

API Gateway WebSocket API mapping template reference

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

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