secrets em Python: tokens e senhas aleatórias com segurança

Aprenda a usar secrets em Python para gerar tokens, senhas e códigos aleatórios, comparar valores com segurança e implementar recuperação de senha.

13 min de leitura Equipe Python Brasil

O módulo secrets é a escolha da biblioteca padrão para gerar tokens, códigos e senhas aleatórias em Python quando a imprevisibilidade tem impacto de segurança. Ele deve ser usado em links de recuperação de senha, convites de uso único, chaves temporárias, identificadores privados e outros valores que um invasor não pode adivinhar.

A recomendação direta é: use secrets.token_urlsafe(32) para a maioria dos tokens enviados em URLs, secrets.token_hex(32) quando o sistema exige texto hexadecimal, secrets.choice() para montar uma senha ou código com alfabeto controlado e secrets.compare_digest() para comparações sensíveis. Não use random, timestamp, UUID curto, e-mail ou CPF como fonte de segredo.

Neste guia, você vai construir um fluxo de recuperação de senha com expiração e uso único, entender quantos bytes escolher, evitar vazamento em logs e separar três problemas diferentes: gerar um segredo, armazenar uma senha e gerenciar a configuração da aplicação.

Por que random não serve para tokens de segurança?

O módulo random foi projetado para simulações, sorteios locais, jogos e testes reproduzíveis. Seu gerador pode ser inicializado com uma semente e produz uma sequência determinística:

import random

random.seed(2026)
print(random.randint(100000, 999999))

Repetir o programa com a mesma semente reproduz o resultado. Essa característica é útil em ciência de dados, mas inadequada quando o valor concede acesso a uma conta.

secrets obtém aleatoriedade da fonte segura do sistema operacional. A interface é simples:

import secrets

print(secrets.token_urlsafe(32))

Você não informa uma semente e não deve tentar tornar o resultado reproduzível. O objetivo é justamente impedir que outra pessoa calcule o próximo valor observando saídas anteriores.

NecessidadeFerramenta indicada
Simulação, amostragem e jogorandom
Teste que precisa repetir resultadosrandom.Random(semente)
Token de recuperação ou convitesecrets
Senha temporária gerada pelo sistemasecrets
Identificador público sem função de autenticaçãouuid pode ser suficiente
Hash de senha de usuárioArgon2id, scrypt ou bcrypt

Um UUID pode identificar um pedido ou correlação de log, mas não deve ser escolhido automaticamente como credencial. Identificação e autorização são responsabilidades diferentes.

As funções mais úteis do módulo secrets

O módulo é pequeno. Quatro APIs resolvem a maior parte dos casos cotidianos.

token_urlsafe(nbytes) gera bytes aleatórios e os representa em Base64 apropriado para URLs:

from secrets import token_urlsafe

TOKEN_BYTES = 32
token = token_urlsafe(TOKEN_BYTES)

print(token)
print(len(token))  # normalmente cerca de 43 caracteres

O comprimento textual não é igual a nbytes, porque a codificação transforma os bytes. Com 32 bytes, o resultado costuma ter aproximadamente 43 caracteres e carrega 256 bits de aleatoriedade antes da codificação.

Use essa opção para:

  • recuperação de senha;
  • confirmação de e-mail;
  • convite para uma equipe;
  • download privado com validade curta;
  • autorização de uma ação única.

O token pode aparecer como segmento de caminho ou parâmetro, conforme o framework. Se usar query string, avalie o risco de ela ser registrada por servidor, proxy, analytics ou ferramenta de suporte.

token_hex: interoperabilidade com sistemas legados

token_hex(nbytes) representa cada byte com dois caracteres hexadecimais:

from secrets import token_hex

chave = token_hex(32)
print(chave)
print(len(chave))  # 64 caracteres

O hexadecimal é maior que o formato URL-safe para a mesma quantidade de bytes, mas é fácil de validar e interoperar. Ele aparece em sistemas que aceitam somente [0-9a-f].

Não confunda o argumento com o tamanho final: token_hex(32) recebe 32 bytes e devolve 64 caracteres.

