AWS Cognitoでプライベート S3 オブジェクトにアクセスする方法

Cognitoユーザープール、API Gateway、Lambda、S3統合によるプライベートファイルへの安全なアクセスガイド。

N
Necmettin Demir
2023年7月21日
読み込み中...

AWS Cognitoでプライベート S3 オブジェクトにアクセスする方法

AWS Cognito S3
AWS Cognito S3

シナリオ

顧客向けにいくつかのアプリケーションを開発していると仮定しましょう。ただし、アプリケーションのレコードに関連するPDF、Word、Excelなどのファイルがあります。シナリオを簡略化するため、これらのファイルがAWSの単一のプライベート(private)S3バケット(bucket)に保存されていると仮定します。
ユーザーは、アプリケーションのURLリンクを通じてプライベートS3バケットからこれらの関連ファイルにアクセスできる必要があります。私たちのソリューションは、社内のあらゆるソフトウェアに対してポータブル(portable)なソリューションとして機能する必要があります。

はじめに

この記事の目的は、Cognitoユーザープール(user pools)を使用してプライベートS3バケット内のファイルをダウンロードする方法を示すことです。Cognitoに加えて、CognitoからオーソライザーAPIGatewayへのフロー、およびAPI GatewayとLambdaの連携を示します。
AWSコンソールから各ステップについてできるだけ多くのスクリーンショットを共有しています。特に初心者向けに、ステップをより明確にするために多くのビジュアルを追加しています。

背景

この記事で開発される内容をより深く理解するために、いくつかの事前読書が役立つ場合があります。特にAWS初心者には、以下のリンクが役立ちます:

何をすべきか?

このようなタスクには多くのフローまたは方法をコーディングできます。ここでは、以下に示す方法を実装します。シナリオの実装方法についての簡単な説明を以下の図で示します。
以下の図は、Cognitoユーザープール、S3バケット、API Gatewayメソッド、Lambda関数などのいくつかの要素を作成する必要があることを示しています。AWS環境にすべてのエンティティを作成した後、それらが連携して動作できるように適切に構成する必要があります。
システムアーキテクチャ
システムアーキテクチャ
AWS環境内のすべての要素を逆順で作成する方がよいでしょう。たとえば、LambdaをAPIメソッドで使用するには、最初にLambda関数を開発すれば、API Gatewayメソッドの作成時にこの関数を簡単にリンクできます。同様に、ステップ5でS3 Webバケットを作成し、callback.htmlファイルを配置する必要があります。そうすれば、ステップ6でCognitoユーザープールを作成するときにこのファイルを使用できます。もちろんこれは必須ではありませんが、この順序で開発が容易になります。そのため、ここではこのアプローチが好まれています。

概要

以下の質問に対する回答を探します。この記事のすべてのステップを実装するには、AWSアカウントが必要であることを忘れないでください。
  1. プライベートS3バケットの作成方法
  2. プライベートS3バケット内のオブジェクトへのアクセス許可のためのカスタムポリシーの作成方法
  3. プライベートS3バケット内のオブジェクトにアクセスするためのLambda関数の作成方法
  4. Lambda関数を使用するためのGateway APIの作成方法
  5. Webフォルダとして使用するためのパブリックS3バケットの作成方法
  6. Cognitoユーザープールの作成と設定の構成方法
  7. シナリオのテスト方法

1. プライベートS3バケットの作成方法

S3は、AWSのリージョンベース(region-based)サービスの1つです。S3バケット内のアイテムはオブジェクトobject)と呼ばれます。したがって、AWS内のS3バケットではオブジェクトとファイルという用語は同じ意味で使用できます。
すべてのパブリックアクセスをブロック」(Block All Public Access)チェックボックスをオンのままにしてください。ここにプライベートS3バケットが作成されています。多くの追加構成オプションがありますが、ソリューションの簡略化のためにデフォルト値で作成しています。
S3 バケット作成
S3 バケット作成
S3バケットへのプライベートアクセスをテストするために、いくつかのオブジェクトをアップロードしてください。その後、許可されていないユーザーまたは可能性のあるアクセスリンクでこれらのオブジェクトにアクセスしてみてください。PDF、DOC、XLSなどのファイルを知っていますが、AWS S3用語ではこれらすべてがオブジェクトと呼ばれます。
ファイルアップロード
ファイルアップロード

2. プライベートS3バケット内のオブジェクトへのアクセス許可のためのポリシー作成

AWSでIAM(Identity and Access Management)はすべてのサービスの基盤です!ユーザー、グループ、ロール、ポリシーは私たちが知っておくべき基本概念です。
多くの組み込み(built-in)ロールがあり、各ロールには権限を意味する多くの組み込みポリシーがあります。これらは「AWS Managed」と呼ばれます。ただし、「Customer Managed」(カスタマーマネージド)ロールとポリシーを作成することも可能です。したがって、ここにカスタムポリシーが作成されています。
  • プライベートS3バケットからオブジェクトを取得するためのカスタムIAMポリシーを作成してください。
  • AWSの既存のポリシーリストを見つけ、以下に示すようにプライベートS3バケットに対してのみGetObject操作を実行する新しいポリシーを作成してください:
