Intermediate

Node.js Microservices Authentication and Authorization

In this Node.js microservices course, we will dive into the crucial aspects of authentication and authorization.

Table of Contents

  1. Course Overview
  2. Module 2 — Setting the stage: microservices security
  1. Module 3 — Do you have something precious? Use the Vault
  1. HashiCorp Consul and East West Traffic
  1. Code files and practical exercises

1. Course Overview

Welcome to this course, Node.js Microservices: Authentication and Authorization. In this Node.js microservices course, we will dive into the crucial aspects of authentication and authorization.

We will explore the intricacies of securing microservices. The main topics covered are:

  • Demystify and implement JSON Web Tokens (JWT) as a security mechanism for Node.js microservices
  • Cut the code into separate services to align with OAuth 2 grants
  • North-South traffic in the cloud: harness the power of Amazon Cognito and Amazon API Gateway to secure this traffic with JWT tokens
  • Hands-on Demo: Integrate the Cognito Identity Provider for Authentication, So Microservices Are More Secure and Reliable
  • Module 3: Use HashiCorp Vault to securely store and share secrets
  • Module 4: secure East-West traffic with HashiCorp Consul — automatically use certificates and establish control via a service mesh

Our objective focuses on two essential traffic patterns: North-South and East-West. By the end of this course, you will have a solid grasp and practical experience in securing these two patterns, ensuring the robustness of your microservices architecture.

Versions used:

  • Node.js version 21 and later (tested with v21 and v22)
  • HashiCorp Vault version 1.15 and later (tested from 1.10 to 1.15)
  • HashiCorp Consul version 1.17 and later

2. Setting the Stage: Microservices Security

Total module duration: 1h 16m 45s

In this module, we explore the dynamic landscape of microservices security, driven by ever-changing business needs. We will review a concrete business scenario for the Globomantics company, then we will travel through the North-South and East-West flows to ensure airtight protection.

2.1 Pressing need for security for microservices

Globomantics faces a pressing business problem: The company recently acquired a promising startup and needs to implement authentication and authorization for that startup’s microservices applications. The startup uses the same technology stack as the parent company, but has neither the resources nor the budget to implement these security mechanisms.

The situation is critical because:

  • Startup’s customer base continues to grow rapidly, increasing risk of security breach
  • The startup has successfully transitioned from a monolith to a microservices architecture and is reaping all the benefits, but is struggling to modernize the security of its applications
  • The operational burden of adopting this architecture is difficult to manage

Highly qualified consultants recommend that Globomantics secure both North-South traffic, East-West traffic, as well as data at rest, to ensure full compliance with US regulations and potentially European GDPR.

2.2 Securing North-South and East-West traffic

North-South Traffic

The typical implementation for securing North-South traffic is to use an API Gateway. The three main cloud providers (Amazon, Microsoft, Google) each offer one. The basic functionality of an API Gateway includes:

  • Expose endpoints and control traffic
  • Authenticate users and applications
  • Issue tokens, refresh tokens, expire tokens
  • Check API keys
  • Apply throttling
  • Manage metering for invoicing

For our demos, we will use the Amazon API Gateway on AWS, which offers:

  • Efficient API development
  • Performance and savings at scale
  • Easy monitoring
  • Flexible security controls including IAM and OIDC/OAuth 2 support

East-West traffic

East-West traffic includes service-to-service communications behind the API Gateway. This traffic is normally secured via:

  1. JWT tokens (jot tokens) — which we will see in detail
  2. Mutual TLS (mTLS) — which requires public and private certificates on each end for services to communicate securely

E-commerce example: the cart service must talk to the inventory service securely — both services need certificates to establish a strong identity.

Zero Trust Concept

Securing both North-South and East-West traffic is the basis of the concept commonly called zero trust, which emerged directly as a result of the decomposition of monolithic applications into microservices.

Brief history:

  • The monolith era: services ran and communicated in the same process, secured via a session ID generated by the application server upon connection. The services operated in the same application context, so there was no need to secure service-to-service communications.

  • The advent of RESTful microservices: each logical component is deployed separately and communicates via HTTP. This gain in development and deployment flexibility (components in parallel) was counterbalanced by an explosion in operational complexity. Traffic between these network components must be secure.

  • The Secrets Management Problem: With tens, hundreds, or even thousands of microservices deployed, each requiring a different security mechanism, centralized secrets management has become a major challenge.

Solution: HashiCorp Vault (module 3) for centralized secrets management, and HashiCorp Consul (module 4) to secure East-West traffic at scale.

2.3 The main players in authentication and authorization

In OAuth 2 scenarios, four main entities interact. They can be deployed on separate VMs or in different containers:

ActorRole
Resource OwnerMost often the end user, who can grant access to protected resources
Client (Application)Requests access to protected resources on behalf of the resource owner
Authorization ServerAuthenticates the resource owner and issues access tokens
Resource ServerHosts the owner’s resources and serves them after successful authorization

OAuth 2 flows (Grant Types)

1. Authorization Code Grant — The most complex but also the most secure, recommended for microservices/APIs:

  1. User calls a client/app that needs permissions
  2. The app contacts the authorization server
  3. Browser redirect to authorization page
  4. User authenticates and approves or denies
  5. The authorization server issues a short-lived authorization code
  6. The client submits this code to the authorization server endpoint token
  7. The authorization server checks the code and issues an access token
  8. The client accesses the resource server with the access token
  9. The resource server submits the access token to the introspection endpoint for verification
  10. The resource server returns the resource if the check is confirmed

Reference: RFC 6749

2. Implicit Grant: similar to the Authorization Code but without an intermediate code step — the client directly receives an access token.

3. Resource Owner Password Credentials: the end user authenticates directly with the client (and not with the authorization server). The credentials are exposed to the app. Use only if the customer is completely trusted.

4. Client Credentials Grant: The end user is missing from the flow. The client authenticates itself with a client_id and a client_secret. Ideal for machine-to-machine communications.

Grant Type Selection Guide

Customer scenarioGrant recommended
Traditional web application (server)Authorization Code
Single-Page Application (SPA)Authorization Code + PKCE
SPA without the need for an access tokenImplicit Grant with Form Post
Client = Resource OwnerCustomer Credentials
Totally trusted customer with credentialsResource Owner Password Credentials

PKCE (Proof Key for Code Exchange, pronounced “pixie”): Countermeasure against authorization code interception attacks on mobile devices.

The refresh token is not a real grant, but an increase in the Authorization Code grant to obtain a new access token more easily.

2.4 All about JWTs

Purpose: Authorization only, not authentication

  • Authentication: check that the username and password are correct
  • Authorization: check that you have already been authenticated AND assign the permissions (actions) that you can perform on the resources

Authorization evolution

Old approach (session-based):

  1. The user presents his credentials
  2. The application server generates a session ID and stores it in memory
  3. The ID is passed in a cookie on each request
  4. The server consults its memory to find the session ID
  5. Problem: If the server restarts, all session IDs are lost

Modern approach (JWT):

  1. Authentication happens in the same way
  2. The authorization server (not just the app server) creates (mint) a token, encodes it and signs it with its own secret private key — which prevents tampering
  3. The server does not need to store the token — this is the fundamental difference
  4. The token can be used with multiple servers
  5. No more need for sticky sessions with a load balancer

