suhail
spoecs
9eafd9f
# JWT Token Schema
**Feature**: 001-auth-security
**Date**: 2026-01-09
## Overview
This document defines the structure and validation rules for JWT tokens used in the authentication system. Tokens are issued by Better Auth on the frontend and verified by the FastAPI backend.
## Token Structure
### Header
```json
{
"alg": "HS256",
"typ": "JWT"
}
```
| Field | Value | Description |
|-------|-------|-------------|
| alg | HS256 | HMAC with SHA-256 algorithm |
| typ | JWT | Token type |
### Payload (Claims)
```json
{
"sub": "1",
"email": "user@example.com",
"iat": 1704801600,
"exp": 1705406400,
"iss": "better-auth"
}
```
| Claim | Type | Required | Description |
|-------|------|----------|-------------|
| sub | string | Yes | Subject - User ID (primary key from users table) |
| email | string | Yes | User's email address |
| iat | integer | Yes | Issued At - Unix timestamp when token was created |
| exp | integer | Yes | Expiration - Unix timestamp when token expires (iat + 604800 seconds = 7 days) |
| iss | string | Yes | Issuer - Always "better-auth" |
### Signature
The signature is created by:
1. Encoding the header and payload as Base64URL
2. Concatenating with a period: `{base64Header}.{base64Payload}`
3. Signing with HMAC-SHA256 using BETTER_AUTH_SECRET
4. Encoding the signature as Base64URL
**Formula**: `HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), BETTER_AUTH_SECRET)`
## Complete Token Format
```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwiaWF0IjoxNzA0ODAxNjAwLCJleHAiOjE3MDU0MDY0MDAsImlzcyI6ImJldHRlci1hdXRoIn0.signature_here
```
**Structure**: `{header}.{payload}.{signature}`
## Validation Rules
### Backend Verification Process
1. **Extract Token**: Get token from `Authorization: Bearer {token}` header
2. **Parse Token**: Split into header, payload, signature
3. **Verify Signature**:
- Recompute signature using BETTER_AUTH_SECRET
- Compare with provided signature
- Reject if signatures don't match
4. **Verify Expiration**:
- Check `exp` claim against current Unix timestamp
- Reject if `exp < current_time`
5. **Verify Required Claims**:
- Ensure `sub`, `email`, `iat`, `exp`, `iss` are present
- Reject if any required claim is missing
6. **Extract User ID**:
- Parse `sub` claim as integer
- Use as authenticated user ID for data filtering
### Validation Checklist
- [ ] Token format is valid (3 parts separated by periods)
- [ ] Header contains correct algorithm (HS256)
- [ ] Signature is valid (matches recomputed signature)
- [ ] Token is not expired (exp > current_time)
- [ ] All required claims are present
- [ ] User ID (sub) is a valid integer
- [ ] Email is a valid email format
## Error Responses
### Missing Token
**HTTP Status**: 401 Unauthorized
```json
{
"detail": "Not authenticated",
"error_code": "TOKEN_MISSING"
}
```
### Invalid Signature
**HTTP Status**: 401 Unauthorized
```json
{
"detail": "Invalid token",
"error_code": "TOKEN_INVALID"
}
```
### Expired Token
**HTTP Status**: 401 Unauthorized
```json
{
"detail": "Token has expired",
"error_code": "TOKEN_EXPIRED"
}
```
### Malformed Token
**HTTP Status**: 401 Unauthorized
```json
{
"detail": "Invalid token format",
"error_code": "TOKEN_MALFORMED"
}
```
### Missing Claims
**HTTP Status**: 401 Unauthorized
```json
{
"detail": "Invalid token payload",
"error_code": "TOKEN_INVALID_PAYLOAD"
}
```
## Security Considerations
### Secret Management
- **BETTER_AUTH_SECRET** must be:
- At least 32 characters long
- Cryptographically random
- Identical in frontend and backend
- Stored in environment variables (never committed to git)
- Rotated periodically in production
### Token Lifetime
- **Expiration**: 7 days (604800 seconds)
- **Rationale**: Balances security with UX (no refresh tokens in this spec)
- **Recommendation**: Implement refresh tokens in future iterations for shorter access token lifetime
### Transport Security
- **HTTPS Required**: Tokens must only be transmitted over HTTPS in production
- **Header Only**: Tokens should never be in URL query parameters
- **httpOnly Cookies**: Frontend stores tokens in httpOnly cookies to prevent XSS
### Attack Mitigation
| Attack | Mitigation |
|--------|------------|
| Token Theft | HTTPS only, httpOnly cookies |
| Token Replay | Short expiration (7 days), HTTPS |
| Signature Forgery | Strong secret (32+ chars), HS256 algorithm |
| XSS | httpOnly cookies, CSP headers |
| CSRF | SameSite cookie attribute, CORS configuration |
## Implementation Examples
### Backend Verification (Python/FastAPI)
```python
import jwt
from datetime import datetime
from fastapi import HTTPException, status
def verify_jwt_token(token: str, secret: str) -> dict:
"""
Verify JWT token and return payload.
Args:
token: JWT token string
secret: BETTER_AUTH_SECRET
Returns:
dict: Token payload with claims
Raises:
HTTPException: 401 if token is invalid or expired
"""
try:
# Verify signature and decode
payload = jwt.decode(
token,
secret,
algorithms=["HS256"],
options={
"verify_signature": True,
"verify_exp": True,
"require": ["sub", "email", "iat", "exp", "iss"]
}
)
# Validate issuer
if payload.get("iss") != "better-auth":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token issuer"
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired",
headers={"WWW-Authenticate": "Bearer"}
)
except jwt.InvalidTokenError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
headers={"WWW-Authenticate": "Bearer"}
)
```
### Frontend Token Inclusion (TypeScript)
```typescript
// Automatically include token in API requests
async function fetchAPI<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const session = await auth() // Better Auth session
const token = session?.token
if (!token) {
throw new Error('Not authenticated')
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
...options.headers,
},
})
if (response.status === 401) {
// Token expired or invalid - redirect to login
window.location.href = '/auth/signin'
throw new Error('Authentication required')
}
return response.json()
}
```
## Testing Checklist
- [ ] Valid token with correct signature is accepted
- [ ] Expired token is rejected with 401
- [ ] Token with invalid signature is rejected with 401
- [ ] Token with missing claims is rejected with 401
- [ ] Token with wrong algorithm is rejected with 401
- [ ] Request without token is rejected with 401
- [ ] Malformed token (not 3 parts) is rejected with 401
- [ ] Token with non-integer user ID is rejected with 401
## Token Lifecycle
```
1. User Sign In
2. Better Auth validates credentials
3. Better Auth creates JWT with user claims
4. Better Auth signs JWT with BETTER_AUTH_SECRET
5. Frontend receives token
6. Frontend stores token in httpOnly cookie
7. Frontend includes token in API requests
8. Backend extracts token from Authorization header
9. Backend verifies signature and expiration
10. Backend extracts user_id from 'sub' claim
11. Backend filters data by user_id
12. Token expires after 7 days
13. User must sign in again
```
## Future Enhancements (Out of Scope)
- Refresh tokens for shorter access token lifetime
- Token revocation/blacklist mechanism
- Multiple device session management
- Token rotation on refresh
- Asymmetric signing (RS256) for microservices