どうもーキュービックでテッ
クリード をやっている尾﨑です。
本日は会計や組織データなどのマスタ管理を行なっているNetSuiteと
REST API 連携した時のお話をしたいと思います。
Komawoの全体アーキテクチャ
ス
クラッチ で連携した事例は希少だと思うので、困っている方の課題解決に繋がると嬉しいです。
今回は前回の
OAuth2.0認証 に続いて、クライアント証明書認証による
REST API 連携をご紹介したいと思います。
・OAuth2.0トークン認証によるREST API連携
・OAuth2.0クライアント認証によるREST API連携★
・トークンベース認証によるRestlet API連携
本日はOAuth2.0クライアント証明書認証によるREST API 連携方法とハマったポイントなどをご紹介したいと思います。
Komawoの前身であるCBA(CUEBiC Analytics)から先行的に
ドメイン として持つべきではないマスタをNetSuiteに退避し、REST API にて同期を行いました。
こちらのアーキテクチャ では前回のOAuth2.0認証ではリフレッシュトーク ンの有効期限が短く、本番運用では利用できないという欠点を克服しました。
アーキテクチャ
事前準備
前提
インテグレーションの発行が完了している前提になります。インテグレーションの発行方法は
前回のOAuth2.0認証 をご参照ください。
事前に以下の準備が必要です
1.クライアント証明書の発行
2.NetSuite上でのクライアント証明書の設定
1.クライアント証明書の発行
NetSuiteの公式サイト上で指定されているopen ssl で発行を行います
openssl req -x509 -newkey rsa:4096 -sha256 -keyout auth-key.pem -out auth-cert.pem -nodes-days730
上記のコマンドをターミナルなるなどで入力すると、対話形式で入力を求められます。
質問内容と解答例を示します。
質問内容
解答例(例です)
Country Name
JP
State or Province Name
Tokyo
Locality Name
Shinjyuku-ku
Organization Name
sample company
Organizational Unit Name
sample department
Common Name
sample.co.jp
Email Address
不要
上記設定値を設定していくとローカルに以下の2ファイルが生成されます
ファイル名
概要/用途
auth-cert.pem
publicファイル(CERTIFICATE) NetSuite上に登録
auth-key.pem
private key JWTの生成で使用
参考にさせていただいたリンク
自己署名証明書の作成方法
qiita.com
2.NetSuite上でのクライアント証明書の設定
次にNetSuite上にクライアント証明書を登録します
設定>インテグレーション>OAuth2.0クライアント資格証明(M2M)設定へすすむ
1.新規作成ボタンを押下
2.エンティティで紐づけたいアカウントを選択
3.ロールは:「Administrator」など高位のロールを選択
4.アプリケーション:インテグレーションで設定したものを選択
5.証明書ファイルを選択:1.クライアント証明書の発行で生成した「auth-cert.pem」を選択
6.保存ボタンを押下して問題なければOK
成功すると以下のように証明書がNetSuite上に設定されます
以下の順番で説明していきます
1.JWTの発行
2.アクセストークンの取得
1.JWTの発行
アクセストーク ンを取得するためのパラメータの「client_assertion」がJWT方式で生成しないといけません。
NetSuiteの公式で指定されているリクエス トトーク ン構造でトーク ンを発行します。
『JWTとは?』
JSON WEB TOKENの略
以下の3つの構成でカンマ区切りでトークンを発行する
・トークンヘッダー
・ペイロード
・クライアント証明のCERTIFICATE
トークンヘッダーとペイロードに認証情報を含んでおり、クライアントはその情報を元に
情報を複合して一致していた場合に認証やトークン発行を行なっている
パラメータ
設定値
typ
JWT(固定)
alg
RS256(HS256は使用できないので注意)
kid
「2.NetSuiteでのクライアント証明書の設定」で登録したクライアント証明書のID
パラメータ
設定値
iss
インテグレーションで発行したクライアントID
scope
rest_webservices
aud
NetSuiteのtokenのエンドポイント
exp
UNIX 時間で設定/トーク ンの有効期限 ※過去日付はNG ※iatより1時間以内であること
iat
UNIX 時間で設定/トーク ン発行日時 ※過去日付はNG
NetSuiteのtokenのエンドポイントの例
https://<accountID>.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token
JWTの生成の実装サンプル
公式がチラッとライブラリーが必要な旨を記載しています・・・
残念ながらJWTは普通に生成できません。
ここでは、node.jsを使用した生成例を解説します。
Node.jsのjsonwebtokenライブラリ を使用します
『選定理由』
ヘッダーとパラメーターの適合率が高かった
即席で実装できるたから
『苦労した点』
NetSuite
ライブラリー
typ
設定不要。JWTが固定で設定される
alg
algorithm
kid
keyid
NetSuite
ライブラリー
iss
iss
scope
なし
aud
audience
exp
exp
iat
設定不要:timestampが設定される
Node.jsは入っている前提になります。
以下のライブラリーをインストールします
npm install --save sleep
npm install --save @types/sleep
「creat_jwt.js」をnode.jsがあるディレクト リと同じディレクト リに作成します
var fs = require('fs' )
var jwt = require('jsonwebtoken' )
const private_key = fs.readFileSync('./auth-key.pem' , 'utf8' )
const public_key = fs.readFileSync('./auth-cert.pem' , 'utf8' )
const jwtheader = {
algorithm: 'RS256' ,
keyid: 'your kid '
} ;
const jwtPayload = {
iss : 'your iss ' ,
scope: 'rest_webservices' ,
aud: 'your audience' ,
exp: 1654498589,
} ;
const token = jwt.sign (
jwtPayload , { key: private_key ,passphrase:'passphrase here' } , jwtheader)
console.log( '=== JWT TOKEN RS256===' );
console.log(token.toString('base64 ' ));
const verified = jwt.verify(token, public_key , { algorithms: 'RS256' } , function (err, decoded) {
if (err) {
console.log(err.message);
} else {
console.log(decoded);
}
} );
トーク ンの発行と発行
トーク ンの認証確認を行います
node creat_jwt.js
成功したらこんな感じの結果が得られます(生成値は正しくないものに変えています)
=== JWT TOKEN RS256===
eyJhbGciOiJSUzI1NiIsInR5cCeyJpc3MiOiI0OTljYjM3MzkyYTM2ZjEyN2IzYjNiYjk3NDAzM2FiNmI5YjRmYzE2M2E5YTM5MjZlNjk4MDc2MDRjNzJjNjQ1Iiwic2NvcGUiOiJyZXN0X3dlYnNlcnZpY2VzIiwiYXVkIjoiaHR0cHM6Ly81Mzg1NzI3LnN1aXRldGFsay5hcGkubmV0c3VpdGUuY29tL3NlcnZpY2VzL3Jlc3QvYXV0aC9vYXV0aDIvdjEvdG9rZW4iLCJleHAMTY1NDQ5NTEzMH0.KL26d4vO827ki6zGwqKMoD4HEUxx1TNi6iGhj17-WPnKpekqxp-HFSk_rddynMPZz_vm2HBnNKxoRlQKh831QoTOB_9jcjsR33GMeMP-smgcf3NZRxVrvBUufph9XHHTmS9ZcvbAf0XQGijVzDB0GZBMmdAMUtQdkG0Lh9hmhr1IR8Mu1P7Tgys-eAfhenb-N5GbsjON-f4BeEao-dUxKvsbkphNSFsEBrO7xvRTJN2VywKwPFScFh89GVYPS1VLYaBjw4xj4_APLYCOtUKp9JPXAfoTaB1hztjQ44GdggT1k5h42cY0qO1_X6JDPDs-mvPLSuQ6kmyW5txRe217vFFoviutQS4zyy5HCEbQUA6ik7ThI7ITd4-sGiVZtaUtWzDOaNOmllrtmpV1eSRbpTHjr5ydxt_6aXgV889sFw0kgiP6zhTkCFPo7Xd6dS8fYucRbWXOzJfJG-7NB-qpKxsEvtb1VXHDNMqXspoPTT-r8Zw2RA6eqh74H0VGYUJL0_Lsll541k9df6E
{
iss: 'your iss',
scope: 'rest_webservices',
aud: 'your aud',
exp: 1654498589,
iat: 1654495130
}
2.アクセストーク ンの取得
JWTが取得できたので、ようやくアクセストーク ンの取得へと移ります。
公式で公開されているサンプル
リクエスト形式:POST
エンドポイント:https://<accountID>.app.netsuite.com/services/rest/auth/oauth2/v1/token?
POST /services/rest/auth/oauth2/v1/token HTTP/1.1
Host: <accountID>.suitetalk.api.netsuite
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiIxYzM0M2E3MTZjMWRjZWI2MGU3ZmMxNDlmYTY3MzU5MjllZjc3ZDI4ZmUxNjI5M2Y4OTI5NzZkZGU3ZDhlM2UyIiwic2NvcGUiOlsicmVzdGxldHMiLCAicmVzdF93ZWJzZXJ2aWNlcyJdLCJhdWQiOiJodHRwczovL3J1bmJveC5jb3JwLm5ldHN1aXRlLmNvbS9zZXJ2aWNlcy9yZXN0L2F1dGgvb2F1dGgyL3YxL3Rva2VuIiwiZXhwIjoxNjI3OTA5MzAzLCJpYXQiOjE2Mjc5MDU3MDN9.j7fhtd0qQP-iD7ns9q_fuG8Arz2aWJyoSvZ8sHRVA8HXOJG3pAQbT5J5F8MLkWIXA9ZuSxHdCWNwQLoRUeKlGURYFFqDHP_yjoWFWWtq5Wb-AnaZg_jBVL8TaOFGY2WByFM8rHsJVopFegwEQsU6bkcwqiFttEKxso-MiSAc5lE9SBgi6Fus2btiYGIFcNrKalFXEWDy6Ah5yVCo3wxkk9dfiPmT6JgLdjFkCc3v7tMCD9CrRHXrmhQvL8aoeyTMzJILURw5rnuy9zAs9ngymtX_iiwes8XpkBeCJbX4totI-EY4myi7L4fc2NgeWT-bvLWo6_sWjXE4BKyewqjtreUJscR9bhJ5Fi7S8nIoGDQbZrwhIgoKM_UI9Waw6kRLwRer_c0QDFY-sMLeGT3HL5vihHRFNXd-cKb-AWplkRiSJrdHXJtuGHLniHRpkK0-A1AFalIzYw4SSykxfck0qsPdf-oFPuawUsKR9lDCcYlyOaDZdQsBNsbjOsp5gGtyCuBwPBS8xz7I6gqLVEfNuzTfDDk8SMw1fN9MQ0NJtZMqMxm-WY_bLjZVkI3gqsvgDS-ADBPC7cymVZGfPUqummDUeG-Ks7SkLaHpfY6i-aZS8KUAY4aN5Do3GWT56aoEM9s1YB_1ZF_YxsBmK_gcX_mmlwUxbvCVpuHJTvKAQzY
パラメータ
項目
設定値
grant_type
client_credentials
client_assertion_type
urn:ietf :params:oauth:client-assertion-type:jwt-bearer
client_assertion
JWTの生成で発行したトーク ン
ヘッダー
項目
設定値
Content-Type
application/x-www-form-urlencoded
レスポンス
項目
設定値
access _token
アクセストーク ン
Expires_in
アクセストーク ンの有効時間常に3600秒(1時間)
token_type
常に「Bearer」
curl コマンドなどで確認してみましょう
curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=client_credentials&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion={your client_assertion}' https://{accountID}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token
成功すると以下のようなレスポンスが返ってきます(access _tokenは適当な値に直しています)
{"access_token":"eyJraWQiOiJjLjUzODU3MjcuMjAyMi0wNC0yNF8xOS00My00OCIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiIzOzgyMCIsImF1ZCI6WyIxRTZEMTI2My0wODIxLTREM0YtQkU1My1FQTk0ODVEQzY4MDM7NTM4NTcyNyIsIjQ5OWNiMTY1NDUwMDI1NSwiZXhwIjoxNjU0NTAzODU1LCJpYXQiOjE2NTQ1MDAyNTUsImp0aSI6IjUzODU3MjcuYS1jLm51bGwuMTY1NDUwMDI1NTUyNSJ9.tbpaJ8xF6Gmpy_NClWszHaSX1t3wiyICzvew9MuCbTI5ut3totCdzV6UIIa7Yyg2gbNm2eL37RGhK4NaAFwMbuxXx6snL6dXaUG5NsfODfCRD3XZIyUA7At1VUUDxycULnkVh5Mf7zGWZvvs5QwASA0itF6B0y5FZ8vpq-Spog021m6K4Y2Y9Vs-uC3nBAc4DUgkK8hFXlf_KxN2A5562mm-UyGrtjG9ZJvrFupxSHwyVQHhyxs35aykwlddbPjLUZbApS6_3GQndIkaqxrWwdFTVsACryfJfUB8F8hYNU","expires_in":"3600","token_type":"Bearer"}
はまった点
『 クライアント証明書のやってはダメなこと』
証明書の有効期限が切れている(最長2年です)
証明書を再発行した上でNS上に再度アップロードしてください
証明書に紐づくアカウントのロールが証明書設定時のものではないものに変更された
証明書を新規ロールで作り直すor元のロールを再度割り当ててください
証明書に紐づくインテグレーションが削除または変更された
インテグレーションを再設定した上で、証明書を再発行してください
『JWT(jason web token)の発行で困ったこと』
JWTのGENERSTERサイト
割とJWT界隈では有名らしいです・・・が、
ヘッダーとペイロード がライブラリーで保証されていませんでした。
NetSuiteの認証要求に耐えられないので断念しました。
トーク ン発行後にコピペしたらパラメータの複合はできたので私のやり方の問題かも・・・
『NetSuiteのinternal_idが連番ではない』
API 連携と直接関係しない話ですがNetSuiteではレコードのナンバリングにinternal_idを使用しています
このIDが曲者で、ある段階から急に100→500に飛んだりするのでマスタを退避して設定している際にRDB のIDと乖離が起きました
一括インポートでやれば大丈夫と思ったそこのあなた!新規レコードの追加時も毎回洗い替えしますか?
結果として同期する際にRDB のスキーマ にinternal_idに相当するカラムを追加する必要が出てきました。
課題
『 更新トリガー』
現状はNetSuiteの更新をトリガーに同期をしているわけではありません
ECSのタスクスケジューラで定期的にバッチ処理 を実行
任意のタイミングでWEBアプリケーション上から同期リクエス ト
対象のマスタ情報を全て取得後にRDSのデータと比較して差分があったもののみを更新
理想系
NetSuiteの更新トリガーで登録/更新レコードのみが同期される
次回はこちらの課題も考慮した「トークンベース認証によるRestlet API連携 」をご紹介します。