Skip to content

go-oidc-provider とは ​

このライブラリが生まれた理由

本ライブラリは個人開発の OSS です。作者が他言語の OIDC Provider ライブラリを組み込む中で感じた不便さを、Go 向けに作り直したものです。たとえば「本来はオプション 1 つで切り替えたい機能が散らばっている」「古い既定値のままだと重大な脆弱性につながる」といった問題です。

二段階認証、パスキー、リスクベース認証、SPA 連携、FAPI 2.0、多言語化などは、後付けではなく最初から扱います。一方で、Implicit flow、ROPC、alg=none などの古く危険な仕様は、公開オプションとしても用意しません。詳細は以下の各節と ユースケース を参照してください。

Go でサービスを書いていると、自前で OpenID Connect Provider を立てる必要が出ることがあります。ID トークンとアクセストークンを発行し、認可エンドポイントとトークンエンドポイントを公開し、discovery 文書を配信する立場になる、ということです。選択肢は大きく 3 つあります。

選択肢得られるもの引き受けるもの
1. 自作する
(go-jose + JWT ライブラリ)
挙動を細かく制御できる脆弱性リスクを自分で抱える。例: 署名アルゴリズム混同、redirect URI の部分一致、PKCE ダウングレード、リフレッシュトークン再利用、cookie の適用範囲ミス、同意 POST の CSRF
2. 既製の IdP を前段に置く運用機能が豊富ユーザテーブル、画面テンプレート、アップグレードの都合を IdP 側に合わせる必要がある。自分の Go サービスが、外部プロダクトの組み込み先になりやすい。
3. go-oidc-providerプロトコルの責務はライブラリが持つユーザアカウント・ストレージ・UI は自分で持ち込む

以降では、選択肢 1 や 2 でつまずきやすい点と、このライブラリがそれぞれにどう答えるかを見ていきます。

どこを誰が持つか
責務の分担を示した図。組み込み側がユーザ・画面・保存先を持ち、ライブラリが OIDC と OAuth の手続きを担い、利用側のアプリはトークンを検証するだけです。組み込み側が持つライブラリが持つ利用側が持つ組み込み側のアプリユーザ · 画面 · 保存先LoginFlow · Storego-oidc-providerOIDC / OAuth の手続き/auth · /token · /jwksRP / API利用するアプリケーション渡されたトークンを検証する
本ライブラリはユーザテーブルもログイン画面もデータベースも要求しません。要求するのはインタフェースです。移行を強いる製品と、組み込むライブラリの違いはここにあります。

境界の引き方は次の 3 点です。

  • ライブラリの担当: トークン発行、署名、PKCE、CSRF、同意とログアウトの手続き。
  • 組み込み側の担当: ユーザ検索、パスワード検証、画面、保存先、ルータへのマウント。
  • 危険な古い方式はどちらの側からも有効化できません。 公開オプションとして用意しておらず、FAPI などのまとまった設定はプロファイルで固定されます。

つまずきやすい点への答え ​

つまずく点ライブラリの答え該当節
FAPI 2.0 の設定が複数箇所でずれるop.WithProfile(profile.FAPI2Baseline)スイッチひとつ
ユーザテーブルを動かせない既存テーブルを 3 メソッドで投影するだけユーザテーブル
cookie と CSRF の細部32 バイト鍵 1 つで決まる固定の方式cookie
SPA でのログイン UIop.WithUI(op.SPAUI{...}) または JSON ドライバSPA
「RFC に従っています」は証拠にならないリリースごとの適合性テストの実行適合性
リフレッシュトークンの盗難と再試行の集中猶予期間付きのローテーションと再利用検知ローテーション
頼んでいない /metrics ルートルートは作らず、利用者のレジストリにカウンタを登録メトリクス

「FAPI 2.0 をスイッチひとつで有効化したい」 ​

