ユースケース — FAPI 2.0 Baseline
プロファイルが入れるもの、利用者が用意するもの
op.WithProfile(profile.FAPI2Baseline) はプロトコル上の設定を固定します。動く構成にするための残りは、利用者の側で用意します。
| プロファイルが入れるもの | 利用者が用意するもの |
|---|---|
PAR、および S256 だけの PKCE | クライアント登録: 公開 JWKS を付けた op.PrivateKeyJWTClient。feature.MTLS を使うなら op.TLSClientAuthClient または op.SelfSignedTLSClientAuthClient |
DPoP に結び付いたトークン。feature.MTLS を有効にしない限り自動で有効 | リスナーの TLS: op.FAPITLSConfig()。TLS 1.2 のために RSA のサーバ証明書が必要 |
クライアント認証は private_key_jwt に限定(feature.MTLS があれば 2 つの mTLS 方式も) | プロキシの背後では op.WithTrustedProxies。mTLS ならさらに op.WithMTLSProxy と op.WithMTLSRootCAs |
クライアント assertion の aud は issuer の文字列だけを受理 | すべてのレプリカで共有するストア(複数レプリカ) |
| ローテーションしないリフレッシュトークン | トークンを検証するリソースサーバ: API を保護する |
| アクセストークンの有効期間は最大 10 分、PAR の有効期間は 600 秒未満 | |
クライアント署名の入力は PS256・ES256・EdDSA、ID トークンは ES256 |
動かせる版: TLS と JWKS の組み立ては examples/50-fapi-tls-jwks、プロキシの背後での証明書バウンドトークンは examples/55-mtls-proxy です。
最終版 Security Profile との関係
OpenID Foundation は FAPI 2.0 Security Profile を最終版として公開しました。以前のドラフトでは同じプロファイルが「Baseline」と呼ばれており、本ライブラリはそのプリセットの名前として profile.FAPI2Baseline を残しています。プリセットに反する構成は op.New が拒否するので、追加のオプションなしで、最終版の次の要件が満たされます。
- PAR と PKCE。 認可リクエストは
/par経由で届き、code_challenge_method=S256だけが受け付けられます。 - 送信者制約付きトークン。 DPoP または mTLS です。どちらも指定されていなければ、プリセットが DPoP を有効にします。
- クライアント認証。
private_key_jwt、またはfeature.MTLSが有効なら RFC 8705 §2 の 2 つの mTLS 方式です。共有シークレット方式と公開クライアントは拒否されます。 - クライアント assertion の
aud(§5.3.2.1(8))。issuer 識別子を単一の JSON 文字列で指定する必要があります。配列やエンドポイント URL はinvalid_clientで拒否されます。他のプロファイルは、これまでどおり issuer と token endpoint URL のどちらも受け付けます。 - リフレッシュトークンのローテーションなし(§5.3.2.1(9))。リフレッシュ応答は新しいアクセストークンを返し、
refresh_tokenを含みません。クライアントが持つリフレッシュトークンは、期限が切れるか失効するまで使えます。ローテーションによる再利用検知はこのプリセットにはありませんが、失効、grant の失効、ログアウトによる無効化は従来どおり働きます。op.WithRefreshGracePeriodが設定する窓はないため、op.Newが拒否します。 - DPoP の
htu。 OP 自身のエンドポイントでは、proof のhtuを OP が discovery で広告するエンドポイント URL と比較します。mtls_endpoint_aliasesを設定していれば、その URL とも比較します。 - アルゴリズムと有効期間(§5.4.1、§5.3.2.2)。クライアントが署名する入力は
PS256、ES256、EdDSA、PAR のrequest_uriの有効期間は 600 秒未満、アクセストークンはサーバ側で失効できること、です。
最終版を超える部分として、プリセットは JAR のサポートを有効にし、事前登録済みの redirect_uri の完全一致を求め、ID トークンを ES256 だけで署名し、RS256 での発行を拒否し、アクセストークンを 10 分までに制限します。これらは本ライブラリの選択で、最終版の要件ではありません。op.New が通ったことは適合の主張にはなりません。TLS の設定、クライアントの挙動、認定そのものはプリセットの範囲外です。
FAPI 2.0 とは
FAPI("Financial-grade API")は OpenID Foundation が策定する OAuth 2.0 のセキュリティプロファイル群です。FAPI 2.0 Security Profile は、PAR、S256 の PKCE、送信者制約付きトークン(DPoP または mTLS)、confidential クライアント、PS256 / ES256 / EdDSA の JWT alg を要求します。JAR は要件に含まれません。また OAuth のプロファイルなので、認証済みユーザの識別子が必要なときに OIDC を併用します。認可エンドポイントは mount prefix 配下の /auth で、既定の /oidc なら /oidc/auth になります。
銀行・医療・行政の運用では、「安全なオプションを全部覚えているか」ではなく「決まったチェックリストに対して監査できるか」が問われます。FAPI 2.0 は、いまも使われている FAPI 1.0 の後継です。別の設定 profile.FAPI2MessageSigning は、JARM、署名付き request object、署名付き introspection を追加します。組み込み側で何が変わるかは FAPI 2.0 Message Signing にまとめています。
本ライブラリは Baseline を プロファイル 1 つの指定(op.WithProfile(profile.FAPI2Baseline))として公開します。必要なチェックをまとめて有効にし、それに反する構成では op.New 自体が起動を拒否します。
PAR / JAR / JARM / DPoP / mTLS / ES256 など各略号の解説は FAPI 2.0 入門 にあります。本ページは構成例を扱います。
このページで触れる仕様
ソース:
examples/03-fapi2/main.goはプロファイルのフローを扱います。examples/50-fapi-tls-jwksはop.FAPITLSConfig()とop.LoadPublicJWKSを示します。LoadPublicJWKSは、client 登録前に private JWK material を取り除きます。
本ライブラリの FAPI2Baseline が強制するもの
| 要件 | RFC | ライブラリの挙動 |
|---|---|---|
| Pushed Authorization Requests | RFC 9126 | プロファイルが feature.PAR を自動有効化。/par で得た request_uri を authorize の入口にする。 |
| Proof Key for Code Exchange | RFC 7636 | code_challenge_method=S256 必須、plain 拒否。 |
| 送信者制約付きトークン(DPoP または mTLS) | RFC 9449 / RFC 8705 | このプロファイルでは profile.RequiredAnyOf が {DPoP, MTLS} を返すため、どちらか一方を満たせばよい。どちらも指定されていなければ、コンストラクタがインフラ前提の少ない DPoP を有効にする。 |
| JWT alg | FAPI 2.0 §5.4.1 | プロファイルが許可するのは PS256、ES256、EdDSA です。本ライブラリの id_token_signing_alg_values_supported はプロファイル下で ["ES256"] です。op.New は RS256 との併用を拒否し、none / HS* は公告しません。 |
| クライアント認証 | FAPI 2.0 §5.3.2.1 | private_key_jwt。feature.MTLS を有効にすれば tls_client_auth と self_signed_tls_client_auth も使える。クライアント認証 を参照。 |
クライアント assertion の aud | FAPI 2.0 §5.3.2.1(8) | issuer 識別子を JSON 文字列で指定したものだけ。配列やエンドポイント URL は invalid_client。 |
| リフレッシュトークン | FAPI 2.0 §5.3.2.1(9) | ローテーションしない。リフレッシュ応答に refresh_token は含まれず、op.WithRefreshGracePeriod は op.New が拒否する。 |
DPoP の htu | RFC 9449 §4.3 | OP が広告するエンドポイント URL、または mtls_endpoint_aliases の URL と比較する。 |
| 短命なアクセストークン | ライブラリのポリシー | アクセストークンの有効期間が 10 分を超える設定は op.New が拒否する(profile.MaxAccessTokenTTL)。 |
| PAR の有効期間 | FAPI 2.0 §5.3.2.2 | op.WithPARTTL が 600 秒以上なら op.New が拒否する。 |
| 署名付き request object | RFC 9101 | feature.JAR が自動で有効になり、/par と /auth は request を受け付ける。ただし必須ではない(必須にするのは profile.FAPI2MessageSigning)。request object を使う場合は、署名されていない query / form パラメータとマージせず、request object が置き換える。request object とクライアントアサーションの RS256 は拒否する。 |
redirect_uri 完全一致 | ライブラリのポリシー | 本ライブラリは事前登録済み URI とのバイト単位の一致を求め、ワイルドカードを認めません。現行の FAPI 2.0 Security Profile は、認証済み PAR リクエストに redirect_uri を含めることを求めますが、事前登録は必須としていません。 |
アーキテクチャ
コード(examples/03-fapi2 からの抜粋)
import (
"github.com/libraz/go-oidc-provider/op"
"github.com/libraz/go-oidc-provider/op/feature"
"github.com/libraz/go-oidc-provider/op/profile"
"github.com/libraz/go-oidc-provider/op/storeadapter/inmem"
)
const (
demoIssuer = "https://op.example.com"
demoClientID = "fapi2-example-client"
demoRedirectURI = "https://rp.example.com/callback"
)
st := inmem.New()
loginFlow := op.LoginFlow{Primary: op.PrimaryPassword{Store: st.UserPasswords()}}
provider, err := op.New(
op.WithIssuer(demoIssuer),
op.WithStore(st),
op.WithKeyset(opKeys.Keyset()),
op.WithCookieKeys(opKeys.CookieKey),
op.WithLoginFlow(loginFlow),
op.WithProfile(profile.FAPI2Baseline), // <--- プロファイル切り替え
op.WithFeature(feature.DPoP), // 省略可: プロファイルが DPoP をすでに有効にします
op.WithStaticClients(op.PrivateKeyJWTClient{
ID: demoClientID,
JWKS: clientJWKs, // 公開 JWK Set を JSON バイト列で
RedirectURIs: []string{demoRedirectURI},
Scopes: []string{"openid", "profile", "email"},
GrantTypes: []string{"authorization_code", "refresh_token"},
ResponseTypes: []string{"code"},
}),
)example は簡潔にするため in-memory のユーザストアを使っています。実際の OP では自前の store.UserPasswordStore を渡します(既存ユーザストアの投影)。PrivateKeyJWTClient は FAPI クライアント用の型付きクライアント定義で、token_endpoint_auth_method=private_key_jwt を自動で設定します。組み込み側でこのフィールドを書く必要はありません。同じ系統の型は クライアント認証 に挙げており、すべてが op.ClientSeed を実装し、WithStaticClients(seeds ...ClientSeed) に渡せます。
example の RP(rpkit.NewFAPI2)は、private_key_jwt のアサーション付きで /par にリクエストを push し、/token では DPoP proof を送ります。そのため http://127.0.0.1:9090/ を開くだけで、ブラウザからフロー全体を試せます。既定の grant は authorization_code と refresh_token なので、example は op.WithGrants を呼びません。
本ライブラリで WithProfile を指定すると、OP は次のように振る舞います。
feature.PARとfeature.JARを自動で有効にします。token_endpoint_auth_methods_supportedを FAPI 2.0 §5.3.2.1 の許可リストと突き合わせて絞り込みます。残るのはprivate_key_jwtと、feature.MTLSが有効な場合の 2 つの mTLS 方式です。id_token_signing_alg_values_supported = ["ES256"]を維持します。OP は ES256 の ID Token を署名・広告します。FAPI 2.0 Security Profile §5.4.1 はRS256を許可しないため、プロファイルはop.WithSigningAlgorithm(op.AlgRS256)を拒否します。プロファイルはPS256とEdDSAも許可しますが、ライブラリにはその発行オプションがありません。redirect_uriの完全一致を強制します。ワイルドカードはどこにも許しません。- DPoP または mTLS の送信者制約要件を満たします。
feature.MTLSが明示されていればそれを尊重し、どちらも選ばれていなければfeature.DPoPを追加します。 - クライアント assertion の
audを issuer の文字列だけに固定し、リフレッシュトークンのローテーションを止め、op.WithRefreshGracePeriodを拒否します。
JAR では profile.MaxRequestObjectAge(profile.FAPI2Baseline) が 60 * time.Minute を返します。op.New はこの値を request-object verifier に渡し、署名済み request object の iat 経過時間に適用します。nbf と exp の検証も引き続き行われます。
クライアント認証
クライアントごとに方式を 1 つ選びます。seed 型が token_endpoint_auth_method を決めるため、認証方式を指定するフィールドはどの型にもありません。
| 方式 | seed 型 | 登録する値 | 追加で必要なもの |
|---|---|---|---|
private_key_jwt | op.PrivateKeyJWTClient | JWKS(JSON の公開 JWK Set)か JWKSURI のどちらか 1 つ | プロファイル以外には不要 |
tls_client_auth | op.TLSClientAuthClient | SubjectDN、SANDNS、SANURI、SANIP、SANEmail のうちちょうど 1 つ | WithFeature(feature.MTLS)、チェーン検証済みの証明書 |
self_signed_tls_client_auth | op.SelfSignedTLSClientAuthClient | JWKS か JWKSURI のどちらか 1 つ | WithFeature(feature.MTLS) |
JWKSURIは、他のクライアントのjwks_uriと同じ SSRF 対策付きの経路で取得されます。プライベートネットワーク上のホストはop.WithAllowPrivateNetwork(op.OutboundJWKS)で許可し、op.WithJWKSHTTPTransportで transport を指定します。tls_client_authが受け付けるのは、チェーンを検証済みの証明書だけです。直接 TLS で受ける場合は、サーバが自前のClientCAsでクライアント証明書を検証している必要があります。証明書をヘッダで前送りするプロキシの後ろでは、OP がop.WithMTLSRootCAsで検証します。tls_client_authとop.WithMTLSProxyを組み合わせて root pool がなければ、op.Newは失敗します。self_signed_tls_client_authは、証明書の公開鍵をクライアントの鍵セットと照合するだけで、チェーンは辿りません。feature.MTLSなしでop.WithMTLSProxyやop.WithMTLSRootCAsを設定すると、op.Newがエラーを返します。op.WithMTLSProxy(op.MTLSProxy{HeaderName, TrustedProxies})は、ヘッダ名と、そのヘッダを設定してよいプロキシの CIDR を指定します。TrustedProxiesが空ならop.WithTrustedProxiesの範囲を使います。- クライアントの認証に使った証明書は、そのクライアントが受け取るアクセストークンの結び付け先(
cnf.x5t#S256)にもなります。
証明書の扱いの詳細は mTLS にあります。
DPoP の代わりに mTLS
プロファイルの既定の送信者制約方式は、TLS クライアント証明書の設定が不要な DPoP です。mTLS に標準化している配備では feature.MTLS を明示し、TLS 終端プロキシ用に op.WithMTLSProxy(...) を設定してください。その明示選択があれば DPoP 既定は追加されません。送信者制約とクライアント認証は別々の選択です。private_key_jwt で登録したクライアントにも、証明書に結び付いたトークンを発行できます。
FAPI 構成で決めておくこと
TLS を終端するプロキシの後ろで動かす
DPoP のためにプロキシで特別な設定をする必要はありません。OP は proof の htu を、issuer、mount prefix、エンドポイントのパスから組み立てた広告 URL と比較します。そのため、プロキシがスキームやホストを書き換えても結果は変わりません。op.WithDiscoveryMetadata で mtls_endpoint_aliases を設定した場合の alias URL も同じです。
ただし op.WithTrustedProxies は引き続き必要です。監査記録、リスク判定、lockout が使うクライアントのアドレスと、OP のほかの検査が読むスキームとホストを決めるからです。ここでいう検査は、ログインやログアウトのリクエストが issuer のオリジンから来たかどうかの判定、redirect_uri のスキームの検査、cookie の Secure の判定です。設定しなければ OP は X-Forwarded-* を無視し、警告を 1 回ログに出します。設定方法は ルータへのマウント と issuer にあります。op/rs で作ったリソースサーバは、OP が API の公開 URL を知らないため、DPoP の htu を受け取ったリクエストと比較します。検証器に渡す前に、前送りされたリクエストのホストと TLS の状態を復元してください(API を保護する)。
サーバ証明書
op.FAPITLSConfig() は、TLS 1.2 に RSA 鍵の AEAD スイートだけを許可します。適合性スイートが検査する FAPI 1.0 Read-Write プロファイルの cipher allow-list に従った設定です。そのため RSA のサーバ証明書が必要です。ECDSA 証明書では使える TLS 1.2 スイートが残らず、TLS 1.2 のハンドシェイクは失敗します。TLS 1.3 は allow-list の対象外で、引き続きネゴシエートされます。ECDSA 証明書が必須の環境では、このヘルパーを使わず自前の tls.Config を組み立ててください。プロキシが TLS を終端する場合は、プロキシの cipher ポリシーが適用され、FAPITLSConfig は関与しません。OP の ES256 署名鍵は別の鍵で、影響を受けません。
複数レプリカで動かす
レプリカ間では、セッション、grant、replay 用ストアを共有します(複数インスタンス運用)。追加で必要になるのは DPoP nonce source だけで、nonce を有効にする場合に限ります。profile.FAPI2Baseline は nonce を要求しません。op.WithDPoPNonceSource を追加するなら、レプリカ間で共有する必要があります。in-memory の source が発行した値は他のレプリカでは通用しないため、op/storeadapter/redis の Store.DPoPNonceSource を使ってください。profile.FAPI2MessageSigning は、DPoP が有効なとき常にこの source を要求します。
公開面確認
curl -s http://localhost:8080/.well-known/openid-configuration | jq '{
pushed_authorization_request_endpoint,
request_parameter_supported,
dpop_signing_alg_values_supported,
token_endpoint_auth_methods_supported,
id_token_signing_alg_values_supported
}'既定の DPoP による送信者制約での期待値:
{
"pushed_authorization_request_endpoint": "http://localhost:8080/oidc/par",
"request_parameter_supported": true,
"dpop_signing_alg_values_supported": ["ES256", "EdDSA", "PS256"],
"token_endpoint_auth_methods_supported": ["private_key_jwt"],
"id_token_signing_alg_values_supported": ["ES256"]
}feature.MTLS を有効にすると、token_endpoint_auth_methods_supported に tls_client_auth と self_signed_tls_client_auth も加わります。profile.FAPI2Baseline 下では id_token_signing_alg_values_supported は ["ES256"] で、OP が発行する ID Token はすべて ES256 署名です。FAPI プロファイルを使わない OP は自身の発行アルゴリズムを公開し、op.WithSigningAlgorithm(op.AlgRS256) の後は ["RS256"] になります。現行の FAPI 2.0 Security Profile は JWT alg として PS256、ES256、EdDSA を許可します(§5.4.1)。本ライブラリの発行アルゴリズムの選択肢(ES256 か RS256)はこれより狭く、RS256 はプロファイル下では拒否されます。dpop_signing_alg_values_supported は DPoP proof 受理用で ["ES256", "EdDSA", "PS256"] です。
適合状況
本ライブラリの profile.FAPI2Baseline はポリシー設定であり、配備が最終版 Security Profile に適合していることを主張するものではありません。最終版 Security Profile との関係 に挙げた差分は、仕様より厳しく設定した部分です。TLS の設定、クライアントの挙動、認定は別の手順になります。
FAPI 2.0 の plan に対する OpenID Foundation 適合性スイートの結果は、スイートのバージョンと実行日とともに OFCS 適合状況 にあります。