AWS AmplifyとAngular8を使ってCognitoでAWS Management Consoleにログインするページを作ってみる

今回のブログではブラウザで動かせるCognitoユーザープールのユーザーでAWSマネジメントコンソールにログインするページを作ります。 そのサイトをAWS AmplifyとAngular8で実装する例を紹介します。
2020.05.14

先日、CognitoユーザープールのユーザーAWSのマネジメントコンソールへフェデレーションログインする方法についてのブログを書きました。

そのブログではAWS CLIを使って、フェデレーションログイン用のURLを生成する方法を紹介していました。

今回のブログではブラウザで動かせるCognitoユーザープールのユーザーでAWSマネジメントコンソールにログインするページを作ります。
そのサイトをAWS AmplifyAngular8で実装する例を紹介します。

ゴール

次のことができるサイトを作ることがゴールです。

  • ログイン、ログアウトができる
  • ログインした際に、AWSマネジメントコンソールへフェデレーションログインができる

こちらが完成メージです。

構成概要図

AWS Amplifyでざっくりこんな感じのAWS環境を作ります。

先日書いたAWS CLIを使ってCognitoのユーザーでAWS Management Consoleにログインした時のフローとやっていることはほとんど変わらないです。

変更点は次の2つです。

  • get-idを省略してCognito IDプールからAWSの一時クレデンシャルキーを取得している
  • AWS Federation Endpointからサインイントークンを取得するためにAPI Gatewayを構築している

サインイントークンを取得するためにわざわざAPI Gatewayを構築しているのはCORSの問題です。
AWS Federation EndpointがCORSに対応していないため、JavaScriptで動的にサインイントークンを取得しようとするとうまくいきません。 そのため、別途CORSに対応したAPI Gatewayを構築して、Lambdaを経由してサインイントークンを取得するようにしています。

CORSとは何ぞや?と、いう方は次の弊社ブログが参考になります。

前提条件

Angular CLI(Version 8)のコマンドが使えるようにしておいてください。

$ ng version
Angular CLI: 8.3.26
Node: 12.16.2
OS: darwin x64
Angular: 8.2.14

無ければインストールしてください。

$ npm install -g @angular/cli@v8-lts

Amplify CLIのコマンドが使えるようにしておいてください。

$ amplify -v
4.19.0

無ければインストールしてください。

$ npm install -g @aws-amplify/cli

サンプルプログラム置き場

今回作成したプログラムはGitHubで公開しています。 実際に動かしてみたい方はこちらから git clone してください。

Amplify CLIを使ったAWS環境の構築

まずは git clone して環境一式をGitHubから取ってきます。

$ git clone https://github.com/rednes/amplify-angular-aws-sso-sample.git
$ cd amplify-angular-aws-sso-sample

次に amplify init コマンドを実行してバックエンドのセットアップをします。
環境名(今回はlocalという名前にします)やデフォルトエディタ、使用するAWSプロファイル名を質問に沿って入力していきます。

$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the environment: local
? Choose your default editor: IntelliJ IDEA
? Do you want to use an AWS profile?: Yes
? Please choose the profile you want to use: YourProfile

次に amplify push コマンドを実行して、バックエンドの環境をAWSに構築します。

$ amplify push

Current Environment: local

| Category | Resource name                | Operation | Provider plugin   |
| -------- | ---------------------------- | --------- | ----------------- |
| Auth     | amplifyangularawsssob83ec7be | Create    | awscloudformation |
| Function | GetSigninTokenLambda         | Create    | awscloudformation |
| Api      | GetSigninTokenApi            | Create    | awscloudformation |
? Are you sure you want to continue? (Y/n)

このコマンドを実行することで、認証のCognito部分とAPIとなるAPI Gateway部分とAPIのロジックとなるLambda部分を自動で構築してくれます。

Amplify CLIを使って作ったAWS環境は、Amplify Consoleに登録されています。 マネジメントコンソールからAmplifyのページを開いてみてください。

そうすると、新しく AmplifyAngularAwsSso というアプリケーションができていることが確認できます。

そこでBackend environmentsを確認してみると、 local という名前の環境が登録されており、AWS環境が構築できていることがわかります。

ローカルで動作確認を行う