まず PAR を有効化し、後から JAR も必要だと気づき、さらに discovery がまだ client_secret_basic を広告していることに気づき、最後にテスト用の経路だけ ID トークンがプロファイルの許可リスト外の alg で署名されていた、という順で発覚しがちです。FAPI では、この種の設定ずれが高くつきます。

FAPI 2.0 が要求するもの

FAPI 2.0 Security Profile(Final)が要求するのは PAR(RFC 9126)、S256 の PKCE(RFC 7636)、DPoP(RFC 9449)または mTLS(RFC 8705)による送信者制約付きトークンです。本ライブラリはポリシーとして redirect_uri の完全一致も適用しますが、FAPI 2.0 §5.5 は完全一致する値の事前登録を要求していません。JAR と OP 発行 JWT の ES256 固定は、本ライブラリの profile.FAPI2Baseline が採用する設定で、FAPI Security Profile 自体の必須要件ではありません。本ライブラリでは Message Signing が署名付き認可応答の JARM を追加します。プロファイルを使うことで、discovery と実装の設定も揃います。

op.WithProfile(profile.FAPI2Baseline) が本ライブラリのプロファイル方針をまとめて適用します。PAR + JAR の有効化、mTLS が明示されていない場合の DPoP 既定選択、token_endpoint_auth_methods_supported のプロファイル許可リストへの絞り込み、OP 発行 JWT の ES256 固定まで一度に設定します。

go
op.New(
  /* 必須オプション */
  op.WithCookieKeys(cookieKey),
  op.WithLoginFlow(loginFlow),
  op.WithProfile(profile.FAPI2Baseline),
  op.WithDPoPNonceSource(nonces),
)

衝突は起動時に検出

コンストラクタは、起動時に検出できるオプションの矛盾や必要な依存設定の不足を拒否します。ただし、現行の FAPI 2.0 Final 仕様の全要件への適合を保証するものではありません。RFC 対応一覧 を確認してください。

「ユーザテーブルを奪われたくない」 ​

既製の IdP では、ユーザの保存までプロダクト境界に含まれがちです。ユーザを取り込む、同期する、相手のプロフィール定義に合わせる、ログイン画面も相手の流儀に寄せる。既に Go サービス側にアカウントモデルがある場合、この形は扱いづらくなります。

op.WithStore(s store.Store) は store.AuthorizationCodeStore / store.SessionStore / store.ClientStore などの小さな保存先インタフェースに接続するだけです。ライブラリは users テーブルを直接読み書きしません。代わりに、そのテーブルを読む 3 つのメソッド(FindBySubject、FindByUsername、ReadPasswordHash)を投影として書き、パスワードのステップに渡します。ユーザストアは store.Store には含まれません。OP はその行に対してログインを検証し、ID トークンと /userinfo の claim も同じ行から読みます。プロトコル状態はアダプタに置いたままです。

go
op.New(
  /* 必須オプション */
  op.WithCookieKeys(cookieKey),
  op.WithStore(myStore), // プロトコル状態
  op.WithLoginFlow(op.LoginFlow{
    Primary: op.PrimaryPassword{Store: applicationUsers}, // ログインも claim も既存 users テーブルから
  }),
)

投影の書き方と、テーブルに足すことがある 1 列については 既存ユーザストアの投影 を参照してください。

参照アダプタは inmem、sql(SQLite / MySQL / Postgres)、redis(揮発サブストア)、dynamodb(サブストアごとに 1 テーブル)、composite(Hot / Cold の振り分け)です。DynamoDB のプロビジョニング方式は DynamoDB ストア を参照してください。

「同意 POST の Cookie / CSRF が地雷原」 ​

難しいのは cookie ライブラリを選ぶことではありません。OAuth のセッション cookie を盗まれにくく、再利用されにくくするためのブラウザ規則を押さえ、ログイン・同意・ログアウトのすべてで同じ規則を適用しきることです。

ひとつ取りこぼすだけで CVE リスクに直結

