gitroomhq/postiz-app · error · HttpException
{ error: 'invalid_grant' }
Error message
{ error: 'invalid_grant' } What it means
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.
Source
Thrown at libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts:282
// authenticate with PKCE instead of a client secret
const isPublicClient = app.dynamic && app.tokenEndpointAuthMethod === 'none';
if (!isPublicClient) {
if (
!clientSecret ||
!app.clientSecret ||
app.clientSecret !== AuthService.fixedEncryption(clientSecret)
) {
throw new HttpException(
{ error: 'invalid_client' },
HttpStatus.UNAUTHORIZED
);
}
}
const encryptedCode = AuthService.fixedEncryption(code);
const auth = await this._oauthRepository.findByCode(encryptedCode);
if (!auth || auth.oauthAppId !== app.id) {
throw new HttpException(
{ error: 'invalid_grant' },
HttpStatus.BAD_REQUEST
);
}
if (!auth.codeExpiresAt || new Date() > auth.codeExpiresAt) {
throw new HttpException(
{ error: 'invalid_grant', error_description: 'Code has expired' },
HttpStatus.BAD_REQUEST
);
}
if (auth.codeChallenge) {
if (!codeVerifier) {
throw new HttpException(
{ error: 'invalid_grant', error_description: 'code_verifier is required' },
HttpStatus.BAD_REQUEST
);View on GitHub (pinned to 0f1647f749)
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)
Example fix
// before await retry(() => exchangeCode(code)); // retries re-send consumed code // after const tokens = await exchangeCode(code); // single attempt; on invalid_grant restart flow
Defensive patterns
Strategy: fallback
Validate before calling
const seen = new Set<string>();
if (seen.has(code)) throw new Error('code already exchanged');
seen.add(code); Type guard
const isFreshCode = (code: string, used: Set<string>): boolean => !used.has(code);
Try / catch
try { return await exchange(code); } catch (e) { if (e?.response?.data?.error === 'invalid_grant') { return restartAuthorizationFlow(); } throw e; } Prevention
- Treat authorization codes as single-use; never auto-retry the token request
- URL-encode the code and avoid manual decode steps
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- { error: 'invalid_grant', error_description: 'Code has expir
- { error: 'invalid_grant', error_description: 'code_verifier
- invalid_grant
- { error: 'invalid_client' }
- { error: 'invalid_grant', error_description: 'Invalid code_v
AI-assisted analysis of gitroomhq/postiz-app@0f1647f749 (2026-08-27).
Data as JSON: /api/errors/e3dd1f71009a05da.
Report an issue: GitHub.