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

  1. Restart the OAuth flow from the authorization endpoint to get a fresh code (codes are single-use)
  2. Confirm the client_id used at exchange matches the one used at authorization
  3. Check the code is transmitted intact — URL-encode it and avoid decode steps that mangle '+' or padding
  4. 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

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


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