How to validate JWT tokens from the Genesys Cloud implicit grant in a React app
What You Will Build
- A React utility module that validates the signature and claims of a Genesys Cloud JWT obtained via the Implicit Grant flow.
- This implementation uses the
joselibrary for cryptographic verification against Genesys Cloud public keys. - The code is written in TypeScript for type safety and clarity.
Prerequisites
- OAuth Client Type: Public Client (SPA) configured for Implicit Grant.
- Required Scopes: The token must contain the scopes your application requires (e.g.,
agent:login,user:read). Validation checks presence, not authorization. - Library:
jose(npm package). This is the standard library for modern JWT handling in JavaScript environments. - Runtime: Node.js 16+ or a modern browser environment supporting ES modules.
- External Dependencies:
josev4.14.0+.
Authentication Setup
The Implicit Grant flow returns the access token directly in the URL fragment after the user authenticates. Unlike the Authorization Code flow with PKCE, the Implicit Grant does not issue an ID token by default in Genesys Cloud. The access token itself is a JWT.
To validate this token client-side, you must verify two things:
- The signature is valid using the public key corresponding to the
kid(Key ID) in the token header. - The claims (
iss,aud,exp) are correct.
You cannot validate the token without the public keys. Genesys Cloud exposes these via the JWKS (JSON Web Key Set) endpoint.
Step 1: Fetching the JWKS
First, you need a utility to fetch and cache the public keys. Genesys Cloud rotates these keys periodically. Your application should fetch them on initialization and cache them, or fetch them when a token with an unknown kid is encountered.
// jwks.ts
import { importJWK, jwtVerify, createRemoteJWKSet } from 'jose';
// The base URL for Genesys Cloud APIs
const GENESYS_BASE_URL = 'https://api.mypurecloud.com';
// The JWKS endpoint for Genesys Cloud
const JWKS_URL = `${GENESYS_BASE_URL}/oauth2/jwks`;
/**
* Creates a remote JWKS set that handles fetching and caching automatically.
* This is the recommended way to handle key rotation.
*/
export const getGenesysJWKS = () => {
return createRemoteJWKSet(new URL(JWKS_URL));
};
Note on Security: In a browser environment, fetching the JWKS endpoint requires CORS headers. Genesys Cloud allows CORS for the JWKS endpoint. However, for production applications, consider fetching the JWKS from your own backend proxy to avoid exposing your environment to direct browser requests to Genesys Cloud infrastructure, which can mitigate certain cross-site scripting risks related to key exposure.
Step 2: Validating the JWT Signature
Once you have the JWKS handler, you can validate the token. The jwtVerify function from jose handles the signature verification against the remote JWKS.
// jwtValidator.ts
import { jwtVerify, createRemoteJWKSet, JWTPayload } from 'jose';
const GENESYS_BASE_URL = 'https://api.mypurecloud.com';
const JWKS_URL = `${GENESYS_BASE_URL}/oauth2/jwks`;
/**
* Validates a Genesys Cloud JWT.
*
* @param token - The raw JWT string.
* @param expectedIssuer - The expected issuer, e.g., 'https://api.mypurecloud.com'.
* @param expectedAudience - The expected audience, usually your OAuth Client ID.
* @returns The decoded payload if valid, or throws an error.
*/
export async function validateGenesysJWT(
token: string,
expectedIssuer: string,
expectedAudience: string
): Promise<JWTPayload> {
try {
// Create a remote JWKS set
const JWKS = createRemoteJWKSet(new URL(JWKS_URL));
// Verify the token
// jwtVerify will:
// 1. Parse the header to find the 'kid'
// 2. Fetch the JWKS if not cached
// 3. Find the key matching the 'kid'
// 4. Verify the signature
// 5. Verify the 'exp' (expiration) claim
// 6. Verify the 'nbf' (not before) claim if present
const { payload } = await jwtVerify(token, JWKS, {
issuer: expectedIssuer,
audience: expectedAudience,
});
return payload;
} catch (error) {
if (error instanceof Error) {
throw new Error(`JWT Validation Failed: ${error.message}`);
}
throw error;
}
}
OAuth Scopes: This validation step does not require any OAuth scope. It is a cryptographic operation performed by your application. However, the token being validated must have been issued with the correct aud (your client ID) and iss (Genesys Cloud).
Step 3: Handling Claims and Scopes
Validation of the signature ensures the token was issued by Genesys Cloud and has not been tampered with. It does not ensure the token has the permissions (scopes) your application needs. You must check the scope claim manually after validation.
// authUtils.ts
import { validateGenesysJWT } from './jwtValidator';
import { JWTPayload } from 'jose';
// Your OAuth Client ID from Genesys Cloud Admin > Security > OAuth Clients
const CLIENT_ID = 'your-oauth-client-id';
const ISSUER = 'https://api.mypurecloud.com';
/**
* Validates the token and checks for required scopes.
*
* @param token - The raw JWT string.
* @param requiredScopes - An array of scope strings that must be present.
* @returns The payload if valid and scopes are present.
*/
export async function validateTokenWithScopes(
token: string,
requiredScopes: string[]
): Promise<JWTPayload> {
// First, validate signature and claims
const payload = await validateGenesysJWT(token, ISSUER, CLIENT_ID);
// Check expiration (jwtVerify already checks this, but good to be explicit for logging)
if (payload.exp && payload.exp < Date.now() / 1000) {
throw new Error('Token has expired');
}
// Check scopes
const tokenScopes = (payload.scope as string)?.split(' ') || [];
const missingScopes = requiredScopes.filter(
(scope) => !tokenScopes.includes(scope)
);
if (missingScopes.length > 0) {
throw new Error(
`Token missing required scopes: ${missingScopes.join(', ')}`
);
}
return payload;
}
Complete Working Example
This example demonstrates a React component that receives a token from the URL fragment (as in Implicit Grant), validates it, and stores the user information if valid.
// App.tsx
import React, { useEffect, useState } from 'react';
import { validateTokenWithScopes } from './authUtils';
import { JWTPayload } from 'jose';
const REQUIRED_SCOPES = ['agent:login', 'user:read'];
const CLIENT_ID = 'your-oauth-client-id';
const ISSUER = 'https://api.mypurecloud.com';
interface UserState {
name: string;
email: string;
sub: string;
valid: boolean;
}
const App: React.FC = () => {
const [user, setUser] = useState<UserState | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const validateToken = async () => {
// In Implicit Grant, the token is in the URL fragment
const hash = window.location.hash;
const params = new URLSearchParams(hash.substring(1));
const token = params.get('access_token');
if (!token) {
setError('No access token found in URL');
setLoading(false);
return;
}
try {
// Validate token signature, issuer, audience, and scopes
const payload = await validateTokenWithScopes(token, REQUIRED_SCOPES);
// Extract user information from claims
// Genesys Cloud JWTs contain 'name', 'email', 'sub' (user ID)
setUser({
name: (payload.name as string) || 'Unknown',
email: (payload.email as string) || 'Unknown',
sub: (payload.sub as string) || '',
valid: true,
});
// Clear the hash from the URL to prevent re-processing
window.history.replaceState({}, document.title, window.location.pathname);
} catch (err) {
if (err instanceof Error) {
setError(err.message);
} else {
setError('An unknown error occurred during validation');
}
} finally {
setLoading(false);
}
};
validateToken();
}, []);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
if (!user) {
return <div>Please authenticate</div>;
}
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>Email: {user.email}</p>
<p>User ID: {user.sub}</p>
</div>
);
};
export default App;
Real API Endpoint: The validation relies on https://api.mypurecloud.com/oauth2/jwks. This endpoint returns a JSON object containing the public keys.
Expected Response from JWKS:
{
"keys": [
{
"kty": "RSA",
"kid": "key-id-123",
"alg": "RS256",
"use": "sig",
"n": "base64url-encoded-modulus",
"e": "AQAB"
}
]
}
Common Errors & Debugging
Error: JWTExpiredError
- What causes it: The
expclaim in the token is in the past. Genesys Cloud access tokens expire after 1 hour by default. - How to fix it: Redirect the user to the Genesys Cloud login URL to obtain a new token. In Implicit Grant, there is no refresh token, so the user must re-authenticate.
- Code showing the fix:
if (error.message.includes('expired')) { window.location.href = 'https://login.mypurecloud.com/oauth/authorize?...'; }
Error: JWTInvalidAudienceError
- What causes it: The
audclaim in the token does not match theexpectedAudiencepassed tojwtVerify. This usually means the token was issued for a different OAuth Client ID. - How to fix it: Ensure the
CLIENT_IDin your code matches the OAuth Client ID used in the login redirect URL. - Code showing the fix:
// Ensure CLIENT_ID matches the one used in the OAuth redirect const CLIENT_ID = 'your-oauth-client-id'; // Must match the client_id param in the authorize URL
Error: JWTInvalidIssuerError
- What causes it: The
issclaim in the token does not match the expected issuer. For Genesys Cloud, this should behttps://api.mypurecloud.com. If you are using a different environment (e.g., EU), the issuer might behttps://api.eu.mypurecloud.com. - How to fix it: Update the
ISSUERconstant to match your Genesys Cloud environment. - Code showing the fix:
// For EU environment const ISSUER = 'https://api.eu.mypurecloud.com'; const JWKS_URL = `${ISSUER}/oauth2/jwks`;
Error: JWTSignatureVerificationFailed
- What causes it: The signature in the token does not match the public key. This can happen if the JWKS cache is stale and Genesys Cloud has rotated keys.
- How to fix it: The
createRemoteJWKSetfunction injosehandles caching. If this error persists, clear your browser cache or restart the application to force a fresh JWKS fetch. - Code showing the fix:
// No code change needed. The library handles this. // If debugging, add console logging to the JWKS fetch.