diff --git a/.redocly.lint-ignore.yaml b/.redocly.lint-ignore.yaml new file mode 100644 index 0000000..570447d --- /dev/null +++ b/.redocly.lint-ignore.yaml @@ -0,0 +1,6 @@ +# This file instructs Redocly's linter to ignore the rules contained for specific parts of your API. +# See https://redocly.com/docs/cli/ for more information. +openapi/paths/oauth2_register_{clientId}.yaml: + no-invalid-media-type-examples: + - >- + #/put/requestBody/content/application~1json/examples/UpdateClient/dataValue/client_id diff --git a/@theme/ext/use-configure-replay.ts b/@theme/ext/use-configure-replay.ts index aaee45a..d396e67 100644 --- a/@theme/ext/use-configure-replay.ts +++ b/@theme/ext/use-configure-replay.ts @@ -28,16 +28,16 @@ type ClientCredentials = { clientId: string; clientSecret: string }; let clientCredentialsPromise: Promise | null = null; async function registerClient(): Promise { + // Standard RFC 7591 dynamic client registration request and response fields. const registerResponse = await fetch(`${BASE_URL}/oauth2/register`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - name: CLIENT_NAME, - redirectUris: [`${BASE_URL}/callback`], - scopes: SCOPES, - grantTypes: ['client_credentials'], + client_name: CLIENT_NAME, + scope: SCOPES.join(' '), + grant_types: ['client_credentials'], }), }); @@ -45,7 +45,7 @@ async function registerClient(): Promise { throw new Error(`Client registration failed with status ${registerResponse.status}`); } - const { clientId, clientSecret } = await registerResponse.json(); + const { client_id: clientId, client_secret: clientSecret } = await registerResponse.json(); return { clientId, clientSecret }; } diff --git a/openapi/cafe.yaml b/openapi/cafe.yaml index a43275d..a723a29 100644 --- a/openapi/cafe.yaml +++ b/openapi/cafe.yaml @@ -41,6 +41,14 @@ paths: $ref: paths/revenue.yaml /oauth2/register: $ref: paths/oauth2_register.yaml + /oauth2/register/{clientId}: + $ref: paths/oauth2_register_{clientId}.yaml + /oauth2/token: + $ref: paths/oauth2_token.yaml + /oauth2/revoke: + $ref: paths/oauth2_revoke.yaml + /.well-known/oauth-authorization-server: + $ref: paths/well-known_oauth-authorization-server.yaml webhooks: order-notification: $ref: webhooks/order-notification.yaml @@ -50,20 +58,18 @@ components: type: oauth2 description: | OAuth2 authorization for API access. The token endpoint accepts `grant_type=authorization_code`, `grant_type=client_credentials`, and `grant_type=refresh_token`. + Standard OAuth2 client libraries can drive these flows unmodified; server capabilities are discoverable from the [RFC 8414 metadata endpoint](https://api.cafe.redocly.com/.well-known/oauth-authorization-server). - ### Differences from the OAuth2 specifications + ### Protocol behavior - A standard OAuth2 client library can drive these flows, with the following to account for. - - Two behaviors do not conform to the specifications: - - - **Errors use RFC 9457 problem+json, not RFC 6749 Section 5.2.** Failures return `application/problem+json` with `type`, `title`, `status`, and `instance`. There is no `error` or `error_description` field, so the standard codes (`invalid_grant`, `invalid_client`, `unsupported_grant_type`) never appear — branch on the HTTP status and `title` instead. A refresh token that is expired, already rotated, or unrecognized returns `400` with a `title` of `Refresh token has expired` or `Invalid refresh token`, where a conformant server would return `error: invalid_grant`. - - **`refresh_token` is not a registrable grant type.** RFC 7591 Section 2 lists it, but `/oauth2/register` accepts only `authorization_code` and `client_credentials` in `grantTypes`. Refreshing requires no registration: holding a refresh token issued to the client is the authorization. A consequence is that refresh capability cannot be disabled per client — every `authorization_code` grant returns a refresh token, so a client intended for a shared or public device cannot be registered without one. - - Two are choices the specifications leave to the server: - - - **Refresh tokens rotate on every use.** A successful refresh retires the token presented and returns a replacement in `refresh_token`, as RFC 6749 Section 6 permits and the OAuth2 Security Best Current Practice recommends. Store the new value; the old one stops working. Refresh tokens expire 30 days after they are issued, and rotation restarts that window. The authorization code flow returns a refresh token with every access token; the client credentials flow returns none (RFC 6749 Section 4.4.3). + - **Errors follow RFC 6749 Section 5.2 and RFC 7591 Section 3.2.2.** The OAuth2 endpoints return `{"error": ..., "error_description": ...}` with the standard codes (`invalid_grant`, `invalid_client`, `unsupported_grant_type`, `invalid_scope`, `invalid_client_metadata`, ...). The rest of the API uses RFC 9457 problem+json. + - **Client authentication.** Both `client_secret_basic` (HTTP Basic per RFC 6749 Section 2.3.1) and `client_secret_post` (credentials in the form body) are accepted at the token and revocation endpoints, but not both in one request. + - **PKCE (RFC 7636) is supported** for the authorization code flow with the `S256` and `plain` challenge methods (`S256` recommended). When an authorization request carries a `code_challenge`, the token exchange requires the matching `code_verifier`. + - **Refresh tokens rotate on every use.** A successful refresh retires the token presented and returns a replacement in `refresh_token`, as RFC 6749 Section 6 permits and RFC 9700 recommends. Store the new value; the old one stops working. Refresh tokens expire 30 days after they are issued, and rotation restarts that window. The authorization code flow returns a refresh token only when the client is registered for the `refresh_token` grant type; the client credentials flow returns none (RFC 6749 Section 4.4.3). + - **Authorization responses carry `iss`** (RFC 9207) alongside `code` and `state`. + - **Tokens can be revoked** at the [revocation endpoint](https://api.cafe.redocly.com/oauth2/revoke) (RFC 7009), and client registrations managed via RFC 7592 using the `registration_access_token`. - **`scope` accepts commas.** The space-delimited form required by RFC 6749 is always accepted and recommended; comma-separated values are additionally tolerated. + oauth2MetadataUrl: https://api.cafe.redocly.com/.well-known/oauth-authorization-server flows: authorizationCode: authorizationUrl: https://api.cafe.redocly.com/oauth2/authorize @@ -88,3 +94,10 @@ components: name: X-API-Key in: header description: API key for internal operations. + RegistrationAccessToken: + type: http + scheme: bearer + description: >- + Registration access token from the client registration response + (RFC 7592), sent as a Bearer token to authenticate requests to the + client configuration endpoint (`/oauth2/register/{clientId}`). diff --git a/openapi/components/parameters/OAuth2ClientId.yaml b/openapi/components/parameters/OAuth2ClientId.yaml new file mode 100644 index 0000000..866a5a3 --- /dev/null +++ b/openapi/components/parameters/OAuth2ClientId.yaml @@ -0,0 +1,7 @@ +name: clientId +in: path +required: true +description: The client identifier issued at registration. +schema: + type: string +example: client_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d diff --git a/openapi/components/responses/OAuth2BadRequest.yaml b/openapi/components/responses/OAuth2BadRequest.yaml new file mode 100644 index 0000000..cae087a --- /dev/null +++ b/openapi/components/responses/OAuth2BadRequest.yaml @@ -0,0 +1,7 @@ +description: | + OAuth2 error (RFC 6749 Section 5.2 / RFC 7591 Section 3.2.2). + The `error` field carries the standard OAuth2 error code. +content: + application/json: + schema: + $ref: ../schemas/OAuthError.yaml diff --git a/openapi/components/responses/OAuth2ServerError.yaml b/openapi/components/responses/OAuth2ServerError.yaml new file mode 100644 index 0000000..d4fd6cc --- /dev/null +++ b/openapi/components/responses/OAuth2ServerError.yaml @@ -0,0 +1,5 @@ +description: Unexpected server error, reported with the `server_error` error code. +content: + application/json: + schema: + $ref: ../schemas/OAuthError.yaml diff --git a/openapi/components/responses/OAuth2Unauthorized.yaml b/openapi/components/responses/OAuth2Unauthorized.yaml new file mode 100644 index 0000000..5804af5 --- /dev/null +++ b/openapi/components/responses/OAuth2Unauthorized.yaml @@ -0,0 +1,14 @@ +description: | + Client authentication failed (`error: invalid_client`, RFC 6749 Section 5.2) + or the presented bearer token is invalid (`error: invalid_token`, RFC 6750). + The response carries a `WWW-Authenticate` challenge naming the expected + authentication scheme. +headers: + WWW-Authenticate: + description: Authentication challenge, e.g. `Basic realm="redocly-cafe"` or `Bearer realm="redocly-cafe", error="invalid_token"`. + schema: + type: string +content: + application/json: + schema: + $ref: ../schemas/OAuthError.yaml diff --git a/openapi/components/schemas/AuthorizationServerMetadata.yaml b/openapi/components/schemas/AuthorizationServerMetadata.yaml new file mode 100644 index 0000000..c7aed6f --- /dev/null +++ b/openapi/components/schemas/AuthorizationServerMetadata.yaml @@ -0,0 +1,61 @@ +type: object +description: OAuth2 authorization server metadata per RFC 8414. +properties: + issuer: + type: string + format: uri + description: The authorization server's issuer identifier. + authorization_endpoint: + type: string + format: uri + token_endpoint: + type: string + format: uri + registration_endpoint: + type: string + format: uri + description: Dynamic client registration endpoint (RFC 7591). + revocation_endpoint: + type: string + format: uri + description: Token revocation endpoint (RFC 7009). + scopes_supported: + type: array + items: + type: string + response_types_supported: + type: array + items: + type: string + response_modes_supported: + type: array + items: + type: string + grant_types_supported: + type: array + items: + type: string + token_endpoint_auth_methods_supported: + type: array + items: + type: string + revocation_endpoint_auth_methods_supported: + type: array + items: + type: string + code_challenge_methods_supported: + type: array + description: PKCE code challenge methods supported (RFC 7636). + items: + type: string + authorization_response_iss_parameter_supported: + type: boolean + description: Whether authorization responses carry the `iss` parameter (RFC 9207). + service_documentation: + type: string + format: uri +required: + - issuer + - authorization_endpoint + - token_endpoint + - response_types_supported diff --git a/openapi/components/schemas/OAuth2Client.yaml b/openapi/components/schemas/OAuth2Client.yaml index dd98342..c8afc65 100644 --- a/openapi/components/schemas/OAuth2Client.yaml +++ b/openapi/components/schemas/OAuth2Client.yaml @@ -1,59 +1,70 @@ type: object -description: OAuth2 client registration response. Per RFC 7591, includes the client identifier, secret, timestamps, and all registered client metadata. +description: | + OAuth2 client information response per RFC 7591 Section 3.2.1, using the + standard snake_case field names. Returned by the registration endpoint and + the RFC 7592 client configuration endpoint. properties: - clientId: + client_id: type: string description: Client identifier issued by the authorization server. - clientSecret: + client_secret: type: string - description: Client secret issued by the authorization server. - clientIdIssuedAt: + description: Client secret issued by the authorization server. Store it securely. + client_id_issued_at: type: integer format: int64 - description: Time when the client_id is issued, represented as seconds since epoch (RFC7591). - clientSecretExpiresAt: + description: Time when the client_id is issued, represented as seconds since epoch (RFC 7591). + client_secret_expires_at: type: integer format: int64 description: Time at which the client_secret expires, represented as seconds since epoch. 0 indicates the secret does not expire (RFC 7591). - name: - type: string - description: Client name (registered metadata). - redirectUris: - type: array - items: - type: string - format: uri - description: List of redirect URIs (registered metadata). - registrationClientUri: + registration_client_uri: type: string format: uri description: URL of the client configuration endpoint for managing this client registration (RFC 7592). - registrationAccessToken: + registration_access_token: type: string - description: Access token to be used at the client configuration endpoint for managing this client registration (RFC 7592). - scopes: + description: Bearer token for the client configuration endpoint (RFC 7592). Store it securely. + client_name: + type: string + description: Client name (registered metadata). Omitted for clients registered without a name. + redirect_uris: type: array items: type: string - enum: - - menu:read - - menu:write - - orders:read - - orders:write - - revenue:read - description: List of scopes (registered metadata). - grantTypes: + format: uri + description: Registered redirect URIs. + grant_types: type: array items: type: string enum: - authorization_code - client_credentials - description: List of grant types (registered metadata). + - refresh_token + description: Registered grant types. + scope: + type: string + description: Space-separated registered scopes. + token_endpoint_auth_method: + type: string + enum: + - client_secret_basic + - client_secret_post + description: | + Registered token endpoint authentication method. Defaults to `client_secret_basic` + when not requested at registration (RFC 7591 Section 2). Informational: the token + and revocation endpoints accept both methods for every client regardless. +# client_name is the only conditional field: it is omitted for clients +# registered without a name. Everything else is always returned. required: - - clientId - - clientSecret - - clientIdIssuedAt - - clientSecretExpiresAt - - registrationClientUri - - registrationAccessToken + - client_id + - client_secret + - client_id_issued_at + - client_secret_expires_at + - registration_client_uri + - registration_access_token + - redirect_uris + - grant_types + - scope + - token_endpoint_auth_method diff --git a/openapi/components/schemas/OAuthError.yaml b/openapi/components/schemas/OAuthError.yaml new file mode 100644 index 0000000..6113c5f --- /dev/null +++ b/openapi/components/schemas/OAuthError.yaml @@ -0,0 +1,28 @@ +type: object +description: | + OAuth2 error response, as defined by RFC 6749 Section 5.2 (token endpoint), + RFC 7591 Section 3.2.2 (registration endpoint), and RFC 7009 (revocation endpoint). + The OAuth2 endpoints return this shape instead of the `application/problem+json` + format used by the rest of the API. +properties: + error: + type: string + description: Machine-readable error code. + enum: + - invalid_request + - invalid_client + - invalid_grant + - unauthorized_client + - unsupported_grant_type + - unsupported_response_type + - invalid_scope + - invalid_client_metadata + - invalid_redirect_uri + - invalid_token + - access_denied + - server_error + error_description: + type: string + description: Human-readable explanation of the error. +required: + - error diff --git a/openapi/components/schemas/RegisterClientObject.yaml b/openapi/components/schemas/RegisterClientObject.yaml index d95876d..93c6b1a 100644 --- a/openapi/components/schemas/RegisterClientObject.yaml +++ b/openapi/components/schemas/RegisterClientObject.yaml @@ -1,15 +1,68 @@ type: object +description: | + Client registration metadata per RFC 7591 Section 2. + Standard snake_case metadata names (`client_name`, `redirect_uris`, `grant_types`, `scope`, + `token_endpoint_auth_method`) and this API's camelCase aliases (`name`, `redirectUris`, + `grantTypes`, `scopes`, `tokenEndpointAuthMethod`) are both accepted. + When both spellings of a field are present, the standard snake_case one takes precedence. + All fields are optional, with one conditional exception enforced by the schema below: + `redirect_uris` is required whenever the effective grant types include `authorization_code` — + including when `grant_types` is omitted, since the default includes it (RFC 7591 Section 2). properties: + client_name: + type: string + description: Human-readable client name, shown on the authorization consent screen. + redirect_uris: + type: array + items: + type: string + format: uri + description: | + Redirect URIs for the `authorization_code` grant. Compared with exact string + matching at authorization time (RFC 6749 Section 3.1.2.3). Required when the + effective grant types include `authorization_code`. + scope: + type: string + description: Space-separated list of scopes. Defaults to all available scopes. + default: menu:read menu:write orders:read orders:write revenue:read + pattern: '^(menu:read|menu:write|orders:read|orders:write|revenue:read)( (menu:read|menu:write|orders:read|orders:write|revenue:read))*$' + example: menu:read orders:read orders:write + grant_types: + type: array + items: + type: string + enum: + - authorization_code + - client_credentials + - refresh_token + default: + - authorization_code + - client_credentials + - refresh_token + description: | + Grant types the client may use. A refresh token is issued with `authorization_code` + grant responses only when `refresh_token` is included here; the `client_credentials` + grant never returns one (RFC 6749 Section 4.4.3). + token_endpoint_auth_method: + type: string + enum: + - client_secret_basic + - client_secret_post + default: client_secret_basic + description: | + Requested client authentication method for the token endpoint, registered as + client metadata and echoed in registration responses. Defaults to + `client_secret_basic` (RFC 7591 Section 2). Informational: the token and + revocation endpoints accept both methods for every client regardless. name: type: string - description: Client name. + description: Alias for `client_name`; used when `client_name` is absent. redirectUris: type: array items: type: string format: uri - default: [] - description: List of redirect URIs (optional, defaults to empty array). + description: Alias for `redirect_uris`; used when `redirect_uris` is absent. scopes: type: array items: @@ -20,13 +73,7 @@ properties: - orders:read - orders:write - revenue:read - default: - - menu:read - - menu:write - - orders:read - - orders:write - - revenue:read - description: List of scopes. + description: Alias for `scope`, as an array instead of a space-separated string; used when `scope` is absent. grantTypes: type: array items: @@ -34,9 +81,66 @@ properties: enum: - authorization_code - client_credentials - default: - - authorization_code - - client_credentials - description: List of grant types. `refresh_token` is not registrable; any client holding a refresh token may present it at the token endpoint. -required: - - name + - refresh_token + description: Alias for `grant_types`; used when `grant_types` is absent. + tokenEndpointAuthMethod: + type: string + enum: + - client_secret_basic + - client_secret_post + description: Alias for `token_endpoint_auth_method`; used when `token_endpoint_auth_method` is absent. +# RFC 7591 Section 2: redirect-based grants require redirect URIs. The condition +# mirrors the server's alias handling — the standard grant_types wins over the +# grantTypes alias when both are present, and omitting both applies the default, +# which includes authorization_code. +if: + anyOf: + - required: + - grant_types + properties: + grant_types: + contains: + const: authorization_code + - allOf: + - not: + required: + - grant_types + properties: + grant_types: {} + - required: + - grantTypes + properties: + grantTypes: + contains: + const: authorization_code + - not: + anyOf: + - required: + - grant_types + properties: + grant_types: {} + - required: + - grantTypes + properties: + grantTypes: {} +# The effective redirect list follows the same precedence: redirect_uris when +# present, otherwise the redirectUris alias — and the effective one must be +# non-empty. +then: + anyOf: + - required: + - redirect_uris + properties: + redirect_uris: + minItems: 1 + - allOf: + - not: + required: + - redirect_uris + properties: + redirect_uris: {} + - required: + - redirectUris + properties: + redirectUris: + minItems: 1 diff --git a/openapi/components/schemas/TokenResponse.yaml b/openapi/components/schemas/TokenResponse.yaml new file mode 100644 index 0000000..feac046 --- /dev/null +++ b/openapi/components/schemas/TokenResponse.yaml @@ -0,0 +1,29 @@ +type: object +description: Access token response per RFC 6749 Section 5.1. +properties: + access_token: + type: string + description: The issued access token (opaque string). + token_type: + type: string + description: Type of the issued token. + const: Bearer + expires_in: + type: integer + description: Access token lifetime in seconds. + example: 3600 + refresh_token: + type: string + description: | + Refresh token, returned for the `authorization_code` grant when the client + is registered for the `refresh_token` grant type, and on every refresh + (tokens rotate on use). Never returned for the `client_credentials` grant + (RFC 6749 Section 4.4.3). + scope: + type: string + description: Space-separated scopes granted to the token. + example: menu:read orders:read +required: + - access_token + - token_type + - expires_in diff --git a/openapi/components/schemas/UpdateClientObject.yaml b/openapi/components/schemas/UpdateClientObject.yaml new file mode 100644 index 0000000..7c7325f --- /dev/null +++ b/openapi/components/schemas/UpdateClientObject.yaml @@ -0,0 +1,26 @@ +description: | + Client registration update request per RFC 7592 Section 2.2: the full set of + registration metadata, plus the `client_id` of the client being updated, + which is required (either as `client_id` or its `clientId` alias). + Omitted metadata fields are reset to their registration defaults. +allOf: + - $ref: ./RegisterClientObject.yaml + - type: object + properties: + client_id: + type: string + description: Required (RFC 7592 Section 2.2); must match the client being updated. Credentials cannot be changed through this endpoint. + client_secret: + type: string + description: Optional; if present, must match the client's current secret. + clientId: + type: string + description: Alias for `client_id`; used when `client_id` is absent. + clientSecret: + type: string + description: Alias for `client_secret`; used when `client_secret` is absent. +anyOf: + - required: + - client_id + - required: + - clientId diff --git a/openapi/paths/oauth2_register.yaml b/openapi/paths/oauth2_register.yaml index 1e00427..4568359 100644 --- a/openapi/paths/oauth2_register.yaml +++ b/openapi/paths/oauth2_register.yaml @@ -3,26 +3,40 @@ post: - Authorization summary: Create OAuth2 client description: | - Register a new OAuth2 client for dynamic client registration. - This endpoint implements the Dynamic Client Registration Protocol (RFC 7591), using camelCase field names instead of the RFC's snake_case convention (e.g., `redirectUris` instead of `redirect_uris`, `grantTypes` instead of `grant_types`). - The `name` field is required. Other fields are optional. If not provided: + Register a new OAuth2 client, implementing the Dynamic Client Registration Protocol (RFC 7591). + The standard snake_case metadata names (`client_name`, `redirect_uris`, `grant_types`, `scope`, `token_endpoint_auth_method`) + are accepted, as are this API's camelCase aliases (`name`, `redirectUris`, `grantTypes`, `scopes`, `tokenEndpointAuthMethod`), + so off-the-shelf DCR clients work without modification. + Responses use only the standard snake_case names (RFC 7591 Section 3.2.1). - - `redirectUris` defaults to an empty array. Note: When using the `authorization_code` grant type, `redirectUris` must be provided (per RFC 7591 Section 2). - - `scopes` defaults to all available scopes (menu:read, menu:write, orders:read, orders:write, revenue:read) - - `grantTypes` defaults to `authorization_code` and `client_credentials` + All fields are optional. If not provided: - These defaults interact: a request that supplies only `name` pairs `authorization_code` with an empty `redirectUris`, which is not a usable combination. - Supply `redirectUris` explicitly to register the `authorization_code` grant, or set `grantTypes` to `client_credentials` alone for a client that needs no redirect URI. + - `scope` defaults to all available scopes (`menu:read menu:write orders:read orders:write revenue:read`) + - `grant_types` defaults to `authorization_code`, `client_credentials`, and `refresh_token` + - `token_endpoint_auth_method` defaults to `client_secret_basic` (RFC 7591 Section 2); both supported methods are accepted at the token endpoint regardless of the registered value - Refresh tokens require no registration and `refresh_token` is not a value you can register in `grantTypes`. - The token endpoint returns a refresh token alongside every access token it issues for the `authorization_code` grant, and accepts `grant_type=refresh_token` from any client presenting a refresh token issued to it. - The `client_credentials` grant returns no refresh token (RFC 6749 Section 4.4.3); those clients request a new access token with their own credentials instead. + This server registers confidential clients only: every client is issued a `client_secret`, + and `token_endpoint_auth_method: none` (public clients such as SPAs or mobile apps, + RFC 7591 Section 2) is not supported by design. - Returns the registered client information per RFC 7591, including: + `redirect_uris` is required whenever the effective grant types include `authorization_code` + (RFC 7591 Section 2) — including when `grant_types` is omitted, since the default includes it. + Register with `grant_types: ["client_credentials"]` for a client that needs no redirect URI. - - `clientId` and `clientSecret` (must be stored securely) - - `clientIdIssuedAt` and `clientSecretExpiresAt` timestamps - - All registered client metadata (name, redirectUris, scopes, grantTypes) + Refresh token behavior follows the registered grant types: the token endpoint returns a refresh + token with `authorization_code` grant responses only when the client registered the + `refresh_token` grant type. The `client_credentials` grant never returns one (RFC 6749 Section 4.4.3); + those clients request a new access token with their own credentials instead. + + Returns the registered client information per RFC 7591 Section 3.2.1, including: + + - `client_id` and `client_secret` (must be stored securely) + - `client_id_issued_at` and `client_secret_expires_at` timestamps + - `registration_client_uri` and `registration_access_token` for managing the registration (RFC 7592) + - All registered client metadata + + Errors use the RFC 7591 Section 3.2.2 format: `400` with `error` set to + `invalid_redirect_uri` or `invalid_client_metadata` and a human-readable `error_description`. operationId: registerOAuth2Client security: [] requestBody: @@ -33,6 +47,7 @@ post: $ref: ../components/schemas/RegisterClientObject.yaml examples: RegisterClientObject: + summary: Client credentials only (camelCase aliases) dataValue: name: auth scopes: @@ -44,16 +59,15 @@ post: grantTypes: - client_credentials RegisterClientForAuthorizationCode: + summary: Authorization code with refresh tokens (RFC 7591 names) dataValue: - name: pos-terminal - redirectUris: + client_name: pos-terminal + redirect_uris: - https://api.cafe.redocly.com/callback - scopes: - - menu:read - - orders:read - - orders:write - grantTypes: + scope: menu:read orders:read orders:write + grant_types: - authorization_code + - refresh_token responses: '201': description: OAuth2 client registered successfully. @@ -62,6 +76,6 @@ post: schema: $ref: ../components/schemas/OAuth2Client.yaml '400': - $ref: ../components/responses/BadRequest.yaml + $ref: ../components/responses/OAuth2BadRequest.yaml '500': - $ref: ../components/responses/InternalServerError.yaml + $ref: ../components/responses/OAuth2ServerError.yaml diff --git a/openapi/paths/oauth2_register_{clientId}.yaml b/openapi/paths/oauth2_register_{clientId}.yaml new file mode 100644 index 0000000..38239da --- /dev/null +++ b/openapi/paths/oauth2_register_{clientId}.yaml @@ -0,0 +1,89 @@ +parameters: + - $ref: ../components/parameters/OAuth2ClientId.yaml +get: + tags: + - Authorization + summary: Retrieve OAuth2 client + description: | + Read the current registration of an OAuth2 client (RFC 7592 Section 2.1). + Authenticate with the `registration_access_token` returned at registration, + as a Bearer token in the `Authorization` header. An invalid or missing token + returns `401` without revealing whether the client exists. + operationId: getOAuth2Client + security: + - RegistrationAccessToken: [] + responses: + '200': + description: Current client registration. + content: + application/json: + schema: + $ref: ../components/schemas/OAuth2Client.yaml + '401': + $ref: ../components/responses/OAuth2Unauthorized.yaml + '500': + $ref: ../components/responses/OAuth2ServerError.yaml +put: + tags: + - Authorization + summary: Update OAuth2 client + description: | + Replace the registration metadata of an OAuth2 client (RFC 7592 Section 2.2). + Authenticate with the `registration_access_token` as a Bearer token. + + This is a full replacement, not a merge: omitted metadata fields are reset to + their registration defaults. The request must include `client_id` (or the + `clientId` alias) matching the client being updated (RFC 7592 Section 2.2); + if `client_secret` is included it must match the current secret. + Credentials cannot be rotated through this endpoint. + operationId: updateOAuth2Client + security: + - RegistrationAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../components/schemas/UpdateClientObject.yaml + examples: + UpdateClient: + dataValue: + client_id: client_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d + client_name: pos-terminal-v2 + redirect_uris: + - https://api.cafe.redocly.com/callback + scope: menu:read orders:read + grant_types: + - authorization_code + - refresh_token + responses: + '200': + description: Updated client registration. + content: + application/json: + schema: + $ref: ../components/schemas/OAuth2Client.yaml + '400': + $ref: ../components/responses/OAuth2BadRequest.yaml + '401': + $ref: ../components/responses/OAuth2Unauthorized.yaml + '500': + $ref: ../components/responses/OAuth2ServerError.yaml +delete: + tags: + - Authorization + summary: Delete OAuth2 client + description: | + Deprovision an OAuth2 client (RFC 7592 Section 2.3). Authenticate with the + `registration_access_token` as a Bearer token. Deleting a client also + invalidates all of its tokens and pending authorization codes. + operationId: deleteOAuth2Client + security: + - RegistrationAccessToken: [] + responses: + '204': + description: Client registration deleted. + '401': + $ref: ../components/responses/OAuth2Unauthorized.yaml + '500': + $ref: ../components/responses/OAuth2ServerError.yaml diff --git a/openapi/paths/oauth2_revoke.yaml b/openapi/paths/oauth2_revoke.yaml new file mode 100644 index 0000000..211cd20 --- /dev/null +++ b/openapi/paths/oauth2_revoke.yaml @@ -0,0 +1,58 @@ +post: + tags: + - Authorization + summary: Revoke token + description: | + OAuth2 token revocation endpoint (RFC 7009). Revokes an access or refresh + token issued to the authenticated client. Access and refresh tokens issued + together are revoked together. + + Authenticate with HTTP Basic or with `client_id` and `client_secret` in the + request body, the same as the token endpoint. + + `token_type_hint` is an optional lookup optimization; both token types are + searched regardless. Per RFC 7009 Section 2.2 the endpoint returns `200` + even when the token is unknown or already revoked, so it cannot be used to + probe for valid tokens. + operationId: revokeToken + security: [] + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + token: + type: string + description: The access or refresh token to revoke. + token_type_hint: + type: string + enum: + - access_token + - refresh_token + description: Optional hint about the token type. + client_id: + type: string + description: Client identifier. Omit when authenticating with HTTP Basic. + client_secret: + type: string + description: Client secret. Omit when authenticating with HTTP Basic. + required: + - token + examples: + RevokeAccessToken: + dataValue: + token: YOUR_ACCESS_TOKEN + token_type_hint: access_token + client_id: client_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d + client_secret: YOUR_CLIENT_SECRET + responses: + '200': + description: The token has been revoked or was already invalid. + '400': + $ref: ../components/responses/OAuth2BadRequest.yaml + '401': + $ref: ../components/responses/OAuth2Unauthorized.yaml + '500': + $ref: ../components/responses/OAuth2ServerError.yaml diff --git a/openapi/paths/oauth2_token.yaml b/openapi/paths/oauth2_token.yaml new file mode 100644 index 0000000..060fd87 --- /dev/null +++ b/openapi/paths/oauth2_token.yaml @@ -0,0 +1,110 @@ +post: + tags: + - Authorization + summary: Create access token + description: | + OAuth2 token endpoint (RFC 6749 Section 3.2). Supports the + `authorization_code`, `client_credentials`, and `refresh_token` grant types. + + **Client authentication.** Authenticate with HTTP Basic + (`Authorization: Basic base64(client_id:client_secret)`) or with `client_id` + and `client_secret` in the request body — not both at once (RFC 6749 Section 2.3.1). + + **PKCE (RFC 7636).** When the authorization request included a + `code_challenge`, the matching `code_verifier` is required here; a mismatch + or a `code_verifier` for a code issued without a challenge is rejected with + `invalid_grant`. `S256` and `plain` challenge methods are supported. + + **Refresh tokens.** Rotate on every use: a successful `refresh_token` grant + retires the presented token and returns a replacement. Refresh tokens expire + 30 days after issuance; rotation restarts that window. The + `authorization_code` grant returns a refresh token only when the client is + registered for the `refresh_token` grant type; the `client_credentials` + grant never returns one (RFC 6749 Section 4.4.3). + + **Scopes.** `scope` is space-separated per RFC 6749 (comma-separated values + are additionally tolerated) and must be a subset of the scopes previously + granted (authorization code or refresh token) or registered (client credentials). + + Errors follow RFC 6749 Section 5.2: `400` (or `401` for `invalid_client`) + with the standard `error` codes such as `invalid_grant`, `invalid_client`, + `unauthorized_client`, `unsupported_grant_type`, and `invalid_scope`. + operationId: createAccessToken + security: [] + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + grant_type: + type: string + enum: + - authorization_code + - client_credentials + - refresh_token + description: The OAuth2 grant type. + client_id: + type: string + description: Client identifier. Omit when authenticating with HTTP Basic. + client_secret: + type: string + description: Client secret. Omit when authenticating with HTTP Basic. + code: + type: string + description: The authorization code (required for `authorization_code`). + redirect_uri: + type: string + format: uri + description: Must match the redirect URI used in the authorization request (required for `authorization_code`). + code_verifier: + type: string + minLength: 43 + maxLength: 128 + description: PKCE code verifier (RFC 7636); required when the authorization request included a `code_challenge`. + refresh_token: + type: string + description: The refresh token (required for `refresh_token`). + scope: + type: string + description: Space-separated scopes to request; defaults to the previously granted or registered scopes. + required: + - grant_type + examples: + ClientCredentials: + summary: Client credentials grant + dataValue: + grant_type: client_credentials + client_id: client_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d + client_secret: YOUR_CLIENT_SECRET + scope: menu:read orders:read + AuthorizationCode: + summary: Authorization code exchange with PKCE + dataValue: + grant_type: authorization_code + client_id: client_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d + client_secret: YOUR_CLIENT_SECRET + code: AUTHORIZATION_CODE + redirect_uri: https://api.cafe.redocly.com/callback + code_verifier: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk + RefreshToken: + summary: Refresh token grant + dataValue: + grant_type: refresh_token + client_id: client_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d + client_secret: YOUR_CLIENT_SECRET + refresh_token: YOUR_REFRESH_TOKEN + responses: + '200': + description: Token issued successfully. The response is not cacheable (it is served with `Cache-Control` set to `no-store`). + content: + application/json: + schema: + $ref: ../components/schemas/TokenResponse.yaml + '400': + $ref: ../components/responses/OAuth2BadRequest.yaml + '401': + $ref: ../components/responses/OAuth2Unauthorized.yaml + '500': + $ref: ../components/responses/OAuth2ServerError.yaml diff --git a/openapi/paths/well-known_oauth-authorization-server.yaml b/openapi/paths/well-known_oauth-authorization-server.yaml new file mode 100644 index 0000000..d9d8e02 --- /dev/null +++ b/openapi/paths/well-known_oauth-authorization-server.yaml @@ -0,0 +1,54 @@ +get: + tags: + - Authorization + summary: Retrieve authorization server metadata + description: | + OAuth2 authorization server metadata (RFC 8414). Lists the authorization, + token, registration, and revocation endpoints along with the supported + scopes, grant types, client authentication methods, and PKCE code challenge + methods, so clients can discover the server's capabilities instead of + hard-coding them. + operationId: getAuthorizationServerMetadata + security: [] + responses: + '200': + description: Authorization server metadata. + content: + application/json: + schema: + $ref: ../components/schemas/AuthorizationServerMetadata.yaml + examples: + Metadata: + dataValue: + issuer: https://api.cafe.redocly.com + authorization_endpoint: https://api.cafe.redocly.com/oauth2/authorize + token_endpoint: https://api.cafe.redocly.com/oauth2/token + registration_endpoint: https://api.cafe.redocly.com/oauth2/register + revocation_endpoint: https://api.cafe.redocly.com/oauth2/revoke + scopes_supported: + - menu:read + - menu:write + - orders:read + - orders:write + - revenue:read + response_types_supported: + - code + response_modes_supported: + - query + grant_types_supported: + - authorization_code + - client_credentials + - refresh_token + token_endpoint_auth_methods_supported: + - client_secret_basic + - client_secret_post + revocation_endpoint_auth_methods_supported: + - client_secret_basic + - client_secret_post + code_challenge_methods_supported: + - S256 + - plain + authorization_response_iss_parameter_supported: true + service_documentation: https://cafe.redocly.com/openapi/cafe + '500': + $ref: ../components/responses/OAuth2ServerError.yaml diff --git a/redocly.yaml b/redocly.yaml index da0e796..c7373f6 100644 --- a/redocly.yaml +++ b/redocly.yaml @@ -14,7 +14,7 @@ apis: assertions: defined: true assertions: - pattern: /^(Create|Get|Retrieve|Replace|Update|Delete|Login|Send|Callback|Cancel|List|Partially update).*[^\.]$/ + pattern: /^(Create|Get|Retrieve|Replace|Update|Delete|Login|Send|Callback|Cancel|List|Partially update|Revoke).*[^\.]$/ logo: srcSet: "./images/cafe-logo-black.svg light, ./images/cafe-logo-white.svg dark"