Access tokens
Build on Kinde
JWT decoding involves parsing and validating JSON Web Tokens to extract their payload information securely. While JWTs are typically signed (not encrypted), decoding refers to the process of parsing the token structure and validating its claims.
Before decoding a JWT, it’s important to understand its structure. A JWT consists of three parts separated by dots (.):
The @kinde/jwt-decoder library provides a simple, type-safe way to decode JWT tokens.
The decoder extracts and returns the decoded payload containing the token’s claims. See an example access token payload.
# npmnpm install @kinde/jwt-decoder
# yarnyarn add @kinde/jwt-decoder
# pnpmpnpm install @kinde/jwt-decoderimport { jwtDecoder } from "@kinde/jwt-decoder"
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
// Simple decodeconst decodedToken = jwtDecoder(token)
console.log(decodedToken)// Output: { sub: '1234567890', name: 'John Doe', admin: true, iat: 1516239022 }import { jwtDecoder, type JWTDecoded } from "@kinde/jwt-decoder"
// Decode with extended typetype CustomJWT = JWTDecoded & { custom_claim?: string feature_flags?: Record<string, any>}
const decodedToken = jwtDecoder<CustomJWT>("ey...")The @kinde/jwt-validator library provides cryptographic JWT validation with support for mobile and edge environments. The validateToken method verifies the token’s signature and returns an object with a valid property indicating whether the token is valid.
# npmnpm install @kinde/jwt-validator
# yarnyarn add @kinde/jwt-validator
# pnpmpnpm install @kinde/jwt-validatorimport { validateToken } from "@kinde/jwt-validator"import { jwtDecoder } from "@kinde/jwt-decoder"
const token = "ey..." // your JWT here
const validateAndDecode = async () => { try { // Validate the token const result = await validateToken({ token, domain: "https://your-subdomain.kinde.com", })
if (result.valid) { console.log("Token is valid")
// Decode after validation const decoded = jwtDecoder(token) console.log("Decoded payload:", decoded) } else { console.log("Token validation failed:", result.message) } } catch (error) { // The validator throws for JWKS or validation errors console.error("Token is invalid:", error) }}
validateAndDecode()If you need to decode JWTs without using Kinde’s libraries, you can implement manual decoding:
This implementation uses atob(), which is available in browsers, web workers, and service workers:
function base64UrlDecode(str) { // Replace Base64URL characters with Base64 characters let base64 = str.replace(/-/g, '+').replace(/_/g, '/')
// Add padding if needed (Base64 strings must be multiples of 4) while (base64.length % 4) { base64 += '=' }
return atob(base64)}
function decodeJWT(token) { try { // Split the token into its three parts const parts = token.split('.')
if (parts.length !== 3) { throw new Error('Invalid JWT format') }
// Decode header and payload (base64url) const header = JSON.parse(base64UrlDecode(parts[0])) const payload = JSON.parse(base64UrlDecode(parts[1]))
return { header, payload, signature: parts[2] } } catch (error) { throw new Error('Failed to decode JWT: ' + error.message) }}
// Usageconst token = "eyJhbGc..."const decoded = decodeJWT(token)console.log(decoded.payload)For Node.js environments, use Buffer instead of atob():
function base64UrlDecode(str) { // Replace Base64URL characters with Base64 characters let base64 = str.replace(/-/g, '+').replace(/_/g, '/')
// Add padding if needed (Base64 strings must be multiples of 4) while (base64.length % 4) { base64 += '=' }
return Buffer.from(base64, 'base64').toString('utf-8')}
function decodeJWT(token) { try { // Split the token into its three parts const parts = token.split('.')
if (parts.length !== 3) { throw new Error('Invalid JWT format') }
// Decode header and payload (base64url) const header = JSON.parse(base64UrlDecode(parts[0])) const payload = JSON.parse(base64UrlDecode(parts[1]))
return { header, payload, signature: parts[2] } } catch (error) { throw new Error('Failed to decode JWT: ' + error.message) }}For code that works in both browser and Node.js environments:
function base64UrlDecode(str) { // Replace Base64URL characters with Base64 characters let base64 = str.replace(/-/g, '+').replace(/_/g, '/')
// Add padding if needed (Base64 strings must be multiples of 4) while (base64.length % 4) { base64 += '=' }
// Use Buffer in Node.js, atob in browser if (typeof Buffer !== 'undefined') { return Buffer.from(base64, 'base64').toString('utf-8') } else { return atob(base64) }}interface JWTHeader { alg: string; typ: string; kid?: string;}
interface JWTPayload { iss: string; sub: string; aud: string | string[]; exp: number; iat: number; jti?: string; [key: string]: any;}
interface DecodedJWT { header: JWTHeader; payload: JWTPayload; signature: string;}
function base64UrlDecode(str: string): string { // Replace Base64URL characters with Base64 characters let base64 = str.replace(/-/g, '+').replace(/_/g, '/')
// Add padding if needed (Base64 strings must be multiples of 4) while (base64.length % 4) { base64 += '=' }
// Use Buffer in Node.js, atob in browser if (typeof globalThis.Buffer !== 'undefined') { return globalThis.Buffer.from(base64, 'base64').toString('utf-8') } else if (typeof globalThis.atob !== 'undefined') { return globalThis.atob(base64) } else { throw new Error('Neither Buffer nor atob is available in this environment') }}
function decodeJWT(token: string): DecodedJWT { try { // Split the token into its three parts const parts = token.split('.')
if (parts.length !== 3) { throw new Error('Invalid JWT format') }
// Decode header and payload (base64url) const header = JSON.parse(base64UrlDecode(parts[0])) as JWTHeader const payload = JSON.parse(base64UrlDecode(parts[1])) as JWTPayload
return { header, payload, signature: parts[2] } } catch (error) { throw new Error('Failed to decode JWT: ' + (error instanceof Error ? error.message : String(error))) }}You can extract user information from decoded tokens, including email, organization code, feature flags, and permissions.
By default, the email claim is not included in the access_token. To enable it:
If you need to access the user’s full name and profile picture, use the id_token instead of the access token. The id_token includes these claims by default. You can decode the id_token using the same method as the access token. Learn more about ID tokens.
import { jwtDecoder } from "@kinde/jwt-decoder"
function displayUserInfo(token) { try { const payload = jwtDecoder(token)
// Note: Email must be enabled in token customization for access tokens console.log(`User: ${payload.email}`) console.log(`Organization code: ${payload.org_code}`) console.log(`Permissions: ${payload.permissions?.join(", ")}`)
return { email: payload?.email || "", org_code: payload.org_code, permissions: payload.permissions || [], } } catch (error) { console.error("Failed to decode token:", error) return null }}About org_code and permissions claims
The org_code and permissions claims are not included by default in access tokens:
org_code: Only included when the user belongs to and signs in to an organization. If a user belongs to no organizations, this claim will be omitted.permissions: Only included when the user signs in to an organization AND has permissions assigned to them or their role. If no permissions are configured or assigned, this claim will be omitted.Always check if these claims exist before accessing them (as shown in the example above using optional chaining or default values).
import { jwtDecoder } from "@kinde/jwt-decoder"
function checkFeatureFlag(token, flagName) { try { const payload = jwtDecoder(token);
const featureFlags = payload.feature_flags;
if (featureFlags && featureFlags[flagName]) { return featureFlags[flagName].v; }
return false; } catch (error) { console.error('Failed to check feature flag:', error); return false; }}About feature_flags claim
The feature_flags claim is not included by default in access tokens. It’s only included when:
If no feature flags are configured or assigned, this claim will be omitted. Always check if the claim exists before accessing it (as shown in the example above).
Feature flags use a compact format with short keys: t for type and v for value.
import { jwtDecoder } from "@kinde/jwt-decoder"
export function getUserPermissions(token) { try { const payload = jwtDecoder(token) return payload.permissions || [] } catch (error) { console.error('Failed to extract permissions:', error) return [] }}About permissions claim
The permissions claim is not included by default in access tokens. It’s only included when:
If no permissions are configured or assigned, this claim will be omitted. Always check if the claim exists before accessing it (as shown in the example above using || [] as a fallback).
exp claim to ensure the token hasn’t expired.When decoding JWTs, ensure you:
iss (issuer) claim matches your Kinde domainaud (audience) claimexp (expiration) claimiat (issued at) claim is reasonablefunction safeDecodeJWT(token) { try { if (!token) { throw new Error('Token is required') }
if (typeof token !== 'string') { throw new Error('Token must be a string') }
const parts = token.split('.') if (parts.length !== 3) { throw new Error('Invalid JWT format - must have 3 parts') }
return jwtDecoder(token) } catch (error) { console.error('JWT decoding error:', error.message) return null }}