{"record":{"id":"2b95a5634ef62a9b","repo":"ruvnet/ruflo","slug":"protocol","errorCode":"protocol","errorMessage":"oauth error: ${body.error} — ${body.error_description}","messagePattern":"oauth error: (.+?) — (.+?)","errorType":"exception","errorClass":"OAuthError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/security/src/oauth/client.ts","lineNumber":80,"sourceCode":"  url.searchParams.set('redirect_uri', redirectUri);\n  url.searchParams.set('scope', SCOPE);\n  url.searchParams.set('state', state);\n  url.searchParams.set('code_challenge', codeChallenge);\n  url.searchParams.set('code_challenge_method', 'S256');\n  return url.toString();\n}\n\nasync function parseTokenResponse(resp: Response): Promise<TokenResponse> {\n  if (resp.ok) {\n    try {\n      return (await resp.json()) as TokenResponse;\n    } catch {\n      throw new OAuthError('unexpected response shape from the server', 'unexpected_shape');\n    }\n  }\n  try {\n    const body = (await resp.json()) as OAuthErrorBody;\n    throw new OAuthError(\n      `oauth error: ${body.error} — ${body.error_description}`,\n      'protocol',\n      body.error,\n      body.error_description,\n    );\n  } catch (e) {\n    if (e instanceof OAuthError) throw e;\n    throw new OAuthError('unexpected response shape from the server', 'unexpected_shape');\n  }\n}\n\nasync function postForm(path: string, form: Record<string, string>, base = authBaseUrl()): Promise<TokenResponse> {\n  let resp: Response;\n  try {\n    resp = await fetch(`${base}${path}`, {\n      method: 'POST',\n      headers: { 'content-type': 'application/x-www-form-urlencoded' },\n      body: new URLSearchParams(form).toString(),","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/security/src/oauth/client.ts#L62-L98","documentation":"The token endpoint answered non-ok with a standard OAuth error body {error, error_description}; it is rethrown as OAuthError code 'protocol' with oauthError/oauthDescription preserved (e.g. invalid_grant, invalid_client, access_denied). This is the server formally rejecting the grant — the message names which OAuth error occurred.","triggerScenarios":"exchangeCode() with an already-used or expired authorization code; PKCE code_verifier that doesn't match the challenge; refreshAccessToken() with a revoked or rotated refresh token; a redirect_uri or client_id that differs from registration.","commonSituations":"Retrying the code exchange after a crash/timeout reuses the one-time code; refresh tokens revoked by logout or rotation; delays consuming a code past its short expiry; clock skew on the client.","solutions":["Inspect e.oauthError — invalid_grant means restart the full authorize flow; access_denied means the user declined; invalid_client indicates client_id/registration problems","Never reuse an authorization code: persist it durably, exchange exactly once, claim before retrying","Keep the code_verifier from the session that generated the challenge","When refresh fails, fall back to re-authorization to obtain a new refresh_token rather than retrying the dead one"],"exampleFix":"// before\nconst tokens = await exchangeCode(code, verifier, redirectUri); // retried after timeout → oauth error: invalid_grant\n\n// after\nif (!exchangeClaimed) {\n  exchangeClaimed = true; // exactly-once guard before first attempt\n  const tokens = await exchangeCode(code, verifier, redirectUri);\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isOAuthProtocolError(e: unknown, oauthError?: string): boolean {\n  return e instanceof Error && e.name === 'OAuthError'\n    && (e as { code?: string }).code === 'protocol'\n    && (oauthError === undefined || (e as { oauthError?: string }).oauthError === oauthError);\n}","tryCatchPattern":"try {\n  return await exchangeCode(code, verifier, redirectUri);\n} catch (e) {\n  if (isOAuthProtocolError(e, 'invalid_grant')) {\n    return await restartAuthorizationFlow(); // fresh authorize + PKCE pair\n  }\n  if (isOAuthProtocolError(e)) {\n    throw new Error(`OAuth rejected the grant: ${(e as { oauthError?: string }).oauthError}`);\n  }\n  throw e;\n}","preventionTips":["Exchange authorization codes exactly once — claim durably before the first attempt","Keep code_verifier with the session that created the challenge","On refresh-token protocol errors, re-authorize instead of retrying the dead token"],"tags":["oauth","protocol","invalid-grant","pkce","refresh-token"],"backgroundTag":"oauth-protocol-error","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}