JavaScript
Android
AWS
iOS

世界に先駆けてAWSサーバレスアーキテクチャでユーザ認証とAPI認可の実装をしてみた

More than 1 year has passed since last update.

下記GitHubにサンプルをアップロードしました。

https://github.com/teradonburi/CognitoUserAuthApiApproval


サーバレスアーキテクチャとは

サーバを運用する場合、大まかにわけて次の3つの選択肢が存在すると思います。

1. 自社でサーバを管理するケース(オンプレミス型)

2. クラウドサービスでサーバを管理してもらうケース(IaaSクラウド型)

3. クラウド上でビジネスロジックのみ記述して、必要な時だけサーバを稼働させ、スケールもクラウドサービスに任せるケース(サーバレス型)

クラウドに依存するほど自前でカスタマイズできる自由度が減る代わりにサーバ運用コストが下がります。

ざっくり言ってしまうとオンプレのサーバは運用が大変なのとコストが高くて自社で持ちたくないのでクラウドのサーバを使いたい。クラウド型だと常時稼働していると運用コストもかかってしまう。そこで、ユーザが使うときにだけ稼働してサーバのスケールアウトも全てクラウドサービスに任せてしまいたいというのがサーバレスアーキテクチャです。

スタートアップのサービスなどでユーザがどれくらいつくかわからないけど

最初はお金をかけたくない、後でスケールアウトも簡単にできるようにしたいという場合に

有効なアーキテクチャです。

サーバレスアーキテクチャに関しては下記が詳しいです。

サーバーレスアーキテクチャという技術分野についての簡単な調査


サーバレスアーキテクチャ構成図

AWS上でのサーバレスアーキテクチャ構成図を描いてみました。

あくまで一例です。

ServerlessArchitecture.png

AWSではAPI GatewayとLambdaというサービスがサーバレスアーキテクチャのコアとなるサービスです。

LambdaはビジネスロジックをNodeJSやPythonなどで記述することができ、AWS内の別サービスからトリガーで実行できます。(バックグラウンドではトリガーでの起動時にAmazon Linuxが立ち上がり、ビジネスロジックを実行してくれます。)

API GatewayはAPIのエンドポイント(URL)を提供します。エンドポイントアクセス時にトリガーでLambdaを起動させることができるため、組み合わせることでRest APIをサーバなしで実装することが可能になります。また、Swaggerなどで記述したAPI定義もインポートすることが可能です。

Lambdaは実行回数で課金されるため、稼働していなかった場合、EC2の運用コストより大幅に安いことがメリットです。Lambda料金参考

デメリットはLambdaではC/C++などのネイティブで実装されたミドルウェア(OpenCV、ffmpegなど)のインストールができない点です。

サービスによってはEC2にミドルウェアをインストールしてAPI Gateway経由で呼び出すという形になるのでしょう。

ちなみに、個人でt2.microのEC2サーバを運用していたのですが、月間のページプレビュー数が5リクエストにかかわらず月額6000円以上かかってました。(泣)

上記はRedHatの場合なので実際EC2の最小構成のみでやる場合は

EC2(t2.micro+Amazon Linux)+Route53で月額2000円程度でしょうか(スケールしませんが・・・)


AWSでのユーザ認証・API認可

2016年の4月中旬にユーザ認証サービスであるCognito User Pool(Beta版)が解放されました。(まだ、us-east-1リージョンのみ)

これにより、ユーザ認証とAPI認可含めてサーバレスアーキテクチャで実現できるようになりました。

今回、世界に先駆けてサーバレスでのユーザ認証とAPI認可を実装してみました。

Cognito User Poolにてユーザ認証を行い、

Cognito Federated Identitlesにてロール認可を行います。

IAMのロール側ではAmazon Security Token Service(STS)という機能を用いて一時認証情報を払い出しします。

一時認証情報を用いてAPI Gatewayの認可APIにアクセスします。

Auth.png

ユーザ認証からAPI認可のフローは次の図のようになります。

authFlow.png

今回、Login ProviderのところにCognito User Poolを使うことができるようになりました。

この図ではAmazon CognitoはCognito Federated Identitiesを指します。

つまり、AWSのCognito User Poolに登録したユーザ認証とFacebook、Twitterなどの外部プロバイダーのユーザ認証をCognito Federated Identitiesで一元化し、STSでIAMロールでの認可を行うことが可能になりました。


