* Revokes a single token on the OAuth server. * * Per RFC 7009, public clients (like Claude Code) should authenticate by including * client_id in the request body, NOT via an Authorization header. The Bearer token * in an Authorization header is meant for resource owner authentication, not clien
({
serverName,
endpoint,
token,
tokenTypeHint,
clientId,
clientSecret,
accessToken,
authMethod = 'client_secret_basic',
}: {
serverName: string
endpoint: string
token: string
tokenTypeHint: 'access_token' | 'refresh_token'
clientId?: string
clientSecret?: string
accessToken?: string
authMethod?: 'client_secret_basic' | 'client_secret_post'
})
| 379 | * approach or ignore unexpected headers. |
| 380 | */ |
| 381 | async function revokeToken({ |
| 382 | serverName, |
| 383 | endpoint, |
| 384 | token, |
| 385 | tokenTypeHint, |
| 386 | clientId, |
| 387 | clientSecret, |
| 388 | accessToken, |
| 389 | authMethod = 'client_secret_basic', |
| 390 | }: { |
| 391 | serverName: string |
| 392 | endpoint: string |
| 393 | token: string |
| 394 | tokenTypeHint: 'access_token' | 'refresh_token' |
| 395 | clientId?: string |
| 396 | clientSecret?: string |
| 397 | accessToken?: string |
| 398 | authMethod?: 'client_secret_basic' | 'client_secret_post' |
| 399 | }): Promise<void> { |
| 400 | const params = new URLSearchParams() |
| 401 | params.set('token', token) |
| 402 | params.set('token_type_hint', tokenTypeHint) |
| 403 | |
| 404 | const headers: Record<string, string> = { |
| 405 | 'Content-Type': 'application/x-www-form-urlencoded', |
| 406 | } |
| 407 | |
| 408 | // RFC 7009 §2.1 requires client auth per RFC 6749 §2.3. XAA always uses a |
| 409 | // confidential client at the AS — strict ASes (Okta/Stytch) reject public- |
| 410 | // client revocation of confidential-client tokens. |
| 411 | if (clientId && clientSecret) { |
| 412 | if (authMethod === 'client_secret_post') { |
| 413 | params.set('client_id', clientId) |
| 414 | params.set('client_secret', clientSecret) |
| 415 | } else { |
| 416 | const basic = Buffer.from( |
| 417 | `${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`, |
| 418 | ).toString('base64') |
| 419 | headers.Authorization = `Basic ${basic}` |
| 420 | } |
| 421 | } else if (clientId) { |
| 422 | params.set('client_id', clientId) |
| 423 | } else { |
| 424 | logMCPDebug( |
| 425 | serverName, |
| 426 | `No client_id available for ${tokenTypeHint} revocation - server may reject`, |
| 427 | ) |
| 428 | } |
| 429 | |
| 430 | try { |
| 431 | await axios.post(endpoint, params, { headers }) |
| 432 | logMCPDebug(serverName, `Successfully revoked ${tokenTypeHint}`) |
| 433 | } catch (error: unknown) { |
| 434 | // Fallback for non-RFC-7009-compliant servers that require Bearer auth |
| 435 | if ( |
| 436 | axios.isAxiosError(error) && |
| 437 | error.response?.status === 401 && |
| 438 | accessToken |
no test coverage detected