Securing Microservice Endpoints With OAuth2 Bearer Tokens

Securing Microservice Endpoints With OAuth2 Bearer Tokens

What You’ll Need

To follow this guide and implement OAuth2 bearer tokens in your microservice architecture, you’ll need:

  • n8n Cloud or self-hosted n8n for workflow orchestration
  • Hetzner VPS or Contabo VPS for hosting your microservices
  • DigitalOcean as an alternative hosting platform
  • Node.js 18+ installed locally for development
  • A code editor like VS Code
  • curl or Postman for testing API endpoints
  • Basic understanding of REST APIs and authentication flows

Table of Contents

Understanding OAuth2 Bearer Tokens

I’ve been working with OAuth2 for years now, and I can tell you that bearer tokens are one of the most practical ways to secure microservice communication. When you have multiple services talking to each other, you need a way to verify that a request is legitimate. OAuth2 bearer tokens solve this elegantly without requiring credentials to be passed around constantly.

Here’s the flow: a client requests a token from your authorization server by providing their credentials. The server validates those credentials and issues an access token with a specific expiration time. The client then includes this token in the Authorization header of every API request using the format “Bearer [token]”. Your microservices validate this token before processing the request.

The beauty of this approach is that it decouples authentication from your actual business logic. Your microservices don’t need to know how to validate passwords. They just need to check if a token is valid and hasn’t expired. This becomes especially important when you’re scaling horizontally and running multiple instances of your services.

Setting Up Your Authorization Server

Let’s build an authorization server from scratch using Node.js and Express. This will be the central authority that issues and validates tokens.

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const cors = require('cors');
require('dotenv').config();

const app = express();
app.use(express.json());
app.use(cors());

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-this-in-production';
const JWT_EXPIRATION = '1h';

const users = [
  {
    id: 1,
    username: 'service-a',
    password: '$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcg7b3XeKeUxWdeS86E36ZXYk1C',
    clientId: 'service-a-client-id',
    clientSecret: 'service-a-client-secret'
  },
  {
    id: 2,
    username: 'service-b',
    password: '$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcg7b3XeKeUxWdeS86E36ZXYk1C',
    clientId: 'service-b-client-id',
    clientSecret: 'service-b-client-secret'
  }
];

const tokenBlacklist = new Set();

app.post('/oauth/token', async (req, res) => {
  const { grant_type, username, password, client_id, client_secret } = req.body;

  if (grant_type !== 'password' && grant_type !== 'client_credentials') {
    return res.status(400).json({
      error: 'invalid_grant',
      error_description: 'Unsupported grant type'
    });
  }

  let user = null;

  if (grant_type === 'password') {
    user = users.find(u => u.username === username);
    if (!user) {
      return res.status(401).json({
        error: 'invalid_credentials',
        error_description: 'Username or password is incorrect'
      });
    }

    const passwordMatch = await bcrypt.compare(password, user.password);
    if (!passwordMatch) {
      return res.status(401).json({
        error: 'invalid_credentials',
        error_description: 'Username or password is incorrect'
      });
    }
  } else if (grant_type === 'client_credentials') {
    user = users.find(u => u.clientId === client_id && u.clientSecret === client_secret);
    if (!user) {
      return res.status(401).json({
        error: 'invalid_client',
        error_description: 'Client ID or secret is incorrect'
      });
    }
  }

  const accessToken = jwt.sign(
    {
      userId: user.id,
      username: user.username,
      clientId: user.clientId,
      type: 'access'
    },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRATION }
  );

  const refreshToken = jwt.sign(
    {
      userId: user.id,
      username: user.username,
      clientId: user.clientId,
      type: 'refresh'
    },
    JWT_SECRET,
    { expiresIn: '7d' }
  );

  res.json({
    access_token: accessToken,
    refresh_token: refreshToken,
    token_type: 'Bearer',
    expires_in: 3600
  });
});

app.post('/oauth/refresh', (req, res) => {
  const { refresh_token } = req.body;

  if (!refresh_token) {
    return res.status(400).json({
      error: 'invalid_request',
      error_description: 'refresh_token is required'
    });
  }

  try {
    const decoded = jwt.verify(refresh_token, JWT_SECRET);

    if (decoded.type !== 'refresh') {
      return res.status(401).json({
        error: 'invalid_token',
        error_description: 'Token is not a refresh token'
      });
    }

    const accessToken = jwt.sign(
      {
        userId: decoded.userId,
        username: decoded.username,
        clientId: decoded.clientId,
        type: 'access'
      },
      JWT_SECRET,
      { expiresIn: JWT_EXPIRATION }
    );

    res.json({
      access_token: accessToken,
      token_type: 'Bearer',
      expires_in: 3600
    });
  } catch (error) {
    return res.status(401).json({
      error: 'invalid_token',
      error_description: 'Refresh token is invalid or expired'
    });
  }
});

