AWSでホストされているアプリケーションをSalesforceから署名付きPOSTで呼び出してみた(Canvas使用)

2022.10.31

AWSなどSalesforceの外部でホストされているアプリケーションをSalesforceから呼び出す方法の一つにCanvasがあります。

Canvas 開発者ガイド
https://developer.salesforce.com/docs/atlas.ja-jp.240.0.platform_connect.meta/platform_connect/canvas_framework_intro.htm

Canvasは署名付きPOST ( Signed Request ) に対応しており、これを使うことで、 Salesforceで認証されたユーザーに対してのみ、目的のアプリケーションに透過的にアクセスさせることが可能です。本エントリーでは、Canvasを使ってAWSでホストされているアプリケーション(以後、Canvasアプリケーションと記述します)にアクセスし、Salesforceのユーザ情報をアプリケーション側で取り出す方法について書きました。

なお、Canvasを扱うためのSDKが提供されています。

しかし、これらはメンテナンスが数年前から動いていないようで、アーキテクチャも古いため、前出の「Canvas 開発者ガイド」に従って、自前で署名付きPOSTを解析する方法を取りました。

Salesforce内部から外部アプリケーションを安全に呼び出したい方の参考になれば嬉しいです。

SalesforceでCanvasを定義する

Canvasを使用するためには、SalesforceにてCanvas用の接続アプリケーションを定義する必要があります。

接続アプリケーションをCanvas向けに定義する

Salesforceの[設定] > [アプリケーションマネージャ]から[新規接続アプリケーション]をクリックして、Canvas用の接続アプリケーションを作成します。

Canvas用接続アプリケーション設定1

Canvas用接続アプリケーション設定2

Canvas用接続アプリケーション設定3

接続アプリケーションの主な設定は次の通りです。

項目 設定値
接続アプリケーション名 任意の値
API 参照名 任意の値(ここではMY_CANVASとしました)
OAuth 設定の有効化 チェックを入れる
コールバック URL 適当なURL(ここではCanvasアプリケーションのURLと同じドメインの/callbackパスとしました)
選択した OAuth 範囲 「APIを使用してユーザデータを管理(api)」を選択
Web サーバフローの秘密が必要 チェックを入れる(デフォルトのまま)
更新トークンフローの秘密が必要 チェックを入れる(デフォルトのまま)
キャンバス チェックを入れる
キャンバスアプリケーションの URL CanvasアプリケーションのURL(POST先のURL)
アクセス方法 「署名付き要求(POST)」を選択
場所 「Visualforce ページ」を選択

Canvasアプリケーションを埋め込む場所にVisualforceを選択しましたが、その他の場所も指定できます。

キャンバスアプリケーションの表示場所

Visualforceから定義した接続アプリケーションを呼び出す

Visualforceから呼び出すためには、定義した接続アプリケーションのAPI参照名を使って次のようにします。

<apex:page>
    <apex:canvasApp developerName="MY_CANVAS" 
        height="1000px" width="800px" 
        parameters="{p1:'value1',p2:'value2',p3:'value3'}"/>
</apex:page>

developerNameにAPI参照名を渡します。
また、parametersにJSON形式でCanvasアプリケーションに渡すパラメータを指定することができます。

外部でホストされるアプリケーション(Canvasアプリケーション)を用意する

POST先のCanvasアプリケーションを用意します。ここでは簡単にAWS Amplifyで用意することにしました。

AWS Amplifyで署名付きPOST先のCanvasアプリケーションを用意する

Canvasアプリケーションのアプリ名をsample-canvasとしました。ここではReactを使っています。

$ npx create-react-app sample-canvas
$ cd sample-canvas

Amplifyプロジェクトを立ち上げて初期化します。

$ amplify init
? Enter a name for the environment dev
? Choose your default editor: Vim (via Terminal, macOS only)
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path:  src
? Distribution Directory Path: build
? Build Command:  npm run-script build
? Start Command: npm run-script start
Using default provider  awscloudformation
? Select the authentication method you want to use: AWS profile
? Please choose the profile you want to use [select your profile]

Lambda functionを追加します。関数名はhelloとしました。

$ amplify add function
? Select which capability you want to add: Lambda function (serverless function)
? Provide an AWS Lambda function name: hello
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World

Available advanced settings:
- Resource access permissions
- Scheduled recurring invocation
- Lambda layers configuration
- Environment variables configuration
- Secret values configuration

