Pular para o conteúdo

Autenticação

Senha e hashing (scrypt, PBKDF2, bcrypt, Argon2id), MFA, 2FA, TOTP, WebAuthn e passkeys, OAuth 2.0, OIDC, SAML, LDAP, JWT, tokens e sessão — com o veredito de cada um.

Autenticação responde quem é você. Esta página percorre os mecanismos usados na indústria e diz, para cada um, o que a linguagem entrega — e o que ela não entrega.

Senha: o hashing#

Guardar senha é guardar um derivado lento e salgado dela. A lentidão é o recurso: ela não incomoda quem faz um login e inviabiliza quem faz bilhões.

AlgoritmoEstado aquiO veredito
scrypto padrão, Crypto.hash_passwordmemory-hard: exige ~32 MB por tentativa, e memória é o que a GPU não tem em abundância por núcleo
PBKDF2-SHA256disponível, algoritmo := "pbkdf2"só encadeia hash — barato de acelerar em GPU. Fica para compatibilidade e para senhas antigas
Argon2idnão existeseria o melhor; em Python puro rodaria lento a ponto de exigir parâmetros fracos, o que o torna pior que o scrypt
bcryptnão existeexige uma implementação de Blowfish; o hashlib não a traz, e escrevê-la em Python é o tipo de código criptográfico que não se deve escrever
dataforge
adopt Arcane.Crypto as Crypto

// O sal e sorteado e vai DENTRO da string guardada: guarde-a inteira.
guardada := Crypto.hash_password("uma senha bem forte 2026")

out guardada[0:24]              // scrypt$32768$8$1$...

assert Crypto.verify_password("uma senha bem forte 2026", guardada)
assert Crypto.verify_password("outra senha", guardada) is no

// Duas vezes a MESMA senha dao strings diferentes — e o sal e o
// motivo: sem ele, senhas iguais teriam o mesmo resumo, e uma
// tabela pronta quebraria todas de uma vez.
assert Crypto.hash_password("igual") isnt Crypto.hash_password("igual")

Rotação: o único momento em que dá para atualizar#

Sem isto, um banco fica para sempre no algoritmo com que nasceu — ninguém sabe quais linhas estão velhas, e não há momento em que a senha em claro esteja disponível para regravar. Exceto um: o login bem-sucedido.

dataforge
adopt Arcane.Crypto as Crypto

action entrar(senha, guardada):
    given Crypto.verify_password(senha, guardada) is no:
        yield {"ok": no}

    // Aqui — e so aqui — a senha em claro existe e ja foi conferida.
    given Crypto.precisa_rehash(guardada):
        yield {"ok": yes, "regravar": Crypto.hash_password(senha)}

    yield {"ok": yes}

// Uma senha guardada no formato antigo continua entrando...
antiga := Crypto.hash_password("segredo do usuario", algoritmo := "pbkdf2")
r := entrar("segredo do usuario", antiga)
assert r["ok"]
// ...e sai do login ja no formato novo.
assert "regravar" in keys(r)

out "o formato antigo entra, e e atualizado na saida"

Força, política e vazamento#

“Senha fraca” não diz o que fazer. O que vai para a tela é a lista do que falta.

dataforge
adopt Arcane.Seguranca as Seg

fraca := Seg.forca_da_senha("Abcdef1!")
out $"{fraca['rotulo']} ({fraca['nota']}/100)"
cycle problema in fraca["problemas"]:
    out $"   - {problema}"

// A politica devolve; 'exigir_politica' levanta com a lista em
// 'e.nota', para quem escreve o caminho feliz.
r := Seg.politica("Tr0vao#Azul7291!x")
assert r["ok"]

// E a entropia, que e uma medida do TEXTO e nao de como ele foi
// escolhido: por isso ela e um dos criterios, e nunca o unico.
out $"entropia: {r['entropia']} bits"

Múltiplos fatores#

Um fator é algo que você sabe (senha), tem (telefone, chave física) ou é (biometria). MFA exige de categorias diferentes — duas senhas não são dois fatores.

FatorEstado aquiObservação
TOTP (app autenticador)existe, RFC 6238 completoo segundo fator com melhor relação custo/benefício
HOTP (por contador)existe, RFC 4226base do TOTP; serve para token físico de contador
Códigos de recuperaçãoexistedevolve o código e o resumo — guarde só o resumo
SMSnão há integraçãodesaconselhado: SIM swap e interceptação de SS7
WebAuthn / passkeys / FIDO2não existeé um protocolo com CBOR, COSE e atestação, não uma função
Chave física (YubiKey)não existechega por WebAuthn
dataforge
adopt Arcane.Seguranca as Seg

// Os vetores do RFC 4226. Uma implementacao de OTP que nao os
// reproduz esta errada mesmo que "funcione": o autenticador do
// usuario vai discordar dela.
assert Seg.hotp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 0) is "755224"
assert Seg.hotp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 1) is "287082"

// O cadastro: sorteie o segredo, mostre o QR, e guarde o segredo
// CIFRADO — ele vale tanto quanto a senha.
segredo := Seg.totp_segredo()
out Seg.totp_uri(segredo, "ana@loja.com", emissor := "Minha Loja")

// A conferencia aceita a janela vizinha: o relogio do telefone anda
// alguns segundos fora, e um codigo digitado no segundo 29 chega no
// 31. Sem tolerancia, uma fracao real dos logins legitimos falha —
// e o usuario aprende a desligar o segundo fator.
codigo := Seg.totp_agora(segredo)
assert Seg.totp_conferir(segredo, codigo)
assert Seg.totp_conferir(segredo, "000000") is no

// E os codigos de recuperacao, para quem perdeu o telefone.
r := Seg.codigos_de_recuperacao(10)
out $"{len(r['codigos'])} codigos; guarde apenas os {len(r['resumos'])} resumos"

