ちょっと話題の記事

Amazon Cognito と仲良くなるために歴史と機能を整理したし、 Cognito User Pools と API Gateway の連携も試した

2017.11.20

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

最近はサーバーレスアーキテクチャに触れることが増えています。その中で、ついて回ることが多い機能が 認証 です。どのようなシステムを構築するかによって認証方式も変わってくるかと思いますが、方法のひとつとして 自社でもっている認証基盤に対して、ユーザーIDとパスワードを使ってログインしてもらう という方式があります。AWS においては Amazon Cognito がその要件をかなえる筆頭サービスです。ただ、私自身、Cognito がどのような機能を提供しているのか、実際の利用例としてどんなものがあるか、把握できていないことも多かったので、整理した上で自分でも実際に試してみることにしました。

Cognito を時系列で振り返る

まずは Cognito が発表されてから現在に至るまで、どのような機能が追加されてきたのか振り返ってみます。なお、以下に記載する内容は、Document History for Amazon Cognito - Amazon Cognito から抜粋し、公式ドキュメントおよび 関連する Developers.IO の記事を記載したものです。

日付 この時点でできること 関連ブログ
2014年7月10日 Amazon Cognito 一般利用開始
Cognito フェデレーテッドアイデンティティ
Cognito Sync
[AWS][iOS] Amazon Cognito のモバイルユーザー認証 & データ同期 を iOS で使ってみた
Amazon Cognitoによる認証はSTSのweb identity federationとどう違うのか!?
2015年4月30日 Cognito フェデレーテッドアイデンティティ において
Twitter/Digits 認証をサポート
【新機能】Amazon CognitoにTwitter認証、Digits認証が増えました
Amazon Cognito の Twitter 連係を iOS アプリから使ってみた
2016年7月28日 新規機能追加
Cognito User Pools 一般利用開始
[新機能] Amazon Cognito に待望のユーザー認証基盤「User Pools」が追加されました!
AWS SDK for JavaScriptを使ってブラウザーからCognito User Poolsへサインアップしてみた
2016年12月15日 新規機能追加
・ユーザーグループとロールベースのアクセスコントロール機能
新機能 – Amazon Cognito グループ、およびきめ細かなロールベースのアクセス制御 - Amazon Web Services ブログ

個人的には、Cognito User Pools が利用できるようになったことで、認証周りが AWS で完結できるようになり、とりわけモバイルアプリケーションの領域で利用が加速した印象です。認証基盤は、準備と実装が大変である一方、システム構築において高確率でセットになる要件なので、AWS から提供されたのは嬉しいですね。

機能別

Cognito が持つ能力を機能別に分けます。公式ドキュメントの内容をより簡単にまとめたものです。

機能名 できること
Amazon Cognito フェデレーテッドアイデンティティ 外部のIDプロバイダーで認証し、AWSでの一時的なID(STS)を作成できる。このIDを利用して、Cognito Sync とデータを同期したり、他のAWSサービスにアクセスできる。
Amazon Cognito Sync AWS SDK を利用することで、バックエンドを用意することなく任意の Key-Value データを複数デバイスで同期・編集できる。
Amazon Cognito ユーザープール - Amazon Cognito ユーザーディレクトリを作成、管理し、モバイルアプリやウェブアプリケーションにサインアップとサインインを追加できる。

Web アプリケーションから使ってみたブログ

ちょっと別の軸で見ていきましょう。「じゃあ、Cognitoって実際のところどうやって使うのか?」をブログにしたものです。

