diff --git a/auth/readme.md b/auth/readme.md index 9e46730..30b49ac 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,275 @@ 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, 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). -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: -## HTTP bearer token auth +* 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. -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. +Two consequences are worth stating explicitly, because they are what make the baseline useful: -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. +* 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. -For each TEA service, one bearer token is needed. The token itself (or the backend) should -specify authorization for the token. +## The token endpoint -## Mutual TLS +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. -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. +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. + +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 `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 +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. + +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. + +## 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, +and it does not replace the challenge as the way a client discovers that authentication is required. ## 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..d2187f9 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,49 @@ 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 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 + (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 +623,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 +1727,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 +2017,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