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アカウントが必要であることを忘れないでください。
- プライベートS3バケットの作成方法
- プライベートS3バケット内のオブジェクトへのアクセス許可のためのカスタムポリシーの作成方法
- プライベートS3バケット内のオブジェクトにアクセスするためのLambda関数の作成方法
- Lambda関数を使用するためのGateway APIの作成方法
- Webフォルダとして使用するためのパブリックS3バケットの作成方法
- Cognitoユーザープールの作成と設定の構成方法
- シナリオのテスト方法
1. プライベートS3バケットの作成方法
S3は、AWSのリージョンベース(region-based)サービスの1つです。S3バケット内のアイテムはオブジェクト(object)と呼ばれます。したがって、AWS内のS3バケットではオブジェクトとファイルという用語は同じ意味で使用できます。
「すべてのパブリックアクセスをブロック」(Block All Public Access)チェックボックスをオンのままにしてください。ここにプライベート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のみを選択してください:
リソース(resource)として「specific」を選択し、ポリシーが必要な機能を持つようにプライベートS3バケットを指定してください:

ポリシーに名前を付けて作成してください。任意の名前を付けることができますが、覚えておく必要があります。

カスタムポリシーのサマリーは以下のようになります。この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関数を作成すると、サンプルの「hello」コードが表示されます。代わりに、独自のコードを開発する必要があります。
ご覧のとおり、Lambda開発環境はWebベースの軽量IDEに似ています。

既存のコードを下記の短いサンプルコードに置き換えてください。コードの新しい状態は以下のようになります。コードを変更した後、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関数がS3バケットに対して制限された
GetObjectアクセス権を持つようになります。
LambdaがS3バケットにアクセスするために必要なことはこれだけです。次に、Lambda関数を使用するためのAWS Gatewayメソッドを作成する時間です。
4. Lambda関数を使用するためのGateway API作成
以下に示すようにAWS Gateway REST APIを作成してください。多くのオプションがありますが、「New API」として「REST」APIを作成しています。API Gatewayに名前を付けてください。

AWS GW APIを作成して実行するためのいくつかのステップがあります:
- API作成
- Resource作成
- Method作成
- APIのデプロイ(Deploy)
REST APIに対して以下に示すように
Resourceを作成してください:
ここで作成されたリソース(resource)は、後でAPIのURLで使用されます。

作成したリソースに対して
GETメソッドを作成してください:
ここでは
GET、POST、PUT、DELETEなどの任意のHTTPメソッドを作成できます。必要なのはGETのみなので、それだけを作成しています。前のステップで作成したLambda関数をこのメソッドにリンクすることを忘れないでください。ここではLambda Proxy Integrationがチェックされています。このアプローチにより、すべてのレスポンス関連のコンテンツをLambda関数内で処理できます。

GETメソッドが作成された後、API GatewayメソッドとLambda関数間のフローは以下のようになります:
以下に示すようにGateway APIのCORSを有効にしてください。Default 4xxとDefault 5xxオプションをチェックできます。これにより、エラーでもスムーズに返されます。

AWS Gatewayメソッドに関するすべてを作成および構成した後、APIをデプロイ(deploy)する時間です。APIは図に示すようにステージ(stage)にデプロイされます。また、このステージ名はパブリックAPI 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が定義された後、以下のように使用できます:

一方、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 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つ目は、すべてのファイルを保存するためのプライベートバケットとして使用されました。

Webフォルダとしてパブリックなパブリックバケットを作成してください。このバケットには
callback.htmlファイルが含まれるため、Cognitoコールバック(callback)アドレスとして使用できます。
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=7uuggclp7269oguth08mi2ee04response_type=tokenscope=openid+profile+emailredirect_uri=https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.htmlstate=fn=testFile.pdf
stateはカスタムURLパラメータです。Hosted UIページに送信でき、Callback.htmlページに返されます。以下に示すようにclient appを作成する必要があります:

App client設定は以下のように確認できます:

Hosted UIのURLとして使用するためにドメイン名(domain name)を設定する必要があります。

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