ちょっと話題の記事

[Amazon Cognito] Facebook / Google / Amazon だけじゃない!独自の認証システムも利用可能になりました!

2014.09.30

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

オリジナルの認証システムも利用可能に!

本日、Amazon CognitoDeveloper Authenticated Identities がサポートされました。

今までは Amazon / Facebook / Google しか使えませんでしたが、それ以外のサービスプロパイダも使えるようになりました!既存の認証システムがあれば、認証済みのエンドユーザー(クライアント)に一意の Identity ID と一時的な AWS Credentials を与えることができます。

背景

旧来、ログインによって利用者(の同一性)を特定したい時、独自の認証システムを作り、そのシステムに基いて認証を行って来ました。

近年、Web identity という考えが一般的になり、Facebook や Google, Twitter などの認証システムに基いて認証することが増えてきています。しかし、例えば Facebook のアカウントを持った全ての人に公開するのではなく、一定の範囲に絞って公開したい、ということもありますよね。そういう場合は独自の認証システムで管理せざるを得ません。

上記の機能は従来の Cognito では不可能でしたが、これが Developer Authenticated Identities で可能になりました。

仕組み

今回追加された機能を使うと、独自の認証システムを使用して AWS Credentials を発行できるようになります。そのため、サーバーサイドの実装が必要になります。Identity Pool ごとに一意な Identity ID と、信頼されたトークンとして Open ID Connect Token を受け取ることができます。この Open ID Connect Token を、STS (Security Token Service) を通して一時的な AWS Credentials を受け取るために使用することができます。

cognito-developer-identity

必須要件

サーバーサイド SDK およびモバイル SDK はいずれもバージョンアップが必要です。

  • AWS SDK for Java 1.8.11
  • AWS SDK for iOS 2.0.8
  • AWS SDK for Android 2.1.0

などなど。

手順

AWS Credentials を得るには、次の手順で実装します。

  1. クライアントで何らかのサービスに普通に認証してアクセストークンを得る
  2. アクセストークンと Identity Pool ID をパラメータに、サーバーにリクエストを送信して Open ID Token を取得する
  3. Open ID Token をもとに Cognito Credentials を取得する
  4. Cognito Credentials を使って AWS サービスにアクセスできるようになる

実装してみる

今回は、サーバーサイドを Java、クライアントを Android で実装してみました。

サーバーサイドの実装 (Java)

まずはサーバーサイドの実装です。Open ID Connect Token を得るには GetOpenIdTokenForDeveloperIdentity という API を叩きます。このリクエストに独自の認証システムで管理しているユーザー ID をセットすると、その ID に結びついた Open ID Connect Token が取得できます。ログイン処理に絡めて実装し、クライアントへのレスポンスに追加するような形で実装すると良いと思います。

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
	BasicAWSCredentials credentials = new BasicAWSCredentials("YOUR_ACCESS_KEY", "YOUR_SECRET_KEY");
	AmazonCognitoIdentityClient client = new AmazonCognitoIdentityClient(credentials);
	
	// 適当なログイン処理を行い、ユーザーIDをセットする
	HashMap<String, String> map = new HashMap<String, String>();
	map.put("CustomProvider", "userId");
	
	// Open ID Connect Tokenを取得する
	GetOpenIdTokenForDeveloperIdentityRequest tokenRequest = new GetOpenIdTokenForDeveloperIdentityRequest();
	// Identity Poolは事前に作成している前提
	tokenRequest.setIdentityPoolId("us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
	// 先ほどのログイン情報を入れる
	tokenRequest.setLogins(map);
	// 生成するOpen ID Connect Tokenの有効期間
	tokenRequest.setTokenDuration(1000L);
	GetOpenIdTokenForDeveloperIdentityResult developerIdentityResult = client.getOpenIdTokenForDeveloperIdentity(tokenRequest);

	// Open ID Connect Tokenを返す
	response.getWriter().write(developerIdentityResult.toString());
}

クライアントの実装 (Android)

クライアントでは、まず Abstract クラスである AWSAbstractCognitoIdentityProvider を実装したカスタムの Provider クラスを作成する必要があります。このクラスは CognitoCachingCredentialsProvider を通して独自のサービスプロバイダを Cognito 用のプロバイダとして動作させるために利用されます。refresh() メソッドで先ほど作成したサーバーサイドの API から得た Open ID Connect Token を返し、getProviderName() メソッドで独自のサービスプロバイダの名前を返すようにします。

public class CognitoTask extends AsyncTaskLoader<Boolean> {

  private static final String accountId = "xxxxxxxxxxxx";
  private static final String poolId = "us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
  private static final String unauthArn = "arn:aws:iam::xxxxxxxxxxxx:role/Cognito_CognitoSampleUnauth_DefaultRole";
  private static final String authArn = "arn:aws:iam::xxxxxxxxxxxx:role/Cognito_CognitoSampleAuth_DefaultRole";
  
  public CognitoTask(Context context) {
    super(context);
    forceLoad();
  }

  @Override
  public Boolean loadInBackground() {
    
    // STSインスタンスを生成
    AWSSecurityTokenServiceClient sts = new AWSSecurityTokenServiceClient(new AnonymousAWSCredentials());
    sts.setRegion(Region.getRegion(Regions.US_EAST_1));
    
    // CustomProviderを生成
    AWSCognitoIdentityProvider provider = new CustomProvider(accountId, poolId);
    // ユーザーIDをセット
    HashMap<String, String> logins = new HashMap<String, String>();
    map.put("CustomProvider", "userId");
    provider.setLogins(logins);
    
    // 一時的なAWS Credentialを取得
    CognitoCachingCredentialsProvider credentialsProvider = new CognitoCachingCredentialsProvider(getContext(), provider, unauthArn, authArn, sts);
    AWSSessionCredentials credentials = credentialsProvider.getCredentials();
    
    Log.d("credentials" , "accessKey:" + credentials.getAWSAccessKeyId());
    Log.d("credentials" , "secretKey:" + credentials.getAWSSecretKey());
    
    return true;
  }
  
  
  /**
   * AWSAbstractCognitoIdentityProviderの実装クラス
   */
  public class CustomProvider extends AWSAbstractCognitoIdentityProvider {