token_bytes: quando a próxima etapa aceita bytes

Se o valor será entregue a uma função criptográfica ou protocolo binário, evite converter para texto sem necessidade:

from secrets import token_bytes

material = token_bytes(32)
assert isinstance(material, bytes)
assert len(material) == 32

A representação em texto é uma decisão de transporte. Base64 e hexadecimal não adicionam entropia nem criptografam o conteúdo; apenas codificam os mesmos bytes.

choice e randbelow: alfabeto ou intervalo controlado

secrets.choice() escolhe um item de uma sequência. randbelow(limite) devolve um inteiro entre zero e limite - 1:

import secrets
import string

alfabeto = string.ascii_uppercase + string.digits
codigo = "".join(secrets.choice(alfabeto) for _ in range(8))
numero = secrets.randbelow(100)

print(codigo)
print(numero)

Um código curto tem muito menos combinações que um token de 32 bytes. Se ele autoriza uma ação, compense com validade curta, limite de tentativas, bloqueio progressivo e associação ao usuário ou à operação correta.

Como gerar uma senha temporária sem criar uma senha fraca

Uma regra ingênua pode gerar oito caracteres apenas numéricos ou produzir uma sequência difícil de digitar. Uma função melhor declara tamanho, alfabeto e requisitos:

import secrets
import string


def gerar_senha_temporaria(tamanho: int = 16) -> str:
    if tamanho < 12:
        raise ValueError("Use pelo menos 12 caracteres")

    letras = string.ascii_letters
    numeros = string.digits
    simbolos = "!@#$%*-_"
    alfabeto = letras + numeros + simbolos

    while True:
        senha = "".join(secrets.choice(alfabeto) for _ in range(tamanho))
        if (
            any(c.islower() for c in senha)
            and any(c.isupper() for c in senha)
            and any(c.isdigit() for c in senha)
            and any(c in simbolos for c in senha)
        ):
            return senha


print(gerar_senha_temporaria())

O while repete a geração até satisfazer os requisitos. Para uma senha criada pelo próprio usuário, regras rígidas de composição nem sempre melhoram a segurança; comprimento, bloqueio de senhas comprometidas e autenticação multifator costumam importar mais. Aqui a composição faz sentido porque o sistema controla a geração.

Se a senha será digitada manualmente, considere uma passphrase com palavras aleatórias de uma lista revisada. Não busque palavras em tempo de execução em uma fonte não confiável e não use uma lista pequena. Para códigos exibidos por telefone ou atendimento, remova caracteres confundíveis, como 0, O, 1, I e l.

Uma senha temporária deve obrigar troca no primeiro acesso e expirar. Sempre que possível, prefira um link de definição de senha: ele evita enviar uma credencial pronta por e-mail.

Exemplo completo: recuperação de senha com token de uso único

Um fluxo seguro não salva o token original no banco. A aplicação envia o original ao usuário, armazena um hash e verifica validade e uso quando o link retorna.

Abaixo está uma implementação independente de framework com SQLite para demonstrar o ciclo de vida:

from __future__ import annotations

import hashlib
import secrets
import sqlite3
from datetime import UTC, datetime, timedelta

TOKEN_BYTES = 32
VALIDADE = timedelta(minutes=30)


def hash_token(token: str) -> str:
    return hashlib.sha256(token.encode("utf-8")).hexdigest()


def criar_tabela(conexao: sqlite3.Connection) -> None:
    conexao.execute(
        """
        CREATE TABLE IF NOT EXISTS password_reset (
            token_hash TEXT PRIMARY KEY,
            usuario_id INTEGER NOT NULL,
            expira_em TEXT NOT NULL,
            usado_em TEXT
        )
        """
    )


def criar_token(conexao: sqlite3.Connection, usuario_id: int) -> str:
    token = secrets.token_urlsafe(TOKEN_BYTES)
    expira_em = datetime.now(UTC) + VALIDADE

    conexao.execute(
        """
        INSERT INTO password_reset
            (token_hash, usuario_id, expira_em, usado_em)
        VALUES (?, ?, ?, NULL)
        """,
        (hash_token(token), usuario_id, expira_em.isoformat()),
    )
    conexao.commit()
    return token


