1. はじめに
コインチェック株式会社(以下、コインチェック)の AI 活用推進を担当している河石です。これまで情シス部門に所属してきましたが、この 4 月から AI 活用推進をミッションとするコーポレートインテリジェンス部 Applied AI グループで働くことになりました。
直近の取り組みとして、すべての業務システムに AI が接続できる環境の提供を目指しており、すでに 10 数個の MCP サーバーを社内に構築・提供しています。Workato には Slackbot や MCP をユーザー本人の権限で動かせる Verified User Access(VUA) といった機能が揃っているため、多くの MCP は簡単に構築できます。
本記事では、その中で出会った以下の問題について、Workato を使った解決方法を紹介します。
社内の LLM エージェント向けに、社内 SaaS を呼び出す MCP サーバーを公開したい。しかし、その SaaS が発行できるのは テナント全体を読める管理者 API キー だけ。これをそのまま MCP に渡すと、誰が呼んでも全社員分のデータが見えてしまう。
弊社では普段から Workato を全社の連携基盤として運用しており、この問題を Workato API Platform の OIDC 機能を組み合わせて解決しました。実装上のハマりどころも含めて整理します。
これから同じような構成を組もうとしている方の参考に少しでもなれば幸いです。
2. 解こうとしている課題
2.1 MCP はユーザー権限スコープでの操作を要求する
Model Context Protocol (MCP) を使うと、LLM エージェントが社内 SaaS を「ツール」のように呼び出せます。社内に MCP サーバーを立てて全社員に開放することを考えたとき、私たちが期待するのは次のような挙動です。
- 部長が「自分の今月の経費精算を引いて」と LLM に頼んだら、本人の経費だけが返ってくる
- 一般社員が同じ MCP を使っても、見える範囲は本人スコープに自動で限定される
つまり「呼び出した本人の権限で、本人が見える情報だけが返る」状態です。
2.2 ところが管理者用 API キーしか発行できない SaaS もある
業務系の SaaS の中には、テナント全体のデータにアクセスできる管理者 API キーしか発行できないものがあります。この場合、API キーには 個別ユーザーのスコープという概念が存在せず、テナントの全データを引ける万能キーが 1 本発行されるだけです。
この管理者 API キーをそのまま MCP サーバーに埋め込むとどうなるか、シナリオで並べてみます。
| シナリオ | 結果 |
|---|---|
| 部長が自分の経費を聞く | 自分以外の経費もすべて引ける状態(API 側は誰の頼みかわからない) |
| 一般社員が同じ MCP を使う | 同上。役員の経費も含めて引ける |
これでは業務利用には出せません。根本の問題は、呼び出し元ユーザーを SaaS の API キーに伝える経路がそもそも存在しない ことにあります。
3. うまくいかないアプローチ
単純な対策として、アプリ側で ?email=user@example.com のようにユーザーを指定してフィルタする方法が考えられます。しかし、これには次の問題があります。
- email パラメータを偽装すれば誰の情報でも引けてしまう
- LLM が誤った email を送ってもそのまま通る
ユーザーの identity が API キーと一緒に運ばれていない 以上、?email= をクライアント側で付けても保証にはなりません。
4. 解決パターン: 認証境界を 2 つに分ける
解決策は 認証境界を 2 段に分ける ことです。「呼び出し元が誰かを確認する処理」と「SaaS を呼び出す管理者キー」を別々の層に分離し、利用者側には管理者キーを一切触らせません。
全体の流れは次のようになります。

構成は 3 つの層に分かれます。この後の実装章は、それぞれの層に対応しています。
- 外側(User-facing Custom Connector / 7 章): MCP クライアントに公開する窓口です。IdP の OIDC でユーザーを認証し、本人を示すトークンを受け取ります
- 認証境界(Workato API Platform): 受け取ったトークンを検証し、正しくないものは弾きます。本人確認と「どこまで許可するか」の判断をここに集約します
- 内側(Recipe + Admin API Connector / 5・6 章): API Platform の奥に隠れる層です。SaaS を呼び出す管理者キーはここだけが保持し、外部からは参照できません
つまり、利用者から見える 外側 はユーザー認証だけを担い、SaaS を呼ぶ管理者キーは 内側 に閉じ込められます。両者の境目でトークンを検証するのが Workato API Platform です。
5. Recipe(内側)の実装
認証境界を分ける枠組みが決まったので、内側の recipe で何をするかを具体に落としていきます。 代表的な例として 勤怠管理 SaaS を考えてみます。
GET /attendance/v1/adits?employee_id=...&date=...のように、ユーザー単位でクエリできる- 「ユーザーが見える範囲」は SaaS 側で正しく評価される(自分の打刻は自分だけが見える、など)
この場合、API endpoint を実装する recipe は最小限で済みます。
API endpoint (受信): - JWT claim から email を取り出す ↓ Admin API Connector を呼ぶ - admin key で /master/v1/employees?email=<from-jwt> を呼び出して employee_id 解決 - admin key で /attendance/v1/adits?employee_id=<id>&date=<from-input> を呼び出す - レスポンスをそのまま返す
入力の date だけは MCP クライアントから受け取り、email は 必ず JWT から取り出します。これによって「自分以外の打刻を見る」操作はそもそも不可能になります。Workato 上では、recipe のトリガーが受け取った JWT の claim がデータピルとして並び、そこから email を後続ステップの入力に割り当てる形になります。

