【AWS使い方ガイド】CloudFront FunctionsでS3バケット内のリソースのアクセス認証を掛ける方法


※ 当ページには【広告/PR】を含む場合があります。
2024/10/07
[AWS] 徹底図解!お名前.comで取得したDNSをAWS Route53/Cloudfrontで管理するまでの手順
蛸壺の技術ブログ|CloudFront FunctionsでS3バケット内のリソースのアクセス認証を掛ける方法

以前の記事で、
「CouldFrontのOAC(Origin Access Control)」を使ったS3バケット内リソースのアクセス制限の方法を解説していました。

合同会社タコスキングダム|蛸壺の技術ブログ
【AWS使い方ガイド】CloudFrontからOAI/OACを利用してアクセス制限付きのS3からリソースを取得する方法

CloudFlontのOAI・OACを使ってS3リソースにアクセス制限を行う

おさらいになりますが、OACによるS3のアクセス制限はエンドユーザーからのS3の直接のアクセスからリソースを保護するための施策です。

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

つまるところ、S3バケットのルートに以下のようなテキストファイルがあったとして、

            
            こんにちは!
        
このファイルへは、CloudFrontからは自由にアクセスできます。

            
            $ curl -XGET https://xxxxxxxxxxxxx.cloudfront.net/greeting.txt
こんにちは!
        
しかし、S3バケットのリソースURLへ直接アクセスは制限されます。

            
            $ curl -XGET https://xxxxxxxxxxxxxxxxx.s3.ap-northeast-1.amazonaws.com/greeting.txt
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>*********</RequestId><HostId>************</HostId></Error>
        
こうすることでユーザーへのファイル供給をCloudFrontに一本化することはできますが、このままだと不特定多数のユーザーがアクセスすることになります。

今回はより進んだS3ファイルのアクセス制限をかけるための初歩として、
「CloudFront Functions (Javascript Runtime 2.0)」(以降CF2)によるアクセス認証の基本を説明していきます。


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

図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書

CloudFront Functions (Javascript Runtime 2.0)の話

まずは簡単にCF2の使い方に触れてみます。

「CloudFront Functions(CF2)」は、よくLambda@Edge(L@E)と比較されますが、基本的にはL@Eから様々なライブラリを削って、シンプルな機能だけを持たせた"超軽量版ローカルLambda"と考えられます。

中身はLambdaのー種類とはいえ、実際はCloudFrontの機能の一部で、利用するにもCloudFront側で設定する必要があります。

細かく見ていくと色々とありますが、CF2の開発者が特に考慮したいことは以下の3点です。

            
            + 実装には独自の「Javascript Runtime」を使う
+ 作成されるCF2インスタンスのロケーションはCFディストリビューションの場所と同じ
+ ビューアーリクエストとビューアーレスポンスの2つの関数タイプしかない
        
まず、Javascript Runtime(JR)については、v2.0になったおかげで、文法面で制約のあったv1.0からとても記述しやすくなりました。

参考|CloudFront Functions の JavaScript ランタイム 2.0 の機能

簡単な例を示すと、ビューアーリクエストでヘッダを書き込みしたい場合、

            
            async function handler(event) {
    const request = event.request;
    const clientIP = event.viewer.ip;

    //クライアントのIP値をtrue-client-ipヘッダーとしてリクエストに追加する
    request.headers['true-client-ip'] = {value: clientIP};

    return request;
}
        
と、ほぼモダンなJavascriptコードとして実装できます。

また、CF2を使う大きなメリットとして、関数アタッチ先のCFディストリビューションと同じリージョンにすることができます。

L@Eだと"us-east-1"に作成する必要があり、他のリージョンに構築したサービスと機能連帯させるにはどうしても
「リージョンまたぎ」になります。

リージョンまたぎ問題は、不要になったときの消し忘れだったり、デプロイバージョンに変わるARN値の指定変更作業だったり、何かとうっかりミスの原因にもなります。

CF2を使うことで、こういった細かな関数の管理も気にせずに利用できます。

ただし、依然としてオリジンリクエストとオリジンレスポンスの関数タイプはL@Eしか利用できないため、今後のCF2の機能アップデートに期待です。

リクエストイベント構造 〜 CF2のリクエストにはBody(ペイロード)が存在しない

個人的にはビューアーリクエスト型でCF2を使うことがほとんどですが、L@Eのイベントリクエスト型とは異なるため、CF2を採用するにはいくつかの注意点があります。

