What RS256 Really Means
RS256 = RSA Signature with SHA-256. It uses RSASSA-PKCS1-v1_5 padding (RFC 3447 §8.2) with SHA-256 as the hash function. It is an asymmetric algorithm: signing needs the private key, verification needs only the public key.
Here is the crucial property that makes RS256 the default for public APIs: you can safely publish the public key anywhere — in a git repo, on a static website, in a JWKS endpoint — and no attacker can use it to forge tokens. Only someone with the private key can produce a valid RS256 signature.
signature = RSASSA-PKCS1-v1_5-SIGN(
key = private_rsa_key,
hash = SHA-256(base64url(header) + "." + base64url(payload))
);
jwt = base64url(header) + "." + base64url(payload) + "." + base64url(signature);The JWKS Endpoint — How to Find the Public Key
Modern auth providers expose public keys via a JWKS (JSON Web Key Set) endpoint at a predictable URL:
- Auth0:
https://YOUR_TENANT.auth0.com/.well-known/jwks.json - Firebase Authentication:
https://www.googleapis.com/robot/v1/metadata/x509/[email protected] - AWS Cognito:
https://cognito-idp.REGION.amazonaws.com/POOL_ID/.well-known/jwks.json - Okta:
https://YOUR_DOMAIN.okta.com/oauth2/default/v1/keys - Azure AD:
https://login.microsoftonline.com/TENANT/discovery/v2.0/keys - Google:
https://www.googleapis.com/oauth2/v3/certs
The JWKS response looks like this:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "abc123",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nn...",
"e": "AQAB"
},
{ /* another key during rotation */ }
]
}Match the token's header.kid to the JWKS entry's kid. The n (modulus) and e (exponent) fields together define the RSA public key. Libraries like jose convert these to a usable key automatically.
Verifying an RS256 JWT in Different Languages
Node.js (jose library — recommended)
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://YOUR_TENANT.auth0.com/.well-known/jwks.json')
);
async function verifyRS256(token) {
const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
issuer: 'https://YOUR_TENANT.auth0.com/',
audience: 'https://api.example.com',
algorithms: ['RS256'],
});
return payload;
}Node.js (jsonwebtoken + jwks-rsa)
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://YOUR_TENANT.auth0.com/.well-known/jwks.json',
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
callback(err, key && key.getPublicKey());
});
}
jwt.verify(token, getKey, {
algorithms: ['RS256'],
issuer: 'https://YOUR_TENANT.auth0.com/',
audience: 'https://api.example.com',
}, (err, decoded) => { /* ... */ });Python (PyJWT + PyJWKClient)
import jwt
from jwt import PyJWKClient
jwks_client = PyJWKClient('https://YOUR_TENANT.auth0.com/.well-known/jwks.json')
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
issuer='https://YOUR_TENANT.auth0.com/',
audience='https://api.example.com',
)Browser (jose in the browser)
import { importSPKI, jwtVerify } from 'jose';
const publicKey = await importSPKI(pemString, 'RS256');
const { payload } = await jwtVerify(token, publicKey);Handling Key Rotation
Identity providers rotate signing keys periodically (Auth0 every 6 months, AWS Cognito on demand). During rotation, the JWKS endpoint returns both the old and new keys for a transition window. Your verifier should:
- Cache the JWKS response for 10 minutes to an hour — refetching every request is wasteful.
- Re-fetch on a kid mismatch — if a token's kid is not in the cached JWKS, refetch immediately.
jose'screateRemoteJWKSetdoes this automatically. - Never hardcode a single public key in your verifier code — always look it up by kid from the JWKS.
Common RS256 Verification Errors
- invalid signature — you are using the wrong public key. Double-check the kid matches and that you fetched the JWKS from the correct issuer.
- signing key does not have alg RS256 — the JWKS key entry declares a different algorithm. Pick a different key from the set.
- error looking up key with kid — the kid in the token header is not in the current JWKS. The signing key may have been rotated out; force a JWKS refresh.
- invalid audience — the token was issued for a different API. Every API should enforce its own audience.
- invalid issuer — the token was issued by a different tenant or auth server. Sanity-check the issuer URL character-by-character.
- token used before nbf — clock skew between the auth server and your API. Allow 30-60 seconds of leeway in your verifier config.
Key Facts
- Algorithm:
- RSASSA-PKCS1-v1_5 with SHA-256 (RFC 3447 §8.2)
- Header alg value:
- "RS256"
- Key type:
- Asymmetric — private key signs, public key verifies
- Recommended RSA size:
- 2048 bits or larger (4096 for long-term)
- Public key format:
- PEM (SPKI) or JWK from a JWKS endpoint
- Best for:
- Public APIs, OpenID Connect, federated identity
Related JWT Tools
- HS256 JWT Decoder — decode & verify HMAC-signed tokens with a shared secret
- JWT Decoder Online — generic decoder for any algorithm
- Verify JWT Signature — HS256 & RS256 verification walkthrough
- JWT Decoder Node.js — jsonwebtoken and jose library patterns
- JWT Decoder Python — PyJWT + PyJWKClient patterns