Structure of a JWT

A JWT is composed of three parts separated by periods (.):

<header encodé en Base64>.<payload encodé en Base64>.<signature>

Header:

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload (example):

{
  "sub": "user_id_ou_username",
  "name": "John Doe",
  "iat": 1700265511,
  "exp": 1700265541
}
  • sub: subject (usually the user ID)
  • name: name of the natural person
  • iat: issued at (issue time)
  • exp: expiration

Signature: The authorization server combines the Base64 encoded header and payload and signs them with its private key. If the header or payload is falsified, the token becomes invalid immediately.

The signing process is similar to password hashing, but instead of a single password, it combines two JSON strings and uses a private key to generate the hash.

JWT and Single Sign-On (SSO)

With JWTs, it is possible to satisfy the need for SSO. When Globomantics acquires new companies, users can navigate through all these applications by logging in only once.

Decoding tool: jwt.io

2.5 Demo: Initial Project Setup

Objective of the two-part demo

Part 1 — Simple JWT:

  • Create tokens
  • Send tokens to users
  • Check these tokens on our server

Part 2 — Refresh tokens:

  • Automatically refresh JWT tokens (better security)
  • Revoke access (similar to a logout function)

Initializing the Node.js project

# Initialiser le projet npm
npm init -y

# Installer les dépendances
npm install express jsonwebtoken dotenv

# Installer la dépendance de développement (nodemon)
npm install --save-dev nodemon

# Installer bcrypt pour le hachage des mots de passe
npm install bcrypt

Configuring package.json

{
  "scripts": {
    "devStartResource": "nodemon resource-server.js",
    "devStartAuth": "nodemon auth-server.js"
  }
}

.env file — Generation of secrets

# Dans le terminal Node.js
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

You must run this command twice to get two different values ​​— one for ACCESS_TOKEN_SECRET and one for REFRESH_TOKEN_SECRET.

ACCESS_TOKEN_SECRET=9c30c2370813582c4d57a51bddd06d2e...
REFRESH_TOKEN_SECRET=dad5e0dd79d305de04146a390c9933e7...

Important: Having completely different values ​​for these two secrets ensures that the tokens are signed correctly.

2.6 Demo: Creating and testing routes

Express Server Basic Structure

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

app.use(express.json())

const orders = [
    {
        id: '1',
        uid: 'gsmith',
        items: [
            {
                name: 'HFS - Lightweight Road Running',
                single_price: '105.20',
                currency: 'USD',
                count: '2'
            }
        ],
        order_total: '210.40'
    },
    {
        id: '2',
        uid: 'rcharles',
        items: [
            {
                name: 'Flying Spider Shoe',
                single_price: '115.20',
                currency: 'USD',
                count: '2'
            }
        ],
        order_total: '230.40'
    }
]

app.get('/orders', (request, response) => {
    response.json(orders)
})

app.listen(3000)

REST requests file (app_requests_1.rest)

To test endpoints without leaving VS Code, we use the REST Client extension:

GET http://localhost:3000/orders

API Testing Tools Note:

  • REST Client (VS Code): ideal for quickly playing with an API without changing context
  • Postman: free enterprise-grade platform, ideal for large collections, team sharing, automation, and conversion of HTTP requests to cURL

2.7 Authenticate and create an access token

User management with bcrypt hashing

const users = [
    { uid: 'gsmith', pwd: '$2b$10$ZlAnB2ouP...' },
    { uid: 'rcharles', pwd: '$2b$10$xcTiAcwt...' },
    { uid: 'jdoe', pwd: '$2b$10$xcTiAcwt...' },
    { uid: 'sjackson', pwd: '$2b$10$BHv1DaTCl...' }
]

// Endpoint GET /users
app.get('/users', (request, response) => {
    response.json(users)
})

// Endpoint POST /users - Création d'un utilisateur avec mot de passe haché
app.post('/users', async (request, response) => {
    try {
        const hashedPwd = await bcrypt.hash(request.body.pwd, 10)
        const user = { uid: request.body.uid, pwd: hashedPwd }
        users.push(user)
        response.status(201).send()
    } catch {
        response.status(500).send()
    }
})

Why bcrypt with Salt? If we apply a simple hash function, a hacker can potentially crack the same password if it belongs to multiple users, thus compromising other accounts. The salt (random string added at the beginning of the password) is different for each user, which ensures that all hashed passwords are different, even if multiple users have the exact same password. Warning: Increasing the cost factor (10 by default) to a value like 30 may take a very long time to generate a single hash.

Login endpoint with JWT creation

const jwt = require('jsonwebtoken')
require('dotenv').config()

app.post('/login', async (request, response) => {
    // Vérifier si l'utilisateur existe
    const user = users.find(user => user.uid == request.body.uid)
    if (user == null) {
        return response.status(400).send('User NOT found!!!')
    }
    try {
        if (await bcrypt.compare(request.body.pwd, user.pwd)) {
            const uid = request.body.uid
            const jwtUser = { uid: uid }
            // Créer et signer le JWT
            const accessToken = jwt.sign(jwtUser, process.env.ACCESS_TOKEN_SECRET)
            response.json({ accessToken: accessToken })
        } else {
            response.send("Access denied.")
        }
    } catch {
        response.status(500).send()
    }
})

For application authentication (machine-to-machine), the uid and password are replaced by client_id and client_secret, often called service account. The client_id and client_secret serve the same purpose but are usually as random strings readable by program.

2.8 Authorize requests with JWT tokens

Token verification middleware

function verifyToken(request, response, next) {
    const authzHeader = request.headers['authorization']
    const accessToken = authzHeader && authzHeader.split(' ')[1]
    
    if (accessToken == null) {
        return response.status(401).send()
    }
    
    jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET, (error, user) => {
        if (error) {
            console.log(error)
            return response.status(403).send()
            // 403 = Je vois que tu as un token, mais il n'est plus valide
        } else {
            request.user = user
            next()
        }
    })
}

Endpoint /orders protected by middleware

app.get('/orders', verifyToken, (request, response) => {
    // Retourner uniquement les commandes de l'utilisateur connecté
    response.json(orders.filter(order => order.uid === request.user.uid))
})

Testing REST requests

# 1. Se connecter pour obtenir un token
POST http://localhost:3001/login
Content-Type: application/json

{
    "uid": "gsmith",
    "pwd": "echo3"
}

###

# 2. Accéder aux ressources avec le token
GET http://localhost:3000/orders
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Expected results:

  • If logged in as gsmith → only gsmith command
  • If logged in as rcharles → only rcharles command
  • If logged in as jdoe (without command) → empty array

2.9 Test the same access token on multiple servers

Demonstration with two server instances

{
  "scripts": {
    "devStartResource": "nodemon resource-server.js",
    "devStartAuth": "nodemon auth-server.js"
  }
}
# Démarrer les deux serveurs
npm run devStartResource  # Port 3000
npm run devStartAuth      # Port 3001

Both servers share the same ACCESS_TOKEN_SECRET. A token generated by any server is accepted by both — it doesn’t matter which one processes the request, because tokens contain all the necessary information.

