Skip to content

spec!: OAuth 2.0 authorization, two matcher types, IETF-style pass#1262

Open
dunglas wants to merge 53 commits into
mainfrom
spec/oauth-authz
Open

spec!: OAuth 2.0 authorization, two matcher types, IETF-style pass#1262
dunglas wants to merge 53 commits into
mainfrom
spec/oauth-authz

Conversation

@dunglas

@dunglas dunglas commented May 29, 2026

Copy link
Copy Markdown
Owner

Summary

This supersedes #959. It carries the entire spec rework: an IETF-style editorial
and security pass, a reduction of the matcher surface, an authorization-model
hardening pass, and a move to a standards-based OAuth 2.0 authorization model.
The through-line is the same: when Mercure was first drafted in 2018, the web
standards that now cover its needs did not exist or were immature, so the
protocol grew its own machinery. Those standards exist now, so this removes the
bespoke parts and reuses them — leaving a smaller spec a generic OAuth
resource-server library can enforce.

Relevance

SSE has become the de-facto transport for streaming output from AI systems: the
OpenAI and Anthropic APIs and the Model Context Protocol all stream tokens over
SSE. Mercure is SSE-native and adds what those ad-hoc uses lack — authorization,
multiplexing, discovery, and reconnection with state reconciliation — over plain
HTTP. RFC 9728, which this work relies on, was itself driven to publication by
AI-agent demand. A standardized, authenticated way to stream incremental results
is more useful now than when this work started.

Authorization: OAuth 2.0 instead of a bespoke claim

The old mercure JWT claim conflated routing ("which update is this") with
access control ("who may read it"): per-resource access was encoded in hand-built
"capability" topics, and a private update could leak its content to a broader
audience than intended.

  • The hub is an OAuth 2.0 protected resource ([RFC 6749]). The credential is a
    JWT access token ([RFC 9068], typ: at+jwt, aud = hub resource identifier).
  • Authorization uses OAuth 2.0 Rich Authorization Requests (authorization_details,
    [RFC 9396]) via a new mercure type carrying actions (publish/subscribe) and
    topics. The bespoke claim is gone; flat scopes are not used (topics are the
    only resource type).
  • Bearer presentation and errors per [RFC 6750]: Authorization: Bearer, an
    access_token query parameter, cookie as a documented extension;
    invalid_token (401) / insufficient_scope (403) / invalid_request (400).
  • Auth requirements are discoverable via OAuth 2.0 Protected Resource Metadata
    ([RFC 9728], 2025). Verification keys are delegated to authorization server
    metadata ([RFC 8414]) or that resource metadata; no Mercure-specific JWKS.
  • Token validation per [RFC 9068] + [RFC 8725]: explicit algorithm allowlist
    (never inferred from the token), kid/role key selection, exp/nbf, required
    aud, checked iss, typ confusion rejected.

Matchers: two types, standards-based

  • URL matching uses the WHATWG URL Pattern standard instead of URI Templates
    ([RFC 6570] defines expansion, not matching, and did not fit). URL Pattern did
    not exist in 2018.
  • The matcher set is reduced to two types, Exact and URL Pattern. The earlier
    exploratory Regexp / URI Template / CEL types and the matcher-type registry are
    dropped as speculative surface without a driving use case.
  • Alternate topics are dropped: an update has exactly one topic. Per-resource read
    authorization becomes a single hub-enforced check against the token, removing the
    content-disclosure footgun of the old "any topic matches any matcher" rule.
  • Subscribe query parameters are match (Exact, the default) and
    match<MatcherType> (e.g. matchURLPattern), mirroring the optional
    matchType member of authorization details; unknown names under the reserved
    match prefix are rejected so typos fail loudly.

Editorial and security pass

  • BCP 14 boilerplate (RFC 8174); refreshed obsolete references (RFC 8288 for Web
    Linking, RFC 9562 for UUID, RFC 7942 for the implementation-status template).
  • Added an Introduction; defined terms; fixed metadata (ipr, area, abbrev).
  • Reworked Security Considerations as analysis plus pointers; covered SSE field
    injection, the reserved hub namespace, event-replay authorization, DoS limits,
    topic normalization/homographs, bearer-token theft (optional DPoP/mTLS, RFC
    9449), publish-request replay, and protected-resource-metadata / authorization-
    server-selection risks (SSRF, AitM).
  • The rarely-used JWE key-set Link attribute is dropped (keys shared out of band).

