Token Validation

How to validate JWTs issued by AuthOS using RS256, JWKS endpoints, and backend token verification with code examples.

Updated Dec 16, 2025
Edit on GitHub
jwt validation jwks security

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:

  1. Verify authenticity: Ensure the token was issued by AuthOS
  2. Check integrity: Confirm the token hasn’t been tampered with
  3. 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 rotation
  • n - RSA public key modulus
  • e - RSA public key exponent
  • alg - Signing algorithm (RS256)
  • use - Key usage (sig = signature)

Validation Process

Follow these steps to validate a JWT:

  1. Extract the JWT: Get the token from the Authorization header
  2. Decode the header: Extract the kid (Key ID) from the JWT header without verifying
  3. Fetch JWKS: Get the public keys from /.well-known/jwks.json (with caching)
  4. Find matching key: Locate the public key with the matching kid
  5. Verify signature: Use the public key to verify the JWT signature
  6. 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 kid in 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 exp claim 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