Zendesk ZIS で固定認証の外部連携を OAuth へ中継してみた
はじめに
Zendesk API トークンを OAuth へ移行するとき、呼び出し元が自作プログラムなら、client_credentialsでアクセストークンを取得して期限ごとに更新できます。一方、管理画面に固定の認証情報を入力するだけの SaaS など、トークンの取得処理を実装できない呼び出し元もあります。
この条件では、期限付きのアクセストークンを固定値として貼り付けても、期限切れ後に連携が止まります。そこで、外部サービスからのリクエストを Zendesk Integration Services (ZIS) の inbound webhook で受け、ZIS が管理する OAuth connection で Zendesk API を呼ぶ構成を試しました。
この構成で ZIS が保持する Zendesk 用 OAuth connection のアクセストークンは、期限切れせず、refresh token も使いません。期限のたびに ZIS がトークンを更新する構成ではなく、初回に管理者が認証して作成した connection を継続して使います。そのため、外部サービス側にトークンの取得や更新を実装する必要がありません。
結果として、外部から固定 Basic 認証でイベントを送り、検証用チケットへ非公開コメントを 1 件追加できました。本記事では、検証に使った最小構成を紹介します。
対象読者
- Zendesk API トークンから OAuth への移行を検討している方
- 呼び出し元に OAuth トークンの取得処理を実装できない方
- ZIS inbound webhook と OAuth connection の組み合わせを試したい方
参考
- Using ZIS inbound webhooks
- Building your first ZIS integration
- Creating and managing OAuth connections
- ZIS custom actions
- Inbound Webhooks API
構成
外部サービスには、inbound webhook の作成時に返る URL、ユーザー名、パスワードを設定します。外部サービスは固定 Basic 認証でイベントを送るだけです。イベントを受けた ZIS は job spec に対応する flow を開始し、OAuth connection を使って Zendesk Support API を呼びます。
検証には Zendesk の検証用テナントと、初期コメントを 1 件持つ新規チケットを使いました。移行時の初期構築として、bundle と job spec の投入には管理者 API トークンを使いました。一方、構築後のチケット更新では API トークンを使わず、OAuth connection を使います。ゴールは、一意な本文を持つ非公開コメントを、イベント送信前の 0 件から送信後の 1 件へ増やすことです。
ZIS OAuth トークンの取得
ここでは、ZIS integration が登録済みであることを前提とします。
ZIS_INTEGRATIONには任意の文字列ではなく、integration を登録したときの名前を指定します。
export ZENDESK_SUBDOMAIN='your_subdomain'
export ZENDESK_ADMIN_EMAIL='admin@example.com'
export ZENDESK_API_TOKEN='your_api_token'
export ZIS_INTEGRATION='your_zis_integration'
export TICKET_ID='12345'
export STAMP=$(date -u +%Y%m%d%H%M%S)
ZIS integration を登録すると、zis_${ZIS_INTEGRATION}という識別子の OAuth クライアントが自動生成されます。
zis_client_id=$(curl -sS \
"https://${ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/oauth/clients.json" \
-u "${ZENDESK_ADMIN_EMAIL}/token:${ZENDESK_API_TOKEN}" \
| jq -er --arg identifier "zis_${ZIS_INTEGRATION}" \
'[.clients[] | select(.identifier == $identifier)]
| if length == 1 then .[0].id else error("ZIS client count must be 1") end')
取得した ID を使い、ZIS API の呼び出しに使う OAuth トークンを発行します。
token_response=$(curl -sS -X POST \
"https://${ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/oauth/tokens.json" \
-u "${ZENDESK_ADMIN_EMAIL}/token:${ZENDESK_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"token\":{\"client_id\":${zis_client_id},\"scopes\":[\"read\",\"write\"]}}")
export ZIS_ACCESS_TOKEN=$(echo "$token_response" | jq -er '.token.full_token')
export ZIS_ACCESS_TOKEN_ID=$(echo "$token_response" | jq -er '.token.id')
unset token_response
OAuth connection の作成
zendeskという名前の OAuth connection を作ります。
まず、OAuth の開始 API を呼び出します。
response=$(curl -X POST \
"https://${ZENDESK_SUBDOMAIN}.zendesk.com/api/services/zis/connections/oauth/start/${ZIS_INTEGRATION}" \
-H "Authorization: Bearer ${ZIS_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"zendesk\",
\"oauth_client_name\": \"zendesk\",
\"oauth_url_subdomain\": \"${ZENDESK_SUBDOMAIN}\",
\"origin_oauth_redirect_url\": \"https://example.local\",
\"permission_scopes\": \"read write\",
\"allow_offline_access\": false
}")
echo "$response" | jq -r '.redirect_url'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 666 0 425 100 241 321 182 0:00:01 0:00:01 --:--:-- 503
https://zis.zendesk.com/api/services/zis/connections/oauth/start_redirect?flow_token=ey****
出力された URL を管理者のブラウザで開き、画面の案内に従って管理者として認証します。認証が完了すると、ZIS は OAuth アクセストークンを connection として保持し、ブラウザを https://example.local へ戻します。example.localは動作確認用のダミー URL なので、ブラウザには接続エラーが表示されます。

これは想定どおりです。ブラウザの表示ではなく、次の API で zendesk connection が作成されたことを確認します。
curl \
"https://${ZENDESK_SUBDOMAIN}.zendesk.com/api/services/zis/connections/${ZIS_INTEGRATION}?name=zendesk" \
-H "Authorization: Bearer ${ZIS_ACCESS_TOKEN}" \
| jq '{name, permission_scope, token_type}'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 869 0 869 0 0 1712 0 --:--:-- --:--:-- --:--:-- 1714
{
"name": "zendesk",
"permission_scope": "read write",
"token_type": "bearer"
}
bundle のアップロード
今回使った bundle は、チケットへ非公開コメントを追加する Action、その Action を呼ぶ Flow、inbound webhook のイベントを Flow へ結び付ける JobSpec の 3 リソースで構成します。
{
"name": "ZIS inbound OAuth bridge __STAMP__",
"description": "Add one private comment to a disposable verification ticket",
"zis_template_version": "2019-10-14",
"resources": {
"blogcomment": {
"type": "ZIS::Action::Http",
"properties": {
"name": "blogcomment",
"definition": {
"method": "PUT",
"path": "/api/v2/tickets/__TICKET_ID__.json",
"connectionName": "zendesk",
"headers": [{"key": "Content-Type", "value": "application/json"}],
"requestBody": {
"ticket": {
"comment": {"body.$": "$.comment_body", "public": false}
}
}
}
}
},
"blogflow": {
"type": "ZIS::Flow",
"properties": {
"name": "blogflow",
"definition": {
"StartAt": "AddPrivateComment",
"States": {
"AddPrivateComment": {
"Type": "Action",
"ActionName": "zis:__INTEGRATION__:action:blogcomment",
"Parameters": {"comment_body.$": "$.input.comment_body"},
"End": true
}
}
}
}
},
"blogjob": {
"type": "ZIS::JobSpec",
"properties": {
"name": "blogjob",
"event_source": "blog_zis_e2e___STAMP__",
"event_type": "comment_requested",
"flow_name": "zis:__INTEGRATION__:flow:blogflow"
}
}
}
}
プレースホルダーへ値を入れた JSON を生成します。
jq \
--arg stamp "$STAMP" \
--arg ticket_id "$TICKET_ID" \
--arg integration "$ZIS_INTEGRATION" \
'walk(
if type == "string" then
gsub("__STAMP__"; $stamp)
| gsub("__TICKET_ID__"; $ticket_id)
| gsub("__INTEGRATION__"; $integration)
else .
end
)' bundle.template.json > rendered-bundle.json
生成したファイルをアップロードし、job spec をインストールします。
curl -X POST \
"https://${ZENDESK_SUBDOMAIN}.zendesk.com/api/services/zis/registry/${ZIS_INTEGRATION}/bundles" \
-u "${ZENDESK_ADMIN_EMAIL}/token:${ZENDESK_API_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary @rendered-bundle.json
curl -X POST \
"https://${ZENDESK_SUBDOMAIN}.zendesk.com/api/services/zis/registry/job_specs/install?job_spec_name=zis:${ZIS_INTEGRATION}:job_spec:blogjob" \
-u "${ZENDESK_ADMIN_EMAIL}/token:${ZENDESK_API_TOKEN}"
inbound webhook の作成
job spec と同じ event_source と event_type を指定します。
curl -X POST \
"https://${ZENDESK_SUBDOMAIN}.zendesk.com/api/services/zis/inbound_webhooks/generic/${ZIS_INTEGRATION}" \
-H "Authorization: Bearer ${ZIS_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"source_system\": \"blog_zis_e2e_${STAMP}\",
\"event_type\": \"comment_requested\"
}"
レスポンスの path、username、password を外部サービスの設定に使用します。
動作確認
作成時に返された値を環境変数へ設定し、inbound webhook へイベントを送ります。
export ZIS_WEBHOOK_PATH='response_path'
export ZIS_WEBHOOK_USERNAME='response_username'
export ZIS_WEBHOOK_PASSWORD='response_password'
curl -X POST \
"https://${ZENDESK_SUBDOMAIN}.zendesk.com${ZIS_WEBHOOK_PATH}" \
-u "${ZIS_WEBHOOK_USERNAME}:${ZIS_WEBHOOK_PASSWORD}" \
-H "Content-Type: application/json" \
-d "{\"comment_body\":\"ZIS_BLOG_E2E_${STAMP}\"}"
POST は 200 を返しました。5 秒後にチケットのコメント一覧を確認すると、送信した本文と完全一致する非公開コメントが 1 件追加されていました。
| 確認項目 | 結果 |
|---|---|
| inbound webhook への POST | 200 |
| 一意な本文の一致数 | 0 件から 1 件 |
| 追加されたコメント ID | 1 件 |
追加コメントの public |
false |
この結果から、外部サービスは固定 Basic 認証のまま、ZIS 内部の OAuth connection を使って Zendesk API を呼べることを確認できました。
まとめ
固定 Basic 認証しか設定できないケースでも、ZIS inbound webhook を中継して Zendesk API を OAuth で呼び出せました。Zendesk 用 OAuth connection のアクセストークンは期限切れしないため、外部サービス側にトークンの取得や更新を実装する必要はありません。なお、本番環境では、呼び出し元が対応している場合 OAuth や署名付き webhook を優先してください。






