Token Validation
This guide explains how to validate JWTs issued by AuthOS in your backend services.
Why Validate Tokens?
When users authenticate with AuthOS, they receive JWTs that they send to your application. Your backend must validate these tokens to:
- Verify authenticity: Ensure the token was issued by AuthOS
- Check integrity: Confirm the token hasn’t been tampered with
- Validate claims: Verify the token is still valid (not expired, correct audience, etc.)
How RS256 Works
AuthOS uses RS256 (RSA with SHA-256) asymmetric signing for JWTs. This has important implications:
- Private Key: The AuthOS API uses a private RSA key to sign tokens
- Public Key: Your backend uses the public RSA key to verify token signatures
- No Shared Secrets: You don’t need access to any secrets to validate tokens
- Security: Even if your public key is compromised, attackers cannot forge valid tokens
JWKS Endpoint
The platform exposes its public keys via the JWKS (JSON Web Key Set) endpoint:
GET /.well-known/jwks.json
Response example:
{
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "sso-key-2025-01-01",
"n": "base64url-encoded-modulus...",
"e": "base64url-encoded-exponent..."
}
]
}
Key Fields:
kid- Key ID used for key rotationn- RSA public key moduluse- RSA public key exponentalg- Signing algorithm (RS256)use- Key usage (sig = signature)
Validation Process
Follow these steps to validate a JWT:
- Extract the JWT: Get the token from the
Authorizationheader - Decode the header: Extract the
kid(Key ID) from the JWT header without verifying - Fetch JWKS: Get the public keys from
/.well-known/jwks.json(with caching) - Find matching key: Locate the public key with the matching
kid - Verify signature: Use the public key to verify the JWT signature
- Validate claims: Check
exp(expiration),iss(issuer),aud(audience), etc.
Implementation Examples
Node.js with Express
Using express-jwt and jwks-rsa libraries:
import { expressjwt } from 'express-jwt';
import jwksRsa from 'jwks-rsa';
// Configure JWKS client to fetch public keys
const jwksClient = jwksRsa({
cache: true, // Cache keys
rateLimit: true, // Rate limit requests
jwksRequestsPerMinute: 5, // Max 5 JWKS requests per minute
jwksUri: 'https://sso.example.com/.well-known/jwks.json'
});
// Function to get signing key from JWKS
function getKey(header, callback) {
jwksClient.getSigningKey(header.kid, (err, key) => {
if (err) {
return callback(err);
}
const signingKey = key.getPublicKey();
callback(null, signingKey);
});
}
// JWT validation middleware
const requireAuth = expressjwt({
secret: getKey,
algorithms: ['RS256'],
credentialsRequired: true,
getToken: (req) => {
if (req.headers.authorization?.startsWith('Bearer ')) {
return req.headers.authorization.substring(7);
}
return null;
}
});
// Use in your routes
app.get('/api/protected', requireAuth, (req, res) => {
// req.auth contains the decoded JWT claims
const { sub, email, org, service, plan, features } = req.auth;
// Check user permissions based on plan/features
if (!features.includes('api-access')) {
return res.status(403).json({ error: 'API access not enabled' });
}
res.json({ message: `Hello ${email}`, plan, features });
});
Manual Validation (Node.js)
If you prefer manual validation without middleware:
import jwt from 'jsonwebtoken';
import jwksRsa from 'jwks-rsa';
const jwksClient = jwksRsa({
jwksUri: 'https://sso.example.com/.well-known/jwks.json'
});
async function validateToken(token: string) {
try {
// Decode without verifying to get the kid
const decoded = jwt.decode(token, { complete: true });
if (!decoded || !decoded.header.kid) {
throw new Error('Invalid token: missing kid');
}
// Get the public key for this kid
const key = await jwksClient.getSigningKey(decoded.header.kid);
const publicKey = key.getPublicKey();
// Verify and decode the token
const verified = jwt.verify(token, publicKey, {
algorithms: ['RS256']
}) as JWTPayload;
// Optional: Additional claim validation
if (verified.exp && Date.now() >= verified.exp * 1000) {
throw new Error('Token expired');
}
return verified; // Returns the decoded claims
} catch (error) {
console.error('Token validation failed:', error);
throw error;
}
}
// Usage
const claims = await validateToken(req.headers.authorization.split(' ')[1]);
console.log(claims.email, claims.org, claims.service, claims.plan);
// Check user subscription features
if (claims.features?.includes('analytics')) {
// User has access to analytics
}
Python with Flask
import jwt
from jwt import PyJWKClient
from functools import wraps
from flask import request, jsonify
# Configure JWKS client
jwks_client = PyJWKClient('https://sso.example.com/.well-known/jwks.json')
def require_auth(f):
@wraps(f)
def decorated_function(*args, **kwargs):
token = None
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
token = auth_header[7:]
if not token:
return jsonify({'error': 'No token provided'}), 401
try:
# Get signing key from JWKS
signing_key = jwks_client.get_signing_key_from_jwt(token)
# Verify token
data = jwt.decode(
token,
signing_key.key,
algorithms=['RS256']
)
# Attach claims to request
request.user_claims = data
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expired'}), 401
except jwt.InvalidTokenError as e:
return jsonify({'error': 'Invalid token'}), 401
return f(*args, **kwargs)
return decorated_function
@app.route('/api/protected')
@require_auth
def protected():
claims = request.user_claims
return jsonify({
'email': claims['email'],
'org': claims.get('org'),
'service': claims.get('service'),
'plan': claims.get('plan'),
'features': claims.get('features', [])
})
Go with Echo Framework
package main
import (
"context"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwk"
)
var jwksCache jwk.Set
func init() {
// Fetch and cache JWKS
ctx := context.Background()
set, err := jwk.Fetch(ctx, "https://sso.example.com/.well-known/jwks.json")
if err != nil {
panic(err)
}
jwksCache = set
}
func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
tokenString := c.Request().Header.Get("Authorization")
if tokenString == "" || len(tokenString) < 7 {
return c.JSON(401, map[string]string{"error": "No token provided"})
}
tokenString = tokenString[7:] // Remove "Bearer "
// Parse and validate token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Get kid from token header
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, fmt.Errorf("kid not found in token header")
}
// Get key from JWKS
key, found := jwksCache.LookupKeyID(kid)
if !found {
return nil, fmt.Errorf("key not found in JWKS")
}
var pubKey interface{}
if err := key.Raw(&pubKey); err != nil {
return nil, err
}
return pubKey, nil
})
if err != nil || !token.Valid {
return c.JSON(401, map[string]string{"error": "Invalid token"})
}
// Extract claims
if claims, ok := token.Claims.(jwt.MapClaims); ok {
c.Set("user_claims", claims)
}
return next(c)
}
}
func protectedHandler(c echo.Context) error {
claims := c.Get("user_claims").(jwt.MapClaims)
return c.JSON(200, map[string]interface{}{
"email": claims["email"],
"org": claims["org"],
"service": claims["service"],
"plan": claims["plan"],
"features": claims["features"],
})
}
Best Practices
1. Cache the JWKS
Fetch the JWKS once and cache it in memory. Refresh periodically (e.g., every hour) or when a token with an unknown kid is encountered.
// Good: Cache with TTL
const jwksClient = jwksRsa({
cache: true,
cacheMaxAge: 3600000, // 1 hour
jwksUri: 'https://sso.example.com/.well-known/jwks.json'
});
2. Validate All Claims
Beyond signature verification, validate these claims:
// Check expiration
if (claims.exp && Date.now() >= claims.exp * 1000) {
throw new Error('Token expired');
}
// Verify issuer (optional but recommended)
if (claims.iss !== 'https://sso.example.com') {
throw new Error('Invalid issuer');
}
// Check audience if you set one (optional)
if (claims.aud && claims.aud !== 'your-service-id') {
throw new Error('Invalid audience');
}
3. Handle Token Expiration
Access tokens expire after 15 minutes. Your frontend should:
- Store the refresh token securely
- Implement automatic token refresh before expiration
- Handle 401 errors by refreshing the token
4. Rate Limit JWKS Requests
Avoid hitting the JWKS endpoint on every request. Use caching and rate limiting:
const jwksClient = jwksRsa({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5
});
5. Validate Token Context
Check the token type matches your use case:
// For organization admin endpoints
if (!claims.org || claims.service) {
throw new Error('Requires organization admin token');
}
// For end-user endpoints
if (!claims.service) {
throw new Error('Requires service context token');
}
// For platform admin endpoints
if (!claims.is_platform_owner) {
throw new Error('Requires platform owner privileges');
}
6. Feature-Based Access Control
Use the features claim for fine-grained permissions:
function requireFeature(feature: string) {
return (req, res, next) => {
const features = req.auth.features || [];
if (!features.includes(feature)) {
return res.status(403).json({
error: `Feature '${feature}' not enabled`
});
}
next();
};
}
// Usage
app.get('/api/analytics', requireAuth, requireFeature('analytics'), handler);
Troubleshooting
“Invalid signature” errors
- Ensure you’re fetching the correct JWKS URL
- Verify the
kidin the token header matches a key in the JWKS - Check that your JWT library supports RS256
- Confirm JWKS cache is refreshed when keys rotate
“Token expired” errors
- Implement token refresh before expiration
- Check server time synchronization (NTP)
- Verify
expclaim is being validated correctly
“Key not found” errors
- The platform may have rotated keys - refresh your JWKS cache
- Ensure you’re looking up keys by
kid - Verify the JWKS endpoint is accessible
Next Steps
- Authentication Concepts - Learn about JWT types and authentication flows
- Getting Started - Quick start guide
- Authentication API Reference - Complete endpoint documentation