経緯
前回の記事でCognitoユーザーがAWSマネジメントコンソールにログインできるようにしましたが、ログイン画面さえも自作しないでできることが分かったので書き残しておきます。
具体的にはCognitoユーザープールのホストされたUIを使います。
ホストされたUIでは不満がありログイン画面のデザインや動きを自作したい場合は前回の記事のパターンやウェブアプリを自作すると良いでしょう。
前提条件
- ユーザーにAWSマネジメントコンソールを使わせたい
- ユーザーはCLIでなく、ブラウザ(ログイン画面)でログインさせたい
- 事情があってAWS SSOに連携するIdPを使えない、使わない
- ログイン画面のためだけにウェブアプリを作りたくない
- サーバーの運用はなるべくしたくない
- IAMユーザーは発行したくない
- 運用チームがCognitoユーザープールでメンテナンスできる程度のユーザー規模
おおまかな流れ
- CognitoユーザープールとIDプールを作る
- マネジメントコンソールへサインインするためのLambdaとAPI Gatewayを作る
CognitoユーザープールとIDプールを作る
こちらの記事を参考にして、CognitoユーザープールとIDプールを作ります。
私の場合、CognitoユーザープールのサインインはEメールのみとしました。こうすると1つのメールアドレスに紐づけられるIAMロールは1種類のみになりますが、1人の人に複数種類のIAMロールを持たせたい時は複数のメールアドレスを使い分けてもらうつもりです。(例えばyamada.taro+developer@example.comとyamada.taro+sales@example.comのように)
Cognitoユーザープールのアプリケーションクライアントも作成します。認証フローはデフォルトでチェックされているALLOW_REFRESH_TOKEN_AUTHのみで良いです。
ホストされたUIのIDプロバイダーはCognitoユーザープール、OAuth付与タイプは認証コード付与、OpenID Connectスコープはopenidのみとします。
コールバックURLには、後で作るREST APIのURLを設定しますがいったん適当なダミー(https://www.example.comとか)を入力しておきます。
IDプールの認証プロバイダーにCognitoを選択し、先ほど作成したユーザープールのユーザープールIDとアプリクライアントIDを設定します。
認証されたロールの選択はトークンからロールを選択するとしておきます。
ユーザープールの側でグループを作成し、それぞれのグループに別々のIAMロールを付けておきます。
(例えばdeveloperグループとsalesグループのように)
グループに付けるIAMロールは、IDプールを作成する時にデフォルトで作成された「認証されたロール」Cognito_xxxxAuth_Roleをコピーして必要な権限を追加するのがおすすめです。
一から自分で作ろうとすると、必要な権限が足りなくてログインができないなどで慌てることになります。私はそうなりました。
ユーザープールに登録されたユーザーをグループに追加しておきます。
例えばyamada.taro+developer@example.comをdeveloperグループに、yamada.taro+sales@example.comをsalesグループに追加します)
こうしておくとyamada.taro+developer@example.comでログインした時は開発者権限でS3バケットにアクセスでき、yamada.taro+sales@example.comでログインした時は営業権限でコストエクスプローラーにアクセスできるというような制御ができます。
サービスのユーザーが増えるたびに、そのユーザーに権限を割り当てる(適切なグループに追加する)作業が発生します。
LambdaとAPI Gatewayを作る
ここでCognitoユーザープールのホストされたUIからコールバックされるREST APIを作ります。
Lambdaを作る
まずはLambdaを作ります。
参考にしたのはこちらです。
Lambda > 関数を作成 > Node.js 16.x, arm64 を選びました。
このREST APIに対して認証コードcodeのパラメータが送られてくる前提です。
以下のコード内のregion, userPoolId, clientId, identityPoolId, tokenEndpointは利用するリージョン、CognitoユーザープールID、アプリクライアントID、IDプールのID、アプリクライアントのドメインに置き換えてください。
redirectUriはこの後API Gatewayを作ると生成されるREST APIのURLを設定するので、一旦ダミーで適当な値を入れておきます。
リクエストを発行するたびにネストが深くなっていてダサいですが時間のある方はFutureで書き直すなどしてみてください。
const AWS = require('aws-sdk');
const https = require('https');
exports.handler = (event, context, callback) => {
    console.log(event);
    const authorizationCode = event.params.code;
    const region = 'ap-northeast-1';
    const userPoolId = 'ap-northeast-1_XXXXXX';
    const clientId = 'XXXXXXXXXXXXXXXX';
    const identityPoolId = 'ap-northeast-1:XXXXXXXXXXXXXXXXXX';
    const tokenEndpoint = 'https://XXXXXXXXXXXX.auth.ap-northeast-1.amazoncognito.com/oauth2/token';
    const redirectUri = 'https://XXXXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/XXXX/XXXXXXXX';
    const tokenParameters = new URLSearchParams({
        grant_type: 'authorization_code',
        client_id: clientId,
        scope: 'openid',
        redirect_uri: redirectUri,
        code: authorizationCode
    }).toString();
    const options = {
        method: 'POST',
        cache: 'no-cache',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        }
    };
    const request = https.request(tokenEndpoint, options, (response) => {
        console.log(`/oauth2/token: ${response.statusCode}, ${response.statusMessage}`);
        response.on('data', (data) => {
            const parsedData = JSON.parse(data);
            const logins = {};
            logins[`cognito-idp.${region}.amazonaws.com/${userPoolId}`] = parsedData.id_token;
            AWS.config.credentials = new AWS.CognitoIdentityCredentials({
                region: region,
                IdentityPoolId: identityPoolId,
                Logins: logins
            });
        
            AWS.config.credentials.get((err) => {
                console.log('getCognitoCredentials: err=');
                console.log(err);
                const sessionId = AWS.config.credentials.data.Credentials.AccessKeyId;
                const sessionKey = AWS.config.credentials.data.Credentials.SecretKey;
                const sessionToken = AWS.config.credentials.data.Credentials.SessionToken;
                const sessionJson = encodeURIComponent(`{"sessionId":"${sessionId}","sessionKey":"${sessionKey}","sessionToken":"${sessionToken}"}`);
                const getSigninTokenUrl = `https://signin.aws.amazon.com/federation?Action=getSigninToken&SessionType=json&Session=${sessionJson}`;
                https.get(getSigninTokenUrl, (response) => {
                    console.log(`getSigninToken: status=${response.statusCode}, ${response.statusMessage}`);
                    
                    response.on('data', (data) => {
                        const parsedData = JSON.parse(data);
                        const signinToken = parsedData.SigninToken;
                        const issuer = encodeURIComponent('https://example.com');
                        const destination = encodeURIComponent('https://console.aws.amazon.com');
                        const signinUrl = `https://signin.aws.amazon.com/federation?Action=login&Issuer=${issuer}&Destination=${destination}&SigninToken=${signinToken}`;
        
                        context.succeed({"Location": signinUrl});
                    });
                }).on('error', (err) => {
                    console.log('getSigninToken on error: err=');
                    console.log(err);
                });
            });
        });
    });
    request.write(tokenParameters);
    request.end();
};
API Gatewayを作る
次にこちらを参考にして、API GatewayがLambdaで返したサインインURLにリダイレクトできるように設定します。
API Gateway + Lambdaで任意のURLにリダイレクトする方法
うちのサービスを使うユーザーは社員のみなので、こちらを参考にして会社のプロキシサーバーのIPアドレス以外からREST APIにアクセスできないように設定しておきます。
特定の IP アドレスのみが API Gateway REST API にアクセスすることを許可するにはどうすればよいですか?
アプリクライアントのコールバックURLとLambda関数のredirectUriを書き直す
API Gatewayを作ってREST APIのURLが生成されたら、先に作っておいたCognitoユーザープールのアプリクライアントのコールバックURLと、Lambda関数のredirectUriに設定します。
使ってみる
アプリクライアントのホストされたUIをブラウザで表示して、ログインしてみましょう。
以上で仕組みはできました。
お疲れ様でした!