AWS側の設定

以下AWSサービスの設定をそれぞれ行います。


  • Cognito User Pool

  • Cognito Federated Identites

  • IAM

  • Lambda

  • API Gateway

以下、us-east-1リージョンにて設定を行います。


Cognito User Poolの作成

作成方法は下記を参考にさせてもらいました。

[新機能] Amazon Cognito に待望のユーザー認証基盤「User Pools」が追加されました!

作成時に細かい部分で気をつけたほうが良いことを追記しておきます。

Attributes項目の必須チェックはUser Pool作成後に変更できません。

(この項目はクライアントからユーザ登録する際に必須パラメータとなってしまいます。

ここには記載されていませんが、これと別途にユーザ名とパスワードはユーザ登録時に必須です。

Custom Attributesに関しては後から変更が可能なため、不要な場合は全てチェックを外しておくことをお勧めしておきます。)

userpool1.png

Verificationの項目はEmail、Phone Numberにチェックを入れておきます。

ログインするためにはVerificationによるユーザのConfirmedが必須です。

(ユーザ登録時にメールアドレス、もしくはSMSに送信されたアクティベートコードを入力する必要があります。)

userpool2.png

Apps項目で

Webブラウザ用とモバイル用(iOS、Android)にAppを二つ作ります。

Webブラウザ用はGenerate client secretのチェックを外して作成します。(ブラウザからのアクセス時には不要なため)

userpool3.png


Cognito Identity Poolの作成

Cognito Identity Poolの作成方法とCognito User Poolの紐付けは下記を参考にしてください。

Amazon Cognito User Poolsを使って、webサイトにユーザ認証基盤を作る

注意点としては、

Identity Poolには1つしかCognito User Poolの割り当てができないため、

ブラウザとモバイルで同一のCognito User Poolを割り当てたい場合は

Identity Pool(とIAMロール)をブラウザ用とモバイル用に2つずつ作成する必要があります。


Lambdaの設定

特にLambda側では特別な設定は必要ありません。

今回のサンプルでは単純に成功を返すLambda関数を作成してテストしてます。

ただし、レスポンスには必ずJavascriptのObject型を返すようにしてください。

理由はAndroid側のAPI Gateway SDKがそれ以外の型に対応していないためです。(後述)

'use strict';

console.log('Loading function');

exports.handler = (event, context, callback) => {
callback(null, {message:"成功"});
// callback('Something went wrong');
};


API Gatewayの設定

APIのMethod RequestのAuthorizationにAWS_IAMを設定します。

gateway1.png

認可したいAPIのarnを控えます。(後述のIAMロールで設定します。)

gateway2.png


CORSの設定(ブラウザ用APIのみ)

上記に加え、Ajax経由でAPIにアクセスするためには、APIにCORSの設定が別途必要です。

下記を参考にしてください。

jQueryからAmazon API Gatewayを使用する

作成時にAccess-Control-Allow-Headersに下記ヘッダーを設定します。

'Content-Type,X-Amz-Date,Authorization,X-Requested-With,X-Requested-By,X-Amz-Security-Token'

X-Requested-With,X-Requested-ByはXHR(Ajax) で必要なヘッダーパラメータです。

X-Amz-Security-Tokenは今回のようなSTSでのAPI認可には必須ヘッダーパラメータです。

gateway3.png


IAMのロールの設定

Cognito Identity Poolで指定したAuthenticatedロールの権限を設定します。

UnAuthenticatedのほうは特に権限を付与する必要はありません。

IAMのロール項目でAuthenticatedロールを開きます。

ポリシーの編集より、ポリシーを編集します。

Actionに"execute-api:invoke"を付与します。

Resourceに認可したいAPI GatewayのAPIのARNを指定します。

{

  "Version": "2012-10-17",
  "Statement": [
  {
  "Effect": "Allow",
  "Action": [
  "execute-api:Invoke",
  "cognito-sync:*",
  "cognito-identity:*"
  ],
  "Resource": [
     "arn:aws:execute-api:us-east-1:<AccountID>:<API Gateway API ID>/*/<Method Type>/<API名>"
  ]
 }
]
}

信頼関係の編集より

1.Actionがsts:AssumeRoleWithWebIdentity

2.cognito-identity.amazonaws.com:audが対象のIdentity Pool Id