def consumir_token(conexao: sqlite3.Connection, token: str) -> int | None:
    agora = datetime.now(UTC)
    token_hash = hash_token(token)

    conexao.execute("BEGIN IMMEDIATE")
    registro = conexao.execute(
        """
        SELECT usuario_id, expira_em, usado_em
        FROM password_reset
        WHERE token_hash = ?
        """,
        (token_hash,),
    ).fetchone()

    if registro is None:
        conexao.rollback()
        return None

    usuario_id, expira_em_texto, usado_em = registro
    expira_em = datetime.fromisoformat(expira_em_texto)

    if usado_em is not None or expira_em <= agora:
        conexao.rollback()
        return None

    atualizado = conexao.execute(
        """
        UPDATE password_reset
        SET usado_em = ?
        WHERE token_hash = ? AND usado_em IS NULL
        """,
        (agora.isoformat(), token_hash),
    )

    if atualizado.rowcount != 1:
        conexao.rollback()
        return None

    conexao.commit()
    return int(usuario_id)

Uso local:

with sqlite3.connect(":memory:") as conexao:
    criar_tabela(conexao)

    token = criar_token(conexao, usuario_id=42)
    link = f"https://exemplo.com.br/redefinir-senha?token={token}"
    print(link)

    assert consumir_token(conexao, token) == 42
    assert consumir_token(conexao, token) is None  # uso único

O exemplo usa BEGIN IMMEDIATE e uma atualização condicionada para reduzir a chance de duas requisições consumirem o mesmo token. Em PostgreSQL, a aplicação pode usar transação e bloqueio de linha ou um UPDATE ... WHERE usado_em IS NULL ... RETURNING. O guia de SQLite com Python ajuda a entender parâmetros, transações e persistência.

Em produção, o endpoint também deve:

  1. receber a nova senha somente por POST;
  2. validar o token antes da alteração;
  3. salvar a senha com algoritmo específico para senhas;
  4. invalidar outros tokens de recuperação do usuário;
  5. considerar encerrar sessões existentes;
  6. registrar o evento sem registrar token ou senha;
  7. aplicar rate limiting por conta, IP e sinais de abuso;
  8. devolver resposta genérica ao solicitar recuperação, sem confirmar se o e-mail existe.

O hash SHA-256 é adequado aqui porque o token original já tem alta entropia. Não aplique essa mesma estratégia a senhas escolhidas por pessoas: elas têm padrões previsíveis e exigem Argon2id, scrypt ou bcrypt.

Use uma função de composição de URL em vez de concatenar parâmetros complexos:

from urllib.parse import urlencode


def montar_link(base_url: str, token: str) -> str:
    parametros = urlencode({"token": token})
    return f"{base_url.rstrip('/')}/redefinir-senha?{parametros}"

Mesmo com token_urlsafe, a construção explícita evita problemas quando novos parâmetros forem adicionados.

O risco principal não está apenas na sintaxe. Tokens em URLs podem aparecer em:

  • access logs do servidor;
  • histórico e sincronização do navegador;
  • captura de tela enviada ao suporte;
  • cabeçalho Referer ao abrir um link externo;
  • ferramentas de analytics que coletam a URL completa;
  • sistemas de inspeção de e-mail.

Configure redirecionamento e política de referência com cuidado, remova o token da URL após a validação quando o framework permitir e nunca envie eventos de analytics contendo a query string sensível. O link deve expirar rápido e funcionar uma única vez.

compare_digest e comparações sensíveis

Uma comparação comum usa ==:

if token_recebido == token_esperado:
    autorizar()

Para valores sensíveis mantidos em memória, secrets.compare_digest() reduz variações de tempo relacionadas ao ponto da primeira diferença:

from secrets import compare_digest


def assinatura_valida(recebida: str, esperada: str) -> bool:
    return compare_digest(recebida, esperada)

Essa função é útil na validação de assinaturas, segredos compartilhados e webhooks. Compare valores do mesmo tipo: str com str ou bytes com bytes.

