Skip to content

[15/15] spec(core): define the protected resource metadata document - #314

Merged
tnunamak merged 3 commits into
spec/integration-0902from
spec/int0902-15v3-protected-resource-metadata
Sep 3, 2026
Merged

[15/15] spec(core): define the protected resource metadata document#314
tnunamak merged 3 commits into
spec/integration-0902from
spec/int0902-15v3-protected-resource-metadata

Conversation

@tnunamak

@tnunamak tnunamak commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

PDPP (the Personal Data Portability Protocol) lets people authorize apps to read selected personal data. It splits that job across two servers: an authorization server, which asks a person what they want to share and issues a grant — the durable record of exactly which streams, fields, and time range that person approved for that app — and a resource server, which holds the data and hands over only what a presented grant covers.

An app that finds a PDPP resource server has no specified way to learn where to send its queries, which kinds of token the server accepts, or which authorization server issues grants for it. The document that already carries those answers exists and is served in production, but it is not defined in any document that binds an implementer. PDPP publishes several specification documents, of which exactly one is normative — binding on anyone claiming conformance — and the rest are Informative, meaning implementer guidance that binds no one. The normative one is spec-core.md, called Core throughout this description, and Core never mentions GET /.well-known/oauth-protected-resource. Every implementer today reads that surface off running code, so two independent implementations can disagree about it without either being non-conformant, and an app written against one will silently fail against the other.

This pull request is child 15 of 15 in a stacked review series: fifteen separate PRs, each branched on the one before it, each proposing one decision for the owner to accept or reject, indexed as [N/15] and tracked from umbrella PR #285. "Child" and "row" both mean one of those fifteen proposals — a child is the PR, and a row is that same proposal's line in the umbrella's decision table.

The surface is live, not hypothetical, and four separate artifacts already depend on it. The reference implementation — the runnable PDPP server, which since August 2026 lives in the PDP-Connect/data-connect repository rather than in PDP-Connect/pdpp, the specification repository this PR is against — serves the document at that URL. @pdpp/reference-contract is a different artifact despite the shared word: it is the npm package publishing PDPP's wire-shape schemas for anyone to validate against, and it schemas this document. Both OpenAPI documents — the machine-readable HTTP interface descriptions, reference-public.openapi.json and reference-full.openapi.json, a narrower public surface and the complete one — publish the endpoint. And both search extension profiles, the optional add-on specifications for lexical and semantic retrieval, advertise their capabilities into it. The only prose describing it sits in Source Declaration Discovery and Trust, a companion document that child [10/15] reclassifies from normative to Informative. That child has not merged — nothing in this series has — so the sequence to picture is: when child 10 lands, the last written description of a live wire surface stops binding anyone, and this child is what replaces it.

Proposed rule: a resource server MUST publish protected resource metadata as defined in RFC 9728, and Core defines the four PDPP-specific members that document already carries. RFC 9728 (OAuth 2.0 Protected Resource Metadata) is the IETF standard for exactly this document, so Core states the obligation and cites the RFC rather than restating its mechanics: Section 3 of that RFC already fixes the URL by inserting /.well-known/oauth-protected-resource into the resource identifier, and Section 5.1 already defines the resource_metadata parameter that a WWW-Authenticate challenge on a 401 uses to point a token-less client at the document. Section 2 of the RFC permits additional members and Section 3.2 requires a reader to ignore any member it does not understand, which is what makes four PDPP-specific members safe to add: a generic OAuth client that knows nothing about PDPP is unaffected by their presence.

