cognito-at-edgeで特定のパスにのみ認証を設定

こんにちは、ひるたんぬです。

最近は自分が食べたいと思ったものを、レシピを参考に作ることにハマっています。
レシピを見る際に、必ず「○人前」と書かれていますが、あれは何を基準としているのでしょうか?
調べてみても具体的な基準などは見当たらないのですが、大体乾麺のパスタだと、80〜100gというレシピが多い印象です。
※ ご存知の方がいらっしゃいましたら、何らかの手段や媒体でご教示・発信いただけると幸いです。。。

私はこの量では満足することができないので、「私は人じゃない…?」と疑いかけますが、それは穿った見方ですね。
ただ、「一人前」と言う表現は個人差が大きいと思うので、もっと良い表現があってもいいのになぁ…と思った今日このごろです。

さて、今回はタイトルにもある通り、Amazon CloudFrontとAmazon S3を用いた静的コンテンツ配信基盤において、cognito-at-edgeで特定のパスにのみ認証を設定する方法をご紹介します。

やりたいこと

静的コンテンツを公開する場合、nginxやapacheを利用してWebサーバーを立てて公開する方法もありますが、クラウドネイティブな方法としてCloudFrontとS3を利用する方法が挙げられます。

これらを利用することにより、需要に応じたリソースのスケールや料金負担が実現できるほか、サーバー(インフラ)のメンテナンスを自身で行う必要がなくなるなど、コンテンツ配信の事業者の方々にとっても大きなメリットがあります。

一方、コンテンツの中には、特定のユーザーに公開を絞りたいケースもあるかと思います。
今回は、一つのサイト(ドメイン)の中で、全体に公開したいコンテンツと、公開を限定したいコンテンツを制御したいという事例を考えてみます。

 

使うもの

配信基盤として用いるものはAmazon CloudFrontとAmazon S3です。
また、認証にはAmazon Cognitoを利用し、CloudFrontと認証機能を連携させる手段としてLambda@Edgeを利用します。

具体的なLambda@Edge内の処理プログラムについては、AWS (AWS Labs)より提供されているcognito-at-edgeを利用します。

 

事前準備・確認

まずは、特定のパスに限定させず、全てのコンテンツに対して公開を制限したいと思います。

コンテンツ格納用バケットの用意

コンテンツを格納するためのS3バケットを用意します。今回は東京リージョンにデプロイしたいと思います。
バケット名以外の設定はデフォルトで問題ありません。(今回は「202509-web-contents-bucket」を作成しました。)

作成が終わったら、何かしらコンテンツを格納しておきましょう。
こういうときに生成AIを使うとサクッと作れていいですね。

ChatGPTをはじめとする生成AIを「チャッピー」と呼ぶ界隈があることを最近知り、少し驚きました。
参考:日本経済新聞社 | 「令和なコトバ「チャッピー」 わたしに寄り添う君の名は」

Cognitoのデプロイ

Cognitoをデプロイします。今回は東京リージョンにデプロイしたいと思います。

CloudFormationより、以下のテンプレートをデプロイします。
パラメータのCognitoCallbackURLについては、ひとまずこのままでもOKです。

AWSTemplateFormatVersion: 2010-09-09
Description: Create Cognito. Please deploy at ap-northeast-1 region.

# Parameters
Parameters:
  ## CognitoコールバックURL
  CognitoCallbackURL:
    ### cognito-at-edgeの仕様上、最後は"/"をつけない
    Description: Cognito callback URL
    Type: String
    Default: https://example.com

# Resources
Resources:  
  # Cognito関連リソース
  ## Cognito User Pool
  CognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: pathauth-cognitouserpool
      UserPoolTier: PLUS  ### Cognitoのプラン
      DeletionProtection: INACTIVE  ### 検証のため(削除できるように)
      UserPoolTags:
        Name: pathauth-cognitouserpool
  ## Cognito User Pool Client
  CognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: pathauth-cognitouserpool-client
      UserPoolId: !Ref CognitoUserPool
      GenerateSecret: false
      AllowedOAuthFlowsUserPoolClient: true
      AllowedOAuthScopes:
        - openid
      AllowedOAuthFlows:
        - code
      CallbackURLs:
        - !Ref CognitoCallbackURL
      SupportedIdentityProviders:
        - COGNITO
      ExplicitAuthFlows:
        - ALLOW_USER_SRP_AUTH
  ## Cognito User Pool Domain
  CognitoUserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      UserPoolId: !Ref CognitoUserPool
      Domain: pathauth-userpool-domain
      ManagedLoginVersion: 2
  ## Cognito Managed Login Page
  CognitoManagedLogin:
    Type: AWS::Cognito::ManagedLoginBranding
    Properties:
      UserPoolId: !Ref CognitoUserPool
      ClientId: !Ref CognitoUserPoolClient
      UseCognitoProvidedValues: true