compare_digest não corrige um protocolo inseguro. Para webhooks, verifique a especificação do provedor: normalmente você precisa calcular um HMAC sobre o corpo bruto da requisição com uma chave secreta e então comparar a assinatura. Também valide timestamp e rejeite repetição quando o protocolo oferecer esses campos.

No fluxo com banco mostrado antes, a busca pelo hash já determina se existe registro. Ainda assim, bibliotecas e frameworks podem usar comparação constante internamente em outras etapas. Não invente seu próprio algoritmo criptográfico.

Tokens não são variáveis de ambiente

Dois usos da palavra “segredo” costumam ser misturados:

  • secrets gera valores aleatórios em tempo de execução;
  • variáveis de ambiente e cofres entregam configuração secreta à aplicação.

Uma chave fixa de API não deve ser recriada a cada inicialização. Ela precisa vir de um gerenciador de segredos, variável de ambiente ou mecanismo equivalente:

import os

CHAVE_PROVEDOR = os.environ["CHAVE_PROVEDOR"]

Não coloque a chave no código, no Git ou em um valor padrão. O tutorial sobre variáveis de ambiente e python-dotenv mostra como separar desenvolvimento e produção sem vazar credenciais.

Por outro lado, um token de recuperação deve ser novo para cada solicitação. Gerá-lo uma vez e guardá-lo no .env destruiria o modelo de uso único.

Erros comuns ao gerar tokens e códigos

Usar timestamp ou dados pessoais

Isto é previsível:

# Não faça isso
reset_token = f"{usuario_id}-{int(datetime.now().timestamp())}"

Também não use CPF, telefone, data de nascimento, CEP, número do pedido ou hash simples desses campos. Um hash esconde a aparência, mas não cria aleatoriedade quando a entrada tem poucas possibilidades.

Cortar um token até ele ficar curto demais

# Reduz drasticamente o espaço de busca
codigo = secrets.token_urlsafe(32)[:6]

Se a interface exige seis caracteres, trate o resultado como código de baixa entropia: expiração de poucos minutos, limite rigoroso de tentativas, associação a uma operação e invalidação após sucesso. Não transforme um token forte em código curto sem rever todo o controle de abuso.

Usar o padrão global quando o tamanho importa

As funções aceitam nbytes opcional, mas o tamanho padrão é um detalhe sujeito a mudança. Se o protocolo ou sua análise de risco exige uma quantidade definida, escreva-a explicitamente:

TOKEN_BYTES = 32
token = secrets.token_urlsafe(TOKEN_BYTES)

Registrar o token para “facilitar o debug”

Evite:

logger.info("Token de reset criado: %s", token)

Registre um identificador interno, o tipo do evento, o usuário de forma adequada à política de privacidade e o horário. O artigo sobre logging em Python explica níveis e logs estruturados; a regra adicional aqui é não transformar o log em banco de credenciais.

Confundir geração aleatória com hash de senha

secrets pode gerar um salt ou token, mas a aplicação não deve montar um armazenamento de senha juntando funções da stdlib sem conhecimento especializado. Use uma biblioteca mantida, parâmetros revisados e atualização gradual do hash após login quando a política mudar. Consulte também as práticas de segurança em aplicações Python.

Como testar sem tornar a produção previsível

Um teste não deve depender do valor aleatório exato. Valide propriedades e comportamento:

import secrets


def gerar_token_reset() -> str:
    return secrets.token_urlsafe(32)


def test_token_tem_tamanho_e_varia() -> None:
    primeiro = gerar_token_reset()
    segundo = gerar_token_reset()

    assert len(primeiro) >= 40
    assert primeiro != segundo
    assert primeiro.isascii()

A chance de colisão com 32 bytes é desprezível para esse teste, mas o ponto principal é não usar random.seed() para controlar secrets.

Para testar um serviço que envia e-mail, injete dependências: relógio, repositório e função de envio. Você pode capturar o token no adaptador falso sem enfraquecer a geração em produção.

from collections.abc import Callable


