gitroomhq/postiz-app · error · HttpException

{ error: 'invalid_grant', error_description: 'Invalid…

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.

Solutions

  1. Regenerate the flow ensuring the SAME verifier instance is stored and later sent — use a standard OAuth library's PKCE support
  2. Verify hash derivation: base64url encoding of sha256 digest, no hex, no padding, no '+' '/' characters
  3. 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

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


AI-assisted analysis of gitroomhq/postiz-app@0f1647f749 (2026-08-27). Data as JSON: /api/errors/d5fc4fb12f16f52b. Report an issue: GitHub.

Appendix: 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)