From 405270bbe09b700fa69ab4bc707aca2ac18303ac Mon Sep 17 00:00:00 2001 From: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Date: Thu, 2 Jul 2026 20:28:03 -0400 Subject: [PATCH 1/4] docs(nips): add NIP-FI federated identity authorization draft Normative spec for authorizing a NIP-42/NIP-98-proven Nostr key only when a valid OIDC/JWT assertion resolves to the same active identity-to-key binding. Covers assertion transport (trusted-proxy and client-attached profiles), validation, per-domain binding bijection, enrollment modes (attested-key/provisioned/tofu), lease/session bounds, revocation and rotation, delegation composition, rejection semantics, discovery, and privacy. Companion formal model to follow on this branch. Generalizes the semantics implemented in #1476 into a provider-neutral standard per buzz-security thread 0d1aee9e. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- docs/nips/NIP-FI.md | 149 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 docs/nips/NIP-FI.md diff --git a/docs/nips/NIP-FI.md b/docs/nips/NIP-FI.md new file mode 100644 index 0000000000..3d5c9f31ca --- /dev/null +++ b/docs/nips/NIP-FI.md @@ -0,0 +1,149 @@ +NIP-FI +====== + +Federated Identity Authorization +-------------------------------- + +`draft` `optional` `relay` + +**Depends on**: NIP-01 (basic event format), NIP-42 (Authentication of Clients to Relays). **Composes with**: NIP-98 (HTTP Auth), NIP-11 (Relay Information Document), NIP-OA (Owner Attestation). + +## Abstract + +This NIP defines how a relay or Nostr-adjacent HTTP service authorizes an already-authenticated Nostr key only when a valid federated identity assertion (an OIDC/JWT credential from an external identity provider) resolves to the same principal. It specifies assertion transport, validation, an identity-to-key binding lifecycle (enroll, conflict, revoke, rotate), session semantics, and failure behavior. + +The identity provider never becomes a Nostr signing authority, and the assertion never substitutes for Nostr proof of key control. This NIP is an authorization layer above NIP-42 and NIP-98, not a replacement for either. + +## Motivation + +Organizations deploying Nostr internally need relay access tied to their workforce identity system: an employee's relay privileges should follow their corporate identity, survive Nostr key rotation, and end at offboarding. Existing primitives each solve part of this: + +- NIP-42 proves control of a Nostr key to a connection but carries no external identity. +- NIP-05 maps an organization-controlled identifier to a pubkey, but by public DNS/HTTPS polling, not by a credential presented on the request being authorized. +- NIP-46 lets a signer demand out-of-band authentication (`auth_url`) but does not bind the resulting external subject to a key at the relay. + +Without a standard, each deployment invents an incompatible binding scheme, and the first large deployment's configuration becomes an accidental protocol. This NIP defines the contract so that any relay behind any OIDC-capable identity provider or generic OAuth2 reverse proxy (Okta, Auth0, Keycloak, oauth2-proxy, etc.) can interoperate with any conforming client. + +## Definitions + +- **assertion**: a JWT issued by a configured identity provider, presented alongside (never instead of) Nostr authentication. +- **federated identity** (`i`): the tuple `(iss, sub)` from a validated assertion. The `iss` value MUST be the exact validated issuer identifier and `sub` the exact non-empty subject string. A username, email, display name, or bare `sub` MUST NOT be used as a federated identity. +- **authorization domain** (`D`): the scope within which bindings apply, chosen by the service (an entire relay, or one tenant of a multi-tenant relay). Bindings MUST NOT cross domains implicitly. +- **binding**: an active record associating exactly one federated identity with exactly one 32-byte Nostr public key within a domain. +- **enrollment mode**: the domain's policy for creating bindings — `attested-key`, `provisioned`, or `tofu` (defined below). +- **Nostr proof**: a valid NIP-42 AUTH event (WebSocket) or NIP-98 event (HTTP) proving control of a key on the current connection or request. +- **lease**: a cached authorization decision for one `(domain, identity, key)`, bounded by the assertion's expiry. + +## Assertion transport + +An assertion reaches the verifier in an HTTP header on the request being authorized: the WebSocket upgrade request for relay connections, or each individual request for NIP-98-authenticated HTTP endpoints. Two transport profiles are defined; a service MUST document which it accepts. + +1. **Trusted proxy**: an authenticating reverse proxy (for example oauth2-proxy or an SSO-aware ingress) injects the assertion header after authenticating the user. This profile is conforming only if untrusted clients cannot reach the verifier directly and the proxy strips any inbound copy of the header before setting it. This is the recommended profile for browser-based clients, which cannot attach arbitrary WebSocket upgrade headers. +2. **Client-attached**: the client sends the assertion itself, in the `Authorization: Bearer` header or a service-configured header. + +The header name is deployment configuration; `Authorization` semantics apply when it is used. A value with a `Bearer ` prefix MUST be accepted with the prefix stripped. + +On a WebSocket connection, the assertion captured at upgrade is evaluated when a key performs NIP-42 AUTH — each authenticating key is authorized against that assertion independently. On HTTP, the assertion and the NIP-98 proof MUST arrive on the same request they authorize. + +Assertions MUST NOT be carried inside Nostr events, event tags, or subscription filters, and MUST NOT be written to relay-visible event history. + +## Assertion validation + +The verifier is configured, per accepted issuer, with: the issuer identifier, a signing-key source (a JWKS endpoint, discoverable via OIDC `/.well-known/openid-configuration`), accepted audience values, and a claim mapping. Validation MUST enforce all of the following; any failure MUST reject the assertion: + +1. The JWT signature verifies under a currently trusted key for an explicitly allowed **asymmetric** algorithm. Symmetric (HS*) and `none` algorithms MUST be rejected before any key lookup. +2. `iss` exactly equals the configured issuer identifier used to select the verification key. +3. At least one `aud` value exactly equals a configured audience. +4. `exp` is present and in the future; `nbf` and `iat`, when present, are not in the future — each within a bounded, configured clock skew. +5. The configured subject claim is present and a non-empty string. A configured claim that is absent when required, not of its expected type, or not unambiguously a single value MUST be rejected. +6. If a key claim is configured and present, it parses to exactly one 32-byte Nostr public key. Lowercase hex is the canonical encoding; `npub` bech32 MAY be accepted as a documented input normalization. + +A display-name claim MAY be extracted as mutable metadata. It MUST NOT participate in any authorization decision. + +Signing-key retrieval failures MUST fail closed. Verifiers SHOULD cache the key set with a bounded lifetime and SHOULD NOT refetch it in response to an unknown `kid` that was absent from a freshly fetched set, so that forged tokens cannot drive request floods to the identity provider. + +## Nostr proof + +The key being authorized is always the key returned by Nostr proof validation — a valid NIP-42 AUTH for the current WebSocket connection, or a valid NIP-98 event for the current HTTP request. It is never taken from an assertion claim, an unsigned request field, or client metadata. A bearer assertion alone MUST NOT authenticate a Nostr key. + +## Authorization + +Given a validated assertion yielding identity `i`, optional asserted key `k_a`, and expiry `exp`, and a Nostr proof yielding key `k`, the verifier evaluates one atomic decision in domain `D`: + +```text +Authorize(D, i, k_a?, k): + if k_a exists and k_a != k: DENY (key mismatch) + + b_i := active binding for i in D, if any + b_k := active binding for k in D, if any + + if b_i = (i, k) and b_k = (i, k): ALLOW (existing binding) + if b_i exists or b_k exists: DENY (binding conflict) + + # no active binding on either side: enrollment + attested-key: k_a required, else DENY; create (i, k); ALLOW + provisioned: DENY (binding must be pre-created by an operator) + tofu: create (i, k); ALLOW +``` + +The check and any insertion MUST be atomic for `(D, i, k)`: under concurrent first use of the same identity or key, at most one binding is created and every other attempt observes it (allow on exact match, deny on conflict). Storage failure or a lost race MUST deny — never fall back to an unchecked allow. + +### Enrollment modes + +- **`attested-key`**: the identity provider carries the user's Nostr public key in the configured key claim. First use binds only when the asserted key equals the proven key. This is the strongest mode and SHOULD be used when the identity provider can carry custom claims. +- **`provisioned`**: bindings are created only through an out-of-band administrative process; requests never create bindings. +- **`tofu`** (trust on first use): first use of an unbound identity with an unbound key creates the binding. A stolen assertion for a never-enrolled identity can bind an attacker's key in this mode; services offering it MUST document this risk. When an assertion in `tofu` mode carries a valid key claim, the binding SHOULD record the stronger `attested-key` provenance, and a binding's recorded provenance MUST NOT be downgraded by later requests. + +### Binding invariant + +Within a domain, active bindings form a partial bijection: an identity has at most one active key and a key has at most one active identity. Every state transition in this NIP preserves this invariant. + +## Session semantics + +For HTTP requests, the decision applies to that request only. + +For a NIP-42 WebSocket connection, the relay MAY cache the decision as a lease. A lease MUST NOT be honored past the assertion's `exp` (implementations MAY enforce a shorter maximum). At expiry the relay MUST require a fresh assertion, reject protected operations, or close the connection. When a relay learns a binding was revoked, it MUST invalidate matching leases; a relay that detects revocation by polling MUST NOT claim immediate revocation and SHOULD document its detection latency. + +When multiple keys authenticate on one connection (NIP-42 permits this), authorization is tracked per key. A lease for one key MUST NOT authorize operations attributed to another. + +## Revocation and rotation + +Revocation is an explicit administrative or policy transition: the binding is removed from the active set and a durable revocation record is retained. A subsequent valid assertion — including one whose key claim matches the revoked key — MUST NOT reactivate a revoked binding unless the domain's documented recovery policy explicitly authorizes that transition. This prevents a replayed, still-valid assertion from silently undoing revocation. + +Key rotation is likewise explicit, never a side effect of authorization: rotating `i` from `k_old` to `k_new` requires administrative or documented recovery authorization, an active `(i, k_old)` binding, no active binding for `k_new`, and — where the domain requires issuer attestation — a fresh assertion whose key claim equals `k_new`. The old binding is revoked and the new one created atomically, and leases for `k_old` are invalidated. A routine request presenting `i` with a new key while `(i, k_old)` is active is a binding conflict and MUST be denied. + +## Delegation + +Delegation is outside the base primitive but composes with it. A service MAY admit a key that presents no assertion when a separately validated delegation proof (for example a NIP-OA `auth` tag) establishes an owner key that holds an active binding in the domain. The delegate key MUST NOT acquire a federated identity binding of its own through this path, and the delegate's authorization is bounded by both the owner's binding state and the delegation's own conditions. Revoking the owner's binding revokes the delegate's admission on the same schedule as the owner's own leases. + +## Rejection semantics + +Machine-readable rejections reuse NIP-01/NIP-42 prefixes on `OK` and `CLOSED` messages: + +- `auth-required: ` — no assertion was presented, or no NIP-42 proof has been performed. +- `restricted: ` — the assertion or proof was presented but failed validation, mismatched, conflicted with an active binding, or the identity's enrollment/binding state does not permit the operation. + +HTTP endpoints respond `401` where `auth-required` applies and `403` where `restricted` applies. Rejection bodies MUST NOT echo assertion contents, claim values, or the conflicting party's identity or key. + +## Discovery + +A relay SHOULD advertise support in its NIP-11 document under `limitation` as `"federated_identity": true`, and MAY publish a `federated_identity` object naming its accepted transport profile(s), enrollment mode, and whether delegation is honored. It MUST NOT publish issuer-internal detail (tenant URLs, claim names, audiences) that is not already public. + +## Privacy + +Federated identities are typically personal data (employee identifiers). A conforming service MUST NOT publish `iss`, `sub`, assertion contents, or display-name claims in Nostr events or tags, and MUST NOT expose another user's binding state through rejection messages. Binding records, audit logs, and metrics are service-internal, and logs MUST NOT record raw bearer assertions. + +## Security considerations + +- **Issuer or proxy compromise** impersonates federated principals, but cannot satisfy Nostr proof for an already-bound uncompromised key, and in `attested-key` mode cannot bind an arbitrary key without also forging the key claim. +- **Assertion theft** cannot authorize an already-bound identity without control of the bound key. Its remaining power — enrolling a never-bound identity — exists only in `tofu` mode, which is why that mode is risk-labeled. +- **Header injection**: the trusted-proxy profile is void if clients can reach the verifier directly or the proxy forwards inbound copies of the assertion header. Deployments MUST verify both properties. +- **Algorithm confusion** is excluded by rejecting symmetric algorithms before key selection. +- **Availability vs. safety**: issuer, key-set, and storage outages deny. Availability MUST NOT override identity safety. +- **Cross-issuer collision**: identical `sub` values under different issuers are distinct identities and MUST never collide or inherit each other's bindings. + +A companion formal model of this protocol — state machine, safety and liveness properties, and attack traces — accompanies this specification. + +## Reference implementation + +Buzz relay: corporate identity enforcement layered above NIP-42/NIP-98/media/git/audio ingress, with JWKS validation, TOFU and attested-key enrollment, atomic binding with conflict detection, and NIP-OA delegation composition. From 65b9f0ae26ca92db2cd1081d343530b0584a894a Mon Sep 17 00:00:00 2001 From: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Date: Thu, 2 Jul 2026 20:32:01 -0400 Subject: [PATCH 2/4] docs(nips): add NIP-FI formal model Define the trust assumptions, authorization predicate, binding state machine, safety and liveness properties, and adversarial traces for federated identity authorization. Tighten the normative draft with a collision-free client assertion header and concrete NIP-11 discovery shape. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- docs/nips/NIP-FI-MODEL.md | 220 ++++++++++++++++++++++++++++++++++++++ docs/nips/NIP-FI.md | 24 +++-- 2 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 docs/nips/NIP-FI-MODEL.md diff --git a/docs/nips/NIP-FI-MODEL.md b/docs/nips/NIP-FI-MODEL.md new file mode 100644 index 0000000000..831e75d8a3 --- /dev/null +++ b/docs/nips/NIP-FI-MODEL.md @@ -0,0 +1,220 @@ +# Scope + +This model specifies a relay or HTTP service authorizing a Nostr principal only when a valid federated identity assertion and a valid Nostr proof resolve to the same active identity-to-key binding. It models authorization, enrollment, revocation, and key rotation. It does not publish the federated identity on Nostr and does not make the identity provider a Nostr signing authority. + +The model is transport-independent. A concrete NIP must separately define how an assertion reaches a verifier and how support is advertised. NIP-42 and NIP-98 remain the mechanisms for proving control of a Nostr key; a bearer assertion alone is never a Nostr proof. + +# Terms and domains + +- `D`: authorization domain chosen by the service (for example one relay tenant). Bindings never cross domains implicitly. +- `I`: federated principal, the tuple `(iss, sub)`. `iss` is the assertion's exact validated issuer identifier and `sub` is its exact non-empty subject string. A username, email, display name, or bare `sub` is not an identity key. +- `K`: 32-byte Nostr public key. +- `A`: federated assertion. +- `P`: Nostr proof authenticating key `k`, such as a valid NIP-42 AUTH event or NIP-98 event. +- `now`: verifier time. +- `B_D`: active binding relation in domain `D`, a partial bijection between `I` and `K`. +- `R_D`: durable history of revoked bindings. +- `mode(D)`: enrollment policy, either `attested-key`, `provisioned`, or `tofu`. + +A binding record is: + +```text +Binding = (domain, identity, key, source, created_at, revoked_at?) +source = attested-key | provisioned | tofu +``` + +`display_name`, email, and similar values may be stored as mutable metadata but are never part of binding identity or an authorization decision. + +# Trust assumptions + +1. The verifier has an authenticated configuration for each accepted issuer: issuer identifier, allowed signing algorithms, key source, accepted audience(s), and claim mapping. +2. TLS and/or a trusted ingress boundary prevents attackers from injecting or replacing assertions. A reverse-proxy assertion header is trusted only when untrusted clients cannot reach the verifier directly and all inbound copies of that header are stripped before the trusted proxy sets it. +3. The issuer protects its signing keys and assigns stable, non-reassignable `sub` values within an issuer. If an issuer reassigns a subject, the model cannot distinguish the people. +4. The Nostr signature primitive is unforgeable and the concrete Nostr proof is fresh and bound to the target relay or HTTP request. +5. Binding-state transactions are serializable with respect to the same domain, identity, or key. The implementation may realize this with locks and unique constraints. +6. The verifier's clock is sufficiently accurate for assertion and proof freshness checks. + +Compromise of an accepted issuer or trusted ingress can impersonate federated principals. It still cannot satisfy Nostr proof for an already-bound uncompromised key, and in `attested-key` mode it cannot bind an arbitrary key unless the compromised issuer also attests that key. Theft of an assertion alone cannot authorize an already-bound identity without control of the bound Nostr key. + +# Assertion validity + +Let `ValidateAssertion(A, C, now)` return either `(i, k_a?, exp)` or failure under issuer configuration `C`. + +It succeeds only if all of the following hold: + +1. the signature validates under a currently trusted key and an explicitly allowed asymmetric algorithm; +2. `A.iss` exactly equals the configured issuer identifier used to select that key; +3. at least one `A.aud` value exactly equals an audience configured for this service; +4. `exp` exists and `now < exp`, allowing only a bounded configured clock skew; +5. if present, `nbf <= now` and `iat` is not unreasonably in the future; +6. the configured subject claim is a non-empty string; +7. `i = (A.iss, A.subject)`; and +8. if a configured Nostr-key claim is present, it parses to exactly one 32-byte key `k_a` (hex on the wire; bech32 may be accepted only as an explicitly documented input normalization). + +Unknown issuers, key IDs, algorithms, claims, and validation failures fail closed. Key retrieval failure also fails closed. A verifier must bound key-cache lifetime and refresh behavior; it must not accept a token merely because parsing succeeded. + +# Nostr-proof validity + +`ValidateProof(P, target, now) = k` only when the applicable Nostr standard verifies the event ID and Schnorr signature, freshness, and target binding: + +- NIP-42: kind, challenge, relay URL, and timestamp are valid; or +- NIP-98: kind, absolute request URL, HTTP method, timestamp, and payload hash when required are valid. + +A service may define another proof profile only if it has equivalent signer-control, freshness, and target/replay binding. The key used for the authorization decision is the key returned by proof validation, never an unsigned request field or assertion display claim. + +# Binding invariant + +For every domain `D`, active bindings are one-to-one: + +```text +∀ i, k1, k2: (i, k1) ∈ B_D ∧ (i, k2) ∈ B_D ⇒ k1 = k2 +∀ i1, i2, k: (i1, k) ∈ B_D ∧ (i2, k) ∈ B_D ⇒ i1 = i2 +``` + +Equivalently, an active identity has at most one key and an active key has at most one identity in a domain. + +# Authorization and enrollment transition + +Given domain `D`, assertion result `(i, k_a?, exp)`, and proof result `k`, evaluate one atomic transaction: + +```text +Authorize(D, i, k_a?, k): + if k_a exists and k_a != k: + DENY(key_mismatch) + + b_i := active binding in B_D for i, if any + b_k := active binding in B_D for k, if any + + if b_i = (i, k) and b_k = (i, k): + ALLOW(existing) + + if b_i exists or b_k exists: + DENY(binding_conflict) + + switch mode(D): + attested-key: + if k_a is absent: DENY(key_attestation_required) + atomically insert (i, k, attested-key) into B_D + ALLOW(created) + provisioned: + DENY(binding_required) + tofu: + atomically insert (i, k, source = k_a exists ? attested-key : tofu) into B_D + ALLOW(created) +``` + +If an insertion loses a race or storage is unavailable, deny; never fall back to an unchecked allow. The check and possible insertion must be linearizable for `(D, i, k)`. + +The resulting authorization lease is: + +```text +L = (D, i, k, binding_version, expires_at) +expires_at <= assertion.exp +``` + +An implementation may impose a shorter maximum lease. A lease authorizes only policy-selected operations in `D`; it does not authorize signing and does not imply that event authors may differ from `k`. + +# Session behavior + +For a single HTTP request, the assertion, Nostr proof, and authorization decision apply only to that request. + +For a NIP-42 WebSocket connection, a relay may cache `L`, but it must not use the lease after `expires_at`. It must require a fresh assertion and proof, reject protected operations, or terminate the connection. A relay that learns that the binding or federated session was revoked must invalidate matching leases. Implementations must document their maximum revocation-detection latency; they cannot claim immediate revocation if they only poll. + +If multiple keys authenticate on one NIP-42 connection, authorization is tracked independently per key. A lease for one `(i, k)` must not authorize another authenticated key. + +# Revocation and rotation + +Revocation is an explicit administrative transition: + +```text +Revoke(D, i, k): + require (i, k) ∈ B_D + atomically remove (i, k) from B_D + append immutable revocation record to R_D + invalidate cached leases for the binding as soon as observed +``` + +An assertion, including one with `k_a = k`, must not silently reactivate the same revoked binding unless the domain's explicit recovery policy authorizes that transition. This prevents replay of a still-valid assertion from undoing revocation. + +Key rotation is not an authorization side effect: + +```text +Rotate(D, i, k_old, k_new): + require explicit recovery/admin authorization + require (i, k_old) ∈ B_D + require no active binding for k_new + if issuer-attested rotation is required, require fresh k_a = k_new + atomically revoke (i, k_old) and create (i, k_new) + invalidate leases for k_old +``` + +A normal request that presents `i` with `k_new` while `k_old` is active is a conflict and must not rotate automatically. + +# Delegation + +Delegation is outside the base identity-binding primitive. A separate delegation standard may allow a bound owner key to authorize a delegate key. If supported, the verifier must first validate the delegation proof and derive the owner key, then require an active, unexpired authorization lease or binding for that owner. It must not create a federated identity binding for the delegate unless explicitly specified. Delegation expiry/revocation and allowed operations remain bounded by both the owner identity authorization and the delegation. + +# Safety properties + +Under the trust assumptions, for direct (non-delegated) authorization: + +1. **Proof possession:** every allowed protected operation is associated with a valid proof of control of its Nostr key. +2. **Federated authenticity:** every allowed protected operation is associated with a currently valid assertion for its issuer-qualified identity. +3. **Agreement:** if the issuer supplies a key claim, the asserted key, proven key, and bound key are equal. +4. **Binding consistency:** no two active identities share a key and no identity has two active keys in one domain. +5. **No implicit rotation:** conflicting assertions or proofs cannot replace an active binding. +6. **Domain separation:** authorization in one domain does not imply authorization in another. +7. **Lease boundedness:** no cached authorization survives assertion expiry; after revocation is observed, no matching cached authorization remains valid. +8. **Fail-closed storage and verification:** validation, key retrieval, or binding-state failures never produce allow. +9. **Privacy:** conforming protocol behavior need not publish `iss`, `sub`, JWTs, email, or display names in Nostr events or relay-visible event history. + +# Liveness properties + +Assuming the issuer, key source, binding store, and network are available: + +1. a valid assertion and matching proof for an existing active binding are eventually authorized; +2. an unbound pair is eventually authorized exactly once when the configured enrollment mode permits it; +3. after an authorized revocation/rotation and bounded cache invalidation, the old key is denied and the new valid binding can be authorized. + +Liveness is intentionally not guaranteed during issuer/JWKS/storage outage; availability must not override identity safety. + +# Representative attack traces + +| Trace | Required result | +|---|---| +| Valid assertion for `i`, attacker proves unbound `k_x`, `i` already bound to `k_v` | Deny `binding_conflict` | +| Valid assertion with key claim `k_v`, attacker proves `k_x` | Deny `key_mismatch` before mutation | +| Stolen assertion for never-enrolled `i`, attacker proves `k_x` | Deny in `attested-key`/`provisioned`; TOFU can bind and explicitly accepts this risk | +| Client injects trusted-proxy header while bypassing proxy | Deployment is non-conforming; verifier must reject direct/untrusted ingress | +| Assertion for issuer `A`, same `sub` as issuer `B` | Distinct identities; never collide or inherit binding | +| Assertion has wrong audience, expired `exp`, unknown algorithm/key, malformed subject/key | Deny without binding mutation | +| Concurrent first use of `(i,k1)` and `(i,k2)` | At most one commits; the other denies conflict | +| Reuse of valid WebSocket authorization after assertion expiry | Deny protected operation or reauthenticate/close | +| Fresh assertion for a revoked pair | Deny unless explicit recovery transition authorizes reactivation | +| New key presented for bound identity | Deny; require explicit rotation | +| Display name/email changes while `(iss,sub)` is stable | May update metadata; binding identity is unchanged | +| One NIP-42 connection authenticates `k1` and `k2`, only `k1` is bound | Only operations attributed to `k1` receive its lease | +| JWT or corporate identifier is accidentally published as event/tag | Non-conforming privacy failure; assertion transport must not enter relay event history | + +# Conformance hooks for the NIP + +The normative NIP should expose enough information for clients and operators to determine: + +- accepted assertion transport profile(s); +- issuer discovery or configured issuer and accepted audience rules without leaking private tenant data; +- whether a key claim is required; +- enrollment mode (`attested-key`, `provisioned`, or explicitly risk-labeled `tofu`); +- authorization lease/re-authentication behavior; +- machine-readable rejection classes using existing NIP-42 `auth-required:` and `restricted:` prefixes where applicable; +- privacy requirements and trusted-proxy deployment requirements. + +It should not standardize database schema, lock mechanism, Okta-specific claims, mutable display metadata, or an administration API. Those are implementation choices as long as the invariants and transitions above hold. + +# Sources + +- NIP-42 authentication: https://github.com/nostr-protocol/nips/blob/8f8444d05a8842c40211ded5d10af3521541f865/42.md +- NIP-98 HTTP auth: https://github.com/nostr-protocol/nips/blob/8f8444d05a8842c40211ded5d10af3521541f865/98.md +- NIP-05 issuer-controlled identifier mapping precedent: https://github.com/nostr-protocol/nips/blob/8f8444d05a8842c40211ded5d10af3521541f865/05.md +- NIP-46 external auth challenge precedent: https://github.com/nostr-protocol/nips/blob/8f8444d05a8842c40211ded5d10af3521541f865/46.md +- Companion protocol specification: [`NIP-FI.md`](NIP-FI.md) +- Buzz implementation semantics reviewed at `bd822f3ea8fc04b449501fd4738097c32d3da950` (PR #1476) diff --git a/docs/nips/NIP-FI.md b/docs/nips/NIP-FI.md index 3d5c9f31ca..5b37a2a35c 100644 --- a/docs/nips/NIP-FI.md +++ b/docs/nips/NIP-FI.md @@ -10,7 +10,7 @@ Federated Identity Authorization ## Abstract -This NIP defines how a relay or Nostr-adjacent HTTP service authorizes an already-authenticated Nostr key only when a valid federated identity assertion (an OIDC/JWT credential from an external identity provider) resolves to the same principal. It specifies assertion transport, validation, an identity-to-key binding lifecycle (enroll, conflict, revoke, rotate), session semantics, and failure behavior. +This NIP defines how a relay or Nostr-adjacent HTTP service authorizes an already-authenticated Nostr key only when a valid federated identity assertion resolves to the same principal and key. It specifies assertion transport, validation, an identity-to-key binding lifecycle (enroll, conflict, revoke, rotate), session semantics, and failure behavior. A separately validated delegation MAY derive narrower authority from a bound owner as described below; that exception does not turn the delegate into the federated principal. The identity provider never becomes a Nostr signing authority, and the assertion never substitutes for Nostr proof of key control. This NIP is an authorization layer above NIP-42 and NIP-98, not a replacement for either. @@ -38,10 +38,10 @@ Without a standard, each deployment invents an incompatible binding scheme, and An assertion reaches the verifier in an HTTP header on the request being authorized: the WebSocket upgrade request for relay connections, or each individual request for NIP-98-authenticated HTTP endpoints. Two transport profiles are defined; a service MUST document which it accepts. -1. **Trusted proxy**: an authenticating reverse proxy (for example oauth2-proxy or an SSO-aware ingress) injects the assertion header after authenticating the user. This profile is conforming only if untrusted clients cannot reach the verifier directly and the proxy strips any inbound copy of the header before setting it. This is the recommended profile for browser-based clients, which cannot attach arbitrary WebSocket upgrade headers. -2. **Client-attached**: the client sends the assertion itself, in the `Authorization: Bearer` header or a service-configured header. +1. **Trusted proxy**: an authenticating reverse proxy (for example oauth2-proxy or an SSO-aware ingress) injects the assertion after authenticating the user. The injected header name is deployment configuration. This profile is conforming only if untrusted clients cannot reach the verifier directly and the proxy strips every inbound copy of that header before setting it. This is the recommended profile for browser-based clients, which cannot attach arbitrary WebSocket upgrade headers. +2. **Client-attached**: the client sends the assertion itself in `Nostr-Federated-Identity: Bearer `. A verifier MAY additionally accept another documented header on WebSocket upgrades, including `Authorization: Bearer`; HTTP requests using NIP-98 MUST use `Nostr-Federated-Identity` because their `Authorization` header carries the `Nostr` proof. -The header name is deployment configuration; `Authorization` semantics apply when it is used. A value with a `Bearer ` prefix MUST be accepted with the prefix stripped. +Assertion acquisition and interactive OIDC login are outside this NIP. A client-attached assertion value MUST use the `Bearer` scheme; after removing that scheme, the value MUST contain exactly one JWT and no comma-separated alternatives. On a WebSocket connection, the assertion captured at upgrade is evaluated when a key performs NIP-42 AUTH — each authenticating key is authorized against that assertion independently. On HTTP, the assertion and the NIP-98 proof MUST arrive on the same request they authorize. @@ -86,7 +86,7 @@ Authorize(D, i, k_a?, k): tofu: create (i, k); ALLOW ``` -The check and any insertion MUST be atomic for `(D, i, k)`: under concurrent first use of the same identity or key, at most one binding is created and every other attempt observes it (allow on exact match, deny on conflict). Storage failure or a lost race MUST deny — never fall back to an unchecked allow. +The check and any insertion MUST be atomic for `(D, i, k)`: under concurrent first use of the same identity or key, at most one binding is created and every other attempt observes it (allow on exact match, deny on conflict). Storage failure or a race whose committed result cannot be read MUST deny — never fall back to an unchecked allow. ### Enrollment modes @@ -127,7 +127,19 @@ HTTP endpoints respond `401` where `auth-required` applies and `403` where `rest ## Discovery -A relay SHOULD advertise support in its NIP-11 document under `limitation` as `"federated_identity": true`, and MAY publish a `federated_identity` object naming its accepted transport profile(s), enrollment mode, and whether delegation is honored. It MUST NOT publish issuer-internal detail (tenant URLs, claim names, audiences) that is not already public. +A relay SHOULD advertise support in its NIP-11 document under `limitation` as `"federated_identity": true`. It MAY additionally include this top-level object: + +```json +{ + "federated_identity": { + "transports": ["trusted-proxy", "client-attached"], + "enrollment": "attested-key", + "delegation": false + } +} +``` + +`transports` contains the supported profile names from this NIP, `enrollment` is exactly one enrollment mode, and `delegation` states whether separately validated delegation may be honored. Unknown fields MUST be ignored. A relay MUST NOT publish issuer-internal detail (tenant URLs, claim names, audiences) that is not already public. ## Privacy From 5f09008091dfe7adb85fe8c6b6b65e1fa43946e4 Mon Sep 17 00:00:00 2001 From: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Date: Thu, 2 Jul 2026 20:37:11 -0400 Subject: [PATCH 3/4] docs(nips): align NIP-FI race and session wording across spec and model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two cold-read consistency fixes: the model's race clause still said a lost race must deny, contradicting the spec's exact-match-allows rule; and the spec's session text offered 'require a fresh assertion' at lease expiry, which has no defined mid-connection transport — assertions only arrive on an upgrade request. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- docs/nips/NIP-FI-MODEL.md | 2 +- docs/nips/NIP-FI.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/nips/NIP-FI-MODEL.md b/docs/nips/NIP-FI-MODEL.md index 831e75d8a3..1db0afa1d4 100644 --- a/docs/nips/NIP-FI-MODEL.md +++ b/docs/nips/NIP-FI-MODEL.md @@ -103,7 +103,7 @@ Authorize(D, i, k_a?, k): ALLOW(created) ``` -If an insertion loses a race or storage is unavailable, deny; never fall back to an unchecked allow. The check and possible insertion must be linearizable for `(D, i, k)`. +If a concurrent attempt finds the identical committed binding, it allows as `existing`; if the committed outcome cannot be read or storage is unavailable, deny — never fall back to an unchecked allow. The check and possible insertion must be linearizable for `(D, i, k)`. The resulting authorization lease is: diff --git a/docs/nips/NIP-FI.md b/docs/nips/NIP-FI.md index 5b37a2a35c..948386831c 100644 --- a/docs/nips/NIP-FI.md +++ b/docs/nips/NIP-FI.md @@ -102,7 +102,7 @@ Within a domain, active bindings form a partial bijection: an identity has at mo For HTTP requests, the decision applies to that request only. -For a NIP-42 WebSocket connection, the relay MAY cache the decision as a lease. A lease MUST NOT be honored past the assertion's `exp` (implementations MAY enforce a shorter maximum). At expiry the relay MUST require a fresh assertion, reject protected operations, or close the connection. When a relay learns a binding was revoked, it MUST invalidate matching leases; a relay that detects revocation by polling MUST NOT claim immediate revocation and SHOULD document its detection latency. +For a NIP-42 WebSocket connection, the relay MAY cache the decision as a lease. A lease MUST NOT be honored past the assertion's `exp` (implementations MAY enforce a shorter maximum). At expiry the relay MUST reject protected operations or close the connection; a fresh assertion arrives only on a new connection's upgrade request. When a relay learns a binding was revoked, it MUST invalidate matching leases; a relay that detects revocation by polling MUST NOT claim immediate revocation and SHOULD document its detection latency. When multiple keys authenticate on one connection (NIP-42 permits this), authorization is tracked per key. A lease for one key MUST NOT authorize operations attributed to another. From 805f1131b8c946cfdcff4508d20f1758515a5990 Mon Sep 17 00:00:00 2001 From: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Date: Thu, 2 Jul 2026 20:38:44 -0400 Subject: [PATCH 4/4] docs(nips): align model lease expiry transition Remove the model's remaining impossible mid-connection assertion refresh option so its WebSocket transition exactly matches the normative transport. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- docs/nips/NIP-FI-MODEL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/nips/NIP-FI-MODEL.md b/docs/nips/NIP-FI-MODEL.md index 1db0afa1d4..c47d2dad29 100644 --- a/docs/nips/NIP-FI-MODEL.md +++ b/docs/nips/NIP-FI-MODEL.md @@ -118,7 +118,7 @@ An implementation may impose a shorter maximum lease. A lease authorizes only po For a single HTTP request, the assertion, Nostr proof, and authorization decision apply only to that request. -For a NIP-42 WebSocket connection, a relay may cache `L`, but it must not use the lease after `expires_at`. It must require a fresh assertion and proof, reject protected operations, or terminate the connection. A relay that learns that the binding or federated session was revoked must invalidate matching leases. Implementations must document their maximum revocation-detection latency; they cannot claim immediate revocation if they only poll. +For a NIP-42 WebSocket connection, a relay may cache `L`, but it must not use the lease after `expires_at`. It must reject protected operations or terminate the connection; obtaining a fresh assertion and proof requires a new connection under this transport profile. A relay that learns that the binding or federated session was revoked must invalidate matching leases. Implementations must document their maximum revocation-detection latency; they cannot claim immediate revocation if they only poll. If multiple keys authenticate on one NIP-42 connection, authorization is tracked independently per key. A lease for one `(i, k)` must not authorize another authenticated key.