Advantages vs Session-based Authentication

With session, in a round-robin load balancer scenario, a user’s session is tied to a particular server instance. If the next request goes to another instance, the user must log in again.

Workarounds with sessions:

  • Sticky session on the load balancer: a user’s requests are directed to the instance that contains their session
  • Clustering with session replication (e.g. JGroups for inter-cluster communications)

These approaches come at a cost in terms of complexity and increased network traffic.

JWT makes server instances truly stateless → more suitable for containerization and microservices architecture.

2.10 Refresh tokens: a new start

Separation of responsibilities

Refresh tokens allow authentication operations to be separated into a separate server:

  • Auth Server (auth-server.js, port 3001): manages the creation, deletion and refreshing of tokens
  • Resource Server (resource-server.js, port 3000): strictly manages CRUD operations on resources (orders)

Why refresh tokens?

Without expiration, anyone who owns a token has access indefinitely. If a hacker gets hold of the token, he can make requests indefinitely.

Best practices for tokens:

  • Keep them secret and secure
  • Avoid adding sensitive data to payload
  • Ensure tokens expire
  • Use HTTPS
  • Map all authorization use cases
  • Store refresh tokens in memory or in an HTTP-only cookie (not accessible via JavaScript)

Reasons to use refresh tokens:

  1. Short tokens: access tokens can be of very short duration (eg: 10 seconds in demo, a few minutes in reality). If hacked, access is limited in time.
  2. Store in memory: not in localStorage nor in a cookie. If accessible via JavaScript, a hacker can also recover it.
  3. Revocation: invalidate a refresh token = create a logout route which removes it from the list of valid tokens.
  4. Scalability: separating the auth server allows it to be scaled independently, especially when Globomantics acquires new applications.

Implementation of the mintAccessToken function

// Crée et retourne un access token
function mintAccessToken(user) {
    return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '10s' })
}

Login endpoint with refresh token

let refreshTokens = []

app.post('/login', async (request, response) => {
    const user = users.find(user => user.uid == request.body.uid)
    if (user == null) {
        return response.status(400).send('User NOT found!!!')
    }
    try {
        if (await bcrypt.compare(request.body.pwd, user.pwd)) {
            const jwtUser = { uid: request.body.uid }
            const accessToken = mintAccessToken(user)
            // Le refresh token n'a pas de date d'expiration (gestion manuelle)
            const refreshToken = jwt.sign(user, process.env.REFRESH_TOKEN_SECRET)
            refreshTokens.push(refreshToken)
            response.json({ accessToken: accessToken, refreshToken: refreshToken })
        } else {
            response.send("Access denied.")
        }
    } catch {
        response.status(500).send()
    }
})

Endpoint /token — Generate a new access token

app.post('/token', (request, response) => {
    const refreshToken = request.body.refresh_token
    if (refreshToken == null) {
        return response.sendStatus(401)
    }
    if (!refreshTokens.includes(refreshToken)) {
        return response.sendStatus(403)
    }
    jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (error, user) => {
        if (error) {
            return response.sendStatus(403)
        }
        const accessToken = mintAccessToken({ uid: user.uid })
        response.json({ accessToken: accessToken })
    })
})

REST requests to test tokens

# Connexion
POST http://localhost:3001/login
Content-Type: application/json

{
    "uid": "gsmith",
    "pwd": "echo3"
}

###

# Rafraîchir le token
POST http://localhost:3001/token
Content-Type: application/json

