見出し画像

Cognito で Google 認証しつつ、システムごとにユーザーを制限する

はじめに

私がお手伝いしているチームは、1つの大きなシステムがあるわけではなく、小さいシステムが複数ある感じなのですが、各システムで認証機能を実装するのは避けた方がいいかなと考えています。

  • 社内メンバーのみで使うシステムが多い

    • 各システムの管理画面とか

  • 社内のセキュリティ要件に合格しようと思うと大変

    • パスワード強度の設定

    • パスワードリセット機能

    • 一定回数パスワードを間違えたときにロックする機能

    • などなど

そこで考えたのが、Cognito + Google 認証です。
Google Workspace を使っていれば、すでにユーザーベースはあるので、それを利用します。
これで解決したように思えますが、逆に Google Workspace のユーザーであれば、どのアプリにも入れてしまうのは問題です。
今回はこれを Cognito@Edge に少し手を加えることで解決したという内容になります。

Cognito@Edge とは

Lambda@Edge から Cognito のユーザープールにアクセスし、認証済みユーザーのみアクセス可能とします。

使い方は簡単で、以下のように authenticator.handle をそのまま Lambda@Edgehandler として export するだけです。

// index.ts
const { Authenticator } = require('cognito-at-edge');

const authenticator = new Authenticator({
  // Replace these parameter values with those of your own environment
  region: 'us-east-1', // user pool region
  userPoolId: 'us-east-1_tyo1a1FHH', // user pool ID
  userPoolAppId: '63gcbm2jmskokurt5ku9fhejc6', // user pool app client ID
  userPoolDomain: 'domain.auth.us-east-1.amazoncognito.com', // user pool domain
});

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

環境変数が使えないので、userPoolId などはベタ書きするか環境変数以外の方法で注入する必要があります。

これに手を加えて、特定のユーザーのみアクセス可能にします。

ユーザーが所属するグループをチェックする

強引ですが、Cognito@Edge の Authenticator を使って、認証ユーザーの情報を取得し、下記の例では、エラーページにリダイレクトします。
(401 などエラーを返してもいいのかもしれません)

// index.ts
import { Authenticator } from "cognito-at-edge";
import type { CloudFrontRequest, CloudFrontRequestEvent } from "aws-lambda";

const authenticator = new Authenticator({
  // Replace these parameter values with those of your own environment
  region: 'us-east-1', // user pool region
  userPoolId: 'us-east-1_tyo1a1FHH', // user pool ID
  userPoolAppId: '63gcbm2jmskokurt5ku9fhejc6', // user pool app client ID
  userPoolDomain: 'domain.auth.us-east-1.amazoncognito.com', // user pool domain
  cookieExpirationDays: 1,
  cookieDomain: 'example.com',
  cookiePath: '/',
});

const requiredUserGroup = 'app1-user-group';
const redirectUrl = 'https://app1.example.com';

async function fetchUser(request: CloudFrontRequest) {
  try {
    const tokens = authenticator._getTokensFromCookie(request.headers.cookie);
    return await authenticator._jwtVerifier.verify(tokens.idToken as string);
  } catch (_error) {
    return null
  }
}

exports.handler = async (event: CloudFrontRequestEvent) => {
  const response = await authenticator.handle(event);
  const { request } = event.Records[0].cf;
  const user = await fetchUser(request);
  if (user) {
    const userGroups = user['cognito:groups'];
    if (!userGroups.includes(requiredUserGroup)) {
      return {
        status: '302',
        headers: {
          'location': [{
            key: 'Location',
            value: [redirectUrl, 'groups', requiredUserGroup].join('/'),
          }],
          'cache-control': [{
            key: 'Cache-Control',
            value: 'no-cache, no-store, max-age=0, must-revalidate',
          }],
          'pragma': [{
            key: 'Pragma',
            value: 'no-cache',
          }],
        },
      };
    }
  }
  return response;
}

実装上のポイントをいくつか挙げておきます。

  • CloudFrontRequestCloudFrontRequestEvent の型定義のために @types/aws-lambdadevDependencies に追加

  • authenticator._getTokensFromCookie で JWTトークンを取得

  • authenticator._jwtVerifier.verify で JWTトークンからユーザー情報を取得

  • user['cognito:groups'] から所属しているグループを取得

  • 必要なグループに属していなければ、302 リダイレクトする

  • (サブ)ドメインごとにクッキーが生成されてしまうので cookieDomain には共通の(上位の)ドメインを指定しておく

  • デフォルトでは、リクエストパスごとにクッキーが生成されてしまうので、cookiePath / で固定しておく

おわりに

Cognito@Edge でグループをチェックするのはどうなのかと思いましたが、参考リンクを見る限り、どうやら事例もありそうなので、しばらく運用してみようと思います。
ではでは。

参考リンク