Recursos & APIs
Autenticação OAuth 2.0 PKCE
Como autenticar usuários com segurança usando PKCE (RFC 7636), State criptográfico e Deep Linking (pandhora-app://).
Autenticação OAuth 2.0 PKCE
O Pandhora fornece uma API nativa e padronizada de OAuth 2.0 com PKCE (Proof Key for Code Exchange — RFC 7636) através de context.oauth.
Esse fluxo elimina totalmente a necessidade de embutir CLIENT_SECRET nos addons e dispensa a abertura de servidores HTTP locais na máquina do usuário.
🔄 Fluxo de Autenticação em 5 Passos
1. Addon (Desktop)
│ context.oauth.generatePKCE() → gera codeVerifier + codeChallenge (SHA-256)
│ context.oauth.generateState('discord') → gera state seguro (UUID)
│ context.oauth.authorize({ authUrl, state })
▼
2. Navegador Web do Usuário
│ Abre a tela de consentimento do Provedor OAuth (Discord, Google, Jira, etc.)
▼
3. Redirecionamento Web
│ Provedor redireciona para a Landing Page:
│ https://pandhoraapp.com.br/oauth/callback?code=AUTH_CODE&state=STATE
▼
4. Deep Link do Sistema Operacional
│ A Landing Page executa o protocolo seguro:
│ pandhora-app://oauth/callback?code=AUTH_CODE&state=STATE
▼
5. Addon (Desktop)
│ AddonLoader valida o state, remove replay e resolve o authorization code
│ Addon executa a troca segura (POST /token com code_verifier, sem secret)
│ Salva o token estruturado no storage seguro (Keytar)
🛡️ Por que PKCE é 100% Seguro em Apps Desktop?
- Sem Segredos Expostos: Como o código do addon roda no dispositivo do usuário, qualquer
CLIENT_SECRETembutido poderia ser extraído. O PKCE substitui o segredo por um desafio criptográfico único gerado em tempo de execução (code_challenge/code_verifier). - Proteção Contra Interceptação de Deep Link: Mesmo que um aplicativo malicioso no sistema operacional capture o Deep Link contendo o
codee ostate, ele não conseguirá obter o token de acesso porque não possui ocode_verifier(que permanece restrito à memória segura do Pandhora). - Prevenção Contra CSRF e Replay: O
stateé gerado viacrypto.randomUUID(), validado estritamente no callback e consumido em modo single-use (qualquer repetição é imediatamente rejeitada).
💻 Exemplo Completo de Implementação
import type { IAddon, AddonContext } from '@pandhora/sdk'
export default class DiscordOAuthAddon implements IAddon {
public id = 'discord-oauth'
public name = 'Discord OAuth Integration'
public version = '1.0.0'
public async handleLogin(context: AddonContext): Promise<void> {
// 1. Gera par PKCE (RFC 7636) e state criptográfico
const { codeVerifier, codeChallenge } = context.oauth.generatePKCE()
const state = context.oauth.generateState('discord')
const CLIENT_ID = '1372352088457220126' // Client ID Público
const REDIRECT_URI =
process.env.PANDHORA_OAUTH_REDIRECT_URI ||
'http://localhost:3000/oauth/callback' // Em produção: https://pandhoraapp.com.br/oauth/callback
const authUrl =
`https://discord.com/api/oauth2/authorize?client_id=${CLIENT_ID}` +
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
`&response_type=code&scope=identify%20rpc` +
`&code_challenge=${codeChallenge}&code_challenge_method=S256&state=${state}`
console.log('[OAuth] Abrindo navegador para autorização...')
// 2. Dispara a autorização e aguarda o Deep Link (com timeout de segurança)
const result = await context.oauth.authorize({
authUrl,
state,
timeoutMs: 120000, // 2 minutos
})
if (!result.code) {
throw new Error('Código de autorização não recebido.')
}
console.log('[OAuth] Código recebido via Deep Link. Trocando pelo Token...')
// 3. Troca do Authorization Code pelo Access Token via PKCE
const params = new URLSearchParams({
client_id: CLIENT_ID,
grant_type: 'authorization_code',
code: result.code,
code_verifier: codeVerifier, // Prova de posse do desafio original
redirect_uri: REDIRECT_URI,
})
const response = await fetch('https://discord.com/api/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params,
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(
`Falha na autenticação: ${errorData.error_description || errorData.error || response.status}`,
)
}
const tokenPayload = await response.json()
if (!tokenPayload.access_token) {
throw new Error('access_token não retornado pelo provedor OAuth.')
}
// 4. Normalização e persistência estruturada no Storage Criptografado
const storedToken = {
accessToken: tokenPayload.access_token,
refreshToken: tokenPayload.refresh_token,
tokenType: tokenPayload.token_type || 'Bearer',
expiresAt: tokenPayload.expires_in
? new Date(Date.now() + tokenPayload.expires_in * 1000).toISOString()
: undefined,
scope: tokenPayload.scope,
}
await context.storage.set('tokenData', JSON.stringify(storedToken))
await context.storage.set('accessToken', storedToken.accessToken)
console.log(
'[OAuth] Autenticação concluída e token armazenado com sucesso!',
)
}
}