CUEBiC TEC BLOG

キュービックTECチームの技術ネタを投稿しております。

NetSuiteとAPI連携してみた_その2(クライアント証明書認証編)

どうもーキュービックでテックリードをやっている尾﨑です。 本日は会計や組織データなどのマスタ管理を行なっている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上に設定されます

APIの設定

以下の順番で説明していきます

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, //The value must not be more than one hour greater than the value of the iat parameter. };

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) { // 認証NGの場合 console.log(err.message); } else { // 認証OKの場合 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連携」をご紹介します。