🔑 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
App redireciona usuário
Postiz monta uma URL para o provedor com client_id, redirect_uri, scope e state.
Provedor exibe consent screen
"O app X quer publicar no seu LinkedIn. Permitir?" Usuário aprova ou nega.
Redirect com code
Provedor redireciona para redirect_uri?code=ABC&state=XYZ. O state precisa bater para evitar CSRF.
Backend troca code por token
POST server-to-server com client_secret. Provedor devolve access_token + refresh_token + expires_in.
App usa o access token
Cada request à API do provedor inclui Authorization: Bearer <token>. Sem senha, sem cookie.
Conceitos-chave
Identificador público do app no provedor.
URL fixa registrada — qualquer outra é rejeitada.
Permissões específicas pedidas no consent.
Token anti-CSRF que volta no redirect.
🎫 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
Segundos até o access token virar pó.
Quem segura o token, tem o acesso — nada mais.
Cada refresh emite um novo refresh token.
Endpoint para invalidar tokens vazados.
🔒 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_tokencomoTEXT(nãoVARCHAR(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
Cifra simétrica + autenticação integrada.
Initialization vector — único por registro.
Assinatura que detecta adulteração.
Trocar a chave-mestra periodicamente.
♻️ 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
Renovar antes da chamada, não depois do erro.
Camada que injeta o token sem cada handler lembrar.
Tentativa única após refresh — não loop infinito.
Serializa refreshes concorrentes da mesma conta.
📋 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
Screencast em alta qualidade
Mostre cada permissão sendo usada no produto, sem cortes — fala em inglês claro e legenda quando possível.
Domínio próprio com HTTPS
Ngrok ou domínio gratuito é rejeição quase certa. Tenha app.seu-dominio.com com cert válido.
Política de privacidade real
Página pública listando dados coletados, finalidade e retenção. Genéricas copiadas são detectadas.
Conta de teste pronta
Forneça login + senha de uma conta de teste no app pronta para o revisor entrar e validar.
Conceitos-chave
App só funciona para desenvolvedores listados.
Aberto a qualquer usuário — exige review.
Permissões sensíveis que pedem revisão extra.
Justificativa de negócio escrita por permissão.
🦋 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
Senha alternativa por app, revogável a qualquer momento.
Stack aberta sobre a qual o Bluesky roda.
Servidor Mastodon — usuário escolhe qual usar.
Identificador descentralizado do Bluesky.
🎯 Resumo do Módulo
Próximo Módulo:
1.4 — Filas, jobs e agendamento de posts