app.post('/oauth/revoke', (req, res) => {
  const { token } = req.body;

  if (!token) {
    return res.status(400).json({
      error: 'invalid_request',
      error_description: 'token is required'
    });
  }

  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    tokenBlacklist.add(token);
    res.json({
      success: true,
      message: 'Token revoked successfully'
    });
  } catch (error) {
    res.status(400).json({
      error: 'invalid_token',
      error_description: 'Token is invalid or already expired'
    });
  }
});

app.get('/oauth/validate', (req, res) => {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      valid: false,
      error: 'missing_token'
    });
  }

  const token = authHeader.substring(7);

  if (tokenBlacklist.has(token)) {
    return res.status(401).json({
      valid: false,
      error: 'revoked_token'
    });
  }

  try {
    const decoded = jwt.verify(token, JWT_SECRET);

    if (decoded.type !== 'access') {
      return res.status(401).json({
        valid: false,
        error: 'invalid_token_type'
      });
    }

    res.json({
      valid: true,
      userId: decoded.userId,
      username: decoded.username,
      clientId: decoded.clientId
    });
  } catch (error) {
    res.status(401).json({
      valid: false,
      error: 'invalid_token',
      message: error.message
    });
  }
});

app.listen(3000, () => {
  console.log('Authorization server running on port 3000');
});

This authorization server handles four critical endpoints. The /oauth/token endpoint issues tokens using either password grant (for user credentials) or client credentials grant (for service-to-service communication). The /oauth/refresh endpoint allows clients to get a new access token using their refresh token without re-authenticating. The /oauth/revoke endpoint blacklists tokens so they can’t be used anymore. Finally, the /oauth/validate endpoint is called by your microservices to verify that a bearer token is legitimate.

💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.

Implementing Token Validation in Microservices

Now that you have an authorization server, your microservices need to validate bearer tokens before processing requests. I recommend creating a middleware function that handles this consistently across all your services. This prevents you from rewriting the same validation logic multiple times.

const express = require('express');
const axios = require('axios');
const app = express();

app.use(express.json());

const OAUTH_VALIDATE_URL = process.env.OAUTH_VALIDATE_URL || 'http://localhost:3000/oauth/validate';
const TOKEN_CACHE = new Map();
const CACHE_TTL = 5 * 60 * 1000;

const validateBearerToken = async (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      error: 'unauthorized',
      message: 'Missing or invalid Authorization header'
    });
  }

  const token = authHeader.substring(7);

  try {
    let validationResult = null;

    const cachedResult = TOKEN_CACHE.get(token);
    if (cachedResult && Date.now() - cachedResult.timestamp < CACHE_TTL) {
      validationResult = cachedResult.data;
    } else {
      const response = await axios.get(OAUTH_VALIDATE_URL, {
        headers: {
          Authorization: `Bearer ${token}`
        },
        timeout: 5000
      });

      validationResult = response.data;

      TOKEN_CACHE.set(token, {
        data: validationResult,
        timestamp: Date.now()
      });
    }

    if (!validationResult.valid) {
      return res.status(401).json({
        error: 'unauthorized',
        message: 'Invalid or expired token',
        details: validationResult.error
      });
    }

    req.user = {
      userId: validationResult.userId,
      username: validationResult.username,
      clientId: validationResult.clientId
    };

    next();
  } catch (error) {
    console.error('Token validation error:', error.message);

    res.status(503).json({
      error: 'service_unavailable',
      message: 'Authorization service is unavailable'
    });
  }
};

app.get('/api/protected-resource', validateBearerToken, (req, res) => {
  res.json({
    message: 'This is a protected resource',
    requestedBy: req.user.username,
    userId: req.user.userId
  });
});

app.post('/api/data', validateBearerToken, (req, res) => {
  const { content } = req.body;

  if (!content) {
    return res.status(400).json({
      error: 'bad_request',
      message: 'content field is required'
    });
  }

  res.json({
    success: true,
    message: 'Data received',
    receivedBy: req.user.username,
    contentLength: content.length
  });
});

app.get('/api/user-info', validateBearerToken, (req, res) => {
  res.json({
    userId: req.user.userId,
    username: req.user.username,
    clientId: req.user.clientId
  });
});

app.listen(3001, () => {
  console.log('Microservice running on port 3001');
});

Notice that I’ve added token caching to this middleware. Every validation request going to your authorization server adds latency. By caching valid tokens for a few minutes, you reduce the load on your auth server and make your microservices respond faster. The cache expires based on the TTL, so you’re not keeping invalid tokens around.

