calcom/cal.diy · error · BadRequestException

Invalid 'redirect_uri' value.

Error message

Invalid 'redirect_uri' value.

What it means

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.

Source

Thrown at apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts:64

  ) {}

  @Post("/authorize")
  @HttpCode(HttpStatus.OK)
  @UseGuards(NextAuthGuard)
  @DocsExcludeEndpoint()
  async authorize(
    @Param("clientId") clientId: string,
    @Body() body: OAuthAuthorizeInput,
    @GetUser("id") userId: number,
    @Response() res: ExpressResponse
  ): Promise<void> {
    const oauthClient = await this.oauthClientRepository.getOAuthClient(clientId);
    if (!oauthClient) {
      throw new BadRequestException(`OAuth client with ID '${clientId}' not found`);
    }

    if (!isOriginAllowed(body.redirectUri, oauthClient.redirectUris)) {
      throw new BadRequestException("Invalid 'redirect_uri' value.");
    }

    const alreadyAuthorized = await this.tokensRepository.getAuthorizationTokenByClientUserIds(
      clientId,
      userId
    );

    if (alreadyAuthorized) {
      throw new BadRequestException(
        `User with id=${userId} has already authorized client with id=${clientId}.`
      );
    }

    const { id } = await this.tokensRepository.createAuthorizationToken(clientId, userId);

    return res.redirect(`${body.redirectUri}?code=${id}`);
  }

View on GitHub (pinned to 176037d0af)

Solutions

  1. Add the exact redirectUri (scheme + host + port + path) to the client's redirectUris via PATCH /v2/oauth-clients/:clientId before calling authorize.
  2. Match scheme, host, port, and trailing slash exactly — isOriginAllowed compares origins, so verify port and scheme.
  3. For local dev, register the precise localhost port you serve on.
  4. Send the identical string in redirectUri that you registered.

Example fix

// before
body: JSON.stringify({ redirectUri: 'http://localhost:3001/callback' })

// after — register it on the client first
await fetch(`/v2/oauth-clients/${clientId}`, { method: 'PATCH', body: JSON.stringify({ redirectUris: ['http://localhost:3001/callback'] }) });
body: JSON.stringify({ redirectUri: 'http://localhost:3001/callback' })
Defensive patterns

Strategy: validation

Validate before calling

// Confirm the redirectUri is registered on the client before authorizing
function isRedirectAllowed(redirectUri: string, allowed: string[]) {
  try {
    const origin = new URL(redirectUri).origin;
    return allowed.some((registered) => new URL(registered).origin === origin);
  } catch {
    return false;
  }
}
if (!isRedirectAllowed(redirectUri, client.redirectUris)) {
  throw new Error('redirectUri origin not registered; PATCH the client first');
}

Type guard

function isAllowedRedirectUri(uri: string, allowed: string[]): boolean {
  return allowed.some((r) => r === uri || (() => { try { return new URL(r).origin === new URL(uri).origin; } catch { return false; } })());
}

Try / catch

try {
  await authorize(clientId, { redirectUri });
} catch (e) {
  if (e instanceof BadRequestException && /redirect_uri/i.test(e.message)) {
    await registerRedirectUri(clientId, redirectUri); // PATCH redirectUris
    await authorize(clientId, { redirectUri });
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12). Data as JSON: /api/errors/8d360c1534848be0. Report an issue: GitHub.