【SREチーム ブログリレー 2日目】 こんにちは、SREチームの青木です。
エムスリーには、アクセスログや社内レポートを配信する仕組みがあります。レポートには部署や案件ごとに見てよい人・見てはいけない人がいるため、アクセス制御を行う認可システムを内製し運用しています。
この認可システムはこれまでブラウザでの SSO ログインを前提としていましたが、「Claude CodeのようなAIエージェントにレポートを読ませて集計や調査をさせたい」という要望に応えるため、OAuth 2.0 の Device Authorization Grant(RFC 8628、いわゆる Device Flow)を実装しました。本記事では認可システムへCLIでのアクセスのニーズとのDevice Flowの実装について紹介します。

社内レポートの認可システムとは
システム構成は次のような役割分担になっています。
- レポートファイルの実体は nginx が配信する
- 認可システムはファイル本体は中継せず nginx の
auth_requestディレクティブから呼ばれ、HTTP ステータス(200/401/403)を返します - 認証は社内 SSO(OIDC)、認可はユーザー・グループ・ロールとリソースの紐付けで判定する
レポートは「この案件の担当者だけが見られる」「この部署のメンバーだけが見られる」といった制御が必要なため、リソース(URL パスのプレフィックス)に対して ユーザー / グループ / ロール を紐付けるモデルで担当者ごとの権限を管理しています。

課題: セッション Cookie 必須で「人間 + ブラウザ」以外が使えない
一方で、認証はブラウザ前提でした。SSO でログインするとセッション Cookie が発行され、auth_request はその Cookie でユーザーを解決します。つまり:
- 踏み台サーバやコンテナ内シェルなど、ブラウザを起動できない環境からレポートを取得できない
- Claude CodeなどのAIエージェントにレポートを渡す手段がない
特に後押しになったのが AIエージェントの普及です。「先週のレポートを読んでエラーの傾向をまとめて」とエージェントに頼みたくても、取得手段がブラウザしかないと、人間がダウンロードしてファイルを手渡しするしかありません。かといって、エージェントにブラウザのセッション Cookie をコピーして渡すのは危険です。セッションはそのユーザーの全権限そのものであり、有効期限も失効手段もエージェント単位では制御できません。
方針検討
CLI から認証する方法として、いくつかの案を比較しました。
| 案 | 評価 |
|---|---|
| 認可システム自身が Device Flow の Authorization Server になる | 採用。追加インフラなし、既存の認可ロジックを無改修で流用できる |
| SSO 基盤の前段に Lambda + API Gateway で Device Flow を構築 | 不採用。単一システムの要件に対してインフラ追加が過剰 |
| Authorization Code + PKCE + ローカル loopback | 不採用。毎回ブラウザ起動が必要で「ブラウザのない環境」という要件に反する |
| Web UI で Personal Access Token (PAT) を発行して手でコピー | 部分採用。Device Flow の前段として Phase 1 でリリース |
ポイントは、利用している社内 SSO(OIDC IdP)がDevice Authorization Grant にネイティブ対応していないことでした。IdP 側に手を入れる案はコストが大きすぎるため、「認証(誰であるか)は従来通り SSO に任せ、CLI 用トークンの発行・検証だけを認可システム自身が担う」という責務分離で対応することにしました。
また、いきなり Device Flow を作るのではなく、Phase 1 として PAT(Web でトークンを発行して手でコピーする方式)を先行リリースしました。トークンの保存形式・Bearer 認証ミドルウェア・失効管理という基盤は PAT と Device Flow で完全に共通なので、PAT で基盤を検証してから Device Flow を載せる、という段階的な進め方ができます。
Device Flow の全体像
Device Flow は「入力手段の乏しいデバイス(テレビやスマートスピーカーなど)でも OAuth 認可を通せるようにする」仕様ですが、「トークンを使う主体(ブラウザのない端末やAIエージェント)と、承認する主体(ブラウザを持った人間)が分かれている」という構図は、社内CLIやAIエージェントのユースケースにも当てはまります。
利用イメージはこうです:
$ report login
以下のURLを開き、コードを入力してください:
URL: https://reports.example.com/device
Code: WDJB-MJHT
承認待ち... (有効期限 10 分)
✓ ログインしました
$ curl -H "Authorization: Bearer $(report token)" \
https://reports.example.com/path/to/report.txt
シーケンス図は以下の通りです。

- CLI が
POST /device/codeを叩き、device_code(CLI が持つ秘密値)とuser_code(人間が入力する短いコード)のペアを受け取る - CLI は URL と
user_codeを画面に表示し、POST /device/tokenをinterval秒間隔でポーリングし始める - ユーザーは手元の PC のブラウザで検証 URL を開く。未ログインなら既存の SSO ログイン(OIDC)に流れる
- ログイン済みのブラウザで
user_codeを入力して承認すると、サーバ側でdevice_codeにユーザーが紐付き、トークンが発行される - 次のポーリングで CLI が
access_tokenとrefresh_tokenを受け取る
認証はあくまで既存の SSO が行い、認可システムは「SSO でログイン済みのユーザーが、この user_code を承認した」という事実だけを扱います。パスワードやセッションが CLI 側に渡ることはありません。
実装のポイント
ここからは、実装の中で考えどころだった点を紹介します。
トークンは平文を保存しない
発行するトークンはランダム値の不透明トークン(opaque token)で、DBにはSHA-256ハッシュのみを保存します。DBが漏れてもトークンとして使える文字列は復元できません。ユーザーへの表示は発行時の一度きりです。
JWTのような自己完結型トークンにしなかったのは、失効を即時反映したいためです。不透明トークンならDBの該当行に失効時刻を打つだけで、次のリクエストから確実に拒否できます。単一システムに閉じたトークンなので、検証のたびにDBを引くコストも問題になりません。
また、トークンには用途がわかるプレフィックス(access token / refresh token で別)を付けています。GitHub の ghp_ などでおなじみの形式で、ログや Slack にうっかり貼られたときに秘密情報だと気付きやすくする効果と、secret scanning に載せやすくする狙いがあります。
既存の認可ロジックは無改修
Bearer 認証はミドルウェアとして追加し、トークンからユーザーを解決したら、あとは既存のセッション認証と同じ認可判定(ユーザー / グループ / ロール とリソースの紐付け)を適用できます。

認可システムの本体である「担当者ごとの権限判定」には一切手を入れていないため、「CLI 経由だと見えるはずのないレポートが見える」という事故が構造的に起きません。nginx 側の変更も、auth_request のサブリクエストに Authorization ヘッダを転送する 1 行だけでした。
proxy_set_header Authorization $http_authorization;
まとめ
- 担当者ごとの権限管理を行う社内レポートの認可システムに、Device Flow を実装しました
- SSO(IdP)には手を入れず、認可システム自身が Authorization Server になることで、追加インフラゼロ・既存認可ロジック無改修で CLI・AIエージェント対応ができました
- 「使う主体(エージェント)と承認する主体(人間)の分離」という Device Flow の性質は、AIエージェントに社内データへのアクセスを許可する仕組みとしてもそのまま機能します
同種の「社内システムの CLI 対応」でも再利用しやすい構成だと思います。どなたかの参考になれば幸いです。
We are hiring!!
エムスリーではSREを募集中です。社内基盤も手を抜かずに作りたいエンジニアを募集しています。カジュアル面談もやっていますので、お気軽にお声がけください!