DEV Community

Cover image for Microservices Authentication & Authorization: A Beginner's Guide
Rushikesh Surve
Rushikesh Surve

Posted on

Microservices Authentication & Authorization: A Beginner's Guide

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:

  1. API Gateway: The entry point (The Bouncer).
  2. Auth Service: Handles Login (The ID Issuer).
  3. 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).

Microservice Auth Architecture - Gateway Offloading Pattern


Project Setup

Let's keep it simple. We'll simulate three services in one folder for this demo.

  1. Create a folder and initialize:

    mkdir microservices-auth-demo
    cd microservices-auth-demo
    npm init -y
    
  2. Install Dependencies:

    npm install express jsonwebtoken http-proxy-middleware dotenv
    
  3. Folder Structure:
    Create three folders inside:

    • auth-service
    • gateway
    • custom-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');
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. /auth -> Public (Login)
  2. /custom/public -> Public (No token needed)
  3. /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');
});
Enter fullscreen mode Exit fullscreen mode

Step 3: The Custom Service

The VIP Area

This service has 3 types of routes:

  1. Public: Accessible by anyone (Gateway let it through).
  2. Protected (User): Accessible by any logged-in user.
  3. 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');
});
Enter fullscreen mode Exit fullscreen mode

Step 4: Testing It Out

Open 3 terminal windows:

  1. node auth-service/index.js
  2. node custom-service/index.js
  3. 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!"}
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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":{...}}
Enter fullscreen mode Exit fullscreen mode

Result: Success!

Scenario E: Access Admin (User Role)

curl http://localhost:3000/custom/admin -H "Authorization: Bearer <BOB_TOKEN>"
# Output: {"message":"Forbidden: Admins only"}
Enter fullscreen mode Exit fullscreen mode

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!"}
Enter fullscreen mode Exit fullscreen mode

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)