What HS256 Really Means
HS256 = HMAC + SHA-256. It is one of the three algorithms every JWT library must implement (per RFC 7518 §3.1), alongside RS256 and ES256. HS256 uses symmetric cryptography — the same secret both signs a token and verifies it. That property gives HS256 its main advantage (fast, simple, no key management infrastructure) and its main disadvantage (every verifier needs the secret, and a leaked secret compromises every past and future token).
The signature is computed as:
signature = HMAC-SHA256(
key = secret,
input = base64url_encode(header) + "." + base64url_encode(payload)
);
jwt = base64url_encode(header) + "." + base64url_encode(payload) + "." + base64url_encode(signature);Note the signature bytes are Base64URL-encoded before being appended. When you split a JWT on the dots, the third segment is the base64url form of the raw HMAC output, not the raw bytes themselves.
HS256 vs RS256 — Which Should You Use?
Think of HS256 as a shared password and RS256 as a signed letter with an attached public seal.
- Use HS256 when: the signing service and all verification services are inside a single trust boundary (a monorepo, a single company, an internal service mesh). Both operations happen on infrastructure you fully control. The secret can be rotated together.
- Use RS256 when: anyone outside your trust boundary needs to verify tokens — public APIs, third-party integrations, OpenID Connect providers, or federated identity. Publish the public key via a JWKS endpoint and keep the private key on the auth server only.
Verifying an HS256 JWT in Different Languages
JavaScript (Browser — SubtleCrypto)
async function verifyHS256(token, secret) {
const [h, p, s] = token.split('.');
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
const sigBytes = Uint8Array.from(
atob(s.replace(/-/g, '+').replace(/_/g, '/')),
c => c.charCodeAt(0)
);
return crypto.subtle.verify(
'HMAC', key, sigBytes,
new TextEncoder().encode(h + '.' + p)
);
}Node.js (jsonwebtoken)
const jwt = require('jsonwebtoken');
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});Python (PyJWT)
import jwt
payload = jwt.decode(
token,
key=os.environ['JWT_SECRET'],
algorithms=['HS256'],
issuer='https://auth.example.com',
audience='https://api.example.com',
)Go (golang-jwt/jwt)
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(os.Getenv("JWT_SECRET")), nil
})Secret Generation — Do It Right
An HS256 token is only as strong as its secret. Never use a memorable phrase; use a CSPRNG:
# Node.js
require('crypto').randomBytes(32).toString('base64')
# Python
python -c "import secrets; print(secrets.token_urlsafe(32))"
# Bash / OpenSSL
openssl rand -base64 32
# Output example (do NOT reuse):
# TG9yZW1JcHN1bURvbG9yU2l0QW1ldENvbnNlY3RldHVyQWRpcA==Store it in a secret manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager, Azure Key Vault) and inject it into your services at boot as an environment variable. Never commit secrets to git; never log them.
The Algorithm-Confusion Attack
The single most important rule when verifying HS256 tokens: always pass an explicit algorithms allow-list to your verifier. If you use RS256 in production but forget the allow-list, an attacker can craft a token with alg: HS256 and sign it using your PUBLIC RSA key as the HMAC secret. Your verifier treats the public key as an HS256 secret, HMAC succeeds, and the attacker impersonates any user. Every major library (jsonwebtoken, PyJWT, golang-jwt) now requires the allow-list — but if you upgrade from an old version, double-check your code passes it.
Common HS256 Verification Errors
- invalid signature — secret mismatch between issuer and verifier. Check env vars in both services.
- jwt malformed — token does not have three dot-separated segments. Client may be sending an opaque token instead of a JWT.
- invalid algorithm — token was signed with RS256 or ES256, not HS256. Check the header.alg field.
- jwt expired — signature is valid but exp claim is in the past. Client must refresh.
- secret or public key must be provided — your verifier code passed undefined as the secret. Likely a missing environment variable.
Key Facts
- Algorithm:
- HMAC with SHA-256 (RFC 4634, RFC 7518)
- Header alg value:
- "HS256"
- Signature length:
- 256 bits (32 bytes raw, 43 chars base64url)
- Key type:
- Symmetric — same shared secret for sign and verify
- Recommended secret size:
- 32+ bytes (256+ bits) from a CSPRNG
- Best for:
- Internal service-to-service auth within a single trust boundary
Related JWT Tools
- RS256 JWT Decoder — decode & verify RSA-signed tokens with a public key
- JWT Decoder Online — generic decoder for any algorithm
- Verify JWT Signature — HS256 & RS256 verification walkthrough
- JWT Decoder in JavaScript — SubtleCrypto + jose patterns
- Hash Generator — compute SHA-256, HMAC, MD5 hashes