From 08779f3ec75940eb91983789d719e4eefd8ce40c Mon Sep 17 00:00:00 2001 From: Pavel Shukhman Date: Tue, 25 Aug 2026 21:16:36 -0400 Subject: [PATCH 1/2] feat: token exchange endpoint and documentation Signed-off-by: Pavel Shukhman --- auth/readme.md | 221 ++++++++++++++++++++++++++++++++++++++++------ spec/openapi.yaml | 175 +++++++++++++++++++++++++++++++++++- 2 files changed, 367 insertions(+), 29 deletions(-) diff --git a/auth/readme.md b/auth/readme.md index 9e46730..06000d0 100644 --- a/auth/readme.md +++ b/auth/readme.md @@ -7,7 +7,7 @@ of a TEA service - the discovery and download of software transparency artefacts __Authorization__: A user of a TEA service may get access to all objects (components, collections) and artefacts or just a subset, depending on the publisher of the data. Authorization is connected -to __authentication__. +to __authentication__. The level of authorization is up to the implementer of the TEA implementation and the publisher, whether an identity gets access to all objects in a service or just a subset. @@ -25,36 +25,205 @@ the customer has not aquired. For most Open Source projects, implementing authentication - setting up accounts and managing authorization - does not make much sense, since the information is usually in the open any way. -This specification does not impose any requirement on authentication on a TEA service. But should -the provider implement authentication, two methods are supported in order to promote interoperability. +## Scope of this specification -* HTTP Bearer Token Authentication -* Mutual TLS with verifiable client and server certificates +This specification does not require a TEA service to authenticate its users. A service that +publishes openly need not implement any of what follows. -A client may use both HTTP bearer token auth and TLS client certificates -when accessing multiple TEA services. It is up to the service provider to select authentication. +Where a service does authenticate, interoperability requires that every TEA client can +authenticate against every TEA server without server-specific code. This specification therefore +defines a single mandatory baseline and leaves everything above it optional: + +* A TEA server that requires authentication __shall__ implement the token endpoint (`POST /token`) + described below, and __shall__ support the API key credential exchange on it. +* A TEA server __may__ support additional credential types on the same endpoint - federated + identity from an external OpenID Connect or SAML provider, mutual TLS, or others. +* A client that holds an API key for a TEA service __shall__ be able to reach every endpoint it + is authorized for using only the baseline exchange. + +Two consequences are worth stating explicitly, because they are what make the baseline useful: + +* All authenticated access goes through the token endpoint. A server __shall not__ accept an API + key directly on the resource endpoints, and a client __shall not__ present one there. The API key + is exchanged for an access token, and the access token is what the resource endpoints see. +* Whatever credential a client starts with, it ends up holding the same thing: a TEA access token + presented as an HTTP bearer token. Clients therefore need one code path for the API itself, + regardless of how the server manages identity. + +## The token endpoint + +The token endpoint is `POST /token`, relative to the TEA API base URL, and is defined in +[the OpenAPI specification](../spec/openapi.yaml). It is an OAuth 2.0 token endpoint as defined +in [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749) section 3.2; this specification constrains +which grant types a conforming server has to accept, and adds nothing to the wire format. + +RFC 6749 leaves the location of the token endpoint outside its scope - `/token` appears only in its +examples - so TEA fixes the path here rather than requiring clients to discover it. TEA defines no +OAuth 2.0 authorization endpoint (RFC 6749 section 3.1): there is no interactive, browser-based +consent step in TEA, and every grant type described below is one a client can complete on its own. + +### API key exchange (mandatory) + +An API key consists of two parts issued together by the service: an __identifier__ and a +__secret__. The identifier need not be confidential; the secret is. + +The client presents them using the HTTP Basic authentication scheme +([RFC 7617](https://www.rfc-editor.org/rfc/rfc7617)) - the identifier as the user-id and the secret +as the password - with the `client_credentials` grant type. This is OAuth 2.0 client +authentication as described in RFC 6749 section 2.3.1, which requires a token endpoint to support +Basic for clients that were issued a secret. + +```http +POST /token HTTP/1.1 +Host: tea.example.com +Authorization: Basic +Content-Type: application/x-www-form-urlencoded + +grant_type=client_credentials +``` + +Per RFC 6749 section 2.3.1 the identifier and secret are each `application/x-www-form-urlencoded` +encoded before being joined with a colon and Base64 encoded. This only matters when a credential +contains a colon or characters outside US-ASCII, but clients and servers __shall__ apply it so that +such credentials interoperate. + +Servers __may__ additionally accept the credentials as `client_id` and `client_secret` form +parameters in the request body. RFC 6749 section 2.3.1 marks this as NOT RECOMMENDED and limits it +to clients that cannot use Basic, so clients __should__ use Basic where they can. -## HTTP bearer token auth +A successful response is the standard OAuth 2.0 token response (RFC 6749 section 5.1): + +```http +HTTP/1.1 200 OK +Content-Type: application/json;charset=UTF-8 +Cache-Control: no-store + +{ + "access_token": "2YotnFZFEjr1zCsicMWpAA", + "token_type": "Bearer", + "expires_in": 3600 +} +``` + +Errors are the standard OAuth 2.0 token error response (RFC 6749 section 5.2), for example +`invalid_client` for a bad API key or `unsupported_grant_type` for a grant the server does not +implement. + +### Using the access token + +The access token is presented on every other TEA endpoint as an HTTP bearer token +([RFC 6750](https://www.rfc-editor.org/rfc/rfc6750)): + +```http +GET /product/d4d9f54a-abcf-11ee-ac79-1a52914d44b HTTP/1.1 +Host: tea.example.com +Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA +``` + +__The access token is opaque to the client.__ Clients __shall not__ inspect, parse, or depend on the +contents of the token, and __shall not__ assume any particular format. Servers are free to issue a +random handle, a signed token such as a JWT access token +([RFC 9068](https://www.rfc-editor.org/rfc/rfc9068)), or anything else, and to change that choice +without notice; only the issuing server interprets it. + +Token lifetime, storage, rotation, and revocation are implementation matters and are deliberately +not specified. Two rules keep clients simple in spite of that: + +* Servers __should__ return `expires_in`, so that a client can obtain a new token before the current + one expires rather than discovering expiry through a failed request. +* If a request to a resource endpoint fails with `401` and the `invalid_token` error code, the + client __may__ obtain a new token from the token endpoint and retry the request once, as described + in RFC 6750 section 3.1. A client __should not__ retry more than once for a single request. + +Between them these cover server-side revocation without the client having to know it happened: +RFC 6750 defines `invalid_token` as covering tokens that are "expired, revoked, malformed, or invalid +for other reasons". This specification defines no revocation endpoint, and clients need not +implement one. + +__No refresh tokens.__ Servers __should not__ issue refresh tokens with the `client_credentials` +grant, following RFC 6749 section 4.4.3. A refresh token exists so that a client can obtain a new +access token without re-presenting a credential, which matters when the credential belongs to a user +who is no longer present. A TEA client holds its own API key permanently and can simply call the +token endpoint again, so a refresh token would be a second long-lived secret to store and protect for +no benefit. + +## Optional credential types + +A server __may__ accept credentials other than an API key at the same token endpoint, selected by the +`grant_type` parameter. In every case the response is the same token response, and the resulting +access token is used the same way, so clients that already implement the baseline need only the +additional request. + +The following are the cases we expect to be common. All are existing OAuth 2.0 profiles; TEA adds +nothing to them. + +| Use case | Grant type | Specification | +|---|---|---| +| Enterprise SSO where the customer's identity provider issues SAML assertions | `urn:ietf:params:oauth:grant-type:saml2-bearer` | [RFC 7522](https://www.rfc-editor.org/rfc/rfc7522) | +| OpenID Connect, or any provider issuing signed JWTs, including workload identity in CI systems | `urn:ietf:params:oauth:grant-type:jwt-bearer` | [RFC 7523](https://www.rfc-editor.org/rfc/rfc7523) | +| A client already holding a token from another security domain, exchanged for a TEA token | `urn:ietf:params:oauth:grant-type:token-exchange` | [RFC 8693](https://www.rfc-editor.org/rfc/rfc8693) | +| A client authenticated by a TLS client certificate rather than a shared secret | `client_credentials` with mutual TLS client authentication | [RFC 8705](https://www.rfc-editor.org/rfc/rfc8705) | + +[RFC 7521](https://www.rfc-editor.org/rfc/rfc7521) defines the common framework the two assertion +grants share. + +Note that a server which delegates identity to an external provider still issues its own TEA access +token from its own token endpoint. The external provider authenticates the user; the TEA server +decides what that user may see. This keeps authorization with the party that owns the data, and +keeps the resource endpoints validating exactly one kind of token. + +### Mutual TLS + +Mutual TLS is a client authentication method at the token endpoint, not a separate way in. A client +authenticated by a certificate presents no `Authorization` header on its token request; the +certificate identifies it. RFC 8705 additionally allows the issued access token to be bound to the +certificate, so that a stolen token is useless without the corresponding private key. -The API will support HTTP bearer token in the __Authorization:__ http header. -How the token is aquired is out of scope for this -specification, as is the potential content of the token. +Client certificates are managed by the service and provided in a service-specific way. Clients +__should__ be able to configure a separate client certificate and private key per TEA service, and +__should not__ assume that a client certificate for one service is trusted anywhere else. + +## Transport security + +All of the above assumes TLS. Credentials and bearer tokens are transmitted in the clear at the HTTP +layer, so a TEA server __shall__ be reachable only over TLS, and clients __shall__ verify the server +certificate. This restates RFC 6749 section 3.2 and RFC 6750 section 5. + +## Discovery -As an example the token can be downloaded from a customer support portal with a long-term -validity. This token is then installed into the software transparency platform (the TEA client) -and used to automatically retrieve software transparency artefacts. - -For each TEA service, one bearer token is needed. The token itself (or the backend) should -specify authorization for the token. - -## Mutual TLS - -For Mutual TLS the client certificates will be managed by the service and provided -in a service-specific way. Clients should be able to configure a separate client certificate -(and private key) on a per-service level, not assuming that a client certificate -for one service is trusted anywhere else. +A client that has an API key for a service knows to call the token endpoint. A client that does not +know whether a service requires authentication at all can simply issue the request: a server that +requires authentication answers `401` with a `WWW-Authenticate` header naming the `Bearer` scheme, +as described in RFC 6750 section 3. + +Servers that delegate identity to an external provider __may__ publish OAuth 2.0 protected resource +metadata ([RFC 9728](https://www.rfc-editor.org/rfc/rfc9728)) and reference it from the +`WWW-Authenticate` challenge, allowing a client to locate the authorization server automatically. +This is optional; it does not replace the token endpoint, which remains the interoperable baseline. ## References -* RFC 6750: The Oauth 2.0 Authorization Framework: Bearer Token - Usage (https://www.rfc-editor.org/rfc/rfc6750) +* RFC 6749: The OAuth 2.0 Authorization Framework (https://www.rfc-editor.org/rfc/rfc6749) +* RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage (https://www.rfc-editor.org/rfc/rfc6750) +* RFC 7617: The 'Basic' HTTP Authentication Scheme (https://www.rfc-editor.org/rfc/rfc7617) +* RFC 7521: Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants (https://www.rfc-editor.org/rfc/rfc7521) +* RFC 7522: SAML 2.0 Profile for OAuth 2.0 Client Authentication and Authorization Grants (https://www.rfc-editor.org/rfc/rfc7522) +* RFC 7523: JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants (https://www.rfc-editor.org/rfc/rfc7523) +* RFC 8693: OAuth 2.0 Token Exchange (https://www.rfc-editor.org/rfc/rfc8693) +* RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens (https://www.rfc-editor.org/rfc/rfc8705) +* RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens (https://www.rfc-editor.org/rfc/rfc9068) +* RFC 9728: OAuth 2.0 Protected Resource Metadata (https://www.rfc-editor.org/rfc/rfc9728) + +### A note on API keys and `X-API-Key` + +There is no IETF specification for API keys, and no registered HTTP authentication scheme for them. +A survey of the IETF datatracker finds only working-group discussion material and an early +individual draft, nothing normative. The widespread `X-API-Key` header is additionally at odds with +[RFC 6648](https://www.rfc-editor.org/rfc/rfc6648), a Best Current Practice that deprecates the `X-` +prefix for new header fields. + +This specification therefore carries the API key in the standard `Authorization` header using the +Basic scheme, which is registered, specified, supported directly by essentially every HTTP client +library, and already the mandatory-to-implement client authentication method for OAuth 2.0 token +endpoints. The result is no harder for a client than a custom header - it is one line in any HTTP +library - and it avoids inventing a TEA-specific credential format. diff --git a/spec/openapi.yaml b/spec/openapi.yaml index 11f5187..ceb5238 100644 --- a/spec/openapi.yaml +++ b/spec/openapi.yaml @@ -12,7 +12,7 @@ info: license: name: Apache 2.0 url: https://github.com/CycloneDX/transparency-exchange-api/blob/main/LICENSE - version: 0.4.0 + version: 0.5.0 servers: - url: http://localhost/tea/v1 description: Local development @@ -548,6 +548,43 @@ paths: $ref: "#/components/responses/404-object-by-id-not-found" tags: - TEA Artifact + /token: + post: + description: | + Exchange credentials for a TEA access token. + + A TEA server that requires authentication shall implement this endpoint, and + shall support the `client_credentials` grant type with HTTP Basic client + authentication (RFC 6749 section 2.3.1, RFC 7617): the API key identifier is + sent as the user-id and the API key secret as the password. + + Servers may support additional grant types for federated identity, for example + SAML 2.0 assertions (RFC 7522), JWT assertions (RFC 7523), or token exchange + (RFC 8693), and may authenticate the client with mutual TLS (RFC 8705) instead + of Basic. Whichever grant type is used, the token returned by this endpoint is + the only credential accepted on the other TEA endpoints. + + The access token is opaque to the client: clients shall not inspect, parse, or + depend on its contents. + operationId: requestToken + security: + - basicAuth: [] + - {} + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/token-request" + responses: + '200': + $ref: "#/components/responses/token-issued" + '400': + $ref: "#/components/responses/400-token-error" + '401': + $ref: "#/components/responses/401-token-error" + tags: + - TEA Authentication /discovery: get: description: Discovery endpoint which resolves TEI into product release UUID. @@ -580,6 +617,85 @@ components: format: date-time pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$" example: '2024-03-20T15:30:00Z' + token-request: + type: object + description: | + Token request, as defined in RFC 6749 section 4.4.2 for the client-credentials + grant. Additional parameters apply only to the optional grant types. + required: + - grant_type + properties: + grant_type: + type: string + description: | + OAuth 2.0 grant type. Servers shall support `client_credentials`. Servers may + support assertion grants (`urn:ietf:params:oauth:grant-type:saml2-bearer`, + `urn:ietf:params:oauth:grant-type:jwt-bearer`) or token exchange + (`urn:ietf:params:oauth:grant-type:token-exchange`). + example: client_credentials + scope: + type: string + description: | + Optional space-delimited scope request (RFC 6749 section 3.3). TEA does not + define scope values; servers may ignore this parameter. + assertion: + type: string + description: Assertion value, when an assertion grant type is used (RFC 7521 section 4.1). + subject_token: + type: string + description: Subject token, when the token-exchange grant type is used (RFC 8693 section 2.1). + subject_token_type: + type: string + description: Type of `subject_token`, when the token-exchange grant type is used (RFC 8693 section 2.1). + token-response: + type: object + description: Successful token response, as defined in RFC 6749 section 5.1. + required: + - access_token + - token_type + properties: + access_token: + type: string + description: | + The issued access token, presented on other TEA endpoints as + `Authorization: Bearer `. Opaque to the client. + example: 2YotnFZFEjr1zCsicMWpAA + token_type: + type: string + description: Token type. Always `Bearer` in TEA. + const: Bearer + expires_in: + type: integer + description: | + Lifetime of the access token in seconds. Servers should include this so that + clients can re-authenticate before expiry rather than on failure. + example: 3600 + scope: + type: string + description: Granted scope, when it differs from the scope requested. + token-error-response: + type: object + description: Token error response, as defined in RFC 6749 section 5.2. + required: + - error + properties: + error: + type: string + description: Error code as defined in RFC 6749 section 5.2. + enum: + - invalid_request + - invalid_client + - invalid_grant + - unauthorized_client + - unsupported_grant_type + - invalid_scope + error_description: + type: string + description: Human-readable text providing additional information about the error. + error_uri: + type: string + format: uri + description: URI of a human-readable web page with information about the error. identifier: type: object description: An identifier with a specified type @@ -1605,9 +1721,53 @@ components: content: application/json: {} 401-unauthorized: - description: Authentication required + description: | + Authentication required, or the presented access token is expired, revoked, or + otherwise invalid. Servers shall include a `WWW-Authenticate` header as defined + in RFC 6750 section 3. On the `invalid_token` error a client may obtain a fresh + token from `/token` and retry the request once (RFC 6750 section 3.1). + headers: + WWW-Authenticate: + description: Bearer challenge, as defined in RFC 6750 section 3. + schema: + type: string + example: Bearer realm="tea", error="invalid_token", error_description="The access token expired" content: application/json: {} + token-issued: + description: Credentials accepted, access token issued + headers: + Cache-Control: + description: Servers shall set `no-store` on token responses (RFC 6749 section 5.1). + schema: + type: string + example: no-store + content: + application/json: + schema: + $ref: "#/components/schemas/token-response" + 400-token-error: + description: | + The token request was malformed, used an unsupported grant type, or the + credentials presented were not valid for the requested grant. + content: + application/json: + schema: + $ref: "#/components/schemas/token-error-response" + 401-token-error: + description: | + Client authentication failed. Returned instead of 400 when the client attempted + to authenticate using the `Authorization` header (RFC 6749 section 5.2). + headers: + WWW-Authenticate: + description: Challenge indicating the client authentication methods supported by the token endpoint. + schema: + type: string + example: Basic realm="tea" + content: + application/json: + schema: + $ref: "#/components/schemas/token-error-response" 404-object-by-id-not-found: description: Object requested by identifier not found content: @@ -1851,13 +2011,22 @@ components: bearerAuth: type: http scheme: bearer + description: | + A TEA access token obtained from `/token`, presented as + `Authorization: Bearer ` (RFC 6750). This is the only credential + accepted on TEA endpoints other than `/token`. basicAuth: type: http scheme: basic + description: | + API key credentials, presented to `/token` only: the API key identifier as the + user-id and the API key secret as the password (RFC 7617, RFC 6749 section + 2.3.1). Servers shall not accept API key credentials directly on other TEA + endpoints; clients shall exchange them for an access token first. security: - bearerAuth: [] - - basicAuth: [] tags: + - name: TEA Authentication - name: TEA Product - name: TEA Product Release - name: TEA Component From a0646ed74cb1597173c2da76127b7cb58eab1288 Mon Sep 17 00:00:00 2001 From: "Claude Code (ReARM Agent)" Date: Fri, 4 Sep 2026 16:54:35 +0000 Subject: [PATCH 2/2] docs: client flow, open and mixed servers; address review on the token endpoint Follow-up to @oej's review of #258. - The token endpoint section referred to the OpenAPI file by a relative repository link, which will not survive extraction into the ECMA document. It now refers to the TEA OpenAPI specification by name. - The `invalid_token` retry rule did not say where the error code lives. It is the `error` attribute of the `WWW-Authenticate: Bearer` challenge header on the 401 response (RFC 6750 section 3), not part of the token, which stays opaque. The sentence names the header and shows it. - The Discovery section is replaced by a Client flow section giving the complete sequence: try the resource without credentials; 200 means open; 401 with a Bearer challenge means obtain a token from /token and repeat with the bearer token; on invalid_token, refresh and retry once. A client holding an API key may go to the token endpoint directly. Clients shall not probe /token to discover authentication, and shall not send the API key to resource endpoints. - New Servers without authentication section: such a server need not implement /token (a token response must carry a token and the mandatory grant requires client authentication, so it could only issue a meaningless one), shall not answer 401, and shall ignore rather than reject a bearer token presented anyway. - New Mixed servers section: a server with any protected endpoint is a server that requires authentication and implements the baseline in full; open endpoints behave as on an open server; protected endpoints challenge per endpoint; authorization of protected data stays the server's decision. - Protected resource metadata (RFC 9728) kept as an optional subsection, stated not to replace the challenge as the discovery mechanism. - Scope section points at the two new sections. - Spec: the /token description says a server that requires authentication on any endpoint implements it, an open server need not, and clients do not probe it; a 404 means only "not implemented". Validated with openapi-generator v7.12.0 (pre-existing unused-model warning only). All intra-document anchors resolve. Signed-off-by: Claude Code (ReARM Agent) --- auth/readme.md | 96 ++++++++++++++++++++++++++++++++++++++++------- spec/openapi.yaml | 14 +++++-- 2 files changed, 93 insertions(+), 17 deletions(-) diff --git a/auth/readme.md b/auth/readme.md index 06000d0..30b49ac 100644 --- a/auth/readme.md +++ b/auth/readme.md @@ -28,7 +28,11 @@ authorization - does not make much sense, since the information is usually in th ## Scope of this specification This specification does not require a TEA service to authenticate its users. A service that -publishes openly need not implement any of what follows. +publishes openly need not implement any of what follows, including the token endpoint; see +[Servers without authentication](#servers-without-authentication) for what such a server and its +clients do instead. A service that requires authentication for any of its endpoints is a service +that requires authentication, and implements the baseline in full; see +[Mixed servers](#mixed-servers). Where a service does authenticate, interoperability requires that every TEA client can authenticate against every TEA server without server-specific code. This specification therefore @@ -52,8 +56,8 @@ Two consequences are worth stating explicitly, because they are what make the ba ## The token endpoint -The token endpoint is `POST /token`, relative to the TEA API base URL, and is defined in -[the OpenAPI specification](../spec/openapi.yaml). It is an OAuth 2.0 token endpoint as defined +The token endpoint is `POST /token`, relative to the TEA API base URL, and is defined in the TEA +OpenAPI specification alongside the resource endpoints. It is an OAuth 2.0 token endpoint as defined in [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749) section 3.2; this specification constrains which grant types a conforming server has to accept, and adds nothing to the wire format. @@ -131,9 +135,16 @@ not specified. Two rules keep clients simple in spite of that: * Servers __should__ return `expires_in`, so that a client can obtain a new token before the current one expires rather than discovering expiry through a failed request. -* If a request to a resource endpoint fails with `401` and the `invalid_token` error code, the - client __may__ obtain a new token from the token endpoint and retry the request once, as described - in RFC 6750 section 3.1. A client __should not__ retry more than once for a single request. +* If a request to a resource endpoint fails with `401` and the `WWW-Authenticate` challenge header + carries `error="invalid_token"`, the client __may__ obtain a new token from the token endpoint and + retry the request once, as described in RFC 6750 section 3.1. A client __should not__ retry more + than once for a single request. The error code is an attribute of the challenge header; it is not + carried in the token, which remains opaque: + + ```http + HTTP/1.1 401 Unauthorized + WWW-Authenticate: Bearer realm="tea", error="invalid_token", error_description="The access token expired" + ``` Between them these cover server-side revocation without the client having to know it happened: RFC 6750 defines `invalid_token` as covering tokens that are "expired, revoked, malformed, or invalid @@ -189,17 +200,76 @@ All of the above assumes TLS. Credentials and bearer tokens are transmitted in t layer, so a TEA server __shall__ be reachable only over TLS, and clients __shall__ verify the server certificate. This restates RFC 6749 section 3.2 and RFC 6750 section 5. -## Discovery - -A client that has an API key for a service knows to call the token endpoint. A client that does not -know whether a service requires authentication at all can simply issue the request: a server that -requires authentication answers `401` with a `WWW-Authenticate` header naming the `Bearer` scheme, -as described in RFC 6750 section 3. +## Client flow + +Whether a server requires authentication is discovered by using it, not by configuration and not by +probing the token endpoint. The complete flow for a client that does not know in advance: + +1. The client sends the resource request it wants, with no credentials. +2. If the server does not require authentication for that endpoint, it answers `200` with the + resource. The client is done. +3. If the server requires authentication, it answers `401` with a `WWW-Authenticate` header naming + the `Bearer` scheme, as described in RFC 6750 section 3. This challenge is the only signal a + client needs. +4. The client calls `POST /token` with its credential - for the baseline, the API key over HTTP + Basic - and receives an access token. +5. The client repeats the resource request with `Authorization: Bearer `, and presents + the same token on subsequent requests until it expires or is rejected. +6. When a later request fails with `401` and `error="invalid_token"`, the client obtains a fresh + token and retries that request once, as described above. + +A client that already holds an API key for a service __may__ skip steps 1 to 3 and call the token +endpoint first: being issued a key is itself the signal that the service requires authentication. +Trying the resource first is for the case where the client does not know. + +Two things the flow deliberately avoids. A client __shall not__ probe `/token` to find out whether +authentication exists; the challenge on the resource is the discovery mechanism, and a `404` from +`/token` means only that the endpoint is not implemented. And a client __shall not__ send its API key +to a resource endpoint; the key goes to the token endpoint only, and resource endpoints see bearer +tokens only. + +The cost of the unknown case is one wasted request per server, once. This is the standard HTTP +pattern and is preferred over any configuration or discovery step a client would otherwise need. + +### Servers without authentication + +A server that requires no authentication on any endpoint: + +* __need not__ implement the token endpoint. There is nothing to exchange: an OAuth 2.0 token + response has to carry an access token, and the mandatory grant requires the client to + authenticate, so a token endpoint on an open server could only issue a token that means nothing. +* __shall not__ answer any resource request with `401`. Its clients complete step 2 of the flow + above and never look for the token endpoint. +* __shall__ ignore, rather than reject, an `Authorization: Bearer` header a client presents anyway, + for example a client that obtained a token elsewhere or applies one by habit. A token has no + meaning on an open server, and ignoring it keeps such clients working. + +### Mixed servers + +A server may publish some endpoints openly and require authentication for others - for example, +listing products and releases openly while restricting artifact downloads to customers, as +described under [Requirements](#requirements). Such a server is a server that requires +authentication: + +* it __shall__ implement the token endpoint and the baseline exchange, because at least one endpoint + needs them; +* its open endpoints behave as on a server without authentication: they answer without a token and + ignore a token that is presented; +* its protected endpoints answer `401` with the `Bearer` challenge when no valid token is presented, + which is how a client learns, per endpoint, that a token is needed. A client __should not__ assume + that a server which served one endpoint openly will serve every endpoint openly, nor the reverse. + +Authorization - which of the protected data an authenticated client may see - is the server's +decision and is not constrained by this specification; a client with a valid token may still receive +a filtered view, or `403`/`404` for individual objects. + +### Protected resource metadata Servers that delegate identity to an external provider __may__ publish OAuth 2.0 protected resource metadata ([RFC 9728](https://www.rfc-editor.org/rfc/rfc9728)) and reference it from the `WWW-Authenticate` challenge, allowing a client to locate the authorization server automatically. -This is optional; it does not replace the token endpoint, which remains the interoperable baseline. +This is optional; it does not replace the token endpoint, which remains the interoperable baseline, +and it does not replace the challenge as the way a client discovers that authentication is required. ## References diff --git a/spec/openapi.yaml b/spec/openapi.yaml index ceb5238..d2187f9 100644 --- a/spec/openapi.yaml +++ b/spec/openapi.yaml @@ -553,10 +553,16 @@ paths: description: | Exchange credentials for a TEA access token. - A TEA server that requires authentication shall implement this endpoint, and - shall support the `client_credentials` grant type with HTTP Basic client - authentication (RFC 6749 section 2.3.1, RFC 7617): the API key identifier is - sent as the user-id and the API key secret as the password. + A TEA server that requires authentication on any of its endpoints shall + implement this endpoint, and shall support the `client_credentials` grant type + with HTTP Basic client authentication (RFC 6749 section 2.3.1, RFC 7617): the + API key identifier is sent as the user-id and the API key secret as the + password. A server that requires no authentication need not implement it. + + Clients do not probe this endpoint to discover whether authentication is + required: they issue the resource request, and a `401` response carrying a + `WWW-Authenticate: Bearer` challenge is the signal to obtain a token here. A + `404` from this endpoint means only that it is not implemented. Servers may support additional grant types for federated identity, for example SAML 2.0 assertions (RFC 7522), JWT assertions (RFC 7523), or token exchange