Hello there! If you've ever felt overwhelmed by the idea of securing a microservices architecture, you are not alone. It's a common stumbling block. In a monolithic app, you just check the session in one place. But when you have 5, 10, or 50 services, how do you make sure only the right people get in?
Today, we're going to build a robust, production-ready authentication flow using Node.js. We'll use a pattern that is both scalable and easy to understand: Gateway Authentication and Service-Level Authorization.
By the end of this tutorial, you'll have a working system with 3 services:
- API Gateway: The entry point (The Bouncer).
- Auth Service: Handles Login (The ID Issuer).
- Custom Service: A demo service with Public, User, and Admin routes.
Let's dive in!
The Architecture
Before we write code, let's visualize what we are building.
We want a central entry point (API Gateway) that handles the "Who are you?" question (Authentication). Once confirmed, it passes the request to the specific service, which handles the "Are you allowed here?" question (Authorization).
Project Setup
Let's keep it simple. We'll simulate three services in one folder for this demo.
-
Create a folder and initialize:
mkdir microservices-auth-demo cd microservices-auth-demo npm init -y -
Install Dependencies:
npm install express jsonwebtoken http-proxy-middleware dotenv -
Folder Structure:
Create three folders inside:auth-servicegatewaycustom-service
Step 1: The Auth Service
The Identity Provider
This service is responsible for validating credentials and issuing tokens.
Create auth-service/index.js:
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
const JWT_SECRET = 'super-secret-key';
// Mock User Database
const users = [
{ id: 1, username: 'alice', password: 'password123', role: 'admin' },
{ id: 2, username: 'bob', password: 'password123', role: 'user' }
];
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username && u.password === password);
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Generate Token with Role
const token = jwt.sign(
{ userId: user.id, role: user.role, username: user.username },
JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token });
});
app.listen(3001, () => {
console.log('Auth Service running on port 3001');
});
Step 2: The API Gateway
The Bouncer
The Gateway intercepts requests. It needs to know which routes are Public and which are Protected.
We want:
-
/auth-> Public (Login) -
/custom/public-> Public (No token needed) -
/custom(everything else) -> Protected (Token required)
Create gateway/index.js:
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const jwt = require('jsonwebtoken');
const app = express();
const JWT_SECRET = 'super-secret-key';
// --- Authentication Middleware ---
const verifyToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
if (!authHeader) {
return res.status(401).json({ message: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
// Pass user info to downstream via Headers
req.headers['x-user-id'] = decoded.userId;
req.headers['x-user-role'] = decoded.role;
next();
} catch (error) {
return res.status(401).json({ message: 'Invalid token' });
}
};
// --- Routes ---
// 1. Auth Service (Public)
app.use('/auth', createProxyMiddleware({
target: 'http://localhost:3001',
changeOrigin: true,
pathRewrite: { '^/auth': '' },
}));
// 2. Custom Service - Public Endpoint
// Note: We place this BEFORE the protected route so it matches first!
app.use('/custom/public', createProxyMiddleware({
target: 'http://localhost:3002',
changeOrigin: true,
// We don't rewrite path here because we want the service to receive '/public'
// But wait, if we forward '/custom/public' to localhost:3002/custom/public,
// the service needs to listen on /custom/public OR we rewrite.
// Let's rewrite so the service just sees '/public'
pathRewrite: { '^/custom': '' },
}));
// 3. Custom Service - Protected Endpoints
// Everything else under /custom requires a token
app.use('/custom', verifyToken, createProxyMiddleware({
target: 'http://localhost:3002',
changeOrigin: true,
pathRewrite: { '^/custom': '' },
}));
app.listen(3000, () => {
console.log('API Gateway running on port 3000');
});
Step 3: The Custom Service
The VIP Area
This service has 3 types of routes:
- Public: Accessible by anyone (Gateway let it through).
- Protected (User): Accessible by any logged-in user.
- Protected (Admin): Accessible only by admins.
Create custom-service/index.js:
const express = require('express');
const app = express();
// --- Authorization Middleware ---
const requireRole = (allowedRole) => {
return (req, res, next) => {
const userRole = req.headers['x-user-role'];
if (userRole !== allowedRole) {
return res.status(403).json({ message: 'Forbidden: Admins only' });
}
next();
};
};
// 1. Public Route
app.get('/public', (req, res) => {
res.json({ message: 'This is a public endpoint. Everyone is welcome!' });
});
// 2. Protected Route (Any Authenticated User)
// If the request reached here via the protected Gateway route,
// it MUST have headers.
app.get('/profile', (req, res) => {
const userId = req.headers['x-user-id'];
const userRole = req.headers['x-user-role'];
res.json({
message: 'This is a protected endpoint.',
user: { id: userId, role: userRole }
});
});
// 3. Protected Route (Admin Only)
app.get('/admin', requireRole('admin'), (req, res) => {
res.json({ message: 'Welcome to the Admin Dashboard!' });
});
app.listen(3002, () => {
console.log('Custom Service running on port 3002');
});
Step 4: Testing It Out
Open 3 terminal windows:
-
node auth-service/index.js -
node custom-service/index.js -
node gateway/index.js
Scenario A: Public Route (No Token)
curl http://localhost:3000/custom/public
# Output: {"message":"This is a public endpoint. Everyone is welcome!"}
Result: Success! Gateway let it through without checking auth.
Scenario B: Protected Route (No Token)
curl http://localhost:3000/custom/profile
# Output: {"message":"No token provided"}
Result: Blocked by Gateway.
Scenario C: Login as 'bob' (User)
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"bob", "password":"password123"}'
# Copy the token...
Scenario D: Access Profile (User Role)
curl http://localhost:3000/custom/profile -H "Authorization: Bearer <BOB_TOKEN>"
# Output: {"message":"This is a protected endpoint.", "user":{...}}
Result: Success!
Scenario E: Access Admin (User Role)
curl http://localhost:3000/custom/admin -H "Authorization: Bearer <BOB_TOKEN>"
# Output: {"message":"Forbidden: Admins only"}
Result: Blocked by Service (AuthZ).
Scenario F: Access Admin (Admin Role)
Login as alice and try again.
curl http://localhost:3000/custom/admin -H "Authorization: Bearer <ALICE_TOKEN>"
# Output: {"message":"Welcome to the Admin Dashboard!"}
Result: Success!
Conclusion
You now have a flexible system. The Gateway handles the heavy lifting of checking tokens for protected routes, while allowing public routes to bypass it. The Services enforce fine-grained permissions.
Happy Coding!

Top comments (0)