diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b8011e..c730d12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `tokenExchange` pre-defined OAuth2 flow key for OAuth 2.0 Token Exchange + ([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)), with `tokenUrl` required +- `auth:refs` allowed on an Authentication Scheme Object, listing the scheme(s) that + may supply this scheme's input — one-of semantics, matching `auth:refs` on Assets + and Links; sequences are expressed as chains of references. Required with at least + one entry for a scheme declaring a `tokenExchange` flow (the token obtained through + whichever referenced scheme was used is the RFC 8693 `subject_token`), so the + multi-step exchange is discoverable from the document; reference chains must be + acyclic +- `subjectTokenType` field (optional) on the `tokenExchange` flow: the RFC 8693 + `subject_token_type` URN, for when the referenced scheme yields more than one kind + of token; defaults from the `type` of the referenced scheme used + ## [v1.1.0] - 2023-04-28 ### Changed diff --git a/README.md b/README.md index 745fc69..5c1d5bd 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ The fields in the table below can be used in these parts of STAC documents: | Field Name | Type | Description | | ----------- | ---------- | ----------- | -| `auth:refs` | \[string\] | A property that specifies which schemes in `auth:schemes` may be used to access an Asset or Link. | +| `auth:refs` | \[string\] | A property that specifies which schemes in `auth:schemes` may be used to access an Asset or Link. `auth:refs` MAY also appear on an [Authentication Scheme Object](#authentication-scheme-object) itself — nested inside `auth:schemes`, so within Catalogs, Collections and Item Properties — where it declares which schemes may supply this scheme's input — see below. | ### Scheme Types @@ -76,10 +76,11 @@ library can be described, as well as a custom signed URL authentication scheme. | ------------------ | ------------------------------------------------------------ | --------------------- | ------------------------------------------------------------ | | `type` | string | *All* | **REQUIRED**. The authentication scheme type used to access the data (`http` \| `s3` \| `signedUrl` \| `oauth2` \| `apiKey` \| `openIdConnect` \| a custom scheme type ). | | `description` | string | *All* | Additional instructions for authentication. [CommonMark 0.29](https://commonmark.org/) syntax MAY be used for rich text representation. | +| `auth:refs` | \[string\] | *All* | Keys of other entries in `auth:schemes` that may supply this scheme's input: any *one* of the referenced schemes is completed first, and the token it yields is the input to this one. **REQUIRED with at least one entry** for an `oauth2` scheme declaring a `tokenExchange` flow (the token obtained through whichever referenced scheme was used is the RFC 8693 `subject_token`); MAY be used by other types, e.g. a `signedUrl` scheme naming the scheme(s) whose token authenticates requests to its `authorizationApi`. References MUST NOT form cycles. | | `name` | string | `apiKey` | **REQUIRED.** The name of the header, query, or cookie parameter to be used. | | `in` | string | `apiKey` | **REQUIRED.** The location of the API key (`query` \| `header` \| `cookie`). | | `scheme` | string | `http` | **REQUIRED.** The name of the HTTP Authorization scheme to be used in the [Authorization header as defined in RFC7235](https://tools.ietf.org/html/rfc7235#section-5.1). The values used SHOULD be registered in the [IANA Authentication Scheme registry](https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml). (`basic` \| `bearer` \| `digest` \| `dpop` \| `hoba` \| `mutual` \| `negotiate` \| `oauth` (1.0) \| `privatetoken` \| `scram-sha-1` \| `scram-sha-256` \| `vapid`) | -| `flows` | Map | `oauth2`, `signedUrl` | **REQUIRED.** Scenarios an API client performs to get an access token from the authorization server. For `oauth2` the following keys are pre-defined for the corresponding OAuth flows: `authorizationCode` \| `implicit` \| `password ` \| `clientCredentials`. The OAuth2 Flow Object applies for `oauth2`, the Signed URL Object applies to `signedUrl`. | +| `flows` | Map | `oauth2`, `signedUrl` | **REQUIRED.** Scenarios an API client performs to get an access token from the authorization server. For `oauth2` the following keys are pre-defined for the corresponding OAuth flows: `authorizationCode` \| `implicit` \| `password` \| `clientCredentials` \| `tokenExchange`. The OAuth2 Flow Object applies for `oauth2`, the Signed URL Object applies to `signedUrl`. | | `openIdConnectUrl` | string | `openIdConnect` | **REQUIRED.** OpenID Connect URL to discover OpenID configuration values. This MUST be in the form of a URL. | The column "Applies to" specifies for which values of `type` the fields only apply. @@ -89,16 +90,59 @@ They are also only required in this context. Based on the [OpenAPI OAuth Flow Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flows-object). Allows configuration of the supported OAuth Flows. +The `tokenExchange` flow corresponds to OAuth 2.0 Token Exchange as defined in +[RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693). +The client presents a token obtained through another scheme (for example an OpenID +Connect identity token) at the `tokenUrl` as the `subject_token`, with +`grant_type=urn:ietf:params:oauth:grant-type:token-exchange`, and receives a different +security token back, such as short-lived, scoped credentials for direct data access. +The response is the one defined in +[RFC 8693, Section 2.2.1](https://datatracker.ietf.org/doc/html/rfc8693#section-2.2.1), +which reports the kind of token issued in `issued_token_type`. + +Which scheme supplies the input token is declared on the *scheme*, with `auth:refs` +(see the [Authentication Scheme Object](#authentication-scheme-object)): a scheme +declaring a `tokenExchange` flow MUST reference at least one other scheme in +`auth:refs`; the client completes any *one* of the referenced schemes, and the token +obtained through it is the `subject_token`. This is what makes the multi-step flow +machine-discoverable — a client resolves the chain from the document (asset → guarding +scheme → input scheme) instead of hardcoding the order — and it reuses the reference +mechanism clients already implement for Assets and Links, with the same one-of +semantics in both positions: on an Asset or Link the referenced schemes are alternative +ways to access the resource, on a scheme they are alternative ways to obtain its input. +A sequence of steps is expressed as a chain — each scheme referencing the one before +it — not as multiple entries in one list. References MUST NOT form cycles — clients +cannot be expected to resolve one, and the JSON Schema cannot detect it. + +The RFC 8693 `subject_token_type` request parameter MUST be taken from the flow's +`subjectTokenType` field when present. When the field is absent it follows from the +`type` of the referenced scheme the client used: +`urn:ietf:params:oauth:token-type:id_token` for `openIdConnect` and +`urn:ietf:params:oauth:token-type:access_token` for `oauth2`. Publishers SHOULD set +`subjectTokenType` explicitly whenever a referenced scheme yields more than one kind +of token (an OpenID Connect provider issues both an ID token and an access token). +Since the field applies to the flow as a whole, referenced alternatives requiring +*different* `subject_token_type` values SHOULD be split into separate exchange +schemes. + +To accept identities from more than one provider, list the identity schemes as +alternatives in the exchange scheme's own `auth:refs` — a token obtained through any +one of them is exchanged at the same `tokenUrl`. | Field Name | Type | Description | | ------------------ | ----------------------- | ------------------------------------------------------------ | | `authorizationUrl` | `string` | **REQUIRED** for parent keys: `"implicit"`, `"authorizationCode"`. The authorization URL to be used for this flow. This MUST be in the form of a URL. | -| `tokenUrl` | `string` | **REQUIRED** for parent keys: `"password"`, `"clientCredentials"`, `"authorizationCode"`. The token URL to be used for this flow. This MUST be in the form of a URL. | +| `tokenUrl` | `string` | **REQUIRED** for parent keys: `"password"`, `"clientCredentials"`, `"authorizationCode"`, `"tokenExchange"`. The token URL to be used for this flow. This MUST be in the form of a URL. | +| `subjectTokenType` | `string` | For parent key `"tokenExchange"`: the RFC 8693 `subject_token_type` URN of the token presented at the `tokenUrl` (e.g. `urn:ietf:params:oauth:token-type:id_token`). When absent, it follows from the `type` of the scheme in the scheme's `auth:refs` through which the token was obtained. | | `scopes` | Map<`string`, `string`> | **REQUIRED.** The available scopes for the authentication scheme. A map between the scope name and a short description for it. The map MAY be empty. | | `refreshUrl` | `string` | The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. | ### Signed URL Object +A `signedUrl` scheme MAY declare the scheme(s) through which its `authorizationApi` +requests can be authenticated via `auth:refs` on the scheme (see the +[Authentication Scheme Object](#authentication-scheme-object)). + | Field Name | Type | Description | | ------------------ | -------------------------------------------------- | ------------------------------------------------------------ | | `method` | `string` | **REQUIRED.** The method to be used for requests | @@ -120,6 +164,8 @@ Definition for a request parameter. ## Examples `auth:schemes` may be referenced identically in a STAC Asset or Link objects. Examples of these two use-cases are provided below. +A complete, focused example of the two-step token-exchange pattern (identity scheme + exchange scheme linked via scheme-level `auth:refs`) +is provided in [examples/collection-token-exchange.json](examples/collection-token-exchange.json). ### Schema definitions diff --git a/examples/collection-token-exchange.json b/examples/collection-token-exchange.json new file mode 100644 index 0000000..0c49547 --- /dev/null +++ b/examples/collection-token-exchange.json @@ -0,0 +1,81 @@ +{ + "type": "Collection", + "stac_version": "1.0.0", + "id": "collection-token-exchange", + "title": "Gated collection — two-step access via OAuth 2.0 Token Exchange", + "description": "Public STAC metadata, access-controlled assets. The asset points at the scheme that guards it (`auth:refs`); that scheme points the same way (scheme-level `auth:refs`) at the identity scheme(s) whose token it exchanges — any one of them. The `oidc` scheme guards no asset on its own: it is the exchange's input.", + "license": "proprietary", + "stac_extensions": [ + "https://stac-extensions.github.io/authentication/v1.1.0/schema.json" + ], + "auth:schemes": { + "oidc": { + "type": "openIdConnect", + "openIdConnectUrl": "https://auth.example.com/.well-known/openid-configuration" + }, + "token_exchange": { + "type": "oauth2", + "description": "RFC 8693 token exchange: present the `oidc` identity token as the `subject_token` at the `tokenUrl` and receive short-lived, scoped credentials covering every asset below.", + "auth:refs": [ + "oidc" + ], + "flows": { + "tokenExchange": { + "tokenUrl": "https://credentials.example.com/oauth/token", + "subjectTokenType": "urn:ietf:params:oauth:token-type:id_token", + "scopes": { + "read:data": "Read this collection's data assets" + } + } + } + } + }, + "extent": { + "spatial": { + "bbox": [ + [ + -3.9, + 40.3, + -3.5, + 40.6 + ] + ] + }, + "temporal": { + "interval": [ + [ + "2025-01-01T00:00:00Z", + null + ] + ] + } + }, + "links": [ + { + "href": "https://example.com/examples/collection-token-exchange.json", + "rel": "self" + } + ], + "assets": { + "data": { + "href": "https://storage.example.com/private-bucket/data.parquet", + "type": "application/vnd.apache.parquet", + "roles": [ + "data" + ], + "auth:refs": [ + "token_exchange" + ] + }, + "metadata": { + "href": "https://storage.example.com/private-bucket/metadata.json", + "type": "application/json", + "roles": [ + "metadata" + ], + "auth:refs": [ + "token_exchange" + ] + } + } +} diff --git a/json-schema/schema.json b/json-schema/schema.json index cd051c9..f46339e 100644 --- a/json-schema/schema.json +++ b/json-schema/schema.json @@ -84,7 +84,7 @@ } ] } - ], + ], "definitions": { "schemes_field": { "type": "object", @@ -166,6 +166,13 @@ "description": { "title": "Authentication scheme description", "type": "string" + }, + "auth:refs": { + "title": "Keys of schemes in auth:schemes that may supply this scheme's input (any one of them is completed first; its result is this scheme's input, e.g. the subject_token of a tokenExchange flow)", + "type": "array", + "items": { + "type": "string" + } } }, "allOf": [ @@ -298,7 +305,7 @@ "authorizationUrl" ] }, - "^(password|clientCredentials|authorizationCode)*$": { + "^(password|clientCredentials|authorizationCode|tokenExchange)*$": { "required": [ "tokenUrl" ] @@ -356,6 +363,38 @@ "openIdConnectUrl": false } } + }, + { + "$comment": "A tokenExchange flow consumes another scheme's token: the scheme must reference at least one scheme; the token obtained through whichever one the client uses is the RFC 8693 subject_token.", + "if": { + "type": "object", + "required": [ + "flows" + ], + "properties": { + "type": { + "const": "oauth2" + }, + "flows": { + "type": "object", + "required": [ + "tokenExchange" + ] + } + } + }, + "then": { + "type": "object", + "required": [ + "auth:refs" + ], + "properties": { + "auth:refs": { + "type": "array", + "minItems": 1 + } + } + } } ] }, @@ -375,6 +414,15 @@ "type": "string", "format": "uri" }, + "subjectTokenType": { + "title": "RFC 8693 subject_token_type URN of the token presented at the tokenUrl (tokenExchange flow)", + "type": "string", + "format": "uri", + "examples": [ + "urn:ietf:params:oauth:token-type:id_token", + "urn:ietf:params:oauth:token-type:access_token" + ] + }, "refreshUrl": { "title": "The URL to be used for obtaining refresh tokens", "type": "string",