diff --git a/auth/readme.md b/auth/readme.md index 30b49ac..a221cd8 100644 --- a/auth/readme.md +++ b/auth/readme.md @@ -28,11 +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, 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). +requires no authentication 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 some or all of its objects +implements the baseline in full; see +[Servers that protect some objects](#servers-that-protect-some-objects). Where a service does authenticate, interoperability requires that every TEA client can authenticate against every TEA server without server-specific code. This specification therefore @@ -203,7 +203,10 @@ 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: +probing the token endpoint. In TEA, resource operations may be called with a Bearer token or +without one; a `401` with a `WWW-Authenticate: Bearer` challenge on a given endpoint is the +runtime signal that a token is required there. 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 @@ -212,7 +215,10 @@ probing the token endpoint. The complete flow for a client that does not know in 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. + Basic - and receives an access token. Unauthenticated `client_credentials` requests shall + not be accepted; an empty OpenAPI security requirement on `/token` allows alternate client + authentication (for example, mutual TLS, JWT client assertion (RFC 7523), or credentials in + the request body), not anonymous token issuance. 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 @@ -233,35 +239,39 @@ pattern and is preferred over any configuration or discovery step a client would ### Servers without authentication -A server that requires no authentication on any endpoint: +A server that requires no authentication on any endpoint (and therefore applies no authorization): * __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. + authenticate, so a token endpoint on such a 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. + meaning on such a server, and ignoring it keeps such clients working. -### Mixed servers +### Servers that protect some objects -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: +A TEA server may make some data available without authentication while requiring authentication +and authorization for other objects - for example, listing products and releases without a token +while restricting artifact downloads to customers, as described under +[Requirements](#requirements). Such a server requires authentication for the protected objects: -* it __shall__ implement the token endpoint and the baseline exchange, because at least one endpoint +* it __shall__ implement the token endpoint and the baseline exchange, because at least one object 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. +* endpoints that do not require authentication answer without a token and ignore a token that is + presented; +* protected endpoints __shall__ 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. They shall not + answer a concealing `404` solely because the client is unauthenticated. A client __should not__ + assume that a server which served one object without authentication will serve every object + without authentication, 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. +a filtered view, `403 Forbidden`, or a concealing `404 Not Found` for individual objects it is +not authorized to see. Clients shall not infer from `404` alone whether the object is absent or +withheld. ### Protected resource metadata diff --git a/spec/openapi.yaml b/spec/openapi.yaml index 758804f..27d5417 100644 --- a/spec/openapi.yaml +++ b/spec/openapi.yaml @@ -37,6 +37,10 @@ paths: $ref: "#/components/schemas/product" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -61,6 +65,10 @@ paths: $ref: "#/components/responses/paginated-product-release" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -85,6 +93,10 @@ paths: "$ref": "#/components/schemas/productRelease" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -109,6 +121,10 @@ paths: "$ref": "#/components/schemas/cle" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -131,6 +147,10 @@ paths: $ref: "#/components/responses/paginated-product-release" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" tags: - TEA Product Release /product/{uuid}/cle: @@ -153,6 +173,10 @@ paths: "$ref": "#/components/schemas/cle" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -176,6 +200,10 @@ paths: $ref: "#/components/responses/paginated-product" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" tags: - TEA Product /component/{uuid}: @@ -198,6 +226,10 @@ paths: "$ref": "#/components/schemas/component" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -222,6 +254,10 @@ paths: $ref: "#/components/responses/paginated-component-release" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -246,6 +282,10 @@ paths: "$ref": "#/components/schemas/cle" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -268,6 +308,10 @@ paths: $ref: "#/components/responses/paginated-component" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" tags: - TEA Component /componentReleases: @@ -288,6 +332,10 @@ paths: $ref: "#/components/responses/paginated-component-release" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" tags: - TEA Component Release /componentRelease/{uuid}: @@ -310,6 +358,10 @@ paths: "$ref": "#/components/schemas/component-release-with-collection" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -334,6 +386,10 @@ paths: "$ref": "#/components/schemas/cle" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -358,6 +414,10 @@ paths: "$ref": "#/components/schemas/collection" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -382,6 +442,10 @@ paths: "$ref": "#/components/schemas/collection" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -406,6 +470,10 @@ paths: $ref: "#/components/responses/paginated-collection" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -430,6 +498,10 @@ paths: $ref: "#/components/responses/paginated-collection" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -460,6 +532,10 @@ paths: "$ref": "#/components/schemas/collection" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -490,6 +566,10 @@ paths: "$ref": "#/components/schemas/collection" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -514,6 +594,10 @@ paths: "$ref": "#/components/schemas/artifact" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -544,6 +628,10 @@ paths: "$ref": "#/components/schemas/artifact" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -576,6 +664,10 @@ paths: $ref: "#/components/responses/artifact-content-redirect" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" '406': @@ -621,6 +713,10 @@ paths: $ref: "#/components/responses/artifact-content-redirect" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" '406': @@ -652,6 +748,10 @@ paths: $ref: "#/components/responses/artifact-content-redirect" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" '406': @@ -703,6 +803,10 @@ paths: $ref: "#/components/responses/artifact-content-redirect" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" '406': @@ -733,6 +837,14 @@ paths: The access token is opaque to the client: clients shall not inspect, parse, or depend on its contents. + + The empty security requirement on this operation does not authorize anonymous + `client_credentials` issuance. It only allows the alternate client-authentication + methods described above (for example, mutual TLS, JWT client assertion + (RFC 7523), or credentials in the request body). For the baseline + `client_credentials` grant with HTTP Basic, unauthenticated requests shall not be + accepted: servers shall respond with `401`, `error: invalid_client`, and a + `WWW-Authenticate: Basic` challenge (see `401-token-error`; RFC 6749 section 5.2). operationId: requestToken security: - basicAuth: [] @@ -785,6 +897,10 @@ paths: $ref: "#/components/responses/discovery-response" '400': $ref: "#/components/responses/400-invalid-request" + '401': + $ref: "#/components/responses/401-unauthorized" + '403': + $ref: "#/components/responses/403-forbidden" '404': $ref: "#/components/responses/404-object-by-id-not-found" tags: @@ -2023,6 +2139,14 @@ components: 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). + + Servers that require no authentication on any endpoint shall not return this + status. On a TEA server where some data is available without authentication, but + not all, protected endpoints return `401` when no valid token is presented; + endpoints that do not require authentication answer without requiring a Bearer + token. A protected object shall not answer `404` solely because the client is + unauthenticated: absence of a valid token yields `401`, so the client flow can + discover that authentication is required. headers: WWW-Authenticate: description: Bearer challenge, as defined in RFC 6750 section 3. @@ -2031,6 +2155,18 @@ components: example: Bearer realm="tea", error="invalid_token", error_description="The access token expired" content: application/json: {} + 403-forbidden: + description: | + The client is authenticated, but is not authorized to access this resource or + perform this operation. Authorization decisions are server-specific and are not + constrained by this specification. + + Servers may instead conceal the existence of a resource from an authenticated + but unauthorized client by answering `404` (see `404-object-by-id-not-found`). + Clients shall treat `403` and that concealing `404` as non-access; neither + implies that retrying with the same token will succeed. + content: + application/json: {} artifact-content: description: | The content of the requested TEA Artifact format. @@ -2163,8 +2299,11 @@ components: $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). + Client authentication failed (`error: invalid_client`). Returned instead of 400 + when the client attempted to authenticate using the `Authorization` header + (RFC 6749 section 5.2). For the baseline Basic exchange this includes a missing + or invalid `Authorization: Basic` credential. Servers shall include + `WWW-Authenticate: Basic`. headers: WWW-Authenticate: description: Challenge indicating the client authentication methods supported by the token endpoint. @@ -2175,8 +2314,17 @@ components: application/json: schema: $ref: "#/components/schemas/token-error-response" + example: + error: invalid_client 404-object-by-id-not-found: - description: Object requested by identifier not found + description: | + Object requested by identifier not found, or the server intentionally conceals + the object's existence from an authenticated but unauthorized client. Clients + shall not infer from `404` alone whether the object is absent or withheld. + + Concealment applies only after authentication. A request to a protected object + with no valid access token shall receive `401` (see `401-unauthorized`), not a + concealing `404`. content: application/json: schema: @@ -2458,6 +2606,17 @@ components: 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`. + + Resource operations declare both this requirement and an empty alternative so + that a server can select which objects require authentication: a client may call + without a token. Whether a given endpoint actually requires a token is discovered at + runtime from a `401` Bearer challenge (see `401-unauthorized`), not from this + document alone. A server that requires no authentication shall ignore a presented + Bearer token. A server that protects some objects but not all may leave some + endpoints without authentication and protect others. Authorization after + authentication (filtered views, `403`, or a concealing `404` for an authenticated + but unauthorized client) remains server-specific. A protected object with no + valid token shall answer `401`, not a concealing `404`. basicAuth: type: http scheme: basic @@ -2467,7 +2626,10 @@ components: 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: + # Bearer token, or no token (server or endpoint that does not require authentication). + # Protected endpoints still answer 401 with a Bearer challenge when a valid token is required. - bearerAuth: [] + - {} tags: - name: TEA Authentication - name: TEA Product