Token Exchange Demo

Como a plataforma vai chamar o back-v2 em nome do usuário logado — sem o browser ver token nenhum. (InAuth, ADR-007)

verificando sessão…
browser ──cookie httpOnly──▶ BFF (este servidor, papel do inbot-admin-backend)
                                │  troca o token no InAuth (grant token-exchange)
                                └──Bearer aud=https://…/rs──▶ resource server (/rs, papel do back-v2)

Nenhum token passa pelo JavaScript. O browser só tem um cookie de sessão. Quem obtém o token certo pro back-v2 é o backend da plataforma, e o back-v2 só aceita token cujo aud é a própria URL dele.

Experimente

O BFF pega o cookie, troca o token do usuário por um com aud = https://token-exchange-demo.in.bot/rs e chama o resource server. Volta quem você é: sub, email, org_id, groups, app_role — é com isso que o back-v2 autoriza. Repare em aud (o destinatário) e azp (quem pediu a troca).

—

O BFF manda o mesmo token do cookie, sem trocar. O resource server recusa: o aud desse token é o app da plataforma, não ele. É por isso que não se faz pass-through — um token que vaza de um app não pode valer em outro serviço.

—

O browser chamando o "back-v2" direto, só com o cookie: 401. O cookie não é credencial pro resource server; só o Bearer trocado é.

—

O que é cada coisa (repo in-bot/inauth)

ArquivoPapel
examples/token-exchange/resource-server.jsO back-v2 em miniatura (~40 linhas, Express + jose): valida o JWT pelo JWKS do InAuth exigindo iss, aud = a própria URL, RS256, exp. O InAuthGuard do NestJS é isso traduzido.
examples/token-exchange/bff.jsO inbot-admin-backend em miniatura: login OIDC → cookie httpOnly; /whoami troca o token e chama o resource server (é o proxy /api/v2/* reduzido a uma rota).
examples/token-exchange/server.jsOs dois no mesmo processo, pra esta demo ficar no ar.
docs/token-exchange-bff.mdA receita completa dos dois lados (regras do proxy, erros, claims disponíveis).
docs/adr/007-token-exchange-bff.mdPor que assim e não de outro jeito (alternativas descartadas, consequências).
test/unit/routes/token-exchange-chain.test.jsEsta cadeia inteira rodando no CI, sem servidor: cookie → exchange → JWKS → claims; passthrough recusado.

Como fica no back-v2 (o básico)

Um guard novo, em arquivo próprio, ao lado dos que já existem. Nada dos guards atuais muda.

// src/guards/inauth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(new URL(process.env.INAUTH_JWKS_URL!)); // https://inauth.in.bot/certs — cache por kid

@Injectable()
export class InAuthGuard implements CanActivate {
  async canActivate(ctx: ExecutionContext) {
    const req = ctx.switchToHttp().getRequest();
    const [type, token] = (req.headers.authorization ?? '').split(' ');
    if (type !== 'Bearer' || !token) throw new UnauthorizedException();
    try {
      const { payload } = await jwtVerify(token, JWKS, {
        issuer: 'inauth.in.bot',
        audience: 'https://api.inbot.com.br/v2',   // a própria URL — nunca o client da plataforma
        algorithms: ['RS256'],
      });
      req.user = payload; // sub, email, org_id, 'inauth:groups', app_permissions, azp
      return true;
    } catch { throw new UnauthorizedException(); }
  }
}

Próximos passos

  1. back-v2: InAuthGuard + GET /api/inauth/whoami devolvendo os claims (piloto, zero risco).
  2. inbot-admin-backend: proxy /api/v2/* — requireAuth + requireBotAccess(botId) + allowlist de rotas; troca o token (cache até expires_in); sobrescreve Authorization, não repassa cookies, valida Origin nas mutações. Começa com só whoami na allowlist.
  3. SPA: client campaigns passa a apontar pra /api/v2. Se /api/v2/inauth/whoami mostrar seu e-mail, a cadeia está provada.
  4. Primeiro endpoint real: template-whatsapp. Daí é repetir por endpoint (trocar o guard no back-v2, adicionar a rota na allowlist do BFF).

Independente disso: os endpoints hoje sem guard nenhum (auth/access-key, whatsapp-bot-config/configs, teams, templateTrigger, dashboard) precisam ser fechados com os guards que já existem e as keys rotacionadas — isso não espera o plano.

sair · demo temporária, sai do ar quando o back-v2 tiver o guard