3.cognito-identity.amazonaws.com:amrにauthenticated(UnAuthenticatedのロールの方はunauthenticatedになっています)

になっていることを確認してください

{

"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"cognito-identity.amazonaws.com:aud": ""us-east-1:<Identity Pool Id>"
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "authenticated"
}
}
}
]
}


実装


Javascript(ブラウザ)での実装

ローカルから起動する場合はAWS-CLIの設定が必要です。

AWS CLI インストール&セットアップ メモ(Mac)


導入


  • Cognito SDKの導入

amazon-cognito-identity-js

Crypto.jsに関しては--with-codecBytes オプションをつけて自前でビルドする必要があります。


  • API Gateway SDKの導入

API GatewayのステージからSDKをダウンロードします。

apigateway.png


  • 初期設定

下記パラメータを設定してください(AWSのID)


login.html

        /////////////////////////

// Config Param
/////////////////////////

var UserPoolId = 'us-east-1_<Pool Id>'; // Cognito User Pool Id
var ClientId = '<Client Id>'; // Client Id(Cognito User Pool App Client Id)
var IdentityPoolId = 'us-east-1:<Identity Pool Id>'; // Cognito Identity Pool Id
var endpoint = 'https://<API>.execute-api.us-east-1.amazonaws.com/<Stage>'; // API Gateway EndPoint
var testAPI = '/<API>'; // API Gateway API

/////////////////////////



ユーザの登録・ユーザのアクティベート・ユーザのログイン

下記記事と変わりませんので割愛します。

Amazon Cognito User Poolsを使って、webサイトにユーザ認証基盤を作る


認可

AccessTokenではなく、IdTokenで認可を行います。

getId関数で認証プロバイダー(Cognito User Pool)に問い合わせを行い、

GetCredentialsForIdentity関数で認可を行います。(Authenticateのロールに切り替わります。)


login.html

        var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);

cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (result) {
var IdToken = result.getIdToken().getJwtToken();
console.log('IdToken:' + IdToken);

var provider = {};
provider['cognito-idp.us-east-1.amazonaws.com/' + UserPoolId] = IdToken;

var params = {
IdentityPoolId:IdentityPoolId,
Logins: provider
};
var cognitoidentity = new AWS.CognitoIdentity({apiVersion: '2016-04-18'});
cognitoidentity.getId(params, function(err, data) {
if (err){
console.log(err, err.stack);
}
else{

var params = {
IdentityId: data.IdentityId,
Logins:provider
};

cognitoidentity.getCredentialsForIdentity(params, function(err, data) {
if (err){
console.log(err, err.stack);
}
else{
console.log("data.Credentials:",data.Credentials);
// Access AWS resources here.
callAPIGateway(data.Credentials);
}
});

}
});

},

onFailure: function(err) {
alert(err);
}

});



認可APIのコール

STSより取得した一時認証情報


  • AccessKeyId

  • SecretKey

  • SessionToken

を用いてAPI GatewayのAPIをコールします。

       function callAPIGateway(token){

var apigClient = apigClientFactory.newClient(endpoint,{
accessKey: token.AccessKeyId,
secretKey: token.SecretKey,
sessionToken:token.SessionToken,
region: 'us-east-1' // OPTIONAL: The region where the API is deployed, by default this parameter is set to us-east-1
});

var param = {
}

var body = {
};

apigClient.doPost(testAPI,param, body)
.then(function(result){
console.log("result:",result);
}).catch( function(result){
console.log("err:",result);
});
}

API GatewayのSDKに関しては自動生成されるAPI(apigClient.js)が気に入らなかったので汎用化してます。


apigClient.js

var apigClientFactory = {};

