Invalid JWT payload when validating Genesys Cloud implicit grant token in React

Getting a JsonWebTokenError: invalid signature when trying to decode the access token received from the Genesys Cloud implicit grant flow.

We’ve got a React SPA that redirects to the Genesys Cloud auth URL, grabs the hash fragment, and parses the access_token. The token looks valid in jwt.io (decoded as JSON), but our backend validation service keeps rejecting it.

Here’s the client-side extraction:

const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
const accessToken = params.get('access_token');

The backend uses jsonwebtoken to verify:

const jwt = require('jsonwebtoken');

// We're fetching the JWKS from the Genesys Cloud endpoint
const jwks = await fetch('https://login.mypurecloud.com/oauth2/jwks').then(r => r.json());

try {
 const decoded = jwt.verify(accessToken, jwks.keys[0], {
 algorithms: ['RS256'],
 issuer: 'https://login.mypurecloud.com/oauth2/token',
 audience: 'MY_CLIENT_ID'
 });
} catch (err) {
 console.error('Token verification failed:', err.message);
}

The error is always invalid signature. I checked the kid in the token header and it matches the key in the JWKS response. The iss and aud claims also look correct.

Is the implicit grant token signed differently? Or am I missing something in the JWKS lookup logic? We’re using the standard login.mypurecloud.com endpoints. No custom domains involved yet.

You’re likely validating against the wrong public key. The implicit grant token is signed with a specific key ID (kid) found in the token header, not the default one you might be pulling from the static JWKS endpoint without checking the kid claim first.

Here is how to debug this step-by-step:

  1. Decode the header of the access_token (the first part before the dot).
  2. Extract the kid value.
  3. Fetch the JWKS from https://api.mypurecloud.com/api/v2/authorization/keys.
  4. Find the key in the response array that matches the kid.
  5. Use that specific key to verify the signature.

If you’re using Node.js with jose, it looks like this:

import { jwtVerify, importSPKI } from 'jose';

// 1. Fetch keys
const res = await fetch('https://api.mypurecloud.com/api/v2/authorization/keys');
const jwks = await res.json();

// 2. Find matching key by kid
const tokenParts = token.split('.');
const header = JSON.parse(Buffer.from(tokenParts[0], 'base64').toString());
const keyObj = jwks.keys.find(k => k.kid === header.kid);

if (!keyObj) {
 throw new Error('Key not found in JWKS');
}

// 3. Import and verify
const publicKey = await importSPKI(keyObj.n + keyObj.e, 'RS256'); // Simplified for example
// Note: You need to perly decode n and e from base64url to binary for RS256
// Or use a library that handles JWKS parsing automatically like 'jsonwebtoken' with jwksUri

const verified = await jwtVerify(token, publicKey, { algorithms: ['RS256'] });
console.log(verified.payload);

Most people skip the kid lookup and try to verify against the first key in the list. That fails because Genesys rotates keys. Also, make sure you’re not trying to decrypt an access token. It’s signed, not encrypted. If you need to decrypt a token, that’s only for refresh tokens in specific flows, not standard access tokens. Check your algorithm setting too. It should be RS256.