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:
- Cliente autentica no Keycloak → recebe access token JWT
- API Gateway valida o token (assinatura + claims básicos)
- Gateway propaga Bearer token downstream
- 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=s3cr3tHabilitar 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: 8081Kubernetes: 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.ioExemplo 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