apigClientFactory.newClient = function (gatewayPath,config) {
var apigClient = { };
if(config === undefined) {
config = {
accessKey: '',
secretKey: '',
sessionToken: '',
region: '',
apiKey: undefined,
defaultContentType: 'application/json',
defaultAcceptType: 'application/json'
};
}
if(config.accessKey === undefined) {
config.accessKey = '';
}
if(config.secretKey === undefined) {
config.secretKey = '';
}
if(config.apiKey === undefined) {
config.apiKey = '';
}
if(config.sessionToken === undefined) {
config.sessionToken = '';
}
if(config.region === undefined) {
config.region = 'us-east-1';
}
//If defaultContentType is not defined then default to application/json
if(config.defaultContentType === undefined) {
config.defaultContentType = 'application/json';
}
//If defaultAcceptType is not defined then default to application/json
if(config.defaultAcceptType === undefined) {
config.defaultAcceptType = 'application/json';
}

// extract endpoint and path from url
var invokeUrl = gatewayPath;
var endpoint = /(^https?:\/\/[^\/]+)/g.exec(invokeUrl)[1];
var pathComponent = invokeUrl.substring(endpoint.length);

var sigV4ClientConfig = {
accessKey: config.accessKey,
secretKey: config.secretKey,
sessionToken: config.sessionToken,
serviceName: 'execute-api',
region: config.region,
endpoint: endpoint,
defaultContentType: config.defaultContentType,
defaultAcceptType: config.defaultAcceptType
};

var authType = 'NONE';
if (sigV4ClientConfig.accessKey !== undefined && sigV4ClientConfig.accessKey !== '' && sigV4ClientConfig.secretKey !== undefined && sigV4ClientConfig.secretKey !== '') {
authType = 'AWS_IAM';
}

var simpleHttpClientConfig = {
endpoint: endpoint,
defaultContentType: config.defaultContentType,
defaultAcceptType: config.defaultAcceptType
};

var apiGatewayClient = apiGateway.core.apiGatewayClientFactory.newClient(simpleHttpClientConfig, sigV4ClientConfig);

apigClient.doGet = function (path,params, body, additionalParams) {
if(additionalParams === undefined) { additionalParams = {}; }

apiGateway.core.utils.assertParametersDefined(params, [], ['body']);

var GetRequest = {
verb: 'get'.toUpperCase(),
path: pathComponent + uritemplate(path).expand(apiGateway.core.utils.parseParametersToObject(params, [])),
headers: apiGateway.core.utils.parseParametersToObject(params, []),
queryParams: apiGateway.core.utils.parseParametersToObject(params, []),
body: body
};

return apiGatewayClient.makeRequest(GetRequest, authType, additionalParams, config.apiKey);
};

apigClient.doPost = function (path,params, body, additionalParams) {
if(additionalParams === undefined) { additionalParams = {}; }

apiGateway.core.utils.assertParametersDefined(params, [], ['body']);

var PostRequest = {
verb: 'post'.toUpperCase(),
path: pathComponent + uritemplate(path).expand(apiGateway.core.utils.parseParametersToObject(params, [])),
headers: apiGateway.core.utils.parseParametersToObject(params, []),
queryParams: apiGateway.core.utils.parseParametersToObject(params, []),
body: body
};

return apiGatewayClient.makeRequest(PostRequest, authType, additionalParams, config.apiKey);
};

return apigClient;
};



iOSでの実装


導入


  • AWS SDKの導入

Cocoa Podにて依存ライブラリをインストールします。

platform :ios, '8.0'

pod 'UICKeyChainStore','2.1.0'
pod 'AWSCore','2.4.2'
pod 'AWSCognitoIdentityProvider','2.4.2'
pod 'AWSCognito','2.4.2'
pod 'AWSAPIGateway','2.4.2'


  • API Gateway SDKの導入

API GatewayのステージからSDKをダウンロードします。

apigateway.png


  • 初期設定

下記パラメータを設定してください(AWSのID)


ViewController.swift

    //////////////////////

// Config Param
//////////////////////

var UserPoolId = "us-east-1_<Pool Id>" // Cognito User Pool Id
var ClientId = "<Client Id>" // Cognito User Pool Client Id
var ClientSecret = "<Client Secret>" // Cognito User Pool Client Secret
var IdentitityPoolId = "us-east-1:<Identity Pool Id>" // Cognito Identity Pool Id
let EndPoint = "https://<API>.execute-api.us-east-1.amazonaws.com/<Stage>" // API Gateway Endpoint
let API = "/<API>" // API Gateway API

//////////////////////



ユーザの登録・ユーザのアクティベート・ユーザのログイン

下記記事と変わらないので割愛します。

Amazon Cognito User Poolsを使って、webサイトにユーザ認証基盤を作る

一箇所だけ補足で現在Cognito User Pool SDKのバグで英語以外の言語(ロケール)でログインできないバグがあります。

https://forums.aws.amazon.com/thread.jspa?messageID=719881

AWS iOS SDK 2.4.2で修正されたようです。


認可

Cognito User Poolログイン後、IdTokenを取得し、

