O que são JSON Web Tokens (JWT)? Por que as APIs os usam?

0
38


Shutterstock.com/TierneyMJ

O padrão JSON Web Tokens (JWT) descreve um método compacto para transferências de dados verificáveis. Cada token contém uma assinatura que permite ao remetente verificar a integridade da mensagem.

Neste artigo, você aprenderá o que a estrutura JWT inclui e como pode gerar seus próprios tokens. Os JWTs são uma maneira popular de proteger APIs e autenticar sessões de usuários porque são simples e independentes.

Como funcionam os JWTs

Uma das tarefas mais comuns em qualquer API é validar se os usuários são quem dizem ser. A autenticação geralmente é tratada fazendo com que o cliente inclua uma chave de API com as solicitações que envia ao servidor. A chave contém informações incorporadas que identificam o usuário. Isso ainda deixa uma grande questão: como o servidor pode validar que emitiu a chave em primeiro lugar?

Os JWTs resolvem convenientemente esse problema usando um segredo para assinar cada token. O servidor pode verificar a validade de um token tentando recalcular a assinatura enviada usando seu segredo privado. Qualquer manipulação fará com que a verificação falhe.

O formato JWT

Os JWTs são formados por três componentes diferentes:

  • Cabeçalho – Isso inclui metadados sobre o próprio token, como o algoritmo de assinatura que foi usado.
  • Carga útil – A carga útil do token pode ser qualquer dado arbitrário relevante para o seu sistema. Pode incluir o ID do usuário e uma lista de recursos com os quais eles podem interagir.
  • Assinatura – A assinatura permite validar a integridade do token no futuro. Ele é criado assinando o cabeçalho e a carga usando um valor secreto conhecido apenas pelo servidor.

Esses três componentes são pontilhados para produzir o JWT:

header.payload.signature

Cada peça é codificada usando Base-64. O token completo é uma string de texto que pode ser facilmente consumida em ambientes de programação e enviada com solicitações HTTP.

Criar um JWT

As etapas para criar um JWT podem ser implementadas em todas as linguagens de programação. Este exemplo usa PHP, mas o processo será semelhante em seu próprio sistema.

Comece criando o cabeçalho. Isso geralmente inclui dois campos, alg S typ:

  • alg – O algoritmo de hash que será usado para criar a assinatura. Geralmente é HMAC SHA256 (HS256).
  • typ – O tipo de token que está sendo gerado. Isso deve ser JWT.

Aqui está o JSON que define o cabeçalho:

{
    "alg": "HS256",
    "typ": "JWT"
}

O cabeçalho JSON deve ser codificado em Base64 abaixo:

$headerData = ["alg" => "HS256", "typ" => "JWT"];
$header = base64_encode(json_encode($headerData));

Em seguida, defina sua carga útil de token como outro objeto JSON. Isso é específico do aplicativo. O exemplo fornece detalhes da conta de usuário autenticada, bem como informações sobre o próprio token. exp, iatS nbf são campos que são usados ​​por convenção para expressar o tempo de expiração do token, eles são emitidos no momento e não são válidos antes do horário (inicial). A carga útil também deve ser codificada em Base64.

$payloadData = [
    "userId" => 1001,
    "userName" => "demo",
    "licensedFeatures" => ["todos", "calendar", "invoicing"],
    "exp" => (time() + 900),
    "iat" => time(),
    "nbf" => time()
];
$payload = base64_encode(json_encode($payloadData));

Tudo o que resta é criar a assinatura. Para produzir isso, ele primeiro combina o cabeçalho e a carga útil em uma única string separada por um . personagem:

$headerAndPayload = "$header.$payload";

Em seguida, você deve gerar um segredo exclusivo para usar como chave de assinatura. O segredo deve ser armazenado com segurança em seu servidor e nunca deve ser enviado aos clientes. Expor esse valor permitiria que qualquer pessoa criasse tokens válidos.

// PHP method to generate 32 random characters
$secret = bin2hex(openssl_random_pseudo_bytes(16));

Conclua o processo usando o segredo para assinar o cabeçalho combinado e a string de carga útil usando o algoritmo de hash indicado no cabeçalho. A assinatura de saída deve ser codificada em Base64 como os outros componentes.

$signature = base64_encode(hash_hmac("sha256", $headerAndPayload, $secret, true));

Agora você tem o cabeçalho, a carga útil e a assinatura como componentes de texto individuais. Junte-se a todos com . separadores para criar o JWT para enviar ao seu cliente:

$jwt = "$header.$payload.$signature";

Verificação de JWT de entrada