# Outputs
Outputs:
  CognitoUserPoolId:
    Description: Cognito User Pool ID
    Value: !Ref CognitoUserPool
  CognitoUserPoolClientId:
    Description: Cognito User Pool Client ID
    Value: !Ref CognitoUserPoolClient
  CognitoUserPoolDomain:
    Description: Cognito User Pool Domain
    Value: !Sub "pathauth-userpool-domain.auth.${AWS::Region}.amazoncognito.com"

Lambda@Edgeのコード準備

続いて、Lambda@Edgeにデプロイするためのcognito-at-edgeを準備します。
事前にバージニア北部リージョンに、完成したコードを格納するためのバケットを用意しておきます。バケット名以外の設定はデフォルトで問題ありません。(今回は「202509-cognito-at-edge-bucket」を作成しました。)

続いて、任意のコードエディタなどで、以下のファイルを作成し、「index.js」という名前で保存します。
userPoolId、userPoolAppId、userPoolDomainについては、先ほど作成したスタックの出力を参照してください。

const { Authenticator } = require('cognito-at-edge');

const authenticator = new Authenticator({
    // Replace these parameter values with those of your own environment
    region: 'ap-northeast-1', // user pool region
    userPoolId: 'ap-northeast-1_abcdefgh1', // user pool ID
    userPoolAppId: '1examp1ec1ient1d', // user pool app client ID
    userPoolDomain: 'pathauth-userpool-domain.auth.ap-northeast-1.amazoncognito.com', // user pool domain
});

exports.handler = async (request) => authenticator.handle(request);

作成が完了したらCloudShellで以下のコマンドを実行します。
[ ]内については適宜作業・ご自身の環境に置き換えてください。

[index.jsをアップロード]
mkdir cognito-at-edge
cd cognito-at-edge
npm install cognito-at-edge
mv ~/index.js ./
npx esbuild --bundle index.js --minify --outfile=bundle/index.js --platform=node
cd bundle
zip -r lambda-edge-auth.zip ./index.js
aws s3 cp lambda-edge-auth.zip s3://[宛先S3バケット名]/lambda-edge-auth.zipさ

最終的にS3に「lambda-edge-auth.zip」というファイルがアップロードされていればOKです。

CloudFront・Lambda@Edgeのデプロイ

Cognitoと同じようにCloudFormationでデプロイします。
バージニア北部リージョンでデプロイするようにしてください。

パラメータについては、それぞれ作成したバケット名、Lambda@Edgeで用いるコードのファイル名(上記に従っていればlambda-edge-auth.zipになっているはずです。)を入力してください。

AWSTemplateFormatVersion: 2010-09-09
Description: Create CloudFront and Lambda@Edge. Please deploy at us-east-1 region.
Transform: AWS::Serverless-2016-10-31

# Mappings
Mappings:
  # CachePolicyIdは以下の記事を参照
  # https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html
  CachePolicyIds:
    # Recommended for S3
    CachingOptimized:
      Id: 658327ea-f89d-4fab-a63d-7e88639e58f6

# Parameters
Parameters:
  # コンテンツの格納先(S3バケット名)
  OriginS3Bucket:
    Description: S3 bucket for contents
    Type: String
    Default: s3-bucket-name-for-contents
  # Lambda@Edgeのコード格納先(S3バケット名)
  LambdaEdgeCodeS3Bucket:
    Description: S3 bucket for Lambda@Edge code
    Type: String
    Default: s3-bucket-name-for-cognito-at-edge
  # Lambda@Edgeのコード格納先(S3キー名)
  LambdaEdgeCodeS3Key:
    Description: S3 key for Lambda@Edge code
    Type: String
    Default: lambda-edge-auth.zip
  