{
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

###

# Accéder aux ressources
GET http://localhost:3000/orders
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

2.11 Check that the refresh token is working correctly

Endpoint /logout — Decommission a refresh token

app.delete('/logout', (request, response) => {
    refreshTokens = refreshTokens.filter(token => token !== request.body.refresh_token)
    response.sendStatus(204)
})

The deletion logic depends on your storage choice:

  • In memory (array) → filter as above
  • In database → execute a DELETE query
  • In Redis cache → corresponding API call

DELETE request for logout

DELETE http://localhost:3001/logout
Content-Type: application/json

{
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Test scenario:

  1. Log in → get accessToken + refreshToken
  2. Use refreshToken → get new accessToken
  3. Perform a DELETE /logout with the refreshToken
  4. Retry the refreshToken403 Forbidden because it was invalidated ✓

2.12 Deploy microservices in the cloud

Introduction to Terraform

Terraform is used to automate infrastructure provisioning. Advantages:

  • Multicloud (AWS, Azure, Google Cloud, etc.)
  • Providers are plugins that translate Terraform commands into invisible API calls to the respective cloud
  • Infrastructure as Code (IaC)

Tooling Installation

# Installer Terraform
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

# Installer l'AWS CLI sur macOS
brew install awscli

# Sur Windows (avec Chocolatey)
choco install terraform
choco install awscli

# Sur Linux (Ubuntu)
sudo apt-get install terraform awscli

AWS Configuration

# Configurer les credentials AWS
aws configure
# Fournir : AWS Access Key ID, AWS Secret Access Key, région, format

Workflow Terraform

# Initialiser le projet (télécharge les plugins)
terraform init

# Voir le plan d'infrastructure
terraform plan

# Appliquer l'infrastructure
terraform apply

# Nettoyer l'état Terraform
rm .terraform.lock.hcl terraform.tfstate terraform.tfstate.backup
rm -rf .terraform/

File provider.tf

terraform {
  required_providers {
    aws = {
        source = "hashicorp/aws"
    }
  }
}

provider "aws" {
  region = "us-west-2"
  shared_credentials_files = ["/Users/gsmith13/.aws/credentials"]
  shared_config_files = ["/Users/gsmith13/.aws/config"]
  alias = "region"
}

File 01_cognito.tf — Amazon Cognito User Pool

# 1. Cognito User Pool
resource "aws_cognito_user_pool" "ecomm_user_pool" {
  name = "ecomm_user_pool"
  deletion_protection = "INACTIVE"
  mfa_configuration   = "OFF"

  schema {
    name                     = "email"
    attribute_data_type      = "String"
    mutable                  = false
    developer_only_attribute = false
    required                 = true

    string_attribute_constraints {
      min_length = 1
      max_length = 100
    }
  }
  
  email_configuration {
    email_sending_account = "COGNITO_DEFAULT"
  }

  auto_verified_attributes = ["email"]

  password_policy {
    require_numbers    = true
    require_symbols    = true
    require_uppercase  = true
    require_lowercase  = true
    minimum_length     = 6
  }

  account_recovery_setting {
    recovery_mechanism {
      name     = "verified_email"
      priority = 1
    }
  }
}

# 2. Cognito User Pool Client
resource "aws_cognito_user_pool_client" "ecomm_user_pool_client" {
  name                    = "ecomm_user_pool_client"
  user_pool_id            = aws_cognito_user_pool.ecomm_user_pool.id
  explicit_auth_flows     = [
    "ALLOW_USER_SRP_AUTH",
    "ALLOW_REFRESH_TOKEN_AUTH",
    "ALLOW_USER_PASSWORD_AUTH"
  ]
  allowed_oauth_flows_user_pool_client = true
  allowed_oauth_flows                  = ["code", "implicit"]
  allowed_oauth_scopes                 = [
    "email", "openid", "phone", "profile", "aws.cognito.signin.user.admin"
  ]
  callback_urls = ["https://example.com/callback"]
  logout_urls   = ["https://example.com/signout"]
  
  generate_secret                  = true
  prevent_user_existence_errors    = "LEGACY"
  id_token_validity                = 1
  access_token_validity            = 1
  refresh_token_validity           = 1
  token_validity_units {
    id_token      = "hours"
    refresh_token = "hours"
    access_token  = "hours"
  }
  supported_identity_providers = ["COGNITO"]
}

# 3. Domaine Cognito pour l'app client
resource "aws_cognito_user_pool_domain" "admin_cognito_domain" { 
  domain       = "bad-temper"
  user_pool_id = "${aws_cognito_user_pool.ecomm_user_pool.id}" 
}

# 4. Utilisateur Cognito de test
resource "aws_cognito_user" "sample_cognito_user" {
  user_pool_id = aws_cognito_user_pool.ecomm_user_pool.id
  username     = "gsmithpluralsight@gmail.com"

  attributes = {
    email          = "gsmithpluralsight@gmail.com"
    email_verified = true
    family_name    = "Smith"
  }
}

File 02_apigateway.tf — API Gateway with Cognito authorizer

# 5. API Gateway REST API
resource "aws_api_gateway_rest_api" "ecomm_api" {
  name        = "ecomm_api"
  description = "Ecommerce API"
}

# 6. Authorizer Cognito
resource "aws_api_gateway_authorizer" "ecomm_api_authorizer" {
  name                             = "ecomm_api_authorizer"
  rest_api_id                      = aws_api_gateway_rest_api.ecomm_api.id
  type                             = "COGNITO_USER_POOLS"
  provider_arns                    = [aws_cognito_user_pool.ecomm_user_pool.arn]
  identity_source                  = "method.request.header.Authorization"
  authorizer_result_ttl_in_seconds = 300
}

# 7. Déploiement (stage "test")
resource "aws_api_gateway_deployment" "ecomm_api_deployment" {
  depends_on = [
    aws_api_gateway_authorizer.ecomm_api_authorizer,
    aws_api_gateway_integration.lambda_integration
  ]
  rest_api_id = aws_api_gateway_rest_api.ecomm_api.id
  stage_name  = "test"
}

# 8. Resource API Gateway (/orders)
resource "aws_api_gateway_resource" "ecomm_api_resource" {
  rest_api_id = aws_api_gateway_rest_api.ecomm_api.id
  parent_id   = aws_api_gateway_rest_api.ecomm_api.root_resource_id
  path_part   = "orders"
}

# 9. Méthode GET protégée par Cognito
resource "aws_api_gateway_method" "api_get_method" {
  rest_api_id          = aws_api_gateway_rest_api.ecomm_api.id
  resource_id          = aws_api_gateway_resource.ecomm_api_resource.id
  http_method          = "GET"
  authorization        = "COGNITO_USER_POOLS"
  authorization_scopes = ["email"]
  authorizer_id        = aws_api_gateway_authorizer.ecomm_api_authorizer.id
}

# 10. Intégration API Gateway → Lambda
resource "aws_api_gateway_integration" "lambda_integration" {
  rest_api_id             = aws_api_gateway_rest_api.ecomm_api.id
  resource_id             = aws_api_gateway_resource.ecomm_api_resource.id
  http_method             = aws_api_gateway_method.api_get_method.http_method
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.get-orders.invoke_arn
}

File 03_lambda.tf — Lambda function

# 11. Fonction Lambda
resource "aws_lambda_function" "get-orders" {
  filename      = "get-orders.zip"
  function_name = "get-orders"
  handler       = "index.handler"
  runtime       = "nodejs20.x"

  source_code_hash = filebase64("get-orders.zip")
  role             = aws_iam_role.ecomm_api_lambda_exec_role.arn
}

# 12. IAM Role pour Lambda
resource "aws_iam_role" "ecomm_api_lambda_exec_role" {
  name = "ecomm_api_lambda_exec_role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      }
    }
  ]
}
EOF
}

# 14. Permission : API Gateway peut exécuter Lambda
resource "aws_lambda_permission" "gateway_exec_lambda_permission" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.get-orders.arn
  principal     = "apigateway.amazonaws.com"
}

File outputs.tf

output "api_invoke_url" {
  value = aws_api_gateway_deployment.ecomm_api_deployment.invoke_url
}

output "lambda_function_arn" {
  value = aws_lambda_function.get-orders.arn
}

Lambda file lambdas/index.mjs

export const handler = async (event) => {
  const orders = [
      {
          id: '1',
          uid: 'gsmith',
          items: [
              {
                  name: 'HFS - Lightweight Road Running',
                  single_price: '105.20',
                  currency: 'USD',
                  count: '2'
              }
          ],
          order_total: '210.40'
      },
      {
          id: '2',
          uid: 'rcharles',
          items: [
              {
                  name: 'Flying Spider Shoe',
                  single_price: '115.20',
                  currency: 'USD',
                  count: '2'
              }
          ],
          order_total: '230.40'
      },
      {
          id: '3',
          uid: 'sjackson',
          items: [
              {
                  name: 'Wild Scorpion Shoes',
                  single_price: '135.20',
                  currency: 'USD',
                  count: '2'
              }
          ],
          order_total: '270.40'
      }
  ];

  console.log(JSON.stringify(orders))

  const response = {
    statusCode: 200,
    body: JSON.stringify(orders)
  };
  
  return response;
};

Lambda packaging script (scripts/build_lambda_pkg.sh)

#!/bin/bash

# Aller dans le dossier cible
cd lambdas

# Créer le package zip Lambda
zip -r ../get-orders.zip .

# Lister le contenu du zip
unzip -l ../get-orders.zip

# Afficher les infos du package
zipinfo ../get-orders.zip

Cognito Concepts

An Amazon Cognito User Pool is a directory of users for authentication and authorization of web and mobile apps:

  • Scaling, patching, updates and zero downtime managed by Amazon
  • Integrates with other services like API Gateway and load balancers
  • Handles email verification, etc.

Important distinction:

  • User Pools → for authentication (identity verification)
  • Identity Pools → for authorization (access control)

Supported authentication modes:

  • Username and password
  • SRP (Secure Remote Password) — without exposing the password
  • Refresh tokens

2.13 Demo: Discovering the mystery of Cognito

Cognito Match ↔ OAuth Flow 2

OAuth 2 conceptComponent in demo
Client / ApplicationPostman
AuthorizationServerCognito User Pool
Authorization PageCognito Hosted UI
ResourceServerAmazon API Gateway
Lambda + AuthorizerBusiness logic + verification

Reference: Auth0 Authorization Code Flow Diagram

Complete sequence of Authorization Code flow with Cognito

  1. User clicks login link
  2. Authorization Code request to the Cognito authorize endpoint
  3. Redirect to Cognito Hosted UI
  4. User authenticates and consents
  5. Authorization code issued → sent to Postman
  6. Postman sends: authorization code + client ID + client secret → endpoint token OAuth
  7. Cognito validates and emits: id_token, access_token, refresh_token
  8. Postman uses access_token to query the Resource Server (API Gateway)
  9. API Gateway passes to Lambda
  10. Lambda returns orders

