Building a custom React portal for supervisors. We’re using the implicit grant flow to let users log in via the standard Genesys Cloud login screen. The redirect back to our app includes the access_token in the URL fragment.
The problem is validation. I can’t just trust the token is valid on the client side, but we don’t have a backend service to verify the signature against the JWKS endpoint. I tried using react-jwt to decode it, but that only decodes, it doesn’t verify the signature.
I need to verify the token is actually signed by Genesys Cloud before calling our internal APIs. Here is the flow:
- User clicks login, redirects to
https://login.mypurecloud.com/oauth/authorize.
- Genesys redirects back to
https://ourapp.com/callback#access_token=eyJ...&token_type=bearer&expires_in=3600.
- React app grabs the token.
I’m trying to fetch the public keys from https://login.mypurecloud.com/oauth/jwks and verify the token locally using jose library. The code looks like this:
import { jwtVerify, importJWK } from 'jose';
async function validateToken(token) {
const jwks = await fetch('https://login.mypurecloud.com/oauth/jwks').then(r => r.json());
const key = jwks.keys[0]; // Assuming first key works
const publicKey = await importJWK(key, 'RS256');
try {
const { payload } = await jwtVerify(token, publicKey);
return payload;
} catch (err) {
console.error('Token invalid', err);
return null;
}
}
This fails with ERR_JWT_INVALID_SIGNATURE. I suspect the key format or the algorithm mismatch. The token header says alg: RS256. The JWKS endpoint returns RSA keys. Is there a simpler way to validate this in the browser without writing custom crypto logic? Or am I missing a step in importing the JWK? The token works fine when passed to /api/v2/me, so the token itself is valid. Just can’t prove it client-side.
Implicit grant is a security nightmare and honestly, you shouldn’t be doing this in a browser. Storing tokens in localStorage or URL fragments makes them trivially easy to steal via XSS. If you really have no backend, you’re stuck with client-side validation, but it’s fragile.
The only way to verify the signature without hitting an introspection endpoint (which requires CORS headers that Genesys doesn’t always play nice with for public keys) is to fetch the JWKS yourself and verify the signature in JS.
Here’s how I’d handle it using jose library in React. It’s not pretty, but it works.
import { jwtVerify, importSPKI } from 'jose';
const JWKS_URL = 'https://api.mypurecloud.com/api/v2/authorization/jwks';
async function validateToken(token) {
try {
// Fetch the public keys once, maybe cache this
const jwksResponse = await fetch(JWKS_URL);
const jwks = await jwksResponse.json();
// Find the key matching the kid in the token header
const header = JSON.parse(atob(token.split('.')[1])); // Naive decode for kid
const keyEntry = jwks.keys.find(k => k.kid === header.kid);
if (!keyEntry) throw new Error('Key not found in JWKS');
// Import the public key
const publicKey = await importSPKI(keyEntry.x, 'RS256');
// Verify the token
const { payload } = await jwtVerify(token, publicKey, {
audience: 'your-client-id', // Check your specific client ID
issuer: 'https://api.mypurecloud.com'
});
return payload;
} catch (err) {
console.error('Token validation failed', err);
return null;
}
}
The catch is that importSPKI expects the x field from the JWKS, which is base64url encoded. The jose library handles the conversion, but if you’re using jwt-decode or something simpler, you’ll have to do the base64 conversion manually.
Also, don’t forget to check the exp claim. The library does it, but if you skip the signature check for some reason, you’re wide open.
This whole setup feels wrong though. A tiny Azure Function or AWS Lambda to proxy the auth is way safer. The implicit flow is deprecated for a reason.