{"record":{"id":"be0e2858968a6af1","repo":"gitroomhq/postiz-app","slug":"error-invalid-grant-error-description-code","errorCode":null,"errorMessage":"{ error: 'invalid_grant', error_description: 'Code has expired' }","messagePattern":"\\{ error: 'invalid_grant', error_description: 'Code has expired' \\}","errorType":"http","errorClass":"HttpException","httpStatus":400,"severity":"error","filePath":"libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts","lineNumber":289,"sourceCode":"      ) {\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        );\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        );","sourceCodeStart":271,"sourceCodeEnd":307,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts#L271-L307","documentation":"Returned as HTTP 400 invalid_grant when the authorization code's codeExpiresAt has passed or is missing. Authorization codes are short-lived (typically minutes) per RFC 6749, so delays between authorization and token exchange invalidate them.","triggerScenarios":"Exchanging a code more than the configured TTL after the user authorized; long user hesitation or queue latency before the exchange; server clock skew; record created without an expiry (legacy data with null codeExpiresAt).","commonSituations":"User sits on the consent/callback page before the app completes the exchange; paused debugger; background job processing the callback late; code saved and replayed later.","solutions":["Complete the token exchange immediately upon receiving the callback; restart the flow if the code expired","Verify server clock synchronization (NTP) if expiry seems premature","If records have null codeExpiresAt from an older schema, re-authorize to create fresh records with expiry set"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"const age = Date.now() - codeIssuedAt;\nif (age > CODE_TTL_MS) { return restartAuthorizationFlow(); }","typeGuard":"const codeIsStillFresh = (issuedAt?: number): boolean => !!issuedAt && Date.now() - issuedAt < 5 * 60_000;","tryCatchPattern":"try { return await exchange(code); } catch (e) { if (e?.response?.data?.error_description === 'Code has expired') { return restartAuthorizationFlow(); } throw e; }","preventionTips":["Exchange the code immediately in the callback handler","Keep server clocks NTP-synced"],"tags":["oauth2","invalid-grant","code-expired","token-exchange"],"backgroundTag":"oauth-authorization-code-expired","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}