From f972605b0db634e98539232d00fae065c518ed85 Mon Sep 17 00:00:00 2001 From: Kevin Cador Date: Wed, 29 Jul 2026 11:46:01 +0200 Subject: [PATCH] docs(api): better document refresh token incl. rotation explanation and error handling --- projects/api/src/contracts/oauth/index.ts | 13 +++++++++++-- .../oauth/schema/request/tokenRefreshSchema.ts | 2 +- .../schema/response/tokenErrorResponseSchema.ts | 11 +++++++++++ .../oauth/schema/response/tokenResponseSchema.ts | 5 ++++- 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 projects/api/src/contracts/oauth/schema/response/tokenErrorResponseSchema.ts diff --git a/projects/api/src/contracts/oauth/index.ts b/projects/api/src/contracts/oauth/index.ts index f83c389..78c2306 100644 --- a/projects/api/src/contracts/oauth/index.ts +++ b/projects/api/src/contracts/oauth/index.ts @@ -7,6 +7,9 @@ import { import { tokenRefreshSchema } from './schema/request/tokenRefreshSchema.ts'; import { tokenRequestSchema } from './schema/request/tokenRequestSchema.ts'; import { codeResponseSchema } from './schema/response/codeResponseSchema.ts'; +import { + tokenErrorResponseSchema, +} from './schema/response/tokenErrorResponseSchema.ts'; import { tokenResponseSchema } from './schema/response/tokenResponseSchema.ts'; const authorizeQuerySchema = z.object({ @@ -135,7 +138,13 @@ When building the authorization URL, you can optionally include the following qu token: { summary: 'Exchange a token', description: - 'Exchange an OAuth authorization code or refresh token for an access token. Send the appropriate token request body; returns token details on success or a `400` response when the request cannot be processed.', + `Exchange an OAuth authorization code or refresh token for an access token. + +#### Refreshing Tokens + +Refresh tokens are single-use. Each successful refresh returns a new \`refresh_token\` and invalidates the previous one. Store the new \`access_token\` and \`refresh_token\` from the response before the next refresh. + +If the exchange cannot be completed, this endpoint returns a \`400\` response with an OAuth error and description.`, path: '/token', method: 'POST', body: z.union([ @@ -144,7 +153,7 @@ When building the authorization URL, you can optionally include the following qu ]), responses: { 200: tokenResponseSchema, - 400: z.undefined(), + 400: tokenErrorResponseSchema, }, }, revoke: { diff --git a/projects/api/src/contracts/oauth/schema/request/tokenRefreshSchema.ts b/projects/api/src/contracts/oauth/schema/request/tokenRefreshSchema.ts index 1f5d6e3..b01af32 100644 --- a/projects/api/src/contracts/oauth/schema/request/tokenRefreshSchema.ts +++ b/projects/api/src/contracts/oauth/schema/request/tokenRefreshSchema.ts @@ -5,7 +5,7 @@ import { tokenBaseSchema } from './tokenBaseSchema.ts'; export const tokenRefreshSchema = tokenBaseSchema.extend({ refresh_token: z.string({ description: - 'The refresh token which was sent from the server during the exchange of the code.', + 'The current refresh token. Refresh tokens are single-use; after a successful exchange, replace it with the new `refresh_token` returned in the response.', }), grant_type: z.string({ description: 'Defines how an access token is obtained.', diff --git a/projects/api/src/contracts/oauth/schema/response/tokenErrorResponseSchema.ts b/projects/api/src/contracts/oauth/schema/response/tokenErrorResponseSchema.ts new file mode 100644 index 0000000..d62eb58 --- /dev/null +++ b/projects/api/src/contracts/oauth/schema/response/tokenErrorResponseSchema.ts @@ -0,0 +1,11 @@ +import { z } from '../../../_internal/z.ts'; + +/** Zod schema for the token exchange error response. */ +export const tokenErrorResponseSchema = z.object({ + error: z.string({ + description: 'OAuth error code, such as `invalid_grant`.', + }), + error_description: z.string({ + description: 'Human-readable details about the token exchange failure.', + }), +}).describe('Bad Request - the token exchange could not be completed.'); diff --git a/projects/api/src/contracts/oauth/schema/response/tokenResponseSchema.ts b/projects/api/src/contracts/oauth/schema/response/tokenResponseSchema.ts index de0c87a..6b8b28a 100644 --- a/projects/api/src/contracts/oauth/schema/response/tokenResponseSchema.ts +++ b/projects/api/src/contracts/oauth/schema/response/tokenResponseSchema.ts @@ -5,7 +5,10 @@ export const tokenResponseSchema = z.object({ access_token: z.string(), token_type: z.string(), expires_in: z.number().int(), - refresh_token: z.string(), + refresh_token: z.string({ + description: + 'The refresh token to store for the next refresh. It replaces the previous refresh token, which can no longer be used.', + }), scope: z.string(), created_at: z.number().int(), });