Skip to content

クライアント種別 ​

クライアント とは、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 クライアント。
このページで触れる仕様

public クライアント ​

public クライアントは、ユーザが制御するデバイス上でコードが動くアプリケーションです。SPA、モバイルアプリ、ネイティブデスクトップアプリ、CLI が該当します。バイナリやページに同梱したものは攻撃者の手にも渡る ため、長寿命のシークレットを隠しておく場所がありません。

いつ使うか ​

public クライアントは「ユーザの端末で人をログインさせる」クライアントに使います。

  • ブラウザ内 SPA が /auth にリダイレクトし、JavaScript から /token で認可コードと PKCE の verifier を交換する。
  • モバイル / デスクトップアプリが カスタム URI スキーム、claimed https、または loopback リダイレクトで認可コードを受け取る。
  • CLI がローカルの loopback リスナを開き、ブラウザログイン後に認可コードを受け取る。

バックエンドだけで動くサーバ側 RP やサービス間呼び出しでは、secret や秘密鍵を保持できるので confidential クライアントを選びます。

なぜ仕様として分かれているか ​

クレデンシャルは、クライアントがそれを秘密にしておけて初めて認証の役に立ちます。SPA やモバイルアプリに client_secret を要求すると、開発者は JavaScript のバンドル、アプリのバイナリ、設定ファイルのどこかに埋め込むしかありません。その値は配布先の全ユーザ、つまり攻撃者にも渡るので、仕様上は秘密として扱えません。

jsonc
// 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 水準のクライアントは非対称メソッドに行き着きます。

クライアントの構造が認証メソッドを決める
3 種類のクライアント構造と、それぞれが到達するトークンエンドポイント認証メソッドを示した図。public クライアントは none と PKCE、confidential クライアントは client secret、FAPI 相当のクライアントは private_key_jwt と送信者制約を使います。クライアントの構造token_endpoint_auth_methodpublic クライアントSPA · ネイティブアプリ · CLI秘密を保持できないnonePKCE (S256) を必ず併用するconfidential クライアントバックエンド web アプリ · サービスsecret を保持するclient_secret_basicclient_secret_postどちらかを選んで登録するFAPI 相当のクライアント送信者制約付き · 高保証FAPI 2.0 プロファイルprivate_key_jwt加えて DPoP または mTLS の送信者制約
構造は選んで名乗るものではなく、コードがどこで動くかによって決まります。ブラウザや端末に配布されるものは秘密を保持できないため、登録内容が何であれ public クライアントです。
メソッド事前共有シークレット?非対称?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.PublicClientnoneなし
op.ConfidentialClientclient_secret_basic(既定)または client_secret_post(AuthMethod で指定)Secret(Argon2id でハッシュ化)または SecretHash
op.PrivateKeyJWTClientprivate_key_jwtJWKS(公開鍵セット。op.LoadPublicJWKS の戻り値など)
go
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 入門 — 高保証デプロイ向けにすべてを束ねるプロファイル。