{"record":{"id":"3b9d6262c6fd7c9a","repo":"calcom/cal.diy","slug":"missing-state-query-param","errorCode":null,"errorMessage":"Missing `state` query param","messagePattern":"Missing `state` query param","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"apps/api/v2/src/modules/conferencing/controllers/conferencing.controller.ts","lineNumber":148,"sourceCode":"  @Get(\"/:app/oauth/callback\")\n  @UseGuards()\n  @Redirect(undefined, 301)\n  @ApiOperation({ summary: \"Conferencing app OAuth callback\" })\n  @ApiParam({\n    name: \"app\",\n    description: \"Conferencing application type\",\n    enum: [ZOOM, OFFICE_365_VIDEO],\n    required: true,\n  })\n  async save(\n    @Query(\"state\") state: string,\n    @Param(\"app\") app: string,\n    @Query(\"code\") code: string,\n    @Query(\"error\") error: string | undefined,\n    @Query(\"error_description\") error_description: string | undefined\n  ): Promise<{ url: string }> {\n    if (!state) {\n      throw new BadRequestException(\"Missing `state` query param\");\n    }\n\n    const decodedCallbackState: OAuthCallbackState = JSON.parse(state);\n    try {\n      if (error) {\n        throw new BadRequestException(error_description);\n      }\n\n      if (decodedCallbackState.teamId && decodedCallbackState.orgId) {\n        const apiUrl = this.config.get(\"api.url\");\n        const url = `${apiUrl}/organizations/${decodedCallbackState.orgId}/teams/${decodedCallbackState.teamId}/conferencing/${app}/oauth/callback`;\n        const params: Record<string, string | undefined> = { state, code, error, error_description };\n        const headers = {\n          Authorization: `Bearer ${decodedCallbackState.accessToken}`,\n        };\n        try {\n          const response = await this.httpService.axiosRef.get(url, { params, headers });\n          const redirectUrl = response.data?.url || decodedCallbackState.onErrorReturnTo || \"\";","sourceCodeStart":130,"sourceCodeEnd":166,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/conferencing/controllers/conferencing.controller.ts#L130-L166","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Verify the redirect URI registered in the Zoom/Microsoft app console matches exactly and the provider is configured to return state.","Do not call the callback endpoint directly; always enter through the auth-url redirect.","If testing locally, append a valid base64/JSON-encoded state generated from a real access token."],"exampleFix":"// before: manual redirect construction\nwindow.location.href = `${apiUrl}/v2/conferencing/zoom/oauth/callback?code=${code}`;\n\n// after: obtain the auth url first, then redirect\nconst { data } = await api.getConferencingOauthUrl('zoom');\nwindow.location.href = data.authUrl; // includes state, returnTo, onErrorReturnTo","handlingStrategy":"validation","validationCode":"function hasStateParam(query: URLSearchParams): asserts query is URLSearchParams & { get(k: 'state'): string } {\n  if (!query.get('state')) {\n    throw new Error('OAuth callback missing state param — start the flow via /oauth/auth-url.');\n  }\n}\n\nhasStateParam(new URL(req.url).searchParams);","typeGuard":"function hasValidStateQuery(q: URLSearchParams): q is URLSearchParams {\n  return Boolean(q.get('state'));\n}","tryCatchPattern":null,"preventionTips":["Never construct the callback URL manually; always redirect through the auth-url endpoint so state is generated server-side.","Verify the redirect URI registered in Zoom/Microsoft exactly matches and preserves query params.","Educate users not to bookmark the callback URL.","Reject callback requests with no state at the edge (WAF/middleware) to reduce noise."],"tags":["oauth","conferencing","zoom","office365","nestjs","callback","state"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}