Secuencia — Client Credentials¶
El flujo Client Credentials es el estándar OAuth 2.0 para comunicación máquina a máquina (M2M) donde no hay intervención de usuario. El cliente se autentica directamente con sus credenciales (client_id + client_secret) y obtiene un access token para consumir APIs protegidas. En esta plataforma, el cliente partner-service-m2m utiliza este flujo para que servicios backend de socios consuman la API de integración.
Diagrama de secuencia¶
sequenceDiagram
autonumber
participant Svc as Servicio Backend<br/>(partner-service-m2m)
participant Traefik as Traefik v3<br/>:443
participant KC as Keycloak 26<br/>realm-partners
participant API as Resource API<br/>(Spring WebFlux)
Note over Svc: Verificación de token en caché
Svc->>Svc: ¿Tiene access_token válido en caché?<br/>exp - 30s > now()
alt Token en caché válido
Note over Svc: Reutiliza token existente
Svc->>Svc: Usa access_token del caché
else Sin token o expirado
Note over Svc,KC: Solicitud de nuevo token
Svc->>Traefik: POST /realms/realm-partners/protocol/openid-connect/token<br/>Content-Type: application/x-www-form-urlencoded<br/>Authorization: Basic BASE64(client_id:client_secret)<br/>Body: grant_type=client_credentials&scope=api:read api:write
Traefik->>KC: Proxy POST (TLS terminado en Traefik)
Note over KC: Validación de credenciales del cliente
KC->>KC: Verifica client_id existe en realm-partners<br/>Verifica client_secret (hash bcrypt)<br/>Verifica que el cliente tiene el grant_type habilitado<br/>Verifica scopes solicitados vs scopes permitidos
KC-->>Traefik: 200 OK
Traefik-->>Svc: JSON con access_token (JWT)<br/>expires_in: 300<br/>token_type: Bearer<br/>scope: api:read api:write
Svc->>Svc: Guarda token en caché local<br/>con TTL = expires_in - 30s
end
Note over Svc,API: Llamada a la API protegida
Svc->>Traefik: GET /api/v1/partners/data<br/>Authorization: Bearer ACCESS_TOKEN_JWT
Traefik->>API: Proxy con header Bearer intacto
Note over API: Validación del JWT
API->>API: Verifica firma RS256 (JWKS cache)<br/>Verifica iss == https://kc.empresa.com/realms/realm-partners<br/>Verifica aud incluye la API<br/>Verifica exp > now()<br/>Verifica claims de servicio (azp, scope)
alt JWT válido
API-->>Traefik: 200 OK — Datos del recurso
Traefik-->>Svc: Respuesta JSON
else JWT inválido o expirado
API-->>Traefik: 401 Unauthorized<br/>WWW-Authenticate: Bearer error="invalid_token"
Traefik-->>Svc: 401 Unauthorized
Svc->>Svc: Invalida caché, reintenta flujo
end
Parámetros del Token Request¶
| Parámetro | Valor | Obligatorio | Descripción |
|---|---|---|---|
grant_type |
client_credentials |
Sí | Tipo de grant M2M |
scope |
api:read api:write |
Recomendado | Scopes solicitados; deben estar configurados en el cliente |
client_id |
partner-service-m2m |
Sí* | En body si no se usa Basic Auth |
client_secret |
s3cr3t-value-rotate-periodically |
Sí | Secreto del cliente confidencial |
*La autenticación del cliente puede ser en header Authorization: Basic (recomendado) o en el body como client_id + client_secret.
Respuesta del Token Endpoint¶
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0yMDI0In0.eyJpc3MiOiJodHRwczovL2tjLmVtcHJlc2EuY29tL3JlYWxtcy9yZWFsbS1wYXJ0bmVycyIsInN1YiI6ImE4YjljMTBkIiwiYXpwIjoicGFydG5lci1zZXJ2aWNlLW0ybSIsInNjb3BlIjoiYXBpOnJlYWQgYXBpOndyaXRlIiwiZXhwIjoxNzIyMDAwMzAwfQ...",
"expires_in": 300,
"token_type": "Bearer",
"scope": "api:read api:write",
"not-before-policy": 0
}
Sin refresh_token en Client Credentials
Este flujo no emite refresh token. Cuando el access token expira, el servicio simplemente solicita uno nuevo con sus credenciales. No hay estado de sesión de usuario que preservar.
Claims del JWT para clientes M2M¶
{
"iss": "https://kc.empresa.com/realms/realm-partners",
"sub": "a8b9c10d-1234-5678-abcd-ef0123456789",
"azp": "partner-service-m2m",
"aud": ["partner-api", "account"],
"scope": "api:read api:write",
"exp": 1722000300,
"iat": 1722000000,
"jti": "unique-token-id-abcd1234",
"realm_access": {
"roles": ["service-account-partner-service-m2m"]
},
"resource_access": {
"partner-api": {
"roles": ["data-reader", "data-writer"]
}
}
}
Ejemplo con curl¶
# Solicitar access token con Client Credentials
TOKEN=$(curl -s \
-X POST "https://kc.empresa.com/realms/realm-partners/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "partner-service-m2m:${CLIENT_SECRET}" \
-d "grant_type=client_credentials" \
-d "scope=api:read api:write" \
| jq -r '.access_token')
echo "Token obtenido: ${TOKEN:0:50}..."
# Usar el token para llamar a la API
curl -s \
-X GET "https://api.empresa.com/v1/partners/data" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Accept: application/json" \
| jq .
# Script completo con manejo de caché y rotación automática
#!/bin/bash
TOKEN_FILE="/tmp/.m2m_token_cache"
TOKEN_EXPIRY_FILE="/tmp/.m2m_token_expiry"
get_token() {
local now
now=$(date +%s)
# Revisar caché: si el token vence en más de 30 segundos, reutilizarlo
if [[ -f "$TOKEN_FILE" && -f "$TOKEN_EXPIRY_FILE" ]]; then
local expiry
expiry=$(cat "$TOKEN_EXPIRY_FILE")
if (( expiry - now > 30 )); then
cat "$TOKEN_FILE"
return 0
fi
fi
# Solicitar nuevo token
local response
response=$(curl -s \
-X POST "https://kc.empresa.com/realms/realm-partners/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-d "grant_type=client_credentials&scope=api:read api:write")
local token expires_in
token=$(echo "$response" | jq -r '.access_token')
expires_in=$(echo "$response" | jq -r '.expires_in')
echo "$token" > "$TOKEN_FILE"
echo $(( now + expires_in )) > "$TOKEN_EXPIRY_FILE"
echo "$token"
}
TOKEN=$(get_token)
curl -s "https://api.empresa.com/v1/partners/data" \
-H "Authorization: Bearer ${TOKEN}"
Configuración en Spring WebFlux (cliente M2M)¶
// Configuración del cliente HTTP con renovación automática de tokens
@Configuration
public class M2MWebClientConfig {
@Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
var provider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
var manager = new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientRepository);
manager.setAuthorizedClientProvider(provider);
return manager;
}
@Bean
public WebClient partnerApiWebClient(ReactiveOAuth2AuthorizedClientManager manager) {
var filter = new ServerOAuth2AuthorizedClientExchangeFilterFunction(manager);
filter.setDefaultClientRegistrationId("partner-service-m2m");
return WebClient.builder()
.baseUrl("https://api.empresa.com")
.filter(filter)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
# application.yml — registro del cliente OAuth2
spring:
security:
oauth2:
client:
registration:
partner-service-m2m:
client-id: partner-service-m2m
client-secret: ${M2M_CLIENT_SECRET}
authorization-grant-type: client_credentials
scope: api:read, api:write
provider:
keycloak-partners:
token-uri: https://kc.empresa.com/realms/realm-partners/protocol/openid-connect/token
Cuándo usar Client Credentials vs otros flujos¶
| Criterio | Client Credentials | Authorization Code + PKCE | Refresh Token |
|---|---|---|---|
| Intervención de usuario | No | Sí | No (después del login inicial) |
| Tipo de cliente | Servicio backend confidencial | SPA / app pública | Cualquier cliente con sesión |
| Guarda secreto | Sí (variable de entorno) | No | No aplica (es un complemento) |
| Refresh token emitido | No | Sí (con offline_access) |
N/A |
| Caso de uso típico | Cron jobs, microservicios, ETL | Portal web de usuario | Mantener sesión activa |
| Duración del access token | Corta (5 min) | Corta (5 min) | Permite renovar sin re-login |
Rotación de client_secret
El client_secret debe rotarse periódicamente (cada 90 días como mínimo) y almacenarse como secreto de Docker o variable de entorno cifrada. Nunca lo incluyas en código fuente, imágenes Docker o logs. Keycloak soporta múltiples secretos simultáneos durante la transición.
No uses Client Credentials en clientes públicos
Las SPAs y aplicaciones móviles no pueden proteger secretos. Si necesitas M2M desde el frontend, usa un BFF (Backend for Frontend) que centralice las credenciales y exponga una sesión autenticada al navegador.
Monitoreo de tokens M2M
Configura alertas en Keycloak Events para detectar fallos repetidos de autenticación de clientes. Un número elevado de errores client_credentials puede indicar una credencial comprometida o rotación pendiente.