Add tokenExchange OAuth2 flow (OAuth 2.0 Token Exchange, RFC 8693) - #44
Conversation
…fication#5428 Adds tokenExchange to the pre-defined OAuth2 flow keys: tokenUrl required, scopes as for all flows. Includes the JSON Schema rule, a collection example, and a CHANGELOG entry.
…catalogs Repurposes this proposal after the upstream agreement: the vending step is standardized upstream (OAI/OpenAPI-Specification#5428 mirrored by stac-extensions/authentication#44), so Portolan adopts the Authentication extension wholesale instead of owning an access: namespace. - spec/extensions/authentication.md: profile note — two-step gated read (openIdConnect identity + oauth2/tokenExchange vending), fields used, client algorithm, chaining convention (pending upstream), validation rules, security considerations. - spec/examples/authentication-collection.json: gated collection with two schemes and two gated assets (GeoParquet + Iceberg metadata) on one exchange; validates against the v1.1.0 schema. - access: extension files removed (shape preserved in history as the recorded fallback).
Links the exchange to the scheme that supplies its subject_token, so a generic client (e.g. STAC Browser) can resolve the order of the steps from the document instead of hardcoding it. Optional; RECOMMENDED when more than one scheme is declared.
…authentication#44 The tokenExchange flow now declares which auth:schemes entry supplies its subject_token (added upstream after maintainer review, for generic- client implementability). Profile: publishers SHOULD provide it, clients MUST use it when present; portolan check requires it and enforces the cross-reference resolution the upstream JSON Schema cannot express. Status note updated: maintainer proposes releasing as a new minor version of the Auth extension, decoupled from the OpenAPI timeline.
Shows the full chain end-to-end: asset -> auth:refs -> token_exchange -> subjectTokenScheme -> oauth. Previously the scheme was declared but unreferenced.
Reverts examples/collection.json to its pristine state and adds examples/collection-token-exchange.json: identity scheme (oidc) + exchange scheme linked via subjectTokenScheme + two gated assets sharing one exchange. The identity scheme is intentionally unreferenced by any auth:refs — the case that motivates the field. Follows the multi-example convention of other extensions (e.g. datacube). README points to it.
|
@cayetanobv I'm looking at the proposal right now (sorry for the delay). I'm not sure I get how the two-step approach is solved yet. I only see it exposed through "Step 1/2" in the description, but that's rather for humans than for a machine client. The item (step 1), asset (step 2) might work if it's somewhere exposed, but I don't see it in the example and I guess - if we go this route - we should document it somewhere. I'm thinking though that making an item locked away behind auth just to get this flow going although it could be public is the wrong way in the "open data" sense. My current idea would be to allow "auth:refs" in the individual "auth:schemes" to mark prerequisites. What do you think? We'd need to adopt the authentication extension a bit, but would allow "endless" chains of auth procedures... |
The field is what makes the two-step exchange machine-discoverable: without it a client can find the tokenUrl but cannot tell which of the declared schemes produces the token it must present as the RFC 8693 subject_token. Left optional, every client has to invent its own fallback and the spec defines none, so make it required for the flow and enforce that in the schema. Also document the grant_type and the RFC 8693 response, and note that subject_token_type follows from the referenced scheme's type so it needs no field of its own. Drop the reference to OAI/OpenAPI-Specification#5428, which is parked pending the OpenAPI security spec split; RFC 8693 is the normative anchor.
The descriptions narrated the flow at such length that the fields carrying it — auth:refs on the asset and subjectTokenScheme on the flow — were the least prominent thing in the document, which invited reading the chain as prose-only. Cut the prose to what no field expresses and drop the asset titles; the auth structure itself is unchanged.
|
Thanks, and no worries about the delay. The chain a client actually follows is in the fields (it's machine-readable), and it still resolves with descriptions stripped out. The descriptions were included only to make easier the human-read of the chain. The path a client follows is: The document is walked backwards from the protected asset to find the starting point, then run forwards. auth:refs says what guards the asset, and subjectTokenScheme says whose token the exchange consumes. On auth:refs inside a scheme, I like it, and I think the two can coexist: they answer different questions. It says that a prerequisite exists, but not what the token is used as. RFC 8693 takes two inputs (subject_token and actor_token), so a bare ref gives an ordering, not a parameter binding, and the exchange is m2m, with no human in the loop to infer which is which. So I'd keep both, with distinct jobs: your scheme-level auth:refs as the general chaining mechanism, and subjectTokenScheme as the role binding inside the flow. I've updated the PR accordingly:
What do you think? |
|
Thanks for the update, @cayetanobv! I'd still lean toward scheme-level auth:refs over subjectTokenScheme for the following reasons:
Two additional issues worth improving:
|
Per review: the prerequisite edge moves from the flow-level subjectTokenScheme onto the scheme itself as auth:refs — reusing the reference mechanism clients already implement, and generalizing to other scheme types (a signedUrl scheme can name the scheme that authenticates its authorizationApi). A tokenExchange scheme must reference exactly one scheme, whose token is the RFC 8693 subject_token; chains are acyclic. The subject_token_type is now explicit-over-implicit: an optional subjectTokenType URN on the flow, defaulting from the referenced scheme's type when absent (OIDC yields both an id_token and an access token, so inference alone is order-dependent).
|
Agreed, and done, scheme-level auth:refs pushed, and PR body updated. On your both-tokens point: the flow now has an optional subjectTokenType (RFC 8693 URN), which clients MUST use when present; absent, it defaults from the referenced scheme's type. Happy to hard-require it instead if you prefer. |
|
This sounds pretty good to me. |
Per review: any one referenced scheme supplies the input token; sequences are chains of single references. tokenExchange cardinality relaxes from exactly one to at least one, multi-IdP collapses into one exchange scheme listing identity alternatives, and the subjectTokenType default resolves from the scheme actually used.
537d146 to
ecbc111
Compare
|
Thanks @m-mohr!
Changed, one-of it is, and you're right that it's more consistent: auth:refs now means the same in both positions (alternatives to access / alternatives to obtain the input), multi IdP collapses into one exchange scheme listing identity alternatives, and sequences stay chains. subjectTokenType default resolves from the scheme actually used; alternatives needing different types SHOULD split into separate exchange schemes. maxItems dropped (minItems: 1 stays). |
|
Thank you! |
There was a problem hiding this comment.
🟡 Changes recommended
The updated JSON Schema patternProperties regex can match unintended keys due to a trailing *, risking incorrect validation behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR extends the STAC Authentication extension to support an OAuth 2.0 Token Exchange flow (tokenExchange, RFC 8693), enabling machine-discoverable multi-step authentication chains via scheme-level auth:refs.
Changes:
- Add
tokenExchangeas a pre-defined OAuth2 flow key and document the RFC 8693 semantics, chaining, and defaults. - Extend the JSON Schema to support scheme-level
auth:refs, require it whentokenExchangeis present, and addsubjectTokenTypeto the flow object. - Add a focused example for a two-step identity → token exchange pattern and document the addition in the changelog.
File summaries
| File | Description |
|---|---|
| README.md | Documents tokenExchange, scheme-level auth:refs chaining semantics, and subjectTokenType. |
| json-schema/schema.json | Updates schema for tokenExchange tokenUrl requirements, adds scheme-level auth:refs, and introduces subjectTokenType. |
| examples/collection-token-exchange.json | Adds a concrete two-step token exchange example using scheme-level chaining. |
| CHANGELOG.md | Records the new flow key and related schema/documentation additions under Unreleased. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This adds
tokenExchangeto the pre-defined OAuth2 flow keys, corresponding to the proposed OpenAPI addition OAI/OpenAPI-Specification#5428, as discussed with @m-mohr in portolan-sdi/portolan-cli#551. The flow key mirrors the OpenAPI proposal; the chaining pieces below (auth:refson a scheme,subjectTokenType) are deliberate STAC-side additions — this extension has precedent for those (s3,signedUrl), OpenAPI's security work is being reorganized on its own timeline, and they are candidates to propose upstream once that settles.The flow corresponds to RFC 8693 (OAuth 2.0 Token Exchange): the client presents a token obtained elsewhere — e.g. an identity token from an
openIdConnectscheme — at thetokenUrland receives a different token back, typically short-lived scoped credentials for direct data access. This is the pattern behind cloud STS endpoints and credential vending in data platforms; the driving STAC use case is catalogs whose assets live in access-controlled object storage.Changes:
tokenExchangeadded to the pre-defined flow keys;tokenUrlREQUIRED for it; paragraphs in the OAuth2 Flow Object section describing the RFC 8693 flow and how the chain resolves.auth:refsallowed on an Authentication Scheme Object (per review), listing the scheme(s) that may supply this scheme's input — one-of semantics, same as on Assets/Links (alternatives for access there, alternatives for input here); a sequence of steps is expressed as a chain of references, not as multiple entries in one list. A scheme declaring atokenExchangeflow MUST reference at least one scheme; the token obtained through whichever one the client uses is the RFC 8693subject_token— this makes the multi-step order machine-discoverable (a generic client resolves the chain from the document instead of hardcoding it), reusing the reference mechanism clients already implement. Chains MUST be acyclic (prose rule; not schema-detectable).subjectTokenType(optional) on thetokenExchangeflow: the RFC 8693subject_token_typeURN, which clients MUST use when present; when absent it defaults from thetypeof the referenced scheme the client used (openIdConnect→id_token,oauth2→access_token). Explicit over implicit for providers that issue more than one kind of token; alternatives that would need different URNs go in separate exchange schemes (the field is flow-level, one value can't cover both).tokenExchangein thetokenUrl-required pattern; scheme-levelauth:refsproperty;auth:refsrequired with at least one entry when atokenExchangeflow is declared;subjectTokenTypeon the flow object.examples/collection-token-exchange.json: a dedicated, focused example of the two-step pattern — asset →token_exchange(scheme-levelauth:refs) →oidc, with an explicitsubjectTokenType(the identity scheme is deliberately unreferenced by any Asset'sauth:refs, which is the case that motivates the chaining edge). Pre-existing examples untouched.npm testpasses (remark + stac-node-validator, examples valid). Documents in this shape also validate against the released v1.1.0 schema (scheme objects tolerate the extra field), so publishers can adopt it ahead of a release.Update (2026-08-31), per review: the chaining edge moved from a flow-level
subjectTokenSchemefield to scheme-levelauth:refs, as proposed — reusing the existing mechanism and generalizing to other scheme types (e.g. asignedUrlscheme naming the scheme that authenticates itsauthorizationApi; noted in that section). The OIDC both-tokens ambiguity is addressed with the explicitsubjectTokenType. Design choices worth stating:subjectTokenTypeis optional-with-defaults rather than required (happy to hard-require it instead).Update (2026-09-01), per review: scheme-level
auth:refsis now one-of, matching the Asset/Link semantics — any one referenced scheme supplies the input token, and multi-step sequences are expressed as chains ("a row ofauth:refs"), as proposed. ThetokenExchangecardinality relaxed from exactly one to at least one, and multiple identity providers collapse into a single exchange scheme listing the identity schemes as alternatives in its ownauth:refs(replacing the earlier one-exchange-scheme-per-IdP workaround).One design question deliberately not addressed here, for discussion: whether/how aResolved: scheme-leveltokenExchangescheme should declare which other scheme's token is the expectedsubject_token(e.g. a field referencing anauth:schemeskey vs. leaving it todescriptionprose). Kept out to stay a faithful mirror of the OpenAPI PR; happy to follow up in a separate issue/PR if there's interest.auth:refs(see update above).Note on tooling: AI tooling was used for research and drafting; I reviewed and take responsibility for all content.