A post-review hardening pass on the same branch: unauthenticated subscribers are
allowed again for non-private updates (publication always requires a token), the
subscription API authorizes against the same relative URL form as subscription
event topics, malformed authorization details map to 401 invalid_token per
RFC 6750, CORS rules forbid wildcard/reflected origins on credentialed
connections, the reserved-namespace check normalizes percent-encoding, the
reserved earliest value cannot be supplied as an update ID, the IANA section
registers the mercure_cookie RFC 9728 metadata member (and no longer targets a
registry RFC 9396 never created), and superseded W3C references point to the
WHATWG living standards. Follow-ups: the Bearer scheme name is matched
case-insensitively per RFC 9110, and {subscriber} identifiers may be
hub-generated (e.g., random UUIDs) — what matters is that clients cannot choose
them, and hub-generated values avoid disclosing sub to other subscribers.

Notes

dunglas and others added 30 commits April 29, 2026 10:54
Co-authored-by: Antoine Bluchet <soyuka@users.noreply.github.com>
Co-authored-by: Antoine Bluchet <soyuka@users.noreply.github.com>
Co-authored-by: Antoine Bluchet <soyuka@users.noreply.github.com>
Co-authored-by: Antoine Bluchet <soyuka@users.noreply.github.com>
Co-authored-by: Antoine Bluchet <soyuka@users.noreply.github.com>
Co-authored-by: Antoine Bluchet <soyuka@users.noreply.github.com>
…dling

Three clarifications surfaced while implementing this spec revision:

- Payload assignment: define what it means for a JWT claim matcher to
  "match" a subscription matcher. A claim matches when it is the
  wildcard `*`, when its (type, pattern) pair equals the subscription's,
  or when its matcher accepts the subscription's `match` string as a
  topic. Payload fallback to top-level `mercure.payload` is stated
  explicitly.

- URL Pattern base URL: recommend absolute patterns and describe the
  hub's freedom in resolving relative ones. Subscribers and publishers
  should not rely on the base URL being anything specific because
  resolution is implementation-defined.

- CEL evaluation cost limit: hubs SHOULD enforce an implementation-
  defined cost limit to mitigate DoS from pathological expressions in
  hostile JWTs; exceeding the limit yields `false`.

Also notes that hubs MAY reject oversized patterns with a 400 response.
Silently treating v8 bare-string claims as `Exact` is an interoperability
trap: the v8 rule was "exact OR URI Template", so a token signed for a
v8 hub would match fewer topics on a v9 hub without any diagnostic.

