Receita — API x API (Machine to Machine)
Cenário: o Serviço A precisa chamar o Serviço B em background (job, cron, webhook outbound) sem ter um usuário logado. Usa OAuth 2.0 Client Credentials.
O que você vai construir
Serviço A (worker)
│
├─ 1. POST /token grant_type=client_credentials
│ │
│ └─ Auth Platform valida client_id + client_secret
│ └─ retorna access_token JWT (sub = client_id)
│
├─ 2. Cacheia o token (renova 60s antes de expirar)
│
└─ 3. GET /dados Authorization: Bearer <token>
│
Serviço B valida o JWT via JWKS
└─ sem roundtrip ao Auth Platform
Pré-requisitos
Criar um sistema M2M no Auth Platform:
curl -X POST http://localhost:4000/admin/systems \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Serviço Worker",
"redirectUris": [],
"allowClientCredentials": true,
"allowSelfRegister": false,
"requireAdminActivation": false
}'
# → guarde clientId e clientSecret (mostrado UMA vez)
Passo 1 — Cliente de token com cache
// src/service-token.ts
interface TokenCache {
token: string;
expiresAt: number; // ms
}
export class ServiceTokenClient {
private readonly issuer: string;
private readonly clientId: string;
private readonly clientSecret: string;
private cache: TokenCache | null = null;
constructor() {
this.issuer = process.env.AUTH_URL ?? 'http://localhost:4000';
this.clientId = process.env.SERVICE_CLIENT_ID ?? '';
this.clientSecret = process.env.SERVICE_CLIENT_SECRET ?? '';
if (!this.clientId || !this.clientSecret) {
throw new Error('SERVICE_CLIENT_ID e SERVICE_CLIENT_SECRET são obrigatórios');
}
}
/**
* Retorna token válido.
* Renova automaticamente 60s antes de expirar.
*/
async getToken(): Promise<string> {
if (this.cache && this.cache.expiresAt - 60_000 > Date.now()) {
return this.cache.token;
}
return this.fetchNewToken();
}
private async fetchNewToken(): Promise<string> {
const res = await fetch(`${this.issuer}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
resource: this.issuer, // audience = issuer (padrão)
scope: 'openid',
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({})) as any;
throw new Error(`Token failed ${res.status}: ${err?.error_description ?? ''}`);
}
const data = await res.json() as { access_token: string; expires_in: number };
this.cache = {
token: data.access_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
return data.access_token;
}
}
// Singleton — reutilizar em toda a aplicação
export const serviceToken = new ServiceTokenClient();
Passo 2 — Chamar o Serviço B
// src/api-client.ts
import { serviceToken } from './service-token';
export async function callServiceB<T>(path: string): Promise<T> {
const token = await serviceToken.getToken();
const res = await fetch(`${process.env.SERVICE_B_URL}${path}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`Service B error ${res.status}`);
return res.json() as Promise<T>;
}
// Uso em qualquer lugar do Serviço A:
// const dados = await callServiceB<Dados[]>('/dados');
Passo 3 — Serviço B valida o token
// Serviço B — src/auth.guard.ts
import { createRemoteJWKSet, jwtVerify } from 'jose';
const AUTH_URL = process.env.AUTH_URL ?? 'http://localhost:4000';
const jwks = createRemoteJWKSet(new URL(`${AUTH_URL}/jwks`));
export async function validateM2MToken(authHeader: string) {
const token = authHeader.replace('Bearer ', '');
const { payload } = await jwtVerify(token, jwks, {
issuer: AUTH_URL,
audience: AUTH_URL,
});
// Token M2M tem sub = clientId (não um userId)
// Verifique se veio do cliente correto se necessário:
if (payload.sub !== process.env.ALLOWED_CLIENT_ID) {
throw new Error('cliente não autorizado');
}
return payload;
}
Passo 4 — Token Exchange (opcional — escopo reduzido)
Se o Serviço A tem um token de usuário e precisa chamar o Serviço B em nome desse usuário com menos escopos:
import { exchangeToken } from './token-exchange';
// O Serviço A tem o token do usuário (scope=*)
// Troca por um token com escopo reduzido para o Serviço B
const exchanged = await exchangeToken({
subjectToken: userAccessToken,
targetAudience: process.env.SERVICE_B_CLIENT_ID!,
scope: 'fin:read', // subset dos escopos originais
});
// Token trocado tem:
// sub = userId (identidade original propagada)
// act = { sub: clientId do Serviço A } (quem fez a troca)
// scope = fin:read
// exp = máx 10 min
await callServiceB(exchanged.access_token);
Veja examples/05-token-exchange para a implementação completa do Token Exchange.
Variáveis de ambiente
# Serviço A
AUTH_URL=http://localhost:4000
SERVICE_CLIENT_ID=seu-client-id
SERVICE_CLIENT_SECRET=seu-client-secret
SERVICE_B_URL=http://servico-b:3001
# Serviço B
AUTH_URL=http://localhost:4000
ALLOWED_CLIENT_ID=client-id-do-servico-a # opcional
Checklist
- Sistema criado com
allowClientCredentials: true -
clientSecretguardado de forma segura (não no código) - Token cacheado — não chamar
/tokena cada request -
resourcepassado no body do/tokenpara o audience correto - Serviço B valida
iss,audeexpdo JWT
Exemplo funcional:
examples/04-client-credentialseexamples/m2m-service