Postman configuration for OAuth 2

In Postman, to configure the GET /orders request:

  1. Authorization → Type: OAuth 2.0
  2. Add authorization data to: Request Headers
  3. New Token:
  • Grant Type: Authorization Code
  • Authorize using browser: ✓ (displays the Hosted UI in the browser)
  • Auth URL: https://<domain>.auth.<region>.amazoncognito.com/oauth2/authorize
  • Access Token URL: https://<domain>.auth.<region>.amazoncognito.com/oauth2/token
  • Client ID: from the Cognito console
  • Secret Client: from the Cognito console

Post-deployment benefits

After deployment with Cognito + API Gateway:

  • No more need for endpoints to manage user CRUD — Cognito takes care of it
  • No more need for /token endpoint — Cognito issues tokens automatically
  • Lambda code is clean and contains only business logic

2.14 Keep your secrets well guarded

At this stage, we have covered the main components of the architecture. But when you want to scale up with hundreds or thousands of services, coding everything by hand is no longer tenable.

Issues:

  • High availability
  • Robust technologies backed by major publishers
  • Free solution, integrating several security backends

Solution: HashiCorp Vault — the Swiss army knife of secrets management. The next module will explore Vault features and how they help meet current and future security requirements.


3. Do you have something valuable? Use the Vault

Total module duration: 22m 47s

3.1 The power and weaknesses of a distributed architecture

Background: Globomantics has been extremely successful in its acquisitions, building a cohesive product portfolio that sells better together than separately. But these companies use different approaches to their microservices, making security difficult to maintain and expensive to scale.

Why HashiCorp Vault?

Vault is built on solid technologies:

  • Go language (Golang) from Google → strong encryption
  • Large Security Model System
  • Dedicated, active and large community
  • Free and open-source

What Vault solves:

ProblemVault Solution
Operational complexityOut-of-the-box integration with virtually any backend (cloud or on-premises)
Fragmented Secrets ManagementCentralized backend for all secrets
Multiple authenticationCan be deployed as its own OIDC server
Integration with the HashiCorp ecosystemNative integration with Consul, Boundary, etc.

Vault as OIDC identity provider:

  • Can do the same as Amazon Cognito
  • Only supports Authorization Code flow (unlike Cognito)
  • Can be used with Node.js by directly calling the Vault API for OIDC functionality
  • For a complete zero trust implementation, consider HashiCorp Boundary

3.2 Demo: Hiding secrets in the Vault

Installing Vault

### macOS ###
brew tap hashicorp/tap
brew install hashicorp/tap/vault
vault -v

# ATTENTION : démarrer via brew = mode DEV uniquement
brew services start vault

# Pour la mise à jour
brew upgrade hashicorp/tap/vault

# Créer les dossiers nécessaires pour le mode production
sudo mkdir /etc/vault.d && touch /etc/vault.d/vault.hcl

### Windows ###
choco install vault

### Linux (Ubuntu) ###
sudo apt update && sudo apt install gpg wget
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vault

### Linux (CentOS / RedHat) ###
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install vault

Important macOS: if you start Vault via brew services, it only starts in DEV mode. For production with HTTPS, it must be started manually.

Vault Configuration (vault.hcl)

ui = true

storage "file" {
    path = "/opt/vault/data"
}

# Listener HTTPS
listener "tcp" {
    address         = "0.0.0.0:8200"
    tls_cert_file   = "/opt/vault/tls/tls.crt"
    tls_key_file    = "/opt/vault/tls/tls.key"
}

Generating TLS certificates

# Créer la structure de dossiers
sudo mkdir -p /opt/vault/{tls,data}
cd /opt/vault/tls

# Générer les certificats avec OpenSSL
openssl req -out tls.crt -new -keyout tls.key -newkey rsa:4096 -nodes -sha256 -x509 \
  -subj "/O=HashiCorp/CN=www.hashicorp.com" \
  -addext "subjectAltName=IP:0.0.0.0,DNS:Georges-Mac-Studio.local" \
  -days 3650

# Alternative avec fichier de config cert.conf
openssl req -x509 -batch -nodes -newkey rsa:2048 -keyout selfsigned.key \
  -out selfsigned.crt -config cert.conf -days 9999

Vault environment variables (.zshrc)

export VAULT_SKIP_VERIFY=true    # Ignorer les certificats self-signés
export VAULT_ADDR='https://127.0.0.1:8200'
export VAULT_TOKEN='<root_token>'
export VAULT_NAMESPACE='admin'

# Ne pas oublier de sourcer :
source ~/.zshrc

Initialization and Unseal

# Initialiser Vault (génère 5 clés, seuil de 3)
vault operator init -key-shares=5 -key-threshold=3

# Pour auto-unseal - définir les seuils de récupération
vault operator init -recovery-shares=5 -recovery-threshold=3

Initialization result:

Unseal Key 1: MBUH/ynNBAZgOHL8s/HK9a0P3EfssXmDcBXQlzBFfRHm
Unseal Key 2: N+z+cmbADLA3X1OBF9K7wx911Ccc6Oo234pymEPiZ911
Unseal Key 3: hQUz/+ght5Kl4X3QDKy9Z+qOlrVaLeSBQJQ0n1MPbSzM
Unseal Key 4: nhPGxdRsUiMtwlF27Tly+o6j2ngLOJYpqwjZNayT6Rvu
Unseal Key 5: ...
Initial Root Token: hvs.kZkb3Xqx7iNPSMcE1xuQyiM1

Shamir’s Secret Sharing: the master key is broken down into N parts (here 5). You need at least K games (here 3) to unlock Vault. Each part is assigned to a different person.

Automatic unseal script (unseal.sh)

#!/bin/bash

vault operator unseal -tls-skip-verify MBUH/ynNBAZgOHL8s/HK9a0P3EfssXmDcBXQlzBFfRHm
vault operator unseal -tls-skip-verify N+z+cmbADLA3X1OBF9K7wx911Ccc6Oo234pymEPiZ911
vault operator unseal -tls-skip-verify hQUz/+ght5Kl4X3QDKy9Z+qOlrVaLeSBQJQ0n1MPbSzM

Verification Commands

# Vérifier le statut (Sealed = false, Total Shares = 5, Threshold = 3)
vault status

# Se connecter avec le root token
sudo vault login -tls-skip-verify hvs.kZkb3Xqx7iNPSMcE1xuQyiM1

# Vérifier la connexion
sudo vault token lookup -tls-skip-verify

# Vérifier que Vault écoute sur les ports attendus
netstat -ntpl | grep -E '8200|8201'

PKI Secrets Engine — Certificate Authority

To generate your own certificates via Vault, you use the PKI Secrets Engine:

# Générer un certificat Root CA (Internal)
vault write -format=json pki/root/generate/internal common_name="vault-ca-root-pki" \
    | tee \
      >(jq -r .data.certificate > vault-ca-root-pki.pem) \
      >(jq -r .data.issuing_ca > vault-ca-root-pki-issuing.pem) \
      >(jq -r .data.private_key > vault-ca-root-pki-key.pem)

# Lister tous les issuers
vault list -format=json pki/issuers