__Host- prefix、Domain なし、Path=/、Secure、AES-256-GCM、double-submit CSRF、Origin / Referer チェック、適切な SameSite — どれかひとつ取りこぼすと、それだけで CVE 級の脆弱性に直結します。

ライブラリ側で次が組み込み済みです。

  • __Host- cookie prefix(Domain なし、Path=/、Secure)
  • AES-256-GCM での暗号化(cookie 鍵を op.WithCookieKeys で渡す)
  • 同意・ログアウト POST に対する double-submit CSRF と Origin / Referer チェック
  • セッション cookie は SameSite=Lax、CSRF cookie は SameSite=Strict

これらを自前で書く必要はありません。32 バイト鍵を生成して WithCookieKeys に渡せば、cookie の構成は正しい状態で立ち上がります。

go
cookieKey := make([]byte, 32)
if _, err := rand.Read(cookieKey); err != nil {
  return err
}

op.New(
  /* 必須オプション */
  op.WithCookieKeys(cookieKey),
)

「ログイン UI を SPA から扱いたい」 ​

次に出す画面を決めるのはプロトコル処理側の仕事で、その画面をどう見せるかはフロントエンドの仕事です。この 2 つは分けて考える必要があります。

op.WithUI(op.SPAUI{...}) を使うと、標準の HTML 画面を、JSON で状態を受け渡しする SPA 向けの対話に差し替えられます。SPA の入口と静的ファイルも OP 側で公開できます。SPA(React / Vue / Svelte / Angular など)は LoginMount/state/{uid} から次に表示すべき状態を JSON で取得し、入力結果を POST で返します。状態遷移はライブラリが担当し、表示は SPA が担当します。

go
op.New(
  /* 必須オプション */
  op.WithCookieKeys(cookieKey),
  op.WithLoginFlow(flow),
  op.WithUI(op.SPAUI{
    LoginMount: "/login",
    StaticDir:  "../internal/webui/static",
  }),
)

UI マウントオプション

op.WithUI は 3 つのモードのうち 1 つを受け取り、どれもそのまま動かせる接続点です。OP 側で SPA の入口を公開するなら op.SPAUI、サーバ描画の独自テンプレート(ログイン・同意・アカウント選択)なら op.HTMLUI の Login / Consent / Chooser を使います。より細かく制御したい場合は op.DriverUI に interaction.JSONDriver を渡し、SPA の配信を自前のルータで行えます。実例は examples/10-react-login を参照してください。

SPA 向けのエラー描画

エラーページは CSP default-src 'none'; style-src 'unsafe-inline' の下で <div id="op-error" data-code="..." data-description="..."> を出力します。SPA 側は HTML を解析せず、document.querySelector('#op-error') で機械可読な値を拾えます。

「『RFC に従っています』ではなく実適合の証拠が欲しい」 ​

セキュリティレビューが止まる理由は、RFC 名を挙げられないことではありません。任意仕様のどの分岐を実装し、どれを拒否したのか、そして適合性テストが実際にどの経路を検証したのかを示せないことです。

各リリースは OpenID Foundation の Conformance Suite に対して回帰検査されます。最新リリースの実行結果の生の内訳、レビュー理由、リリースを止める判定は OFCS 適合状況 にまとめています。

REVIEW / SKIPPED の読み方

REVIEW は OFCS の「人間レビューが必要」判定で、エラーページで止まる挙動はそれが正しい姿です(詳細)。SKIPPED は OP が設計上拒否しているケースです(例: alg=none の request object)。生の FAILED と未判定を黙って受け入れることはなく、リリース時の検証を通すにはレビュー済みかつ期限内の除外設定と一致しなければなりません。

「リフレッシュトークンのローテーションを観測可能にしたい」 ​

モバイルアプリが同じリフレッシュ要求を再試行しただけなら、猶予期間内は同じ結果を返すのが望ましい挙動です。一方、古いリフレッシュトークンが後から現れた場合は、連鎖全体を失効させ、その理由を監査ログに残す必要があります。この 2 つを同時に満たすことが目標になります。