AWSCognitoCredentialsProviderクラスでCognito User Providerを認証プロバイダーとして設定します。

credentialメソッドで一時認証情報を取得します。

キャッシュとして認証情報は保持されるようなので(ポリシーの変更が即時反映されない)

キャッシュクリアする場合はclearKeychainメソッドを呼びます。


ViewController.swift


let user: AWSCognitoIdentityUser = pool!.getUser(self.loginName.text!)

user.getSession(self.loginName.text!, password: self.loginPassword.text!, validationData: nil, scopes: nil).continueWithBlock({task in
if((task.error) != nil) {
print(task.error)
} else {
print(task.result)

let cognitoIdentityPoolId = self.IdentitityPoolId
let ret = task.result as! AWSCognitoIdentityUserSession
let IdToken : String = ret.idToken!.tokenString

// 認証プロバイダーにUser Poolを使う
let provider = "cognito-idp.us-east-1.amazonaws.com/" + self.UserPoolId

let userpoolProvider = UserPoolProvider()
userpoolProvider.setting(provider, token: IdToken)
let credentialsProvider = AWSCognitoCredentialsProvider(
regionType: .USEast1,
identityPoolId: cognitoIdentityPoolId,
identityProviderManager: userpoolProvider
)
// credentialのキャッシュをクリア
credentialsProvider.clearKeychain()

// credential取得
credentialsProvider.credentials().continueWithBlock { (task: AWSTask!) -> AnyObject! in

if (task.error != nil) {
print(task.error)

} else {
print(task.result)

// 認証情報を渡し、API GatewayのAPIをコール
self.callAPI(credentialsProvider)

}
return nil
}

}

return nil
})


UserPoolProviderクラスはAWSIdentityProviderManagerを実装したクラスです。

参考:モバイルアプリでユーザー認証やデータ同期が行えるAmazon CognitoがiOSで動かないのを何とか調べた話


UserPoolProvider.swift

import Foundation

public class UserPoolProvider:NSObject, AWSIdentityProviderManager{

var providerName:String!
var token:String!

public func setting(providerName:String,token:String){
self.providerName = providerName
self.token = token
}

public func logins() -> AWSTask {
var providers = [String:String]()
providers[self.providerName] = self.token
return AWSTask(result: providers as AnyObject)
}
}



認可APIのコール

認証に成功すると認証プロバイダークラス内で一時認証情報を保持するようになります。

API Gatewayのクラスに認証プロバイダーをセットし、

認可APIをコールします。


ViewController.swift

     func callAPI(credentialsProvider:AWSCognitoCredentialsProvider){

// API Gatewayのエンドポイント設定
ServerlessClient.setEndPoint(EndPoint)

// Cognito identityPoolIdによるAPI認可
let configuration = AWSServiceConfiguration(region: .USEast1, credentialsProvider: credentialsProvider)
ServerlessClient.registerClientWithConfiguration(configuration,forKey: "Auth")

///////////////////////API認可されたAPIの呼び出し///////////////////////////

let client = ServerlessClient(forKey: "Auth")

client.Post(API,param: ["":""]).continueWithBlock { (task: AWSTask!) -> AnyObject! in

if (task.error != nil) {
print(task.error)

} else {
print(task.result)

}
return nil
}

}


エクスポートされたAPI Gatewayのクラスのほうは汎用性に欠けるのでカスタマイズしています。

(Serverless.h、Serverless.m)


Serverless.h


#import <Foundation/Foundation.h>
#import <AWSAPIGateway/AWSAPIGateway.h>
#import <AWSCore/AWSCore.h>

@interface ServerlessClient: AWSAPIGatewayClient

+ (void)setEndPoint:(NSString*)endpoint;

+ (instancetype)defaultClient;
+ (void)registerClientWithConfiguration:(AWSServiceConfiguration *)configuration forKey:(NSString *)key;
+ (instancetype)clientForKey:(NSString *)key;
+ (void)removeClientForKey:(NSString *)key;

- (AWSTask *)Get:(NSString *)path
param:(NSDictionary *)param;

- (AWSTask *)Post:(NSString *)path
param:(NSDictionary *)param;

@end



Serverless.m

#import "ServerlessClient.h"

#import <AWSCore/AWSSignature.h>
#import <AWSCore/AWSSynchronizedMutableDictionary.h>
#import <AWSCognitoIdentityProvider/AWSCognitoIdentityProvider.h>