? Do you want to configure advanced settings? No
? Do you want to edit the local lambda function now? No

定義したLambda functionにアクセスするAPI Gatewayを追加します。API名もhelloとしました。

$ amplify add api
? Select from one of the below mentioned services: REST
✔ Would you like to add a new path to an existing REST API: (y/N) · no
✔ Provide a friendly name for your resource to be used as a label for this category in the project: · hello
✔ Provide a path (e.g., /book/{isbn}): · /hello
✔ Choose a Lambda source · Use a Lambda function already added in the current Amplify project
✔ Choose the Lambda function to invoke by this path · hello
✔ Restrict API access? (Y/n) · no
✔ Do you want to add another path? (y/N) · no

Restrict API access?をnoにしていますが、本番運用する場合はYesにしてください。

定義したリソースをデプロイします。

$ amplify publish

https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/helloのようなAPI GatewayのURLが生成されます。

生成されたAPI GatewayのURLはSalesforceのCanvasアプリケーション設定のキャンバスアプリケーションの URLに忘れずに設定してください。

署名付きPOSTをLambdaで解析して、Salesforceユーザー情報を取り出す

ここまでで、作成したVisualforceページにアクセスすると、CanvasによってAmplifyで作成したCanvasアプリケーションが認証付きPOSTでコールされるようになっています。あとは、Canvasアプリケーション側で認証付きPOSTを解析して、Salesforceから渡される各種情報を取り出します。

具体的にはAmplifyで作成したLambda function(amplify/backend/function/hello/src/index.js)を次のように書き換えます。

import crypto from 'crypto';

function decode(signed_request, secret) {
    if( !signed_request || !secret ) {
        throw new Error('Must pass both signed_request and api secret');
    }

    // decode the data
    let data, encoded_data, sig;
    try {
        encoded_data = signed_request.split('.', 2);
        sig = encoded_data[0];
        const json = Buffer.from(encoded_data[1], 'base64').toString();
        data = JSON.parse(json);
    } catch ( e ) {
        throw new Error('Could not parse signed-request');
    }

    // check algorithm
    if ( !data.algorithm || data.algorithm.toUpperCase() !== 'HMACSHA256' ) {
        throw new Error('Unknown algorithm. Expected HMACSHA256');
    }

    // check sign
    const expected_sig = crypto.createHmac('sha256', secret).update(encoded_data[1]).digest('base64');
    if ( sig !== expected_sig ) {
        throw new Error('Bad signed JSON Signature!');
    }
    return data;
}

export async function handler(event) {
    try {
        const signed_req = decodeURIComponent(event.body.split('=', 2)[1]);
        const body = decode(signed_req, '<接続アプリケーションの[コンシューマの秘密]を指定する>');

        return {
            statusCode: 200,
            body: JSON.stringify(body.context.user)
        };
    } catch ( e ) {
        console.error(e);
    }
};

POSTのbodyにsigned_request=署名付きPOSTの値というフォーマットで署名付きPOSTの値が送られてくるので、これを取り出して、署名付き要求の検証および復号化のドキュメントにならって、リクエストの検証と復号化をdecode関数で行なっています。

最初にdecodeURIComponentでリクエストをURIデコードする必要があることに留意してください(これでめちゃくちゃ嵌りました)。

最後に、body.context.userでSalesforceのユーザ情報を取り出して返しています。そのほかの情報も取り出せますので、詳しくは次のリファレンスを参照してください。

CanvasRequest

ユーザ情報にはロールID(roleId)なども含まれているので、ユーザのロールによってCanvasアプリケーション側の挙動を変える、といったことも可能です。

署名付きPOSTを検証することで、確実にCanvasを定義したSalesforce組織からのリクエストであることを保証できています。 作成したVisualforceページにアクセスして、Salesforceのユーザ情報(今、ログインしている自身のSalesforceのユーザ情報)がJSONで返ってきていれば成功です。

まとめ

SalesforceのCanvasを用いて、外部でホストされているアプケーションをSalesforce内部から安全に呼び出す方法について書きました。署名付き要求の取り扱い方の参考になれば幸いです。

Canvasを活用することで、Salesforceの認証を引き継ぎつつ、複雑な画面機能はAWSなどのSalesforce外部の仕組みを使って実装することが可能になります。アプリケーションの表現力が強力になりますので、検討してみてください。