⚡ AutomationsAI|Portal de Cursos →

Verificando acesso...

MÓDULO 1.3

🔐 OAuth e tokens

Como redes sociais autenticam apps sem trocar senha: do consent screen ao refresh automático, com tokens criptografados em repouso.

6
Tópicos
40
Minutos
Básico
Nível
Teoria
Tipo
1

🔑 OAuth 2.0 em 1 minuto

OAuth 2.0 é o protocolo que permite ao Postiz postar no seu LinkedIn sem nunca ver sua senha. Em vez disso, o usuário aprova um consent screen, e o provedor devolve um access token com escopo limitado (ex.: "publicar posts", mas não "ler mensagens privadas").

O fluxo mais usado para apps com backend é o Authorization Code Flow: o usuário é redirecionado para o provedor, autoriza, volta para uma redirect URI registrada com um code de uso único, e o backend troca esse code por um token.

Fluxo Authorization Code passo a passo

1

App redireciona usuário

Postiz monta uma URL para o provedor com client_id, redirect_uri, scope e state.

2

Provedor exibe consent screen

"O app X quer publicar no seu LinkedIn. Permitir?" Usuário aprova ou nega.

3

Redirect com code

Provedor redireciona para redirect_uri?code=ABC&state=XYZ. O state precisa bater para evitar CSRF.

4

Backend troca code por token

POST server-to-server com client_secret. Provedor devolve access_token + refresh_token + expires_in.

5

App usa o access token

Cada request à API do provedor inclui Authorization: Bearer <token>. Sem senha, sem cookie.

Conceitos-chave

Client ID

Identificador público do app no provedor.

Redirect URI

URL fixa registrada — qualquer outra é rejeitada.

Scope

Permissões específicas pedidas no consent.

State

Token anti-CSRF que volta no redirect.

2

🎫 Access token vs refresh token

O provedor devolve dois tokens com propósitos opostos. Confundir os dois é o erro nº1 de quem começa: vazar um access token é incômodo (expira em horas), vazar um refresh token é incidente de segurança grave (vale meses).

📊 Comparação por provedor

  • Meta (Facebook/Instagram): access token de curta duração (~1h) + long-lived token (~60 dias). Não há refresh token clássico — você "troca" o short-lived pelo long-lived.
  • LinkedIn: access token 60 dias, refresh token 365 dias. Renovação obrigatória para apps que querem postar continuamente.
  • X (Twitter): access token 2h, refresh token rotativo (cada uso emite um novo refresh).
  • Google/YouTube: access token 1h, refresh token sem expiração (até o usuário revogar).

🎫 Access token

  • Usado em cada chamada à API.
  • Curta duração (1h a 60 dias).
  • Pode ser JWT (legível) ou opaco.
  • Vai no header Authorization: Bearer.
  • Quando expira → status 401.

♻️ Refresh token

  • Usado só para obter novo access token.
  • Longa duração (meses ou indefinido).
  • Sempre opaco — nunca decifre.
  • Trocado no endpoint /token.
  • Se vazar → revogar TUDO imediatamente.

⏱️ Quando renovar?

Não espere o 401. Renove proativamente 5 minutos antes do expires_at. Isso evita falhas em meio a posts agendados e burst de retries que disparam rate limit do provedor.

Conceitos-chave

expires_in

Segundos até o access token virar pó.

Bearer

Quem segura o token, tem o acesso — nada mais.

Rotativo

Cada refresh emite um novo refresh token.

Revogação

Endpoint para invalidar tokens vazados.

3

🔒 Como guardar tokens (AES-256-GCM)

Tokens em texto puro no banco é o mesmo que guardar senhas em texto puro: catastrófico se houver leak. O padrão da indústria é encrypt at rest com AES-256-GCM, usando uma chave mestra que vive em variável de ambiente — nunca no código.

AES-256-GCM dá dois benefícios: criptografia simétrica forte (256 bits) e autenticação (GCM detecta se o ciphertext foi adulterado). Sempre use IV aleatório por registro — reusar IV em GCM quebra o esquema inteiro.

// crypto.service.ts — encrypt/decrypt de tokens
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

const KEY = Buffer.from(process.env.TOKEN_ENCRYPTION_KEY!, 'hex'); // 32 bytes