When you’re deploying services across multiple servers, you might want to use n8n Cloud to orchestrate your microservice communication. n8n has built-in support for OAuth2, making it easy to authenticate requests between your services without manually managing tokens.

Securing Your Bearer Token Flow

Implementing OAuth2 is just half the battle. You also need to ensure the infrastructure and communication channels are secure. I always recommend running your services behind HTTPS to prevent token interception in transit.

const https = require('https');
const fs = require('fs');
const express = require('express');

const app = express();

const sslOptions = {
  key: fs.readFileSync(process.env.SSL_KEY_PATH || './server.key'),
  cert: fs.readFileSync(process.env.SSL_CERT_PATH || './server.crt')
};

app.use(express.json());

const validateBearerToken = async (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      error: 'unauthorized',
      message: 'Missing or invalid Authorization header'
    });
  }

  const token = authHeader.substring(7);

  if (token.length < 20) {
    return res.status(401).json({
      error: 'unauthorized',
      message: 'Token format is invalid'
    });
  }

  next();
};

const rateLimitMap = new Map();

const rateLimitMiddleware = (req, res, next) => {
  const clientId = req.user?.clientId || req.ip;
  const key = `${clientId}:${req.path}`;
  const now = Date.now();

  if (!rateLimitMap.has(key)) {
    rateLimitMap.set(key, []);
  }

  const timestamps = rateLimitMap.get(key);
  const recentRequests = timestamps.filter(t => now - t < 60000);

  if (recentRequests.length >= 100) {
    return res.status(429).json({
      error: 'too_many_requests',
      message: 'Rate limit exceeded'
    });
  }

  recentRequests.push(now);
  rateLimitMap.set(key, recentRequests);

  next();
};

app.use(rateLimitMiddleware);

app.get('/api/secure-endpoint', validateBearerToken, (req, res) => {
  res.json({
    message: 'Secure endpoint accessed successfully',
    timestamp: new Date().toISOString()
  });
});

https.createServer(sslOptions, app).listen(443, () => {
  console.log('Secure microservice running on HTTPS port 443');
});

I’ve added rate limiting here too. Even with proper authentication, a compromised token could be used to hammer your API with requests. By implementing rate limiting per client, you protect yourself against abuse.

When implementing OAuth2 across distributed systems, consider how you’ll handle token validation during network partitions. If your authorization server goes down temporarily, should your microservices reject all requests or operate in a degraded mode? Some teams use JWT tokens because they can be validated locally without calling the auth server, similar to how implementing HMAC signature verification for inbound webhooks works for webhook authentication.

For managing infrastructure, I recommend hosting your authorization server and microservices on Hetzner VPS or Contabo VPS. Both offer excellent uptime and low latency within Europe. If you need US-based hosting, DigitalOcean provides reliable options with built-in SSL support.

Testing Your OAuth2 Implementation

Let’s test the complete flow using curl. First, get a token from the authorization server.

curl -X POST http://localhost:3000/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "password",
    "username": "service-a",
    "password": "your-password"
  }'

This returns a response like:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Copy the access_token value and use it to make authenticated requests to your microservice.

curl -X GET http://localhost:3001/api/protected-resource \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

If the token is valid, you get:

{
  "message": "This is a protected resource",
  "requestedBy": "service-a",
  "userId": 1
}

Test the refresh token flow to get a new access token without re-authenticating.

curl -X POST http://localhost:3000/oauth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }'

And finally, test token revocation.

curl -X POST http://localhost:3000/oauth/revoke \
  -H "Content-Type: application/json" \
  -d '{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }'

After revocation, attempting to use that token returns an unauthorized response.

If you’re using n8n for orchestrating API calls between services, you can configure OAuth2 authentication at the node level. This eliminates manual token management in your workflows.

Getting Started

Now you have a complete OAuth2 implementation ready to deploy. Here’s what you need to do next:

  1. Set up your authorization server on Hetzner VPS or Contabo VPS.

  2. Configure SSL certificates so all communication is encrypted.

  3. Deploy your microservices with the token validation middleware.

  4. If you haven’t already, set up n8n Cloud to orchestrate requests between services. n8n has built-in OAuth2 support that integrates seamlessly with this setup.

  5. Test thoroughly with curl and monitor your authorization server logs for failed authentication attempts.

  6. Set up proper logging and alerting so you catch token validation failures quickly.

Remember that this is a foundation. As your system grows, you might want to add scopes to your tokens (so different services only get access to what they need), implement token rotation policies, or use certificates instead of shared secrets for service-to-service authentication.

Outsource Your Automation

Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr, mention SYS3-HUGO for priority. Or DM at chasebot.online.

Want to automate this yourself?

Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.

Want this engine running on your own VPS?

This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.

Get System 3
system online