{"record":{"id":"8d360c1534848be0","repo":"calcom/cal.diy","slug":"invalid-redirect-uri-value","errorCode":null,"errorMessage":"Invalid 'redirect_uri' value.","messagePattern":"Invalid 'redirect_uri' value\\.","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts","lineNumber":64,"sourceCode":"  ) {}\n\n  @Post(\"/authorize\")\n  @HttpCode(HttpStatus.OK)\n  @UseGuards(NextAuthGuard)\n  @DocsExcludeEndpoint()\n  async authorize(\n    @Param(\"clientId\") clientId: string,\n    @Body() body: OAuthAuthorizeInput,\n    @GetUser(\"id\") userId: number,\n    @Response() res: ExpressResponse\n  ): Promise<void> {\n    const oauthClient = await this.oauthClientRepository.getOAuthClient(clientId);\n    if (!oauthClient) {\n      throw new BadRequestException(`OAuth client with ID '${clientId}' not found`);\n    }\n\n    if (!isOriginAllowed(body.redirectUri, oauthClient.redirectUris)) {\n      throw new BadRequestException(\"Invalid 'redirect_uri' value.\");\n    }\n\n    const alreadyAuthorized = await this.tokensRepository.getAuthorizationTokenByClientUserIds(\n      clientId,\n      userId\n    );\n\n    if (alreadyAuthorized) {\n      throw new BadRequestException(\n        `User with id=${userId} has already authorized client with id=${clientId}.`\n      );\n    }\n\n    const { id } = await this.tokensRepository.createAuthorizationToken(clientId, userId);\n\n    return res.redirect(`${body.redirectUri}?code=${id}`);\n  }\n","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts#L46-L82","documentation":"Thrown by POST /authorize after the client is found but isOriginAllowed(body.redirectUri, oauthClient.redirectUris) returns false. The redirectUri in the request body is compared against the list of redirect URIs registered on the OAuth client; a mismatch (origin or full URI not in the allow-list) yields BadRequestException (HTTP 400). This is a security control: open-redirect prevention via strict allow-listing.","triggerScenarios":"POST /authorize with a redirectUri whose origin is not in oauthClient.redirectUris, or with a redirectUri that has a different port/path/scheme than any registered entry. Common: localhost vs 127.0.0.1, http vs https, trailing slash mismatch, port omitted.","commonSituations":"Frontend runs on a new port not yet registered; http used during local dev but only https registered; a trailing-slash or case difference between the configured URI and the request; third-party integration sends their own callback URL that was never allow-listed.","solutions":["Add the exact redirectUri (scheme + host + port + path) to the client's redirectUris via PATCH /v2/oauth-clients/:clientId before calling authorize.","Match scheme, host, port, and trailing slash exactly — isOriginAllowed compares origins, so verify port and scheme.","For local dev, register the precise localhost port you serve on.","Send the identical string in redirectUri that you registered."],"exampleFix":"// before\nbody: JSON.stringify({ redirectUri: 'http://localhost:3001/callback' })\n\n// after — register it on the client first\nawait fetch(`/v2/oauth-clients/${clientId}`, { method: 'PATCH', body: JSON.stringify({ redirectUris: ['http://localhost:3001/callback'] }) });\nbody: JSON.stringify({ redirectUri: 'http://localhost:3001/callback' })","handlingStrategy":"validation","validationCode":"// Confirm the redirectUri is registered on the client before authorizing\nfunction isRedirectAllowed(redirectUri: string, allowed: string[]) {\n  try {\n    const origin = new URL(redirectUri).origin;\n    return allowed.some((registered) => new URL(registered).origin === origin);\n  } catch {\n    return false;\n  }\n}\nif (!isRedirectAllowed(redirectUri, client.redirectUris)) {\n  throw new Error('redirectUri origin not registered; PATCH the client first');\n}","typeGuard":"function isAllowedRedirectUri(uri: string, allowed: string[]): boolean {\n  return allowed.some((r) => r === uri || (() => { try { return new URL(r).origin === new URL(uri).origin; } catch { return false; } })());\n}","tryCatchPattern":"try {\n  await authorize(clientId, { redirectUri });\n} catch (e) {\n  if (e instanceof BadRequestException && /redirect_uri/i.test(e.message)) {\n    await registerRedirectUri(clientId, redirectUri); // PATCH redirectUris\n    await authorize(clientId, { redirectUri });\n  } else throw e;\n}","preventionTips":["Register exact scheme, host, port, and path of every redirect URI.","For local dev, register the precise localhost port.","Send the identical redirectUri string that was registered."],"tags":["oauth","redirect-uri","security","open-redirect","cors"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}