@interface SeverlessModel : AWSModel

@property (strong, nonatomic) NSDictionary* param;

@end

@implementation SeverlessModel

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{};
}

@end

@interface AWSAPIGatewayClient()

// Networking
@property (nonatomic, strong) NSURLSession *session;

// For requests
@property (nonatomic, strong) NSURL *baseURL;

// For responses
@property (nonatomic, strong) NSDictionary *HTTPHeaderFields;
@property (nonatomic, assign) NSInteger HTTPStatusCode;

- (AWSTask *)invokeHTTPRequest:(NSString *)HTTPMethod
URLString:(NSString *)URLString
pathParameters:(NSDictionary *)pathParameters
queryParameters:(NSDictionary *)queryParameters
headerParameters:(NSDictionary *)headerParameters
body:(id)body
responseClass:(Class)responseClass;

@end

@interface ServerlessClient()

@property (nonatomic, strong) AWSServiceConfiguration *configuration;

@end

@interface AWSServiceConfiguration()

@property (nonatomic, strong) AWSEndpoint *endpoint;

@end

@implementation ServerlessClient

@synthesize configuration = _configuration;

static AWSSynchronizedMutableDictionary *_serviceClients = nil;
static NSString *url = nil;

+ (void)setEndPoint:(NSString*)endpoint;
{
url = endpoint;
}

+ (instancetype)defaultClient {
AWSServiceConfiguration *serviceConfiguration = nil;
if ([AWSServiceManager defaultServiceManager].defaultServiceConfiguration) {
serviceConfiguration = AWSServiceManager.defaultServiceManager.defaultServiceConfiguration;
} else {
serviceConfiguration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionUnknown
credentialsProvider:nil];
}

static ServerlessClient *_defaultClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_defaultClient = [[ServerlessClient alloc] initWithConfiguration:serviceConfiguration];
});

return _defaultClient;
}

+ (void)registerClientWithConfiguration:(AWSServiceConfiguration *)configuration forKey:(NSString *)key {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_serviceClients = [AWSSynchronizedMutableDictionary new];
});
[_serviceClients setObject:[[ServerlessClient alloc] initWithConfiguration:configuration]
forKey:key];
}

+ (instancetype)clientForKey:(NSString *)key {
return [_serviceClients objectForKey:key];
}

+ (void)removeClientForKey:(NSString *)key {
[_serviceClients removeObjectForKey:key];
}

- (instancetype)init {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"`- init` is not a valid initializer. Use `+ defaultClient` or `+ clientForKey:` instead."
userInfo:nil];
return nil;
}

- (instancetype)initWithConfiguration:(AWSServiceConfiguration *)configuration {
if (self = [super init]) {
_configuration = [configuration copy];

NSString *URLString = url;
if ([URLString hasSuffix:@"/"]) {
URLString = [URLString substringToIndex:[URLString length] - 1];
}
_configuration.endpoint = [[AWSEndpoint alloc] initWithRegion:_configuration.regionType
service:AWSServiceAPIGateway
URL:[NSURL URLWithString:URLString]];

AWSSignatureV4Signer *signer = [[AWSSignatureV4Signer alloc] initWithCredentialsProvider:_configuration.credentialsProvider endpoint:_configuration.endpoint];

_configuration.baseURL = _configuration.endpoint.URL;
_configuration.requestInterceptors = @[[AWSNetworkingRequestInterceptor new], signer];
}

return self;
}

- (AWSTask *)Get:(NSString *)path
param:(NSDictionary *)param
{

NSDictionary *headerParameters = @{
@"Content-Type": @"application/json",
@"Accept": @"application/json",
};

NSDictionary *queryParameters = @{

};
NSDictionary *pathParameters = @{

};

SeverlessModel* model = [[SeverlessModel alloc] init];
model.param = param;

return [self invokeHTTPRequest:@"GET"
URLString:path
pathParameters:pathParameters
queryParameters:queryParameters
headerParameters:headerParameters
body:model
responseClass:nil];

}

- (AWSTask *)Post:(NSString *)path
param:(NSDictionary *)param
{

NSDictionary *headerParameters = @{
@"Content-Type": @"application/json",
@"Accept": @"application/json",
};

NSDictionary *queryParameters = @{

};
NSDictionary *pathParameters = @{

};

SeverlessModel* model = [[SeverlessModel alloc] init];
model.param = param;

return [self invokeHTTPRequest:@"POST"
URLString:path
pathParameters:pathParameters
queryParameters:queryParameters
headerParameters:headerParameters
body:model
responseClass:nil];

}