ポリシーリスト
ポリシーリスト
以下に示すようにカスタムポリシーを作成してください。サービスとしてS3を、アクション(action)としてGetObjectのみを選択してください:
ポリシー設定1
ポリシー設定1
リソース(resource)として「specific」を選択し、ポリシーが必要な機能を持つようにプライベートS3バケットを指定してください:
ポリシー設定2
ポリシー設定2
ポリシーに名前を付けて作成してください。任意の名前を付けることができますが、覚えておく必要があります。
ポリシー設定3
ポリシー設定3
カスタムポリシーのサマリーは以下のようになります。このJSONコンテンツを直接使用してポリシーを作成することも可能です:
ポリシー JSON
ポリシー JSON
ポリシーJSON定義:
JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::private-s3-for-interfacing/*"
        }
    ]
}

3. プライベートS3バケット内のオブジェクトにアクセスするためのLambda関数の作成

ここではLambda関数にNodeJSの最新バージョンが使用されています。Lambda関数を作成してNodeJSを選択してください。Lambda関数にはPython、Go、Java、.NET Coreなどサポートされている任意の言語を選択できます。
Lambda作成
Lambda作成
Lambda関数を作成すると、サンプルの「hello」コードが表示されます。代わりに、独自のコードを開発する必要があります。
ご覧のとおり、Lambda開発環境はWebベースの軽量IDEに似ています。
Lambdaコード変更
Lambdaコード変更
既存のコードを下記の短いサンプルコードに置き換えてください。コードの新しい状態は以下のようになります。コードを変更した後、Lambda関数を使用するために「Deploy」ボタンを押してください。
シナリオを簡略化するため、バケット名は静的に使用されています。ファイル名はfnという名前でパラメータとして送信されます。デフォルトのコンテンツタイプ(content type)はpdfとして想定されていますが、Lambda関数コードに実装されている任意のファイルタイプにすることができます。API Gateway接続でLambda関数のプロキシ機能を使用することを選択するため、レスポンスヘッダー(response header)には必要な追加データが含まれています。
NodeJS Lambdaコード(Blobとして返す):
JavaScript
// Lambda関数コードはこのように見えます
// このコードはレスポンスをblobコンテンツとして返します
// ファイルをダウンロードするには、添付ファイルのCallback-to-Download-Blob.htmlを使用できます

const AWS = require('aws-sdk');
const S3= new AWS.S3();
exports.handler = async (event, context) => {
    
  let fileName;
  let bucketName;
  let contentType;
  let fileExt;
    
  try {
    bucketName = 'private-s3-for-interfacing';
    fileName = event["queryStringParameters"]['fn']
    contentType = 'application/pdf';
    fileExt = 'pdf';
    
    //------------
    fileExt = fileName.split('.').pop();
    
    switch (fileExt) {
        case 'pdf': contentType = 'application/pdf'; break;        
        case 'png': contentType = 'image/png'; break;
        case 'gif': contentType = 'image/gif'; break;
        case 'jpeg': case 'jpg': contentType = 'image/jpeg'; break;
        case 'svg': contentType = 'image/svg+xml'; break;
        case 'docx': contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; break;
        case 'xlsx': contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; break;
        case 'pptx': contentType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; break;
        case 'doc': contentType = 'application/msword'; break;
        case 'xls': contentType = 'application/vnd.ms-excel'; break;
        case 'csv': contentType = 'text/csv'; break;
        case 'ppt': contentType = 'application/vnd.ms-powerpoint'; break;
        case 'rtf': contentType = 'application/rtf'; break;
        case 'zip': contentType = 'application/zip'; break;
        case 'rar': contentType = 'application/vnd.rar'; break;
        case '7z': contentType = 'application/x-7z-compressed'; break;
        default: ;
    }
    
    //------------
    const data = await S3.getObject({Bucket: bucketName, Key: fileName}).promise();
    
    return {
       headers: {
          'Content-Type': contentType,
          'Content-Disposition': 'attachment; filename=' + fileName, // 成功の鍵
          'Content-Encoding': 'base64',
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 
          'Access-Control-Allow-Methods': 'GET,OPTIONS'
      },
      body: data.Body.toString('base64'),
      isBase64Encoded: true,
      statusCode: 200
    }
  } catch (err) {
    return {
      statusCode: err.statusCode || 400,
      body: err.message || JSON.stringify(err.message) + ' - fileName: '+ fileName + ' - bucketName: ' + bucketName
    }
  }
}
Lambda関数で以下に示すようにPythonコードを使用することも可能です:
Python
# 以下のコードは上記のNodeJSの例のように開発できます
    
import base64
import boto3
import json
import random

s3 = boto3.client('s3')

def lambda_handler(event, context):
    try:
        fileName = event['queryStringParameters']['fn']
        bucketName = 'private-s3-for-interfacing'        
        contentType = 'application/pdf'
        
        response = s3.get_object(
            Bucket=bucketName,
            Key=fileName,
        )
        
        file = response['Body'].read()
        
        return {
            'statusCode': 200,
            'headers': {  
                         'Content-Type': contentType,                            
                         'Content-Disposition': 'attachment; filename='+ fileName,
                         'Content-Encoding': 'base64'
                         # 必要に応じてCORS関連のコードをここに追加できます
                        },
            'body': base64.b64encode(file).decode('utf-8'),           
            'isBase64Encoded': True
        }
    except:
        return {
            'headers': { 'Content-type': 'text/html' },
            'statusCode': 200,
            'body': 'Lambdaでエラーが発生しました!' 
        }
別の方法として、Lambdaでpresigned URLを作成することもできます:
JavaScript
// このメソッドはpresigned urlを提供します
// presigned URLリンクを使用するには、Callback-for-preSignedUrl.htmlファイルを使用できます

var AWS = require('aws-sdk');
var S3 = new AWS.S3({
  signatureVersion: 'v4',
});

exports.handler = async (event, context) => {
    
  let fileName;
  let bucketName;
  let contentType;
    
  bucketName = 'private-s3-for-interfacing';
  fileName = event["queryStringParameters"]['fn'];
  contentType = 'application/json';
    
  const presignedUrl = S3.getSignedUrl('getObject', {
    Bucket: bucketName,
    Key: fileName,
    Expires: 300 // 秒
  });

  let responseBody = {'presignedUrl': presignedUrl};
  
  return {
       headers: {
          'Content-Type': contentType,
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 
          'Access-Control-Allow-Methods': 'GET,OPTIONS'
      },
      body: JSON.stringify(responseBody), 
      statusCode: 200
    }    
};
Lambda関数が作成されると、それと一緒にロールが作成されます。ただし、このロールにはプライベートS3バケット内のオブジェクトへのアクセス許可がありません。次に、前のステップで作成した「Customer Managed」ポリシーをLambda関数で作成されたこのロールにアタッチする必要があります。
Lambda関数を作成した後、以下に示すように自動的に作成されたロールを見つけることができます:
Lambdaロールの検索
Lambdaロールの検索
前のステップで作成したカスタムポリシーをこのロールにアタッチしてください。これにより、Lambda関数がS3バケットに対して制限されたGetObjectアクセス権を持つようになります。
ポリシーのアタッチ
ポリシーのアタッチ
LambdaがS3バケットにアクセスするために必要なことはこれだけです。次に、Lambda関数を使用するためのAWS Gatewayメソッドを作成する時間です。

4. Lambda関数を使用するためのGateway API作成

以下に示すようにAWS Gateway REST APIを作成してください。多くのオプションがありますが、「New API」として「REST」APIを作成しています。API Gatewayに名前を付けてください。
REST API作成
REST API作成
AWS GW APIを作成して実行するためのいくつかのステップがあります:
  • API作成
  • Resource作成
  • Method作成
  • APIのデプロイ(Deploy)
REST APIに対して以下に示すようにResourceを作成してください:
Resource作成ステップ1
Resource作成ステップ1
ここで作成されたリソース(resource)は、後でAPIのURLで使用されます。
Resource作成ステップ2
Resource作成ステップ2
作成したリソースに対してGETメソッドを作成してください:
GETメソッド作成
GETメソッド作成
ここではGETPOSTPUTDELETEなどの任意のHTTPメソッドを作成できます。必要なのはGETのみなので、それだけを作成しています。前のステップで作成したLambda関数をこのメソッドにリンクすることを忘れないでください。
ここではLambda Proxy Integrationがチェックされています。このアプローチにより、すべてのレスポンス関連のコンテンツをLambda関数内で処理できます。
Lambda Proxy統合
Lambda Proxy統合
GETメソッドが作成された後、API GatewayメソッドとLambda関数間のフローは以下のようになります:
フロービュー
フロービュー
以下に示すようにGateway APIのCORSを有効にしてください。Default 4xxDefault 5xxオプションをチェックできます。これにより、エラーでもスムーズに返されます。
CORS有効化
CORS有効化
AWS Gatewayメソッドに関するすべてを作成および構成した後、APIをデプロイ(deploy)する時間です。APIは図に示すようにステージ(stage)にデプロイされます。また、このステージ名はパブリックAPI URLで使用されます。
APIデプロイ
APIデプロイ
デプロイ後、URLは以下のようになります。これで、このリンクを任意のアプリケーションから使用できます。
デプロイURL
デプロイURL
APIゲートウェイへのアクセスを制限するには、Authorizer(オーソライザー)を定義する必要があります。以下に示すようにCognito Authorizerを定義できます。
以下の図に示すように、Authorizationは、認可されたAPIメソッドを使用するためにリクエストのheader部分に追加する必要があるJWTトークン(token)です。
Cognito Hosted UIがCognitoユーザー/パスワードで送信されると、Cognitoはユーザーをid_tokenと追加のstateデータを渡してコールバックURLにリダイレクトします。
header部分に追加する必要があるトークンがToken Sourceの下で「Authorization」という名前であることを確認してください。
Cognito Authorizer定義
Cognito Authorizer定義
CognitoベースのAuthorizerが定義された後、以下のように使用できます:
Authorizer使用
Authorizer使用
一方、API GatewayにAuthorizerを定義したくない場合は、以下に示すように「Resource Policy」(リソースポリシー)でAPI URLへのアクセスを制限できます。
Resource Policyが変更/追加された場合、APIを再度デプロイする必要があります。xxx.xxx.xxx.xxxとして表示されるIPは、サーバーのIPにすることができます。誰かが別のIPからURLにアクセスしようとすると、次のメッセージが表示されます:
{"Message":"User: anonymous is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:eu-west-2:********8165:... with an explicit deny"}
Resource Policy設定
Resource Policy設定
Resource Policy JSONコードは以下のようになります:
JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "*"
        },
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "*",
            "Condition": {
                "NotIpAddress": {
                    "aws:SourceIp": "xxx.xxx.xxx.xxx"
                }
            }
        }
    ]
}

5. Webフォルダとして使用するパブリックS3バケット

ソリューションには2つのS3バケット(bucket)が必要です。1つ目は前のセクションで作成されました。2つ目は現在作成されており、Webフォルダとして使用されます。1つ目は、すべてのファイルを保存するためのプライベートバケットとして使用されました。
2つのS3バケット構造
2つのS3バケット構造
Webフォルダとしてパブリックなパブリックバケットを作成してください。このバケットにはcallback.htmlファイルが含まれるため、Cognitoコールバック(callback)アドレスとして使用できます。
Web Bucket作成
Web Bucket作成
Web用のS3バケットはパブリック(public)である必要があります。そのため、以下のポリシーを適用できます:
JSON
// ポリシーJSONはこのように見えます

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

ソースファイルのダウンロード

Callback.htmlおよびその他のソースファイルは、以下のリンクからダウンロードできます:

6. Cognitoユーザープールの作成と構成

  • Callbackアドレス: https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.html
  • OAuth 2.0 Flows: 「implicit grant」オプションをチェックしてください。
  • OAuth 2.0 Scopes: email、openid、profile。
以下のhosted UIリンクを確認してください。 Hosted Cognitoログインページにパラメータを送信するために追加の「state」URLパラメータを追加してください。「state」パラメータはCallback.htmlファイルに渡されます。
Cognito Hosted UIリンクには、以下に示すように多くのURLパラメータが含まれています:
https://test-for-user-pool-for-s3.auth.eu-west-2.amazoncognito.com/login?client_id=7uuggclp7269oguth08mi2ee04&response_type=token&scope=openid+profile+email&redirect_uri=https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.html&state=fn=testFile.pdf
フィールド:
  • client_id=7uuggclp7269oguth08mi2ee04
  • response_type=token
  • scope=openid+profile+email
  • redirect_uri=https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.html
  • state=fn=testFile.pdf
stateはカスタムURLパラメータです。Hosted UIページに送信でき、Callback.htmlページに返されます。
以下に示すようにclient appを作成する必要があります:
Client App作成
Client App作成
App client設定は以下のように確認できます:
App Client設定
App Client設定
Hosted UIのURLとして使用するためにドメイン名(domain name)を設定する必要があります。
ドメイン設定
ドメイン設定

7. シナリオのテスト方法

Cognitoユーザープールを使用して制限されたアクセスを許可するAPIのテスト方法を見てみましょう。
任意のエンドユーザーがこのプロセスを開始するためにリンクをクリックできます。以下のHTMLコンテンツをホストするWebページがあると仮定しましょう。ご覧のとおり、各ファイルへのリンクはCognito hosted UIのURLです。
LinkToS3Files.htmlファイルを使用してシナリオをテストできます。

テストファイルのダウンロード


結論

この記事がAWSクラウド環境の初心者にとって役立つことを願っています。

クラウドコンピューティングサービス

AWS、Azure、Google Cloudプラットフォームでのインフラストラクチャ設計、移行、管理、最適化サービスを提供しています。

サービスを見る

お問い合わせ

AWSとクラウドコンピューティングソリューションの詳細については、チームにお問い合わせください。

お問い合わせ