Bare-string claims are now only accepted when the hub is explicitly
running in protocol-compatibility mode; v9 hubs reject them with 401.
This forces clients to migrate to the unambiguous object form.
Topics are absolute IRIs by design, and the protocol has no notion of a
base URL against which a subscriber could expect a relative pattern to
resolve. Allowing relative patterns forced hubs to invent an implicit
base (typically the hub's own location), which made subscriptions
non-portable across hubs and the resolution implementation-defined.

Hubs MUST now reject relative URL patterns. The Go implementation in
PR #1207 already does this.
The hub publishes subscription events on relative topic IDs
(e.g., /.well-known/mercure/subscriptions/...). Requiring patterns
to be absolute prevents subscribers from matching those topics
with matchURLPattern.
…egistry

- Bump draft to -08
- Drop redundant "exactly one of" wording in Subscription
- Add (#discovery) cross-reference from Subscription
- Switch topic matcher query parameter names to case-sensitive,
  registered in a new IANA "Mercure Topic Matcher Query Parameter Names"
  registry
- URL Pattern: MUST NOT enable ignoreCase; host case-insensitivity
  retained per URL canonicalization
- Exact and Regexp: MUST NOT resolve relative values against a base URL
- Topic Matcher List: reject non-object entries with 400 (was 401 bare-string),
  matchType value is case-sensitive and matches the canonical name from
  the matcher-type table
- Payloads: define top-level mercure.payload, drop the redundant "*"
  wildcard rule (catchall expressible via regex .*)
- Subscription Events: {matchType} URL component uses canonical case;
  JSON-LD matchType field case-sensitive
- Fix dart_mercure underscore typo

Signed-off-by: Kévin Dunglas <kevin@dunglas.fr>
Address security issues identified in a section-by-section audit. Adds
normative requirements to close authentication, authorization, denial-
of-service, injection, and information-disclosure gaps that were
previously implicit or unspecified.

Terminology and Subscription
- Constrain topic strings to UTF-8 and forbid C0 controls and DEL
- Bump per-request matcher count and pattern length limits from MAY
  to SHOULD, with 400 on overflow
- Require UTF-8 and control-char rejection for matcher query values
- Reword the subscription creation rule to match the registered query
  parameter set; allow hubs to deduplicate identical matchers

Matcher Types
- Exact: forbid relative resolution and implicit normalization; add
  guidance on NFC and IDNA canonicalization
- URL Pattern: require an RE2-class engine or evaluation cost/time
  limit; warn about scheme-wildcard hazards (data:, javascript:, etc.)
- Regular Expression: require an RE2-class engine or evaluation
  cost/time limit; I-Regexp is a dialect, not an engine
- CEL: bar custom functions from network, filesystem, process, or
  other side-effectful access; bump evaluation cost limit from SHOULD
  to MUST; cap topics array size
- Other Matcher Types: register names in the new IANA "Mercure
  Matcher Types" registry; vendor-prefix implementation-specific names

Publication
- Reserve /.well-known/mercure/ for hub-generated topics; reject
  publish requests violating the rule with 403
- Make the private field's truthiness rule explicit (presence wins,
  value ignored), preventing accidental opt-out interpretations
- Forbid C0 controls in id, LF/CR in type, and require ASCII digits
  for retry, with 400 on violation
- Require UTF-8 for field names, values, and data
- Cap request body and field length; reject overflow with 413

Authorization
- Add a JWS Validation subsection mandating rejection of alg: none,
  alg/key-type binding, signature verification, exp/nbf enforcement,
  and optional aud validation; require minimum alg support of EdDSA,
  ES256, and RS256
- Distinguish 401 (JWS missing/invalid) from 403 (insufficient scope);
  forbid disclosing which one failed
- Promote different-key-per-role guidance from MAY to SHOULD
- Promote cookie Secure and HttpOnly from SHOULD to MUST; default
  SameSite=Strict, Lax permitted only for cross-site discovery flows
- Topic Matcher List: any per-entry parse failure rejects the whole
  request with 400 (no partial acceptance, which could alter scope);
  cap matcher count and pattern length, with 400 on overflow
- Payloads: warn that first-match-wins ordering can mask specific
  entries; warn that payloads broadcast to other authorized
  subscribers via subscription events and must not carry per-
  subscriber identifiers

Reconnection
- Require authorization re-check on replayed events against the
  current JWS scope (not the scope at publication time)
- Forbid the Last-Event-ID response header from disclosing the
  identifier of an event the subscriber is not authorized to receive

Active Subscriptions
- Subscriber identifier is hub-assigned; clients MUST NOT supply,
  suggest, or override it; hub MUST guarantee uniqueness and MAY
  derive the value from the JWS sub claim
- Recommend correlating subscriptions of the same client across
  requests (e.g., by sub claim)

JSON-LD Context
- Subscribers SHOULD NOT auto-fetch the @context URL; embed or cache
  it locally to prevent MITM-induced semantic drift

Discovery
- Warn that a compromised publisher can redirect subscribers to a
  malicious hub; recommend restricting hub origins
- Tighten key-set hosting language; require publisher-side access
  control on the JWK Set endpoint; warn that misconfiguration defeats
  encryption

Encryption
- Restrict JWE algorithms to currently strong choices; forbid
  algorithms with known compromises (e.g., RSA1_5)
- Note that JWE has no replay protection; recommend in-payload
  freshness indicators
- Note that long-lived JWE keys lack forward secrecy; recommend
  rotation and ephemeral key agreement

IANA
- Add the "Mercure Matcher Types" registry (Specification Required)
  with initial entries Exact, URLPattern, Regexp, CEL, URITemplate
- Update query-parameter registry to reference the matcher-types
  registry

Security Considerations
- Add subsections summarizing the new threats: JWS validation, SSE
  field injection, reserved hub namespace, replay authorization,
  subscriber identifier assignment, regex/URL Pattern DoS, CEL
  sandbox, payload privacy, topic normalization, resource limits,
  hub trust, JWE algorithms and replay

References
- Add UNICODE and re2 bibliography entries

Signed-off-by: Kévin Dunglas <kevin@dunglas.fr>
…ontroller

- Replace ambiguous "No topic value MAY start with the prefix
  /.well-known/mercure/" with "Topic values MUST NOT start with the
  prefix". MAY is a permission keyword; combining it with "No" mixed
  RFC 2119 semantics with English negation.
- Add a normative body rule for per-JWS concurrent subscription
  limits (MAY + 429), aligning the body with the Resource Limits
  entry in Security Considerations.
- State "The change controller for all initial entries is the IETF"
  in both new IANA registries (Topic Matcher Query Parameter Names
  and Matcher Types), matching the convention used by the other
  IANA registrations in this document.

Signed-off-by: Kévin Dunglas <kevin@dunglas.fr>
Collapse the two newly added IANA registries into one. Query parameter
names are mechanically derived from matcher type names (match +
TypeName, plus the historical match alias for Exact), so maintaining
two separate registries adds editorial weight without adding
information. The merged registry binds the type name and its query
parameter at the moment of registration, making the relationship
explicit and reducing the chance of drift.

Signed-off-by: Kévin Dunglas <kevin@dunglas.fr>
Custom matcher types extend the protocol surface and the attack
surface. Without explicit constraints, hubs implementing them risk
authorization bypass through type confusion, denial of service via
unguarded pattern evaluation, and SSRF or sensitive-material
disclosure via expression-based matchers.

- Require non-registered names to use reverse-DNS dotted notation
  (com.example.Foo) or an absolute HTTPS URI rooted in a
  domain controlled by the implementer. The PascalCase production
  is reserved for registered names; collisions are syntactically
  prevented.
- Require parity with built-in DoS and sandbox controls: linear-time
  matching engine or evaluation cost limit, no network/filesystem/
  process/clock/random access from expressions.
- Add an authorization invariant: a custom matcher MUST NOT produce
  an authorization decision unreachable by some registered matcher
  type for the same match value and topic.
- State explicitly that JWSs using non-registered names are not
  portable, and that incompatible hubs reject with 501.
- Add a Security Considerations subsection summarizing the rationale
  and requiring hub operators to review each custom matcher before
  enabling it.

Signed-off-by: Kévin Dunglas <kevin@dunglas.fr>
Refinements following further review.

- Custom matchers
  - Drop the URI-form name option (matchhttps%3A%2F%2F... was
    parser-hostile after query-key percent-encoding).
  - Relax to "vendor-controlled prefix separated by .": DNS-reverse
    domain (com.example.Foo) or trademark (ACME.Foo). The intent is
    explicitly the inverse of RFC 6648's deprecated X- convention —
    require globally-unique prefixes rather than ambiguous markers.
  - Update the Subscription query-parameter rule so hub-supported
    non-registered names are accepted, not only IANA-registered ones.

- Authorization
  - Note that hubs MAY be deployed without JWS-based authorization
    when the network is fully trusted and the hub is unreachable from
    untrusted clients. Avoids relaxing alg=none, which would be
    unsafe even on intranets.
  - Compress JWS Validation by delegating to RFC 8725 (JWT BCP).
    Keep the high-impact items (alg=none, alg/key binding, exp/nbf,
    aud, recommended alg list) inline; defer the rest.

- Reconnection
  - Replace the predecessor-event lookup with a two-state Last-Event-
    ID response: same identifier if the requested event existed and
    was visible to the subscriber, earliest otherwise. Avoids both
    the backward-scan DoS vector and the leak of unauthorized event
    identifiers.

- Subscription Events
  - Subscriber identifier: drop the strict "hub-generated" rule.
    Reality: the JWS is minted by the application server, not the
    hub. Reframe as a forgery-prevention rule: clients MUST NOT
    forge, supply, or override the identifier; the hub MUST derive
    it from cryptographically-validated information (typically the
    sub claim). Updated Security Considerations subsection
    correspondingly.

Signed-off-by: Kévin Dunglas <kevin@dunglas.fr>
Switch Last-Event-ID response semantics from the binary two-state
form back to a precise predecessor identifier, with two guards:

- the predecessor MUST be visible to the requesting subscriber
  (private events the subscriber cannot read are never disclosed
  via this header);
- the lookup is bounded by an implementation-defined backward-scan
  limit, preventing it from becoming a DoS vector through ancient
  Last-Event-ID values.

When the cap is hit or no visible predecessor is found, the hub
returns "earliest". This restores the event-store use case
(precise recovery anchor) while keeping the security and DoS
protections of the previous version.

Signed-off-by: Kévin Dunglas <kevin@dunglas.fr>
The previous wording required hubs to enforce a backward-scan limit
unconditionally. The real cost of the lookup is per-event
authorization filtering, not the predecessor seek itself, which is
O(log N) on backends such as Redis Streams (XREVRANGE) and constant
on b-tree indexes.

- Demote the scan-limit rule from MUST to SHOULD, and explicitly
  permit hubs with cheap predecessor seek and cheap per-event
  authorization filtering to omit it entirely.
- Keep the authorization-filter constraint on the response header
  unchanged (MUST NOT disclose unauthorized event identifiers).
- Add an operator note explaining that the privacy impact of
  predecessor disclosure depends on the hub's identifier format:
  opaque random IDs (UUIDv4) leak only existence, while time-
  ordered IDs (UUIDv7, Redis Stream IDs, snowflake) additionally
  leak timing and ordering of events the subscriber cannot read.
  Operators handling highly sensitive private updates SHOULD prefer
  opaque random identifiers.

Signed-off-by: Kévin Dunglas <kevin@dunglas.fr>
Architectural/technical:
- Reserved-namespace publish check now tests the resolved path component,
  host-agnostic, closing the absolute-topic subscription-event forgery gap.
- 401 only when no credential is presented; 403 for every presented-credential
  failure, so the status code no longer discloses validation vs scope.
- Matcher query-param names: 400 if malformed, 501 if well-formed but
  unimplemented, matching the claim-list handling.
- Define URITemplate matching (reverse-expansion) and its DoS limit.
- Specify full-string anchoring for Regexp matching.
- Subscriptions are created for registered and hub-specific matchers alike.
- State that payload claim-matching governs selection only, never authorization.

References:
- Add RFC 8174 (BCP 14 boilerplate).
- RFC 5988 -> 8288, RFC 4122 -> 9562, RFC 6982 -> 7942.
- Demote EventSource interface and XHR to informative.
- Convert inline DID links to a proper reference.

Editorial:
- Add Introduction; define canonical/alternate topic in Terminology.
- Clarify the JWS payload is a JWT.
- Registrable domain (eTLD+1) instead of "second-level domain".
- Rewrite Security Considerations as analysis plus pointers, no duplicated
  normative keywords; move the custom-matcher operator-review requirement
  into the body.
- Recommend 200 on publish, 201 NOT RECOMMENDED.

Metadata:
- Add ipr=trust200902 and abbrev; area -> Web and Internet Transport.
The pluggable matcher system (Regexp, CEL, URITemplate, the IANA matcher-type
registry, and reverse-DNS custom types) was speculative and unshipped. It
expanded the interoperability surface without a driving use case: a conforming
hub could share almost nothing with another beyond Exact, and the advanced
types had no second implementation.

Keep two mandatory types, Exact and URLPattern. URLPattern supersedes URI
Template, which lacked defined matching semantics (RFC 6570 specifies expansion,
not matching). Restore a reserved `*` wildcard for match-all.

- Subscription: only match/matchExact/matchURLPattern; unknown names -> 400.
- Topic matcher list: matchType MUST be Exact or URLPattern; drop the 501
  unsupported-type paths (both types are now mandatory).
- Remove the CEL sandbox and custom-matcher security subsections; fold the DoS
  note into URL-Pattern only.
- Remove the "Mercure Matcher Types" IANA registry and the cel/RFC 9485
  references.

RFC 6570 is retained for subscription-event URI Template expansion.
- JWS validation: require an explicit algorithm allowlist (never inferred from
  the token); add key-selection guidance (kid / endpoint role, never token-
  steered); strengthen aud from conditional to SHOULD-require-when-configured,
  bounding a token to its intended hub.
- Subscribers: hubs SHOULD impose a maximum connection lifetime independent of
  exp, so a no-exp token cannot hold an unrevocable connection open.
- Document the private-update audience leak: delivery is gated per update, not
  per topic, so a broad alternate topic discloses sensitive content to its
  audience. Add a publisher MUST NOT and a Security Considerations subsection,
  and name the publisher-delegated-authorization model as a known limitation.
- Add Security Considerations for bearer-token theft (optional DPoP/mTLS
  sender constraint, RFC 9449) and publish-request replay.
dunglas added a commit that referenced this pull request Jun 9, 2026
Replace the bespoke mercure JWT claim with RFC 9396 authorization_details as
the modern authorization model. Access tokens are now validated as RFC 9068
JWT access tokens: the typ header must be at+jwt and the aud claim must contain
the hub's resource identifier (NewHub requires WithResourceIdentifier or
WithPublicURL when JWT auth is enabled in modern mode). The publish/subscribe
grant and per-subscription payload resolution run against the validated
authorization details.

