見出し画像

Amazon Cognito を使って複数アプリケーションでのログイン/ログアウトを考える

Solution Architect の t_maru です。

今回は Amazon Cognito を使用して、別々のアプリケーションに同じユーザー情報を使ってログインする方法と global sign out を実施したときのアプリケーション挙動についてご紹介します。

※ 正式名称は Amazon Cognito ですが、以降 Cognito と表記させていただきます。

※ 本記事の内容は以下の CTC クラウド関連コラムにも掲載される予定の内容と同等です。
https://www.ctc-g.co.jp/solutions/cloud/column/


Cognito とは?

Cognito とは Web/Mobile アプリケーションを作る際の認証/認可のために使用することができるサービスです。

ユーザーによるサインアップ機能や外部の ID プロバイダーなどと連携したユーザー認証、SAML、OpenID Connect、ソーシャル ID プロバイダー (Amazon、 Apple、 Facebook、 Google、 X など) による認証など、ユーザー認証/認可に必要な機能が一通り揃っており、インフラのスケールも AWS 管理のため運用負荷を最小限に抑えて認証機能を Web/Mobile アプリケーションに組み込むことができます。

念の為、認証と認可についての補足も記載して起きます。

  • 認証: ユーザーの身元を確認することを意味する

  • 認可: ユーザーへリソースにアクセスするための権限を与えることを意味する

今回は Cognito の機能のうち、User pool を使った認証の機能について取り上げますが、詳細な説明については下記の公式ドキュメントを参照ください。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools.html

1 つの User pool を使用して複数のアプリケーションにログイン機能を実装する

まずそれぞれのアプリケーションでログインできるようにする

今回は `User pool は 1 つで、複数のアプリケーションに同じユーザー情報を使用してログインする` 方法について取り上げます。

コンシューマー向けアプリケーションの場合、最近ではソーシャル ID プロバイダーを利用して認証するものも一般的になっているので上記のようなケースはそれほど多くはないと思いますが、外部の ID プロバイダーを使用できない環境下で、複数のアプリケーションを運用する必要がある場合には Cognito の User pool を使って実現することもできます。

以降で CDK (TypeScript 版) を使って必要なリソースを定義する方法を紹介します。
(リソース定義で必要な部分のみ抜き出して記載しています)

まずは User pool の作成です。

今回はログインする処理だけを作ることにしますので Identity pool の設定 (認可に相当する設定) は不要になりますが、ログインしたユーザーで S3 など AWS リソースにアクセスが必要な場合には Identity pool も設定してください。

const userPool = new UserPool(this, 'UserPool', {
  userPoolName: 'multi-app-test-pool',
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});

CDK を使っている場合、デフォルトの設定が存在するため userPoolName, removalPolicy どちらも設定せずとも使用することはできます。

今回は User pool を見つけやすくするためと、検証終了後に User pool を残す必要がないので上記のような設定にしておりますが、これ以外にもカスタマイズ可能な多くの設定が存在しますので、必要に応じて設定を追加していただければと思います。

次はこの User pool を使用して Web アプリからログインできるように App client を追加しますが、この App client は使用するアプリ毎に作成します。

App client の詳細については下記の公式ドキュメントを参照ください。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-settings-client-apps.html

userPool.addClient('FirstAppClient', {
  userPoolClientName: 'FirstAppClient',
  authFlows: {
    userSrp: true,
  },
});

userPool.addClient('SecondAppClient', {
  userPoolClientName: 'SecondAppClient',
  authFlows: {
    userSrp: true,
  },
});

今回は 2 つのアプリケーションを使用しますので、2 つの client を作成しました。

これらをデプロイすると、userPoolId、userPoolClientId などは具体的な ID が割り当てられるのでそれらを使って Web アプリ側で認証機能の設定を行います。
今回は Amplify v6 の設定の例で見てみます。

interface AuthConfig {
  Cognito: {
    userPoolClientId: string;
    userPoolId: string;
  };
}

type AmplifyConfig = {
  Auth: AuthConfig;
};

const amplifyConfig: AmplifyConfig = {
  Auth: {
    Cognito: {
      userPoolId: 'デプロイされたリソースから取得',
      userPoolClientId: 'デプロイされたリソースから取得',
    },
  },
};

Amplify.configure(amplifyConfig);

AuthConfig は下記を参考に定義していますので、より詳しい情報は Amplify のドキュメントを参照ください。
https://docs.amplify.aws/gen1/javascript/tools/libraries/configure-categories/