# Créer un rôle pour l'issuer Root CA
vault write pki/roles/2023-servers allow_any_name=true

# Configurer les URLs du Root CA
vault write pki/config/urls \
     issuing_certificates="$VAULT_ADDR/v1/pki/ca" \
     crl_distribution_points="$VAULT_ADDR/v1/pki/crl"

### INTERMEDIATE CA ###
# Activer le PKI sur un chemin différent
vault secrets enable -path=pki_int pki
vault secrets tune -max-lease-ttl=43800h pki_int

# Générer le CSR de l'Intermediate CA
vault write -format=json pki_int/intermediate/generate/internal \
     common_name="example.com Intermediate Authority" \
     issuer_name="example-dot-com-intermediate" \
     | jq -r '.data.csr' > pki_intermediate.csr

# Signer le CSR avec le Root CA
vault write -format=json pki/root/sign-intermediate \
     issuer_ref="root-2023" \
     csr=@pki_intermediate.csr \
     format=pem_bundle ttl="43800h" \
     | jq -r '.data.certificate' > intermediate.cert.pem

# Importer le cert de l'Intermediate CA dans Vault
vault write pki_int/intermediate/set-signed certificate=@intermediate.cert.pem

# Créer un rôle pour l'Intermediate CA
vault write pki_int/roles/example-dot-com \
     issuer_ref="$(vault read -field=default pki_int/config/issuers)" \
     allowed_domains="example.com" \
     allow_subdomains=true \
     max_ttl="720h"

# Créer des certificats
vault write -format=json pki_int/issue/example-dot-com common_name="test.example.com" ttl="24h"

Order of certificates generated:

  1. Root CA private key
  2. CSR of Intermediate CA
  3. Private key (cert) of the Intermediate CA

Management of KV secrets (Key-Value)

# Écrire un premier secret
sudo vault kv put -tls-skip-verify -mount=kv2 secret_user1 name=Neo context=matrix

# Lire un secret
sudo vault kv get -tls-skip-verify -mount=kv2 secret_user1

# Créer un secret KVv2 via curl
curl -k -H "X-Vault-Request: true" \
     -H "X-Vault-Token: hvs.kZkb3Xqx7iNPSMcE1xuQyiM1" \
     --request POST \
     --data @payload.json \
     https://127.0.0.1:8200/v1/secrets/data/access_token_secret

3.3 Demo: Write Node.js code to interact with the Vault

AppRole — Machine-to-machine authentication

AppRole is Vault’s mechanism specifically designed for machine-to-machine communications (service accounts). It is capable of handling a large number of applications and is geared towards automated workflows.

Features:

  • Use role_id and secret_id instead of username/password
  • Credentials are generally machine readable
# Activer AppRole (monté sur /auth/approle)
sudo vault auth enable -tls-skip-verify approle

# Créer le rôle et l'associer à une policy
sudo vault write -tls-skip-verify auth/approle/role/globo-role \
    secret_id_ttl="720h" \
    token_ttl="120h" \
    token_max_ttl="120h" \
    policies="globo-policy"

# Lire le role_id
sudo vault read -tls-skip-verify auth/approle/role/globo-role/role-id

# Créer le secret_id
sudo vault write -tls-skip-verify -f auth/approle/role/globo-role/secret-id
# Résultat :
# secret_id:          f9fc707e-f1cc-62d9-98bb-347911f76454
# secret_id_accessor: 9be2985c-b19a-7b0a-fa0c-958df8f91bd9
# secret_id_num_uses: 0
# secret_id_ttl:      720h

# Login avec AppRole
vault write auth/approle/login \
    role_id=36674f56-d29b-1b2c-f00e-cd0f9c4cc753 \
    secret_id=f9fc707e-f1cc-62d9-98bb-347911f76454

Policy Vault (sampl.hcl)

# Exemple de policy (globo-policy.hcl)
# Le chemin "secret/*" est le plus important — c'est là où on écrit les tokens
# Créer/uploader une policy
vault policy write globo-policy globo-policy.hcl

# Mettre à jour une policy
vault write sys/policy/globo-policy policy=@globo-policy.hcl

# Supprimer une policy
vault policy delete my-policy

# Lister les policies
vault policy list

# Lire une policy
vault policy read my-policy

Installing the Node.js wrapper for Vault

npm install hashi-vault-js

Important manufacturer configurations:

const Vault = require('hashi-vault-js')

const vault = new Vault({
    https: true,          // Communications sécurisées (TLS)
    proxy: false,
    baseUrl: 'https://127.0.0.1/8200/v1',
    cacert: './certs/ca.crt',
    cert: './certs/client.crt',
    rootPath: 'secrets',  // Attention : doit être 'AuthRole' pour AppRole
    timeout: 2000
});

Warning: in the official documentation, rootPath is set to secret, but it must be set to AuthRole for it to work with AppRole.

.env file with AppRole credentials

ACCESS_TOKEN_SECRET=9c30c2370813582c4d57a51bddd06d2e...
REFRESH_TOKEN_SECRET=dad5e0dd79d305de04146a390c9933e7...

APPROLE_ROLE_ID=<role_id_obtenu>
APPROLE_SECRET_ID=<secret_id_obtenu>

auth-server.js complete with Vault integration

const express = require('express')
const app = express()
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
require('dotenv').config()
const Vault = require('hashi-vault-js')

// Credentials AppRole pour l'authentification à Vault
const roleId = process.env.APPROLE_ROLE_ID
const secretId = process.env.APPROLE_SECRET_ID

app.use(express.json())

let refreshTokens = []

const users = [
    { uid: 'gsmith',   pwd: '$2b$10$ZlAnB2ouP/aoJT.dI9UjXuyDaN2fDxj0kEmSnzUUFOP/c3LUdm5tq' },
    { uid: 'rcharles', pwd: '$2b$10$xcTiAcwtin1ayku9pg2gD.buZdmcIXSv2bkQMJsGmxOFxJ23EEbp.' },
    { uid: 'jdoe',     pwd: '$2b$10$xcTiAcwtin1ayku9pg2gD.buZdmcIXSv2bkQMJsGmxOFxJ23EEbp.' },
    { uid: 'sjackson', pwd: '$2b$10$BHv1DaTClj0RUYJ267A7M.yVLJyBA9Au76uo.mQkhCu96OMZ8foPy' }
]

const vault = new Vault({
    https: true,
    proxy: false,
    baseUrl: 'https://127.0.0.1/8200/v1',
    cacert: './certs/ca.crt',
    cert: './certs/client.crt',
    rootPath: 'secrets',
    timeout: 2000
});

app.post('/login', async (request, response) => {
    const user = users.find(user => user.uid == request.body.uid)
    if (user == null) {
        return response.status(400).send('User NOT found!!!')
    }
    try {
        if (await bcrypt.compare(request.body.pwd, user.pwd)) {
            const accessToken = mintAccessToken(user)
            const refreshToken = jwt.sign(user, process.env.REFRESH_TOKEN_SECRET)
            refreshTokens.push(refreshToken)
            response.json({ accessToken: accessToken, refreshToken: refreshToken })
        } else {
            response.send("Access denied.")
        }
    } catch {
        response.status(500).send()
    }
})