逆に、SaaS API が「ユーザー単位で絞り込む手段」をそもそも持っていない場合は、recipe 内でフィルタを完結させる別の工夫が必要になります。SaaS ごとに最適解が変わるため、その話は本記事の範囲外とします。本記事の焦点は 認証境界の分け方 であり、内側 recipe の中身はこの枠組みとは独立して設計できます。
6. Admin API Connector(内側)の実装
上記 recipe が呼ぶ Admin API Connector も、Workato の Connector SDK で書きます。管理者 API キーを保持するのはこのコネクタだけで、外側には一切露出させません。base_uri は外側コネクタと違い、SaaS 本体の URL を指します。
{
title: 'SaaS X (Admin)',
connection: {
fields: [
# 管理者 API キーを接続情報として持つ
{ name: 'admin_api_key', label: 'Admin API key',
control_type: 'password', optional: false }
],
# 1. 全リクエストに管理者キーを付与
authorization: {
type: 'custom_auth',
apply: lambda do |connection|
headers(Authorization: "Bearer #{connection['admin_api_key']}")
end
},
# 2. base_uri は SaaS 本体(外側コネクタは Workato を指していたのと対照的)
base_uri: lambda do |_connection|
'https://api.saasx.example.com'
end
},
actions: {
# 3. JWT 由来の email から employee_id を解決する
find_employee_by_email: {
input_fields: lambda do |_object_definitions|
[{ name: 'email', optional: false }]
end,
execute: lambda do |_connection, input|
employee = get('/master/v1/employees', email: input['email'])
.dig('employees', 0)
error("employee not found: #{input['email']}") if employee.blank?
employee
end,
output_fields: lambda do |_object_definitions|
[{ name: 'employee_id' }, { name: 'email' }, { name: 'name' }]
end
},
# 4. 解決した employee_id で打刻を引く
get_attendance: {
input_fields: lambda do |_object_definitions|
[{ name: 'employee_id', optional: false },
{ name: 'date', optional: false }]
end,
execute: lambda do |_connection, input|
get('/attendance/v1/adits',
employee_id: input['employee_id'],
date: input['date'])
end,
output_fields: lambda do |_object_definitions|
[{ name: 'adits', type: 'array', of: 'object' }]
end
}
}
}
ポイントは、このコネクタが受け取る email は 必ず内側 recipe が JWT から取り出した値であり、MCP クライアントから直接渡せる経路がないことです。管理者キーで何でも引ける強力なコネクタですが、その入力は JWT から取り出した本人の email に固定されるため、「自分以外のデータを引く」操作は成立しません。
7. カスタムコネクタ(外側)の実装
外側の User-facing Custom Connector は、Workato の Connector SDK で書きます。骨組みは次のような形になります。
{
title: 'SaaS X (User)',
connection: {
authorization: {
type: 'oauth2',
# 1. IdP の Custom Authorization Server に Authorization Code Flow で投げる
authorization_url: lambda do |_connection|
'https://<tenant>.idp.example.com/oauth2/<auth-server-id>/v1/authorize' \
'?response_type=code&scope=openid+email+offline_access'
end,
# 2. token endpoint
token_url: lambda do |_connection|
'https://<tenant>.idp.example.com/oauth2/<auth-server-id>/v1/token'
end,
# 3. SDK の規約: authorization_url に client_id / redirect_uri を埋めると
# SDK の override が効かなくなるため、必ず別 lambda で返す
client_id: lambda { account_property('saasx_client_id') },
client_secret: lambda { account_property('saasx_client_secret') },
# 4. acquire: 認可コード → トークン交換
acquire: lambda do |_connection, auth_code, redirect_uri, _verifier|
basic = Base64.strict_encode64("#{account_property('saasx_client_id')}:" \
"#{account_property('saasx_client_secret')}")
post('https://<tenant>.idp.example.com/oauth2/<auth-server-id>/v1/token').
params(grant_type: 'authorization_code', code: auth_code,
redirect_uri: redirect_uri,
scope: 'openid email offline_access').
headers(Authorization: "Basic #{basic}").
request_format_www_form_urlencoded
end,
# 5. apply: 取れた access_token を全リクエストの Authorization に付与
apply: lambda do |_connection, access_token|
headers(Authorization: "Bearer #{access_token}")
end,
refresh_on: [401],
refresh: lambda do |_connection, refresh_token| ... end
},
# 6. base_uri は Workato API Platform のエンドポイント
# SaaS の本体 URL ではないことに注意
base_uri: lambda do |_connection|
'https://apim.workato.com/<workspace-slug>/saasx-api-v1/'
end
},
# 7. action: 通常通りの REST 呼び出し
actions: {
get_my_records: {
execute: lambda do |_connection, input|
get('records').params(date: input['date'])
# ↑ ここで Authorization ヘッダーに OIDC JWT が乗る
# API Platform 側 recipe が JWT から email を取り出してフィルタする
end
}
}
}
外側 connector のポイントは base_uri が SaaS ではなく Workato API Platform を指している ことです。クライアントから見ると単に SaaS X を呼び出しているように見えますが、実体は Workato 上の薄いラッパーです。
8. Workato API Platform の設定
内側 recipe と外側 connector のコードが揃ったら、両者をつなぐ Workato API Platform 側を設定します。ここが認可の中心になります。手順は大きく 4 ステップです。
なお、Workato 側の OIDC 設定(Discovery URL の登録から API key の JWT claim への埋め込みまで)の公式手順は OpenID Connect | Workato Docs にまとまっています。本記事では要点と実装上の注意に絞って説明します。
8.1 recipe を REST endpoint として公開する
まず内側 recipe を API として外から呼び出せる状態にします。Platform → API platform → API collections で API Collection を作り、recipe を API Endpoint として紐付けます。ここで決まる base path(例: /saasx-api-v1/)が、外側 connector の base_uri に対応します。

