A common challenge in modern distributed architectures is coordinating authentication and authorization across heterogeneous backend services.
Consider this realistic scenario:
- Service A (Auth & Identity): Built with Python FastAPI and backed by Microsoft SQL Server (MSSQL). It handles user registration, password hashing, MFA, role management, and login.
- Service B (Shopping Cart Service): Built with Node.js (Express / Fastify) and backed by MySQL. It manages user shopping carts, cart items, quantities, and checkout state.
When a user logs in via FastAPI and subsequently attempts to add an item to their cart or fetch their cart via Node.js (GET /api/v1/cart), how does the Node.js backend validate the user's identity and authorize access without directly querying the MSSQL database or creating brittle coupling?
┌────────────────┐ 1. POST /auth/login
│ │ ──────────────────────────────────────► ┌─────────────────────────┐
│ │ │ FastAPI (Python) │
│ │ ◄────────────────────────────────────── │ Auth Service │
│ │ 2. Returns Access Token (JWT) └───────────┬─────────────┘
│ │ │
│ Client │ MSSQL (Users Table)
│ (Web / App) │
│ │ 3. GET /api/v1/cart + Token
│ │ ──────────────────────────────────────► ┌─────────────────────────┐
│ │ │ Node.js (Express) │
│ │ ◄────────────────────────────────────── │ Cart Service │
└────────────────┘ 4. Returns Cart Data └───────────┬─────────────┘
│
MySQL (Cart Table)
In this technical guide, we will explore all 5 primary architectural patterns to solve this problem, analyze their trade-offs, and implement production-ready code examples in Python and TypeScript.
The Core Architectural Requirements
Before selecting a method, let's establish the key requirements for our distributed cart validation:
- Language & Framework Independence: FastAPI (Python) and Express (Node.js) run in separate processes or containers.
- Database Isolation: The Node.js service should not maintain a direct database connection to MSSQL. Database sharing violates microservice boundary principles and creates schema coupling.
- Low Latency: Cart operations are high-frequency; token validation must not add significant overhead to every HTTP request.
- Prevention of IDOR (Insecure Direct Object Reference): The user identity must be cryptographically verified so a malicious user cannot access another user's cart by simply passing
userId=123.
Method 1: Stateless Asymmetric Cryptographic Tokens (RS256 / Ed25519 JWT + JWKS) ⭐ Recommended
The industry standard for microservices is Asymmetric JSON Web Tokens (RS256 or Ed25519) paired with a JSON Web Key Set (JWKS) endpoint.
How It Works
- FastAPI (Auth Service) holds a Private Key (RSA or ECDSA). Upon successful login against MSSQL, FastAPI signs an access token containing claims (
sub: user_id,email,roles,exp). - FastAPI exposes a public endpoint:
GET /.well-known/jwks.json, which serves only the Public Key. - Node.js (Cart Service) downloads and caches the public key in-memory.
- When a cart request arrives, Node.js cryptographically verifies the token signature using the cached public key—completely offline without any network hop or database lookup.
- The extracted
sub(User ID) is used directly in MySQL queries:SELECT * FROM carts WHERE user_id = ?.
┌────────┐ ┌───────────────────┐ ┌───────────────────┐
│ Client │ │ FastAPI (Auth) │ │ Node.js (Cart) │
└───┬────┘ └─────────┬─────────┘ └─────────┬─────────┘
│ 1. Login (MSSQL) │ │
│─────────────────────────────►│ │
│ 2. Sign with Private Key │ │
│ 3. Return RS256 JWT │ │
│◄─────────────────────────────│ │
│ │
│ 4. Fetch & Cache Public Key │
│ │◄─────────────────────────────────│
│ │ (GET /.well-known/jwks.json) │
│ │─────────────────────────────────►│
│ │
│ 5. GET /cart + Bearer JWT │
│────────────────────────────────────────────────────────────────►│
│ │ 6. Verify RS256 in memory
│ │ 7. Query MySQL with sub (user_id)
│ 8. 200 OK (Cart items) │
│◄────────────────────────────────────────────────────────────────│
Implementation: FastAPI Auth Service (Python)
# auth_service/main.py
import time
from datetime import datetime, timedelta, timezone
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import jwt
from fastapi import FastAPI, HTTPException, Depends, status
from pydantic import BaseModel
app = FastAPI(title="Auth Service (FastAPI + MSSQL)")
# Generate or load RSA 2048-bit Keypair (In production, load from Vault/AWS Secrets Manager)
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
).decode("utf-8")
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode("utf-8")
class LoginRequest(BaseModel):
email: str
password: str
@app.get("/.well-known/jwks.json")
def get_public_key():
"""Exposes the public key so other microservices can verify tokens."""
return {"public_key": public_pem, "alg": "RS256"}
@app.post("/api/v1/auth/login")
def login(credentials: LoginRequest):
# 1. Query MSSQL database for user and verify password hash (e.g. bcrypt / Argon2)
# mock_user = query_mssql_user(credentials.email)
user_id = 1042 # Retrieved from MSSQL Primary Key
user_email = credentials.email
# 2. Issue short-lived access token signed with PRIVATE key
now = datetime.now(timezone.utc)
payload = {
"sub": str(user_id),
"email": user_email,
"roles": ["customer"],
"iat": int(now.timestamp()),
"exp": int((now + timedelta(minutes=15)).timestamp()),
"iss": "auth.mystore.com",
"aud": "api.mystore.com"
}
access_token = jwt.encode(payload, private_pem, algorithm="RS256")
return {"access_token": access_token, "token_type": "bearer", "expires_in": 900}
Implementation: Node.js Cart Service (Express + TypeScript)
// cart-service/src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt, { JwtPayload } from 'jsonwebtoken';
import axios from 'axios';
// Extend Express Request to attach authenticated user identity
export interface AuthenticatedRequest extends Request {
user?: {
userId: number;
email: string;
roles: string[];
};
}
let cachedPublicKey: string | null = null;
let lastFetchTime = 0;
const CACHE_TTL_MS = 60 * 60 * 1000; // Cache for 1 hour
async function getPublicKey(): Promise<string> {
const now = Date.now();
if (cachedPublicKey && now - lastFetchTime < CACHE_TTL_MS) {
return cachedPublicKey;
}
const response = await axios.get<{ public_key: string }>(
process.env.AUTH_SERVICE_JWKS_URL || 'http://auth-service:8000/.well-known/jwks.json',
{ timeout: 3000 }
);
cachedPublicKey = response.data.public_key;
lastFetchTime = now;
return cachedPublicKey;
}
export async function authenticateToken(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Format: Bearer <TOKEN>
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const publicKey = await getPublicKey();
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'auth.mystore.com',
audience: 'api.mystore.com',
}) as JwtPayload;
req.user = {
userId: parseInt(decoded.sub as string, 10),
email: decoded.email as string,
roles: (decoded.roles as string[]) || [],
};
return next();
} catch (err: any) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token has expired' });
}
return res.status(403).json({ error: 'Invalid token signature' });
}
}
Accessing the MySQL Cart Database Safely
// cart-service/src/routes/cart.ts
import { Router, Response } from 'express';
import { authenticateToken, AuthenticatedRequest } from '../middleware/auth';
import mysql from 'mysql2/promise';
const router = Router();
const pool = mysql.createPool(process.env.MYSQL_DATABASE_URL!);
// GET /api/v1/cart - Retrieve current user's cart
router.get('/', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const userId = req.user!.userId; // Extracted securely from verified token
try {
const [rows] = await pool.execute(
`SELECT c.item_id, c.quantity, c.price, c.created_at
FROM carts c
WHERE c.user_id = ?`,
[userId]
);
return res.json({ userId, items: rows });
} catch (error) {
console.error('MySQL query error:', error);
return res.status(500).json({ error: 'Failed to retrieve cart' });
}
});
// POST /api/v1/cart/items - Add item to cart
router.post('/items', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const userId = req.user!.userId;
const { itemId, quantity, price } = req.body;
if (!itemId || !quantity || quantity <= 0) {
return res.status(400).json({ error: 'Invalid item data' });
}
try {
await pool.execute(
`INSERT INTO carts (user_id, item_id, quantity, price)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)`,
[userId, itemId, quantity, price]
);
return res.status(201).json({ message: 'Item added to cart' });
} catch (error) {
return res.status(500).json({ error: 'Failed to update cart' });
}
});
export default router;
Why Method 1 is the Gold Standard
- Zero Inter-Service Latency: Node.js does not make an HTTP request to FastAPI or MSSQL for each cart item added.
- Asymmetric Security: If the Node.js container is compromised, the attacker only has access to the Public Key. They cannot forge valid tokens for other services.
- High Scalability: Supports unlimited instances of Node.js and FastAPI independently.
Method 2: Symmetric Shared Secret (HS256 JWT)
If you run a small-to-medium deployment and want the simplest setup without public/private key infrastructure, both services can share a cryptographic secret key (JWT_SECRET_KEY).
Architecture
FastAPI (Signs with Secret) ──► JWT ──► Node.js (Verifies with Same Secret)
- Store
JWT_SECRET_KEY=super_secure_random_string_64_bytesin a shared environment variable or secrets manager. - FastAPI generates token:
jwt.encode(payload, SECRET, algorithm="HS256"). - Node.js validates token:
jwt.verify(token, SECRET, { algorithms: ["HS256"] }).
Node.js Implementation
import jwt from 'jsonwebtoken';
const SECRET_KEY = process.env.JWT_SECRET_KEY!;
export function authenticateHS256(req: AuthenticatedRequest, res: Response, next: NextFunction) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token provided' });
try {
const payload = jwt.verify(token, SECRET_KEY, { algorithms: ['HS256'] }) as jwt.JwtPayload;
req.user = {
userId: Number(payload.sub),
email: payload.email,
roles: payload.roles || []
};
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
Trade-offs
- ✅ Pros: Very fast, trivial to configure, minimal dependencies.
- ❌ Cons: Secret proliferation. Any service holding the symmetric secret can sign arbitrary tokens as any user.
Method 3: Centralized Distributed Session Store (Redis)
When your application requires instant token revocation (e.g., user clicks "Log Out", changes their password in MSSQL, or an admin bans the user), pure stateless JWTs have a flaw: they remain valid until their expiration timestamp (exp).
A centralized Redis cluster bridges FastAPI and Node.js for real-time session tracking.
Architecture
┌────────┐ 1. Login ┌──────────────────┐ 2. Write Session
│ Client │ ──────────────────► │ FastAPI (MSSQL) │ ──────────────────────┐
└────┬───┘ └──────────────────┘ ▼
│ 3. Returns Session ID / Token ┌──────────────┐
│ │ Redis │
│ 4. GET /cart + Token ┌──────────────────┐ 5. Lookup │ (Shared TTL) │
└───────────────────────► │ Node.js (MySQL) │ ─────────────► └──────────────┘
└──────────────────┘ Session Valid?
How It Works
- FastAPI validates credentials against MSSQL.
- FastAPI creates a random UUID session token (or JWT with
jti) and saves it in Redis:SET session:9f8e7d6c-5b4a... '{"user_id": 1042, "role": "customer"}' EX 3600 - Node.js receives the Bearer token in the cart request, performs an atomic Redis
GET session:<token>(taking< 1ms), and retrieves theuser_id.
Node.js Redis Middleware Example
// cart-service/src/middleware/redisAuth.ts
import { Request, Response, NextFunction } from 'express';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
export async function validateRedisSession(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Session token required' });
try {
const sessionData = await redis.get(`session:${token}`);
if (!sessionData) {
return res.status(401).json({ error: 'Session expired or invalidated' });
}
const session = JSON.parse(sessionData);
req.user = {
userId: session.user_id,
email: session.email,
roles: session.roles
};
return next();
} catch (err) {
console.error('Redis session lookup error:', err);
return res.status(500).json({ error: 'Authentication service unavailable' });
}
}
Trade-offs
- ✅ Pros: Immediate session invalidation, easy sliding expiration (
EXPIRE session:id 3600on activity). - ❌ Cons: Requires managing a high-availability Redis instance; sub-millisecond network hop per request.
Method 4: Synchronous Token Introspection (HTTP / gRPC)
In this pattern, Node.js delegates validation back to FastAPI over HTTP or gRPC on every request.
Client ──(GET /cart)──► Node.js ──(POST /auth/introspect)──► FastAPI (MSSQL/Cache)
│ │
└───────◄ (200 OK: { user_id: 1042 }) ──┘
Node.js Introspection Middleware with Local In-Memory Caching
To avoid overwhelming FastAPI, Node.js should cache valid responses for 30–60 seconds:
import { Request, Response, NextFunction } from 'express';
import axios from 'axios';
import { LRUCache } from 'lru-cache';
const tokenCache = new LRUCache<string, { userId: number; email: string }>({
max: 10000,
ttl: 1000 * 60, // Cache validation for 60 seconds
});
export async function introspectToken(req: AuthenticatedRequest, res: Response, next: NextFunction) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Token missing' });
// 1. Check local Node.js memory cache
const cached = tokenCache.get(token);
if (cached) {
req.user = { userId: cached.userId, email: cached.email, roles: [] };
return next();
}
// 2. Call FastAPI Auth Introspection endpoint
try {
const response = await axios.post(
'http://auth-service:8000/api/v1/auth/introspect',
{ token },
{ timeout: 1500 }
);
if (!response.data.active) {
return res.status(401).json({ error: 'Token is inactive or invalid' });
}
const userData = {
userId: response.data.user_id,
email: response.data.email,
};
// Save to local cache
tokenCache.set(token, userData);
req.user = { ...userData, roles: [] };
return next();
} catch (error) {
return res.status(401).json({ error: 'Failed to authenticate token with auth server' });
}
}
Trade-offs
- ✅ Pros: Zero cryptography logic needed in Node.js; FastAPI retains complete authority.
- ❌ Cons: Creates a tight runtime dependency. If FastAPI has an outage or slow MSSQL connection, the Node.js Cart service breaks simultaneously (Cascading Failure).
Method 5: API Gateway Pattern (Reverse Proxy Auth Offloading)
In enterprise microservice architectures, authentication is stripped away from individual backend services and offloaded to an API Gateway (e.g., Kong, Envoy, Traefik, AWS API Gateway, NGINX, or a Next.js BFF layer).
Architecture
┌────────────────────────────────────────────────────────┐
│ API GATEWAY │
│ 1. Verifies Bearer Token │
│ 2. Injects internal headers: │
│ X-User-Id: 1042 │
│ X-User-Roles: customer │
└───────────────┬────────────────────────┬───────────────┘
│ │
Forward /api/v1/auth/* │ │ Forward /api/v1/cart/*
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ FastAPI (Python) │ │ Node.js (Express) │
│ MSSQL Database │ │ MySQL Database │
└────────────────────┘ └────────────────────┘
Node.js Implementation (Gateway Header Trust)
Since the API Gateway guarantees that requests without a valid token are rejected at the edge (HTTP 401), the Node.js Cart service simply reads trusted headers:
// cart-service/src/middleware/gatewayAuth.ts
import { Request, Response, NextFunction } from 'express';
export function authenticateGatewayHeaders(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) {
const userIdHeader = req.headers['x-user-id'];
if (!userIdHeader) {
return res.status(401).json({ error: 'Unauthorized: Missing gateway identity header' });
}
req.user = {
userId: parseInt(userIdHeader as string, 10),
email: (req.headers['x-user-email'] as string) || '',
roles: ((req.headers['x-user-roles'] as string) || '').split(','),
};
return next();
}
Important Security Rule: To use the Gateway pattern safely, the Node.js service must be in a private VPC/network where external public traffic cannot bypass the Gateway and send spoofed
X-User-Idheaders.
Comprehensive Comparison Matrix
| Method | Verification Latency | Coupling | Revocation Speed | Scalability | Complexity |
|---|---|---|---|---|---|
| 1. Asymmetric RS256 + JWKS | < 0.1ms (In-Memory) | Decoupled | On token expiry (or via Redis blacklist) | Highest | Moderate |
| 2. Symmetric HS256 | < 0.1ms (In-Memory) | Shared Secret | On token expiry | High | Lowest |
| 3. Redis Centralized Session | 0.5 - 1ms (Cache Read) | Shared Redis | Instantaneous | Very High | Moderate |
| 4. Token Introspection | 10 - 50ms (HTTP Roundtrip) | High Runtime Coupling | Instantaneous | Low - Medium | Low |
| 5. API Gateway Offloading | 0ms in Service | Zero Auth in Node.js | Managed at Gateway | Highest | High (DevOps) |
Production Security Best Practices
1. The Dual-Token Pattern (Access + Refresh)
Never issue a 30-day access token.
- Issue a 15-minute RS256 Access Token for the client to present to the Node.js Cart service.
- Issue a 7-day Refresh Token stored in an
HttpOnly,Secure,SameSite=Strictcookie handled exclusively by FastAPI and MSSQL. - When the 15-minute access token expires, the frontend silently calls FastAPI's
/api/v1/auth/refreshto obtain a fresh access token without prompting the user to re-login.
2. Preventing IDOR (Insecure Direct Object Reference)
Never allow the client to specify the target userId in the URL parameter or JSON request body when modifying personal cart data:
// ❌ VULNERABLE: Malicious user can wipe someone else's cart by changing body
router.post('/cart/clear', authenticateToken, async (req, res) => {
const { userId } = req.body; // Unsafe!
await db.query('DELETE FROM carts WHERE user_id = ?', [userId]);
});
// ✅ SECURE: Always use the verified identity from the decoded token
router.post('/cart/clear', authenticateToken, async (req: AuthenticatedRequest, res) => {
const userId = req.user!.userId; // Safe!
await db.query('DELETE FROM carts WHERE user_id = ?', [userId]);
});
3. Handling User Deletion / Cascade Integrity
Since MSSQL and MySQL are separate databases, deleting a user in MSSQL will not automatically trigger a SQL Foreign Key cascade in MySQL.
- Use an Event-Driven Message Bus (RabbitMQ, Apache Kafka, or AWS SQS).
- When a user is deleted in FastAPI: publish a
UserDeletedEvent(userId=1042). - Node.js Cart service consumes this event and runs:
DELETE FROM carts WHERE user_id = 1042;.
Summary: Which Approach Should You Choose?
- If you want the industry-standard, high-performance, and loosely coupled solution: Choose Method 1 (Asymmetric RS256 JWT with JWKS).
- If you need instant logout and session revocation across all services: Choose Method 3 (Redis Distributed Sessions).
- If you have an established DevOps infrastructure with a reverse proxy: Choose Method 5 (API Gateway Auth Offloading).
By implementing Asymmetric JWTs or Redis sessions, your Python FastAPI auth layer and Node.js cart service can evolve independently, scale horizontally, and maintain airtight security across both MSSQL and MySQL data stores.