SalesforceからOpenID ConnectでAWS S3/DynamoDB/Lambdaへ接続する

# 概要

SalesforceからAWSのサービスを呼び出す際に、AWSのアクセスキーIDやシークレットアクセスキーをカスタム設定などに保持し、サーバサイドで署名を作成してHTTPコールアウトする方法をとっている人は多いかと思います。

本記事では、Salesforceが実装しているOpenID Connect OPとしての機能と、AWSが持っている [Web Identity Federation](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_oidc.html) の仕組みを利用して、AWSのシークレット情報をSalesforce側で管理することなく、認証保護されたAWSの各種サービス（S3/DynamoDB/Lambdaなど）にアクセスする方法を提示します。

※ なおSAMLを用いて同様の連携を行う方法は[一昨年の記事](http://stomita.hatenablog.com/entry/2013/12/23/195110)に記載していたりします。

## ステップ０：前提

1. Salesforceのマイドメインは設定済みとします。以下の記事中では、マイドメイン設定後のURLとして `aws-federation-demo-dev-ed.my.salesforce.com` と記載していますが、適宜自分の環境に置き換えて下さい。

2. AWS上では、S3のバケット、DynamoDBのテーブルとデータ、あるいはLambdaのFunctionは作成済みであるものとします。

## ステップ１：Visualforceページの作成

まずはSalesforce上にVisualforceページを１つ作ります。とりあえず中身は空で構いません。

```html:AWSFederationDemo.page
<apex:page>
</apex:page>
```

作成したページの名前を`AWSFederationDemo` としておくと、このVisualforceページには `https://aws-federation-demo-dev-ed.my.salesforce.com/apex/AWSFederationDemo` でアクセスできるはずです。

このURLにブラウザでアクセスするとリダイレクトされるので、リダイレクト後に表示されたURLのアドレスをアドレスバーからコピーし、記録しておきます。

## ステップ２：接続アプリケーションの作成

次に接続アプリケーションを作成します。アプリケーション名はなんでも構いません。「OAuth 設定の有効化」のチェックボックスをチェックし、さらに「コールバックURL」としてステップ１で取得したVisualforceページのURL、「選択されたOAuth範囲」に「一意の識別子へのアクセスを許可 (openid)」を選んでおきます。

<img width="1317" alt="接続アプリケーション_ AWS ~ Salesforce - Developer Edition.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/6495/08b753e9-528b-8584-7fa4-6c150d5f9eff.jpeg">


保存ボタンを押すと、接続アプリケーションの詳細情報としてコンシューマ鍵（＝OAuth2のClient ID）が表示されますので、値をコピーして記録しておきます。

<img width="1445" alt="接続アプリケーション_ AWS ~ Salesforce - Developer Edition-1-1.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/6495/6a09a7ff-d036-aed4-9ea1-ffd1d1cd8552.jpeg">


次に、接続アプリケーションの詳細画面の「Manage」ボタンをクリックし、OAuthポリシーの「許可されているユーザ」が「管理者が承認したユーザは事前承認済み」となっているかどうかを確かめます。また、「プロファイル」関連リストに、AWS連携したいユーザのプロファイルを選択し、追加しておきます。

<img width="1383" alt="接続アプリケーション_ AWS ~ Salesforce - Developer Edition-2.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/6495/6aa35d92-03b3-e5b4-7ed8-3fe3018c4559.jpeg">

<img width="1568" alt="接続アプリケーション_ AWS ~ Salesforce - Developer Edition-3.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/6495/8017d14d-10f4-f7ab-93e9-91f90c5bfef8.jpeg">


## ステップ３：AWS IDプロバイダの作成

次に、AWS Console上でIDプロバイダを登録します。AWSのサービスからIAMを選択し、IDプロバイダの新規作成を実行します。

「プロバイダーのタイプ」として「OpenID Connect」を選択し、さらに「プロバイダーのURL」としてマイドメインのURL（`https://aws-federation-demo-dev-ed.my.salesforce.com`）、を入力します。「対象者」には、ステップ２で記録したコンシューマ鍵（Client ID）を設定します。

<img width="1504" alt="IAM Management Console-14.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/6495/791c9a3a-a493-16b1-29d2-c032c10c3c83.jpeg">


## ステップ４：IAMロールの作成およびポリシーの付与

IDプロバイダの作成が完了したら、次にIAMロールを作成します。まず、任意の名前でロール名を設定します。

<img width="1568" alt="IAM Management Console-19.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/6495/25eace7a-d424-9b44-7f9c-31b9fe59c3ce.jpeg">

次のステップでロールタイプの選択を要求されますので、IDプロバイダアクセス用のロールから「ウェブIDプロバイダにアクセスを付与」を選択します。

<img width="1568" alt="IAM Management Console-20.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/6495/9fcc86d2-50f3-6a67-a22f-3ca111467065.jpeg">


次に、IDプロバイダとして先ほど作成したOpenID Connectプロバイダ（`aws-federation-demo-dev-ed.my.salesforce.com`）を選択します。（対象者には自動的に登録したClient IDが選ばれます）

<img width="1568" alt="IAM Management Console-21.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/6495/60e5c82e-7d04-904e-3c39-639cb77007d8.jpeg">

その後のポリシーのアタッチではとりあえず何も選択せずに、ロール作成を完了します。

作成したロールの詳細画面にアクセスし、「ロールARN」に表示されている値を記録しておきます。また、管理ポリシーの「ポリシーのアタッチ」ボタンを押して、同ロールを使用してログインしたユーザが DynamoDB/S3/Lambda などのサービスにアクセス可能となるよう、適切なポリシーをロールに付与しておきます。

<img width="1568" alt="IAM Management Console-28-1.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/6495/9e6c62c9-6a52-220c-abe6-4af7d72f0f9b.jpeg">

※ 上記画像の例はかなり権限を与えているので、本来は最小限に絞ったポリシーを設定することが望ましい



## ステップ５：AWSサービスの呼び出し

最後に、SalesforceからOpenID ConnectでIDトークンを取得し、このトークンを利用してAWSサービスにアクセスします。

まず、AWS JS SDKをCDNサービスのURLから直接ロードし、Visualforce内で利用できるようにします。（サンプルコードの都合上jQueryも同様にCDNからロードしています）

```html:AWSFederationDemo.page
<apex:page docType="html-5.0">
    <script src="https://sdk.amazonaws.com/js/aws-sdk-2.2.22.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
    <script>
      // ...
    </script>
    <div class="result">
        <h2>S3 objects:</h2>
        <ul class="s3-result"></ul>
        <h2>DynamoDB tables:</h2>
        <ul class="dynamo-result"></ul>
        <h2>Lambda Executed Result:</h2>
        <pre class="lambda-result"></pre>
    </div>
</apex:page>
```

上記Visualforceページの `<script>` タグの中には、以下のコードを記述します。

```js
// 接続アプリケーション（OAuth2）関連の設定情報
var authzUrl = "https://aws-federation-demo-dev-ed.my.salesforce.com/services/oauth2/authorize";
var clientId = "3MVG9ZL0ppGP5UrBJIGcl7LArbcbE8OHpX3t4.....iFI8c";
var redirectUri = "https://aws-federation-demo-dev-ed--c.ap2.visual.force.com/apex/AWSFederationDemo";

// AWS関連の設定情報
var awsRoleARN = "arn:aws:iam::xxxxx:role/DemoSalesforceUserRole"; // ステップ4で作成したロールのARN
var awsRegion = "us-east-1";
var s3BucketName = "my-s3-bucket";
var dynamoTableName = "MyDynamoTable";
var lambdaFuncName = "MyLambdaFunc";
  
// IDトークンを取得するためにAuthorization Serverへリダイレクト  
function authorize() {
  var url = authzUrl;
  // アクセストークンの他にIDトークンをレスポンスに含める形でOAuth2 Implicit Flowを開始
  url += "?response_type=" + encodeURIComponent('token id_token');
  url += "&client_id=" + encodeURIComponent(clientId);
  url += "&redirect_uri=" + encodeURIComponent(redirectUri);
  var nonce = Math.random(); // 本来はnonce値をCookie等に保持しておき、IDトークン受け取りの際に検証するべき
  url += "&nonce=" + nonce;
  location.href = url;
}

// Query文字列をハッシュオブジェクトに変換
function parseQueryString(query) {
  var params = {};
  var qparams = query.split('&');
  for (var i=0; i<qparams.length; i++) {
    var qpair = qparams[i].split('=');
    params[qpair[0]] = decodeURIComponent(qpair[1]);
  }
  return params;
}

// AWSサービスへの資格情報の設定
function connectToAWS(idToken) {
  // OpenID ConnectのIDトークンを利用してAWSサービスへ接続
  AWS.config.credentials = new AWS.WebIdentityCredentials({
    RoleArn: awsRoleARN,
    WebIdentityToken: idToken
  });
  executeAWS();
}

// AWSの各種サービスを呼び出し
function executeAWS() {
  // S3バケット内のオブジェクト一覧を取得（バケットはCORSが有効になっている必要あり）
  var s3 = new AWS.S3();
  s3.listObjects({ Bucket: s3BucketName }, function(err, ret) {
    console.log("s3 objects = ", ret.Contents);
    $.map(ret.Contents, function(obj) {
      $('ul.s3-result').append($('<li>').text(obj.Key));
    });
  });
  // DynamoDB テーブル内の全エントリを取得
  var dynamo = new AWS.DynamoDB({ region: awsRegion });
  dynamo.scan({ TableName: dynamoTableName }, function(err, ret) {
    $.map(ret.Items, function(item) {
      $('ul.dynamo-result').append($('<li>').text(JSON.stringify(item)));
    });
  });
  // Lambda Functionの呼び出し
  var lambda = new AWS.Lambda({ region: awsRegion });
  var msg = { hello: "world" };
  lambda.invoke({ FunctionName: lambdaFuncName, Payload: JSON.stringify(msg) }, function(err, ret) {
    $('pre.lambda-result').text(ret.Payload);
  });
}

// ページ表示時の初期処理
if (location.hash && /id_token=/.test(location.hash)) { // Authz Serverからトークンを受け取った場合
  var params = parseQueryString(location.hash.substring(1));
  location.hash = ''; // clear tokens received
  connectToAWS(params.id_token);
} else {
  authorize(); // OAuth2フローの開始
}
```

再びVisualforceページを表示すると、以下の画面のようにAWSから情報が取得できているのがわかるはずです。

<img width="1568" alt="Salesforce - Developer Edition.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/6495/64d66140-e96d-5a3b-f357-212d7481ee44.jpeg">


# まとめ

以上、全くSalesforce側にAWSのシークレット情報を登録することなく、AWSの各種サービスにSalesforceからアクセスできました。

今回の例ではVisualforceからAWS JS SDKを利用してアクセスしましたが、IDトークンをApex側に渡せばサーバサイドからの呼び出しもおそらく可能です[^1]。

[^1]: IDトークンを用いてApexからAWSにアクセスする場合は、最初に`AssumeRoleWithWebIdentity` という[AWS STS APIのコール](http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html)を行ってtemporary security credentialsを取得する必要があります

また、IDトークンとしてSalesforceユーザのアイデンティティ情報を引き継ぐことができるため、アクセスできるDynamoDB内のレコードデータをユーザに応じて絞り込むといった[FGAC（=Fine-Grained Access Control）をAWS側で追加する](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/FGAC_DDB.html)ことも容易になるはずです。

これからのSalesforce/AWSの連携においては、AWSアクセスキー/シークレットを用いてデータ連携するといった画一的な方法だけではなく、実現したいアプリケーションの特性に応じていろいろと手段を検討してみてはいかがでしょうか。

（なお、今回は端折ってしまいましたが、IDトークンを受け取った時にはnonceチェックをしないとReplay Attackの危険性があることは覚えておいたほうがいいでしょう）