{"record":{"id":"d5fc4fb12f16f52b","repo":"gitroomhq/postiz-app","slug":"error-invalid-grant-error-description-inva","errorCode":null,"errorMessage":"{ error: 'invalid_grant', error_description: 'Invalid code_verifier' }","messagePattern":"\\{ error: 'invalid_grant', error_description: 'Invalid code_verifier' \\}","errorType":"http","errorClass":"HttpException","httpStatus":400,"severity":"error","filePath":"libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts","lineNumber":304,"sourceCode":"    }\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      );\n    }\n\n    const token = 'pos_' + makeId(40);\n    const encryptedToken = AuthService.fixedEncryption(token);\n    const {\n      organizationId,\n      organization: { paymentId },","sourceCodeStart":286,"sourceCodeEnd":322,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts#L286-L322","documentation":"Returned as HTTP 400 invalid_grant when the PKCE code_verifier is provided but base64url(SHA-256(verifier)) does not equal the stored codeChallenge. The verifier must be the exact random string used to derive the challenge during authorization.","triggerScenarios":"Using a different verifier than the one that generated the challenge (regenerated verifier on retry); computing the hash wrong (hex vs base64url, or sending the raw verifier as challenge at authorize time); verifier truncated or whitespace-padded.","commonSituations":"SPA reloaded and generated a new verifier; verifier stored/round-tripped through JSON with escaping issues; custom PKCE implementation hashing incorrectly; using 'plain' style but server stores S256.","solutions":["Regenerate the flow ensuring the SAME verifier instance is stored and later sent — use a standard OAuth library's PKCE support","Verify hash derivation: base64url encoding of sha256 digest, no hex, no padding, no '+' '/' characters","Trim whitespace and confirm the verifier charset/length (43-128 unreserved chars per RFC 7636)"],"exampleFix":"// before\nconst challenge = verifier; // wrong: plain\n// after\nconst challenge = Buffer.from(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))).toString('base64url');","handlingStrategy":"validation","validationCode":"const hashed = createHash('sha256').update(verifier).digest('base64url');\nif (hashed !== storedChallenge) throw new Error('verifier does not match — restart flow');","typeGuard":"const verifierMatches = (verifier: string, challenge: string): boolean => createHash('sha256').update(verifier).digest('base64url') === challenge;","tryCatchPattern":"try { return await exchange(body); } catch (e) { if (/Invalid code_verifier/.test(e?.response?.data?.error_description)) { return restartFlowWithPkce(); } throw e; }","preventionTips":["Derive and store the verifier once; reuse that exact instance at exchange","Use base64url, never hex, for the challenge"],"tags":["oauth2","pkce","invalid-grant","verifier-mismatch"],"backgroundTag":"oauth-pkce-verifier-mismatch","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}