Amazon AppStream 2.0のユーザー認証をAmazon Cognitoで実装し、WAFでIP制限も追加してみた

2024.05.28

はじめに

Amazon AppStream 2.0でのユーザー認証は、いくつか選択肢があります。

  • Amazon AppStream 2.0のユーザープールを利用
  • SAML 2.0 によるサードパーティーの ID プロバイダーと連携
  • SAML 2.0 + Active Directory 連携
  • 独自 ID サービスを構築することによるカスタム ID 認証

今回は、AWSが提供するワークショップを参考に、Amazon Cognito のユーザープールを利用した Amazon AppStream 2.0 の認証構成を構築する方法を紹介します。

今回構築する構成は、以下の図に示す通りです。(ユーザー登録時のメール送信のためAmazon SESも利用されます)

ワークショップの内容に加えて、ログイン時のセキュリティ強化のため、API GatewayにAWS WAFを適用してIP制限を行います。

Cognitoを利用するケースとしては、SAML 2.0が利用できないかつ、要件としてIP制限が必要な場合が考えられます。

API GatewayにはWAFが適用できますので、本構成ではAppStream 2.0を利用するユーザーに対してIP制限が可能です。

前提条件

S3で静的ウェブサイトをホスティング

CloudFront と S3 を組み合わせて静的ウェブサイトをホスティングするのが一般的ですが、今回は検証目的のため、ワークショップの手順通りS3のみを使用します。

S3バケットを作成します。appstream-cognito-workshopsという名前で作成しました。

以下のAWS側で用意されている架空のサイト(Example Corp.)のリポジトリをダウンロードします。

解凍後、S3にアップロードします。

静的ウェブサイトホスティングを有効にし、インデックスドキュメントはindex.htmlを指定します。

ブロックパブリックアクセスは、すべてオフ、バケットポリシーは以下を設定します。

YOUR_BUCKET_NAMEは、作成したバケット名を記載します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
    }
  ]
}

静的ウェブサイトホスティングを有効化すると、以下のURLからアクセスします

  • http://S3バケット名.s3-website-ap-northeast-1.amazonaws.com

架空のサイト(Example Corp.)が確認できました。

Cognito ユーザープール作成

ユーザープールを作成します。ワークショップは、CloudFormationテンプレートが用意されており、ユーザープールのみを抜粋したテンプレートが以下です。

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation template for the Amazon AppStream 2.0 SaaS project
Resources:
  CognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: appstream-cognito-pool
      Schema:
        - Mutable: true
          Name: email
          Required: true
      AutoVerifiedAttributes:
        - email
  CognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: appstream-cognito-app
      UserPoolId: !Ref CognitoUserPool
      GenerateSecret: false

テンプレートの内容をもとに、マネジメントコンソールで作成します。

プロバイダーのタイプは、Cognito ユーザープールのみです。

MFAはなしです。

必須の属性は、emailにします。

送信元Eメールアドレスは、設定済みのSESです。

ユーザープール名とアプリケーションクライアント名を入力します。

高度なアプリケーションクライアントは、デフォルトのままです。これで作成します。

作成後、2点をメモしておきます。

  • ユーザープール ID
    • ユーザープールの概要に記載
  • アプリクライアント ID
    • [アプリケーションの統合]のアプリクライアントと分析]に記載

Lambda作成

ワークショップではNode.js 8.10がランタイムとして指定されていますが、Node.js 8.10は選択できないため、本記事ではNode.js 16.xを使用します。

Node.js 16.xまでは、AWS SDK for JavaScript v2ですが、最新のNode.js 18.xからはAWS SDK for JavaScript v3になりますので、Node.js 18.x以降に指定する場合、コード修正が必要になります。