@end



Androidでの実装

下記参考にしてます。


導入


  • AWS SDKの導入

Android Studioのbuild.gradleのdependenciesにAWS SDKの各種ライブラリを追加します。


build.gradle

dependencies {

compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.3.0'
compile 'com.amazonaws:aws-android-sdk-core:2.2.17+'
compile 'com.amazonaws:aws-android-sdk-cognito:2.2.17+'
compile 'com.amazonaws:aws-android-sdk-cognitoidentityprovider:2.2.17+'
compile 'com.amazonaws:aws-android-sdk-apigateway-core:2.2.17+'
}


  • API Gateway SDKの導入

下記、API GatewayのステージからSDKをダウンロードしません

apigateway.png

理由はカスタマイズが利かないからです。

ダウンロードしたSDKはMavenにてJarファイルを作成する形式になっているのですが

この方式だとAPI GatewayのAPIを追加、変更するたびにダウンロードして、

Mavenでコンパイルしてjarを作成するという作業が必要になります。

そのため、今回Android版はAPIコール部分はスクラッチで実装します。(後述)


  • 初期設定

下記パラメータを設定してください(AWSのID)


MainActivity.java

    //////////////////////

// Config Param
//////////////////////
private static final String userPoolId = "us-east-1_<User Pool Id>";
private static final String clientId = "<User Pool Client Id>";
private static final String clientSecret = "<User Pool Client Secret>";
private static final String identityPoolId = "us-east-1:<Identity Pool Id>";
//////////////////////


ユーザの登録・ユーザのアクティベート・ユーザのログイン

どうもAndroid版のほうも言語設定をEnglishにしないとCognito User PoolにログインできないSDKのバグがあるようです。(Beta版だし仕方ないね・・・)

https://github.com/aws/aws-sdk-android/issues/132

AWS Android SDK 2.2.17で修正されたようです。


  • Cognito User Pool初期化

コンストラクタにて初期化パラメータを渡してCognitoUserPoolクラスを作成します。


MainActivity.java

CognitoUserPool userPool = new CognitoUserPool(this, userPoolId, clientId, clientSecret, new ClientConfiguration());



  • ユーザ登録

CoginitoUserPoolクラスのsignUpInBackgroundメソッドでユーザ登録します。


MainActivity.java

                // Read user data and register

CognitoUserAttributes userAttributes = new CognitoUserAttributes();

String name = nameEdit.getText().toString();
String password = passwordEdit.getText().toString();
String email = emailEdit.getText().toString();
userAttributes.addAttribute("email", email);

userPool.signUpInBackground(name, password, userAttributes, null, new SignUpHandler() {
@Override
public void onSuccess(CognitoUser user, boolean signUpConfirmationState,
CognitoUserCodeDeliveryDetails cognitoUserCodeDeliveryDetails) {
// Check signUpConfirmationState to see if the user is already confirmed
Boolean regState = signUpConfirmationState;
if (signUpConfirmationState) {
// User is already confirmed
Toast.makeText(getApplicationContext(), "すでにConfirmedされています", Toast.LENGTH_LONG).show();
}
else {
// User is not confirmed
Toast.makeText(getApplicationContext(), "認証コードを入力してアクティベートしてください", Toast.LENGTH_LONG).show();
}
}

@Override
public void onFailure(Exception exception) {
Toast.makeText(getApplicationContext(), "ユーザ登録に失敗しました", Toast.LENGTH_LONG).show();
}
});



  • ユーザのアクティベート

confirmSignUpInBackgroundメソッドにてアクティベートします。

User Poolの登録ユーザがConfirmedされます。


MainActivity.java

                String verifyName = verifyNameEdit.getText().toString();

String activateCode = activateCodeEdit.getText().toString();

userPool.getUser(verifyName).confirmSignUpInBackground(activateCode, true, new GenericHandler() {
@Override
public void onSuccess() {
Toast.makeText(getApplicationContext(), "アクティベートされました", Toast.LENGTH_LONG).show();
}

@Override
public void onFailure(Exception exception) {

}
});



  • ユーザのログイン

