gitroomhq/postiz-app · error · HttpException

invalid_grant

invalid_grant

Error message

{ error: 'invalid_grant', error_description: 'redirect_uri does not match the authorization request' }

What it means

Returned as HTTP 400 invalid_grant when the authorization record has a redirectUri and the redirect_uri sent at token exchange differs from it. RFC 6749 requires redirect_uri at exchange to match exactly when one was used at authorization.

Source

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

    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 },
    } = await this._oauthRepository.exchangeCodeForToken(
      auth.id,
      encryptedToken
    );

    return {
      id: organizationId,
      cus: paymentId,

View on GitHub (pinned to 0f1647f749)

Solutions

  1. Send the identical redirect_uri string at both authorization and exchange
  2. Centralize the callback URL in one config value used by both calls
  3. Check for trailing slashes, scheme (http vs https), port, and encoding differences

Example fix

// before
authorizeURL({ redirect_uri: 'http://localhost:3000/cb' });
exchangeCode({ redirect_uri: 'http://localhost:3000/callback' });
// after
const redirectUri = process.env.REDIRECT_URI!;
authorizeURL({ redirect_uri: redirectUri });
exchangeCode({ redirect_uri: redirectUri });
Defensive patterns

Strategy: validation

Validate before calling

if (authRedirectUri && exchangeRedirectUri !== authRedirectUri) {
  throw new Error('redirect_uri mismatch between authorize and exchange');
}

Type guard

const redirectUrisMatch = (a?: string, b?: string): boolean => !a || a === b;

Try / catch

try { return await exchange(body); } catch (e) { if (/redirect_uri/.test(e?.response?.data?.error_description)) { body.redirect_uri = savedRedirectUri; return exchange(body); } throw e; }

Prevention

When it happens

Trigger: Including a different redirect_uri in the token request than in the authorize request; omitting it inconsistently; differing trailing slash, port, or query parameter; proxy rewrites changing the externally visible URL.

Common situations: App running on a different port/host between the two steps; load balancer terminating SSL so http vs https differs; hardcoded redirect in one place and env-driven in another; URL normalization by a framework.

Related errors


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