Table of Contents
- 2.1 Pressing need for security for microservices
- 2.2 Secure North-South and East-West traffic
- 2.3 The main players in authentication and authorization
- 2.4 All about JWTs
- 2.5 Demo: Initial project configuration
- 2.6 Demo: Creating and testing routes
- 2.7 Authenticate and create an access token
- 2.8 Allow requests with JWT tokens
- 2.9 Test the same access token on multiple servers
- 2.10 Refresh tokens: a new start
- 2.11 Check the correct functioning of the refresh token
- 2.12 Deploy microservices in the cloud
- 2.13 Demo: Discovering the mystery of Cognito
- 2.14 Keep Your Secrets Well Kept
- 3.1 The power and weaknesses of a distributed architecture
- 3.2 Demo: Hiding secrets in the Vault
- 3.3 Demo: Write Node.js code to interact with the Vault
- 3.4 From raw programming to reflective architecture
- 5.1 Module 2 — Terraform Infrastructure (AWS Cognito + API Gateway + Lambda)
- 5.2 Module 4 — JWT application with HashiCorp Vault
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:
- JWT tokens (jot tokens) — which we will see in detail
- 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:
| Actor | Role |
|---|---|
| Resource Owner | Most 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 Server | Authenticates the resource owner and issues access tokens |
| Resource Server | Hosts 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:
- User calls a client/app that needs permissions
- The app contacts the authorization server
- Browser redirect to authorization page
- User authenticates and approves or denies
- The authorization server issues a short-lived authorization code
- The client submits this code to the authorization server endpoint token
- The authorization server checks the code and issues an access token
- The client accesses the resource server with the access token
- The resource server submits the access token to the introspection endpoint for verification
- 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 scenario | Grant recommended |
|---|---|
| Traditional web application (server) | Authorization Code |
| Single-Page Application (SPA) | Authorization Code + PKCE |
| SPA without the need for an access token | Implicit Grant with Form Post |
| Client = Resource Owner | Customer Credentials |
| Totally trusted customer with credentials | Resource 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):
- The user presents his credentials
- The application server generates a session ID and stores it in memory
- The ID is passed in a cookie on each request
- The server consults its memory to find the session ID
- Problem: If the server restarts, all session IDs are lost
Modern approach (JWT):
- Authentication happens in the same way
- 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
- The server does not need to store the token — this is the fundamental difference
- The token can be used with multiple servers
- 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 personiat: 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 (
10by default) to a value like30may 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
uidand password are replaced byclient_idandclient_secret, often called service account. Theclient_idandclient_secretserve 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→ onlygsmithcommand - If logged in as
rcharles→ onlyrcharlescommand - 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:
- 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.
- Store in memory: not in localStorage nor in a cookie. If accessible via JavaScript, a hacker can also recover it.
- Revocation: invalidate a refresh token = create a logout route which removes it from the list of valid tokens.
- 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:
- Log in → get
accessToken+refreshToken - Use
refreshToken→ get newaccessToken✓ - Perform a DELETE /logout with the
refreshToken - Retry the
refreshToken→ 403 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 concept | Component in demo |
|---|---|
| Client / Application | Postman |
| AuthorizationServer | Cognito User Pool |
| Authorization Page | Cognito Hosted UI |
| ResourceServer | Amazon API Gateway |
| Lambda + Authorizer | Business logic + verification |
Reference: Auth0 Authorization Code Flow Diagram
Complete sequence of Authorization Code flow with Cognito
- User clicks login link
- Authorization Code request to the Cognito
authorizeendpoint - Redirect to Cognito Hosted UI
- User authenticates and consents
- Authorization code issued → sent to Postman
- Postman sends: authorization code + client ID + client secret → endpoint token OAuth
- Cognito validates and emits:
id_token,access_token,refresh_token - Postman uses
access_tokento query the Resource Server (API Gateway) - API Gateway passes to Lambda
- Lambda returns orders
Postman configuration for OAuth 2
In Postman, to configure the GET /orders request:
- Authorization → Type:
OAuth 2.0 - Add authorization data to: Request Headers
- 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
/tokenendpoint — 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:
| Problem | Vault Solution |
|---|---|
| Operational complexity | Out-of-the-box integration with virtually any backend (cloud or on-premises) |
| Fragmented Secrets Management | Centralized backend for all secrets |
| Multiple authentication | Can be deployed as its own OIDC server |
| Integration with the HashiCorp ecosystem | Native 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
Nparts (here 5). You need at leastKgames (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:
- Root CA private key
- CSR of Intermediate CA
- 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_idandsecret_idinstead 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,
rootPathis set tosecret, but it must be set toAuthRolefor 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
- Send login request → get
accessToken+refreshToken - Copy the
refreshToken - Calling the
/tokenendpoint → triggers the sequence:
vault.healthCheck()→ checks that Vault is operationalvault.loginWithAppRole(roleId, secretId)→ gets avaultTokenvault.createKVSecret(vaultToken, label, data)→ stores the access tokenvault.listKVSecrets(vaultToken)→ list stored secretsvault.readKVSecret(vaultToken, label)→ read secret
- Receive a new
accessToken - In the Vault UI, navigate to
secret/→ see the createdaccess_tokenwith its metadata - 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 2 | Approach Module 3 |
|---|---|
| Hand coding (Node.js developer perspective) | Architectural approach |
| Single process and stack | Microservices security as a cross-cutting concern |
| Secrets in code / .env | Outsourced 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:
| Challenge | Consul Solution |
|---|---|
| Discovery Service | Automatic service detection |
| Centralized configuration | Configuration management and distribution |
| Traffic Segmentation | Securing 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
- Start Consul with both service files
- Start sidecar proxies for each service
- Start
auth-server.js(port 3001) andresource-server.js(port 3000) - Normal test: GET /orders request → auth server checks the token → orders returned
- Test with DENY intent: create intent → GET /orders request → failure
- 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:
- Generation of local JWT tokens to secure microservices
- Separation of services to align with OAuth 2.0 flows
- Delegation to Amazon Cognito and API Gateway in the cloud for token and flow management
- HashiCorp Vault to store and retrieve tokens securely
- HashiCorp Consul to automatically encrypt and control East-West traffic with X.509 certificates and Intents (service mesh)
Top 3 recommendations to continue
-
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.
-
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.
-
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
| Component | Role in Zero Trust |
|---|---|
| Amazon Cognito + API Gateway | Secure North-South Traffic (JWT, OAuth 2) |
| HashiCorp Vault | Centralized secret management, OIDC identity provider |
| HashiCorp Consul | Securing East-West traffic (automatic mTLS, intentions) |
| HashiCorp Boundary | Zero 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/
| File | Description |
|---|---|
provider.tf | Configuring the AWS provider |
01_cognito.tf | User Pool, User Pool Client, domain, test user |
02_apigateway.tf | API Gateway, Cognito authorizer, deployment, GET method |
03_lambda.tf | Node.js Lambda function, IAM role, permissions |
outputs.tf | Outputs: URL invoke API Gateway, ARN Lambda |
lambdas/index.mjs | Lambda code returning commands (ES Module handler) |
scripts/build_lambda_pkg.sh | Lambda packaging script in ZIP |
scripts/clean-tf-state.sh | Terraform 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 / Folder | Description |
|---|---|
auth-server.js | Authentication server (login, logout, refresh token, Vault integration) |
resource-server.js | Resource Server (GET /orders with JWT verification) |
package.json | Node.js dependencies |
.env | Environment variables (ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, AppRole credentials) |
vault.env | Vault Variables (VAULT_ADDR, VAULT_CACERT) |
app_requests_1.rest | Collection of REST requests for testing |
certs/ | PKI certificates generated by Vault |
notes/vault.toml | Vault Configuration Notes (macOS/Homebrew) |
shell_scripts/install_vault.sh | Installing Vault on macOS/Windows/Linux |
shell_scripts/ops.sh | Vault operations commands (init, unseal, PKI, AppRole, etc.) |
shell_scripts/prod_setup.sh | Production setup: folder structure, generation of OpenSSL certificates |
shell_scripts/unseal.sh | Automatic unseal script (3 keys) |
shell_scripts/vault_env_vars.sh | Vault environment variables (dev mode) |
shell_scripts/sampl.hcl | Example 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