試している Cognito の機能 ブログ 内容の説明
・Cognito User Pools
・Cognito フェデレーテッドアイデンティティ
・DynamoDB との連携
CognitoUserPoolsを使うAngular2 SPAのサンプルを動かす サンプルアプリケーションは、Cognito User Pools でユーザー登録、DynamoDB へアクセスするための一時IDの発行のために フェデレーテッドアイデンティティ、そしてログイン履歴の管理として DynamoDB を利用しているようです。
・Cognito User Pools AWS SDK for JavaScriptでCognito User Poolsを使ったログイン画面を作ってみた Cognito User Pools は SDK を利用してユーザー認証することにより、トークンが手に入ります。この記事では、トークンを利用したクライアントアプリケーションでのセッションの取り回しについて解説しています。
・Cognito User Pools
・API Gateway との連携
本記事 Cognito User Pools から発行されるトークンは、API Gateway に対する認可情報としても使えます。本記事では主に Cognito User Pools と API Gateway との連携を試しています。

API Gateway のアクセス制限に関するブログ

API Gateway の話がでてきました。実は、API Gateway にアクセス制限をかける方法はいくつかあり、Cognito User Pools の認証トークンを利用することはその一つです。

API Gateway のアクセス制限と許可の方法 ブログ
API をコールする際、固定の「APIキー」をヘッダに求める Amazon API GatewayでAPIキー認証を設定する
・APIをコールする際、APIへのアクセスが許可されている IAM ロールを求める
・Cognito Identity pool にて、匿名ユーザーでの認証を受けた場合、APIへのアクセスが許可された IAM ロールを付与する
Amazon API Gateway の API を Cognito で認証して呼び出す
・APIをコールする際、任意のヘッダを検証する Lambda Function を通す Amazon API Gateway で Custom Authorization を使ってクライアントの認可を行う
・APIをコールする際、Aurhorization ヘッダにて Cognito User Pools の ID Token を求める
・Cognito User pool にて、ログインに成功したユーザーへ ID Token を渡す
本記事

Angular SPA でログインし、API Gateway を呼び出す

様々な軸で整理しました。本記事の位置づけとしては、SPAから Cognito User Pools へログインし、払い出されたトークンを使って API Gateway の API をコールする ということになります。API Gateway は DynamoDB につながっており、API経由で Query を発行して DynamoDB のデータを取得できることが目標です。以下の順で進めます。

  1. 認可不要の API を定義し、SPAからデータを取得してみる
  2. Cognito User Pools を作成し、新しいユーザーを用意する
  3. Cognito User Pools から払い出された IDトークンが必要になるよう API Gateway に制限をかける
  4. SPA にサインイン機能を追加する
  5. SPA から制限付きAPIをコールする

認可不要のAPIを使ってDynamoDBのデータを取得する

こちらのブログの API Gateway と DynamoDB をそのまま使います。

この状態は、API に制限がかかっていません。API Gateway でデプロイすると、GETリクエストを送ることでデータがそのまま取得できます。

apigateway_no_auth.png

Cognito User Pools を作成し、新しいユーザーを用意する

API Gateway に制限をかけていきたいところですが、あらかじめ Cognito User Pools を作成しておく必要があります。というわけで Cognito の画面にいきましょう。今回は 「ユーザープールの管理」へ行きます。

名前と属性を設定し、あとはデフォルト値のままでOKですので、User Pools を作成します。

userpool_name.png

userpool_attributes.png

User Pools が作成されたら、ユーザーを作成します。

userpool_new_user.png

管理者によりユーザーを作成すると、ステータスは FORCE_CHANGE_PASSWORD になるようです。これは後ほど、クライアントアプリケーションから CONFIRMED に変えてやる(仮パスワードから本パスワードに変更してもらう)必要があります。

ひとまずこれで Cognito User Pools の準備は完了です。

API Gateway に制限をかける

2つ設定します。

設定したいAPIのメニューで、「オーソライザー>新しいオーソライザーの作成」とします。以下のように、Cognito User Pools を使うようにします。

apigateway_authorizer.png

これで、API Gateway に新しいオーソライザーが追加されました。あとはメソッドリクエストで使うよう設定します。「Cognito ユーザープールオーソライザー」が設定できるようになっていますので、これを設定すればOKです。注意点として、CORS用の OPTIONS メソッドには認証をかけないようにしてください。