def solicitar_reset(
    usuario_id: int,
    salvar: Callable[[int, str], None],
    enviar: Callable[[str], None],
) -> None:
    token = secrets.token_urlsafe(32)
    salvar(usuario_id, hash_token(token))
    enviar(token)

Nos testes, enviar armazena o argumento em uma lista. Em produção, ele chama o provedor de e-mail. O guia de testes unitários com pytest aprofunda fixtures, mocks e isolamento.

Checklist para usar secrets em produção

Antes de publicar um fluxo sensível, confirme:

  • secrets, e não random, gera o valor;
  • o tamanho foi escolhido explicitamente;
  • tokens longos usam pelo menos uma margem compatível com o risco, como 32 bytes em sistemas novos;
  • o banco guarda hash, validade, propósito e estado de uso;
  • o token é invalidado de forma atômica;
  • existe expiração curta e limpeza de registros antigos;
  • tentativas são limitadas e monitoradas;
  • respostas não permitem enumerar contas;
  • token, senha e chave de API não aparecem em logs ou analytics;
  • o tráfego usa HTTPS;
  • a senha final usa Argon2id, scrypt ou bcrypt, não SHA-256 puro;
  • testes cobrem sucesso, expiração, segundo uso e token inválido;
  • mensagens de erro não revelam detalhes desnecessários;
  • chaves fixas vêm de um cofre ou ambiente, não de secrets a cada boot.

Perguntas frequentes

Qual a diferença entre secrets e random em Python?

random produz números pseudoaleatórios determinísticos para simulações, jogos e amostragem. secrets usa a fonte segura do sistema operacional e é a opção correta para tokens, senhas temporárias, convites e valores que protegem uma ação. Não troque um pelo outro apenas porque as duas APIs parecem gerar texto ou números aleatórios.

Quantos bytes devo usar em um token de recuperação de senha?

Para aplicações novas, 32 bytes aleatórios oferecem uma margem confortável e geram aproximadamente 43 caracteres com token_urlsafe. O tamanho não resolve tudo: o token também precisa expirar, ser de uso único, ficar fora dos logs, trafegar por HTTPS e ter tentativas limitadas.

token_urlsafe pode ser colocado diretamente em uma URL?

Sim. Ele usa uma codificação apropriada para URLs. Ainda assim, monte parâmetros com urllib.parse.urlencode ou recursos do framework e proteja a URL contra coleta por logs, analytics, histórico e cabeçalho Referer. A segurança vem do ciclo de vida completo, não apenas do alfabeto.

Devo salvar o token de recuperação em texto puro no banco?

Evite. Envie o original ao usuário e armazene um hash SHA-256 do token aleatório, junto com validade, finalidade e estado de uso. Ao receber o link, calcule o mesmo hash e procure o registro. Essa técnica é adequada para tokens fortes; não use SHA-256 puro para senhas humanas.

secrets serve para armazenar ou criptografar senhas de usuários?

Não. O módulo gera aleatoriedade, mas não implementa armazenamento de senha. Use uma biblioteca confiável com Argon2id, scrypt ou bcrypt. Criptografia reversível também não é o padrão para senhas: o servidor normalmente deve verificar um hash específico para esse fim, sem recuperar a senha original.

Conclusão

secrets elimina uma decisão perigosa que aparece em muitos projetos: como gerar um valor que não pode ser previsto. Para links e credenciais temporárias, comece com secrets.token_urlsafe(32); use token_hex quando houver exigência de formato; use choice com um alfabeto explícito para senhas e códigos; e aplique compare_digest em comparações sensíveis.

O módulo, porém, é apenas a fonte de aleatoriedade. Um recurso seguro também precisa de expiração, uso único, armazenamento por hash, transação, limite de tentativas, HTTPS e logs sem credenciais. Senhas de usuários exigem algoritmos próprios, enquanto chaves fixas pertencem a um cofre ou variável de ambiente.

Dominar essa separação é valioso em backends, automações, APIs e nas vagas de Python: você deixa de tratar “um texto difícil de adivinhar” como detalhe e passa a modelar todo o ciclo de vida do segredo.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português