サーバーから外部APIを呼び出す処理で、API keyを固定文字列として埋め込むと、権限の分離、期限管理、失効、監査が難しくなります。OAuth 2.0のClient Credentials Grantを使うと、利用者の操作を介さず、登録済みclientが自分の資格情報で短期間のaccess tokenを取得できます。
ただし、client_idとclient_secretを送ってtokenを受け取るだけでは安定した連携になりません。scope、期限、cache、再取得、401・403の処理、秘密情報の保存場所までを一つのデータ契約として設計する必要があります。
Client Credentialsが解決すること
RFC 6749のClient Credentials Grantは、clientが自分自身として保護されたresourceへアクセスする用途に使います。人のログインや同意画面を必要としません。
代表的な利用例は次のとおりです。
- 夜間処理が部品マスターAPIを取得する
- CAD変換serverがfile保管APIへ結果を登録する
- ETL処理がクラウド上のデータを定期同期する
- 社内service同士が権限を限定して通信する
この方式はsecretを安全に保持できるconfidential client専用です。ブラウザ内JavaScript、配布するdesktop application、mobile appなど、利用者がsecretを取り出せる環境へ固定secretを埋め込む用途には適しません。
OAuth 2.0はauthorizationの枠組みです。Client Credentialsでは、access tokenが「人のログイン状態」ではなく、clientへ許可された権限を表します。
3つの登場主体を分ける
最小構成には、次の3つがあります。
| 主体 | 役割 | 保持するもの |
|---|---|---|
| Client | APIを利用する処理 | client ID、secret参照、必要scope |
| Authorization Server | clientを認証してtokenを発行 | client登録、許可scope、token policy |
| Resource Server | Bearer tokenを検証して業務APIを提供 | resource、scope・audience検証規則 |
Authorization ServerとResource Serverは同じ製品内に存在する場合もありますが、役割は別です。token取得先へ業務データを送ったり、Resource Serverへclient secretを送ったりしないようにします。
入力からAPI呼び出しまでの流れ
サーバー間連携は次の順序になります。
client設定を読み込む
↓
利用可能なaccess tokenがあるか確認
↓ ない・期限が近い
token endpointへClient Credentials request
↓
token responseの構造・型・scopeを検証
↓
期限付きでmemory cache
↓
Authorization headerでResource Serverを呼ぶ
↓
401・403・一時障害を分類して処理
毎回tokenを取得すると、token endpointの負荷やrate limit、障害点が増えます。発行済みtokenを期限内で再利用し、期限直前だけ再取得します。
Token requestのインターフェース契約
RFC 6749では、Client Credentialsのrequest bodyをapplication/x-www-form-urlencoded、文字コードをUTF-8としています。grant_typeはclient_credentials、scopeは任意です。
POST /token HTTP/1.1
Host: auth.example.com
Authorization: Basic <client credentials>
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=assets.read%20drawings.write
ここで示すendpointとscope名は説明用です。実装時は接続先の公式仕様、Authorization Server Metadata、登録内容を確認します。client認証方式もclient_secret_basic、証明書、署名付きassertionなどがあり、独自判断で混在させません。
scopeは空白区切りの文字列として送るのがOAuth 2.0の基本形です。必要以上のscopeを要求せず、読取処理には書込権限を与えないようにします。
Token responseをデータとして検証する
成功時のresponseはJSON objectです。
{
"access_token": "opaque-access-token",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "assets.read drawings.write"
}
各fieldの役割は次のとおりです。
| field | 型 | 扱い |
|---|---|---|
access_token | string | 必須。Resource Serverへ提示する秘密値 |
token_type | string | 必須。Bearerであることを確認する |
expires_in | number | 発行時点から有効な秒数 |
scope | string | 実際に発行されたscope。requestと異なる場合は確認する |
access_tokenの長さや内部形式を仮定してはいけません。見た目がJWTでも、接続契約で求められていない限りclient側でclaimを業務判定に使わず、不透明な文字列として扱います。
Client Credentialsのresponseにはrefresh tokenを含めないことがRFC 6749で推奨されています。通常はaccess tokenの期限が近づいたら、同じgrantで新しいtokenを取得します。
未知のresponse fieldは無視できるようにしつつ、既知fieldの型と意味は厳密に検証します。
期限は受信時刻から計算する
expires_in: 3600は絶対日時ではなく、response生成時点からの秒数です。clientは受信時刻を基準に使用期限を計算します。
usable_until = received_at + expires_in - safety_margin
たとえば有効期間が3600秒なら、30〜120秒程度の安全余裕を引いて再取得する設計が考えられます。余裕値は通信時間、時計差、処理時間、接続先仕様に合わせて決めます。
process内だけでcacheする場合は、時刻補正の影響を受けにくいmonotonic clockを利用できます。期限をDBへ永続化して複数processで共有する場合は、取得日時、絶対期限、発行元、scopeを明確にし、token本体は暗号化や専用secret保管を検討します。
Pythonでtoken responseを正規化する
次の例は、token responseを検証し、monotonic clock上の使用期限へ変換します。
from dataclasses import dataclass
import time
@dataclass(frozen=True)
class AccessToken:
value: str
expires_at: float
scopes: frozenset[str]
def is_usable(self, *, margin_seconds: int = 60) -> bool:
return time.monotonic() < self.expires_at - margin_seconds
def parse_token_response(data: dict) -> AccessToken:
if not isinstance(data, dict):
raise TypeError("token responseはobjectである必要があります")
access_token = data.get("access_token")
if not isinstance(access_token, str) or not access_token:
raise ValueError("access_tokenがありません")
token_type = data.get("token_type")
if not isinstance(token_type, str) or token_type.lower() != "bearer":
raise ValueError("未対応のtoken_typeです")
expires_in = data.get("expires_in")
if type(expires_in) is not int or expires_in <= 0:
raise ValueError("expires_inは正の整数が必要です")
scope_text = data.get("scope", "")
if not isinstance(scope_text, str):
raise TypeError("scopeは文字列である必要があります")
scopes = frozenset(part for part in scope_text.split(" ") if part)
return AccessToken(
value=access_token,
expires_at=time.monotonic() + expires_in,
scopes=scopes,
)
Pythonではboolがintのsubclassなので、type(expires_in) is not intとしてtrueを1秒として通さないようにしています。実装する接続先が小数や文字列の期限を定義している場合は、その公式契約に合わせます。
Bearer tokenはAuthorization headerへ入れる
RFC 6750は、Bearer tokenをAuthorization request headerで送る方法を定義しています。
GET /v1/assets/asset-0042 HTTP/1.1
Host: api.example.com
Authorization: Bearer <access token>
Accept: application/json
Bearer tokenは、値を持っている主体が利用できるcredentialです。URL queryへ入れると、browser history、proxy、access log、Refererなどへ残る可能性があります。通常はAuthorization headerを使い、tokenをpayload、URL、通常logへ含めません。
scopeとaudienceを最小化する
scopeはclientができる操作を制限します。
assets.read
assets.write
drawings.read
drawings.write
「管理者」のような大きなscopeを全連携で共有せず、用途と環境ごとにclient登録を分けます。開発、検証、本番でもcredentialを分離します。
RFC 9700は、access tokenの権限を用途に必要な最小範囲へ制限し、特定のResource Serverまたは小さな集合へaudienceを制限することを推奨しています。tokenが漏れた場合の影響範囲を小さくできます。
scope文字列だけを見てclient側が最終認可を決めるのではなく、Resource Serverがtokenの署名・失効・issuer・audience・scopeを検証します。
401と403を分けて処理する
API呼び出しが失敗したとき、すべてをtoken再取得で解決しようとすると無限loopになります。
| 状況 | 基本対応 |
|---|---|
| token未送信・期限切れ・無効 | 401を確認し、cacheを破棄して1回だけ再取得 |
| scope不足 | 403またはinsufficient_scopeとして設定・権限を見直す |
token endpointでinvalid_client | client ID、secret、認証方式を確認。自動連続再試行しない |
invalid_scope | 登録済みscopeとrequestを確認 |
| 429・503 | Retry-Afterや接続先仕様に従い、上限付きで再試行 |
401後の再送でも、元のAPI操作がPOSTや増分処理なら二重実行へ注意します。OAuthのtoken再取得と、業務requestの冪等性は別の問題です。request ID、idempotency key、一意制約などを組み合わせます。
403で新しいtokenを取り続けても、clientに必要scopeが許可されていなければ成功しません。認証失敗、認可不足、一時障害を分類します。
Secretと連携設定を分離する
接続設定には、公開可能な値と秘密値が混在します。
integration_id: parts-master-production
token_url: https://auth.example.com/token
resource_base_url: https://api.example.com/v1
client_id_ref: secret://parts-master/client-id
client_secret_ref: secret://parts-master/client-secret
scopes:
- assets.read
audience: parts-api
timeout_seconds: 10
設定fileやDBにはsecretそのものではなく参照先を保持し、環境変数やsecret managerから実行時に取得します。source code、Git、記事、画面capture、例外文へ実値を入れません。
client secretを更新するときは、旧secretと新secretを短期間並行利用できるかを確認し、先に新secretを配布してから旧secretを無効化します。失効手順と担当を決めておくことも契約の一部です。
正常系と異常系をテストする
最低限、次のケースを自動テストへ含めます。
- 正常なBearer tokenと正の
expires_inを受理する access_token欠落、空文字、型違いを拒否する- 未対応
token_typeを拒否する expires_inが0、負数、boolean、文字列なら拒否する- 未知fieldが追加されても処理を継続する
- 発行scopeが必要scopeを満たすか確認する
- 期限前はcacheを利用し、期限接近時だけ再取得する
- 401後の再取得を1回に制限する
- 403ではtoken取得loopへ入らない
- log、例外、監視eventにtokenとsecretが含まれない
token endpointとResource Serverをtest doubleに分けると、発行失敗と業務API失敗を独立して検証できます。
CAD・DB・自動化へ再利用する
CAD変換、BOM同期、設計計算、定期ETLでも、認証処理を各scriptへ複製せず共通のtoken providerへ切り出します。
業務moduleは「必要scope」と「呼び出したいAPI」を指定し、token providerがcache、期限、再取得、秘密情報の取得を担当します。認証方式がclient secretから証明書やprivate keyへ変わっても、業務データ変換のロジックを変更せずに済みます。
連携recordにはtokenを保存せず、integration_id、request ID、対象resource ID、実行結果、発生日時を記録します。秘密情報と再処理に必要な業務情報を分離します。
まとめ
Client Credentialsは、単にaccess tokenを取得する手順ではなく、機械同士の権限と秘密情報を管理するデータ契約です。
- secretを保持できるconfidential clientで使う
- token endpointとResource Serverの役割を分ける
- requestの形式、認証方式、scopeを接続先仕様で固定する
- token responseの型、期限、発行scopeを検証する
- access tokenを不透明な秘密値として扱う
- 期限内はcacheし、安全余裕を取って再取得する
- Bearer tokenはAuthorization headerへ入れる
- scopeとaudienceを必要最小限へ制限する
- 401、403、一時障害を分けて処理する
- secret、token、公開可能な連携設定を分離する
- OAuthの再認証と業務requestの冪等性を別々に設計する
この構造を共通moduleにすれば、API、Python、DB、CAD、自動化処理が同じtoken管理規則を共有し、credential更新や接続先変更にも対応しやすくなります。