8.2 OIDC Client を作成する
次に「誰が呼んでいるか」を識別する OIDC Client を作ります。Platform → API platform → Clients → New client で作成し、認証方式に OIDC を選びます。

8.3 Discovery URL と検証ルールを設定する
作成した client の Access configuration → Authentication method → Edit で、Okta の Authorization Server が公開している Discovery URL(.well-known/openid-configuration)を貼り付けます(9.1 で取得します)。これを設定するだけで、JWKS 取得・署名検証・claim 照合までを Workato 側が自動で行います。

8.4 API key を確認する
client 詳細画面の "API keys" タブ に表示される文字列が、この client を一意に示す API key です。この値を次の Okta 設定(9.2)で JWT の claim に埋め込みますので、控えておきます。

最後に Access profile / policy で、この client から先ほどの API Collection へのアクセスを許可すれば Workato 側は完了です。
9. Okta の設定
最後に IdP(ここでは Okta)側で、ユーザー認証と「API key を JWT に載せる」設定を行います。
9.1 Custom Authorization Server を用意する
Security → API → Authorization Servers で Custom Authorization Server を作成(または既存のものを利用)します。ここで発行される metadata の URL が、先ほど Workato の Discovery URL 欄に貼ったものです。audience は Workato 側で設定した値と一致させます。

9.2 API key を Custom Claim として埋め込む
Authorization Server の Claims → Add Claim で、Workato の "API keys" タブで控えた文字列を 静的リテラルとして claim 値に設定します。
- Name:
sub(Workato が照合する claim 名) - Include in token type: Access Token
- Value type: Expression
- Value:
'wrk_xxxxxxxx...'← シングルクォートで括った静的リテラル

9.3 OIDC アプリ(Application)を作成する
外側 connector が Authorization Code Flow で認証するためのアプリを Applications → Create App Integration で作成します。
- Sign-in method: OIDC
- Grant type: Authorization Code(+ Refresh Token)
- Sign-in redirect URI: 外側 connector の callback URL(Workato が払い出す
https://www.workato.com/oauth/callback等) - Scopes:
openid/email/offline_access
ここで発行される client ID / client secret を、connector 側の account_property('saasx_client_id') / ('saasx_client_secret') に設定すれば、エンドツーエンドでつながります。

10. おわりに
認証境界を 2 つに分けることで、管理者キーしか提供しない SaaS でも、ユーザー単位での操作を MCP / アプリから安全に公開できます。SaaS ごとの差分は内側 recipe の実装に閉じ込められ、外側のカスタムコネクタ・Workato API Platform 設定・IdP 設定は SaaS が変わってもほぼ流用できます。
MCP の普及で「ユーザー権限スコープでの操作」が当たり前になりつつある今、admin key しか出さない SaaS に対しても安全に向き合う手段は揃ってきました。Workato と OIDC IdP がすでに動いている組織であれば、新しいインフラを増やすことなくこの構成に乗せられるはずです。
ただし、これはあくまで応急処置です。AI エージェントが当たり前になっていく時代を見据えると、SaaS 側でもユーザー単位のスコープを持つ API 認証に対応してほしいというのが本音ですが、現時点ではまだそこまで整備されていないのが実情です。そのギャップを埋める手段として、今回紹介した Workato を使ったアプローチが当面は有効に機能するので、ぜひ試してみてください。