The legacy mercure claim (string and object forms), the namespaced fallback,
mercure.payload and the "authorization" query parameter move behind the new
deprecated_claim build tag and are honored only in compatibility mode
(WithProtocolVersionCompatibility). The modern query parameter is access_token.

BREAKING CHANGE: tokens carrying the mercure claim are rejected unless the hub
is built with the deprecated_claim tag and runs in compatibility mode. The Go
API loses canReceive/canDispatch (replaced by the internal authorization-detail
grant logic).

Refs #1262
dunglas added a commit that referenced this pull request Jun 9, 2026
Map authorization failures to RFC 6750: a 401 with a bare
`WWW-Authenticate: Bearer` challenge (carrying the RFC 9728 resource_metadata
parameter) when no token is presented, 401 `invalid_token` when a presented
token fails validation, 403 `insufficient_scope` when a valid token lacks the
required publish/subscribe grant, and 400 `invalid_request` for malformed
authorization requests or invalid authorization details.

Previously every authorization failure returned a bare 401.

Refs #1262
dunglas added a commit that referenced this pull request Jun 9, 2026
…identifier

Update the Caddy test fixtures, the demo UI and the conformance suite to RFC
9068 access tokens (typ at+jwt, audience set) carrying an authorization_details
claim, and configure resource_identifier in the test, dev and production
Caddyfiles so the hub validates the audience. The deprecated Caddy tests now
require both the deprecated_topic and deprecated_claim build tags.

