Kaique Mitsuo Silva Yamamoto
Arquitetura softwareSeguranca

Keycloak com Microserviços

Em arquiteturas de microserviços, o Keycloak atua como provedor central de identidade (IdP). Cada serviço valida tokens JWT localmente, sem round-trip ao Keycloak em cada requisição.


Arquitetura de Referência

                        ┌─────────────────┐
                        │    Keycloak      │
                        │  (IdP / AS)      │
                        └────────┬────────┘
                                 │ JWKS / Token endpoint
                 ┌───────────────┼───────────────┐
                 │               │               │
         ┌───────▼──────┐        │        ┌──────▼──────┐
         │  API Gateway  │        │        │   Service   │
         │    (Kong)     │        │        │    Mesh     │
         └───────┬───────┘        │        └──────┬──────┘
                 │ Bearer token   │               │
      ┌──────────┼──────────┐     │    ┌──────────┼──────────┐
      │          │          │     │    │          │          │
 ┌────▼────┐ ┌───▼────┐ ┌───▼────┐│ ┌──▼─────┐ ┌─▼──────┐ ┌▼───────┐
 │ User    │ │Order   │ │Payment ││ │Catalog │ │Inventory│ │Notify  │
 │ Service │ │Service │ │Service ││ │Service │ │ Service │ │Service │
 └─────────┘ └────────┘ └────────┘│ └────────┘ └─────────┘ └────────┘
      Validam JWT localmente (sem chamar Keycloak)

Fluxo típico:

  1. Cliente autentica no Keycloak → recebe access token JWT
  2. API Gateway valida o token (assinatura + claims básicos)
  3. Gateway propaga Bearer token downstream
  4. Cada microserviço valida o token localmente via JWKS cacheado

Token Propagation (Bearer Downstream)

O API Gateway (ou o primeiro serviço) passa o token original para serviços internos via header Authorization: Bearer.

// Express middleware — propagar token recebido
import { Request, Response, NextFunction } from 'express'

export function propagateToken(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization
  if (authHeader) {
    // Repassar para chamadas downstream
    req.app.locals.authHeader = authHeader
  }
  next()
}

// Cliente HTTP com token propagado
async function callOrderService(authHeader: string, orderId: string) {
  const response = await fetch(`http://order-service/orders/${orderId}`, {
    headers: {
      Authorization: authHeader,
      'Content-Type': 'application/json',
    },
  })
  return response.json()
}

Service-to-Service via Client Credentials

Quando serviços precisam se comunicar sem um usuário humano, usam Client Credentials com service accounts dedicadas.

// token-manager.ts
class TokenManager {
  private cachedToken: string | null = null
  private expiresAt: number = 0

  async getToken(): Promise<string> {
    // Reusar token enquanto válido (com margem de 30s)
    if (this.cachedToken && Date.now() < this.expiresAt - 30_000) {
      return this.cachedToken
    }

    const response = await fetch(
      `${process.env.KEYCLOAK_URL}/realms/${process.env.REALM}/protocol/openid-connect/token`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
          grant_type: 'client_credentials',
          client_id: process.env.CLIENT_ID!,
          client_secret: process.env.CLIENT_SECRET!,
          scope: 'api:internal',
        }),
      }
    )

    const data = await response.json()
    this.cachedToken = data.access_token
    this.expiresAt = Date.now() + data.expires_in * 1000
    return this.cachedToken!
  }
}

export const tokenManager = new TokenManager()
// Usar em qualquer serviço
const token = await tokenManager.getToken()
const result = await fetch('http://payment-service/charge', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ amount: 100 }),
})

Token Exchange (RFC 8693)

Token Exchange permite que um serviço obtenha um token com identidade diferente — útil para impersonation e delegation.

# Serviço A troca seu token por um token atuando como o usuário original
POST /realms/myrealm/protocol/openid-connect/token

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<ACCESS_TOKEN_DO_USUARIO>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&requested_token_type=urn:ietf:params:oauth:token-type:access_token
&audience=target-service
&client_id=service-a
&client_secret=s3cr3t