app.delete('/logout', (request, response) => {
    refreshTokens = refreshTokens.filter(token => token !== request.body.refresh_token)
    response.sendStatus(204)
})

app.post('/token', (request, response) => {
    const refreshToken = request.body.refresh_token
    if (refreshToken == null) {
        return response.sendStatus(401)
    }
    if (!refreshTokens.includes(refreshToken)) {
        return response.sendStatus(403)
    }
    jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (error, user) => {
        if (error) {
            return response.sendStatus(403)
        }
        const accessToken = mintAccessToken({ uid: user.uid })

        // Stocker le token dans Vault
        vault.healthCheck().then((status) => {
            if (!status.sealed) {
                // S'authentifier pour obtenir un token Vault
                vault.loginWithAppRole(roleId, secretId).then((data) => {
                    const apiToken = data.client_token;
                    console.log('API token received: ', apiToken);
                    
                    // Stocker le secret au format K/V
                    secrets.array.forEach((secret) => {
                        vault.createKVSecret(apiToken, secret.label, secret.pairs).then(() => {
                            vault.listKVSecrets(apiToken).then((data) => {
                                console.log('Stored keys: ', data);
                            });
                            vault.readKVSecret(apiToken, secret.label).then((data) => {
                                console.log('KVs for ' + secret.label + ': ', data);
                            });
                        }).catch((createError) => {
                            console.error('KV creation error: ', createError);
                        });
                    });
                }).catch((loginError) => {
                    console.error('Login error occurred: \n', loginError)
                });
            }
        }).catch((error) => {
            console.error('Health check returned an error: \n', error)
        });

        response.json({ accessToken: accessToken })
    })
})

function mintAccessToken(user) {
    return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '10s' })
}

console.log('Auth Server1 started - OK');
app.listen(3001)

resource-server.js — Resource server

const express = require('express')
const app = express()
const jwt = require('jsonwebtoken')
require('dotenv').config()

app.use(express.json())

const orders = [
    { id: '1', uid: 'gsmith',   items: [{ name: 'HFS - Lightweight Road Running', single_price: '105.20', currency: 'USD', count: '2' }], order_total: '210.40' },
    { id: '2', uid: 'rcharles', items: [{ name: 'Flying Spider Shoe', single_price: '115.20', currency: 'USD', count: '2' }], order_total: '230.40' },
    { id: '3', uid: 'sjackson', items: [{ name: 'Wild Scorpion Shoes', single_price: '135.20', currency: 'USD', count: '2' }], order_total: '270.40' }
]

app.get('/orders', verifyToken, (request, response) => {
    response.json(orders.filter(order => order.uid === request.user.uid));
})

function verifyToken(request, response, next) {
    const authzHeader = request.headers['authorization']
    const accessToken = authzHeader && authzHeader.split(' ')[1]
    if (accessToken == null) {
        return response.status(401).send()
    }
    jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET, (error, user) => {
        if (error) {
            console.log(error)
            return response.status(403).send()
        }
        request.user = user
        next()
    })
}

console.log('Server1 started - OK');
app.listen(3000)

Test sequence with Vault

  1. Send login request → get accessToken + refreshToken
  2. Copy the refreshToken
  3. Calling the /token endpoint → triggers the sequence:
  • vault.healthCheck() → checks that Vault is operational
  • vault.loginWithAppRole(roleId, secretId) → gets a vaultToken
  • vault.createKVSecret(vaultToken, label, data) → stores the access token
  • vault.listKVSecrets(vaultToken) → list stored secrets
  • vault.readKVSecret(vaultToken, label) → read secret
  1. Receive a new accessToken
  2. In the Vault UI, navigate to secret/ → see the created access_token with its metadata
  3. Compare versions via the Version diff feature

Advanced Vault UI features for secrets

  • Version History: see all versions of a secret
  • Diff version: compare two versions and see the changes
  • Metadata: Last updated, Maximum versions, Check-and-Set required, Delete version after
  • Paths: API path, CLI path, API path for metadata — useful for debugging

3.4 From raw programming to thoughtful architecture

Looking back, the progress has been remarkable:

Approach Module 2Approach Module 3
Hand coding (Node.js developer perspective)Architectural approach
Single process and stackMicroservices security as a cross-cutting concern
Secrets in code / .envOutsourced Secrets in HashiCorp Vault

HashiCorp Vault (open-source, written in Golang) not only addresses architectural concerns (scale, reliability, auditability), but can also serve as an OIDC identity provider, similar to Amazon Cognito.


4. HashiCorp Consul and East West Traffic

Total module duration: 13m 55s

4.1 Go east or west?

The challenge of securing East-West traffic in microservices architectures often involves services that communicate directly without the mediation of an API Gateway.

HashiCorp Consul addresses three key challenges related to the monolith → microservices transition:

ChallengeConsul Solution
Discovery ServiceAutomatic service detection
Centralized configurationConfiguration management and distribution
Traffic SegmentationSecuring and controlling East-West traffic

Our focus: Traffic Segmentation, because it is the crucial role in securing East-West traffic.

Consul in 2-tier and 3-tier architectures

Consul manages in particular:

  • Add/remove DNS records
  • Query DNS or configuration files
  • Detect changes (polling or other methods) and manage cache
  • Distribute configuration changes to services
  • Signal services to reload their configurations

Beyond that, Consul plays a vital role in securing connections between components, ensuring the confidentiality and integrity of communications.

In the world of microservices, Consul secures East-West connections not only between a web app and its services, but also between the services themselves (eg: service orders ↔ service shipment) via mutual TLS with X.509 certificates.

4.2 Demo: Speak through the Consulate

Installing Consul

# Sur Ubuntu
# (copier les instructions depuis la documentation officielle)
sudo apt-get install consul

# Vérifier la version
consul version

# Créer le répertoire de données
sudo mkdir -p /opt/consul/data
sudo chown $USER:$USER /opt/consul/data

Configuring Consul Services

Two service files are created in /etc/consul.d/:

resource_server.json — Resource server

{
  "service": {
    "name": "resourceserver",
    "port": 3000,
    "connect": {
      "sidecar_service": {
        "proxy": {
          "upstreams": [
            {
              "destination_name": "authserver",
              "local_bind_port": 9191
            }
          ]
        }
      }
    }
  }
}

Key point: the resource server does not talk directly to the auth server. It talks to a local proxy on port 9191. It is this sidecar proxy that establishes a secure mTLS connection with the sidecar proxy of the auth server.

auth_server.json — Authentication server

{
  "service": {
    "name": "authserver",
    "port": 3001,
    "connect": {
      "sidecar_service": {
        "proxy": {}
      }
    }
  }
}

The auth server is not connecting to any upstreams at the moment — the connect configuration points to an empty proxy.

Architecture with Consul Service Mesh

Client
  ↓ HTTPS
API Gateway (North-South)
  ↓
Resource Server (port 3000)
  ↕ via sidecar proxy (port 9191) — mTLS automatique
Auth Server (port 3001)

Consul sidecar proxies automatically manage:

  • Negotiation of X.509 certificates
  • Establishing mTLS connections
  • Control via Intentions

Improved Node.js code: verification via auth server

