Cognito で Google 認証しつつ、システムごとにユーザーを制限する
はじめに
私がお手伝いしているチームは、1つの大きなシステムがあるわけではなく、小さいシステムが複数ある感じなのですが、各システムで認証機能を実装するのは避けた方がいいかなと考えています。
社内メンバーのみで使うシステムが多い
各システムの管理画面とか
社内のセキュリティ要件に合格しようと思うと大変
パスワード強度の設定
パスワードリセット機能
一定回数パスワードを間違えたときにロックする機能
などなど
そこで考えたのが、Cognito + Google 認証です。
Google Workspace を使っていれば、すでにユーザーベースはあるので、それを利用します。
これで解決したように思えますが、逆に Google Workspace のユーザーであれば、どのアプリにも入れてしまうのは問題です。
今回はこれを Cognito@Edge に少し手を加えることで解決したという内容になります。
Cognito@Edge とは
Lambda@Edge から Cognito のユーザープールにアクセスし、認証済みユーザーのみアクセス可能とします。
使い方は簡単で、以下のように authenticator.handle をそのまま Lambda@Edge の handler として 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;
}
実装上のポイントをいくつか挙げておきます。
CloudFrontRequest、CloudFrontRequestEvent の型定義のために @types/aws-lambda を devDependencies に追加
authenticator._getTokensFromCookie で JWTトークンを取得
authenticator._jwtVerifier.verify で JWTトークンからユーザー情報を取得
user['cognito:groups'] から所属しているグループを取得
必要なグループに属していなければ、302 リダイレクトする
(サブ)ドメインごとにクッキーが生成されてしまうので cookieDomain には共通の(上位の)ドメインを指定しておく
デフォルトでは、リクエストパスごとにクッキーが生成されてしまうので、cookiePath を / で固定しておく
おわりに
Cognito@Edge でグループをチェックするのはどうなのかと思いましたが、参考リンクを見る限り、どうやら事例もありそうなので、しばらく運用してみようと思います。
ではでは。