Amazon Cognitoによる認証はSTSのweb identity federationとどう違うのか!?

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

よく訓練されたアップル信者、都元です。少し前の話になりますが、2014年7月に Amazon Cognito がローンチしました。Cognitoは、モバイルアプリのためのサービスで、弊社ブログでも何度か取り上げています。

Cognitoの機能は、実は「Cognito Sync(複数デバイス間のデータ同期)」と「Cognito Identity(ID管理)」という2つのカテゴリに分類されます。もしかしたら、元々は別のプロダクトだった感もあります。というのも…

APIドキュメントが別

AWS SDK for Javaのクライアント実装が別、Javaパッケージも別

  • com.amazonaws.services.cognitoidentity.AmazonCognitoIdentity
  • com.amazonaws.services.cognitosync.AmazonCognitoSync

そもそもjarファイルも別

ともあれ、モバイルデバイス向けのサービスとして1つのサービスにパッケージングして世に出たAmazon Cognitoであります。ところでこのうち、Cognito Identityの方にはAWS APIの一時キー (temporary credentials *1)を発行する機能があります。要するに、Facebookログインのトークンを、AWSのアクセスキーに変換する機能です。

これを知った時、正直「え、それSTSでできるよ」というもやもやに襲われました。既にできる事をパッケージングを変えて売り出しているだけだろうか、と。しばしばAWSはAPIだと言われますが、同じ機能のAPIを複数用意してしまうのは、それはさすがにDRY的にも気持ち悪い。何故これ(Cognito)を作ったのか? 今まで技術的に出来ないことができるようになったのか? それともリブランディングって話だけなのか? もやもやもやもやもやもやもやもや………

ってしたまま3ヶ月。そろそろ本気になって調べてみました。

調査編

こういう時は、何はともあれドキュメントを精査です。

Cognito IdentityのAPIドキュメント(の1ページ目)を見てみる

Amazon Cognito Identity

本エントリーの結論を理解した後でまたこのドキュメントを見ると、実は全てが書いてあったりするんですが、自分の英語読解力で結論に達するのは少々苦労しました。そんな中まず見つけたのが、一時キーを手に入れる手順がざっと書いてあることです。

  1. まずCognitoに対して(あれば、IdPからのトークンを渡して)「GetId」を呼び出すと、「IdentityID *2」を返す。
  2. 次にCognitoに対してIdentityID(あれば、IdPからのトークンも)を渡して「GetOpenIdToken」を呼び出すと、「OpenID token」を返す。
  3. 最後にSTSに対してOpenID tokenを渡して「AssumeRoleWithWebIdentity」を呼び出すと、「一時キー」を返す。

おっと、一時キーを発行するのはCognitoではなく、あくまでもSTSであるため、APIのDRYは保たれていることが分かりました。

AssumeRoleWithWebIdentityのドキュメントを見てみる

続いてAssumeRoleWithWebIdentityの冒頭を見てみると。要するに AssumeRoleWithWebIdentity が対応するIdP(identity provider)は現時点で以下の5種類だと読み取れます。

  • Amazon Cognito
  • Login with Amazon
  • Facebook
  • Google
  • any OpenID Connect-compatible identity provider

なるほど、ここで「Amazon CognitoというのはIdPなんだ」ということが明確になりましたね。IdPの正確な定義には自信がありませんが、要するにユーザリストを持っていて、それぞれのユーザの認証を行う能力を持つシステム、ということでしょう。

とは言えCognitoにはユーザ登録・削除等のユーザリスト管理の機能は無く、あくまでも別のIdP(FacebookやGoogle)のプロキシをしているに過ぎません。ただ右から左に流すだけのプロキシには存在価値は無いでしょう。Cognitoはどんな機能を付加するプロキシなのでしょうか。

ここで冷静に考えると、Cognitoでは「未認証ゲスト」をサポートしていますね。確かにFacebook等はIdPとして「ユーザ」を個別に認証ますが、未認証ゲストを扱うことはできず、もちろん AssumeRoleWithWebIdentity をコールすることもできませんでした。

Cognito認証のによって出来るようになったこと その1: 未認証ゲストのサポート

ということです。

もう一つ突然閃いたCognitoの大事な機能

これはどこのドキュメントを読んで、という話ではないんですが、色々調べていくうちに急に閃いて認識した大事な機能がありました。

AssumeRoleWithWebIdentityによる認証は前述の通り、GoogleやFacebook等複数のIdPをサポートしています。しかし、AssumeRoleWithWebIdentityによって認証を行うと、IdPを越えた共通のIdentityの認識ができません。すなわち、Facebookのfooさん(foo@fb)とGoogleのbarさん(bar@g)さんは同一人物だったとしても、AWS上では同一人物として扱うことはできません。例えばS3のBucket Policyで、ユーザ毎のディレクトリを切ってアクセス権限を管理していた場合、foo@fbとbar@gの両者からアクセスできるけど、他の人からはアクセスできない、という設定はできないのです。