The resource server no longer verifies the JWT token locally itself. It delegates verification to the auth server by calling a new verification endpoint:

// Sur le resource server, au lieu de vérifier localement :
// On appelle l'auth server via le proxy local Consul (port 9191)

app.get('/orders', async (request, response) => {
    try {
        // Appel à l'auth server via le proxy sidecar Consul
        const verifyResponse = await fetch('http://localhost:9191/verify', {
            headers: {
                'Authorization': request.headers['authorization']
            }
        });
        
        if (!verifyResponse.ok) {
            return response.status(verifyResponse.status).send();
        }
        
        const user = await verifyResponse.json();
        response.json(orders.filter(order => order.uid === user.uid));
    } catch (error) {
        response.status(500).send();
    }
});
// Nouvel endpoint sur l'auth server
app.get('/verify', verifyToken, (request, response) => {
    response.json(request.user);
});

Consul Intents — Traffic Control

Intents in Consul allow you to control which services can communicate with each other:

# Créer une intention DENY (bloquer la communication)
consul intention create -deny resourceserver authserver

# Tester - le resource server ne peut plus atteindre l'auth server
# → 500 Internal Server Error

# Supprimer l'intention (restaurer la communication)
consul intention delete resourceserver authserver

# Tester - la communication est rétablie
# → 200 OK avec les ordres

Complete test sequence

  1. Start Consul with both service files
  2. Start sidecar proxies for each service
  3. Start auth-server.js (port 3001) and resource-server.js (port 3000)
  4. Normal test: GET /orders request → auth server checks the token → orders returned
  5. Test with DENY intent: create intent → GET /orders request → failure
  6. Remove intent: GET /orders request → success again

4.3 Beyond Microservices Security

Full route summary

Module 2 - Local (JWT manuel)
    ↓
Module 2 - Séparation auth/resource server
    ↓
Module 2 - Cloud (Amazon Cognito + API Gateway + Lambda)
    ↓
Module 3 - HashiCorp Vault (stockage sécurisé des secrets)
    ↓
Module 4 - HashiCorp Consul (mTLS automatique, service mesh East-West)

In detail, the complete course of the course:

  1. Generation of local JWT tokens to secure microservices
  2. Separation of services to align with OAuth 2.0 flows
  3. Delegation to Amazon Cognito and API Gateway in the cloud for token and flow management
  4. HashiCorp Vault to store and retrieve tokens securely
  5. HashiCorp Consul to automatically encrypt and control East-West traffic with X.509 certificates and Intents (service mesh)

Top 3 recommendations to continue

  1. Security/productivity balance: The more secure you make a system, the less usable it becomes. A policy that only allows login from a secure, secret location cripples productivity. Design and implement security with productivity in mind.

  2. Dive Deeper into HashiCorp Vault: Explore the vast number of authentication methods and secrets engines in Vault. This will make your security architecture future-proof and ensure that your organization’s needs are continually met.

  3. Two other products to explore:

  • HashiCorp Boundary: for a complete zero trust implementation at a lower cost
  • HashiCorp Consul (more in depth): to master all the capabilities of the service mesh

Zero Trust in summary

ComponentRole in Zero Trust
Amazon Cognito + API GatewaySecure North-South Traffic (JWT, OAuth 2)
HashiCorp VaultCentralized secret management, OIDC identity provider
HashiCorp ConsulSecuring East-West traffic (automatic mTLS, intentions)
HashiCorp BoundaryZero trust network access (ZTNA) complete

5. Code files and practice exercises

5.1 Module 2 — Terraform Infrastructure (AWS Cognito + API Gateway + Lambda)

Location: 02/demos/

FileDescription
provider.tfConfiguring the AWS provider
01_cognito.tfUser Pool, User Pool Client, domain, test user
02_apigateway.tfAPI Gateway, Cognito authorizer, deployment, GET method
03_lambda.tfNode.js Lambda function, IAM role, permissions
outputs.tfOutputs: URL invoke API Gateway, ARN Lambda
lambdas/index.mjsLambda code returning commands (ES Module handler)
scripts/build_lambda_pkg.shLambda packaging script in ZIP
scripts/clean-tf-state.shTerraform state cleanup script

Terraform cleanup script

#!/bin/bash
# Nettoyer l'état Terraform avant d'exécuter "terraform init" à nouveau
cd ../ && rm .terraform.lock.hcl terraform.tfstate terraform.tfstate.backup && rm -rf .terraform/

5.2 Module 4 — JWT Application with HashiCorp Vault

Location: 04/demos/jwtauth/

File / FolderDescription
auth-server.jsAuthentication server (login, logout, refresh token, Vault integration)
resource-server.jsResource Server (GET /orders with JWT verification)
package.jsonNode.js dependencies
.envEnvironment variables (ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, AppRole credentials)
vault.envVault Variables (VAULT_ADDR, VAULT_CACERT)
app_requests_1.restCollection of REST requests for testing
certs/PKI certificates generated by Vault
notes/vault.tomlVault Configuration Notes (macOS/Homebrew)
shell_scripts/install_vault.shInstalling Vault on macOS/Windows/Linux
shell_scripts/ops.shVault operations commands (init, unseal, PKI, AppRole, etc.)
shell_scripts/prod_setup.shProduction setup: folder structure, generation of OpenSSL certificates
shell_scripts/unseal.shAutomatic unseal script (3 keys)
shell_scripts/vault_env_vars.shVault environment variables (dev mode)
shell_scripts/sampl.hclExample Vault HCL configuration file
policies/Policies Vault (HCL)

package.json

{
  "name": "jwtauth",
  "version": "1.0.0",
  "scripts": {
    "devStartResource": "nodemon resource-server.js",
    "devStartAuth": "nodemon auth-server.js"
  },
  "dependencies": {
    "bcrypt": "^5.1.1",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "hashi-vault-js": "^0.4.14",
    "jsonwebtoken": "^9.0.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

Vault environment variables (dev mode)

#!/bin/bash
export VAULT_TOKEN='root'
export VAULT_ADDR='http://127.0.0.1:8200'
# vault.env (production)
VAULT_ADDR="https://Georges-Mac-Studio.local:8200"
VAULT_CACERT="/opt/vault/tls/tls.crt"

Full REST request collection (app_requests_1.rest)

### Accéder aux ordres (avec access token)
GET http://localhost:3000/orders
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

###

### Rafraîchir l'access token
POST http://localhost:3001/token
Content-Type: application/json

{
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

###

### Logout
DELETE http://localhost:3001/logout
Content-Type: application/json

{
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

###

### Voir tous les utilisateurs
GET http://localhost:3000/users

###

### Créer un utilisateur
POST http://localhost:3000/users
Content-Type: application/json

{
    "uid": "gsmith",
    "pwd": "echo3"
}

###

### Login
POST http://localhost:3001/login
Content-Type: application/json

{
    "uid": "gsmith",
    "pwd": "echo3"
}


Search Terms

node.js · microservices · authentication · authorization · apis · backend · full-stack · web · vault · cognito · token · consul · secrets · endpoint · jwt · refresh · traffic · lambda · requests · rest · server · terraform · test · tokens

Interested in this course?

Contact us to book it or get a custom training plan for your team.