クライアント種別
クライアント とは、OP にトークンを要求するアプリケーションのことです。クライアントは public(シークレットを保持できない)と confidential(保持できる)の 2 つに分かれ、FAPI 2.0 構成は confidential の上にさらに高い要件を積みます。どちらに属するかで token endpoint での認証メソッドが決まり、その選択がリフレッシュトークンのポリシー、送信者制約の要否、同意画面をスキップできるかどうかに影響します。
30 秒でつかむ要点
- public クライアント: SPA、ネイティブアプリ、モバイルアプリ。コードがユーザの手に届くので、中に入れたシークレットは秘密になりません。
none+ PKCE を使います。 - confidential クライアント: バックエンドサービス、機械間呼び出し。シークレットや秘密鍵を保持できます。
client_secret_*またはprivate_key_jwtを使います。 - FAPI 水準のクライアント: 送信者制約付きトークン(DPoP / mTLS)、PAR、非対称鍵認証で強化された confidential クライアント。
このページで触れる仕様
- RFC 6749 — OAuth 2.0 §2.1(クライアント種別)、§2.3(クライアント認証)
- RFC 7521 / RFC 7523 — Assertion ベースのクライアント認証
- RFC 7636 — PKCE
- RFC 8252 — OAuth 2.0 for Native Apps
- RFC 8705 — Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
- RFC 9449 — DPoP
- OpenID Connect Core 1.0 — §9(クライアント認証)
- FAPI 2.0 Security Profile — §5.3.2.1(許可されるクライアント認証方式)
public クライアント
public クライアントは、ユーザが制御するデバイス上でコードが動くアプリケーションです。SPA、モバイルアプリ、ネイティブデスクトップアプリ、CLI が該当します。バイナリやページに同梱したものは攻撃者の手にも渡る ため、長寿命のシークレットを隠しておく場所がありません。
いつ使うか
public クライアントは「ユーザの端末で人をログインさせる」クライアントに使います。
- ブラウザ内 SPA が
/authにリダイレクトし、JavaScript から/tokenで認可コードと PKCE の verifier を交換する。 - モバイル / デスクトップアプリが カスタム URI スキーム、claimed
https、または loopback リダイレクトで認可コードを受け取る。 - CLI がローカルの loopback リスナを開き、ブラウザログイン後に認可コードを受け取る。
バックエンドだけで動くサーバ側 RP やサービス間呼び出しでは、secret や秘密鍵を保持できるので confidential クライアントを選びます。
なぜ仕様として分かれているか
クレデンシャルは、クライアントがそれを秘密にしておけて初めて認証の役に立ちます。SPA やモバイルアプリに client_secret を要求すると、開発者は JavaScript のバンドル、アプリのバイナリ、設定ファイルのどこかに埋め込むしかありません。その値は配布先の全ユーザ、つまり攻撃者にも渡るので、仕様上は秘密として扱えません。
// NG: SPA bundle に secret を同梱するしかなく、secret として扱えない
{
"client_id": "browser-spa",
"token_endpoint_auth_method": "client_secret_basic",
"client_secret": "shipped-in-javascript"
}
// OK: public client として登録し、code + PKCE で守る
{
"client_id": "browser-spa",
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code"],
"redirect_uris": ["https://app.example.com/callback"]
}そこで OAuth / OIDC は public クライアントを明示的に認め、別の仕組みで守ります。登録済み client_id、redirect URI の完全一致、リクエストごとの PKCE、リフレッシュトークンのローテーション、必要に応じた DPoP です。public クライアントは「信用しないクライアント」ではなく、「シークレットを保持できない前提で別の防御を積むクライアント」です。
本ライブラリは TokenEndpointAuthMethod が "none" のクライアントを public として扱います。シークレットがない分は、次の保護で補います。
- PKCE が必須。OP は
S256のみを受理し、plainはプロファイルに関係なく拒否します(設計判断 #14)。code_verifierは、フローを開始したクライアントと終わらせるクライアントが同じであることを示すリクエストごとのシークレットです。これがなければ、リダイレクトで奪われた認可コードだけでトークンが発行されてしまいます。 - リフレッシュトークンはローテーションされ、DPoP に結び付けられる。リフレッシュ交換が成功するたびに前のリフレッシュトークンは無効化され、次のトークンはクライアントが提示した DPoP 鍵に結び付けられます。public クライアントで漏洩したリフレッシュトークンは、対応する鍵がなければ再利用できません。すでにローテーション済みのトークンが再提示された場合は連鎖全体が失効します。詳細は設計判断 #15。
- loopback リダイレクトが許容される。RFC 8252 §7.3 により、CLI は OS が割り当てる一時ポートを
127.0.0.1/[::1]上で利用できます。詳細はRedirect URI。
public クライアントも ID トークンとリフレッシュトークンを受け取り、/userinfo を呼び出せます。厳しい条件下で動くだけで、RP としての機能は変わりません。
confidential クライアント
confidential クライアントは、シークレットや秘密鍵をユーザの手が届かないストレージ(サーバ側の環境変数、シークレットマネージャ、HSM など)に保管できるバックエンドサービスです。TokenEndpointAuthMethod には "none" 以外を登録します。
confidential クライアントには次の扱いが加わります。
- token endpoint での認証。すべての
POST /tokenでクレデンシャルを送り、OP は何かを発行する前にそれを検証します。検証メソッドは下表のいずれかです。 - code フローでの PKCE。既定プロファイルの
profile.OAuth21は confidential クライアントにも PKCE を要求します。任意になるのは、旧来の RP 向けにop.WithProfile(profile.OIDCCore)を宣言した場合だけです。セキュリティプロファイルの宣言 を参照してください。 - 緩めのリフレッシュトークンポリシー。他のクライアントと同様にローテーションされます(2 つの FAPI 2.0 プロファイルではローテーションしません)が、連鎖自体は既定では DPoP に結び付けられません — 各リフレッシュで発行されるアクセストークンは提示された鍵に結び付けられますが、リフレッシュトークンの連鎖そのものはリフレッシュごとに鍵を切り替えられます。詳細は設計判断 #15。
- ファーストパーティの自動同意の対象になり得る。
op.WithFirstPartyClients(...)で、審査済みの社内クライアントの同意画面をスキップできます。対象は静的シード由来と管理者がプロビジョンしたクライアントだけで、DCR 由来は意図的に除外されます。FAPI 2.0 プロファイルとの併用はop.Newが拒否します。
FAPI 水準のクライアント
FAPI 2.0 Security Profile は、confidential クライアント、PAR、送信者制約付きアクセストークン(DPoP または mTLS)、mTLS または private_key_jwt によるクライアント認証を要求します。署名付きリクエストオブジェクトは、この仕様の必須要件ではありません。本ライブラリの profile.FAPI2Baseline は、追加で JAR を要求し、トークンエンドポイントのクライアント認証には private_key_jwt と、feature.MTLS が有効な場合の mTLS 2 方式だけを許可します(後述)。詳しくは FAPI 2.0 入門 を参照してください。
プロトコル上「FAPI クライアント種別」が別にあるわけではありません。FAPI 水準のクライアントは、追加要件を満たした confidential クライアントです。本ライブラリは構築時とリクエスト時にその要件を強制します。
認証メソッド
本ライブラリは IANA の "OAuth Token Endpoint Authentication Methods" レジストリにある 6 つの方式を op.AuthMethod 定数として持っています。4 方式はどの構成でも使え、mTLS によるクライアント認証 2 方式には feature.MTLS が必要です。
次の図は、クライアントの構造から登録する token_endpoint_auth_method の値までを辿ります。public クライアントは none、confidential クライアントは client_secret_* 系、FAPI 水準のクライアントは非対称メソッドに行き着きます。
| メソッド | 事前共有シークレット? | 非対称? | FAPI 2.0? | 主な用途 |
|---|---|---|---|---|
none | — | — | 不可 | public クライアント(SPA、ネイティブ、CLI)+ PKCE。 |
client_secret_basic | あり | — | 不可 | 共有シークレット保持のバックエンド。HTTP Basic で送る。confidential クライアントの既定。 |
client_secret_post | あり | — | 不可 | 同じシークレットを form のボディで送る。新規デプロイには非推奨 — Basic が標準的な通信路上の形式。 |
private_key_jwt | — | あり | 可 | バックエンドが自分の秘密鍵で短寿命の JWT アサーションに署名し、OP が登録済み JWKS で検証。本ライブラリの FAPI 設定で使う方式。 |
tls_client_auth | — | あり(PKI) | 可(feature.MTLS が必要) | クライアントは、OP がチェーンを検証した証明書で認証し、登録された subject DN または SAN の 1 つと照合されます。op.TLSClientAuthClient でシードします。 |
self_signed_tls_client_auth | — | あり(pin) | 可(feature.MTLS が必要) | 証明書の公開鍵が、クライアントの JWKS にある鍵と一致する必要があります。op.SelfSignedTLSClientAuthClient でシードします。 |
feature.MTLS がないと、discovery はどちらの方式も載せません。この方式を使うクライアントがあるなら、feature を有効にしてください。両方式の詳細は mTLS にあります。
mTLS で結び付けたアクセストークン(RFC 8705 §3)は別の機能で、この 2 方式を必要としません。feature.MTLS を有効にすると、クライアントが証明書を提示した場合、どの方式で認証したかにかかわらず cnf.x5t#S256 が付きます。
client_secret_jwt(HS256 共有シークレット JWT)は 実装していません。共有シークレット JWT は漏洩時の被害を広げるだけで、private_key_jwt にない利点がありません。
private_key_jwt の assertion は次のように厳密に検査します。
issとsubはclient_idと一致し、audは受理する audience のいずれか(Issuer を参照)で、expは現在から最長 5 分先までです。jtiは必須で、256 文字以下、かつ 1 回限りです。再送された assertion はinvalid_clientになります。jwks_uriで登録したクライアントが未知のkidで署名した場合は、検証失敗の前に、1 回だけ頻度を制限したうえで、キャッシュを迂回して JWKS を再取得します。正当な鍵ローテーションにはすぐ追従でき、ランダムなkidによる外向き取得の増幅は起きません。- 過負荷や一時的な障害でクライアントの JWKS を取得できないときは、
/tokenがinvalid_clientではなく503 server_errorを返します。クライアントストアの障害では500 server_errorです。どちらもクライアント側で再試行できます。
メソッドはクライアントごとに 1 つに絞る
discovery 文書には OP が受理するすべてのメソッドが列挙されますが、各クライアントが登録するのは正確に 1 つです。1 クライアント上での複数メソッド混在は標準化されておらず、ダウングレード攻撃の研究論文の元ネタになってきました — 本ライブラリは登録メソッドと正確に照合し、フォールバックを拒否します。
プロビジョニング方法
クライアントとその TokenEndpointAuthMethod が OP に届く経路は 2 つあります。
静的シード は op.WithStaticClients(seeds ...ClientSeed) で渡します。シードは 3 種類の型付きビルダのどれかで、認証メソッドはビルダが決めます。
| ビルダ | 登録されるメソッド | クレデンシャルのフィールド |
|---|---|---|
op.PublicClient | none | なし |
op.ConfidentialClient | client_secret_basic(既定)または client_secret_post(AuthMethod で指定) | Secret(Argon2id でハッシュ化)または SecretHash |
op.PrivateKeyJWTClient | private_key_jwt | JWKS(公開鍵セット。op.LoadPublicJWKS の戻り値など) |
jwks, err := op.LoadPublicJWKS("keys/fapi-client.jwks.json")
if err != nil {
return err
}
provider, err := op.New(
// ...
op.WithStaticClients(
op.PublicClient{
ID: "web-spa",
RedirectURIs: []string{"https://app.example.com/callback"},
Scopes: []string{"openid", "profile"},
},
op.ConfidentialClient{
ID: "billing-backend",
Secret: os.Getenv("BILLING_CLIENT_SECRET"),
RedirectURIs: []string{"https://billing.example.com/callback"},
Scopes: []string{"openid"},
},
op.PrivateKeyJWTClient{
ID: "bank-connector",
JWKS: jwks,
RedirectURIs: []string{"https://bank.example.com/callback"},
Scopes: []string{"openid"},
},
),
)シードはすべて構築時に検証され、不正なものがあれば op.New は起動を拒否します。特に重要なルールは次の 2 つです。
client_secret_basicまたはclient_secret_postを使うConfidentialClientには、SecretかSecretHashが必須です。どちらもないシードは拒否されます。空のハッシュでは空のclient_secretが通ってしまうためです。平文のSecretが 32 byte 未満の場合も拒否されます。op.NewClientSecretが secret とそのSecretHashを返します。- クライアント記録は
jwksとjwks_uriを同時に持てません(RFC 7591 §2)。動的登録はこの組み合わせを拒否し、自前実装のストアなどで両方を持つ記録ができても、リクエストオブジェクトの検証、private_key_jwt、レスポンス暗号化のどれでも拒否されます。
3 つのビルダはいずれも DefaultMaxAge(*int64、秒)、RequireAuthTime、DefaultACRValues を取ります。意味は登録メタデータの default_max_age、require_auth_time、default_acr_values と同じです。DefaultMaxAge はリクエストが max_age を省略したとき、DefaultACRValues は acr_values を省略したときに適用されます。op.New は負の DefaultMaxAge と、OP が広告していない既定の acr 値を拒否します。
静的シードは、社内ツール、FAPI デプロイ、シングルテナント SaaS のような既知のクライアント集団に向いています。
動的クライアント登録(RFC 7591) は op.WithDynamicRegistration(...) で有効にします。RP は POST /register で実行時に自己登録します。OP は同じ検証を適用し、生成した client_secret を保管時にハッシュ化し、平文は 1 回だけ返します(設計判断 #20)。RFC 7592 の管理エンドポイントも公開されます。マーケットプレース、マルチテナント SaaS、OP の運用者がクライアントを事前に把握できない場面に向いています。
2 つの経路は同じ OP 上で共存できます。静的な FAPI クライアントの群れと、パートナー連携用の動的クライアントを同居させる構成はよくあります。
public クライアントも登録は必要
「public クライアント」は「誰でも飛び込んで来られる」という意味ではありません。client_id は静的シードか DCR のどちらかで発行されたもので、OP は知らない client_id のリクエストを拒否します。PKCE のみの認証は、OP がすでに知っている client_id が対象で、「無登録モード」ではありません。
次に読む
- scope と claim — 認証を済ませたクライアントが何を要求できるか。
- 送信者制約 — DPoP / mTLS でアクセストークンをクライアントが保持する鍵に結び付ける。FAPI 水準の必須要件。
- 動的クライアント登録 — 実行時自己登録の経路とライフサイクル全体。
- FAPI 2.0 入門 — 高保証デプロイ向けにすべてを束ねるプロファイル。