{"record":{"id":"3b5f14a71cf4c6fb","repo":"gitroomhq/postiz-app","slug":"error-invalid-grant-error-description-code-3b5f14","errorCode":null,"errorMessage":"{ error: 'invalid_grant', error_description: 'code_verifier is required' }","messagePattern":"\\{ error: 'invalid_grant', error_description: 'code_verifier is required' \\}","errorType":"http","errorClass":"HttpException","httpStatus":400,"severity":"error","filePath":"libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts","lineNumber":297,"sourceCode":"    const encryptedCode = AuthService.fixedEncryption(code);\n    const auth = await this._oauthRepository.findByCode(encryptedCode);\n    if (!auth || auth.oauthAppId !== app.id) {\n      throw new HttpException(\n        { error: 'invalid_grant' },\n        HttpStatus.BAD_REQUEST\n      );\n    }\n\n    if (!auth.codeExpiresAt || new Date() > auth.codeExpiresAt) {\n      throw new HttpException(\n        { error: 'invalid_grant', error_description: 'Code has expired' },\n        HttpStatus.BAD_REQUEST\n      );\n    }\n\n    if (auth.codeChallenge) {\n      if (!codeVerifier) {\n        throw new HttpException(\n          { error: 'invalid_grant', error_description: 'code_verifier is required' },\n          HttpStatus.BAD_REQUEST\n        );\n      }\n      const hashed = createHash('sha256').update(codeVerifier).digest('base64url');\n      if (hashed !== auth.codeChallenge) {\n        throw new HttpException(\n          { error: 'invalid_grant', error_description: 'Invalid code_verifier' },\n          HttpStatus.BAD_REQUEST\n        );\n      }\n    }\n\n    if (auth.redirectUri && redirectUri !== auth.redirectUri) {\n      throw new HttpException(\n        { error: 'invalid_grant', error_description: 'redirect_uri does not match the authorization request' },\n        HttpStatus.BAD_REQUEST\n      );","sourceCodeStart":279,"sourceCodeEnd":315,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts#L279-L315","documentation":"Returned as HTTP 400 invalid_grant when the stored authorization record has a codeChallenge (PKCE was used at authorization) but the token exchange request omits the code_verifier parameter required by RFC 7636.","triggerScenarios":"Authorization was done with a code_challenge, but the token request body lacks code_verifier; verifier lost between requests (page reload, server restart losing state, session lost).","commonSituations":"Storing the verifier in memory in an SPA that reloaded; storing it in a session that expired; splitting authorization and exchange across processes without passing the verifier; SDK configured for PKCE at authorize but not at exchange.","solutions":["Persist the code_verifier (cookie, session, DB) alongside the state from authorization through callback handling","Include code_verifier in the token exchange request body","Restart the flow if the verifier is unrecoverable"],"exampleFix":"// before\nconst tokens = await exchangeCode({ grant_type: 'authorization_code', client_id, code, redirect_uri });\n// after\nconst tokens = await exchangeCode({ grant_type: 'authorization_code', client_id, code, redirect_uri, code_verifier: session.pkceVerifier });","handlingStrategy":"validation","validationCode":"if (flowUsedPkce && !codeVerifier) {\n  throw new Error('code_verifier missing — restore it from the authorize-step session');\n}","typeGuard":"const hasVerifier = (v?: string | null): v is string => typeof v === 'string' && v.length >= 43;","tryCatchPattern":"try { return await exchange(body); } catch (e) { if (/code_verifier is required/.test(e?.response?.data?.error_description)) { return restartFlowWithPkce(); } throw e; }","preventionTips":["Persist the verifier server-side keyed by state, not in SPA memory","Use one library call that owns both authorize and exchange steps"],"tags":["oauth2","pkce","invalid-grant","token-exchange"],"backgroundTag":"oauth-pkce-verifier-missing","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}