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
  • clientSecret guardado de forma segura (não no código)
  • Token cacheado — não chamar /token a cada request
  • resource passado no body do /token para o audience correto
  • Serviço B valida iss, aud e exp do JWT

Exemplo funcional: examples/04-client-credentials e examples/m2m-service