ユースケース — ストレージ構成の選び方
本ライブラリは、users テーブルもデータベースもマイグレーションツールも所有しません。ストレージには小さな store.* サブストアインターフェース越しに触れ、その裏側をどう用意するかは組み込み側が決めます。このページは、その判断の入口です。状況を選ぶと、読むべき具体ガイドが分かります。
考え方
ほぼすべては 2 つの問いで決まります: OIDC のテーブルをどこに置くか(1 つのバックエンドか、2 つに分けるか)、そして すでに合わせるべきスキーマを持っているか。サブストアインターフェースはどの場合も同一で、変わるのは裏側の保存先だけです。
はじめて本番構成を作る場合
最初は SQL アダプタ 1 つで始めるのがいちばん簡単です。Redis や composite は、ログイン途中の状態や JTI 再利用防止セットが SQL の負荷要因になってから検討しても遅くありません。既存の users テーブルだけを使いたい場合も、OIDC のプロトコルテーブルまで自前実装する必要はありません。
入口を選ぶ
| 状況 | 構成 | ガイド |
|---|---|---|
| 更地 — スキーマがまだ無い | SQL バックエンド 1 つ、アダプタ所有のテーブル | SQL ストア |
| AWS / DynamoDB — AWS ネイティブな永続バックエンドを使いたい | サブストアごとに DynamoDB テーブル 1 つ | DynamoDB ストア |
既存データベース — すでに users テーブルとマイグレーションを運用している | アダプタ所有の oidc_* テーブル(命名を合わせて変更)+ ID 情報を投影 | テーブル名の差し替え · ユーザストアを自前実装 · ストアを自前実装(カラムも独自にする場合) |
| スケール / 高頻度生成 — 揮発状態を永続ストアから外したい | composite で分離: 永続 SQL + 揮発 Redis | Hot / Cold 分離 |
3 番目の分岐には落とし穴が 2 つあります。認可コードと PAR の request_uri は短寿命なので揮発状態に見えますが、置き場所は永続側です。composite.TxClusterKinds のメンバは、すべて同じバックエンドに解決される必要があります。消費済み jti の再利用防止セットも短寿命ですが、有効なマーカーが追い出されると再利用検知の窓が再び開くため、追い出しをしない Redis インスタンスでない限り永続側に置きます。
更地
sql アダプタはスキーマ一式を同梱しているので、組み込み側が OIDC テーブルを設計する必要はありません。New(db, dialect) が SQLite / MySQL 8.0+ / PostgreSQL 14+ に対して(対応する SQLite() / MySQL() / Postgres() dialect 経由で)ストアを構築し、アダプタが自身の oidc_* テーブルを所有します。
- migration ツールがない場合: デプロイジョブからストアの
Migrate(ctx)を 1 回呼びます。Migrations()のファイルを順に適用し、進捗をoidc_schema_migrationsテーブルに記録するので、同じ呼び出しで新規 DB の作成も古い DB の更新もできます。 - migration ツールがある場合: そのツールで
Migrations()を適用します。golang-migrate はx-migrations-table=oidc_schema_migrationsでバージョンテーブルを共有でき、goose、Flyway、Atlas は自前の履歴テーブルを使います。op-schema exportは goose と Flyway の形式でファイルを書き出します。 - レビュー用: ストアの
Schema()は、dialect 固有の現行の完全な DDL を(WithNamingの上書きを適用済みの状態で)返します。アダプタが前提とするスキーマと本番スキーマを、レビューで差分比較できます。
以前のリリースで作成した DB では、最初のリクエスト前に、それ以降のリリースすべての Store.Migrations() の migration ファイルを順番に適用してください(内容は schema/MIGRATIONS.md にあります)。カラムが欠けている場合、スキーマの確認は適用すべきファイルを示します。ファイルが追加する MySQL のインデックスと照合順序は SQL ガイドで扱い、ロールアウトと GC の順序は ストレージの保守にまとめています。
既存データベース
データベースもマイグレーションも組み込み側のまま維持します。アダプタがその隣にどう収まるかは、2 つの事実で決まります。
アダプタは形が固定されたテーブルを所有します。
WithNamingは 物理テーブル名 を変更します(oidc_clientsを任意の命名規則へ)が、列は固定 です — アダプタは既知の列集合に対してクエリを組み立てます。未知の論理キーはNewを失敗させるので、打ち間違いは最初のクエリ時ではなく構築時に表面化します。本物の
usersテーブルは組み込み側のものです。 OP が必要とするのは、そこから得る subject だけです。subject はユーザごとに一度だけ割り当て、再利用しない不透明な値です。すでにそうした列があればカラムは増えません。auto-increment の ID やメールアドレスしか持たないテーブルには、その 1 列を足します(subject の選び方)。組み込み側の 列を読んでstore.Userを返すstore.UserStore(組み込みのパスワードログインを使うならstore.UserPasswordStoreも)を実装し、op.WithUserStoreかop.PrimaryPasswordのStoreとして渡してください。アダプタ自身のoidc_usersテーブルが確認されるのは、アダプタのUsers()かUserPasswords()を渡したときだけです。ここでは確認されず、空のままです。ユーザデータをそこへ移行することはありません。使われないテーブルは存在しなくても構いません。
op.Newが確認するのは設定が使うテーブルだけで(既定の構成では 13 個)、空のデータベースに対して実行すれば、必要なテーブルがエラーに一度に列挙されます。そのうち数個は、失効処理やトークン検査が設定にかかわらず参照するので、常に確認され、空のままのこともあります。理由はテーブルごとにリファレンスに書いてあります。schema/TABLES.mdに、各テーブルのWithNamingキー、保持する内容、OP が使う条件、カラムが一覧されています。データアクセス層が
database/sqlである必要はありません。 pgx や sqlc で書かれたアプリケーションでも、アダプタには*sql.DBを渡します。github.com/jackc/pgx/v5/stdlibが、同じ DSN から、あるいは既存のpgxpool.Poolからstdlib.OpenDBFromPoolで作れます。プロトコル側のサブストアを自前で書くのは遠回りです。下の補足を参照してください。
つまり「既存テーブルを再利用したい」は 2 つに分かれます: OIDC の プロトコル テーブル(認可コード、リフレッシュチェーン、grant など)はアダプタのものなので WithNaming で名前を合わせてマイグレーションに作らせ、ID 情報は自前実装の UserStore の背後に置いたままにします。
どのサブストアを自前実装する価値があるか
どのサブストアも自前実装できますが、組み込み側がほぼ必ず所有するのはユーザの投影(UserStore / UserPasswordStore)です。ユーザレコードは組み込み側のドメインだからです。プロトコル系サブストア(コード、トークン、grant)は手書きのバックエンドで得をすることはまれなので、そこは sql アダプタを使ってください。ゼロからの実装例は ストアを自前実装 を、ID だけを差し替える場合は ユーザストアを自前実装 を参照してください。
スケールのための分離(Hot/Cold)
1 つのバックエンドではもう形が合わなくなったとき — 永続行に揮発状態が生む QPS は要らず、揮発行に永続ストアの保証は要らない — composite アダプタが各サブストアを永続側か揮発側のバックエンドへルーティングします。標準的な本番構成は、永続側に SQL、揮発側に Redis を置く形です。詳しい手順は Hot / Cold 分離 のページにあります。
Redis に載せてよいもの — ルール
Redis か SQL かの選択は「そのデータが短寿命かどうか」では ありません。1 つの不変条件に従います: アトミックに同時コミットしなければならないサブストア(composite.TxClusterKinds)は同じバックエンドを共有する必要があるため、永続側に留まります。データの寿命が決めるのは、その残りだけです。
| バケット | サブストア | 理由 |
|---|---|---|
| 永続側(SQL)必須 | AuthorizationCodeStore、RefreshTokenStore、GrantStore、PushedAuthRequestStore、AccessTokenRegistry、OpaqueAccessTokenStore、GrantRevocationStore | トランザクションクラスタのメンバー — 1 つの整合性ドメインでコミット / CAS されます。Redis アダプタはこれらに nil を返すので、composite は永続側の外へルートできません。 |
| Redis 向き(揮発) | InteractionStore | 短寿命・高頻度生成で、単体では失っても差し支えなく(ユーザはログインをやり直す)、クラスタの 外 にあります。 |
永続側、または noeviction の Redis | ConsumedJTIStore | クラスタの外ですが、再利用防止マーカーが追い出されると、盗まれた DPoP や private_key_jwt の proof が再利用できてしまいます。Redis に置けるのは追い出しをしないインスタンスだけで、そのインスタンスでは、ほかのサブストア向けに volatile-* や allkeys-* を使えません。 |
| 永続側 | AuthnLockouts | クラスタの外ですが、brute-force カウンタは subject をキーにしており、再起動後も残り、全レプリカで共有される必要があります。SQL と DynamoDB のアダプタは実装していますが、Redis アダプタは実装していません。ルートしない場合は Clients に従います。password や second-factor の step を持つログインフローは、lockout ストアが解決されないと op.New が失敗します(op.WithAuthnLockoutStore)。 |
| 組み込み側の判断 | SessionStore | どちらの保存先にもルートできます。WithSessionDurabilityPosture は logout の起点 / セッションのスナップショットの耐久性を宣言します。BCL の target は grant 由来で解決され、bcl.no_sessions_for_subject は eligible RP target が 0 件だった session 付き logout を記録します。 |
直感に反するメンバーが PAR です: request_uri は短寿命で 揮発に見えます が、OP はこれを認可コードの経路の中で消費するため、クラスタに属し永続側に留まります。サブストア単位の正式な表、サブストアごとの Redis の追い出しポリシー、必須バックエンドが nil のときの op.New のガードレールは Hot / Cold 分離 のページにあります。
Redis の最低ライン
oidcredis.New は TLS(rediss://)と AUTH 無しでは起動を拒否します。開発専用の例外口は oidcredis.WithDevModeAllowPlaintext で、本番で使うのは手で打ち込まないと出てこないセキュリティ後退です。
構成を健全に保つ
起動前にスキーマを適用し、writer の互換性境界を確認します。運用中は SQL の retention GC をスケジュールして GCStats を監視し、DynamoDB TTL は期限判定ではなくストレージ回収として扱います。ストレージの保守、バックアップ / DR、バックエンド別の SQL ストア、DynamoDB ストア、BYO ストア を参照してください。