JWT — structure, signing algorithms, refresh, security pitfalls.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add Dev-Toolbelt/dev-team-agents --skill jwt --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Jwt?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/dev-toolbelt-jwt)More formats (shields.io, HTML) on the badges page.
---
name: jwt
description: JWT — structure, signing algorithms, refresh, security pitfalls.
---
# JWT (JSON Web Token)
## Structure
A JWT is three Base64URL-encoded segments separated by dots:
```
header.payload.signature
```
**Header**
```json
{ "alg": "HS256", "typ": "JWT" }
```
**Payload** (claims)
```json
{
"sub": "user-uuid",
"iss": "https://auth.example.com",
"aud": "api.example.com",
"exp": 1700000000,
"iat": 1699996400,
"jti": "unique-token-id",
"email": "user@example.com",
"role": "admin"
}
```
**Signature** — HMAC or RSA/ECDSA over `header.payload` using the signing key.
---
## Standard Claims (RFC 7519)
| Claim | Name | Required | Description |
|---|---|---|---|
| `iss` | Issuer | Recommended | Who issued the token |
| `sub` | Subject | Recommended | Who the token represents (user ID) |
| `aud` | Audience | Recommended | Who the token is intended for |
| `exp` | Expiration | **Required** | Unix timestamp — reject after this |
| `iat` | Issued At | Recommended | When it was issued |
| `nbf` | Not Before | Optional | Not valid before this timestamp |
| `jti` | JWT ID | Optional | Unique ID for this token (for revocation) |
Always validate `exp`. Validate `iss` and `aud` when the system has multiple token issuers or audiences.
---
## Signing Algorithms
| Algorithm | Type | When to use |
|---|---|---|
| `HS256` / `HS384` / `HS512` | Symmetric (shared secret) | Single service; secret never leaves the server |
| `RS256` / `RS384` / `RS512` | Asymmetric (RSA) | Multiple services; public key distributed for verification |
| `ES256` / `ES384` / `ES512` | Asymmetric (ECDSA) | Same as RS but smaller keys, faster |
**Default recommendation**: `HS256` for single-service systems, `RS256` or `ES256` when multiple services need to verify tokens independently.
**Never use `none` algorithm** — it disables signature verification entirely.
---
## Token Expiry & Refresh Strategy
Short-lived access tokens + long-lived refresh tokens:
| Token | Typical TTL | Where to store |
|---|---|---|
| Access token | 15 min – 1 hour | Memory (JS), `Authorization` header |
| Refresh token | 7–30 days | `httpOnly` secure cookie or secure storage |
**Refresh flow**:
1. Client sends expired access token (or proactively refreshes before expiry)
2. Server validates refresh token (check DB — is it still valid, not revoked?)
3. Server issues new access token (and optionally rotates the refresh token)
4. Client uses the new access token
**Refresh token rotation** (recommended): each use of a refresh token invalidates the old one and issues a new one. Detects stolen tokens — if a rotated token is used, revoke the entire session.
---
## Validation Checklist
Every JWT validation must check all of the following:
- [ ] Signature is valid (using the correct key and algorithm)
- [ ] `exp` is in the future
- [ ] `iat` is in the past (clock skew tolerance: ≤ 60 seconds)
- [ ] `iss` matches the expected issuer
- [ ] `aud` matches the expected audience (if used)
- [ ] Algorithm header matches what the server expects — **never accept `none`**
- [ ] Token is not on the revocation list (if implementing revocation)
---
## Implementation by Language
**Node.js / TypeScript**
```typescript
import jwt from 'jsonwebtoken'
// Sign
const token = jwt.sign(
{ sub: userId, role: 'user' },
process.env.JWT_SECRET!,
{ expiresIn: '1h', issuer: 'api.example.com' }
)
// Verify — throws on invalid/expired
const payload = jwt.verify(token, process.env.JWT_SECRET!, {
issuer: 'api.example.com',
algorithms: ['HS256']
})
```
**Python**
```python
import jwt # PyJWT
token = jwt.encode(
{"sub": user_id, "exp": datetime.utcnow() + timedelta(hours=1)},
settings.JWT_SECRET,
algorithm="HS256"
)
payload = jwt.decode(token, settings.JWT_SECRET, algorithms=["HS256"])
```
**Go**
```go
import "github.com/golang-jwt/jwt/v5"
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": userID,
"exp": time.Now().Add(time.Hour).Unix(),
})
signed, _ := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
// Verify
parsed, err := jwt.Parse(signed, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
```
**PHP**
```php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$token = JWT::encode(['sub' => $userId, 'exp' => time() + 3600], $_ENV['JWT_SECRET'], 'HS256');
$decoded = JWT::decode($token, new Key($_ENV['JWT_SECRET'], 'HS256'));
```
---
## Custom Claims
Add application-specific claims to the payload. Prefix custom claims to avoid collisions:
```json
{
"sub": "user-uuid",
"exp": 1700000000,
"https://example.com/roles": ["admin", "editor"],
"tenant_id": "acme-corp"
}
```
**Authorization decisions** must be based on claims set by the server, not user-supplied data. If claims are read from a user-controlled source, an attacker can elevate privileges.
---
## Revocation
JWTs are stateless — they're valid until `exp`. To revoke before expiry:
| Strategy | How |
|---|---|
| **Blocklist** | Store revoked `jti` values in Redis; check on each request |
| **Refresh token revocation** | Don't revoke access tokens — make them short-lived (≤15 min). Revoke refresh tokens in DB |
| **Secret rotation** | Rotate the signing secret — invalidates all tokens at once; use only for emergency |
Short-lived access tokens (15 min) make individual revocation largely unnecessary — prefer this over a blocklist for most use cases.
---
## Security Pitfalls
| Pitfall | Risk | Fix |
|---|---|---|
| Long-lived access tokens (days/weeks) | Stolen token stays valid forever | Keep to ≤ 1 hour |
| Storing JWT in `localStorage` | XSS can steal it | Use memory or `httpOnly` cookie |
| Accepting `none` algorithm | Auth bypass — no signature checked | Allowlist algorithms explicitly |
| Not validating `iss` / `aud` | Token from another service accepted | Always validate when multi-service |
| Sensitive data in payload | JWTs are not encrypted — anyone can decode | Never put passwords, PII, or secrets in payload |
| Weak secret (`secret123`) | Brute-forceable signature | Use ≥ 256-bit cryptographically random secret |
| Trusting user-supplied claims | Privilege escalation | Only trust claims from verified token, issued by known issuer |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!