0
0

【AWS×Webアプリ】CloudFront+Basic認証(SAM)

Last updated at Posted at 2024-06-09

目的

・AWS上の静的Webサイトホスティングを有効にしたS3をCloudFrontで公開。
・Basic認証を実装。

前提条件

・SAMを使用してAWS上にリソースを作成する。
・Amazon CloudFront KeyValueStore+CloudFront Functionsを使用して実装する。

完成イメージ

image.png

SAMテンプレート

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  WebBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${AWS::StackName}-webbucket-${AWS::AccountId}
      VersioningConfiguration:
        Status: Enabled
      WebsiteConfiguration:
        IndexDocument: index.html
      PublicAccessBlockConfiguration:
        IgnorePublicAcls: false
  WebBucketBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref WebBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Principal:
              Service: cloudfront.amazonaws.com
            Action:
              - s3:GetObject
            Effect: Allow
            Resource:
              - !Sub ${WebBucket.Arn}/*
            Condition:
              StringEquals:
                AWS:SourceArn:
                  - !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${WebBucketDistribution}
  WebBucketDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          TargetOriginId: WebBucketOrigin
          Compress: true
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods:
            - GET
            - HEAD
          CachedMethods:
            - GET
            - HEAD
          CachePolicyId: !Ref WebBucketCachePolicy
          OriginRequestPolicyId: !Ref WebBucketOriginRequestPolicy
          FunctionAssociations:
            - EventType: viewer-request
              FunctionARN: !GetAtt WebBucketBasicAuthFunction.FunctionARN
        PriceClass: PriceClass_200
        DefaultRootObject: index.html
        Enabled: true
        Origins:
          - DomainName: !GetAtt WebBucket.RegionalDomainName
            Id: WebBucketOrigin
            OriginAccessControlId: !GetAtt WebBucketOriginAccessControl.Id
            S3OriginConfig:
              OriginAccessIdentity: ''
  WebBucketOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        SigningBehavior: always
        OriginAccessControlOriginType: s3
        SigningProtocol: sigv4
        Name: !Sub ${AWS::StackName}-OAC-${AWS::AccountId}
  WebBucketCachePolicy:
    Type: AWS::CloudFront::CachePolicy
    Properties:
      CachePolicyConfig:
        Name: WebBucketCachePolicy
        DefaultTTL: 86400
        MaxTTL: 31536000
        MinTTL: 3600
        ParametersInCacheKeyAndForwardedToOrigin:
          EnableAcceptEncodingGzip: true
          EnableAcceptEncodingBrotli: true
          CookiesConfig:
            CookieBehavior: none
          HeadersConfig:
            HeaderBehavior: none
          QueryStringsConfig:
            QueryStringBehavior: none
  WebBucketOriginRequestPolicy:
    Type: AWS::CloudFront::OriginRequestPolicy
    Properties:
      OriginRequestPolicyConfig:
        Name: WebBucketOriginRequestPolicy
        CookiesConfig:
          CookieBehavior: none
        HeadersConfig:
          HeaderBehavior: none
        QueryStringsConfig:
          QueryStringBehavior: none
  DeleteCacheLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: LambdaS3Policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: arn:aws:logs:*:*:*
              - Effect: Allow
                Action:
                  - cloudfront:CreateInvalidation
                  - cloudfront:ListDistributions
                Resource: '*'
  DeleteCache:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.lambda_handler
      Runtime: python3.11
      Role: !GetAtt DeleteCacheLambdaRole.Arn
      Environment:
        Variables:
          Stack_NAME: !Sub ${AWS::StackName}
      CodeUri: ./DeleteCache
      Events:
        WebBucket:
          Type: S3
          Properties:
            Bucket: !Ref WebBucket
            Events:
              - s3:ObjectCreated:*
              - s3:ObjectRemoved:*
            Filter:
              S3Key:
                Rules:
                  - Name: suffix
                    Value: index.html
  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref DeleteCache
      Action: lambda:InvokeFunction
      Principal: s3.amazonaws.com
      SourceAccount: !Ref AWS::AccountId
      SourceArn: !Sub arn:aws:s3:::${WebBucket}
  WebBucketBasicAuthKVS:
    Type: AWS::CloudFront::KeyValueStore
    Properties:
      Name: WebBucketBasicAuthKVS
  WebBucketBasicAuthFunction:
    Type: AWS::CloudFront::Function
    Properties:
      Name: WebBucketBasicAuthFunction
      FunctionConfig:
        Comment: Function with KeyValueStore
        Runtime: cloudfront-js-2.0
        KeyValueStoreAssociations:
          - KeyValueStoreARN: !GetAtt WebBucketBasicAuthKVS.Arn
      FunctionCode: !Sub |
        import cf from 'cloudfront';
        const kvsId = '${WebBucketBasicAuthKVS.Id}';
        const kvsHandle = cf.kvs(kvsId);
        async function handler(event) {
          const request = event.request;
          const headers = request.headers;
          if (
            typeof headers.authorization === 'undefined' ||
            typeof headers.authorization.value === 'undefined'
          ) {
            return return401();
          }
          const encoded = headers.authorization.value.split(' ')[1];
          const decoded = Buffer.from(encoded, 'base64').toString();
          const userRequest = decoded.split(':')[0];
          const passRequest = decoded.split(':')[1];
          const exist = await kvsHandle.exists(userRequest);
          if (!exist) {
            return return401();
          }
          const passStore = await kvsHandle.get(userRequest);
          if (passStore !== passRequest) {
            return return401();
          }
          return request;
        }
        const return401 = () => {
          return {
            statusCode: 401,
            statusDescription: 'Unauthorized',
            headers: { 'www-authenticate': { value: 'Basic' } },
          };
        };
      AutoPublish: true
Outputs:
  WebsiteURLonCF:
    Description: URL for website hosted on CloudFront
    Value: !GetAtt WebBucketDistribution.DomainName

①以下、前回記事より設定内容に変更なし
・WebBucket(S3バケット)
・WebBucketBucketPolicy(バケットポリシー)
・WebBucketOriginAccessControl(OAC)
・WebBucketCachePolicy(キャッシュポリシー)
・WebBucketOriginRequestPolicy(オリジンリクエストポリシー)
・DeleteCacheLambdaRole(Lambda用IAMロール)
・DeleteCache(キャッシュ削除用Lambda)
・LambdaInvokePermission(Lambdaに付与するリソースベースのポリシー)

②WebBucketDistribution(CloudFrontディストリビューション)
・FunctionAssociations:CloudFront Functionsとの関連付け

③WebBucketBasicAuthKVS(CloudFront KeyValueStore)

④WebBucketBasicAuthFunction(CloudFront Functions)

動作確認

①作成したKeyValueStoreでKey・Valueペアを作成
image.png
image.png

②Webサイトへアクセス
・ユーザ名とパスワードが要求されることを確認
image.png

③正しいユーザで認証されることを確認1
image.png
image.png

④正しいユーザで認証されることを確認2
image.png
image.png

⑤未登録のユーザ・誤ったパスワードでの認証が失敗することを確認
image.png
image.png
image.png

参考(前回記事)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0