calcom/cal.diy · error · BadRequestException

Missing `state` query param

Error message

Missing `state` query param

What it means

Thrown by ConferencingController.save (the OAuth callback handler at GET /v2/conferencing/{app}/oauth/callback) when the `state` query parameter is missing. Cal.com encodes the entire OAuth flow context (accessToken, returnTo, teamId, orgId) into the state param before redirecting to Zoom/Microsoft; the provider echoes it back. Without state, the callback cannot reconstruct who initiated the flow. Returns HTTP 400.

Source

Thrown at apps/api/v2/src/modules/conferencing/controllers/conferencing.controller.ts:148

  @Get("/:app/oauth/callback")
  @UseGuards()
  @Redirect(undefined, 301)
  @ApiOperation({ summary: "Conferencing app OAuth callback" })
  @ApiParam({
    name: "app",
    description: "Conferencing application type",
    enum: [ZOOM, OFFICE_365_VIDEO],
    required: true,
  })
  async save(
    @Query("state") state: string,
    @Param("app") app: string,
    @Query("code") code: string,
    @Query("error") error: string | undefined,
    @Query("error_description") error_description: string | undefined
  ): Promise<{ url: string }> {
    if (!state) {
      throw new BadRequestException("Missing `state` query param");
    }

    const decodedCallbackState: OAuthCallbackState = JSON.parse(state);
    try {
      if (error) {
        throw new BadRequestException(error_description);
      }

      if (decodedCallbackState.teamId && decodedCallbackState.orgId) {
        const apiUrl = this.config.get("api.url");
        const url = `${apiUrl}/organizations/${decodedCallbackState.orgId}/teams/${decodedCallbackState.teamId}/conferencing/${app}/oauth/callback`;
        const params: Record<string, string | undefined> = { state, code, error, error_description };
        const headers = {
          Authorization: `Bearer ${decodedCallbackState.accessToken}`,
        };
        try {
          const response = await this.httpService.axiosRef.get(url, { params, headers });
          const redirectUrl = response.data?.url || decodedCallbackState.onErrorReturnTo || "";

View on GitHub (pinned to 176037d0af)

Solutions

  1. Ensure the auth-url endpoint (GET /v2/conferencing/{app}/oauth/auth-url) is used to start the flow so state is generated and embedded by Cal.com.
  2. Verify the redirect URI registered in the Zoom/Microsoft app console matches exactly and the provider is configured to return state.
  3. Do not call the callback endpoint directly; always enter through the auth-url redirect.
  4. If testing locally, append a valid base64/JSON-encoded state generated from a real access token.

Example fix

// before: manual redirect construction
window.location.href = `${apiUrl}/v2/conferencing/zoom/oauth/callback?code=${code}`;

// after: obtain the auth url first, then redirect
const { data } = await api.getConferencingOauthUrl('zoom');
window.location.href = data.authUrl; // includes state, returnTo, onErrorReturnTo
Defensive patterns

Strategy: validation

Validate before calling

function hasStateParam(query: URLSearchParams): asserts query is URLSearchParams & { get(k: 'state'): string } {
  if (!query.get('state')) {
    throw new Error('OAuth callback missing state param — start the flow via /oauth/auth-url.');
  }
}

hasStateParam(new URL(req.url).searchParams);

Type guard

function hasValidStateQuery(q: URLSearchParams): q is URLSearchParams {
  return Boolean(q.get('state'));
}

Prevention

When it happens

Trigger: The OAuth provider (Zoom/Microsoft) redirected back without the state query param; the callback URL was hit directly by a browser/script without state; a misconfigured redirect URI in the provider console stripped state; the user bookmarked/refreshed the callback URL.

Common situations: Redirect URL configured in Zoom/Microsoft Entra missing the state passthrough; manual testing of the callback endpoint; URL shortener or proxy that drops query params; provider deviated from RFC 6749 state handling.

Related errors


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