export function encryptToken(plaintext: string): string {
  const iv = randomBytes(12);                              // 96 bits, único por registro
  const cipher = createCipheriv('aes-256-gcm', KEY, iv);
  const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  // formato: iv.tag.ciphertext (todos em base64)
  return [iv, tag, enc].map(b => b.toString('base64')).join('.');
}

export function decryptToken(blob: string): string {
  const [iv, tag, enc] = blob.split('.').map(s => Buffer.from(s, 'base64'));
  const decipher = createDecipheriv('aes-256-gcm', KEY, iv);
  decipher.setAuthTag(tag);
  return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf8');
}

✓ O que FAZER

  • Gerar chave com openssl rand -hex 32.
  • Guardar a chave em variável de ambiente, KMS ou Vault.
  • IV aleatório novo a cada criptografia.
  • Marcar coluna encrypted_token como TEXT (não VARCHAR(255)).

✗ O que NÃO fazer

  • Commitar a chave (mesmo em .env.example).
  • Logar tokens em arquivo, Sentry ou stdout.
  • Usar a mesma chave em dev e prod.
  • Salvar o IV fora do registro do token.

Conceitos-chave

AES-256-GCM

Cifra simétrica + autenticação integrada.

IV

Initialization vector — único por registro.

Auth Tag

Assinatura que detecta adulteração.

Key rotation

Trocar a chave-mestra periodicamente.

4

♻️ Rotina de refresh automático

Postar a 1h da manhã com token expirado é o pesadelo clássico. A solução é um interceptor que checa expires_at antes de cada chamada e renova proativamente, mais um retry com novo token caso a API retorne 401 mesmo assim.

O exemplo abaixo é o coração de qualquer integração OAuth séria: três blocos em ordem — preflight check, chamada, retry on 401.

// social-api.client.ts — request com refresh automático
async function apiCall(account: SocialAccount, url: string, body: any) {
  // 1) Preflight: renova se faltam menos de 5 min
  if (account.expiresAt.getTime() - Date.now() < 5 * 60_000) {
    await refreshAccessToken(account);
  }

  let token = decryptToken(account.accessToken);

  // 2) Chamada
  let res = await fetch(url, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });

  // 3) Retry com novo token se o provedor expirou antes do esperado
  if (res.status === 401) {
    await refreshAccessToken(account);
    token = decryptToken(account.accessToken);
    res = await fetch(url, {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
  }

  if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);
  return res.json();
}

async function refreshAccessToken(account: SocialAccount) {
  const refresh = decryptToken(account.refreshToken);
  const r = await fetch(account.provider.tokenUrl, {
    method: 'POST',
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refresh,
      client_id: process.env[`${account.provider.name}_CLIENT_ID`]!,
      client_secret: process.env[`${account.provider.name}_CLIENT_SECRET`]!,
    }),
  });
  const data = await r.json();
  account.accessToken = encryptToken(data.access_token);
  if (data.refresh_token) account.refreshToken = encryptToken(data.refresh_token); // rotativo
  account.expiresAt = new Date(Date.now() + data.expires_in * 1000);
  await account.save();
}

💡 Dica prática

Use um lock distribuído (Redis SETNX por account.id) durante o refresh. Sem isso, dois workers que disparam o mesmo job ao mesmo tempo fazem dois refreshes em paralelo — e provedores com refresh rotativo invalidam o token do segundo worker.

Conceitos-chave

Preflight check

Renovar antes da chamada, não depois do erro.

Interceptor

Camada que injeta o token sem cada handler lembrar.

Retry on 401

Tentativa única após refresh — não loop infinito.

Lock

Serializa refreshes concorrentes da mesma conta.

5

📋 App Review (Meta/LinkedIn/TikTok)

Provedores grandes não liberam scopes sensíveis (como "publicar posts") sem que seu app passe por App Review. Em modo dev o app só funciona com contas dos próprios desenvolvedores; para qualquer usuário externo postar, a review é obrigatória.