The four members are pdpp_core_query_base (the base path the Core query endpoints extend); pdpp_token_kinds_supported (PDPP defines two kinds of access token — an owner token, which a person uses to reach their own data, and a client token, which an app uses under a grant that person issued — and this member says which of the two this server accepts); pdpp_self_export_supported (whether an owner token may read the owner's own data through the app-facing query endpoints without any app grant); and pdpp_provider_connect_version (which PDPP version the interface implements). Each gets one sentence in the specification text.

resource is named as the resource server's own identifier. A source declaration is the JSON document a data source publishes describing what streams and fields it offers, and its source.id field is that source's identifier. For a provider_native source — one whose own operator publishes that declaration, as opposed to a source reached through a connector, the adapter software that collects data from a platform that publishes no declaration of its own — resource and source.id are the same identifier. That is not new here: Core's Section 5, which defines the source declaration and the conditions under which an authorization server accepts one, already requires a server to verify that binding before it asks a person for consent. This subsection states the same identity from the resource server's side.

Two things are deliberately left out. capabilities stays with the extension profiles that advertise into it, so Core neither requires it nor constrains its contents. The reason is a layering rule the specification states about itself: Core must not depend on the optional extension profiles, because an implementer may conform to Core while implementing none of them — so a Core definition of capabilities would have to describe contents Core is not allowed to know about. pdpp_source_declaration_uri stays in the Informative companion, because it is an onboarding pointer an authorization server follows once, not something a client reads to query records.

Decision Choice
Proposed text Core's Section 8, which defines the resource server's query interface — the HTTP endpoints an app calls to read records, and how a grant is enforced on each call — gains a Protected resource metadata subsection: publish RFC 9728 metadata (MUST), resource equals source.id for a provider-native source, four pdpp_-prefixed members defined one sentence each, capabilities left to the extension profiles, WWW-Authenticate behavior cited to RFC 9728 Section 5.1. Core also carries a numbered checklist per role of what a conformant implementation does; the resource server's list gains item 16, requiring publication of the document.
Strongest alternative Leave the surface undefined. Core's Section 12, which lists what v0.1 puts out of scope, already defers a full grammar for advertising capabilities, and an undefined document constrains nobody — but it also lets two conformant servers publish incompatible documents, which is the interoperability failure Core exists to prevent.
Second alternative Define it in Source Declaration Discovery and Trust instead. Rejected because child [10/15] makes that document Informative, so a requirement placed there binds no one.
Rationale The document is already deployed and already schema'd; the gap is that nothing says what it means. Citing RFC 9728 rather than restating it keeps Core small and keeps a generic OAuth client working.
Confidence Medium-high on the four members and the RFC citations, both verified against source (below). Medium on Section 8 — the query-interface section — being the right home, rather than a small separate discovery profile: this document is something a client reads before it holds a token, whereas everything else in Section 8 assumes one.
Status Proposed — new row, not previously reviewed.

What was verified, and how. The member list was read from the reference implementation on origin/main of PDP-Connect/data-connect, in reference-implementation/server/metadata.ts — the ProtectedResourceMetadata interface and the buildProtectedResourceMetadata function that populates it. That file is in the other repository, so this PR's own diff does not contain the evidence: a reviewer who wants to check the member list has to read it there. Every RFC 9728 section number in the new text was checked against the RFC's own text fetched from rfc-editor.org rather than recalled, because a wrong section number in a specification is a defect a reader hits immediately.

What is not verified, specifically. The reference implementation publishes more members than this child defines. Beyond the four it defines, the running server emits pdpp_discovery_hints, pdpp_agent_discovery, and pdpp_owner_agent_onboarding — three members that stay undefined here, so a reader of Core still cannot account for everything a real PDPP server publishes. Whether those three belong in Core, in a future agent-facing profile, or nowhere is a separate decision this child does not make. (A fourth extra member, pdpp_source_declaration_uri, is not in that undefined set: it is deliberately left with the Informative companion, as described above.) Separately, RFC 9728 contains no recommendation to use vendor prefixes for extension members — the pdpp_ convention is PDPP's own choice, and the new text says so rather than attributing it to the RFC.

Evidence. pnpm spec:check — one of the repository's own check scripts, run through the pnpm package manager — regenerates the site's specification pages from the root spec-*.md files and fails on any difference between the two copies; its pass means the published site and the source of truth agree, so a reader of the website sees the same requirement an implementer reads in the repository. pnpm spec:owner-filter-check guards a specific past mistake: PDPP's owner tokens carry no grant, so prose about filtering an owner's own reads must not borrow the vocabulary used for narrowing an app's grant, or an implementer could apply grant-narrowing logic to a token that has no grant to narrow. Its pass means this change introduced no such crossover. pnpm spec:dates checks that each specification file's declared Date: header is not older than the last commit that changed that file's prose (as opposed to a commit that only moved the stamp itself); it reports one failure, on spec-reference-implementation-examples.md. That file documents worked examples drawn from the reference implementation, and it fails the same way on origin/main — the shared mainline branch, the same main named above — with none of this series applied, so this change neither caused nor fixed it. (origin/main fails on a second file, spec-collection-profile.md, which describes the optional connector-collection profile; an earlier child in this series moved that file's Date: stamp forward, which is why only one failure remains here.) The site build prerenders every specification route; the built HTML for Core contains the new #protected-resource-metadata anchor, all four member names, and the new resource-server conformance item requiring the document be published — so the requirement reaches the rendered page, not just the source file. No client, authorization server, or resource server was executed against this text: nothing here is covered by a runtime test.