getSessionInBackgroundメソッドにてログインします。


MainActivity.java

                String loginName = loginNameEdit.getText().toString();

final String loginPassword = loginPasswordEdit.getText().toString();

userPool.getUser(loginName).getSessionInBackground(new AuthenticationHandler() {
@Override
public void onSuccess(CognitoUserSession cognitoUserSession) {
Toast.makeText(getApplicationContext(), "ログイン成功", Toast.LENGTH_LONG).show();

                                                // API認可の処理

}

@Override
public void getAuthenticationDetails(AuthenticationContinuation authenticationContinuation, String username) {

AuthenticationDetails authenticationDetails = new AuthenticationDetails(username, loginPassword, null);
authenticationContinuation.setAuthenticationDetails(authenticationDetails);
authenticationContinuation.continueTask();

}

@Override
public void getMFACode(MultiFactorAuthenticationContinuation multiFactorAuthenticationContinuation) {
// MFAの設定がない場合は不要
}

@Override
public void onFailure(Exception e) {
Toast.makeText(getApplicationContext(), "ログインに失敗しました", Toast.LENGTH_LONG).show();
}
});



認可

ユーザログイン後に

CognitoCredentialsProviderクラスに

User Pool認証プロバイダーとログインユーザのIdTokenを指定し、

setLoginsメソッドで認可します。


MainActivity.java

                        CognitoCredentialsProvider credentialsProvider = new CognitoCredentialsProvider(

identityPoolId,Regions.US_EAST_1);
//credentialsProvider.clear();

Map<String, String> logins = new HashMap<String, String>();
String userPoolProvider = "cognito-idp.us-east-1.amazonaws.com/" + userPoolId;
logins.put(userPoolProvider, cognitoUserSession.getIdToken().getJWTToken());
credentialsProvider.setLogins(logins);
//credentialsProvider.getCredentials();

// 認可後、APIコール
callAPI(credentialsProvider);



認可APIのコール

認可後の認証プロパイダーをAPIClientFactoryクラスに設定します。

APIClientFactoryクラスのbuildメソッドによりAPIコールクライアントを作成します。

内部的にはProxyクラスによるApiClientHandlerクラスのインジェクションと

APIコール時にはGsonライブラリによりJSONとモデルクラスのORマッピングを行っています。


MainActivity.java

    private void callAPI(final CognitoCredentialsProvider credentialsProvider)

{

new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
ApiClientFactory factory = new ApiClientFactory();

if(credentialsProvider != null){
factory.credentialsProvider(credentialsProvider);
}

// Build
final TestClient client = factory.build(TestClient.class);

Gson gson = new Gson();
RequestData requestData = new RequestData("パラメータです");

ResponseData data = client.testPost(gson.toJson(requestData));
return null;
}

}.execute();

}


APIクライアントクラス(TestClientクラス)は次のように定義します。

Serviceアノテーションのendpointの部分はAPI GatewayのステージデプロイURLに合わせて変更してください。

OperationアノテーションにAPIのパスとメソッド形式を指定します。


TestClient.java

package com.example.teraidaiki.serverless;

import com.amazonaws.mobileconnectors.apigateway.annotation.Operation;
import com.amazonaws.mobileconnectors.apigateway.annotation.Service;

@Service(
endpoint = "https://<API Gateway Stage ID>.execute-api.us-east-1.amazonaws.com/test"
)
public interface TestClient {
@Operation(
path = "/test",
method = "POST"
)
ResponseData testPost(String param);

}


RequestDataクラスは送信用パラメータを定義したクラスです。

GsonライブラリでJSON文字列化します。


RequestData.java

package com.example.teraidaiki.serverless;

public class RequestData {
String someData;

public RequestData(String someData){
this.someData = someData;
}
}


ResponseDataクラスはレスポンスで受け取るデータを定義したモデルクラスです。


ResponseData.java

package com.example.teraidaiki.serverless;

public class ResponseData {
String message;// レスポンスデータに合わせて変える

public ResponseData() {
}
}


Android側でレスポンスデータを取得するためには、ResponseDataクラスのメンバ変数を適宜変える必要があります。

今回、Lambda側からはレスポンスで次のようなデータを返却します。

{message:"成功"}

上記のようなJavascriptのObject型のレスポンスでないとAPIコール時にGsonライブラリ内でORマッピングに失敗し、Androidアプリが落ちます。