その最たるものが、CF2のイベントリクエスト型では、
「Body(ペイロード部)」が存在しない点です。

参考|CloudFront Functions のイベント構造

CF2のリクエストイベントには、以下のプロパティフィールドが含まれています。

            
            method:
    リクエストされたHTTPメソッド(読み取り専用:値変更不可)
uri:
    リクエストされたURL(の相対パス)
querystring:
    リクエストされたクエリ文字列(のオブジェクト)
headers:
    リクエストのHTTPヘッダー(を表すオブジェクト)
    ただし、Cookieヘッダーは除外
cookies:
    リクエストのCookie(を表すオブジェクト)
    ヘッダーの内、Cookieヘッダーから取得されたもの
        
CF2でBodyが制限されているのは、内部メモリが2MBしかないため、それなりの大きさのデータが送り出されるとそもそも処理できるはずがない、という設計面を考慮した構造になっています。

よって、POSTやPUTメソッドで送り出されたデータペイロードをCF2で処理することは現状不可能であることは最初の内に頭に入れておく必要があります。


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

図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書

URLのクエリ文字列からパスコードを渡す方法

CF2の概要はこの辺にして、ここからはS3リソースのアクセス制限機能を実装して試してみましょう。

前準備として、
前回説明した手順のように、適当なCFディストリビューションでS3バケットをOACで保護するところまで行ってください。

CloudFrontのダッシュボードから
[関数] > [関数を作成]をクリックします。

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

適当な名前で関数をJR2.0で作成します。

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

作成した関数の中身を編集していきます。

[構築] > [関数コード]のコード欄にコードを貼り付け、[変更を保存]で保存します。

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

ここで使うサンプルコードとして、以下のようなものを使います。

            
            //認証に失敗した際のレスポンス
const response401 = {
    statusCode: 401,
    statusDescription: 'Unauthorized'
};

function token_verify(token, key) {
    //認証トークンが無い場合
    if (!token) {
        throw new Error('No token supplied');
    }
    //認証できない場合
    if (!_verify(token, key)) {
        throw new Error('Signature verification failed');
    }
}

//認証のロジック
function _verify(input, key) {
    return input === key;
}

async function handler(event) {
    let request = event.request;
    //tokenヘッダーが存在しない場合には認証失敗で返す
    if (!request.querystring.token) {
        console.log("No token supplied in the querystring");
        return response401;
    }
    const token = request.querystring.token.value;
    //認証用の秘密鍵
    const secretKey = 'HOGE';
    try {
        token_verify(token, secretKey);
    }
    catch (e) {
        console.log(e);
        return response401;
    }
    //レスポンスからのtokenヘッダー値を空にする
    delete request.querystring.token;
    return request;
}
        
CF2関数が未発行のままだと、テストも出来ないので、[発行]タブから、[関数を発行]をクリックします。

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

次にテストを行います。

[テスト]タブから[JSONを編集]画面に行き、

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

以下のテストスニペットを貼り付けて、
[保存]します。

            
            {
    "version": "1.0",
    "context": {
        "eventType": "viewer-request"
    },
    "viewer": {
        "ip": "1.2.3.4"
    },
    "request": {
        "method": "GET",
        "uri": "/index.html",
        "headers": {},
        "cookies": {},
        "querystring": {
            "token": {
                "value": "aaaaa"
            }
        }
    }
}
        
では[関数をテスト]ボタンを押してテストを開始してみます。

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

テストの結果は、
401 Unauthorizedのレスポンスが返ってきています。

クエリ文字列で
token=aaaaaとパスコードと異なる値で試したのでこれは期待通りです。

では同じ要領で、JSONスニペットで
token=HOGE(パスコードと一致)にして再度関数をテストしてみると、

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

これも期待通りに、認証してアクセスを通してくれていることが分かります。

これでCF2の動作確認が終わったので、この関数をCFディストリビューションへ紐付けします。

対象のCFディストリビューションを選択し、
[ビヘイビア] > [ビヘイビアを作成]からビヘイビアを追加します。

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

ビヘイビアの他の設定項目はそのままデフォルト値のままにしておいて、
[関数の関連付け]の欄にある[ビューワーリクエスト]に、関数タイプをCloudFront Functions、関数ARNに先程のCF2関数を指定して、[ビヘイビアを作成]で確定します。