apigateway_method_auth.png

デプロイし、この状態でリクエストを送ってみましょう。

request_block.png

"Unauthorized" が返ってきていることがわかります。ヘッダーに何も設定しないと、リクエストが拒否されるようになりました。では、このAPIに対して認証情報を付与し、レスポンスを返してもらうようにしていきます。

SPA にサインイン機能を追加する

API Gateway の Authorization ヘッダーに設定するのは Cognito User Pools から払い出される IDトークン です。トークンについて整理しておきましょう。サインインすると以下3つのトークンが手に入ります。

トークン名 種類 想定用途
IDトークン JWT Cognito User Pools の ユーザー属性(例えばメールアドレスなど)も含めたトークン。認可時、ユーザーに関する情報をフルで取得したい場合はこちらを使う。API Gateway はこちらを採用。
アクセストークン JWT Cognito User Pools の 最低限のユーザー情報を含めたトークン。認可時、必要なのがユーザー名程度であればこちらを採用する。
リフレッシュトークン 文字列 IDトークンおよびアクセストークンを更新するために利用する。Cognito User Pools のクライアントSDKを利用している場合は、自動で更新されるため、特にこのトークンをアプリケーションから意識して使うことはない。

余談ですが、API Gateway のアクセス制限方法として「Lambda Function を通す」というものもありました。Lambda Function で JWTトークンをデコードし、例えばある特定の属性を持つユーザーだけがAPIリクエストを許可する、といった芸当も可能です。今回はJWTトークンをそのまま Cognito へ渡す形で認可に使っているのみですがJWTトークンであるメリットはこのような点にもありますね。

Angular でのサインインコードを載せておきます。

environment.ts

export const environment = {
  production: false,
  region: 'ap-northeast-1',
  userPoolId: 'ap-northeast-1_ABCDEFG10', // User Pools の画面から取得できる User Pools そのもののID。
  clientId: 'XXXxxxXXXX'  // User Pools で発行したクライアントアプリケーションのID。
};

cognito.service.ts

import {Injectable} from '@angular/core';
import * as AWS from 'aws-sdk';
import {AuthenticationDetails, CognitoUser, CognitoUserPool} from 'amazon-cognito-identity-js';
import {environment} from '../environments/environment';

@Injectable()
export class CognitoService {

  public userPool = null;

  constructor() {
    AWS.config.region = environment.region;
    const data = {UserPoolId: environment.userPoolId, ClientId: environment.clientId};
    this.userPool = new CognitoUserPool(data);
  }

  signIn(username, password, callBack) {
    const userData = {
      Username: username,
      Pool: this.userPool,
      Storage: localStorage
    };
    const cognitoUser = new CognitoUser(userData);
    const authenticationData = {
      Username: username,
      Password: password,
    };
    const authenticationDetails = new AuthenticationDetails(authenticationData);
    cognitoUser.authenticateUser(authenticationDetails, {
      onSuccess: function (result) {
        alert('SignIn is success!');
        console.log('id token + ' + result.getIdToken().getJwtToken());
        console.log('access token + ' + result.getAccessToken().getJwtToken());
        console.log('refresh token + ' + result.getRefreshToken().getToken());
        if (callBack) {
          callBack();
        }
      },
      onFailure: function (err) {
        alert(err);
      },
      newPasswordRequired: function (userAttributes, requiredAttributes) {
        // 本当は初回ログイン時、仮パスワードの変更画面とその処理が必要になるのですが、今回はハードコートで割愛しています。
        delete userAttributes.email_verified;
        cognitoUser.completeNewPasswordChallenge('12345678', userAttributes, this);
      }
    });
    return;
  }

  signOut() {
    console.log('sign out!');
    const currentUser = this.userPool.getCurrentUser();
    if (currentUser) {
      currentUser.signOut();
    }
  }
}