サーバー側のリソースが構築できたので、次のコマンドを実行してローカルでAngularを動かして動作確認をしてみます。

$ npm i
$ ng serve --open

そうすると、こんな感じのログイン画面が表示されます。 ログインしようにもアカウントが無いので、まずはサインアップします。

次の情報を適当に入力して、アカウントを作ります。 E-mailアドレス宛に検証コードが届くので、必ず自分で受信可能なE-mailアドレスを入力してください。

次の画面でE-mailアドレスに届いた検証コードを入力すれば、サインアップは完了です。

サインアップ後に画面を更新すると、こんな風にAWSマネジメントコンソールへフェデレーションログイン可能なURLが表示されます。

URLをクリックしてマネジメントコンソールが開ければ成功です。

Amplify Consoleを使ったデプロイ

GitHubのリポジトリをforkしてAmplify Consoleの画面でソースコードリポジトリと接続すれば、簡単にCI/CD環境を作ることもできます。

Amplify ConsoleでCI/CD環境構築の詳細は、こちらの弊社ブログをご参照ください。

プログラムの解説

環境は構築できました。ここからはプログラムの中身について解説します。

このシステムで肝となるロジックは、 src/app/app.component.ts に書いています。

まず、 this.amplifyService.authStateChange$ で認証の状態変化を監視し、ログインした時に後続の処理を実行するようにしています。

this.amplifyService.authStateChange$
      .pipe<AuthState, AuthState>(
        filter((authState: AuthState) => authState.user),
        map((authState: AuthState) => {
          this.zone.run(() => {
            this.signedIn = authState.state === 'signedIn';
          });
          this.user = null;

          return authState;
        })
      )

次に this.amplifyService.auth().currentUserCredentials で、ログインユーザーのAWS一時クレデンシャルキーを取得しています。

.pipe<ICredentials>(
        flatMap<AuthState, Observable<ICredentials>>((authState: AuthState): Observable<ICredentials> => {
          this.zone.run(() => {
            this.user = authState.user.username;
          });

          return from<Observable<ICredentials>>(this.amplifyService.auth().currentUserCredentials());
        })
      )

次に this.amplifyService.api().post で、Amplifyで構築したAPIに対して、動的にPOSTリクエストをしてサインイントークンを取得しています。

.pipe<any>(
        flatMap((credentials: ICredentials) => {
          const sessionId = credentials.accessKeyId;
          const sessionKey = credentials.secretAccessKey;
          const sessionToken = credentials.sessionToken;

          const credentialsJson = {
            sessionId,
            sessionKey,
            sessionToken,
          };
          const credentialsEncoded = encodeURIComponent(JSON.stringify(credentialsJson));
          const uri = `https://signin.aws.amazon.com/federation?Action=getSigninToken&SessionType=json&Session=${credentialsEncoded}`;

          const myInit = {
            headers: {
              'Content-Type': 'application/json',
            },
            body: {
              uri,
            },
          };
          return from(this.amplifyService.api().post('GetSigninTokenApi', '/api', myInit));
        })
      )

最後に this.awsLoginUrl = ... の部分でサインイントークンからフェデレーションログイン用のURLを作成して代入しています。

.pipe<void>(
        flatMap((response: any): Observable<void> => {
          const {SigninToken: signinToken} = response;

          this.zone.run(() => {
            this.awsLoginUrl = `https://signin.aws.amazon.com/federation?Action=login&Issuer=${this.urlEncoded}&` +
              `Destination=${this.consoleEncoded}&SigninToken=${signinToken}`;
          });

          return new Observable<void>((observer: Subscriber<void>) => {
            setTimeout(() => observer.next(), 60000);
          });
        })
      )
      .subscribe(():void => {
        document.location.href = this.awsLoginUrl;
      });

実際にコードを追っていくと、書き方がAngular(TypeScript)になっただけで、全体的なフローはAWS CLIでやっていたものと変わっていないことが分かると思います。

終わりに

Amplifyを使うことで簡単にCognitoの環境構築とログイン機能の構築をすることができました。

Cognitoの細かい仕様を知らなくても、Amplifyのライブラリを使用するだけでログイン機能として利用できるので、ハードルがぐっと下がると思います。

このブログがどなたかの実装の参考になれば幸いです。