【AWS×Webアプリ】CloudFront+Basic認証（SAM）

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

## 前提条件
・SAMを使用してAWS上にリソースを作成する。
・Amazon CloudFront KeyValueStore＋CloudFront Functionsを使用して実装する。

## 完成イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/5e8df92c-3413-2859-46e7-ab9d0dd1a174.png)

## SAMテンプレート
```yaml: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/7b5f0c54-7799-d54d-a6ac-8bb699ab36f3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/20fb1209-f59c-55bf-c0ac-df0691ad8577.png)

②Webサイトへアクセス
・ユーザ名とパスワードが要求されることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/ef3fd6fc-2f23-028d-6c4c-b147efa5d27b.png)

③正しいユーザで認証されることを確認１
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/d51f227c-ca9e-e945-4210-f25e6d708b91.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/44388b8d-cfed-974f-08ec-e0ccc06952eb.png)

④正しいユーザで認証されることを確認２
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/10e88648-4b7c-9ab3-6ca9-29a3b09ceeb2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/58c84cad-d8b3-4899-7f7b-7381452d24c6.png)

⑤未登録のユーザ・誤ったパスワードでの認証が失敗することを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/cfa0c189-cd35-658f-243b-25f8776ebb44.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/fdda107b-e15c-c830-e2dc-8ba108968c53.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/a739e627-9e1a-5818-d2d3-99e7cec2da65.png)

## 参考（前回記事）
https://qiita.com/Haruki-N/items/f9e8fb32fd20285b57ad