CFディストリビューションの変更が反映されデプロイされるのを待って、早速認証が有効になったか、外部からアクセスを試してみます。

まずは、認証トークンを含むクエリがなかったり、間違っていたりする場合の応答を確認します。

            
            $ curl -i -XGET https://xxxxxxxxxx.cloudfront.net/greeting.txt
HTTP/2 401
...

$ curl -i -XGET https://xxxxxxxxxx.cloudfront.net/greeting.txt?token=aaaaa
HTTP/2 401
...
        
ちゃんと401に応答していることが分かります。

ではちゃんとパスコードが一致した場合はどうでしょうか。

            
            $ curl -i -XGET https://xxxxxxxxxx.cloudfront.net/greeting.txt?token=HOGE
HTTP/2 200
content-type: text/plain
...

こんにちは!
        
今度はちゃんとファイルにアクセス出来ています。


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

図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書

ヘッダーからパスコードを渡す方法

先程のやり方だと、認証情報をもったクエリ文字列のパラメーターが第三者に読み取られる可能性もあり、あまりセキュアな方法とは言えません。

ヘッダーに認証用のトークンを付与する仕組みには、
「Authorization」ヘッダーがあり、これを使うことでブラウザ側に特別な扱いを行うように仕向けることができます。

「Authorization」には主にBasic認証とBearer認証の2つが使われることが多いですが、今回のように自前で認証用のロジックを構築するために「Authorization」を使ってしまうと、別に使っている認証用のサービスやライブラリー(Bearerの場合がほとんど)と競合する恐れもあります。

AuthorizationがどのサービスやAPIと競合するかどうかに配慮しながら開発するくらいなら、独自に認証用のヘッダーを作るほうが懸命と言えます。

ということで、
「X-Auth-Token」という名前のヘッダーをオレオレ認証用に使ってみましょう。

CF2関数の設定手順は先程の節と全く同じですので、同じ説明は割愛し、CF2関数の中身だけを以下に交換します。

            
            //認証に失敗した際のレスポンス
const response401 = {
    statusCode: 401,
    statusDescription: 'Unauthorized'
};

function token_verify(token, key) {
    //認証トークンが無い場合
    if (!token) {
        throw new Error('No token supplied');
    }
    //認証できない場合
    if (!_verify(token, key)) {
        throw new Error('Signature verification failed');
    }
}

//認証のロジック
function _verify(input, key) {
    return input === key;
}

async function handler(event) {
    let request = event.request;

    //X-Auth-Tokenヘッダーが存在しない場合には認証失敗で返す
    if (!request.headers['x-auth-token']) {
        console.log("No Authrization supplied in the headers");
        return response401;
    }
    const token = request.headers['x-auth-token'].value;

    //認証用の秘密鍵
    const secretKey = 'HOGE';
    try {
        token_verify(token, secretKey);
    }
    catch (e) {
        console.log(e);
        return response401;
    }
    //レスポンスからのX-Auth-Tokenヘッダー値を空にする
    delete request.headers['x-auth-token'];
    return request;
}
        
先程のコードから変わったところは、リクエストオブジェクトからのヘッダーを値を引き出すのに、request.headers['x-auth-token']としているところくらいです。

注意すべきなのは、コード内で利用するヘッダー名は小文字に変換する必要があります。つまり、
X-Auth-Tokenx-auth-tokenと直す必要があります。

他は至って先程のクエリ文字列の場合でやったことを同じです。

ではこのCF2関数をCFディストリビューションに割り当てて、外部からアクセスを試してみましょう。

            
            #👇トークンが異なるためアクセス拒否
$ curl -i -XGET -H 'X-Auth-Token:aaaaaaaa' https://xxxxxxxxxx.cloudfront.net/greeting.txt
HTTP/2 401
...

#👇トークンが一致
$ curl -i -XGET -H 'X-Auth-Token:HOGE' https://xxxxxxxxxx.cloudfront.net/greeting.txt
HTTP/2 200
content-type: text/plain
...

こんにちは!
        
いい感じにアクセス制限が出来ていることが分かります。


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

図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書

まとめ

今回は2種類のCloudFront Functionを使って、カスタム認証を行う基礎的な実装を紹介してみました。

まだパスコードによる復号・暗号化などの高度な仕組みについては触れていませんが、S3リソースの保護する方法の基礎的なやり方はご理解いただけたかと思います。

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

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

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