Amended 2026-09-03. Two additions, one from the independent decision review (item J4) and one from the standards review (item I4).

authorization_servers was left to RFC 9728's OPTIONAL. That RFC makes the member optional because some deployments cannot enumerate their issuers, but PDPP silence means a client that fetches the document may still not learn which server issues grants for the resource — which defeats part of the point of publishing it. A PDPP resource server now MUST publish authorization_servers when that set is enumerable, and MUST omit it rather than publish a partial list when it is not: a truncated list is worse than none, because a client cannot tell it is incomplete. The alternative path for the non-enumerable case is now stated rather than implied — the client uses the issuer from the grant it already holds, or the deployment's own onboarding — and Core defines no third route.

Core never required the RFC 6750 challenge. WWW-Authenticate appeared nowhere in Core before this child introduced it for RFC 9728, yet Core defines authentication_error mapped to 401 and claims RFC 6750 conformance for both token kinds. RFC 6750 Section 3 makes that header mandatory on exactly this response. A client holding no token therefore had no specified way to discover where to authenticate. The resource server now MUST send WWW-Authenticate: Bearer on a 401, MUST set error="invalid_token" when a token was presented and rejected, and MUST carry the RFC 9728 resource_metadata parameter pointing at this document. All three are stated together on purpose: RFC 9728 Section 5.1 extends the very challenge RFC 6750 Section 3 requires, and splitting them would make a reader assemble the bootstrap path from two sections.

Resource Server conformance item 16 is extended to cover both.

Not addressed here. The standards review also notes that Core reuses insufficient_scope with a meaning narrower than RFC 6750 Section 3.1's ("Expansion requests a stream not in the grant"). Renaming or realigning a defined error code is wire-visible and belongs to the error-vocabulary row, not to this one.

Assisted-by: AI

@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
pdpp Ready Ready Preview Sep 3, 2026 11:08am UTC

Request Review

@tnunamak
tnunamak force-pushed the spec/int0902-14v3-declaration-operational-rules branch from 17b7359 to 611de31 Compare September 3, 2026 09:32
@tnunamak
tnunamak force-pushed the spec/int0902-15v3-protected-resource-metadata branch from 88c79b4 to f09572a Compare September 3, 2026 09:32
@tnunamak
tnunamak force-pushed the spec/int0902-14v3-declaration-operational-rules branch from 611de31 to 168f3b0 Compare September 3, 2026 10:11
@tnunamak
tnunamak force-pushed the spec/int0902-15v3-protected-resource-metadata branch from f09572a to eb2f908 Compare September 3, 2026 10:11
@tnunamak
tnunamak force-pushed the spec/int0902-14v3-declaration-operational-rules branch from 168f3b0 to 92494ef Compare September 3, 2026 10:53
@tnunamak
tnunamak force-pushed the spec/int0902-15v3-protected-resource-metadata branch from eb2f908 to b267978 Compare September 3, 2026 10:53
Base automatically changed from spec/int0902-14v3-declaration-operational-rules to spec/integration-0902 September 3, 2026 10:59
@tnunamak
tnunamak changed the base branch from spec/int0902-14v3-declaration-operational-rules to spec/integration-0902 September 3, 2026 10:59
@tnunamak
tnunamak force-pushed the spec/int0902-15v3-protected-resource-metadata branch from b267978 to 8c59bb1 Compare September 3, 2026 11:00
Core never said what `GET /.well-known/oauth-protected-resource` is,
although it is already a live wire surface. The reference implementation
serves it, the public contract package schemas it, both OpenAPI documents
publish it, and both search extension profiles advertise into it. A
reader working only from Core could not learn that the document exists,
let alone what a resource server must put in it.

This adds the smallest subsection that closes the gap without inventing a
capability grammar. A resource server MUST publish RFC 9728 protected
resource metadata; RFC 9728 Section 3 already fixes where, and Section
5.1 already defines the `resource_metadata` challenge parameter that
points a token-less client at it, so Core states the obligation and cites
rather than restating the mechanics.

