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

  • 10
    Like
  • 0
    Comment
More than 1 year has passed since last update.

概要

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

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

※ なおSAMLを用いて同様の連携を行う方法は一昨年の記事に記載していたりします。

ステップ0:前提

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

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

ステップ1:Visualforceページの作成

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

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

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

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

ステップ2:接続アプリケーションの作成

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

接続アプリケーション_ AWS ~ Salesforce - Developer Edition.jpg

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

接続アプリケーション_ AWS ~ Salesforce - Developer Edition-1-1.jpg

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

接続アプリケーション_ AWS ~ Salesforce - Developer Edition-2.jpg

接続アプリケーション_ AWS ~ Salesforce - Developer Edition-3.jpg

ステップ3:AWS IDプロバイダの作成

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

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

IAM Management Console-14.jpg

ステップ4:IAMロールの作成およびポリシーの付与

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

IAM Management Console-19.jpg

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

IAM Management Console-20.jpg

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

IAM Management Console-21.jpg

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

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

IAM Management Console-28-1.jpg

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

ステップ5:AWSサービスの呼び出し

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

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

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> タグの中には、以下のコードを記述します。

// 接続アプリケーション(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から情報が取得できているのがわかるはずです。

Salesforce - Developer Edition.jpg

まとめ

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

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

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

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

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


  1. IDトークンを用いてApexからAWSにアクセスする場合は、最初にAssumeRoleWithWebIdentity というAWS STS APIのコールを行ってtemporary security credentialsを取得する必要があります