上記の設定をし、Amplify の auth module が提供している `signIn` method を呼び出すことで Cognito に登録したユーザーでアプリケーションにログインすることができます。

認証情報入力の回数を減らす

ここまでで、それぞれのアプリケーションで username と password を入力して signIn 処理を実施すればログインできますが、同じユーザー情報を使ってログインする必要のある別のアプリケーションの場合、 username と password の入力の回数 (現在は FirstApp で 1 回、SecondApp で 1 回の計 2 回) を減らすことができるとユーザーとしても使い勝手が良くなりそうです。

上記を実現するために以下の変更を加えます。

  • ログイン画面を Cognito の Hosted UI に変更して各 Web app では username, password の入力をしないように変更

  • 各 Web app では `signIn` ではなく `signInWithRedirect` を使用して Cognito の Hosted UI にリダイレクトして認証するように変更

Cognito の Hosted UI を使用するに当たり User pool に domain を追加します。

userPool.addDomain('CognitoDomain', {
  cognitoDomain: {
    domainPrefix: 'multi-app-test-domain',
  },
});

さらに、各 Web app 用の UserPoolClient に OAuth の設定を追加します。

import { OAuthScope } from 'aws-cdk-lib/aws-cognito';

// ...

userPool.addClient('FirstAppClient', {
  userPoolClientName: 'FirstAppClient',
  authFlows: {
    userSrp: true,
  },
  oAuth: {
    callbackUrls: ['http://localhost:3001'],
    logoutUrls: ['http://localhost:3001'],
    flows: { authorizationCodeGrant: true, implicitCodeGrant: false },
    scopes: [OAuthScope.EMAIL, OAuthScope.OPENID, OAuthScope.PHONE, OAuthScope.COGNITO_ADMIN],
  }
});

userPool.addClient('SecondAppClient', {
  userPoolClientName: 'SecondAppClient',
  authFlows: {
    userSrp: true,
  },
  oAuth: {
    callbackUrls: ['http://localhost:3002'],
    logoutUrls: ['http://localhost:3002'],
    flows: { authorizationCodeGrant: true, implicitCodeGrant: false },
    scopes: [OAuthScope.EMAIL, OAuthScope.OPENID, OAuthScope.PHONE, OAuthScope.COGNITO_ADMIN],
  }
});

CDK の変更はここまでです。

次は Web app の Amplify の設定を見直します。

type OAuthScope = 'openid' | 'email' | 'phone' | 'profile' | 'aws.cognito.signin.user.admin';

// loginWith 以下が追加
interface AuthConfig {
  Cognito: {
    userPoolClientId: string;
    userPoolId: string;
    loginWith: {
      oauth: {
        domain: string;
        scopes: OAuthScope[];
        redirectSignIn: string[];
        redirectSignOut: string[];
        responseType: 'code' | 'token';
      };
    };
  };
}

// 変更なし
type AmplifyConfig = {
  Auth: AuthConfig;
};

// loginWith 以下の設定を追加
// FirstApp の設定例
const amplifyConfig: AmplifyConfig = {
  Auth: {
    Cognito: {
      userPoolId: 'デプロイされたリソースから取得',
      userPoolClientId: 'デプロイされたリソースから取得',
      loginWith: {
        oauth: {
          // https:// 部分は不要
          // <cognito-domain で設定した prefix>.auth.<region>.amazoncognito.com の形式で記載
          domain: 'デプロイされたリソースから取得',
          scopes: ['openid', 'email', 'phone', 'aws.cognito.signin.user.admin'],
          redirectSignIn: ['http://localhost:3001'],
          redirectSignOut: ['http://localhost:3001'],
          responseType: 'code',
        },
      },
    },
  },
};

Amplify.configure(amplifyConfig);

今回は Web app を起動している server が local PC 上の開発サーバーを想定して localhost の URL を記載していますが、実際には Web app が配信されている server の URL を設定します。

同様の Amplify 設定を 2 つの Web app で行い、FirstApp で Cognito の Hosted UI に遷移して username と password を使ってログインを行います。。

その後、SecondApp を開き signInWithRedirect を呼び出すボタンを押すと Cognito の Hosted UI に遷移したあと既に認証済みの扱いとなり、すぐに redirectSignIn の項目で設定した URL にリダイレクトが行われます。

リダイレクトされたあとの SecondApp では Cognito のユーザー情報が Amplify の API (getCurrentUser など) を使用して取得可能になっているため、ログインを完了している状態として扱うことができます。

