Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .redocly.lint-ignore.yaml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 5 additions & 5 deletions @theme/ext/use-configure-replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,24 @@ type ClientCredentials = { clientId: string; clientSecret: string };
let clientCredentialsPromise: Promise<ClientCredentials> | null = null;

async function registerClient(): Promise<ClientCredentials> {
// 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'],
Comment thread
redocly[bot] marked this conversation as resolved.
}),
});

if (!registerResponse.ok) {
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 };
}

Expand Down
35 changes: 24 additions & 11 deletions openapi/cafe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Comment thread
DmitryAnansky marked this conversation as resolved.
- **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
Expand All @@ -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}`).
7 changes: 7 additions & 0 deletions openapi/components/parameters/OAuth2ClientId.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: clientId
in: path
required: true
description: The client identifier issued at registration.
schema:
type: string
example: client_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d
7 changes: 7 additions & 0 deletions openapi/components/responses/OAuth2BadRequest.yaml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions openapi/components/responses/OAuth2ServerError.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
description: Unexpected server error, reported with the `server_error` error code.
content:
application/json:
schema:
$ref: ../schemas/OAuthError.yaml
14 changes: 14 additions & 0 deletions openapi/components/responses/OAuth2Unauthorized.yaml
Original file line number Diff line number Diff line change
@@ -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
61 changes: 61 additions & 0 deletions openapi/components/schemas/AuthorizationServerMetadata.yaml
Original file line number Diff line number Diff line change
@@ -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
81 changes: 46 additions & 35 deletions openapi/components/schemas/OAuth2Client.yaml
Original file line number Diff line number Diff line change
@@ -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:
Comment thread
DmitryAnansky marked this conversation as resolved.
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: High

OAuth2Client.yaml requires client_secret and registration_access_token. Returning these in GET/PUT responses for /oauth2/register/{clientId} violates RFC 7592 Section 2.1 and exposes sensitive credentials unnecessarily. Use a separate schema for management responses.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not valid

What RFC 7592 actually specifies:

Section 2.1 (Read) and Section 2.2 (Update) both say, verbatim: "Some values in the response, including the client_secret and registration_access_token, MAY be different from those in the initial registration response." The RFC doesn't merely permit these fields in GET/PUT responses — it builds a feature on top of them: the server may rotate the secret or registration token during a read/update, and the response is how the client learns the new values. Omitting them would break that mechanism.
Section 3 (Client Information Response) — the response format both operations reference — says the server "MUST return all registered metadata about this client", extends RFC 7591's client information response ("the response contains the client identifier as well as the client secret, if the client is a confidential client"), and its example response literally contains both client_secret and registration_access_token.

So our GET/PUT responses (and OAuth2Client.yaml requiring those fields) are not a violation of §2.1 — they're an implementation of it. The contract tests validate exactly this shape.

Where the reviewer likely went wrong: §2.2 does contain a MUST NOT, but it applies to the request: the update request must not include registration_access_token, registration_client_uri, client_id_issued_at, or client_secret_expires_at (and any included client_secret must match). We enforce that direction correctly. It's an easy sentence to misattribute to the response.

- redirect_uris
- grant_types
- scope
Comment thread
DmitryAnansky marked this conversation as resolved.
- token_endpoint_auth_method
Comment thread
redocly[bot] marked this conversation as resolved.
28 changes: 28 additions & 0 deletions openapi/components/schemas/OAuthError.yaml
Original file line number Diff line number Diff line change
@@ -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
Comment thread
DmitryAnansky marked this conversation as resolved.
error_description:
type: string
description: Human-readable explanation of the error.
required:
- error
Loading
Loading