Chuyển đến nội dung chính

Lesson 22: API Gateway, Nginx and Microservices

Keycloak integrates with Nginx (lua-resty-openidc/nginx-oidc-module or OAuth2 Proxy), Kong Gateway (OIDC plugin), Traefik (ForwardAuth middleware), API Gateway pattern for microservices, service account authentication (client credentials grant), token exchange, internal service-to-service authentication and complete Docker Compose stack.

🔒 DevSecOps — Lesson 22 Lesson 22: API Gateway, Nginx and Microservices

Keycloak from Basic to Advanced

Part 6: Integrating practical applications

xdev.asia

1. API Gateway Pattern with Keycloak

In a microservices architecture, API Gateway acts as a single entry point for all client requests. When combined with Keycloak, the gateway handles centralized authentication and JWT propagation to downstream services.

                    ┌──────────────────────────────────────┐
                    │           API Gateway                │
                    │    (Nginx / Kong / Traefik)          │
 ┌────────┐        │                                      │        ┌──────────────┐
 │ Client │────────▶│  1. Validate JWT (Keycloak JWKS)    │───────▶│ Service A    │
 │ (SPA)  │        │  2. Rate Limiting                    │        │ (User API)   │
 └────────┘        │  3. Route to service                 │        └──────────────┘
                    │  4. Propagate JWT header             │
 ┌────────┐        │                                      │        ┌──────────────┐
 │ Mobile │────────▶│                                      │───────▶│ Service B    │
 │  App   │        │                                      │        │ (Order API)  │
 └────────┘        └───────────────┬──────────────────────┘        └──────────────┘
                                    │                                      │
                    ┌───────────────▼──────────────────────┐               │
                    │           Keycloak                    │               │
                    │  - Token Validation (JWKS)           │        ┌──────▼───────┐
                    │  - Token Exchange                    │        │ Service C    │
                    │  - Service Account Tokens            │        │ (Payment)    │
                    └──────────────────────────────────────┘        └──────────────┘
GatewayAuth MethodAdvantages
Nginx + lua-resty-openidcLua module OIDCLightweight, high performance
Nginx + OAuth2 ProxySidecar proxyEasy setup, no need for Lua
Kong GatewayOIDC PluginEnterprise features, plugin ecosystem
TraefikForwardAuthCloud-native, auto-discovery

2. Nginx + Keycloak Integration

2.1 Approach 1: lua-resty-openidc

lua-resty-openidc is OpenID Connect module for Nginx/OpenResty, supports JWT verification, token introspection and OIDC login flow directly at Nginx layer.

2.1.1 Install OpenResty

# Dockerfile.openresty
FROM openresty/openresty:1.25.3.1-jammy

# Cài đặt lua-resty-openidc và dependencies
RUN luarocks install lua-resty-openidc 1.7.6
RUN luarocks install lua-resty-session 4.0.5

COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY default.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

2.1.2 Nginx Configuration with Bearer Token Validation

# /etc/nginx/conf.d/default.conf

lua_package_path '/usr/local/openresty/lualib/?.lua;;';

# Shared dict cho OIDC session & discovery cache
lua_shared_dict discovery 1m;
lua_shared_dict jwks 1m;
lua_shared_dict introspection 10m;

# Resolver cho DNS (Docker internal DNS)
resolver 127.0.0.11 ipv6=off;

upstream user-service {
    server user-service:8081;
}

upstream order-service {
    server order-service:8082;
}