# Resources
Resources:
  # CloudFront周辺リソース
  ## CloudFront Distribution
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - Id: "S3Origin"
            DomainName: !Sub "${OriginS3Bucket}.s3.ap-northeast-1.amazonaws.com"
            S3OriginConfig:
              OriginAccessIdentity: ""
            OriginAccessControlId: !Ref CloudFrontOriginAccessControl
        DefaultCacheBehavior:
          TargetOriginId: "S3Origin"
          ViewerProtocolPolicy: redirect-to-https
          # Recommended for S3
          CachePolicyId: !FindInMap [CachePolicyIds, CachingOptimized, Id]
          # cognito-at-edge
          LambdaFunctionAssociations:
            - EventType: viewer-request
              LambdaFunctionARN: !Ref LambdaEdgeFunctionForAuth.Version
        Enabled: true
        IPV6Enabled: false
      Tags:
        - Key: Name
          Value: !Sub "pathauth-cloudfront"
  ## CloudFront OriginAccessControl
  CloudFrontOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Description: "Origin Access Control For S3 Static Website Hosting"
        Name: !Sub "pathauth-s3oac"
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

  # Lambda@Edge周辺リソース
  ## Lambda@Edge Function for authentication
  LambdaEdgeFunctionForAuth:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub "pathauth-lambdaedge-auth"
      AutoPublishAlias: pathauth
      Handler: index.handler
      Runtime: nodejs22.x
      MemorySize: 128
      Timeout: 5
      CodeUri:
        Bucket: !Ref LambdaEdgeCodeS3Bucket
        Key: !Ref LambdaEdgeCodeS3Key
      Role: !GetAtt RoleForLambdaEdge.Arn
  ## CloudWatch Log Group for Lambda@Edge
  ### 実際の実行ログは各エッジロケーションに作成される
  LogGroupForLambdaEdge:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${LambdaEdgeFunctionForAuth}"
      RetentionInDays: 30
  ## IAM Role for Lambda@Edge
  RoleForLambdaEdge:
    Type: AWS::IAM::Role
    Properties:
      RoleName: cognito-at-edge-role
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
                - edgelambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - !Ref PolicyForLambdaEdge
  ## IAM Policy for Lambda@Edge
  PolicyForLambdaEdge:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: cognito-at-edge-policy
      Description: Policy for Lambda@Edge Function
      Path: /
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - iam:CreateServiceLinkedRole
            Resource: "*"
          - Effect: Allow
            Action:
              - lambda:GetFunction
              - lambda:EnableReplication
            Resource:
              ### Allow access to all Lambda functions in the account at the specified region(us-east-1)
              - !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:*:*"
          - Effect: Allow
            Action:
              - cloudfront:UpdateDistribution
            Resource:
              ### Allow access to all CloudFront distributions in the account
              - !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/*"

コンテンツバケットのバケットポリシー設定

コンテンツバケットにCloudFrontからのアクセス(OAC)を許可します。
まずは、CloudFrontの当該ディストリビューションを開き、「オリジン」タブを選択します。

「S3Origin」を選択し、「編集」を押下します。
画面中段にあるOACに関する設定項目から、「ポリシーをコピー」を押下し、その下にある「S3 バケットアクセス許可に移動」を押下します。

すると、コンテンツバケットのページが開くので、「アクセス許可」からバケットポリシーを変更します。

これにより、CloudFrontの指定ディストリビューションからオブジェクトの取得が許可されます。

CognitoのコールバックURL変更

今回は検証のため、CloudFrontのデフォルトドメイン(*.cloudfront.net)を使用します。
それに対応するよう、CognitoのコールバックURLを変更します。今回はコンソールより手動で変更します。

Cognitoのユーザープールを開いたら、「アプリケーションクライアント」から作成したアプリケーションクライアント(pathauth-cognitouserpool-client)を開きます。

次に、「ログインページ」タブを押下し、「マネージドログインページの設定」を編集します。
編集画面の一番上にある「許可されているコールバックURL」に、CloudFrontのURLを貼り付けます。

URLの最後に” / “をつけないようご注意ください。
変更が完了したら保存します。

動作確認

では、きちんと認証機能が働くか確認をします。
任意のWebブラウザを開き、CloudFrontのドメイン名と、表示させたいコンテンツのパス(例:examp1edoma1n.cloudfront.net.index.html)を入力しアクセスします。

きちんとサインインページに遷移しましたね。実際にユーザー名などを入力しサインインすると…

コンテンツがしっかり表示されました。

今回は、あらかじめCognitoのユーザーをコンソールより作成しています。

 

検証

…ここからが本番です。
特定のパスのみ認証を設定していきます。今回は以下のようなフォルダ構成を仮定し、limited配下のみ公開に制限をかけます。

コンテンツバケット
├ limited/
│ ├ index.html
│ └ style.css
├ index.html
└ style.css

CloudFrontディストリビューションのビヘイビア編集

CloudFrontのコンソール画面から「ビヘイビア」タブを選択し、「ビヘイビアの作成」を押下します。

パスパターンに制限をかけたいパスにアスタリスクを追加(/limited*)、オリジンには宛先のS3オリジン(S3Origin)を選択し、他の箇所はデフォルトで「Create behavior」を押下します。

次に、デフォルトビヘイビア(*)を編集します。編集画面の一番下、「関数の関連付け」を「関連付けなし」に設定します。

CognitoのコールバックURL編集

CognitoのコールバックURLをパスつきのURLに変更します。
詳細な手順は事前準備のときとほとんど同じなので省略しますが、以下のようになっていればOKです。

Lambda@Edgeのコード編集

Lambda@Edgeのコードを特定のパスに対してのみ認証できるよう変更します。
先ほど作成したindex.jsに一部追記をします。

const { Authenticator } = require('cognito-at-edge');

const authenticator = new Authenticator({
    // Replace these parameter values with those of your own environment
    region: 'ap-northeast-1', // user pool region
    userPoolId: 'ap-northeast-1_abcdefgh1', // user pool ID
    userPoolAppId: '1examp1ec1ient1d', // user pool app client ID
    userPoolDomain: 'pathauth-userpool-domain.auth.ap-northeast-1.amazoncognito.com', // user pool domain
    parseAuthPath: '/limited', // 追記①
    cookiePath: '/limited' // 追記②
});

exports.handler = async (request) => authenticator.handle(request);

追記が終わったら先程と同じようにCloudShellからzip化し、S3にアップロードします。名前は変えておくと分かりやすいかと思います。今回は「lambda-edge-auth-path.zip」としました。

Lambda@Edgeの更新

作成したコードでLambda@Edgeを更新します。今回はコンソールより更新します。
まず、先程アップロードしたS3バケットから、当該コンテンツのオブジェクトURLをコピーします。

次にLambdaのコンソールから、認証に用いているLambda(pathauth-lambdaedge-auth)を探し開きます。
コード編集画面の右上にある「アップロード元」から「Amazon S3の場所」を選択し、先程コピーしたURLを貼り付けます。

関数の更新が完了したら、Lambda@Edgeにデプロイします。
右上の「アクション」から「Lambda@Edgeへのデプロイ」を選択します。

ディストリビューションは作成したもの、キャッシュ動作には先程作成したビヘイビア(/limited*)を選択します。CloudFrontイベントについては「ビューアーリクエスト」に変更してください。

最後に一番下の「Lambda@Edge へのデプロイを確認」に✅️を入れてデプロイをします。

以上で変更作業は完了です。

動作確認

早速想定の挙動になるか見ていきます。
まずは、公開されているコンテンツ(ルートのindex.html)にアクセスします。

問題なく表示されました。認証もありません。
では、次に限定コンテンツ(/limited/index.html)にアクセスします。

きちんと認証画面に移りましたね!良かったです。認証情報を入れてサインインしてみると…

いかにも特別感のあるページが表示されました。ありがとう、チャッピー。

メンバー数やコンテンツ数などそれっぽく出ていますが、中身は空っぽです。

 

終わりに

事前準備が少し多かったので長くなってしまいましたが、パスごとの認証は思ったよりもシンプルな手順でできたので良かったです。
本記事の内容が、どなたかの参考になることを願っております。

余談ですが、先日生まれて初めてオイルマッサージなるものを体験してきました。
担当の方に「全身ボロボロですね…」と遠回しに言われ少し傷ついたので、折を見て通おうかなと思ったりしています。

タイトルとURLをコピーしました