ユースケース — 永続化(SQL)
OP は何を保存するのか、どこに保存するかが効くのか
OP が保持する行のうち、OAuth / OIDC 仕様が 再起動越しに保持 することを要求するもの:
- refresh token チェーン(RFC 6749 §6, RFC 9700 §4.14)— 失えば全ユーザのセッションが切れる。
- 登録クライアント(DCR が ON なら OIDC Dynamic Client Registration 1.0 / RFC 7591、OFF なら静的シード)— 失えば全 RP が動かなくなる。
- セッション(OIDC RP-Initiated Logout 1.0 / Back-Channel Logout 1.0)— ログアウトの fan-out に必要。
- 同意グラント(OIDC Core 1.0 §3.1.2.4)— 失えば再起動のたびに全ユーザに再同意を強いることになる。
- 監査 / introspection / revocation の shadow 行 — Tokens で説明した access token registry。
デフォルトの inmem ストアは再起動で全てを失う点で、テスト・デモには十分ですが本番には不向きです。ライブラリは op/storeadapter/sql を同梱しており、database/sql アダプタで SQLite / MySQL 8.0+ / PostgreSQL 14+ を対象にします。
ソース:
examples/06-sql-store— SQLite クイックスタート(CGO 不要)。examples/07-mysql-store— 本番形プールを持つ MySQL。OP と in-process RP を組み合わせ、docker-compose スタックとして同梱。
なぜサブモジュール
SQL アダプタは 別 Go モジュール として公開されているので、driver 依存(SQL driver、migration ライブラリ)はオプトインするまで go.sum に混入しません:
go get github.com/libraz/go-oidc-provider/op/storeadapter/sql@latestRedis アダプタも同様です。
アーキテクチャ
各サブストア(AuthCodeStore、RefreshTokenStore、ClientStore、SessionStore など)がテーブルにマップされます。
新しいサブストア
SQL アダプタは以下のテーブルを同梱します:
oidc_opaque_access_tokens— opaque access token サブストアの裏側。op.WithAccessTokenFormat(op.AccessTokenFormatOpaque)またはop.WithAccessTokenFormatPerAudience(...)を有効にしたときだけ書き込まれます。oidc_grant_revocations+oidc_revoked_jtis— 既定のRevocationStrategyGrantTombstoneを支えるテーブル。
どちらもトランザクションクラスタの一部で、起点となる grant / refresh の書き込みと同時にコミットされます — カスケードが途中で切れて「失効した grant の隣に、まだ引き換え可能なトークンが残る」状況にはなりません。
同梱アダプタを使わずカスタムの Store 実装をシップする場合は、OpaqueAccessTokens() と GrantRevocations() の実装が 必須 です。対応するオプションを利用しない限り nil を返してかまいません。オプションを有効化しているのに該当アクセサが nil の場合は、op.New が起動時に検出して構成を拒否します。
コード
import (
databasesql "database/sql"
_ "modernc.org/sqlite" // または MySQL / Postgres driver
"github.com/libraz/go-oidc-provider/op"
oidcsql "github.com/libraz/go-oidc-provider/op/storeadapter/sql"
)
db, err := databasesql.Open("sqlite", "file:op.db?_journal=WAL&_busy_timeout=5000")
if err != nil { /* ... */ }
storage, err := oidcsql.New(db, oidcsql.SQLite()) // または oidcsql.MySQL() / oidcsql.Postgres()
if err != nil { /* ... */ }
if err := storage.Migrate(context.Background()); err != nil {
/* ... */
}
provider, err := op.New(
op.WithIssuer("https://op.example.com"),
op.WithStore(storage),
op.WithKeyset(myKeyset),
op.WithCookieKey(myCookieKey),
)マイグレーション
*sql.Store.Migrate(ctx) がアクティブな dialect 用の同梱スキーマを適用します。最初のリクエストが届く前のデプロイ時に実行してください。Schema() は同じ DDL を文字列で返すので、自前の migration ツールに渡すこともできます。スキーマファイルは op/storeadapter/sql/schema/ に embed されています。
MySQL プールサイズ
examples/07-mysql-store は本番形の DSN を示します:
db, err := stdsql.Open("mysql",
"oidc:secret@tcp(mysql:3306)/op?parseTime=true&charset=utf8mb4&collation=utf8mb4_0900_ai_ci")
db.SetMaxOpenConns(64)
db.SetMaxIdleConns(8)
db.SetConnMaxLifetime(30 * time.Minute)charset=utf8mb4 は必須です — 4 バイト UTF-8(絵文字、CJK 拡張)を claim 値で切り詰めずに往復させるためです。
ユーザ名 + password による認証情報
SQL アダプタは store.UserPasswordStore(inmem リファレンスアダプタと同じインターフェース)を実装するので、ビルトインの op.PrimaryPassword Step を SQL バックエンドに対してそのまま組み込めます。glue コードは不要です:
flow := op.LoginFlow{
Primary: op.PrimaryPassword{Store: storage.UserPasswords()},
}
provider, err := op.New(
/* ... */
op.WithLoginFlow(flow),
)スキーマは oidc_users に 2 つのカラムを追加します。ユーザ名検索用の一意インデックス(FindByUsername が利用)と、PHC 形式のハッシュを保持する password_hash カラム(ReadPasswordHash が読み出す)です。
ハッシュ符号化は組み込み側の責務です — 補助の writer *sql.Store.PutUserWithPassword(ctx, user, username, hash) は op.HashPassword(argon2id、ライブラリ既定値)が返したハッシュを受け取り、PutUser と同じ upsert を経由します:
hash, _ := op.HashPassword("demo")
_ = storage.PutUserWithPassword(ctx, &store.User{
Subject: "demo-user",
Claims: map[string]any{"name": "Demo User"},
}, "demo", hash)ユーザ名を空文字、ハッシュを nil にして渡すと認証情報を消去できます — passkey 専用に移行したユーザを扱うときに便利です。
ReadPasswordHash は subject が未知の場合と、行は存在するが password を持たない場合の両方で store.ErrNotFound を返すので、LoginFlow 側はどちらの場合も user enumeration 攻撃に対して安全な応答を返せます。
Contract test ハーネス
inmem を検査する同じ contract test suite (op/store/contract) が、SQL アダプタに対しても go test -tags=testcontainers で testcontainers-go 経由の実 MySQL / Postgres を起動して実行されます。「SQL アダプタは Store interface を実装する」というライブラリの主張は、モックではなく実エンジンに対して検証されたものです。
pin されているイメージ(mysql:8.4、postgres:16-alpine)は、examples/07-mysql-store および examples/09-redis-volatile の docker-compose スタックが使うエンジンマトリクスと揃えてあるので、アダプタレベルと example レベルの統合検証が同じマトリクスを共有します。
いつ Redis を載せるか
Hot データ(interaction、消費済み JTI)は生成と陳腐化のサイクルが速く、永続 DB に載せるとテーブルが肥大化します。次ページの Hot / Cold + Redis で、永続サブストアを SQL に保ったまま揮発サブストアを Redis にルーティングする方法を示します。