    public CustomProvider(String accountId, String identityPoolId) {
      super(accountId, identityPoolId);
    }

    @Override
    public String refresh() {
      return getOpenIDConnectToken();
    }

    @Override
    public String getProviderName() {
      // 自分のサーバーのプロバイダ名を返す(任意の値)
      return "CustomProvider";
    }
  }
  
  /**
   * Open ID Connect Tokenを取得する
   * @return Open ID Connect Token
   */
  private String getOpenIDConnectToken() {
    // 自分のサーバーのAPIにリクエストしてOpen ID Connect Tokenを取得する処理を実装する
    String idToken = "";
    return idToken;
    
  }
}

これで一時的な AWS Credentials が得られるはずです。

クライアントの実装 (iOS)

ついでに iOS もやってみました。基本的に Android と同様の流れになります。iOS の場合は AWSAbstractIdentityProvider のサブクラスを作成し、getIdentityId: メソッドと refresh: メソッドを実装します。戻り値は BFTask となっていますが、これは Bolts のタスクオブジェクトです。Bolts の詳しい解説は割愛しますが、任意のタイミングでタスクのコールバックを呼びたいときは BFTaskCompletionSource を使います。

CustomIdentityProvider.h

#import <AWSCore.h>

@interface CustomIdentityProvider : AWSAbstractIdentityProvider

@property (nonatomic, strong) AWSCognitoIdentity *cib;
@property (nonatomic, strong) NSString *accountId;
@property (nonatomic, strong) NSString *identityPoolId;
@property (nonatomic, strong) NSString *identityId;
@property (nonatomic, strong) NSString *token;

@end

CustomIdentityProvider.m

#import "CustomIdentityProvider.h"

@implementation CustomIdentityProvider

@synthesize accountId = _accountId;
@synthesize identityPoolId = _identityPoolId;
@synthesize identityId = _identityId;
@synthesize token = _token;

- (BFTask *)getIdentityId
{
    if (self.identityId) {
        // IdentityIdがある場合はBFTaskをそのまま返す
        return [BFTask taskWithResult:nil];
    } else {
        // IdentityIdがない場合はrefreshを実行する
        return [[BFTask taskWithResult:nil] continueWithBlock:^id(BFTask *task) {
            if (!self.identityId) {
                return [self refresh];
            }
            return nil;
        }];
    }
}

- (BFTask *)refresh
{
    // 自分のサーバーのAPIにリクエストしてOpen ID Connect Tokenを取得する処理を実装する
    NSURL *url = [NSURL URLWithString:@"YOUR_SERVER_API_URL"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource];
    [NSURLConnection sendAsynchronousRequest:request
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
    {
        if (data) {
            // レスポンスからIdentity IDとOpen ID Connect Tokenを取得
            NSError *jsonError = nil;
            NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data
                                                                   options:NSJSONReadingMutableLeaves
                                                                     error:&jsonError];
            NSLog(@"result: %@", result);
            self.identityId = result[@"identityId"];
            self.token = result[@"token"];
            ;
        } else {
           NSLog(@"error: %@", error);
            ;
        }
    }];
    return ;
}

@end

あとは適当なタイミングで、次のような感じで実装します。iOS の場合は AWSServiceManager に AWSServiceConfiguration をセットして AWS Credentials を登録します。

static NSString* const kAccountId = "xxxxxxxxxxxx";
static NSString* const kIdentityPoolId = "us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
static NSString* const kUnauthArn = "arn:aws:iam::xxxxxxxxxxxx:role/Cognito_CognitoSampleUnauth_DefaultRole";
static NSString* const kAuthArn = "arn:aws:iam::xxxxxxxxxxxx:role/Cognito_CognitoSampleAuth_DefaultRole";

// CustomIdentityProviderの生成
CustomIdentityProvider *customIdentityProvider = [CustomIdentityProvider new];
customIdentityProvider.accountId = kAccountId;
customIdentityProvider.identityPoolId = kIdentityPoolId;
customIdentityProvider.logins = @{@"CustomProvider" : @"userId"};

// AWSCognitoCredentialsProviderの生成
AWSCognitoCredentialsProvider *provider = [[AWSCognitoCredentialsProvider alloc] initWithRegionType:AWSRegionUSEast1
                                                                                   identityProvider:customIdentityProvider
                                                                                      unauthRoleArn:kUnAuthArn
                                                                                        authRoleArn:kAuthArn];

// AWS Credentialに登録
AWSServiceConfiguration *configuration = [AWSServiceConfiguration configurationWithRegion:AWSRegionUSEast1
                                                                      credentialsProvider:provider];
[AWSServiceManager defaultServiceManager].defaultServiceConfiguration = configuration;

// 認証
[[provider getIdentityId] continueWithSuccessBlock:^id(BFTask *task){
    NSString* cognitoId = provider.identityId;
    NSLog(@"cognitoId: %@", cognitoId);
    NSLog(@"logins: %@", provider.logins);
    return nil;
}];

まとめ

Amazon Cognito はリリース当初から「Twitter は使えないの?」とかいろいろありましたが、Developer Authenticated Identities がサポートされたことによって、もう怖いものなしですね!