{"id":124,"date":"2026-06-17T17:02:25","date_gmt":"2026-06-17T17:02:25","guid":{"rendered":"https:\/\/shattered.io\/pt\/2026\/06\/17\/oauth2-nodejs-pkce\/"},"modified":"2026-06-17T17:03:45","modified_gmt":"2026-06-17T17:03:45","slug":"oauth2-nodejs-pkce","status":"publish","type":"post","link":"https:\/\/shattered.io\/pt\/2026\/06\/17\/oauth2-nodejs-pkce\/","title":{"rendered":"OAuth 2.0 em Node.js com PKCE: 12 Passos [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">OAuth 2.0 \u00e9 o protocolo de autoriza\u00e7\u00e3o que est\u00e1 por tr\u00e1s de praticamente todos os bot\u00f5es &#8220;Iniciar sess\u00e3o com Google&#8221; ou &#8220;Entrar com GitHub&#8221; que utiliza todos os dias. Em 2026, a sua implementa\u00e7\u00e3o segura, com PKCE (Proof Key for Code Exchange), deixou de ser opcional: o <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc9126\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9126 da IETF<\/a> recomenda PKCE para todos os clientes OAuth, incluindo aplica\u00e7\u00f5es confidenciais no servidor. O rascunho OAuth 2.1 elimina por completo o fluxo impl\u00edcito e torna o PKCE obrigat\u00f3rio no fluxo de c\u00f3digo de autoriza\u00e7\u00e3o.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Neste guia passo a passo, vai construir um servidor Express 5 completo em Node.js 22 LTS que implementa o fluxo Authorization Code com PKCE usando o GitHub como fornecedor. O projeto final inclui gera\u00e7\u00e3o de PKCE nativa (sem bibliotecas externas para a criptografia), gest\u00e3o de sess\u00f5es, refresh autom\u00e1tico de tokens, middleware de prote\u00e7\u00e3o de rotas e logout seguro. S\u00e3o 12 passos, cerca de 45 minutos de trabalho.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"o-que-e-oauth-2-0-e-porque-substitui-passwords\">O que \u00e9 OAuth 2.0 e Porque Substitui Passwords<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0, definido no RFC 6749, \u00e9 um protocolo de autoriza\u00e7\u00e3o delegada. Em vez de partilhar a password com uma aplica\u00e7\u00e3o terceira, o utilizador concede \u00e0 aplica\u00e7\u00e3o um token de acesso com escopo limitado e prazo de validade. A aplica\u00e7\u00e3o nunca v\u00ea as credenciais do utilizador.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">O protocolo define quatro entidades principais:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Resource Owner<\/strong>: o utilizador que det\u00e9m os dados.<\/li>\n<li><strong>Client<\/strong>: a aplica\u00e7\u00e3o que quer aceder aos dados (o seu servidor Node.js).<\/li>\n<li><strong>Authorization Server<\/strong>: o servidor que autentica o utilizador e emite tokens (GitHub, Google, Keycloak).<\/li>\n<li><strong>Resource Server<\/strong>: a API que protege os dados (por exemplo, a API do GitHub).<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">O fluxo mais seguro para aplica\u00e7\u00f5es web com servidor \u00e9 o <strong>Authorization Code Flow com PKCE<\/strong>. O servidor redireciona o utilizador para o Authorization Server, que devolve um c\u00f3digo de autoriza\u00e7\u00e3o de curta dura\u00e7\u00e3o. O servidor troca esse c\u00f3digo por tokens de acesso e refresh, sem que o browser veja os tokens finais. Este isolamento \u00e9 a principal vantagem sobre o fluxo impl\u00edcito (agora removido do OAuth 2.1), onde os tokens apareciam diretamente no URL do browser.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 resolve um problema real: sem ele, uma aplica\u00e7\u00e3o terceira precisaria da password do utilizador para aceder, por exemplo, aos seus contactos do Google. Com OAuth, o utilizador autoriza um acesso limitado (apenas leitura de contactos, por 30 dias) sem nunca partilhar a password. Se a aplica\u00e7\u00e3o terceira for comprometida, o atacante obt\u00e9m apenas um token revog\u00e1vel, n\u00e3o a password.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"pkce-porque-e-obrigatorio-em-2026\">PKCE: Porque \u00e9 Obrigat\u00f3rio em 2026<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">PKCE (Proof Key for Code Exchange), definido no <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7636\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 7636<\/a>, resolve o ataque de interce\u00e7\u00e3o de c\u00f3digo de autoriza\u00e7\u00e3o. Sem PKCE, um processo malicioso na mesma m\u00e1quina que capte o redirect URI consegue trocar o c\u00f3digo por tokens. Com PKCE, isso \u00e9 imposs\u00edvel porque o Authorization Server exige prova criptogr\u00e1fica de que quem pede o token \u00e9 o mesmo que iniciou o fluxo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">O mecanismo funciona assim:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>O cliente gera um <strong>code_verifier<\/strong>: uma string aleat\u00f3ria de alta entropia (43 a 128 caracteres, base64url).<\/li>\n<li>O cliente calcula o <strong>code_challenge<\/strong>: <code>BASE64URL(SHA256(code_verifier))<\/code>.<\/li>\n<li>O cliente envia o <code>code_challenge<\/code> e <code>code_challenge_method=S256<\/code> no pedido de autoriza\u00e7\u00e3o.<\/li>\n<li>O Authorization Server guarda o <code>code_challenge<\/code> associado ao c\u00f3digo emitido.<\/li>\n<li>Na troca do c\u00f3digo, o cliente envia o <code>code_verifier<\/code> original. O servidor verifica se <code>SHA256(code_verifier) == code_challenge<\/code>.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Um atacante que intercete o c\u00f3digo de autoriza\u00e7\u00e3o n\u00e3o tem o <code>code_verifier<\/code>, por isso n\u00e3o consegue obter tokens. O RFC 9126 recomenda SHA-256 (S256) como \u00fanico m\u00e9todo aceite; o m\u00e9todo &#8220;plain&#8221; deve ser rejeitado pelos servidores modernos por oferecer prote\u00e7\u00e3o nula contra interce\u00e7\u00e3o. O OAuth 2.1, em fase de rascunho avan\u00e7ado, incorpora PKCE como requisito base para todos os clientes que usem o fluxo de c\u00f3digo de autoriza\u00e7\u00e3o, incluindo clientes confidenciais com servidor.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"comparacao-de-fluxos-oauth-2-0\">Compara\u00e7\u00e3o de Fluxos OAuth 2.0<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Fluxo<\/th><th>Caso de Uso<\/th><th>PKCE<\/th><th>Estado em OAuth 2.1<\/th><\/tr><\/thead><tbody>\n<tr><td>Authorization Code + PKCE<\/td><td>Aplica\u00e7\u00f5es web com servidor, SPAs, apps m\u00f3veis<\/td><td>Obrigat\u00f3rio<\/td><td>Padr\u00e3o recomendado<\/td><\/tr>\n<tr><td>Client Credentials<\/td><td>Machine-to-machine sem utilizador<\/td><td>N\/A<\/td><td>Mantido<\/td><\/tr>\n<tr><td>Authorization Code (sem PKCE)<\/td><td>Aplica\u00e7\u00f5es web legadas<\/td><td>Recomendado pelo RFC 9126<\/td><td>Descontinuado sem PKCE<\/td><\/tr>\n<tr><td>Implicit Flow<\/td><td>SPAs antigas com tokens no URL<\/td><td>N\/A<\/td><td><strong>Removido<\/strong><\/td><\/tr>\n<tr><td>Resource Owner Password<\/td><td>Aplica\u00e7\u00f5es de confian\u00e7a total<\/td><td>N\/A<\/td><td><strong>Removido<\/strong><\/td><\/tr>\n<tr><td>Device Authorization<\/td><td>Dispositivos sem browser (TV, CLI)<\/td><td>N\/A<\/td><td>Mantido<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"pre-requisitos\">Pr\u00e9-requisitos<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Antes de come\u00e7ar, confirme que tem instalado e configurado:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Node.js 22 LTS<\/strong> ou superior (execute <code>node --version<\/code> para confirmar)<\/li>\n<li><strong>npm 10<\/strong> ou superior (<code>npm --version<\/code>)<\/li>\n<li>Uma conta no <strong>GitHub<\/strong> gratuita, para criar a aplica\u00e7\u00e3o OAuth de teste<\/li>\n<li>Conhecimento b\u00e1sico de <strong>Express.js<\/strong> e middleware HTTP<\/li>\n<li>Familiaridade com <strong>sess\u00f5es HTTP<\/strong> e cookies<\/li>\n<li>Terminal com acesso \u00e0 internet<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Pacotes npm utilizados neste tutorial:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Pacote<\/th><th>Vers\u00e3o<\/th><th>Fun\u00e7\u00e3o<\/th><\/tr><\/thead><tbody>\n<tr><td>express<\/td><td>5.2.1<\/td><td>Framework web<\/td><\/tr>\n<tr><td>express-session<\/td><td>1.19.0<\/td><td>Gest\u00e3o de sess\u00f5es no servidor<\/td><\/tr>\n<tr><td>axios<\/td><td>1.18.0<\/td><td>Pedidos HTTP ao Authorization Server<\/td><\/tr>\n<tr><td>dotenv<\/td><td>17.4.2<\/td><td>Vari\u00e1veis de ambiente<\/td><\/tr>\n<tr><td>crypto (built-in)<\/td><td>Node.js 22<\/td><td>Gera\u00e7\u00e3o de PKCE (randomBytes, SHA-256)<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-1-criar-a-aplicacao-oauth-no-github\">Passo 1: Criar a Aplica\u00e7\u00e3o OAuth no GitHub<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">O GitHub serve como Authorization Server neste tutorial. As instru\u00e7\u00f5es aplicam-se a qualquer fornecedor OAuth 2.0 compat\u00edvel (Google, Microsoft Entra ID, Keycloak), com ajuste dos endpoints e par\u00e2metros espec\u00edficos de cada fornecedor.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Siga estes passos no GitHub:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Aceda ao <strong>GitHub.com<\/strong>, clique na sua foto de perfil e selecione <strong>Settings<\/strong>.<\/li>\n<li>No menu lateral, clique em <strong>Developer settings<\/strong> (em baixo).<\/li>\n<li>Selecione <strong>OAuth Apps<\/strong> e clique em <strong>New OAuth App<\/strong>.<\/li>\n<li>Preencha o formul\u00e1rio com estes valores exatos:<\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Application name<\/strong>: <code>oauth2-pkce-tutorial<\/code><\/li>\n<li><strong>Homepage URL<\/strong>: <code>http:\/\/localhost:3000<\/code><\/li>\n<li><strong>Authorization callback URL<\/strong>: <code>http:\/\/localhost:3000\/auth\/callback<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Ap\u00f3s criar a aplica\u00e7\u00e3o, o GitHub mostra o <strong>Client ID<\/strong> imediatamente. Para o Client Secret, clique em <strong>Generate a new client secret<\/strong> e copie o valor mostrado, pois n\u00e3o ser\u00e1 apresentado novamente. Guarde ambos em local seguro: o <code>GITHUB_CLIENT_ID<\/code> e o <code>GITHUB_CLIENT_SECRET<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Os endpoints do GitHub para OAuth 2.0 s\u00e3o:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Autoriza\u00e7\u00e3o<\/strong>: <code>https:\/\/github.com\/login\/oauth\/authorize<\/code><\/li>\n<li><strong>Token<\/strong>: <code>https:\/\/github.com\/login\/oauth\/access_token<\/code><\/li>\n<li><strong>API do utilizador<\/strong>: <code>https:\/\/api.github.com\/user<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Consulte a <a href=\"https:\/\/docs.github.com\/en\/apps\/oauth-apps\/building-oauth-apps\/authorizing-oauth-apps\" target=\"_blank\" rel=\"noopener noreferrer\">documenta\u00e7\u00e3o oficial do GitHub OAuth Apps<\/a> para a lista completa de escopos dispon\u00edveis e limites de taxa da API.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-2-inicializar-o-projeto-node-js\">Passo 2: Inicializar o Projeto Node.js<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Crie a estrutura do projeto e instale as depend\u00eancias com os comandos seguintes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Criar e entrar na pasta do projeto\nmkdir oauth2-pkce-nodejs && cd oauth2-pkce-nodejs\n\n# Inicializar package.json com valores padr\u00e3o\nnpm init -y\n\n# Instalar depend\u00eancias de produ\u00e7\u00e3o com vers\u00f5es exatas\nnpm install express@5.2.1 express-session@1.19.0 axios@1.18.0 dotenv@17.4.2\n\n# Criar estrutura de ficheiros do projeto\nmkdir -p src\ntouch src\/index.js src\/pkce.js src\/auth.js src\/middleware.js .env .gitignore<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Atualize o <code>package.json<\/code> para adicionar o script de desenvolvimento com hot-reload nativo do Node.js 22:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"name\": \"oauth2-pkce-nodejs\",\n  \"version\": \"1.0.0\",\n  \"description\": \"OAuth 2.0 Authorization Code Flow com PKCE em Node.js\/Express\",\n  \"main\": \"src\/index.js\",\n  \"scripts\": {\n    \"start\": \"node src\/index.js\",\n    \"dev\": \"node --watch src\/index.js\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^1.18.0\",\n    \"dotenv\": \"^17.4.2\",\n    \"express\": \"^5.2.1\",\n    \"express-session\": \"^1.19.0\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A estrutura final do projeto ap\u00f3s todos os passos:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>oauth2-pkce-nodejs\/\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 index.js      # Servidor Express principal e defini\u00e7\u00e3o de rotas\n\u2502   \u251c\u2500\u2500 pkce.js       # Gera\u00e7\u00e3o de code_verifier e code_challenge (PKCE)\n\u2502   \u251c\u2500\u2500 auth.js       # L\u00f3gica do fluxo OAuth (autoriza\u00e7\u00e3o e troca de c\u00f3digo)\n\u2502   \u2514\u2500\u2500 middleware.js # Middleware de prote\u00e7\u00e3o de rotas autenticadas\n\u251c\u2500\u2500 .env              # Vari\u00e1veis de ambiente (nunca incluir no controlo de vers\u00f5es)\n\u251c\u2500\u2500 .gitignore        # Excluir .env e node_modules\n\u2514\u2500\u2500 package.json<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-3-configurar-variaveis-de-ambiente\">Passo 3: Configurar Vari\u00e1veis de Ambiente<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nunca coloque credenciais OAuth diretamente no c\u00f3digo-fonte. Preencha o ficheiro <code>.env<\/code> com os valores obtidos no GitHub no Passo 1:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .env\n# Credenciais da aplica\u00e7\u00e3o OAuth no GitHub\nGITHUB_CLIENT_ID=Ov23liXXXXXXXXXXXXXX\nGITHUB_CLIENT_SECRET=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0\n\n# URL base da aplica\u00e7\u00e3o (sem barra final)\nAPP_URL=http:\/\/localhost:3000\n\n# URL de callback OAuth (tem de coincidir EXATAMENTE com o registo no GitHub)\nREDIRECT_URI=http:\/\/localhost:3000\/auth\/callback\n\n# Porta do servidor\nPORT=3000\n\n# Segredo da sess\u00e3o (use 64+ bytes aleat\u00f3rios em produ\u00e7\u00e3o)\nSESSION_SECRET=substitua_por_valor_aleatorio_seguro_em_producao<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Adicione <code>.env<\/code> ao <code>.gitignore<\/code> imediatamente para evitar expor as credenciais num commit acidental:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .gitignore\n.env\nnode_modules\/\n*.log\n.DS_Store<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Gere um segredo de sess\u00e3o seguro com o Node.js diretamente no terminal:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node -e \"console.log(require('crypto').randomBytes(64).toString('hex'))\"\n# Exemplo de sa\u00edda:\n# a3f8e2b1c9d4...128 caracteres hexadecimais<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-4-implementar-a-geracao-de-pkce\">Passo 4: Implementar a Gera\u00e7\u00e3o de PKCE<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">O m\u00f3dulo <code>crypto<\/code> nativo do Node.js 22 tem tudo o que precisa para PKCE: <code>randomBytes<\/code> para entropia criptograficamente segura e <code>createHash<\/code> para SHA-256. Nenhuma depend\u00eancia externa \u00e9 necess\u00e1ria para esta parte, o que reduz a superf\u00edcie de ataque da cadeia de fornecimento.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Crie o ficheiro <code>src\/pkce.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/pkce.js\nconst crypto = require('crypto');\n\n\/**\n * Gera um code_verifier de alta entropia.\n * RFC 7636 exige 43-128 caracteres do alphabet [A-Z, a-z, 0-9, -, ., _, ~]\n * base64url cobre este alphabet sem padding.\n *\/\nfunction generateCodeVerifier() {\n  \/\/ 32 bytes = 256 bits de entropia, codificados em base64url = 43 caracteres\n  return crypto.randomBytes(32).toString('base64url');\n}\n\n\/**\n * Calcula o code_challenge a partir do code_verifier.\n * M\u00e9todo S256 (obrigat\u00f3rio): BASE64URL(SHA256(ASCII(code_verifier)))\n *\/\nfunction generateCodeChallenge(codeVerifier) {\n  return crypto\n    .createHash('sha256')\n    .update(codeVerifier, 'ascii')\n    .digest('base64url');\n}\n\n\/**\n * Gera um valor state aleat\u00f3rio para prote\u00e7\u00e3o CSRF.\n * O state vincula o pedido de autoriza\u00e7\u00e3o \u00e0 sess\u00e3o do browser.\n *\/\nfunction generateState() {\n  return crypto.randomBytes(16).toString('hex');\n}\n\nmodule.exports = { generateCodeVerifier, generateCodeChallenge, generateState };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A codifica\u00e7\u00e3o <code>base64url<\/code> (suportada nativamente desde Node.js 14.18.0) difere do base64 padr\u00e3o: substitui <code>+<\/code> por <code>-<\/code>, <code>\/<\/code> por <code>_<\/code> e remove o padding <code>=<\/code>. Estes caracteres s\u00e3o seguros em URLs sem necessidade de percent-encoding adicional, requisito obrigat\u00f3rio do RFC 7636 para os valores PKCE.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Para verificar que a gera\u00e7\u00e3o funciona corretamente, teste no terminal:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node -e \"\nconst { generateCodeVerifier, generateCodeChallenge } = require('.\/src\/pkce');\nconst verifier = generateCodeVerifier();\nconst challenge = generateCodeChallenge(verifier);\nconsole.log('code_verifier:', verifier);\nconsole.log('code_verifier length:', verifier.length);\nconsole.log('code_challenge:', challenge);\n\"\n\n# Sa\u00edda esperada (valores variam a cada execu\u00e7\u00e3o):\n# code_verifier: xK9mP2nQrT5uVwYz3aB6cD8eF1gH4iJ7\n# code_verifier length: 43\n# code_challenge: Xr4Kp9mN2qT5uVwYz3aB6cD8eF1gH4iJ<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-5-construir-o-url-de-autorizacao-e-iniciar-o-fluxo\">Passo 5: Construir o URL de Autoriza\u00e7\u00e3o e Iniciar o Fluxo<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">O URL de autoriza\u00e7\u00e3o \u00e9 o ponto de entrada do fluxo OAuth. O servidor redireciona o utilizador para este URL, que inclui os par\u00e2metros PKCE e o state anti-CSRF. Crie o ficheiro <code>src\/auth.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/auth.js\nconst axios = require('axios');\nconst { generateCodeVerifier, generateCodeChallenge, generateState } = require('.\/pkce');\n\nconst GITHUB_AUTHORIZE_URL = 'https:\/\/github.com\/login\/oauth\/authorize';\nconst GITHUB_TOKEN_URL = 'https:\/\/github.com\/login\/oauth\/access_token';\nconst GITHUB_API_URL = 'https:\/\/api.github.com';\n\n\/**\n * Inicia o fluxo OAuth: gera PKCE, guarda na sess\u00e3o e redireciona.\n *\/\nfunction startAuthFlow(req, res) {\n  const codeVerifier = generateCodeVerifier();\n  const codeChallenge = generateCodeChallenge(codeVerifier);\n  const state = generateState();\n\n  \/\/ Guardar na sess\u00e3o do servidor (nunca expor ao browser)\n  req.session.codeVerifier = codeVerifier;\n  req.session.oauthState = state;\n\n  const params = new URLSearchParams({\n    client_id: process.env.GITHUB_CLIENT_ID,\n    redirect_uri: process.env.REDIRECT_URI,\n    scope: 'read:user user:email',\n    response_type: 'code',\n    state: state,\n    \/\/ PKCE: enviar code_challenge para fornecedores que suportam RFC 7636\n    \/\/ (Google, Microsoft Entra ID, Keycloak suportam; GitHub suporte parcial)\n    code_challenge: codeChallenge,\n    code_challenge_method: 'S256',\n  });\n\n  res.redirect(`${GITHUB_AUTHORIZE_URL}?${params.toString()}`);\n}\n\n\/**\n * Trata o callback OAuth: valida state, troca c\u00f3digo por tokens.\n *\/\nasync function handleCallback(req, res) {\n  const { code, state } = req.query;\n\n  \/\/ Valida\u00e7\u00e3o 1: verificar par\u00e2metros obrigat\u00f3rios\n  if (!code || !state) {\n    return res.status(400).send('Par\u00e2metros OAuth em falta na resposta do callback.');\n  }\n\n  \/\/ Valida\u00e7\u00e3o 2: verificar state anti-CSRF (CR\u00cdTICO)\n  if (state !== req.session.oauthState) {\n    return res.status(403).send('State inv\u00e1lido. Poss\u00edvel ataque CSRF detetado.');\n  }\n\n  \/\/ Valida\u00e7\u00e3o 3: verificar que a sess\u00e3o tem o code_verifier\n  const codeVerifier = req.session.codeVerifier;\n  if (!codeVerifier) {\n    return res.status(400).send('Sess\u00e3o expirada. Inicie o login novamente.');\n  }\n\n  try {\n    \/\/ Trocar o c\u00f3digo de autoriza\u00e7\u00e3o por tokens de acesso\n    const tokenResponse = await axios.post(\n      GITHUB_TOKEN_URL,\n      {\n        client_id: process.env.GITHUB_CLIENT_ID,\n        client_secret: process.env.GITHUB_CLIENT_SECRET,\n        code: code,\n        redirect_uri: process.env.REDIRECT_URI,\n        \/\/ Para fornecedores com suporte PKCE completo (Google, Keycloak):\n        \/\/ code_verifier: codeVerifier,\n      },\n      {\n        headers: { Accept: 'application\/json' }, \/\/ Fundamental para GitHub\n      }\n    );\n\n    const { access_token, token_type, scope } = tokenResponse.data;\n\n    if (!access_token) {\n      throw new Error('Resposta sem access_token: ' + JSON.stringify(tokenResponse.data));\n    }\n\n    \/\/ Guardar token na sess\u00e3o (server-side, n\u00e3o acess\u00edvel ao JavaScript do browser)\n    req.session.accessToken = access_token;\n    req.session.tokenType = token_type;\n    req.session.tokenScope = scope;\n\n    \/\/ Limpar dados PKCE e state da sess\u00e3o (j\u00e1 n\u00e3o s\u00e3o necess\u00e1rios)\n    delete req.session.codeVerifier;\n    delete req.session.oauthState;\n\n    \/\/ Obter dados do utilizador autenticado\n    const userResponse = await axios.get(`${GITHUB_API_URL}\/user`, {\n      headers: { Authorization: `Bearer ${access_token}` },\n    });\n\n    req.session.user = {\n      id: userResponse.data.id,\n      login: userResponse.data.login,\n      name: userResponse.data.name,\n      email: userResponse.data.email,\n      avatar: userResponse.data.avatar_url,\n    };\n\n    \/\/ For\u00e7ar persist\u00eancia da sess\u00e3o antes do redirect\n    req.session.save((err) => {\n      if (err) return res.status(500).send('Erro ao guardar sess\u00e3o.');\n      res.redirect('\/dashboard');\n    });\n  } catch (error) {\n    console.error('Erro no callback OAuth:', error.response?.data || error.message);\n    res.status(500).send('Falha na autentica\u00e7\u00e3o OAuth. Tente novamente.');\n  }\n}\n\nmodule.exports = { startAuthFlow, handleCallback };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-6-criar-o-middleware-de-autenticacao\">Passo 6: Criar o Middleware de Autentica\u00e7\u00e3o<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">O middleware verifica se o utilizador tem uma sess\u00e3o v\u00e1lida antes de permitir acesso a rotas protegidas. Crie o ficheiro <code>src\/middleware.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/middleware.js\n\n\/**\n * requireAuth: bloqueia rotas para utilizadores n\u00e3o autenticados.\n * Redireciona para \/auth\/login em vez de devolver 401 (melhor UX para rotas web).\n *\/\nfunction requireAuth(req, res, next) {\n  if (req.session && req.session.user && req.session.accessToken) {\n    return next();\n  }\n  \/\/ Guardar URL pretendida para redirecionar ap\u00f3s login bem-sucedido\n  req.session.returnTo = req.originalUrl;\n  res.redirect('\/auth\/login');\n}\n\n\/**\n * requireNoAuth: impede utilizadores j\u00e1 autenticados de aceder ao login.\n * Evita loops de redirect e states duplicados.\n *\/\nfunction requireNoAuth(req, res, next) {\n  if (req.session && req.session.user) {\n    return res.redirect('\/dashboard');\n  }\n  next();\n}\n\n\/**\n * attachUser: torna o utilizador dispon\u00edvel em res.locals para vistas.\n * Usar como middleware global em todas as rotas.\n *\/\nfunction attachUser(req, res, next) {\n  res.locals.user = req.session.user || null;\n  next();\n}\n\nmodule.exports = { requireAuth, requireNoAuth, attachUser };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-7-montar-o-servidor-express-principal\">Passo 7: Montar o Servidor Express Principal<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">O ficheiro <code>src\/index.js<\/code> une todos os m\u00f3dulos e define as rotas da aplica\u00e7\u00e3o:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/index.js\nrequire('dotenv').config(); \/\/ PRIMEIRO: carregar vari\u00e1veis de ambiente\n\nconst express = require('express');\nconst session = require('express-session');\nconst { startAuthFlow, handleCallback } = require('.\/auth');\nconst { requireAuth, requireNoAuth, attachUser } = require('.\/middleware');\n\nconst app = express();\nconst PORT = process.env.PORT || 3000;\n\n\/\/ Necess\u00e1rio se a app estiver atr\u00e1s de proxy (Nginx, Cloudflare, etc.)\n\/\/ app.set('trust proxy', 1);\n\n\/\/ Configura\u00e7\u00e3o da sess\u00e3o com op\u00e7\u00f5es de seguran\u00e7a\napp.use(session({\n  secret: process.env.SESSION_SECRET,\n  resave: false,\n  saveUninitialized: false,\n  cookie: {\n    httpOnly: true,           \/\/ Impede acesso ao cookie via JavaScript (prote\u00e7\u00e3o XSS)\n    secure: process.env.NODE_ENV === 'production', \/\/ HTTPS obrigat\u00f3rio em produ\u00e7\u00e3o\n    sameSite: 'lax',          \/\/ Prote\u00e7\u00e3o CSRF adicional (lax permite redirect OAuth)\n    maxAge: 24 * 60 * 60 * 1000, \/\/ 24 horas em milissegundos\n  },\n  name: 'oauth_sid',          \/\/ Nome personalizado (n\u00e3o revela que usa express-session)\n}));\n\n\/\/ Middleware global\napp.use(express.json());\napp.use(express.urlencoded({ extended: false }));\napp.use(attachUser);\n\n\/\/ Rota principal\napp.get('\/', (req, res) => {\n  if (req.session.user) return res.redirect('\/dashboard');\n  res.send(`<!DOCTYPE html>\n<html lang=\"pt\">\n<head><meta charset=\"UTF-8\"><title>OAuth 2.0 PKCE Demo<\/title><\/head>\n<body>\n  <h1>OAuth 2.0 com PKCE em Node.js<\/h1>\n  <p>Demonstra\u00e7\u00e3o do fluxo Authorization Code com PKCE usando GitHub.<\/p>\n  <a href=\"\/auth\/login\">Iniciar sess\u00e3o com GitHub<\/a>\n<\/body>\n<\/html>`);\n});\n\n\/\/ Rotas OAuth\napp.get('\/auth\/login', requireNoAuth, startAuthFlow);\napp.get('\/auth\/callback', handleCallback);\n\n\/\/ Rota protegida: dashboard\napp.get('\/dashboard', requireAuth, (req, res) => {\n  const { user } = req.session;\n  res.send(`<!DOCTYPE html>\n<html lang=\"pt\">\n<head><meta charset=\"UTF-8\"><title>Dashboard<\/title><\/head>\n<body>\n  <h1>Bem-vindo, ${user.name || user.login}!<\/h1>\n  <p><strong>Username GitHub:<\/strong> @${user.login}<\/p>\n  <p><strong>Email:<\/strong> ${user.email || 'n\u00e3o p\u00fablico'}<\/p>\n  <p><strong>Escopos:<\/strong> ${req.session.tokenScope}<\/p>\n  <p><a href=\"\/api\/profile\">Ver dados da sess\u00e3o (JSON)<\/a><\/p>\n  <form action=\"\/auth\/logout\" method=\"POST\">\n    <button type=\"submit\">Terminar Sess\u00e3o<\/button>\n  <\/form>\n<\/body>\n<\/html>`);\n});\n\n\/\/ Rota protegida: dados JSON da sess\u00e3o\napp.get('\/api\/profile', requireAuth, (req, res) => {\n  res.json({\n    user: req.session.user,\n    scopes: req.session.tokenScope,\n    authenticated: true,\n    sessionId: req.sessionID,\n  });\n});\n\n\/\/ Logout via POST (evita logout CSRF com links ou imagens)\napp.post('\/auth\/logout', (req, res) => {\n  req.session.destroy((err) => {\n    if (err) {\n      console.error('Erro ao destruir sess\u00e3o:', err);\n      return res.status(500).send('Erro ao terminar sess\u00e3o.');\n    }\n    res.clearCookie('oauth_sid');\n    res.redirect('\/');\n  });\n});\n\n\/\/ Tratamento global de erros (Express 5 propaga erros async automaticamente)\napp.use((err, req, res, next) => {\n  console.error('Erro n\u00e3o tratado:', err.stack);\n  res.status(500).send('Erro interno do servidor.');\n});\n\napp.listen(PORT, () => {\n  console.log(`Servidor OAuth 2.0 PKCE a correr em http:\/\/localhost:${PORT}`);\n  console.log(`Callback configurado: ${process.env.REDIRECT_URI}`);\n});\n\nmodule.exports = app;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-8-testar-o-fluxo-oauth-completo\">Passo 8: Testar o Fluxo OAuth Completo<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Inicie o servidor e teste o fluxo OAuth completo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Iniciar o servidor em modo de desenvolvimento\nnpm run dev\n\n# Sa\u00edda esperada:\n# Servidor OAuth 2.0 PKCE a correr em http:\/\/localhost:3000\n# Callback configurado: http:\/\/localhost:3000\/auth\/callback<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Abra o browser em <code>http:\/\/localhost:3000<\/code> e clique em &#8220;Iniciar sess\u00e3o com GitHub&#8221;. A sequ\u00eancia correta de eventos \u00e9:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>O browser acede a <code>\/auth\/login<\/code>: o servidor gera code_verifier, code_challenge e state, guarda na sess\u00e3o, e redireciona.<\/li>\n<li>O browser chega a <code>https:\/\/github.com\/login\/oauth\/authorize?client_id=...&state=abc123&code_challenge=xyz...<\/code><\/li>\n<li>O utilizador clica &#8220;Authorize oauth2-pkce-tutorial&#8221; na p\u00e1gina do GitHub.<\/li>\n<li>O GitHub redireciona para <code>http:\/\/localhost:3000\/auth\/callback?code=XXXX&state=abc123<\/code><\/li>\n<li>O servidor valida o state, troca o c\u00f3digo por token, obt\u00e9m perfil do GitHub.<\/li>\n<li>Redirect final para <code>http:\/\/localhost:3000\/dashboard<\/code> com o utilizador autenticado.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Se tudo correr bem, o dashboard mostra o nome e username do GitHub. Para inspecionar os dados da sess\u00e3o em JSON, aceda a <code>http:\/\/localhost:3000\/api\/profile<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-9-pkce-completo-com-fornecedores-compativeis-google-keycloak\">Passo 9: PKCE Completo com Fornecedores Compat\u00edveis (Google, Keycloak)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ao contr\u00e1rio do GitHub (que n\u00e3o implementa PKCE no fluxo web padr\u00e3o), o Google Identity Platform, Microsoft Entra ID e Keycloak suportam PKCE completo. Para estes fornecedores, o <code>code_verifier<\/code> \u00e9 enviado na troca do c\u00f3digo, tornando o fluxo completamente resistente a interce\u00e7\u00e3o.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A altera\u00e7\u00e3o \u00e9 m\u00ednima no ficheiro <code>src\/auth.js<\/code>: descomente a linha <code>code_verifier: codeVerifier<\/code> no pedido de troca de token e configure o endpoint do fornecedor. Para o Google como exemplo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/auth.js - vers\u00e3o com PKCE completo para Google OAuth 2.0\n\nconst GOOGLE_AUTHORIZE_URL = 'https:\/\/accounts.google.com\/o\/oauth2\/v2\/auth';\nconst GOOGLE_TOKEN_URL = 'https:\/\/oauth2.googleapis.com\/token';\n\nfunction startGoogleAuthFlow(req, res) {\n  const codeVerifier = generateCodeVerifier();\n  const codeChallenge = generateCodeChallenge(codeVerifier);\n  const state = generateState();\n\n  req.session.codeVerifier = codeVerifier;\n  req.session.oauthState = state;\n\n  const params = new URLSearchParams({\n    client_id: process.env.GOOGLE_CLIENT_ID,\n    redirect_uri: process.env.REDIRECT_URI,\n    response_type: 'code',\n    scope: 'openid email profile',\n    state: state,\n    code_challenge: codeChallenge,      \/\/ PKCE: challenge vai no pedido de autoriza\u00e7\u00e3o\n    code_challenge_method: 'S256',\n    access_type: 'offline',             \/\/ Receber refresh_token do Google\n    prompt: 'select_account',\n  });\n\n  res.redirect(`${GOOGLE_AUTHORIZE_URL}?${params.toString()}`);\n}\n\nasync function handleGoogleCallback(req, res) {\n  const { code, state } = req.query;\n\n  if (!code || !state || state !== req.session.oauthState) {\n    return res.status(403).send('State inv\u00e1lido ou par\u00e2metros em falta.');\n  }\n\n  const codeVerifier = req.session.codeVerifier;\n\n  const tokenResponse = await axios.post(GOOGLE_TOKEN_URL, {\n    client_id: process.env.GOOGLE_CLIENT_ID,\n    client_secret: process.env.GOOGLE_CLIENT_SECRET,\n    code: code,\n    redirect_uri: process.env.REDIRECT_URI,\n    grant_type: 'authorization_code',\n    code_verifier: codeVerifier,         \/\/ PKCE: verifier vai na troca do c\u00f3digo\n  });\n\n  const { access_token, refresh_token, expires_in } = tokenResponse.data;\n  req.session.accessToken = access_token;\n  req.session.refreshToken = refresh_token;\n  req.session.tokenExpiresAt = Date.now() + (expires_in * 1000);\n\n  delete req.session.codeVerifier;\n  delete req.session.oauthState;\n\n  req.session.save(() => res.redirect('\/dashboard'));\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-10-refresh-automatico-de-token\">Passo 10: Refresh Autom\u00e1tico de Token<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Os tokens de acesso do Google expiram ao fim de 3600 segundos (1 hora). Um middleware de refresh autom\u00e1tico verifica a validade antes de cada chamada a APIs protegidas e renova o token de forma transparente:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/middleware.js - adicionar ao ficheiro existente\n\n\/**\n * refreshTokenIfNeeded: renova o access_token se estiver dentro de 5 minutos da expira\u00e7\u00e3o.\n * Apenas para fornecedores que emitem refresh_token (Google, Keycloak, etc.)\n *\/\nasync function refreshTokenIfNeeded(req, res, next) {\n  \/\/ Sem refresh_token dispon\u00edvel, continuar normalmente\n  if (!req.session.refreshToken) return next();\n\n  const expiresAt = req.session.tokenExpiresAt || 0;\n  const fiveMinutes = 5 * 60 * 1000;\n  const needsRefresh = Date.now() > (expiresAt - fiveMinutes);\n\n  if (!needsRefresh) return next();\n\n  try {\n    const axios = require('axios');\n    const response = await axios.post(process.env.TOKEN_ENDPOINT, {\n      grant_type: 'refresh_token',\n      refresh_token: req.session.refreshToken,\n      client_id: process.env.OAUTH_CLIENT_ID,\n      client_secret: process.env.OAUTH_CLIENT_SECRET,\n    });\n\n    const { access_token, refresh_token, expires_in } = response.data;\n    req.session.accessToken = access_token;\n\n    \/\/ Alguns servidores rotacionam o refresh_token a cada uso\n    if (refresh_token) req.session.refreshToken = refresh_token;\n    req.session.tokenExpiresAt = Date.now() + (expires_in * 1000);\n\n    req.session.save(() => next());\n  } catch (error) {\n    console.error('Falha ao renovar token, for\u00e7ando novo login:', error.message);\n    req.session.destroy(() => res.redirect('\/auth\/login'));\n  }\n}\n\nmodule.exports = { requireAuth, requireNoAuth, attachUser, refreshTokenIfNeeded };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Aplique o middleware nas rotas que chamam APIs externas:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/index.js - rota com refresh autom\u00e1tico integrado\nconst { requireAuth, requireNoAuth, attachUser, refreshTokenIfNeeded } = require('.\/middleware');\n\napp.get('\/api\/repos', requireAuth, refreshTokenIfNeeded, async (req, res) => {\n  try {\n    const response = await axios.get('https:\/\/api.github.com\/user\/repos?sort=updated&per_page=10', {\n      headers: { Authorization: `Bearer ${req.session.accessToken}` },\n    });\n    res.json(response.data.map(r => ({\n      name: r.full_name,\n      stars: r.stargazers_count,\n      language: r.language,\n    })));\n  } catch (error) {\n    if (error.response?.status === 401) {\n      \/\/ Token revogado pelo utilizador na conta GitHub\n      req.session.destroy(() => res.redirect('\/auth\/login'));\n    } else {\n      res.status(500).json({ error: 'Falha ao obter reposit\u00f3rios.' });\n    }\n  }\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-11-seguranca-das-sessoes-em-producao\">Passo 11: Seguran\u00e7a das Sess\u00f5es em Produ\u00e7\u00e3o<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A sess\u00e3o \u00e9 o elo mais cr\u00edtico desta implementa\u00e7\u00e3o. Verifique cada configura\u00e7\u00e3o antes de lan\u00e7ar em produ\u00e7\u00e3o:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Configura\u00e7\u00e3o<\/th><th>Desenvolvimento<\/th><th>Produ\u00e7\u00e3o<\/th><th>Risco se Incorreto<\/th><\/tr><\/thead><tbody>\n<tr><td><code>cookie.httpOnly<\/code><\/td><td>true<\/td><td>true (obrigat\u00f3rio)<\/td><td>JavaScript rouba o cookie via XSS<\/td><\/tr>\n<tr><td><code>cookie.secure<\/code><\/td><td>false<\/td><td>true (HTTPS)<\/td><td>Cookie transmitido em texto claro por HTTP<\/td><\/tr>\n<tr><td><code>cookie.sameSite<\/code><\/td><td>&#8216;lax&#8217;<\/td><td>&#8216;lax&#8217; ou &#8216;strict&#8217;<\/td><td>CSRF em pedidos cross-site<\/td><\/tr>\n<tr><td><code>secret<\/code><\/td><td>qualquer string<\/td><td>64+ bytes aleat\u00f3rios<\/td><td>Falsifica\u00e7\u00e3o de sess\u00e3o por for\u00e7a bruta<\/td><\/tr>\n<tr><td><code>saveUninitialized<\/code><\/td><td>false<\/td><td>false<\/td><td>Sess\u00f5es vazias consomem mem\u00f3ria desnecessariamente<\/td><\/tr>\n<tr><td>Armazenamento<\/td><td>Em mem\u00f3ria<\/td><td>Redis ou PostgreSQL<\/td><td>Sess\u00f5es perdidas ao reiniciar o servidor<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Para produ\u00e7\u00e3o com Redis, instale o adaptador e configure:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install connect-redis ioredis\n\n\/\/ src\/index.js - armazenamento Redis para produ\u00e7\u00e3o\nconst { RedisStore } = require('connect-redis');\nconst Redis = require('ioredis');\n\nconst redisClient = new Redis({\n  host: process.env.REDIS_HOST || 'localhost',\n  port: process.env.REDIS_PORT || 6379,\n  password: process.env.REDIS_PASSWORD,\n  tls: process.env.NODE_ENV === 'production' ? {} : undefined,\n});\n\napp.use(session({\n  store: new RedisStore({ client: redisClient, prefix: 'oauth:' }),\n  secret: process.env.SESSION_SECRET,\n  resave: false,\n  saveUninitialized: false,\n  cookie: { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 86400000 },\n  name: 'oauth_sid',\n}));<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-12-logout-seguro-e-revogacao-de-token\">Passo 12: Logout Seguro e Revoga\u00e7\u00e3o de Token<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Um logout seguro tem tr\u00eas componentes: destrui\u00e7\u00e3o da sess\u00e3o no servidor, limpeza do cookie no browser e, quando suportado, revoga\u00e7\u00e3o expl\u00edcita do token no Authorization Server. O GitHub suporta revoga\u00e7\u00e3o via API com autentica\u00e7\u00e3o Basic do client_id e client_secret:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/index.js - logout completo com revoga\u00e7\u00e3o de token\n\napp.post('\/auth\/logout', async (req, res) => {\n  const accessToken = req.session.accessToken;\n\n  \/\/ 1. Revogar o token no GitHub (boa pr\u00e1tica, n\u00e3o bloqueia o logout se falhar)\n  if (accessToken) {\n    try {\n      await axios.delete(\n        `https:\/\/api.github.com\/applications\/${process.env.GITHUB_CLIENT_ID}\/token`,\n        {\n          auth: {\n            username: process.env.GITHUB_CLIENT_ID,\n            password: process.env.GITHUB_CLIENT_SECRET,\n          },\n          data: { access_token: accessToken },\n          headers: { Accept: 'application\/vnd.github.v3+json' },\n        }\n      );\n      console.log('Token GitHub revogado com sucesso.');\n    } catch (err) {\n      console.warn('Aviso: falha ao revogar token:', err.message);\n    }\n  }\n\n  \/\/ 2. Destruir sess\u00e3o no servidor (remove todos os dados da sess\u00e3o)\n  req.session.destroy((err) => {\n    if (err) console.error('Erro ao destruir sess\u00e3o:', err);\n    \/\/ 3. Instruir o browser a apagar o cookie\n    res.clearCookie('oauth_sid');\n    res.redirect('\/');\n  });\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">O formul\u00e1rio HTML para o bot\u00e3o de logout usa POST, n\u00e3o GET. Um logout via GET \u00e9 vulner\u00e1vel a ataques de logout CSRF, onde uma imagem ou link malicioso numa p\u00e1gina terceira pode desligar o utilizador silenciosamente:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- Correto: logout via POST --&gt;\n&lt;form action=\"\/auth\/logout\" method=\"POST\"&gt;\n  &lt;button type=\"submit\"&gt;Terminar Sess\u00e3o&lt;\/button&gt;\n&lt;\/form&gt;\n\n&lt;!-- ERRADO: logout via GET (vulner\u00e1vel a logout CSRF) --&gt;\n&lt;!-- &lt;a href=\"\/auth\/logout\"&gt;Sair&lt;\/a&gt; --&gt;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"erros-comuns-e-como-evita-los\">Erros Comuns e Como Evit\u00e1-los<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Estes s\u00e3o os 7 erros mais frequentes em implementa\u00e7\u00f5es OAuth 2.0 com Node.js, documentados pelo <a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/OAuth2_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP OAuth 2.0 Cheat Sheet<\/a>:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-1-nao-validar-o-parametro-state\">Erro 1: N\u00e3o Validar o Par\u00e2metro State<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Omitir a valida\u00e7\u00e3o do <code>state<\/code> no callback exp\u00f5e a aplica\u00e7\u00e3o a ataques de login CSRF: um atacante for\u00e7a o utilizador a autenticar-se com a conta do atacante, permitindo acesso aos dados que o utilizador armazena na aplica\u00e7\u00e3o sem que este perceba. A solu\u00e7\u00e3o \u00e9 sempre verificar <code>state === req.session.oauthState<\/code> e rejeitar com HTTP 403 em caso de discrep\u00e2ncia, como mostrado no Passo 5.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-2-guardar-tokens-em-localstorage\">Erro 2: Guardar Tokens em localStorage<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">O <code>localStorage<\/code> \u00e9 acess\u00edvel a qualquer JavaScript na p\u00e1gina, incluindo scripts injetados por XSS. Um \u00fanico ataque XSS rouba todos os tokens. Os tokens devem existir apenas no servidor, dentro da sess\u00e3o com cookie <code>httpOnly<\/code>. Este tutorial segue este padr\u00e3o: o browser nunca v\u00ea o <code>access_token<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-3-redirect-uri-com-validacao-parcial\">Erro 3: Redirect URI com Valida\u00e7\u00e3o Parcial<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Aceitar redirect URIs com correspond\u00eancia por prefixo (ex: qualquer URL que comece com <code>https:\/\/app.exemplo.com<\/code>) permite a um atacante usar <code>https:\/\/app.exemplo.com.atacante.com<\/code> para redirecionar o c\u00f3digo de autoriza\u00e7\u00e3o para um dom\u00ednio malicioso. Os Authorization Servers devem usar correspond\u00eancia exata de URI. Ao registar a aplica\u00e7\u00e3o, use sempre o URI completo e exato.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-4-usar-o-fluxo-implicito-em-aplicacoes-novas\">Erro 4: Usar o Fluxo Impl\u00edcito em Aplica\u00e7\u00f5es Novas<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">O fluxo impl\u00edcito devolve o <code>access_token<\/code> diretamente no URL do redirect (<code>#access_token=...<\/code>). Este token fica no hist\u00f3rico do browser, em logs do servidor web e pode ser roubado por scripts na p\u00e1gina. O OAuth 2.1 remove este fluxo por completo. N\u00e3o existe nenhum caso de uso leg\u00edtimo para fluxo impl\u00edcito em aplica\u00e7\u00f5es novas.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-5-expor-o-client-secret-em-codigo-frontend\">Erro 5: Expor o Client Secret em C\u00f3digo Frontend<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">O <code>client_secret<\/code> \u00e9 uma credencial do servidor. Inclu\u00ed-lo em c\u00f3digo JavaScript de frontend (React, Angular, Vue) ou em aplica\u00e7\u00f5es m\u00f3veis exp\u00f5e-no a qualquer pessoa que inspecione o bundle. Aplica\u00e7\u00f5es frontend puras s\u00e3o &#8220;public clients&#8221; e n\u00e3o devem ter <code>client_secret<\/code>: usam apenas PKCE. Para SPAs, use o padr\u00e3o Backend-for-Frontend (BFF), onde o servidor Node.js gere os tokens e o frontend comunica apenas com o BFF.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-6-sessoes-sem-prazo-de-validade\">Erro 6: Sess\u00f5es Sem Prazo de Validade<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Sess\u00f5es que nunca expiram permitem que tokens roubados sejam usados indefinidamente. Defina sempre <code>maxAge<\/code> no cookie de sess\u00e3o. Para opera\u00e7\u00f5es sens\u00edveis (altera\u00e7\u00e3o de password, pagamentos), re-autentique o utilizador mesmo com sess\u00e3o ativa, em vez de confiar na sess\u00e3o existente.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-7-nao-limpar-o-pkce-apos-o-callback\">Erro 7: N\u00e3o Limpar o PKCE Ap\u00f3s o Callback<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Manter o <code>code_verifier<\/code> e o <code>oauthState<\/code> na sess\u00e3o ap\u00f3s a troca de tokens bem-sucedida \u00e9 um desperd\u00edcio de mem\u00f3ria e potencialmente um risco se a sess\u00e3o for comprometida. O Passo 5 deste tutorial apaga estes valores imediatamente ap\u00f3s o callback, com <code>delete req.session.codeVerifier<\/code> e <code>delete req.session.oauthState<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"resolucao-de-problemas\">Resolu\u00e7\u00e3o de Problemas<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-redirect_uri_mismatch\">Erro: &#8220;redirect_uri_mismatch&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Causa:<\/strong> O <code>redirect_uri<\/code> no pedido n\u00e3o coincide byte-a-byte com o registado no GitHub. Uma barra final, http vs https ou porta diferente causam este erro. <strong>Solu\u00e7\u00e3o:<\/strong> Compare o valor de <code>REDIRECT_URI<\/code> no <code>.env<\/code> com o URL registado em GitHub &gt; Settings &gt; Developer settings &gt; OAuth Apps.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-bad_verification_code\">Erro: &#8220;bad_verification_code&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Causa:<\/strong> O c\u00f3digo de autoriza\u00e7\u00e3o foi usado mais do que uma vez (por exemplo, o utilizador refrescou a p\u00e1gina do callback) ou j\u00e1 expirou. Os c\u00f3digos do GitHub expiram ao fim de 10 minutos. <strong>Solu\u00e7\u00e3o:<\/strong> Ap\u00f3s o callback, redirecione imediatamente para outra p\u00e1gina. Trate o erro devolvendo redirect para <code>\/auth\/login<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-state-invalido-http-403\">Erro: State Inv\u00e1lido (HTTP 403)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Causa:<\/strong> A sess\u00e3o expirou durante o fluxo OAuth, ou o utilizador abriu m\u00faltiplos tabs com fluxos paralelos (cada tab sobrescreve o state da sess\u00e3o). <strong>Solu\u00e7\u00e3o:<\/strong> Aumente o <code>maxAge<\/code> da sess\u00e3o para pelo menos 15 minutos. Para m\u00faltiplos tabs, guarde um array de states v\u00e1lidos em vez de um \u00fanico valor.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-token-devolvido-como-string-url-encoded-github\">Erro: Token Devolvido como String URL-Encoded (GitHub)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Causa:<\/strong> O GitHub devolve o token em formato <code>application\/x-www-form-urlencoded<\/code> por padr\u00e3o. Sem o header <code>Accept: application\/json<\/code>, o axios recebe <code>access_token=gho_xxx&token_type=bearer<\/code> como string. <strong>Solu\u00e7\u00e3o:<\/strong> Sempre incluir <code>headers: { Accept: 'application\/json' }<\/code> no pedido ao endpoint de token.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-error-secret-option-required-for-sessions\">Erro: &#8220;Error: secret option required for sessions&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Causa:<\/strong> A vari\u00e1vel <code>SESSION_SECRET<\/code> \u00e9 undefined porque o <code>require('dotenv').config()<\/code> n\u00e3o foi chamado antes da configura\u00e7\u00e3o da sess\u00e3o. <strong>Solu\u00e7\u00e3o:<\/strong> Coloque <code>require('dotenv').config()<\/code> como primeira linha do <code>src\/index.js<\/code>, antes de qualquer outro <code>require<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-sessao-nao-persistida-apos-redirect\">Erro: Sess\u00e3o N\u00e3o Persistida Ap\u00f3s Redirect<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Causa:<\/strong> Alguns armazenamentos de sess\u00e3o s\u00e3o ass\u00edncronos. O <code>res.redirect()<\/code> pode ser chamado antes de a sess\u00e3o ser guardada. <strong>Solu\u00e7\u00e3o:<\/strong> Usar <code>req.session.save(callback)<\/code> antes do redirect, como mostrado no Passo 5 deste tutorial.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-cookie-nao-enviado-em-producao-com-https\">Erro: Cookie N\u00e3o Enviado em Produ\u00e7\u00e3o com HTTPS<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Causa:<\/strong> A aplica\u00e7\u00e3o est\u00e1 atr\u00e1s de um proxy (Nginx, Cloudflare) e o Express n\u00e3o deteta HTTPS corretamente, por isso descarta o cookie <code>secure: true<\/code>. <strong>Solu\u00e7\u00e3o:<\/strong> Adicione <code>app.set('trust proxy', 1)<\/code> antes da configura\u00e7\u00e3o de sess\u00e3o para confiar no header <code>X-Forwarded-Proto<\/code> do proxy.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"erro-cannot-get-auth-callback-apos-autorizacao\">Erro: &#8220;Cannot GET \/auth\/callback&#8221; ap\u00f3s Autoriza\u00e7\u00e3o<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Causa:<\/strong> A rota <code>\/auth\/callback<\/code> n\u00e3o est\u00e1 definida ou o servidor n\u00e3o est\u00e1 a correr. <strong>Solu\u00e7\u00e3o:<\/strong> Verifique que o servidor est\u00e1 ativo (<code>npm run dev<\/code>), que a rota existe no <code>src\/index.js<\/code>, e que o <code>REDIRECT_URI<\/code> no <code>.env<\/code> aponta para <code>localhost:3000<\/code> e n\u00e3o para outro porto.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"dicas-avancadas-para-producao\">Dicas Avan\u00e7adas para Produ\u00e7\u00e3o<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"usar-oauth4webapi-para-implementacoes-multi-fornecedor\">Usar oauth4webapi para Implementa\u00e7\u00f5es Multi-Fornecedor<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Para aplica\u00e7\u00f5es de produ\u00e7\u00e3o com v\u00e1rios fornecedores OAuth, o <a href=\"https:\/\/oauth.net\/code\/nodejs\/\" target=\"_blank\" rel=\"noopener noreferrer\">oauth4webapi<\/a> \u00e9 o cliente OpenID Certified recomendado para JavaScript e Node.js em 2026. Gera automaticamente PKCE, valida tokens JWT e suporta OpenID Connect. A biblioteca tem zero depend\u00eancias externas e corre em Edge Runtime (Vercel, Cloudflare Workers), o que a torna adequada para arquiteturas serverless.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"implementar-detecao-de-reutilizacao-de-refresh-token\">Implementar Dete\u00e7\u00e3o de Reutiliza\u00e7\u00e3o de Refresh Token<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Com rota\u00e7\u00e3o de refresh tokens (suportada pelo Google e Keycloak), cada uso do refresh token invalida o anterior. Se o token antigo for apresentado novamente, indica que o original foi roubado. Implemente esta dete\u00e7\u00e3o: ao receber erro de refresh token inv\u00e1lido, destrua imediatamente TODAS as sess\u00f5es do utilizador e notifique-o de potencial comprometimento de conta.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"adicionar-helmet-js-para-headers-de-seguranca-http\">Adicionar Helmet.js para Headers de Seguran\u00e7a HTTP<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">XSS \u00e9 a principal amea\u00e7a aos dados de sess\u00e3o. Um Content Security Policy (CSP) restrito reduz drasticamente o risco. Adicione o pacote <code>helmet<\/code> ao servidor Express:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install helmet\n\n\/\/ src\/index.js - adicionar antes das rotas\nconst helmet = require('helmet');\napp.use(helmet({\n  contentSecurityPolicy: {\n    directives: {\n      defaultSrc: [\"'self'\"],\n      scriptSrc: [\"'self'\"],\n      connectSrc: [\"'self'\", 'api.github.com'],\n      imgSrc: [\"'self'\", 'avatars.githubusercontent.com', 'data:'],\n    },\n  },\n  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },\n}));<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"limitar-taxa-de-pedidos-nas-rotas-oauth\">Limitar Taxa de Pedidos nas Rotas OAuth<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">As rotas <code>\/auth\/login<\/code> e <code>\/auth\/callback<\/code> s\u00e3o alvos de ataques de for\u00e7a bruta e enumera\u00e7\u00e3o de c\u00f3digos. Aplique rate limiting nestas rotas especificamente, com limites mais restritivos que no resto da aplica\u00e7\u00e3o (por exemplo, 10 pedidos por minuto por IP).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"usar-keycloak-como-authorization-server-local\">Usar Keycloak como Authorization Server Local<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Para ambientes empresariais com SSO, o Keycloak oferece suporte completo a OAuth 2.0 com PKCE, OpenID Connect, refresh token rotation, e revoga\u00e7\u00e3o de tokens. Em desenvolvimento, use Docker para executar uma inst\u00e2ncia local sem depender de servi\u00e7os externos de terceiros:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">docker run -p 8080:8080 \\\n  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \\\n  -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \\\n  quay.io\/keycloak\/keycloak:latest start-dev\n\n# Aceder \u00e0 consola de administra\u00e7\u00e3o:\n# http:\/\/localhost:8080\/admin<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"comparacao-de-bibliotecas-oauth-para-node-js-em-2026\">Compara\u00e7\u00e3o de Bibliotecas OAuth para Node.js em 2026<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Biblioteca<\/th><th>PKCE<\/th><th>OIDC<\/th><th>Zero Deps<\/th><th>Edge Runtime<\/th><th>Melhor Para<\/th><\/tr><\/thead><tbody>\n<tr><td>oauth4webapi<\/td><td>Sim<\/td><td>Sim (Certified)<\/td><td>Sim<\/td><td>Sim<\/td><td>Produ\u00e7\u00e3o multi-fornecedor<\/td><\/tr>\n<tr><td>openid-client<\/td><td>Sim<\/td><td>Sim (Certified)<\/td><td>N\u00e3o<\/td><td>Parcial<\/td><td>Apps Node.js enterprise<\/td><\/tr>\n<tr><td>simple-oauth2 5.1.0<\/td><td>Manual<\/td><td>N\u00e3o<\/td><td>N\u00e3o<\/td><td>N\u00e3o<\/td><td>OAuth 2.0 simples sem OIDC<\/td><\/tr>\n<tr><td>passport-oauth2 1.8.0<\/td><td>Manual<\/td><td>N\u00e3o<\/td><td>N\u00e3o<\/td><td>N\u00e3o<\/td><td>Apps Passport.js existentes<\/td><\/tr>\n<tr><td>Implementa\u00e7\u00e3o nativa<\/td><td>Sim (manual)<\/td><td>N\u00e3o<\/td><td>Sim<\/td><td>Sim<\/td><td>Aprendizagem e controlo total<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"cobertura-relacionada\">Cobertura Relacionada<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"artigos-relacionados\">Artigos Relacionados<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/pt\/autenticacao-dois-fatores-nodejs\/\">Autentica\u00e7\u00e3o de Dois Fatores em Node.js: 12 Passos<\/a> (adicione TOTP como segundo fator ap\u00f3s o login OAuth)<\/li>\n<li><a href=\"\/pt\/mtls-node-js-tls-13\/\">mTLS em Node.js: TLS 1.3 em 12 Passos<\/a> (use mTLS para Sender-Constrained tokens em APIs internas)<\/li>\n<li><a href=\"\/pt\/autenticacao-ssh-chaves\/\">Autentica\u00e7\u00e3o SSH com Chaves Ed25519: 12 Passos<\/a> (criptografia de curva el\u00edptica para autentica\u00e7\u00e3o segura)<\/li>\n<li><a href=\"\/pt\/https-nginx-lets-encrypt\/\">HTTPS no Nginx com Let&#8217;s Encrypt: 12 Passos<\/a> (configure TLS no servidor antes de lan\u00e7ar OAuth em produ\u00e7\u00e3o)<\/li>\n<li><a href=\"\/pt\/nmap-auditar-rede-12-passos\/\">Nmap: Auditar a Rede em 12 Passos<\/a> (audite os endpoints OAuth expostos na infraestrutura)<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"perguntas-frequentes\">Perguntas Frequentes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"oauth-2-0-e-openid-connect-sao-a-mesma-coisa\">OAuth 2.0 e OpenID Connect s\u00e3o a mesma coisa?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">N\u00e3o. OAuth 2.0 \u00e9 um protocolo de <strong>autoriza\u00e7\u00e3o<\/strong>: emite tokens de acesso para APIs protegidas. OpenID Connect (OIDC) \u00e9 uma camada de <strong>identidade<\/strong> constru\u00edda sobre OAuth 2.0: adiciona o <code>id_token<\/code> (um JWT com informa\u00e7\u00e3o do utilizador) e o endpoint padronizado <code>\/userinfo<\/code>. Para login de utilizadores (&#8220;quem \u00e9 este utilizador?&#8221;), use OIDC. Para acesso delegado a APIs (&#8220;o utilizador autorizou esta a\u00e7\u00e3o?&#8221;), use OAuth 2.0 puro.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"preciso-de-pkce-se-a-minha-aplicacao-tem-um-backend-seguro-com-client_secret\">Preciso de PKCE se a minha aplica\u00e7\u00e3o tem um backend seguro com client_secret?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Sim. O RFC 9126 recomenda PKCE para todos os clientes OAuth, incluindo aplica\u00e7\u00f5es confidenciais com servidor. O risco de interce\u00e7\u00e3o de c\u00f3digo \u00e9 menor com um backend seguro, mas o PKCE adiciona defesa em profundidade sem custo computacional significativo. Com OAuth 2.1, o PKCE torna-se obrigat\u00f3rio para todos os clientes no fluxo de c\u00f3digo de autoriza\u00e7\u00e3o.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"qual-a-diferenca-entre-access_token-e-refresh_token\">Qual a diferen\u00e7a entre access_token e refresh_token?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">O <code>access_token<\/code> \u00e9 uma credencial de curta dura\u00e7\u00e3o (1 hora no Google, indefinida no GitHub) usada para aceder a APIs protegidas. O <code>refresh_token<\/code> \u00e9 uma credencial de longa dura\u00e7\u00e3o usada exclusivamente para obter novos <code>access_token<\/code> sem interven\u00e7\u00e3o do utilizador. O <code>refresh_token<\/code> exige maior prote\u00e7\u00e3o: o seu comprometimento permite acesso cont\u00ednuo at\u00e9 revoga\u00e7\u00e3o expl\u00edcita. Guarde-o apenas no servidor, nunca no browser.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"como-testar-oauth-2-0-localmente-sem-conta-github\">Como testar OAuth 2.0 localmente sem conta GitHub?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use o Keycloak em Docker como Authorization Server local. Suporta PKCE completo, emite refresh tokens e permite simular m\u00faltiplos utilizadores e escopos sem depender de servi\u00e7os externos. O comando de arranque est\u00e1 no Passo &#8220;Dicas Avan\u00e7adas&#8221;. Aceda a <code>http:\/\/localhost:8080\/admin<\/code> para criar realm, clientes e utilizadores de teste.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"o-que-acontece-se-o-utilizador-revogar-o-acesso-na-conta-github\">O que acontece se o utilizador revogar o acesso na conta GitHub?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">O <code>access_token<\/code> torna-se inv\u00e1lido imediatamente. A pr\u00f3xima chamada \u00e0 API do GitHub retorna <code>401 Unauthorized<\/code>. A aplica\u00e7\u00e3o deve tratar este erro destruindo a sess\u00e3o do utilizador e redirecionando para <code>\/auth\/login<\/code>. O Passo 10 deste tutorial mostra este tratamento dentro do bloco <code>catch<\/code> com verifica\u00e7\u00e3o de <code>error.response?.status === 401<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"qual-a-duracao-recomendada-para-access-tokens-e-refresh-tokens\">Qual a dura\u00e7\u00e3o recomendada para access tokens e refresh tokens?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Para <strong>access tokens<\/strong>: 15 a 60 minutos para APIs de uso geral; 5 a 15 minutos para APIs de alto valor. Para <strong>refresh tokens<\/strong>: 7 dias para aplica\u00e7\u00f5es consumer com uso di\u00e1rio; 30 dias para enterprise com rota\u00e7\u00e3o ativa. Tokens de maior dura\u00e7\u00e3o convencem os utilizadores, mas aumentam a janela de explora\u00e7\u00e3o ap\u00f3s roubo. O modelo de amea\u00e7a da aplica\u00e7\u00e3o deve guiar esta decis\u00e3o.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"posso-usar-oauth-2-0-para-autenticar-utilizadores-internos-sem-identidade-federada\">Posso usar OAuth 2.0 para autenticar utilizadores internos sem identidade federada?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Sim, com um Authorization Server pr\u00f3prio (Keycloak auto-hospedado ou <code>@node-oauth\/oauth2-server<\/code> npm). Para utilizadores internos sem identidade federada, considere Passkeys (WebAuthn) como alternativa mais moderna: resistente a phishing, sem depend\u00eancia de terceiros e com suporte nativo nos browsers atuais. OAuth faz mais sentido quando existe identidade federada, SSO empresarial ou acesso delegado a APIs de terceiros.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>OAuth 2.0 \u00e9 o protocolo de autoriza\u00e7\u00e3o que est\u00e1 por tr\u00e1s de praticamente todos os bot\u00f5es &#8220;Iniciar sess\u00e3o com Google&#8221; ou &#8220;Entrar com GitHub&#8221; que utiliza todos os dias. Em\u2026<\/p>\n","protected":false},"author":6,"featured_media":125,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-124","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-security"],"_links":{"self":[{"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/posts\/124","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/comments?post=124"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/posts\/124\/revisions"}],"predecessor-version":[{"id":126,"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/posts\/124\/revisions\/126"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/media\/125"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/media?parent=124"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/categories?post=124"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/tags?post=124"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}