server {
    listen 80;
    server_name api.example.com;

    # ===== Public endpoints (không cần auth) =====
    location /api/public/ {
        proxy_pass http://user-service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # ===== Protected endpoints (JWT validation) =====
    location /api/users/ {
        access_by_lua_block {
            local opts = {
                discovery = "http://keycloak:8080/realms/my-realm/.well-known/openid-configuration",
                token_signing_alg_values_expected = { "RS256" },
                accept_none_alg = false,
                accept_unsupported_alg = false,
            }

            -- Verify Bearer token
            local res, err = require("resty.openidc").bearer_jwt_verify(opts)

            if err or not res then
                ngx.status = 401
                ngx.header["Content-Type"] = "application/json"
                ngx.say('{"error": "Unauthorized", "message": "' .. (err or "invalid token") .. '"}')
                return ngx.exit(ngx.HTTP_UNAUTHORIZED)
            end

            -- Propagate user info to upstream via headers
            ngx.req.set_header("X-User-ID", res.sub)
            ngx.req.set_header("X-User-Name", res.preferred_username or "")
            ngx.req.set_header("X-User-Email", res.email or "")

            -- Extract và propagate roles
            local realm_roles = ""
            if res.realm_access and res.realm_access.roles then
                realm_roles = table.concat(res.realm_access.roles, ",")
            end
            ngx.req.set_header("X-User-Roles", realm_roles)
        }

        proxy_pass http://user-service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /api/orders/ {
        access_by_lua_block {
            local opts = {
                discovery = "http://keycloak:8080/realms/my-realm/.well-known/openid-configuration",
                token_signing_alg_values_expected = { "RS256" },
            }

            local res, err = require("resty.openidc").bearer_jwt_verify(opts)

            if err or not res then
                ngx.status = 401
                ngx.header["Content-Type"] = "application/json"
                ngx.say('{"error": "Unauthorized"}')
                return ngx.exit(ngx.HTTP_UNAUTHORIZED)
            end

            -- Kiểm tra role cần thiết
            local has_role = false
            if res.realm_access and res.realm_access.roles then
                for _, role in ipairs(res.realm_access.roles) do
                    if role == "USER" or role == "ADMIN" then
                        has_role = true
                        break
                    end
                end
            end

            if not has_role then
                ngx.status = 403
                ngx.header["Content-Type"] = "application/json"
                ngx.say('{"error": "Forbidden", "message": "Insufficient permissions"}')
                return ngx.exit(ngx.HTTP_FORBIDDEN)
            end

            ngx.req.set_header("X-User-ID", res.sub)
            ngx.req.set_header("X-User-Name", res.preferred_username or "")
        }

        proxy_pass http://order-service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

2.2 Approach 2: OAuth2 Proxy Sidecar

oauth2-proxy is a reverse proxy that provides authentication via OAuth2/OIDC providers. This approach does not require Lua and is easier to setup:

┌────────┐     ┌──────────────┐     ┌───────┐     ┌─────────────┐
│ Client │────▶│ Nginx        │────▶│OAuth2 │────▶│ Backend     │
│        │     │ (Reverse     │     │Proxy  │     │ Service     │
│        │     │  Proxy)      │     │       │     │             │
└────────┘     └──────────────┘     └───┬───┘     └─────────────┘
                                        │
                                  ┌─────▼─────┐
                                  │ Keycloak  │
                                  │           │
                                  └───────────┘

2.2.1 OAuth2 Proxy Configuration

# oauth2-proxy.cfg
provider = "keycloak-oidc"
provider_display_name = "Keycloak"

# Keycloak OIDC configuration
oidc_issuer_url = "http://keycloak:8080/realms/my-realm"
client_id = "oauth2-proxy-client"
client_secret = "your-client-secret"

# Cookie configuration
cookie_secret = "a-32-byte-base64-encoded-secret!"
cookie_secure = false  # true cho HTTPS
cookie_name = "_oauth2_proxy"

# Redirect URL
redirect_url = "http://localhost:4180/oauth2/callback"

# Upstream configuration
upstreams = [
  "http://user-service:8081"
]

# Email domain (cho phép tất cả)
email_domains = ["*"]

# Pass headers to upstream
set_xauthrequest = true
pass_access_token = true
pass_authorization_header = true

# Skip auth cho health endpoints
skip_auth_routes = [
  "^/api/public/",
  "^/health"
]

# Token refresh
cookie_refresh = "1m"

2.2.2 Nginx with auth_request

# nginx.conf - sử dụng auth_request directive
server {
    listen 80;
    server_name api.example.com;

    # OAuth2 Proxy endpoints
    location /oauth2/ {
        proxy_pass http://oauth2-proxy:4180;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Auth-Request-Redirect $request_uri;
    }

    location = /oauth2/auth {
        proxy_pass http://oauth2-proxy:4180;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Content-Length "";
        proxy_pass_request_body off;
    }

    # Public endpoints
    location /api/public/ {
        proxy_pass http://user-service:8081;
    }

    # Protected endpoints
    location /api/ {
        auth_request /oauth2/auth;

        # Pass auth headers to upstream
        auth_request_set $user $upstream_http_x_auth_request_user;
        auth_request_set $email $upstream_http_x_auth_request_email;
        auth_request_set $access_token $upstream_http_x_auth_request_access_token;

        proxy_set_header X-User $user;
        proxy_set_header X-Email $email;
        proxy_set_header Authorization "Bearer $access_token";

        # Error handling
        error_page 401 = /oauth2/sign_in;

        proxy_pass http://user-service:8081;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

3. Kong Gateway + Keycloak

3.1 Kong OIDC Plugin Configuration

Kong Gateway supports OIDC plugin (Kong Enterprise or community plugin) to validate JWT tokens from Keycloak:

# kong.yml - Declarative configuration
_format_version: "3.0"

services:
  # ===== User Service =====
  - name: user-service
    url: http://user-service:8081
    routes:
      - name: user-api
        paths:
          - /api/users
        strip_path: false
    plugins:
      - name: openid-connect
        config:
          issuer: http://keycloak:8080/realms/my-realm
          client_id:
            - my-kong-client
          client_secret:
            - your-client-secret
          auth_methods:
            - bearer
          bearer_token_param_type:
            - header
          # Chỉ verify bearer token (Resource Server mode)
          bearer_only: "yes"
          # Cache introspection results
          cache_introspection: true
          cache_token_exchange: true
          # Propagate consumer headers
          upstream_headers_claims:
            - sub
            - preferred_username
            - email
          upstream_headers_names:
            - X-User-ID
            - X-User-Name
            - X-User-Email

  # ===== Order Service =====
  - name: order-service
    url: http://order-service:8082
    routes:
      - name: order-api
        paths:
          - /api/orders
        strip_path: false
    plugins:
      - name: openid-connect
        config:
          issuer: http://keycloak:8080/realms/my-realm
          client_id:
            - my-kong-client
          client_secret:
            - your-client-secret
          auth_methods:
            - bearer
          bearer_only: "yes"
          # Scope và role check
          scopes_required:
            - openid
          roles_required:
            - USER

  # ===== Public Service (no auth) =====
  - name: public-service
    url: http://user-service:8081
    routes:
      - name: public-api
        paths:
          - /api/public
        strip_path: false

# ===== Rate Limiting =====
plugins:
  - name: rate-limiting
    config:
      minute: 100
      policy: local

3.2 Kong Admin API Configuration

Configuration via Kong Admin API instead of declarative:

# 1. Tạo service
curl -X POST http://localhost:8001/services \
  -d name=user-service \
  -d url=http://user-service:8081

# 2. Tạo route
curl -X POST http://localhost:8001/services/user-service/routes \
  -d 'name=user-api' \
  -d 'paths[]=/api/users' \
  -d 'strip_path=false'

# 3. Thêm OIDC plugin cho route
curl -X POST http://localhost:8001/routes/user-api/plugins \
  -d 'name=openid-connect' \
  -d 'config.issuer=http://keycloak:8080/realms/my-realm' \
  -d 'config.client_id=my-kong-client' \
  -d 'config.client_secret=your-client-secret' \
  -d 'config.auth_methods=bearer' \
  -d 'config.bearer_only=yes'

# 4. Thêm rate limiting
curl -X POST http://localhost:8001/routes/user-api/plugins \
  -d 'name=rate-limiting' \
  -d 'config.minute=60' \
  -d 'config.policy=local'

4. Traefik + Keycloak

4.1 ForwardAuth Middleware

Traefik uses ForwardAuth middleware to delegate authentication to an external service (OAuth2 Proxy):

# traefik.yml - Static configuration
api:
  dashboard: true
  insecure: true

entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"

providers:
  docker:
    exposedByDefault: false
  file:
    filename: /etc/traefik/dynamic.yml
# dynamic.yml - Dynamic configuration
http:
  middlewares:
    # ForwardAuth middleware - delegate auth to OAuth2 Proxy
    keycloak-auth:
      forwardAuth:
        address: "http://oauth2-proxy:4180/oauth2/auth"
        trustForwardHeader: true
        authResponseHeaders:
          - X-Auth-Request-User
          - X-Auth-Request-Email
          - X-Auth-Request-Access-Token
          - Authorization

    # Rate limiting
    rate-limit:
      rateLimit:
        average: 100
        burst: 50
        period: 1m

  routers:
    # Public routes (no auth)
    public-api:
      rule: "PathPrefix(`/api/public`)"
      service: user-service
      entryPoints:
        - web

    # Protected routes (with auth)
    user-api:
      rule: "PathPrefix(`/api/users`)"
      service: user-service
      entryPoints:
        - web
      middlewares:
        - keycloak-auth
        - rate-limit

    order-api:
      rule: "PathPrefix(`/api/orders`)"
      service: order-service
      entryPoints:
        - web
      middlewares:
        - keycloak-auth

    # OAuth2 Proxy routes
    oauth2-proxy:
      rule: "PathPrefix(`/oauth2`)"
      service: oauth2-proxy
      entryPoints:
        - web

  services:
    user-service:
      loadBalancer:
        servers:
          - url: "http://user-service:8081"

    order-service:
      loadBalancer:
        servers:
          - url: "http://order-service:8082"

    oauth2-proxy:
      loadBalancer:
        servers:
          - url: "http://oauth2-proxy:4180"

4.2 Traefik with Docker Labels

# docker-compose.yml snippet cho Traefik + Docker labels
services:
  user-service:
    image: user-service:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.user-api.rule=PathPrefix(`/api/users`)"
      - "traefik.http.routers.user-api.entrypoints=web"
      - "traefik.http.routers.user-api.middlewares=keycloak-auth@file"
      - "traefik.http.services.user-service.loadbalancer.server.port=8081"

5. Service Account Authentication

5.1 Client Credentials Grant

For service-to-service communication (without user context), use Client Credentials Grant:

┌──────────────┐                    ┌──────────────┐
│ Service A    │                    │ Keycloak     │
│ (Order)      │                    │              │
│              │── 1. client_id ───▶│              │
│              │   + client_secret  │              │
│              │                    │              │
│              │◀── 2. Access ──────│              │
│              │    Token           │              │
└──────┬───────┘                    └──────────────┘
       │
       │ 3. Bearer token
       ▼
┌──────────────┐
│ Service B    │
│ (Payment)    │
│              │
└──────────────┘

5.1.1 Keycloak Client Setup

Client Settings cho Service Account:
  Client ID:           order-service
  Client Protocol:     openid-connect
  Access Type:         confidential
  Service Accounts:    ON
  Standard Flow:       OFF (không cần user login)
  Direct Access:       OFF

  Service Account Roles:
    → Assign realm roles hoặc client roles cần thiết
    → Ví dụ: payment-read, payment-write

5.1.2 Get Service Account Token

# Client Credentials Grant
curl -s -X POST \
  "http://localhost:8080/realms/my-realm/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=order-service" \
  -d "client_secret=order-service-secret" \
  | jq .

# Response:
# {
#   "access_token": "eyJhbGci...",
#   "expires_in": 300,
#   "token_type": "Bearer",
#   "not-before-policy": 0,
#   "scope": "profile email"
# }

5.1.3 Service Account trong Spring Boot

package com.example.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.time.Instant;
import java.util.Map;

@Service
public class ServiceAccountTokenProvider {

    @Value("${keycloak.auth-server-url}")
    private String keycloakUrl;

    @Value("${keycloak.realm}")
    private String realm;

    @Value("${keycloak.service-account.client-id}")
    private String clientId;

    @Value("${keycloak.service-account.client-secret}")
    private String clientSecret;

    private final RestTemplate restTemplate = new RestTemplate();

    private String cachedToken;
    private Instant tokenExpiry;

    public synchronized String getServiceAccountToken() {
        // Return cached token nếu còn hạn
        if (cachedToken != null && Instant.now().isBefore(tokenExpiry)) {
            return cachedToken;
        }

        // Request new token
        String tokenUrl = String.format(
            "%s/realms/%s/protocol/openid-connect/token",
            keycloakUrl, realm
        );

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "client_credentials");
        body.add("client_id", clientId);
        body.add("client_secret", clientSecret);

        HttpEntity<MultiValueMap<String, String>> request =
            new HttpEntity<>(body, headers);

        ResponseEntity<Map> response = restTemplate.postForEntity(
            tokenUrl, request, Map.class
        );

        Map<String, Object> tokenResponse = response.getBody();
        cachedToken = (String) tokenResponse.get("access_token");
        int expiresIn = (Integer) tokenResponse.get("expires_in");
        // Refresh 30 giây trước khi hết hạn
        tokenExpiry = Instant.now().plusSeconds(expiresIn - 30);

        return cachedToken;
    }
}
// Sử dụng service account token để gọi downstream service
@Service
public class PaymentServiceClient {

    private final RestTemplate restTemplate = new RestTemplate();
    private final ServiceAccountTokenProvider tokenProvider;

    public PaymentServiceClient(ServiceAccountTokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    public PaymentResult processPayment(PaymentRequest request) {
        String token = tokenProvider.getServiceAccountToken();

        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(token);
        headers.setContentType(MediaType.APPLICATION_JSON);

        HttpEntity<PaymentRequest> entity = new HttpEntity<>(request, headers);

        ResponseEntity<PaymentResult> response = restTemplate.exchange(
            "http://payment-service:8083/api/payments",
            HttpMethod.POST,
            entity,
            PaymentResult.class
        );

        return response.getBody();
    }
}

5.2 Token Exchange (RFC 8693)

Token Exchange allows a service to exchange a user token to a new token with a different scope/audience, or impersonate a user when calling a downstream service:

# Bật Token Exchange trong Keycloak
# Realm Settings → Token → Token Exchange: Enable

# Exchange user token thành service-specific token
curl -s -X POST \
  "http://localhost:8080/realms/my-realm/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  -d "client_id=order-service" \
  -d "client_secret=order-service-secret" \
  -d "subject_token=$USER_ACCESS_TOKEN" \
  -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
  -d "audience=payment-service" \
  -d "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \
  | jq .

# Response chứa token mới với audience = payment-service
# Token vẫn giữ user identity nhưng scoped cho payment-service
Luồng Token Exchange:

User ──▶ Order Service (user token)
              │
              ├── Exchange user token → Keycloak
              │   (audience = payment-service)
              │
              ◀── New token (user context, scoped for payment)
              │
              ├── Call Payment Service (exchanged token)
              │
              ◀── Payment result

5.3 Internal Service Authentication Patterns

PatternUse CaseProsCons
JWT PropagationForward user token to downstreamSimple, user context preservedToken expiry issues
Client CredentialsService-to-service, no user contextIndependent of user sessionNo user identity
Token ExchangeImpersonation, audience restrictionUser context + scoped accessExtra Keycloak call
mTLSZero-trust service meshStrong identity, no tokens neededCertificate management

6. Complete Docker Compose Stack

Docker Compose stack complete with Keycloak, PostgreSQL, Nginx gateway and backend services:

# docker-compose.yml
version: '3.9'

services:
  # ===== PostgreSQL Database =====
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: keycloak_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U keycloak"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # ===== Keycloak =====
  keycloak:
    image: quay.io/keycloak/keycloak:25.0
    command: start-dev --import-realm
    environment:
      KC_DB: postgres
      KC_DB_URL_HOST: postgres
      KC_DB_URL_DATABASE: keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: keycloak_password
      KC_HOSTNAME: localhost
      KC_HOSTNAME_PORT: 8080
      KC_HTTP_ENABLED: "true"
      KC_HEALTH_ENABLED: "true"
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    volumes:
      - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json
    ports:
      - "8080:8080"
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && echo -e 'GET /health/ready HTTP/1.1\r\nHost: localhost\r\n\r\n' >&3 && cat <&3 | grep -q '200'"]
      interval: 30s
      timeout: 10s
      retries: 5
    networks:
      - backend

  # ===== API Gateway (Nginx + OAuth2 Proxy) =====
  oauth2-proxy:
    image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
    command:
      - --provider=keycloak-oidc
      - --provider-display-name=Keycloak
      - --oidc-issuer-url=http://keycloak:8080/realms/my-realm
      - --client-id=oauth2-proxy-client
      - --client-secret=oauth2-proxy-secret
      - --cookie-secret=bXktMzItYnl0ZS1iYXNlNjQtZW5jb2RlZC1zZWNyZXQ=
      - --cookie-secure=false
      - --email-domain=*
      - --upstream=static://202
      - --http-address=0.0.0.0:4180
      - --set-xauthrequest=true
      - --pass-access-token=true
      - --pass-authorization-header=true
      - --skip-auth-route=^/api/public/
      - --skip-provider-button=true
    depends_on:
      keycloak:
        condition: service_healthy
    networks:
      - backend

  nginx:
    image: nginx:1.27-alpine
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
    ports:
      - "80:80"
    depends_on:
      - oauth2-proxy
      - user-service
      - order-service
    networks:
      - backend

  # ===== Backend Services =====
  user-service:
    build:
      context: ./services/user-service
      dockerfile: Dockerfile
    environment:
      SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: http://keycloak:8080/realms/my-realm
      SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://keycloak:8080/realms/my-realm/protocol/openid-connect/certs
      SERVER_PORT: 8081
    ports:
      - "8081:8081"
    depends_on:
      keycloak:
        condition: service_healthy
    networks:
      - backend

  order-service:
    build:
      context: ./services/order-service
      dockerfile: Dockerfile
    environment:
      SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: http://keycloak:8080/realms/my-realm
      SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://keycloak:8080/realms/my-realm/protocol/openid-connect/certs
      SERVER_PORT: 8082
      # Service account cho gọi payment-service
      KEYCLOAK_SERVICE_ACCOUNT_CLIENT_ID: order-service
      KEYCLOAK_SERVICE_ACCOUNT_CLIENT_SECRET: order-service-secret
    ports:
      - "8082:8082"
    depends_on:
      keycloak:
        condition: service_healthy
    networks:
      - backend

volumes:
  postgres_data:

networks:
  backend:
    driver: bridge

6.1 Nginx Configuration cho Docker Compose

# nginx/conf.d/default.conf
upstream user-service {
    server user-service:8081;
}

upstream order-service {
    server order-service:8082;
}

upstream oauth2-proxy {
    server oauth2-proxy:4180;
}

server {
    listen 80;
    server_name localhost;

    # ===== OAuth2 Proxy endpoints =====
    location /oauth2/ {
        proxy_pass http://oauth2-proxy;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Auth-Request-Redirect $request_uri;
    }

    location = /oauth2/auth {
        proxy_pass http://oauth2-proxy;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Content-Length "";
        proxy_pass_request_body off;
    }

    # ===== Public APIs (no auth) =====
    location /api/public/ {
        proxy_pass http://user-service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # ===== Protected: User APIs =====
    location /api/users {
        auth_request /oauth2/auth;
        auth_request_set $user $upstream_http_x_auth_request_user;
        auth_request_set $email $upstream_http_x_auth_request_email;
        auth_request_set $token $upstream_http_x_auth_request_access_token;

        proxy_set_header X-User $user;
        proxy_set_header X-Email $email;
        proxy_set_header Authorization "Bearer $token";

        error_page 401 = /oauth2/sign_in;

        proxy_pass http://user-service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # ===== Protected: Order APIs =====
    location /api/orders {
        auth_request /oauth2/auth;
        auth_request_set $token $upstream_http_x_auth_request_access_token;

        proxy_set_header Authorization "Bearer $token";

        error_page 401 = /oauth2/sign_in;

        proxy_pass http://order-service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # ===== Health check =====
    location /health {
        return 200 '{"status":"UP"}';
        add_header Content-Type application/json;
    }
}

7. Monitoring and Rate Limiting

7.1 Monitor Service Account Tokens

# Kiểm tra active sessions cho service account
curl -s "http://localhost:8080/admin/realms/my-realm/clients/$CLIENT_UUID/service-account-user" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq .

# Liệt kê sessions
curl -s "http://localhost:8080/admin/realms/my-realm/users/$SERVICE_ACCOUNT_USER_ID/sessions" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq .

# Revoke tất cả sessions của service account
curl -X POST \
  "http://localhost:8080/admin/realms/my-realm/users/$SERVICE_ACCOUNT_USER_ID/logout" \
  -H "Authorization: Bearer $ADMIN_TOKEN"

7.2 Service Account Best Practices

#PracticeDescription
1Short-lived tokensSet Access Token Short Lifespan (5 minutes) for service accounts
2Least privilegeOnly assign minimum roles needed
3Rotate secretsChange client_secret periodically
4Token cachingCache token on client side, refresh before expiration
5Separate clientsEach service uses its own client, does not share credentials
6Network policiesRestrict network access between services
7Audit loggingLog service account token requests

7.3 Rate Limiting at Gateway

# Nginx rate limiting
http {
    # Định nghĩa rate limit zones
    # $binary_remote_addr: limit per IP
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

    # Limit per authenticated user (từ JWT sub claim)
    limit_req_zone $http_x_user_id zone=user_limit:10m rate=30r/s;

    server {
        # Apply rate limiting cho API endpoints
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
            limit_req zone=user_limit burst=50 nodelay;

            limit_req_status 429;

            # Custom error response cho rate limit
            error_page 429 = @rate_limited;
            # ... proxy_pass ...
        }

        location @rate_limited {
            default_type application/json;
            return 429 '{"error": "Too Many Requests", "message": "Rate limit exceeded. Please retry after a moment."}';
        }
    }
}

8. Summary

GatewayComplexityPerformanceBest For
Nginx + lua-resty-openidcMediumVery HighHigh-traffic, custom logic
Nginx + OAuth2 ProxyLowHighQuick setup, simple auth
KongMediumHighEnterprise, plugin ecosystem
TraefikLowHighCloud-native, Kubernetes

API Gateway + Keycloak Deployment Checklist:

#ItemStatus
1JWT validation at gateway (JWKS caching)☐
2Propagate user headers to downstream services☐
3Service accounts cho internal communication☐
4Rate limiting per IP and per user☐
5CORS configuration at gateway☐
6Health check endpoints (bypass auth)☐
7TLS termination at gateway☐
8Logging and monitoring☐
9Client secret rotation strategy☐
10Token expiry and refresh handling☐

The next series will delve into advanced topics such as Keycloak clustering, production deployment and performance tuning.