一方、Cognitoプロキシ(?)経由でFacebook等のIdPを利用すると、複数のWeb IdentityをCognito独自のID体系 *3にマッピングして認証認可に利用できるようになります。コレは画期的ですね!

... Amazon Cognito uniquely identifies a device and supplies the user with a consistent identity over ...

... Cognito delivers a unique identifier for each user and ...

... set the Logins map with the identity provider token. GetId returns a unique identifier for the user ...

oh... ドキュメントで何度も繰り返される unique という単語にコレほどまでに深い意味が隠されていたとは。いや、どっかに明確に書いてのあるかもしれませんが。

Cognito認証のによって出来るようになったこと その2: 複数のIdPによるIDの名寄せ

検証編

さて、だんだん眠くなってきたのでコードでも。事前にIdentity poolは作っておいてください。その上で、未認証ゲストとしてAWSのクレデンシャルを取ってみましょう。

AmazonCognitoIdentity cognito = new AmazonCognitoIdentityClient(new AnonymousAWSCredentials());
Map<String, String> logins = new HashMap<String, String>();

GetIdResult getIdResult = cognito.getId(new GetIdRequest()
  .withAccountId(AWS_ACCOUNT_ID)
  .withIdentityPoolId(IDENTITY_POOL_ID)
  .withLogins(logins));

String identityId = getIdResult.getIdentityId();
System.out.println("identity ID = " + identityId);

GetOpenIdTokenResult getOpenIdTokenResult = cognito.getOpenIdToken(new GetOpenIdTokenRequest()
  .withIdentityId(identityId)
  .withLogins(logins));

String token = getOpenIdTokenResult.getToken();
System.out.println("token = " + token);

AWSSecurityTokenService sts = new AWSSecurityTokenServiceClient(new AnonymousAWSCredentials());
AssumeRoleWithWebIdentityResult assumeRoleWithWebIdentityResult = sts.assumeRoleWithWebIdentity(
  new AssumeRoleWithWebIdentityRequest()
    .withWebIdentityToken(token)
    .withRoleArn(UNAUTH_ROLE)
    .withRoleSessionName("anySessionName")
    .withDurationSeconds(3600));

assert assumeRoleWithWebIdentityResult.getAudience().equals(IDENTITY_POOL_ID);
assert assumeRoleWithWebIdentityResult.getSubjectFromWebIdentityToken().equals(identityId);

System.out.println("Provider = " + assumeRoleWithWebIdentityResult.getProvider());
System.out.println("AssumedRoleUser = " + assumeRoleWithWebIdentityResult.getAssumedRoleUser());
System.out.println("Credentials = " + assumeRoleWithWebIdentityResult.getCredentials());

出力はこんなかんじ。

identity ID = us-east-1:235aeaef-ac8d-4461-972f-8a6bbEXAMPLE
token = eyJhbGciOiJSU...lNK3Z2H-K3i9J_9iqwCEXAMPLE
Provider = cognito-identity.amazonaws.com
AssumedRoleUser = {AssumedRoleId: AROAIWDEXAMPLEEXAMPLE:anySessionName,Arn: arn:aws:sts::000000000000:assumed-role/Cognito_SamplePoolUnauth_DefaultRole/anySessionName}
Credentials = {AccessKeyId: ASIAJEXAMPLEEXAMPLE,SecretAccessKey: ****,SessionToken: ****,Expiration: Fri Oct 31 13:17:38 JST 2014}

未認証ゲスト機能として、各API Clientのコンストラクタには匿名クレデンシャル(AnonymousAWSCredentials)しか渡していないのに、最終的に一時キーが取得できている、というところに注目です。

また、名寄せ機能として、GetIdのところでMapで渡された複数のweb identityを1つにまとめる操作をしているんですね。

まとめ

Cognitoは、恐らくブランディングの観点からモバイルのための機能をパッケージングして見せた、という側面があります。また、従来の「STSのAPIを叩いて明示的にAPI Credentialsを取得する」というわかりづらさに対して、identity pool作成時にサンプルコードを提供し「CognitoCredentialsProviderを使えばOKだよ」というシンプルなSDKを提供しました。CognitoCredentialsProviderはCognitoのAPIを叩くと共に、STSのAPIも叩く実装になっています。

一方、それが故に、CognitoとSTSの境界線がぼやけ、技術面でどんな問題を解決するのかが見えづらくなっていました。少なくとも私個人にとってはw

しかし、技術的な側面としてもCognitoに「未認証ゲスト」と「名寄せ」という大きな存在意義があることが確認できました。また、プリミティブなAPIレベルでは非常に洗練されたAPIがありました。

いやー、当初感じていたもやもやがスッキリしました。

脚注

  1. IAMによるAWS権限管理運用ベストプラクティス (2)参照。
  2. 余談ですが、IDってidentifierの略ですよね…。カオス。
  3. これがIdentityIDですね。