[AWS CDK] CloudFront FunctionでWebサイトにBasic認証をかける

2021.07.06

こんにちは、CX事業本部の若槻です。

PoCやデモなどで使用するWebサイト(CloudFront + S3)で「Cognitoを使うほどでは無いけど何らかの認証は掛けておきたい」という場合はBasic認証という選択肢があります。

このBasic認証をCloudFront + S3という構成で掛けたい場合の実装は、今まではLambda@Edgeを使う場合が多かったですが、最近リリースされた、よりユーザーに近いロケーションでより高速な処理が可能なCloudFront Functionsでも実装が可能です。

今回は、このCloudFront Functionを使用してWebサイトにBasic認証をかける設定をAWS CDKで実装してみました。

CloudFront Functionコード

lambda/basic-authentication/index.js

function handler(event) {
  var request = event.request;
  var headers = request.headers;

  // echo -n user:pass | base64
  var authString = "Basic dXNlcjpwYXNz";

  if (
    typeof headers.authorization === "undefined" ||
    headers.authorization.value !== authString
  ) {
    return {
      statusCode: 401,
      statusDescription: "Unauthorized",
      headers: { "www-authenticate": { value: "Basic" } }
    };
  }

  return request;
}

下記の記事のコードがシンプルでしたのでそのまま活用させて頂きました。

CDKコード

CloudFront + S3 + CloudFront Function から成る静的ホスティングなWebサイトです。

lib/sample-app-stack.ts

import * as cdk from "@aws-cdk/core";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as s3 from "@aws-cdk/aws-s3";
import * as s3deploy from "@aws-cdk/aws-s3-deployment";
import * as iam from "@aws-cdk/aws-iam";

export class SampleAppStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const accountId = cdk.Stack.of(this).account;

    const websiteBucket = new s3.Bucket(this, "WebsiteBucket", {
      bucketName: `${props.stageName}-${props.projectName}-website-${accountId}`,
      websiteErrorDocument: "index.html",
      websiteIndexDocument: "index.html",
    });

    const websiteIdentity = new cloudfront.OriginAccessIdentity(
      this,
      "WebsiteIdentity",
      {
        comment: `${props.stageName}-${props.projectName}-identity`,
      }
    );

    const webSiteBucketPolicyStatement = new iam.PolicyStatement({
      actions: ["s3:GetObject"],
      effect: iam.Effect.ALLOW,
      principals: [
        new iam.CanonicalUserPrincipal(
          websiteIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId
        ),
      ],
      resources: [`${websiteBucket.bucketArn}/*`],
    });

    websiteBucket.addToResourcePolicy(webSiteBucketPolicyStatement);

    // CloudFront Functionリソースの定義
    const basicAuthFunction = new cloudfront.Function(
      this,
      "BasicAuthFunction",
      {
        functionName: `basic-authentication`,
        code: cloudfront.FunctionCode.fromFile({
          filePath: "lambda/basic-authentication/index.js",
        }),
      }
    );

    const websiteDistribution = new cloudfront.CloudFrontWebDistribution(
      this,
      "WebsiteDistribution",
      {
        comment: `${props.stageName}-${props.projectName}-distribution`,
        errorConfigurations: [
          {
            errorCachingMinTtl: 300,
            errorCode: 403,
            responseCode: 200,
            responsePagePath: "/index.html",
          },
          {
            errorCachingMinTtl: 300,
            errorCode: 404,
            responseCode: 200,
            responsePagePath: "/index.html",
          },
        ],
        originConfigs: [
          {
            s3OriginSource: {
              s3BucketSource: websiteBucket,
              originAccessIdentity: websiteIdentity,
            },
            behaviors: [
              {
                isDefaultBehavior: true,
                // CloudFront FunctionをDistributionに設定
                functionAssociations: [
                  {
                    eventType: cloudfront.FunctionEventType.VIEWER_REQUEST,
                    function: basicAuthFunction,
                  },
                ],
              },
            ],
          },
        ],
        priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
      }
    );

    new s3deploy.BucketDeployment(this, "WebsiteDeploy", {
      sources: [s3deploy.Source.asset("./web/build")],
      destinationBucket: websiteBucket,
      distribution: websiteDistribution,
      distributionPaths: ["/*"],
    });
  }
}

DistributionへのFunctionの紐付けは、Lambda@Edgeの場合はlambdaFunctionAssociationsを使用しますが、CloudFront Functionの場合はfunctionAssociationsを使用することに注意してください。

動作確認

cdk deployでリソースをデプロイします。

WebサイトのURLにアクセスするとブラウザのBasic認証のダイアログが表示されます。

IDとパスワードを入力してログインをクリックします。

S3バケットに静的ホスティングされたWebサイトにアクセスできました。

その他動作としては、IDまたはパスワードを間違えると再度ダイアログが表示され、ダイアログでキャンセルをクリックすると真っ白なページのみ表示される動作となります。

CloudFront Functionでは「Buffer」が使えない?

CloudFront Functionのコード内でBufferを使用してIDとパスワードのBase64エンコード出来ないか試してみました。

    //var authString = "Basic dXNlcjpwYXNz";
    var authString = 'Basic ' + Buffer.from('user:pass').toString('base64');

Functionをデプロイしてテストすると下記のようにBufferがReferenceErrorとなりました。

Error Message: The CloudFront function associated with the CloudFront distribution is invalid or could not run. ReferenceError: "Buffer" is not defined in 7

Lambda@Edgeの場合はBufferが使えるはずなのですがCloudFront Functionでは使えないようです。高速化の一環としてFunctionへのモジュールのバンドルを最低限にしているようですね。

おわりに

CloudFront Functionを使用してWebサイトにBasic認証をかける設定をAWS CDKで実装してみました。Cognitoの導入などの重い実装もなくサクッとWebサイトに認証機能を導入出来てよかったです。

参考

以上