JWT Deep Dive: Signing, Verification, and the Attacks That Work on Misconfigured Implementations
JWT Deep Dive
A JSON Web Token is three Base64URL-encoded segments separated by dots. The first segment declares the algorithm used to sign the token. The second segment contains the claims. The third segment is the signature over the first two segments.
This structure is both the JWT’s power and its vulnerability. The power: any party with the public key can verify the token without contacting the issuer. The vulnerability: the token declares its own algorithm, and if the verifier trusts that declaration without restriction, an attacker can choose the algorithm.
The JWT specification (RFC 7519) and the JSON Web Signature specification (RFC 7515) together define how tokens are structured and verified. Both specs are well-designed. The vulnerabilities exist in implementations that follow the spec’s flexibility rather than constraining it. A spec-compliant JWT library that accepts any algorithm declared in the header is spec-compliant and vulnerable. Security comes from constraining the spec to your specific use case.
This chapter covers JWT structure at the byte level, the signing algorithms that matter, the algorithm confusion attack that exploits unconstrained verification, and the Spring Security configuration that prevents it. It also covers custom claims (adding tenant_id and roles to tokens issued by Spring Authorization Server), token introspection, and JWKS endpoint configuration.
Structure at the Byte Level
A typical JWT:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0wMDEifQ.
eyJzdWIiOiJ1c2VyLTEyMyIsInRlbmFudF9pZCI6ImFjbWUtY29ycCIsInNjb3BlIjpbInRlbmFudDpyZWFkIiwidGVuYW50OndyaXRlIl0sImlzcyI6Imh0dHBzOi8vYXV0aC5zYWFzLmV4YW1wbGUiLCJhdWQiOiJjb3JlLWFwaSIsImV4cCI6MTcwMDAwMDMwMCwiaWF0IjoxNzAwMDAwMDAwfQ.
<signature-bytes>
Header (first segment):
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-001"
}
alg: The algorithm used to sign. RS256 = RSASSA-PKCS1-v1_5 using SHA-256.typ: Token type. Always “JWT” for JWTs.kid: Key ID. Tells the verifier which key from the JWKS endpoint to use for verification. Critical during key rotation when multiple keys are active.
Payload (second segment):
{
"sub": "user-123",
"tenant_id": "acme-corp",
"scope": ["tenant:read", "tenant:write"],
"iss": "https://auth.saas.example",
"aud": "core-api",
"exp": 1700000300,
"iat": 1700000000
}
sub: Subject. The user identifier.iss: Issuer. The authorization server that issued this token.aud: Audience. The resource server this token is intended for.exp: Expiration. Unix timestamp after which the token must be rejected.iat: Issued at. Unix timestamp when the token was created.tenant_id,scope: Custom claims added by the token customizer.
Signature (third segment):
The RSA-SHA256 signature over base64url(header) + "." + base64url(payload) using the authorization server’s private key. The resource server verifies this signature using the public key published at the JWKS endpoint.
Signing Algorithm Selection
Three algorithm families are relevant:
RS256/RS384/RS512 (RSA): Asymmetric. The authorization server signs with a private key. Resource servers verify with the public key. The public key can be freely distributed (via JWKS endpoint). No shared secrets between issuer and verifier. Key size: 2048-bit minimum, 4096-bit for long-lived keys.
ES256/ES384/ES512 (ECDSA): Asymmetric. Same model as RSA but with smaller keys and faster verification. ES256 uses P-256 curve. Signatures are non-deterministic (different signature for the same input each time), which complicates debugging but does not affect security.
HS256/HS384/HS512 (HMAC): Symmetric. The same secret signs and verifies. Both the issuer and verifier must have the secret. This means every resource server that validates tokens must hold a secret capable of forging tokens. Suitable only when the issuer and verifier are the same process. Not suitable for distributed systems.
The decision rule: use RS256 or ES256 for any system where the issuer and verifier are different processes. Use ES256 if you care about token size (smaller signatures) or verification speed. Use RS256 if you need maximum compatibility with existing libraries. Never use HS256 in a system where resource servers are separate from the authorization server.