gitroomhq/postiz-app · error · HttpException

{ error: 'invalid_grant', error_description: 'Code has expir

Error message

{ error: 'invalid_grant', error_description: 'Code has expired' }

What it means

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.

Source

Thrown at libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts:289

      ) {
        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
        );
      }
      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
        );

View on GitHub (pinned to 0f1647f749)

Solutions

  1. Complete the token exchange immediately upon receiving the callback; restart the flow if the code expired
  2. Verify server clock synchronization (NTP) if expiry seems premature
  3. If records have null codeExpiresAt from an older schema, re-authorize to create fresh records with expiry set
Defensive patterns

Strategy: fallback

Validate before calling

const age = Date.now() - codeIssuedAt;
if (age > CODE_TTL_MS) { return restartAuthorizationFlow(); }

Type guard

const codeIsStillFresh = (issuedAt?: number): boolean => !!issuedAt && Date.now() - issuedAt < 5 * 60_000;

Try / catch

try { return await exchange(code); } catch (e) { if (e?.response?.data?.error_description === 'Code has expired') { return restartAuthorizationFlow(); } throw e; }

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


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