{"record":{"id":"e3dd1f71009a05da","repo":"gitroomhq/postiz-app","slug":"error-invalid-grant","errorCode":null,"errorMessage":"{ error: 'invalid_grant' }","messagePattern":"\\{ error: 'invalid_grant' \\}","errorType":"http","errorClass":"HttpException","httpStatus":400,"severity":"error","filePath":"libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts","lineNumber":282,"sourceCode":"    // authenticate with PKCE instead of a client secret\n    const isPublicClient = app.dynamic && app.tokenEndpointAuthMethod === 'none';\n    if (!isPublicClient) {\n      if (\n        !clientSecret ||\n        !app.clientSecret ||\n        app.clientSecret !== AuthService.fixedEncryption(clientSecret)\n      ) {\n        throw new HttpException(\n          { error: 'invalid_client' },\n          HttpStatus.UNAUTHORIZED\n        );\n      }\n    }\n\n    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        );","sourceCodeStart":264,"sourceCodeEnd":300,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts#L264-L300","documentation":"Returned as HTTP 400 invalid_grant when the authorization code lookup fails: either no OAuth authorization record has that code, or the record belongs to a different OAuth app (auth.oauthAppId !== app.id). Codes are stored encrypted, so any mismatch yields this generic error per RFC 6749.","triggerScenarios":"Reusing an already-consumed code (they're one-time use), sending a code issued to a different client_id, truncating/corrupting the code string, or a clock/DB race where the record was deleted.","commonSituations":"Double token-exchange retry after a network timeout; multiple tabs/tokens from the same authorize flow; mixing codes between staging and production client apps; code URL-decoded incorrectly ( '+' became space).","solutions":["Restart the OAuth flow from the authorization endpoint to get a fresh code (codes are single-use)","Confirm the client_id used at exchange matches the one used at authorization","Check the code is transmitted intact — URL-encode it and avoid decode steps that mangle '+' or padding","Prevent duplicate exchange calls in retry logic (idempotency around the token request)"],"exampleFix":"// before\nawait retry(() => exchangeCode(code)); // retries re-send consumed code\n// after\nconst tokens = await exchangeCode(code); // single attempt; on invalid_grant restart flow","handlingStrategy":"fallback","validationCode":"const seen = new Set<string>();\nif (seen.has(code)) throw new Error('code already exchanged');\nseen.add(code);","typeGuard":"const isFreshCode = (code: string, used: Set<string>): boolean => !used.has(code);","tryCatchPattern":"try { return await exchange(code); } catch (e) { if (e?.response?.data?.error === 'invalid_grant') { return restartAuthorizationFlow(); } throw e; }","preventionTips":["Treat authorization codes as single-use; never auto-retry the token request","URL-encode the code and avoid manual decode steps"],"tags":["oauth2","invalid-grant","authorization-code","token-exchange"],"backgroundTag":"oauth-invalid-grant","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}