今回は SecondApp 側でもサインインのボタンを設置し、それを押すことで認証処理が実行されるようにしましたが、特定の URL や特定のパラメータが付与されている場合は自動的に認証処理を実行するように実装を行うことで、より自然な画面遷移を実現できるのではないかと思います。

サインアウト時の挙動の注意

サインアウト時の挙動が Web app のログインしているユーザーを確認する機能の実装に影響を与えることがありますので、注意事項として共有しておきます。

Amplify の API を見ると `signOut` という method には `{ global: boolean }` というオプションが存在し、これを true で指定したうえで signOut を呼び出すと、同じ User pool を使用している他のアプリケーションからもログアウトすることができます。

以下、sign out に関する Amplify の公式ドキュメントです。
https://docs.amplify.aws/javascript/build-a-backend/auth/connect-your-frontend/sign-out/

ドキュメントを確認すると、global sign-out が呼び出されると refresh token が無効になるため access token、 id token の更新時にエラーが出るようになりますが、access token と id token の有効期限は token 発行後 1 時間であるため、最長で 1 時間は他のアプリケーションでログインセッションが継続される可能性があります。

access token や id token の期限が切れるまではそのまま動作しても問題なければ特段対策を講じる必要はないのですが、他の端末、他のアプリケーションでログアウトが行われた際、今操作しているアプリケーションでも即座にログアウトさせたい場合には、使用している token (access token か id token) が有効なのかをチェックする必要があります。

Amplify には token の有効性を確認するためだけの用途の関数は用意されていないため、以下のような方法を検討する必要がありそうです。

  • token の有効性を確認するためのライブラリを使用する

  • token を使用する API を call して API 側で token の有効性をチェックしてもらう

`token の有効性を確認するためのライブラリを使用する` については、Cognito のドキュメントに JWT の検証のための方法とライブラリが紹介されているので参考にしていただければと思います。

https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html#amazon-cognito-user-pools-using-tokens-aws-jwt-verify

2 つ目の方法として紹介した `token を使用する API を call して API 側で token の有効性をチェックしてもらう` という方法ですが、今回はちょうど Cognito に登録された User に設定されている属性情報を取得する必要があったのでこちらを活用してみました。

Amplify の auth から提供されている `fetchUserAttributes` がこれに該当するのですが、この関数を呼び出すと cognito-idp の endpoint に通信が行われ、access token が無効だと `NotAuthorizedException: Access Token has been revoked` というエラーが返却されます。

これを活用して以下のように処理を作成してみました。

import { signOut, getCurrentUser, fetchUserAttributes } from 'aws-amplify/auth';

interface UserInfo {
  userId: string;
  username: string;
  locale: string;
}

const findUserInfo = async (): Promise<UserInfo | null> => {
  try {
    const { userId, username } = await getCurrentUser();
    const { locale } = await fetchUserAttributes();

    const userInfo: UserInfo = {
      userId,
      username,
      locale: locale ?? 'none',
    };

    return userInfo;
  } catch (e) {
    console.warn(`findUserInfo error: `, e);


    // 他のアプリケーションでサインアウトしている場合はこのエラー処理にたどり着くので この app からもサインアウトする
    // 本来であれば Error の詳細を確認して `NotAuthorizedException: Access Token has been revoked`
    // だった場合にのみこの処理を呼び出すほうがよい
    await signOut();

    return null;
  }
};

こちらの処理をアプリケーション起動時や何かしらのユーザー操作に紐づけて実行されるように実装することで、他のアプリケーションでログアウト処理が行われた場合に即座にログアウトするという挙動を実現することができるようになります。

※ 今回の実装では、開いているアプリケーションが自動的にログアウトする挙動ではなく、ユーザーによる何らかの処理をトリガーに上記の判定処理を回してログアウトする処理になります。ユーザー操作なしに自動的にログアウトさせる必要がある場合はもう少し工夫が必要になります。

まとめ

Cognito の User pool を活用して、複数のアプリケーションでログインに必要な手間を削減する方法と、複数のアプリケーションから同時にサインアウトする方法についてご紹介しました。

要件によっては Cognito を利用できないケースなども存在するかもしれませんが、Cognito が提供している機能と同等の仕組みを自前で用意しようと思うと多大な工数が必要となると思いますので、Web/Mobile アプリの開発を検討中で Cognito の利用に対して何らかの制約 (社内のルールやネットワーク的にそもそも Cognito の API と通信できないなど) がない場合には積極的に活用を検討すると良いのではないかと思います。