{"record":{"id":"9d0a82ff31cadbfd","repo":"gitroomhq/postiz-app","slug":"invalid-grant","errorCode":"invalid_grant","errorMessage":"{ error: 'invalid_grant', error_description: 'redirect_uri does not match the authorization request' }","messagePattern":"\\{ error: 'invalid_grant', error_description: 'redirect_uri does not match the authorization request' \\}","errorType":"http","errorClass":"HttpException","httpStatus":400,"severity":"error","filePath":"libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts","lineNumber":312,"sourceCode":"\n    if (auth.codeChallenge) {\n      if (!codeVerifier) {\n        throw new HttpException(\n          { error: 'invalid_grant', error_description: 'code_verifier is required' },\n          HttpStatus.BAD_REQUEST\n        );\n      }\n      const hashed = createHash('sha256').update(codeVerifier).digest('base64url');\n      if (hashed !== auth.codeChallenge) {\n        throw new HttpException(\n          { error: 'invalid_grant', error_description: 'Invalid code_verifier' },\n          HttpStatus.BAD_REQUEST\n        );\n      }\n    }\n\n    if (auth.redirectUri && redirectUri !== auth.redirectUri) {\n      throw new HttpException(\n        { error: 'invalid_grant', error_description: 'redirect_uri does not match the authorization request' },\n        HttpStatus.BAD_REQUEST\n      );\n    }\n\n    const token = 'pos_' + makeId(40);\n    const encryptedToken = AuthService.fixedEncryption(token);\n    const {\n      organizationId,\n      organization: { paymentId },\n    } = await this._oauthRepository.exchangeCodeForToken(\n      auth.id,\n      encryptedToken\n    );\n\n    return {\n      id: organizationId,\n      cus: paymentId,","sourceCodeStart":294,"sourceCodeEnd":330,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts#L294-L330","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Send the identical redirect_uri string at both authorization and exchange","Centralize the callback URL in one config value used by both calls","Check for trailing slashes, scheme (http vs https), port, and encoding differences"],"exampleFix":"// before\nauthorizeURL({ redirect_uri: 'http://localhost:3000/cb' });\nexchangeCode({ redirect_uri: 'http://localhost:3000/callback' });\n// after\nconst redirectUri = process.env.REDIRECT_URI!;\nauthorizeURL({ redirect_uri: redirectUri });\nexchangeCode({ redirect_uri: redirectUri });","handlingStrategy":"validation","validationCode":"if (authRedirectUri && exchangeRedirectUri !== authRedirectUri) {\n  throw new Error('redirect_uri mismatch between authorize and exchange');\n}","typeGuard":"const redirectUrisMatch = (a?: string, b?: string): boolean => !a || a === b;","tryCatchPattern":"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; }","preventionTips":["Single source of truth for the callback URL","Beware proxies rewriting scheme/host — configure forwarded headers correctly"],"tags":["oauth2","invalid-grant","redirect-uri","token-exchange"],"backgroundTag":"oauth-redirect-uri-mismatch","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}