📊 Prazos típicos (2025-2026)

  • Meta (Facebook/Instagram): 3-15 dias úteis. Pode rejeitar várias vezes pedindo screencast detalhado do uso de cada permissão.
  • LinkedIn: 2-6 semanas. O Marketing Developer Platform tem fila longa e exige caso de uso B2B claro.
  • TikTok: 5-20 dias. Exige domínio verificado, política de privacidade e teste em sandbox.
  • X (Twitter): Acesso "Free" instantâneo (limitado); "Basic" $200/mês; "Pro" $5k/mês.
  • YouTube: Quota inicial baixa, expansão exige formulário detalhado a cada salto.

O que acelera a aprovação

1

Screencast em alta qualidade

Mostre cada permissão sendo usada no produto, sem cortes — fala em inglês claro e legenda quando possível.

2

Domínio próprio com HTTPS

Ngrok ou domínio gratuito é rejeição quase certa. Tenha app.seu-dominio.com com cert válido.

3

Política de privacidade real

Página pública listando dados coletados, finalidade e retenção. Genéricas copiadas são detectadas.

4

Conta de teste pronta

Forneça login + senha de uma conta de teste no app pronta para o revisor entrar e validar.

Conceitos-chave

Dev Mode

App só funciona para desenvolvedores listados.

Live Mode

Aberto a qualquer usuário — exige review.

Advanced Access

Permissões sensíveis que pedem revisão extra.

Use Case

Justificativa de negócio escrita por permissão.

6

🦋 Bluesky/Mastodon: exceção sem aprovação

Enquanto Meta e LinkedIn te fazem esperar semanas, o fediverso resolve o mesmo problema em minutos. Bluesky (AT Protocol) e Mastodon (ActivityPub) seguem outra filosofia: instâncias abertas, autenticação por App Password ou OAuth dinâmico, sem comitê de revisão.

Para um MVP que precisa rodar essa semana, ou para qualquer self-hosted que não quer depender da boa vontade de empresas privadas, esses dois são o caminho rápido.

# Bluesky — autenticação direta via App Password
# Settings > App Passwords > gera uma string tipo abcd-efgh-ijkl-mnop

curl -X POST https://bsky.social/xrpc/com.atproto.server.createSession \
  -H "Content-Type: application/json" \
  -d '{
    "identifier": "seu-handle.bsky.social",
    "password": "abcd-efgh-ijkl-mnop"
  }'
# Devolve { accessJwt, refreshJwt, did, handle } — pronto para postar.

# Mastodon — registra app via API e autoriza usuário
curl -X POST https://mastodon.social/api/v1/apps \
  -d 'client_name=MeuApp' \
  -d 'redirect_uris=urn:ietf:wg:oauth:2.0:oob' \
  -d 'scopes=write:statuses'

✓ Vantagens

  • Sem App Review, sem fila, sem rejeição arbitrária.
  • Sem custo de tier (X cobra $200+/mês).
  • APIs documentadas e estáveis.
  • Audiência cresce rápido entre devs e early adopters.

⚠️ Limitações

  • Alcance menor que Instagram/LinkedIn.
  • Mastodon exige descobrir a instância do usuário primeiro.
  • App Password do Bluesky dá acesso total — trate como senha.
  • Métricas/analytics ainda limitados versus redes centralizadas.

🚀 Dica prática

Comece o desenvolvimento pelo Bluesky. Você valida toda a arquitetura de tokens, criptografia e refresh em uma tarde, e quando a aprovação do Meta sair semanas depois, é só plugar o adapter — o resto do código já está testado.

Conceitos-chave

App Password

Senha alternativa por app, revogável a qualquer momento.

AT Protocol

Stack aberta sobre a qual o Bluesky roda.

Instância

Servidor Mastodon — usuário escolhe qual usar.

DID

Identificador descentralizado do Bluesky.

🎯 Resumo do Módulo

OAuth 2.0 entendido — authorization code flow, redirect URI fixa, consent screen e state anti-CSRF.
Access vs refresh token — duração, propósito, retomada proativa antes do expires_at.
Criptografia em repouso — AES-256-GCM com IV único, chave em env var, nunca em código.
Refresh automático — interceptor com preflight check, retry on 401 e lock por conta.
App Review na prática — prazos por provedor, screencast, domínio HTTPS, política de privacidade.
Atalho do fediverso — Bluesky/Mastodon liberam dev sem comitê e aceleram o MVP.

Próximo Módulo:

1.4 — Filas, jobs e agendamento de posts