`resource` is the resource server's own identifier, and for a
`provider_native` source it is the identifier the declaration carries as
`source.id` — the binding Section 5 already requires an authorization
server to check. Four `pdpp_`-prefixed members become Core concepts with
one sentence each: `pdpp_core_query_base`, `pdpp_token_kinds_supported`,
`pdpp_self_export_supported`, and `pdpp_provider_connect_version`. RFC
9728 Section 2 permits additional parameters and Section 3.2 requires a
reader to ignore what it does not understand, so a generic OAuth client
is unaffected.

`capabilities` stays with the extension profiles that advertise into it.
Core neither requires it nor constrains its contents. `resource_name` is
named as RFC 9728's own member rather than a PDPP extension, because a
consent surface has no other name to show for the resource.

The Informative discovery-and-trust companion described this document
first and keeps `pdpp_source_declaration_uri`, which is a provider-native
onboarding concern rather than a Core one. It now points at Core Section 8
for the rest, and its RFC 9728 citation is corrected from Section 3.1 to
Section 3: 3.1 gives worked request examples, while Section 3 states the
well-known-URI derivation rule itself.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
…on_servers

Two gaps in the metadata surface this row defines, both of which leave a
client unable to bootstrap.

The 401 challenge was missing entirely. Core defines `authentication_error`
mapped to 401 and claims RFC 6750 conformance for both token kinds, but
`WWW-Authenticate` appeared nowhere in the document before this row
introduced it for RFC 9728. RFC 6750 Section 3 makes that header mandatory
on exactly this response: a resource server that receives no credentials,
or a token that does not grant access, MUST include it. A client holding no
token therefore had no specified way to discover where to authenticate.
The resource server now MUST send `WWW-Authenticate: Bearer` on a 401, MUST
set `error="invalid_token"` when a token was presented and rejected, and
MUST carry the RFC 9728 `resource_metadata` parameter pointing at this
document. Putting all three in one place is deliberate: RFC 9728 Section
5.1 extends the very challenge RFC 6750 Section 3 requires, so splitting
them would make a reader assemble the bootstrap path from two sections.

`authorization_servers` was left to RFC 9728's OPTIONAL. That RFC makes the
member optional because some deployments cannot enumerate their issuers,
but leaving PDPP silent means a client that fetches the document may still
not learn which server issues grants for the resource. A PDPP resource
server now MUST publish it when the set is enumerable, and MUST omit it
rather than publish a partial list when it is not — a truncated list is
worse than none, because a client cannot tell it is incomplete. The
alternative path for the non-enumerable case is stated rather than implied:
the client uses the issuer from the grant it holds, or the deployment's own
onboarding, and Core defines no third route.

Resource Server conformance item 16 is extended to cover both.

Not addressed here: Core reuses `insufficient_scope` with a meaning
narrower than RFC 6750 Section 3.1's. Renaming a defined error code is
wire-visible and belongs to the error-vocabulary row, not to this one.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
@tnunamak
tnunamak force-pushed the spec/int0902-15v3-protected-resource-metadata branch from 8c59bb1 to 1566140 Compare September 3, 2026 11:06
…unwrap two companion paragraphs

The prose gate caps sentences at 40 words and flags prose hard-wrapped
mid-sentence. Four in-scope findings, all in text this child added or
edited.

Two long sentences in the Section 8 subsection. The 401 challenge rule was
44 words carrying three separate MUSTs; each is now its own sentence, which
matters here because a reader implementing the bootstrap path needs all
three and a 44-word chain invites missing the third. The
`authorization_servers` rule was 46 words joining the omit-rather-than-
truncate requirement to the alternative discovery path with a semicolon;
split at that seam.

Two hard-wrapped paragraphs in the Informative companion. The pointer
paragraph this child adds was wrapped across four lines, and the
metadata-URL paragraph it edits (correcting the RFC 9728 citation from
Section 3.1 to Section 3) was wrapped across five. Both are now one
paragraph per line, which is the convention the root specs follow and what
the gate expects. No words change in either.

No requirement changes. Verified after the split: the Bearer challenge, the
`error="invalid_token"` parameter, the `resource_metadata` pointer, and the
omit-rather-than-truncate rule are all still present and still MUST, and
the corrected Section 3 citation survives.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
@tnunamak
tnunamak marked this pull request as ready for review September 3, 2026 11:13
@tnunamak
tnunamak merged commit dfccf8b into spec/integration-0902 Sep 3, 2026
14 checks passed
@tnunamak
tnunamak deleted the spec/int0902-15v3-protected-resource-metadata branch September 3, 2026 11:13
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.

1 participant