O aplicativo cliente pode determinar os recursos disponíveis para o usuário decodificando a carga útil do token. Aqui está um exemplo em JavaScript:

const tokenComponents = jwt.split(".");
const payload = token[1];
const payloadDecoded = JSON.parse(atob(payload));
 
// ["todos", "calendar", "invoicing"]
console.log(payloadDecoded.licensedFeatures);

Um invasor pode perceber que esses dados são texto simples e parecem fáceis de modificar. Eles podem tentar convencer o servidor de que têm uma função adicional alterando a carga útil do token em sua próxima solicitação:

// Create a new payload component
const modifiedPayload = btoa(JSON.stringify({
    ...payloadDecoded,
    licensedFeatures: ["todos", "calendar", "invoicing", "extraPremiumFeature"]
}));
 
// Stitch the JWT back together with the original header and signature
const newJwt = `${token[0]}.${modifiedPayload}.${token[2]}`

A resposta de como o servidor se defende contra esses ataques está no método usado para gerar a assinatura. O valor da assinatura leva em consideração o cabeçalho e a carga do token. Modificar a carga útil, como neste exemplo, significa que a assinatura não é mais válida.

O código do lado do servidor verifica os JWTs recebidos recalculando suas assinaturas. O token foi adulterado se a assinatura enviada pelo cliente não corresponder ao valor gerado no servidor.

$tamperedToken = $_POST["apiKey"];
list($header, $payload, $signature) = $tamperedToken;
 
// Determine the signature this token *should* have 
// when the server's secret is used as the key
$expectedSignature = hash_hmac("sha256", "$header.$payload", $secret, true);
 
// The token has been tampered with because its 
// signature is incorrect for the data it includes
if ($signature !== $expectedSignature) {
    http_response_code(403);
}
// The signatures match - we generated this 
// token and can safely trust its data
else {
    $user = fetchUserById($payload["userId"]);
}

É impossível para um invasor gerar um token válido sem acesso ao segredo do servidor. Isso também significa que a perda acidental ou rotação deliberada do segredo invalidará imediatamente todos os tokens emitidos anteriormente.

Em uma situação do mundo real, seu código de autenticação também deve inspecionar a expiração e os carimbos de data/hora “não antes” na carga do token. Eles são usados ​​para determinar se a sessão do usuário ainda é válida.

Quando usar o JWT

Os JWTs são frequentemente usados ​​para autenticação de API porque são simples de implementar no servidor, fáceis de consumir no cliente e simples de transmitir através dos limites da rede. Apesar de sua simplicidade, eles têm boa segurança porque cada token é assinado com a chave secreta do servidor.

Os JWTs são um mecanismo sem estado, portanto, você não precisa registrar informações sobre tokens emitidos em seu servidor. Você pode obter informações sobre o cliente apresentando um JWT a partir da carga útil do token, em vez de precisar fazer uma pesquisa no banco de dados. Essas informações podem ser confiáveis ​​com segurança depois de verificar a assinatura do token.

Usar o JWT é uma boa opção sempre que você precisar trocar informações entre duas partes sem risco de adulteração. No entanto, existem pontos fracos a serem observados: todo o sistema será comprometido se a chave secreta do seu servidor vazar ou se o código de verificação de assinatura contiver um erro. Por esse motivo, muitos desenvolvedores optam por usar uma biblioteca de código aberto para implementar a geração e validação de JWT. As opções estão disponíveis para todas as linguagens de programação populares. Eles eliminam o risco de supervisão quando você mesmo verifica os tokens.

Resumo

O padrão JWT é um formato de troca de dados que inclui verificação de integridade integrada. JWTs são comumente usados ​​para proteger interações entre servidores de API e aplicativos cliente. O servidor pode confiar nos tokens de entrada se puder reproduzir suas assinaturas. Isso permite que as ações sejam executadas com segurança usando as informações obtidas da carga útil do token.

Os JWTs são convenientes, mas têm algumas desvantagens. A representação textual codificada em Base64 de um JWT pode ficar grande rapidamente se você tiver mais de um punhado de campos de carga útil. Isso pode se tornar uma sobrecarga inaceitável quando seu cliente precisar enviar o JWT com cada solicitação.

A apatridia dos JWTs também é outra desvantagem potencial: uma vez emitidos, os tokens são imutáveis ​​e devem ser usados ​​como estão até expirarem. Os clientes que usam cargas JWT para determinar as permissões de um usuário ou recursos licenciados precisarão obter um novo token do back-end sempre que seus mapeamentos forem alterados.