リフレッシュトークンは既定でローテーションします。ローテーション済みのトークンが猶予期間を過ぎて提示されると、OP は盗難とみなし(RFC 9700 §2.2.2)、そのリフレッシュ連鎖と、その grant で発行済みのアクセストークンをすべて失効させます。/revoke で単一のリフレッシュトークンを失効させる場合はもっと狭く、そのトークンの連鎖だけを止めるので、ユーザの他の端末のリフレッシュトークンは有効なままです。2 つの FAPI 2.0 プロファイルはローテーションしません。FAPI 2.0 Security Profile(Final)§5.3.2.1(9) が通常のリフレッシュトークンのローテーションを禁止しているため、これらのプロファイルではリフレッシュ応答に新しいリフレッシュトークンが含まれず、提示したトークンは期限が切れるか失効するまで有効です。

  • op.WithRefreshGracePeriod — 再試行が競合するクライアント向けのローテーション直後の猶予期間を設定する(既定 60 秒、0 で無効)。FAPI 2.0 の 2 つのプロファイルはローテーションしないため、これらでは op.New がこのオプションを拒否します。
  • リフレッシュのローテーションでは、提示トークンを消費する前に実効 scope をクライアントの現在の登録と再照合するため、登録を狭めると既存の連鎖にも反映されます。
  • op.WithRefreshTokenOfflineTTL — offline_access 付きリフレッシュトークン(ログイン状態の維持)の寿命を、通常のローテーションから分離する。

token.issued / token.refreshed 監査イベントは extras に offline_access フラグを載せるので、SOC ダッシュボードで連鎖を分けて可視化できます。

go
op.New(
  /* 必須オプション */
  op.WithCookieKeys(cookieKey),
  op.WithLoginFlow(loginFlow),
  op.WithRefreshGracePeriod(60*time.Second),
  op.WithRefreshTokenOfflineTTL(90*24*time.Hour),
  op.WithAuditLogger(auditLogger),
)

「メトリクスは欲しいけれど、頼んでもいない /metrics ルートはいらない」 ​

ライブラリが観測用のルートを勝手にマウントすると、利用者側のルータ、認証境界、パス規約、SRE ミドルウェアと衝突しがちです。このライブラリはプロトコル上の信号だけを出し、それをどこで公開するかは組み込み側に委ねます。

op.WithPrometheus(reg) は絞り込んだ業務系カウンタを利用者のレジストリに登録します。ライブラリは /metrics を 公開しません。それはルータ側の仕事だからです。

同じ分離方針はトレーシング(外側で otelhttp をラップする)にも、HTTP リクエスト所要時間ヒストグラム(外側でミドルウェアをラップする)にも適用されます。

go
reg := prometheus.NewRegistry()
provider, _ := op.New(
  /* 必須オプション */
  op.WithCookieKeys(cookieKey),
  op.WithLoginFlow(loginFlow),
  op.WithPrometheus(reg),
)

router.Handle("/", provider)
router.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))

このライブラリではないもの ​

意図的に対象外

  • IdP ではありません。 ユーザを保存することも、メールを送信することもありません。ユーザモデルは組み込み側の責務で、ユーザの記録と保存済みのパスワードハッシュを返す store を用意すると、パスワードのステップがそれを検証します。パスワード、パスキー、TOTP、メール OTP、リカバリーコードのステップは同梱しています。同梱していない要素は op.ExternalStep として差し込みます。
  • 汎用 OAuth 2.0 フレームワークではありません。 OpenID Connect Core 1.0 と FAPI 2.0 系列を主な対象にしています。OAuth 2.0 単体の構成は op.WithOpenIDScopeOptional で表現できますが、設計は OIDC 寄りに寄せています。
  • UI キットではありません。 標準の HTML ドライバは無設定でも OP を起動できるよう同梱しているだけで、本番運用する組み込み側は独自テンプレートか SPA を持ち込む前提です。

次に読むもの ​