gitroomhq/postiz-app · error · HttpException
{ error: 'invalid_grant', error_description: 'Invalid code_v
Error message
{ error: 'invalid_grant', error_description: 'Invalid code_verifier' } What it means
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.
Source
Thrown at libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts:304
}
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
);
}
const hashed = createHash('sha256').update(codeVerifier).digest('base64url');
if (hashed !== auth.codeChallenge) {
throw new HttpException(
{ error: 'invalid_grant', error_description: 'Invalid code_verifier' },
HttpStatus.BAD_REQUEST
);
}
}
if (auth.redirectUri && redirectUri !== auth.redirectUri) {
throw new HttpException(
{ error: 'invalid_grant', error_description: 'redirect_uri does not match the authorization request' },
HttpStatus.BAD_REQUEST
);
}
const token = 'pos_' + makeId(40);
const encryptedToken = AuthService.fixedEncryption(token);
const {
organizationId,
organization: { paymentId },View on GitHub (pinned to 0f1647f749)
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)
Example fix
// before
const challenge = verifier; // wrong: plain
// after
const challenge = Buffer.from(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))).toString('base64url'); Defensive patterns
Strategy: validation
Validate before calling
const hashed = createHash('sha256').update(verifier).digest('base64url');
if (hashed !== storedChallenge) throw new Error('verifier does not match — restart flow'); Type guard
const verifierMatches = (verifier: string, challenge: string): boolean => createHash('sha256').update(verifier).digest('base64url') === challenge; Try / catch
try { return await exchange(body); } catch (e) { if (/Invalid code_verifier/.test(e?.response?.data?.error_description)) { return restartFlowWithPkce(); } throw e; } Prevention
- Derive and store the verifier once; reuse that exact instance at exchange
- Use base64url, never hex, for the challenge
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- { error: 'invalid_grant', error_description: 'code_verifier
- code_challenge is required for this client
- Only the S256 code_challenge_method is supported
- { error: 'invalid_grant' }
- { error: 'invalid_grant', error_description: 'Code has expir
AI-assisted analysis of gitroomhq/postiz-app@0f1647f749 (2026-08-27).
Data as JSON: /api/errors/d5fc4fb12f16f52b.
Report an issue: GitHub.