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
- 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.
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
- 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.
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
- PermissionsGuard - no oAuth client found for access token=${
- ApiAuthStrategy - access token - Invalid request origin - pl
- PermissionsGuard - no authentication provided. Provide eithe
- PermissionsGuard - oAuth client with id=${oAuthClient.id} do
- PermissionsGuard - no oAuth client found for client id=${id}
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/8d360c1534848be0.
Report an issue: GitHub.