Habilitar no Keycloak:

  • Realm Settings → Tokens → Token Exchange → Enable

Casos de uso:

  • Impersonation: admin atua em nome de usuário para debug
  • Delegation: serviço A atua em nome do usuário B ao chamar serviço C
  • Narrowing: reduzir escopos do token para menor privilégio

Integração com Kong API Gateway

# kong.yml — declarative config
services:
  - name: order-service
    url: http://order-service:3000

routes:
  - name: orders-route
    service: order-service
    paths:
      - /api/orders

plugins:
  - name: oidc
    service: order-service
    config:
      client_id: kong-gateway
      client_secret: ${KONG_CLIENT_SECRET}
      discovery: https://auth.example.com/realms/myrealm/.well-known/openid-configuration
      introspection_endpoint: https://auth.example.com/realms/myrealm/protocol/openid-connect/token/introspect
      bearer_only: "yes"
      realm: myrealm
      redirect_after_logout_uri: /

Kong valida o token antes de rotear para o serviço — os serviços internos confiam no header X-Forwarded-User injetado pelo gateway.


Sidecar Proxy / Envoy + Keycloak

# envoy.yaml — ext_authz para validação centralizada
http_filters:
  - name: envoy.filters.http.ext_authz
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
      grpc_service:
        envoy_grpc:
          cluster_name: keycloak_authz
        timeout: 0.25s
      failure_mode_allow: false

clusters:
  - name: keycloak_authz
    connect_timeout: 0.25s
    type: LOGICAL_DNS
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: keycloak_authz
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: keycloak-authz-adapter
                    port_value: 8081

Kubernetes: Validação via OIDC

# kube-apiserver flags
--oidc-issuer-url=https://auth.example.com/realms/myrealm
--oidc-client-id=kubernetes
--oidc-username-claim=sub
--oidc-groups-claim=groups
--oidc-ca-file=/etc/ssl/certs/keycloak-ca.pem
# ClusterRoleBinding — mapear grupo Keycloak para ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: keycloak-admins
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: Group
    name: k8s-admins  # grupo no token Keycloak
    apiGroup: rbac.authorization.k8s.io

Exemplo Completo: Microserviço Node.js

// src/middleware/auth.ts
import { createRemoteJWKSet, jwtVerify } from 'jose'
import type { Request, Response, NextFunction } from 'express'

const KEYCLOAK_URL = process.env.KEYCLOAK_URL!
const REALM = process.env.KEYCLOAK_REALM!
const AUDIENCE = process.env.SERVICE_NAME!

const JWKS = createRemoteJWKSet(
  new URL(`${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/certs`)
)

export interface AuthRequest extends Request {
  user?: {
    sub: string
    email: string
    roles: string[]
  }
}

export async function authenticate(
  req: AuthRequest,
  res: Response,
  next: NextFunction
) {
  const authHeader = req.headers.authorization
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Token ausente' })
  }

  const token = authHeader.slice(7)

  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: `${KEYCLOAK_URL}/realms/${REALM}`,
      audience: AUDIENCE,
    })

    req.user = {
      sub: payload.sub as string,
      email: payload.email as string,
      roles: (payload.realm_access as any)?.roles ?? [],
    }

    next()
  } catch (err) {
    return res.status(401).json({ error: 'Token inválido' })
  }
}

export function requireRole(role: string) {
  return (req: AuthRequest, res: Response, next: NextFunction) => {
    if (!req.user?.roles.includes(role)) {
      return res.status(403).json({ error: 'Permissão insuficiente' })
    }
    next()
  }
}
// src/routes/orders.ts
import { Router } from 'express'
import { authenticate, requireRole } from '../middleware/auth'

const router = Router()

router.use(authenticate)

router.get('/', async (req, res) => {
  // req.user disponível após middleware
  res.json({ orders: [], user: req.user?.sub })
})

router.delete('/:id', requireRole('admin'), async (req, res) => {
  res.json({ deleted: req.params.id })
})

export default router

Recursos

On this page