Skip to content

Add tokenExchange OAuth2 flow (OAuth 2.0 Token Exchange, RFC 8693) - #44

Merged
m-mohr merged 9 commits into
stac-extensions:mainfrom
cayetanobv:token-exchange-flow
Sep 1, 2026
Merged

Add tokenExchange OAuth2 flow (OAuth 2.0 Token Exchange, RFC 8693)#44
m-mohr merged 9 commits into
stac-extensions:mainfrom
cayetanobv:token-exchange-flow

Conversation

@cayetanobv

@cayetanobv cayetanobv commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

This adds tokenExchange to 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:refs on 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 openIdConnect scheme — at the tokenUrl and 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:

  • README: tokenExchange added to the pre-defined flow keys; tokenUrl REQUIRED for it; paragraphs in the OAuth2 Flow Object section describing the RFC 8693 flow and how the chain resolves.
  • auth:refs allowed 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 a tokenExchange flow MUST reference at least one scheme; the token obtained through whichever one the client uses is the RFC 8693 subject_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 the tokenExchange flow: the RFC 8693 subject_token_type URN, which clients MUST use when present; when absent it defaults from the type of the referenced scheme the client used (openIdConnectid_token, oauth2access_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).
  • JSON Schema: tokenExchange in the tokenUrl-required pattern; scheme-level auth:refs property; auth:refs required with at least one entry when a tokenExchange flow is declared; subjectTokenType on the flow object.
  • examples/collection-token-exchange.json: a dedicated, focused example of the two-step pattern — asset → token_exchange (scheme-level auth:refs) → oidc, with an explicit subjectTokenType (the identity scheme is deliberately unreferenced by any Asset's auth:refs, which is the case that motivates the chaining edge). Pre-existing examples untouched.
  • CHANGELOG entry under Unreleased.

npm test passes (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 subjectTokenScheme field to scheme-level auth:refs, as proposed — reusing the existing mechanism and generalizing to other scheme types (e.g. a signedUrl scheme naming the scheme that authenticates its authorizationApi; noted in that section). The OIDC both-tokens ambiguity is addressed with the explicit subjectTokenType. Design choices worth stating: subjectTokenType is optional-with-defaults rather than required (happy to hard-require it instead).

Update (2026-09-01), per review: scheme-level auth:refs is 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 of auth:refs"), as proposed. The tokenExchange cardinality 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 own auth:refs (replacing the earlier one-exchange-scheme-per-IdP workaround).

One design question deliberately not addressed here, for discussion: whether/how a tokenExchange scheme should declare which other scheme's token is the expected subject_token (e.g. a field referencing an auth:schemes key vs. leaving it to description prose). Kept out to stay a faithful mirror of the OpenAPI PR; happy to follow up in a separate issue/PR if there's interest. Resolved: scheme-level auth:refs (see update above).

Note on tooling: AI tooling was used for research and drafting; I reviewed and take responsibility for all content.

…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.
@m-mohr
m-mohr self-requested a review July 7, 2026 15:41
cayetanobv added a commit to cayetanobv/portolan-cli that referenced this pull request Jul 7, 2026
…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.
cayetanobv added a commit to cayetanobv/portolan-cli that referenced this pull request Jul 13, 2026
…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.
@m-mohr

m-mohr commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

@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.
For example, for the example in the auth extension (excerpt):

"token_exchange": {
  "type": "oauth2",
  "auth:refs": ["oidc"],
  "flows": {

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.
@cayetanobv

Copy link
Copy Markdown
Contributor Author

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:

resolve:  asset (auth:refs) -> token_exchange (subjectTokenScheme) -> oidc
execute:  oidc (id_token) -> token_exchange (credentials) -> asset

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:

  • subjectTokenScheme is now REQUIRED for the tokenExchange flow, enforced in the schema. RFC 8693 already requires subject_token in the request, so the input always exists; what the RFC doesn't do is say where it comes from, it assumes a client configured out-of-band. STAC documents are meant to be self-describing, so naming that input is discovery's job: left optional, every client invents its own fallback and the spec defines none.
  • Documented grant_type, and that subject_token_type follows from the referenced scheme's type so it needs no field of its own.
  • Trimmed the example so those two edges are the visible content to avoid confusion.

What do you think?

@m-mohr

m-mohr commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Thanks for the update, @cayetanobv!

I'd still lean toward scheme-level auth:refs over subjectTokenScheme for the following reasons:

  • It's not inherited from anywhere, so alignment to an RFC is not needed
  • auth:refs reuses what clients already implement and it's consistent with the current extension. Also, auth:refs could similarly be used in other schemes, e.g. as an input for the signedUrl type.
  • The actor_token concern is fair but IMHO theoretical here, we can just say the referenced scheme is the subject_token. Actors we'll probably never need in STAC (famous last words).

Two additional issues worth improving:

  • The detection based on subject_token_type is potentially problematic, I think OIDC gives both tokens. So the order of the check mattres, which is not ideal. (Also I've seen a lot of weird OIDC/OAuth implementations out there, so I feel explicit may win over implicit here.)
  • The body says "optional" for subjectTokenScheme but the schema makes it REQUIRED.

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).
@cayetanobv

Copy link
Copy Markdown
Contributor Author

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.
Also in the README: a tokenExchange scheme references exactly one scheme (multi-IdP = one exchange scheme per identity, as asset-level alternatives); chains are acyclic; and auth:refs means alternatives on assets, prerequisites on schemes.
If this works for you, could we aim for the minor release you suggested?

@m-mohr

m-mohr commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

This sounds pretty good to me.
I'd think the prerequisites should be one of, not all of, or what do you think? It would be consistent and allow to retrieve tokens through multiple mechanism. If you want multiple in a row, this could be expressed with a row of auth:refs... @cayetanobv

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.
@cayetanobv

Copy link
Copy Markdown
Contributor Author

Thanks @m-mohr!

I'd think the prerequisites should be one of, not all of, or what do you think? It would be consistent and allow to retrieve tokens through multiple mechanism. If you want multiple in a row, this could be expressed with a row of auth:refs...

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).

@m-mohr

m-mohr commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Thank you!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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 tokenExchange as 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 when tokenExchange is present, and add subjectTokenType to 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.

Comment thread json-schema/schema.json
Comment thread README.md Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@m-mohr
m-mohr merged commit 2f01c3d into stac-extensions:main Sep 1, 2026
1 check passed
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.

3 participants