🪪
ServiceNow のサーバースクリプトから AWS SigV4 署名付きリクエストを生成するクラス実装
TL;DR
- ServiceNow サーバースクリプトから AWS API (Lambda/S3/SES 等) を直接呼ぶための再利用可能クラス
X_AWSSigV4RequestBuilderを実装する。 - 必須項目 (method / service / region / endpoint / credentialId) を渡すだけで SigV4 署名済み
sn_ws.RESTMessageV2を取得または即時実行できる。
はじめに
ServiceNow のサーバースクリプトから AWS の REST API (S3, Lambda, SES など) を直接呼びたいとき、AWS Signature Version 4 (以降 AWS SigV4) の署名生成が課題になります。
本記事では単一クラス X_AWSSigV4RequestBuilder を使い、再利用しやすい形で sn_ws.RESTMessageV2 に署名ヘッダーを組み込み、即時実行まで行う実装例を紹介します。
背景と課題
今回 AWS Lambda 関数 URL を AWS_IAM 認証を用いて ServiceNow のサーバースクリプトから呼び出すニーズがあり、実装に取り掛かりました。
ServiceNow のドキュメント Amazon 署名ベースのカスタムアルゴリズムの設定
には、署名アルゴリズムを追加方法、アクセスキー ID とシークレットアクセスキーの保存方法、フローデザイナーの REST アクションで SigV4 署名を行う方法が記載されています。
しかし、今回実現したかったサーバースクリプト (Script Include やビジネスルール) から直接呼び出す場合の具体的な手順は記載されておらず、実装に悩みました。
今後、同様のニーズを抱える ServiceNow 開発者の参考になればと考え、記事にまとめました。
実装のゴール
- 必須パラメータを渡すだけで手軽に署名済み
RESTMessageV2を取得もしくは実行できる
前提
- ServiceNow Zurich バージョンで動作を確認
- 事前に署名アルゴリズム
AWS Signature Version 4を作成済み - 事前にアクセスキー ID とシークレットアクセスキーを認証情報として保存済み
全体像
コード
S3 を想定した記述を含みますが、Lambda への GET/POST でのみ動作確認しています。
コード
/**
* AWS Signature Version 4 認証付き RESTMessageV2 ビルダー
*/
var X_AWSSigV4RequestBuilder = Class.create();
X_AWSSigV4RequestBuilder.prototype = {
/**
* @param {Object} params
* @param {sn_ws.RESTMessageV2} params.restMessage - RESTMessageV2 オブジェクト
* @param {String} params.method - HTTP メソッド (GET, POST, PUT, DELETE)
* @param {String} params.service - AWS サービス名 (例: "lambda", "s3")
* @param {String} params.region - AWS リージョン (例: "ap-northeast-1")
* @param {String} params.endpoint - エンドポイント URL (例: "https://<YOUR_LAMBDA_URL>.lambda-url.ap-northeast-1.on.aws/")
* @param {String} params.credentialId - 認証情報の sys_id
* @param {String} [params.content] - リクエストボディ(省略時は空文字列)
* @param {Object} [params.queryParams] - クエリパラメータのキー・バリューオブジェクト
*/
initialize: function (params) {
this.params = params || {};
this.restMessage = this.params.restMessage || new sn_ws.RESTMessageV2();
this.method = this.params.method.toUpperCase();
this.service = this.params.service;
this.region = this.params.region;
this.endpoint = this.params.endpoint;
this.host = this._parseUrl(this.endpoint).host;
this.credentialId = this.params.credentialId;
this.content = this.params.content || "";
this.queryParams = this.params.queryParams || {};
this.httpRequestData = null;
this.signature = null;
this._isJsonBody = false; // 署名後に Content-Type を追加するためフラグ保持
},
/**
* RESTMessageV2 オブジェクトをビルドして返します。
* @return {sn_ws.RESTMessageV2} 署名付き RESTMessageV2 オブジェクト
*/
build: function () {
// 1. パラメータ検証
this._validateParams();
// 2. HttpRequestData オブジェクトを構築
this._buildHttpRequestData();
// 3. 署名付きリクエストを生成
this._generateSignature();
// 4. RESTMessageV2 を構築
this._buildRestMessage();
return this.restMessage;
},
/**
* 即時実行
* @return {sn_ws.RESTResponseV2} RESTResponseV2 応答オブジェクト
*/
execute: function () {
var restMessage = this.build();
var response = restMessage.execute();
return response;
},
/**
* @private
* URLを解析してホストとパスを抽出します。
* @param {String} url - 解析するURL
* @returns {Object} ホストとパスを含むオブジェクト e.g., { host: "example.com", path: "/api" }
*/
_parseUrl: function (url) {
var match = url.match(/^(https?:\/\/)?([^\/]+)(\/.*)?$/);
if (!match) {
throw new Error("Invalid URL format: " + url);
}
return {
host: match[2],
path: match[3] || "/",
};
},
/**
* @private
* 必須パラメータの検証
* @throws {Error} 必須パラメータが不足している場合
*/
_validateParams: function () {
if (!this.method) {
throw new Error("パラメータ method が指定されていません。");
}
if (!this.service) {
throw new Error("パラメータ service が指定されていません。");
}
if (!this.region) {
throw new Error("パラメータ region が指定されていません。");
}
if (!this.endpoint) {
throw new Error("パラメータ endpoint が指定されていません。");
}
if (!this.credentialId) {
throw new Error("パラメータ credentialId が指定されていません。");
}
},
/**
* @private
* HttpRequestData オブジェクトを構築します。
*/
_buildHttpRequestData: function () {
this.httpRequestData = new sn_auth.HttpRequestData();
this.httpRequestData.setHttpMethod(this.method);
this.httpRequestData.setService(this.service);
this.httpRequestData.setRegion(this.region);
this.httpRequestData.setEndpoint(this.endpoint);
this.httpRequestData.setHost(this.host);
// クエリパラメータ設定
var queryKeys = Object.keys(this.queryParams);
if (queryKeys.length > 0) {
queryKeys.sort();
for (var i = 0; i < queryKeys.length; i++) {
var qKey = queryKeys[i];
var rawVal = this.queryParams[qKey];
// 値が undefined / null の場合は空文字列扱い
var qVal = rawVal === undefined || rawVal === null ? "" : String(rawVal);
this.httpRequestData.addQueryParam(qKey, qVal);
}
}
// ボディ設定 (POST / PUT の場合のみ)
if (this.method === "POST" || this.method === "PUT") {
var body = this.content;
if (body === null || body === undefined) {
// null / undefined は空文字列
body = "";
} else if (typeof body === "object") {
// オブジェクトは JSON 文字列化
try {
body = JSON.stringify(body);
this._isJsonBody = true;
} catch (e) {
throw new Error("JSON stringify に失敗しました: " + e.message);
}
} else {
// その他は文字列化
body = String(body);
// 既に JSON 文字列っぽい場合 (単純判定) は application/json を付与
if ((body.charAt(0) === "{" && body.charAt(body.length - 1) === "}") || (body.charAt(0) === "[" && body.charAt(body.length - 1) === "]")) {
this._isJsonBody = true;
}
}
this.content = body;
this.httpRequestData.setContent(body);
}
// S3の場合、x-amz-content-sha256 ヘッダーを追加
if (this.service === "s3") {
var sha256 = new sn_auth.SHA256();
var payloadHash =
this.content && this.content.length > 0 ? sha256.hashString(this.content) : "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; // 空のペイロードのSHA256ハッシュ
this.httpRequestData.addHeader("x-amz-content-sha256", payloadHash);
}
},
/**
* @private
* 署名付きリクエストを生成します。
*/
_generateSignature: function () {
// 認証情報を取得
var credential = new sn_cc.StandardCredentialsProvider().getAuthCredentialByID(this.credentialId);
// RequestAuthAPIオブジェクトを作成し、リクエストに署名します
var signingAPI = new sn_auth.RequestAuthAPI(this.httpRequestData, credential);
this.signature = signingAPI.generateAuth();
},
/**
* @private
* RESTMessageV2 オブジェクトを構築します。
*/
_buildRestMessage: function () {
this.restMessage.setHttpMethod(this.method);
this.restMessage.setEndpoint(this.endpoint);
// クエリパラメータ設定
var queryMap = this.httpRequestData.getQueryParamMap();
for (var key in queryMap) {
this.restMessage.setQueryParameter(key, queryMap[key]);
}
// ボディ設定
this.restMessage.setRequestBody(this.httpRequestData.getContent());
// ヘッダー設定
var headerMap = this.signature.getHeaderMap();
for (var hKey in headerMap) {
this.restMessage.setRequestHeader(hKey, headerMap[hKey]);
}
// 署名後に Content-Type を追加(署名対象ヘッダーに含めない)
this.restMessage.setRequestHeader("Content-Type", this._isJsonBody ? "application/json" : "text/plain");
},
type: "X_AWSSigV4RequestBuilder",
};
使用方法
GET
try {
var builder = new X_AWSSigV4RequestBuilder({
method: "GET",
service: "lambda",
region: "ap-northeast-1",
endpoint: "https://<YOUR_LAMBDA_URL>.lambda-url.ap-northeast-1.on.aws/",
credentialId: "<your_cred_sys_id>",
queryParams: { key: "value" },
});
var restMessage = builder.build();
var response = restMessage.execute();
gs.print("Response Status: " + response.getStatusCode());
gs.print("Response Body: " + response.getBody());
} catch (e) {
gs.print("Error: " + e.message);
}
POST
try {
var builder = new X_AWSSigV4RequestBuilder({
method: "POST",
service: "lambda",
region: "ap-northeast-1",
endpoint: "https://<YOUR_LAMBDA_URL>.lambda-url.ap-northeast-1.on.aws/",
credentialId: "<your_cred_sys_id>",
content: { key: "value" }
});
var response = builder.execute(); // build + execute 一括
gs.print("Response Status: " + response.getStatusCode());
gs.print("Response Body: " + response.getBody());
} catch (e) {
gs.print("Error: " + e.message);
}
Discussion