Lambdaには以下のIAMポリシーを適用します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "appstream:CreateStreamingURL",
            "Resource": [
                "arn:aws:appstream:ap-northeast-1:アカウントID:fleet/*",
                "arn:aws:appstream:ap-northeast-1:アカウントID:stack/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

コードはワークショップで紹介されている通りのコードです。

// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

const AWS = require('aws-sdk');
const appstream = new AWS.AppStream;

exports.handler = (event, context, callback) => {
    if (!event.requestContext.authorizer) { //checks to see if Cognito Authorization has been configured 
        errorResponse('Authorization has not been configured, please configure an authorizer in API Gateway', context.awsRequestId, callback);
        return;
    }
    const username = event.requestContext.authorizer.claims['cognito:username'];
    
    var params = {
        FleetName: '<Fleet-Name>', /* required */
        StackName: '<Stack-Name>', /* required */
        UserId: username,
        Validity: 5

    };

    createas2streamingurl(params, context.awsRequestId, callback);

};

function errorResponse(errorMessage, awsRequestId, callback) { //Function for handling error messaging back to client
    callback(null, {
        statusCode: 500,
        body: JSON.stringify({
            Error: errorMessage,
            Reference: awsRequestId,
        }),
        headers: {
            'Access-Control-Allow-Origin': '<origin-domain>', //This should be the domain of the website that originated the request, example: amazonaws.com 
        },
    });
}

function createas2streamingurl(params, awsRequestId, callback) {
    var request = appstream.createStreamingURL(params);
    request.
        on('success', function (response) {
            console.log("Success. AS2 Streaming URL created.");
            var url = response.data.StreamingURL;
            callback(null, {
                statusCode: 201,
                body: JSON.stringify({
                    Message: url,
                    Reference: awsRequestId,
                }),
                headers: {
                    'Access-Control-Allow-Origin': '<origin-domain>', //This should be the domain of the website that originated the request, example: amazonaws.com
                },
            });
        }).
        on('error', function (response) {
            console.log("Error: " + JSON.stringify(response.message));
            errorResponse('Error creating AS2 streaming URL.', awsRequestId, callback);

        }).
        send();
}

コード内の3つは、値を置き換えます。

  • Stack-Name
    • AppStream 2.0のスタック名
  • Fleet-Name
    • AppStream 2.0のフリート名
  • origin-domain(2箇所あります)
    • S3のファイルを書き換えるURL
      • http://バケット名.s3-website-ap-northeast-1.amazonaws.com

API Gateway作成

REST API を作成します。

オーソライザーを作成します。先程作成したCognitoユーザープールを選択し、トークンのソースは、Authorizationを指定します。

以下の設定でリソースを作成します

  • リソースパス:/
  • リソース名:auth
  • CORS:有効化

/authを選択し、以下の設定でメソッドを作成します。

  • メソッドタイプ:POST
  • Lambdaプロキシ統合:有効化
  • Lambda関数:先ほど作成したLambda
  • メソッドリクエストの設定:認可(先程のオーソライザーを選択)

ステージ名authで、APIをデプロイします。

[URLを呼び出す]は、メモしておきます。

S3のファイルを書き換える

S3にアップロードしたファイルのうち、assets/js/config.jsを書き換えます。

修正前は以下の通りです。

window._config = {
    cognito: {
        userPoolId: '', 
        userPoolClientId: '', 
        region: '' 
    },
    api: {
        invokeUrl: '' 
    }
};

以下の4つの値を入れて、ファイルをアップロードします。

  • userPoolId:ユーザープール ID
  • userPoolClientId:アプリクライアント ID
  • region:リージョン名
  • invokeUrl:API Gatewayの[URLを呼び出す]のURL
window._config = {
    cognito: {
        userPoolId: 'ap-northeast-1_xxxxxxxx', 
        userPoolClientId: 'xxxxxxxxxxxx', 
        region: 'ap-northeast-1' 
    },
    api: {
        invokeUrl: 'https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/auth' 
    }
};

テスト

ブラウザで以下のURLにアクセスします。

  • http://S3バケット名.s3-website-ap-northeast-1.amazonaws.com/signin.html

[New User]をクリックします。

メールアドレスとパスワードを入力し、登録します。

登録したメールアドレスに、6桁の数字が送られますので、入力します。これでユーザー登録が完了です。

ログインします。

AppStream2.0のアプリケーションの選択画面からFirefoxを選択してみます。

Firefoxを選択すると、以下の画面の通り、利用できることが確認できました

WAFを適用

IP制限を設定したWAFを作成し、API Gatewayに適用します。WAFの作成方法は以下をご参照ください。

ログイン画面から、特定のIPからのみログインができることが確認できます。

構築とテストは以上となります。検証後は、不要なリソースによるコスト発生やセキュリティリスクを避けるため、最低でも以下の対応を行うことをお勧めします。

  • AppStream 2.0はフリートを停止
  • WAFは削除
  • URLを呼び出されないよう、API Gatewayのステージは削除
  • S3の静的ウェブサイトホスティングを無効。ブロックパブリックアクセスを有効化

最後に

本記事では、AWS が提供するワークショップを参考に、Cognito のユーザープールを使用してAppStream 2.0のユーザー認証を行う方法について説明しました。

さらに、AWS WAFを使用してIP制限を追加することで、セキュリティを強化する方法も紹介しました。

参考になれば、幸いです。

参考