signIn()における大まかな流れは、

  • 呼び出し元からユーザー名とパスワードを受け取る
  • それらをもとに AWS SDK の関数を呼び出す

というものです。結果、localStorage に先程の3種トークンが保存され、これまたSDKの関数を呼び出すことで、ストレージに保存されたトークンを取得することができます。このサービスを利用するよう実装したログインコンポーネントがこちら。

login.component.ts

login() {
  this.loading = true;
  const gotoChart = (router: Router, cognitoService: CognitoService) => {
    let timerID = setInterval(() => {
      console.log(cognitoService.userPool.getCurrentUser());
      if (cognitoService.userPool.getCurrentUser()) {
        // wait終了時の後処理
        router.navigate(['/chart']);
        clearInterval(timerID);
        timerID = null;
        this.loading = false;
      }
    }, 1000);
  };
  this.cognitoService.signIn(this.model.username, this.model.password, gotoChart.bind(null, this.router, this.cognitoService));
}

ログインに成功したら chart のパスへ遷移します。ログインしてみましょう。

login.png

console.logで3種のトークンを出力しています。その値を見て下さい。(※画像のトークンはダミー値です)

tokens.png

idトークンを使って、 Postman に Authorization ヘッダーを設定した上でリクエストしてみます。

postman_with_auth.png

レスポンスが返ってくることを確認できました。これと同じように、SPA側でも、データ取得リクエストを送る際に Authorization ヘッダーを設定してやればよさそうです。

SPA から制限付き API をコールする

CognitoService に IdToken を取得する処理を追加し、それを API コール時に呼び出すようにします。

cognito.service.ts

getCurrentUserIdToken(): Promise<string> {
  return new Promise((resolve, reject) => {
    this.userPool.getCurrentUser().getSession((err, session) => {
      if (err) {
        reject(err);
      } else {
        resolve(session.getIdToken().getJwtToken());
      }
    });
  });
}

vote-data.service.ts

getVotes(candidateId: string): Promise<Vote[]> {
  const request = (validJwtToken) => {
    const h = this.authHeader(validJwtToken);
    console.log(h);
    return this.http.get(this.voteUrl(candidateId), {headers: h})
      .toPromise()
      .then(response => response.json().votes as Vote[])
      .catch(this.handleError);
  };
  return this.cognito.getCurrentUserIdToken().then(jwtToken => request(jwtToken));
}

結果、 Cognito User Pools と連携した API を呼び出すことができました。

vote_chart

まとめ

Cognito の機能を時系列で整理し、実際に Cognito User Pools と API Gateway の連携を試しました。Cognito と AWS との連携は、以下のパターンがありそうです。

  • Cognito フェデレーテッドアイデンティティにより STS を発行し、そのSTSが持つIAMポリシーを使って他のAWSサービスを利用する
  • Cognito User Pools にサインインすることで取得できるトークンを使ってAWSサービスにアクセスする(API Gateway はこれ)
  • Cognito User Pools にサインインすることで取得できるトークンを使ってAWSサービスにアクセスするようサーバーサイドを実装し、サーバーサイドアプリケーションへ渡す認証情報としてトークンを使う

Cognito に関する調べ物をしていると、かなりいろいろなことができる印象ですが、実は持っている機能自体はとてもシンプルです。私の場合は、他のAWSサービスと連携するときに、「これはCognitoの機能」「ここからは連携先のAWSサービスの機能」というように明確に線引きをすることで理解が進みました。当初このあたりを一緒くたにしてしまっていて苦労しました。

Cognito により独自の認証基盤を用意することなく、ユーザー情報を管理できることはサーバーレスアーキテクチャでの大きなメリットです。今後、他のIDプロバイダーとの連携なども試していきたいと思います。

バージョン情報

利用ツール・ライブラリ バージョン
@angular/core 4.2.4
amazon-cognito-identity-js 1.25.0

参考