WebAuthn e passkeys — o veredito#

São hoje o mecanismo mais forte disponível: a chave privada nunca sai do dispositivo, e a assinatura é ligada ao domínio, o que torna o phishing estruturalmente impossível.

OAuth 2.0, OIDC, SAML, LDAP#

São integrações com sistemas externos, e não primitivas. O que a biblioteca traz são as peças locais de cada fluxo — as que envolvem criptografia e que são justamente onde as implementações erram.

ProtocoloPara quêEstado aqui
OAuth 2.0autorização delegada — acesso a um recursoas peças locais: pkce, estado_de_oauth; o HTTP é Arcane.Malha
OpenID Connectautenticação sobre OAuth — quem é a pessoaa verificação do id_token é Crypto.jwt_verificar (HS\; para RS256 é preciso um serviço)*
SAMLSSO corporativo em XMLnão existe — exige XML-DSig e canonicalização, cheios de armadilhas
LDAP / Active Directorydiretório corporativonão existe — é um protocolo binário próprio
dataforge
adopt Arcane.Seguranca as Seg

// PKCE (RFC 7636) — o que o OAuth 2.0 exige hoje, e a peca que a
// biblioteca entrega. O 'code' volta pela URL do navegador; num app
// de celular ou numa SPA nao existe segredo do cliente para provar
// quem e, e quem interceptar o codigo o troca por um token.
par := Seg.pkce()

// Ao PEDIR o codigo, manda-se o desafio (o SHA-256 do verificador).
out $"code_challenge={par['desafio'][0:16]}... method={par['metodo']}"

// Ao TROCAR o codigo, manda-se o verificador. So quem sorteou o tem.
assert Seg.conferir_pkce(par["verificador"], par["desafio"])
assert Seg.conferir_pkce("outro-verificador-que-nao-e-o-certo-123456", par["desafio"]) is no

// E o 'state', que e a defesa de CSRF do fluxo: sem ele, um atacante
// inicia o proprio fluxo e faz a vitima completa-lo — a conta dela
// acaba ligada a conta dele no provedor.
estado := Seg.estado_de_oauth()
// guarde na sessao, e CONFIRA na volta. Guardar e nao conferir e o
// defeito mais comum deste fluxo.
assert len(estado) bigger 20

out "as pecas locais do OAuth, e a rede fica com o Arcane.Malha"

JWT#

Um JWT é um objeto JSON assinado. Ele é útil quando o receptor precisa validar sem consultar um banco — e é exatamente por isso que ele é difícil de revogar.

dataforge
adopt Arcane.Crypto as Crypto

chave := Crypto.random_hex(32)

token := Crypto.jwt_assinar({"sub": "ana", "papel": "editor"}, chave,
    expira_em := 900)

// O ALGORITMO E DECIDIDO POR QUEM VERIFICA, e nao pelo token.
// Essa e a falha classica do JWT: aceitar o 'alg' que vem dentro
// permite 'alg: none' — e o token passa a ser texto assinado por
// ninguem. Aqui o algoritmo e argumento da verificacao.
r := Crypto.jwt_verificar(token, chave, algoritmo := "HS256")
assert r["valido"]
assert r["carga"]["sub"] is "ana"

// 'jwt_ler' le SEM verificar — para inspecionar, nunca para decidir.
out Crypto.jwt_ler(token)["papel"]

// Ele DEVOLVE o veredito em vez de levantar: o motivo entra no log,
// e quem chama decide o que responder ao cliente.
outra := Crypto.jwt_verificar(token, "outra chave", algoritmo := "HS256")
assert outra["valido"] is no
out $"recusado: {outra['motivo']}"
A armadilhaComo se fecha aqui
alg: noneo algoritmo é argumento de quem verifica, nunca lido do token
confusão HS256/RS256idem — não há como o token escolher
token que não expiraexpira_em é conferido na verificação
revogaçãonão tem solução no formato: use prazo curto + refresh
dado sensível no corpoo corpo é base64, não é cifrado — qualquer um lê

Tokens e sessão#

TipoVidaOnde guardarComo revogar
acesso5–15 minmemória do clientenão se revoga: expira
refreshdias/semanascookie HttpOnlyno banco — e rotacione a cada uso
sessãohorascookie HttpOnlyapagar do armazém
API keylongacofre do clienteno banco, por prefixo
de uso único (e-mail, senha)minutosnão se guardapropósito + prazo, e marcar usado
dataforge
adopt Arcane.Seguranca as Seg

chave := Seg.chave_de_assinatura()

// Um link de redefinir senha: proposito E prazo. Sem o proposito, o
// token que confirma um e-mail SERVE para trocar a senha — os dois
// sao assinados com a mesma chave.
link := Seg.assinar({"usuario": 42}, chave, proposito := "trocar-senha")

monitor:
    Seg.ler_assinado(link, chave, prazo := 900, proposito := "confirmar-email")
    assert no
handle SignatureError as e:
    out "recusado: o proposito e outro"

lido := Seg.ler_assinado(link, chave, prazo := 900, proposito := "trocar-senha")
assert lido["valor"]["usuario"] is 42

// E a URL assinada, para download temporario: mudar QUALQUER
// parametro invalida a assinatura.
u := Seg.assinar_url("https://loja.com/baixar?id=9", chave, prazo := 60)
assert Seg.conferir_url(u, chave)["ok"]
AtributoPorque
HttpOnlyo JavaScript não lê — um XSS deixa de virar roubo de sessão
Securenão viaja em HTTP puro
SameSite=Laxnão vai numa requisição de outro site (defesa de CSRF)
Path=/, sem Domainnão vaza para subdomínio de terceiro
id novo após o loginimpede fixação de sessão