Shared Signals
op.WithSharedSignals() を指定すると、OP は Shared Signals Framework(SSF)1.0 の transmitter になります。「このユーザのセッションが終了した」「このアカウントが無効化された」といったユーザに関するセキュリティイベントを、署名付きの Security Event Token(SET、RFC 8417)として他のシステムへ知らせます。Receiver は、受信を許可されたこの OP の通常の OAuth クライアントです。他の transmitter からイベントを受け取る機能はありません。
既定では無効です。オプションを指定しなければ、エンドポイントも worker も scope も作られません。
有効化
op.New(
/* 必須オプション */
op.WithStore(myStore), // store.SharedSignalsStore を実装していること
op.WithCookieKeys(currentKey, previousKey),
op.WithSharedSignals(),
)次の条件をすべて満たさないと、op.New は起動を拒否します。
- store が
store.SharedSignalsStoreを実装し、SSFStreams()とSSFEvents()の両方が実体のある substore を返すこと。 Store.Grants()がstore.GrantClientListerを実装していること。OP はこれで、ある subject が grant を持つクライアントを調べます。WithCookieKeysを指定していること。Receiver が push 配送のために渡すauthorization_headerは、cookie 鍵のリングから派生した鍵で AES-256-GCM により封印して保存するため、平文では保存されません。リングを通常どおりローテーションしても、現行の鍵、次に以前の鍵の順に試すので封印済みのヘッダは読めます。リング内のどの鍵でも開けないヘッダは、reasonsealのssf.push_abandonedとして報告されます。その場合、Receiver は stream をPUTし直す必要があります。WithScopeでssf.で始まる scope を登録していないこと。この接頭辞は予約されています。
cookie 鍵のリングが無いとヘッダを封印する鍵がないため、このオプションはリング無しでは有効にできません。
インメモリ、SQL(SQLite、MySQL、PostgreSQL)、Redis、DynamoDB、composite の各アダプタが、この拡張を実装しています。Redis アダプタは単独では一部の substore しか持たないため、composite アダプタ経由で使ってください。SSF の substore に route が無い composite store は、クライアントを扱う backend にフォールバックします。stream を変更すると、その stream のキューにあるイベントも同じ操作で動かすため、2 つの substore は同じ backend に置く必要があります。独自の store には 2 つの substore(SSFStreamStore と SSFEventStore)が必要で、op/store/contract の RunSSF が契約テストになります。SQL アダプタは 2 つのテーブルをほかのスキーマと同じように作成し、ValidateSchema はこの機能を有効にするかどうかにかかわらずそれらを要求します。
エンドポイント
| エンドポイント | 既定のパス(mount prefix の内側) | Endpoints のフィールド |
|---|---|---|
| stream の設定 | /ssf/streams | SSFConfiguration |
| stream の status | /ssf/status | SSFStatus |
| verification | /ssf/verify | SSFVerify |
| poll | /ssf/poll/{stream_id} | SSFPoll |
transmitter のメタデータは /.well-known/ssf-configuration(issuer に path があればそれを付けた形)で配信します。保護リソースメタデータと同じく、mount prefix の外側に置かれます。spec_version は 1_0、配送方式は push と poll、default_subjects は "ALL" です。OIDC discovery は変わりません。
Receiver を登録する
Receiver は、クライアントストアにほかのクライアントと同じように登録するクライアントです。次の 2 点で Receiver になります。
client_credentialsgrant が許可されている。Scopesにssf.read、ssf.manageのいずれか、または両方が含まれる。
2 つの scope は OP が自分で登録します(public ではありません)。どのクライアントがそれを要求できるかは、client.Scopes だけで決まります。
| scope | できること |
|---|---|
ssf.read | 自分の stream と status の取得、poll |
ssf.manage | 上記に加え、stream の作成・更新・置換・削除、status の設定、verification の要求 |
Receiver は token endpoint で client_credentials grant によりアクセストークンを取得し、Authorization ヘッダに付けて SSF のエンドポイントを呼びます。受け付けるのは Receiver が自分のために取得したトークンだけで、ユーザーに代わって発行されたトークンは ssf. の scope を持っていても拒否します。JWT 形式と opaque 形式のどちらのアクセストークンも受け付け、DPoP や mTLS の束縛は /userinfo と同じ規則で検査します。resource を付けて取得したトークンは、audience が issuer ではなくなるため拒否します。scope が足りない場合は RFC 6750 の標準的な challenge を返し、別のクライアントのトークンでは他者の stream が見えません(応答は 404 です)。
stream のライフサイクル
Receiver が持てる stream は 1 本までです。2 本目の作成は 409 になります。
- 作成(
POST /ssf/streams): Receiver がdelivery、events_requested、任意でdescriptionを送ります。deliveryを省略すると poll の stream になります。stream の audience は Receiver のclient_idです。events_deliveredは、Receiver が要求したものと OP が対応するものの共通部分です。作成の直後に、OP がその stream へ verification イベントを送ります。 - 取得(
GET)、更新(PATCH)、置換(PUT)、削除(DELETE):PATCHは送ったメンバーだけを変更し、新しいauthorization_headerを渡さない限り元のヘッダを保持します。ただし push stream のendpoint_urlを別の URL に変えるPATCHではヘッダを送り直す必要があり、送らなければ 400 を返します。保存済みの資格情報が、渡された先とは別のホストに送られることはありません。PUTは省略したメンバーを、ヘッダを含めて削除します。stream を削除すると、キューに残ったイベントも削除されます。取得の応答にauthorization_headerは含まれません。 - status(
/ssf/status):enabled、paused、disabledのいずれかです。pausedは通常のイベントをキューに保留し、enabledに戻したときに送ります。disabledは、キューにある通常のイベントを破棄し、新しいイベントも受け付けません。- status を変更するたびに
stream-updatedイベントがキューに入り、verification を要求すると verification イベントがキューに入ります。どちらも status にかかわらず配送されるため、stream を無効にした Receiver にもそのことが伝わります。
- verification(
POST /ssf/verify): Receiver がstateを指定して、verification イベントの送信を OP に依頼します。保留中の verification は stream ごとに 1 件までで、新しい要求は古いものを置き換えます。
push と poll
| push(RFC 8935) | poll(RFC 8936) | |
|---|---|---|
| 開始する側 | OP が Receiver の endpoint_url へ POST する | Receiver が /ssf/poll/{stream_id} へ POST する |
| 受領の通知 | Receiver が 202 を返す | 次の poll リクエストの ack |
| 失敗したとき | スケジュールに従って再送する | 受領されるまでキューに残る |
push。 op.New が起動し Provider.Shutdown が止める背景 worker が、5 秒ごとに期限の来た SET を取り出し、バックログがなくなるまで取り出しを続けます。1 回に取り出すのは最大 32 件で、1 つの stream からは最大 4 件です。1 つの stream の SET は 1 件ずつ順に送り、並列に扱う stream は最大 8 つです。Receiver が SET を受け取らなかった場合、その stream の残りは次の回に回り、再送の回数には数えません。遅い Receiver や止まった Receiver があっても、ほかの Receiver への配送は遅れません。取り出しの負荷は、取り出す件数と push 待ちのイベントを持つ stream の数で決まり、特定の stream にどれだけ溜まっているかには左右されません。202 で配送完了です。400 は SET をただちに破棄します(ssf.push_rejected)。5xx、429、ネットワーク失敗は、約 1 分後、5 分後、25 分後、2 時間後(それぞれ最大 10% の jitter 付き)に再送し、4 回目の再送でも届かなければ SET を破棄して ssf.push_abandoned を発火します。7 日たっても配送されていない SET はキューから削除されます。Receiver の endpoint_url は https の URL でなければなりません。redirect は辿らず、private・loopback・クラウドメタデータのアドレスは接続時に拒否します。
poll。 Receiver は {} を POST して保留中の SET を受け取り、処理済みの jti を次のリクエストの ack に列挙します。ack した SET は二度と返りません。ack していない SET は再び返ります。maxEvents の既定は 32、上限は 256 で、maxEvents: 0 は ack だけを行います。保留中の SET が無く returnImmediately が false または省略のときは、OP は SET が届くまで最大 10 秒待ってから応答します(long poll)。処理できなかった SET は setErrs で報告でき、その SET は破棄されて ssf.poll_set_error が発火します。
署名。 SET は、キューに入れる時ではなく送る時に、OP のそのときの active な署名鍵で署名します。そのためキューにあるイベントは鍵のローテーションをまたいで生き残り、Receiver は OP がいま公開している JWKS で検証できます。再送しても SET の jti は変わりません。CAEP Interoperability Profile に従う Receiver は RS256 を前提にしている(section 2.6)ので、その場合は op.WithSigningAlgorithm(op.AlgRS256) で OP を設定してください。
private ネットワークと開発用のオプション
| オプション | 効果 |
|---|---|
op.WithAllowPrivateNetwork(op.OutboundSharedSignals) | RFC 1918 と IPv6 ULA の push エンドポイントを許可します。クラウドメタデータのアドレスは引き続き拒否します |
op.WithAllowInsecureOutboundForDev(op.OutboundSharedSignals) | http と loopback の push エンドポイントを許可します。issuer 自体が開発用ホスト(loopback、localhost、.localhost / .test / .invalid / .example の名前)でなければ、op.New は拒否します |
どちらも、同じマシン上の Receiver を相手にしたローカルでの動作確認にだけ使ってください。
Receiver に届くイベント
ユーザに関するイベントは、そのユーザが Receiver のクライアントに対する grant を持ち、かつ Receiver の stream がそのイベント型を要求している場合にだけ、その Receiver のキューに入ります。ここでの default_subjects: "ALL" の意味はこれです。システム内の全ユーザではなく、Receiver のクライアントに対する grant を持つすべての subject を指します。SET の sub_id は iss_sub 形式で、そのクライアントから見える subject を入れるため、pairwise のクライアントには pairwise の値が届きます。
session-revoked は OP 自身が生成します。
- RP-Initiated Logout は、終了させたセッション 1 件につき 1 件送ります。複数のセッションを持つユーザでは、1 つの Receiver に
session-revokedが複数届きます。OP は SET にセッション ID を意図的に載せません。 Provider.RevokeSubjectは、grant を削除する前に 1 件送ります。
それ以外はアプリケーションが通知します。OP が知り得ない事象だからです。
err := provider.EmitSecurityEvent(ctx, op.CredentialChange{
Subject: userID,
CredentialType: op.CredentialTypePassword,
ChangeType: op.CredentialChangeUpdate,
})イベント型は、CAEP の CredentialChange と SessionRevoked、RISC の AccountDisabled、AccountEnabled、AccountPurged、CredentialCompromise です。RISC の型は experimental です。CAEP のイベントには必ず reason_admin が入り、空のままなら OP が既定の文言を補います。
EmitSecurityEvent は変更の前に呼ぶ
Receiver はユーザの grant をたどって決まります。RevokeSubject の後やユーザを削除した後に EmitSecurityEvent を呼ぶと、subject の grant はもう存在せず、イベントは誰にも届きません。先にイベントを通知してから、失効または削除してください。
WithSharedSignals を指定していないとき、EmitSecurityEvent は何もせず nil を返します。そのためオプションの付け忘れは呼び出し側からは見えません。Subject が空の場合や未知の列挙値は、オプションの有無にかかわらずエラーです。
順序
1 つの stream の中での順序は保証されません。配送は並列で、再送によって順序が入れ替わるため、Receiver は SET の event_timestamp で並べ替えてください。
監査イベント
transmitter は 10 種類の ssf.* イベントを発火します。一覧は監査イベントカタログにあります。いずれも監査専用で、metrics は更新しません。特に ssf.push_abandoned と ssf.enqueue_failed を監視してください。前者は Receiver がイベントを受け取り損ねたことを、後者はイベントがキューに入らなかったことを示します。後者の場合も、イベントの発生元のログアウトや失効は失敗させません。
複数インスタンス
同じ store に対して、いくつの OP インスタンスでも push worker を動かせます。store は期限の来たイベントを同時に 1 つの worker にしか渡さないため、複数のインスタンスが同じ SET を二重に push することはありません。1 つの stream への変更が複数のインスタンスに同時に届いても、キューは整合したまま残り、キューにある各イベントは stream の最終的な status と配送方式に揃います。共有 store の要件全般はマルチインスタンス展開を参照してください。