$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

ALBで特定のpathのときだけCognito認証を通す構成をaws-cdkで作る

管理画面の時だけ認証をかけたいみたいな要件はよくある。今回はその要件をALBで特定のpathの時だけCognito認証を通すという構成で実装してみたのでメモ。構成はaws-cdkを使った。

やりたいこと

  • /admin/以下の場合はCognitoの認証をかけ、許可したユーザーしかアクセスできないようにする
  • それ以外のパスではアプリケーションに認証なしで繋がるようにする

f:id:shiba_yu36:20200918094327p:plain

実装

aws-cdk 1.62.0を使って実装した。なお今回のコードは実際の実装をミニマムに変更しているので、動作確認はちゃんと出来ていない(たぶんそのままだと動かなそう)。雰囲気だけ感じてください。

import { IVpc, Port } from "@aws-cdk/aws-ec2";
import { Ec2Service } from "@aws-cdk/aws-ecs";
import {
  ApplicationLoadBalancer,
  ApplicationProtocol,
  ListenerAction,
  ListenerCondition,
} from "@aws-cdk/aws-elasticloadbalancingv2";
import { AuthenticateCognitoAction } from "@aws-cdk/aws-elasticloadbalancingv2-actions";
import { Construct, Duration } from "@aws-cdk/core";
import {
  UserPool,
  UserPoolClient,
  OAuthScope,
  UserPoolDomain,
  CfnUserPoolClient,
} from "@aws-cdk/aws-cognito";

export interface PathRestrictedAlbProps {
  /**
   * アプリケーション
   */
  appService: Ec2Service;

  /**
   * ALBなどを配置するvpc
   */
  vpc: IVpc;
}
export class PathRestrictedAlb extends Construct {
  constructor(
    parent: Construct,
    name: string,
    props: PathRestrictedAlbProps
  ) {
    super(parent, name);

    const applicationName = "example";

    // パスワードポリシーとか自分の用途に合わせていい感じにする
    const userPool = new UserPool(this, "UserPool", {
      userPoolName: applicationName,
      selfSignUpEnabled: false,
      signInAliases: {
        username: true,
        email: true,
      },
      standardAttributes: {
        email: {
          required: true,
        },
      },
      passwordPolicy: {
        minLength: 20,
        requireLowercase: true,
        requireUppercase: true,
        requireDigits: true,
        requireSymbols: true,
      },
    });
    const userPoolClient = new UserPoolClient(this, "UserPoolClient", {
      userPool: userPool,

      // Required minimal configuration for use with an ELB
      generateSecret: true,
      authFlows: {
        userPassword: true,
        refreshToken: true,
      },
      oAuth: {
        flows: {
          authorizationCodeGrant: true,
        },
        scopes: [OAuthScope.EMAIL],
        callbackUrls: [`https://${applicationName}.com/oauth2/idpresponse`],
      },
    });
    const cfnClient = userPoolClient.node
      .defaultChild as CfnUserPoolClient;
    cfnClient.addPropertyOverride("RefreshTokenValidity", 1);
    cfnClient.addPropertyOverride("SupportedIdentityProviders", ["COGNITO"]);

    const userPoolDomain = new UserPoolDomain(this, "UserPoolDomain", {
      userPool: userPool,
      cognitoDomain: {
        domainPrefix: applicationName,
      },
    });

    const alb = new ApplicationLoadBalancer(this, "Alb", {
      vpc: props.vpc,
      loadBalancerName: applicationName,
      internetFacing: true,
    });
    alb.connections.allowFromAnyIpv4(Port.tcp(443));

    const listener = alb.addListener("Listener", {
      port: 443,
      protocol: ApplicationProtocol.HTTPS,
      open: true,
      certificateArns: [
        // 証明書を設定する
        "...",
      ],
    });

    // デフォルトはECSへ
    const targetGroup = listener.addTargets("Target", {
      targetGroupName: applicationName,
      protocol: ApplicationProtocol.HTTP,
      port: 80,
      targets: [props.appService],
    });

    // conditionsをいい感じに設定することで特定pathだけCognito通すとかができる
    // ここでは/admin/以下の場合はCognito認証を通し、認証をパスした場合のみ
    // ECSへ到達するようにする
    listener.addAction(`Action1`, {
      action: new AuthenticateCognitoAction({
        userPool: userPool,
        userPoolClient: userPoolClient,
        userPoolDomain: userPoolDomain,
        next: ListenerAction.forward([targetGroup]),
      }),
      conditions: ListenerCondition.pathPatterns(["/admin/*"]),
      priority: 1,
    });
  }
}

あとはCognitoのユーザー管理でアクセスできる人を追加したら良い。 f:id:shiba_yu36:20200921131912p:plain

これで、/admin/以下にアクセスしたときに認証のダイアログが出て、先程追加したユーザーしかアクセスできなくなる。 f:id:shiba_yu36:20200921131953p:plain

まとめ

今回はALBで特定のpathのときだけCognito認証を通す構成をaws-cdkで作ってみた。今回は特定パス以下だけ認証を通すようにしたが、例えば管理画面はドメインごと変えたいとなった場合にもデフォルトアクション全てで認証をかけるようにすれば同じような構成で出来ると思う。

Cognitoは他にもMFAを使うなど認証系は簡単に実装できるので、自分で実装したらだるい認証系はSaaSに任せていけると良さそうだった。

参考

https://docs.aws.amazon.com/cdk/api/latest/docs/aws-elasticloadbalancingv2-actions-readme.html