The dev and production Caddyfiles redact the access_token query parameter from
logs alongside the legacy authorization parameter.

BREAKING CHANGE: the official Caddyfile now sets resource_identifier (default
https://localhost/.well-known/mercure); set MERCURE_RESOURCE_IDENTIFIER to the
audience your access tokens carry, or enable protocol_version_compatibility 8
to keep accepting v8 mercure-claim tokens.

Refs #1262
dunglas added a commit that referenced this pull request Jun 9, 2026
Add deprecated_claim to the CI GOFLAGS, the goreleaser builds and the project
build-tag list, and document the OAuth 2.0 authorization model (access tokens,
authorization_details, RFC 6750 errors, resource_identifier, the protected
resource metadata endpoint) in the upgrade guide and the hub configuration
reference.

Refs #1262
dunglas added 9 commits June 10, 2026 14:29
Replace the stale `topic` query parameter in the access_token example
with the encoded `match` form, quote ETag values per RFC 9110, cite
RFC 8615 (which obsoletes RFC 5785), drop the pre-OAuth authentication
sentence from the subscription section, mark `topic` as the required
publish field instead of listing it among optional tuples, make
bearer_methods_supported reflect what the hub actually accepts, and
spell out the reserved `match` prefix rationale and the payload
matcher-against-matcher semantics.
Requiring an access token from every subscriber made the most common
deployment (public feeds and dashboards) impossible without minting
tokens for anonymous visitors. Split the rule: publication always
requires a token, while hubs may accept unauthenticated subscribers
that only receive non-private updates. Adjust the 401 challenge, the
exp-based connection close, and the subscription event subscriber
identifier accordingly.
"The request target resolved against the hub's URL" yields an absolute
URL, while subscription event topics are relative paths. Exact matching
is byte-for-byte with no resolution, so no Exact matcher could cover
both a subscription event and the API URL describing it. Define the
canonical topic for these endpoints as the absolute path, shared with
subscription event topics.
Authorization details live inside the token, so defects there are
token-validation failures: per RFC 6750 they map to 401 invalid_token,
not 400 invalid_request. This also avoids disclosing that the
signature verified but the claims were malformed, consistent with the
single invalid_token rationale of the error responses section.
RFC 9396 establishes no registry of authorization details type
identifiers (it leaves type registration out of scope), so drop the
registration into a nonexistent registry and state that the mercure
type is defined by this document. Register the mercure_cookie and new
mercure_version members in the OAuth Protected Resource Metadata
registry established by RFC 9728. mercure_version gives clients a way
to detect hubs speaking incompatible earlier revisions, which changed
the subscribe query parameters and the token format.
The kid header is attacker-controlled, contradicting the requirement
to select keys independently of attacker input: restate it as a hint
choosing among pre-trusted keys, never introducing new key material
(jwk/jku/x5u). Define which RFC 9068 claims hubs actually require so
strict validators do not reject minimal self-issued tokens, recommend
the hub URL as the default resource identifier, and add a minimal
self-issued token example. Replace the untestable "compromised
algorithm" MUST NOTs with SHOULD NOT, keeping a testable MUST NOT for
RSA1_5.
The cookie mechanism explicitly targets cross-origin EventSource
connections with credentials, but the spec said nothing about CORS.
Require hubs serving cross-origin browsers to send CORS headers, and
forbid wildcard or reflected Access-Control-Allow-Origin on
credentialed connections: reflection would let any website read
updates with the visitor's cookie.
Normalize percent-encoding (unreserved characters) before the
reserved-namespace path test, so /.well-known/%6Dercure/... cannot
bypass it. Forbid publisher-supplied update IDs equal to the reserved
value earliest, which would otherwise make Last-Event-ID handling
ambiguous despite the no-collision claim in the reconciliation
section.
The W3C Server-Sent Events Recommendation and HTML 5.2 are superseded;
SSE is maintained in the WHATWG HTML Living Standard and
application/x-www-form-urlencoded in the URL Living Standard, both
already cited elsewhere in this draft (URL Pattern, XHR).
Copilot AI review requested due to automatic review settings June 10, 2026 12:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.

dunglas added 2 commits June 10, 2026 14:49
A version number in resource metadata has no negotiation semantics and
contradicts IETF practice: the published specification is the version,
and an incompatible revision would be a new spec defining its own
metadata members or well-known location. The stated purpose (detecting
pre-standardization hubs) is already served by the absence of protected
resource metadata, which those hubs never publish; keep that as an
explicit hint instead. The draft-revision number 8 was also meaningless
as a protocol version.
Requiring the identifier to be derived exclusively from validated token
claims contradicted the reference implementation, which assigns a
random UUID per connection, and overlooked a privacy cost: an
identifier derived from sub discloses that claim to every subscriber
authorized for subscription events. Permit hub-generated identifiers
explicitly; the security property that matters is that clients cannot
choose or influence the value.
Copilot AI review requested due to automatic review settings June 10, 2026 13:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.

dunglas added 2 commits June 10, 2026 15:29
HTTP authentication scheme names are case-insensitive per RFC 9110
§11.1; spell it out so implementations do not byte-compare the
prefix.
Copilot AI review requested due to automatic review settings June 10, 2026 13:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.

dunglas added 3 commits June 10, 2026 17:22
RFC 9728 defines this optional member; advertising ["mercure"] lets
discovery-driven clients learn that the hub understands the mercure
authorization detail type.
Copilot AI review requested due to automatic review settings June 10, 2026 15:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants