{"record":{"id":"7ce38156a06a9629","repo":"calcom/cal.diy","slug":"invalid-refresh-token","errorCode":null,"errorMessage":"Invalid refresh token","messagePattern":"Invalid refresh token","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"apps/api/v2/src/modules/oauth-clients/services/oauth-flow.service.ts","lineNumber":154,"sourceCode":"      refreshTokenExpiresAt: refreshTokenExpiresAt.valueOf(),\n    };\n  }\n\n  async refreshToken(clientId: string, clientSecret: string, tokenSecret: string): Promise<KeysDto> {\n    const oauthClient = await this.oAuthClientRepository.getOAuthClientWithRefreshSecret(\n      clientId,\n      clientSecret,\n      tokenSecret\n    );\n\n    if (!oauthClient) {\n      throw new BadRequestException(\"Invalid OAuthClient credentials.\");\n    }\n\n    const currentRefreshToken = oauthClient.refreshToken[0];\n\n    if (!currentRefreshToken) {\n      throw new BadRequestException(\"Invalid refresh token\");\n    }\n\n    const { accessToken, refreshToken } = await this.tokensRepository.refreshOAuthTokens(\n      clientId,\n      currentRefreshToken.secret,\n      currentRefreshToken.userId\n    );\n\n    return {\n      accessToken: accessToken.secret,\n      accessTokenExpiresAt: accessToken.expiresAt.valueOf(),\n      refreshToken: refreshToken.secret,\n      refreshTokenExpiresAt: refreshToken.expiresAt.valueOf(),\n    };\n  }\n\n  private _generateActKey(accessToken: string) {\n    return `act_${accessToken}`;","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/oauth-clients/services/oauth-flow.service.ts#L136-L172","documentation":"Thrown by refreshToken after the client+secret matched, when oauthClient.refreshToken[0] is falsy. The refresh token row is absent — meaning the refresh token was revoked, deleted, already rotated (old one removed), or never existed for this client. BadRequestException (HTTP 400). The user must re-authorize to obtain a new grant.","triggerScenarios":"POST /refresh with a refresh token that has been revoked or rotated out; a refresh token from a different client; a refresh token whose row was deleted.","commonSituations":"Refresh-token rotation policies that delete the old token; an admin revoked the session; the refresh token expired and was cleaned up; reusing a refresh token after it was already used under one-time-use rotation.","solutions":["Re-run the full OAuth authorize+exchange flow to obtain a fresh refresh token.","If using refresh-token rotation, never reuse a refresh token that was already exchanged.","Store only the latest refresh token after each refresh.","Surface a 're-login required' prompt to the user when refresh fails this way."],"exampleFix":"// before\nawait oauthFlow.refreshToken(clientId, secret, oldRefreshToken); // 400\n\n// after — re-authorize to get a new grant\nconst code = await authorize(userId, clientId, redirectUri);\nconst { accessToken, refreshToken } = await oauthFlow.exchangeAuthorizationToken(code, clientId, secret);\nawait oauthFlow.refreshToken(clientId, secret, refreshToken);","handlingStrategy":"fallback","validationCode":"// Track the latest refresh token; detect when re-authorize is required\nfunction canAttemptRefresh(token: { refreshToken: string } | null | undefined): token is { refreshToken: string } {\n  return Boolean(token && typeof token.refreshToken === 'string' && token.refreshToken.length > 0);\n}\nif (!canAttemptRefresh(stored)) {\n  throw new Error('No usable refresh token; user must re-authorize');\n}","typeGuard":"function hasRefreshToken(value: unknown): value is { refreshToken: string } {\n  return typeof value === 'object' && value !== null && typeof (value as any).refreshToken === 'string';\n}","tryCatchPattern":"try {\n  return await oauthFlow.refreshToken(clientId, clientSecret, refreshToken);\n} catch (e) {\n  if (e instanceof BadRequestException && /refresh token/i.test(e.message)) {\n    // refresh token revoked/rotated — fall back to full re-authorization\n    const code = await reAuthorize(userId, clientId, redirectUri);\n    return await oauthFlow.exchangeAuthorizationToken(code, clientId, clientSecret);\n  }\n  throw e;\n}","preventionTips":["Under refresh-token rotation, never reuse an already-used refresh token.","Store only the latest refresh token after each refresh.","Prompt re-login when refresh fails with this error."],"tags":["oauth","refresh-token","revoked","rotation","re-authorize"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}