Você está na página 1de 72

Tópicos Comece a aprender Pesquise mais de 50.

000 cursos, even… O que há de novo

6 Tokens independentes e JWTs


Este capítulocapas

Dimensionamento da autenticação baseada em token com armazenamento


criptografado do lado do cliente
Protegendo tokens com MACs e criptografia autenticada
Gerando tokens da Web JSON padrão
Manipulando a revogação do token quando todo o estado está no cliente

Você mudou a API do Natter para usar o armazenamento de token do banco de


dados com tokens armazenados no Web Storage. A boa notícia é que Natter está
realmente decolando. Sua base de usuários cresceu para milhões de usuários re-
gulares. A má notícia é que o banco de dados de tokens está lutando para lidar
com esse nível de tráfego. Você avaliou diferentes back-ends de banco de dados,
mas ouviu falar sobre tokens sem estado que permitiriam que você se livrasse to-
talmente do banco de dados. Sem um banco de dados atrasando você, Natter será
capaz de escalar conforme a base de usuários continua a crescer. Neste capítulo,
você implementará tokens autossuficientes com segurança e examinará algumas
das compensações de segurança em comparação com tokens baseados em banco
de dados. Você também aprenderá sobre o padrão JSON Web Token (JWT), que é o
formato de token mais usado atualmente.

DEFINIÇÃO JSON Web Tokens (JWTs, pronuncia-se “jots”) são um formato pa-
drão para tokens de segurança independentes. Um JWT consiste em um conjunto
de declarações sobre um usuário representado como um objeto JSON, juntamente
com um cabeçalho que descreve o formato do token. Os JWTs são protegidos crip-
tograficamente contra adulteração e também podem ser criptografados.

6.1 Armazenando o estado do token no cliente

oA ideia por trás dos tokens sem estado é simples. Em vez de armazenar o estado
do token no banco de dados, você pode codificar esse estado diretamente no ID do
token e enviá-lo ao cliente. Por exemplo, você pode serializar os campos de token
em um objeto JSON, que você codifica em Base64url para criar uma string que
pode ser usada como o ID do token. Quando o token é apresentado de volta à API,
você simplesmente decodifica o token e analisa o JSON para recuperar os atribu-
tos da sessão.

A Listagem 6.1 mostra um armazenamento de token JSON que faz exatamente


isso. Ele usa teclas curtas para atributos, como sub para o assunto (nome de usuá-
rio) e exp para o tempo de expiração, para economizar espaço. Esses são atribu-
tos JWT padrão, como você aprenderá na seção 6.2.1. Deixe o revoke métodoem
branco por enquanto, você voltará a isso em breve na seção 6.5. Navegue até a
pasta src/main/java/com/manning/apisecurityinaction/token e crie um novo ar-
quivo JsonTokenStore.java em seu editor. Digite o conteúdo da listagem 6.1 e salve
o novo arquivo.

AVISO Este código não é seguro por conta própria porque os tokens JSON puros
podem ser alterados e falsificados. Você adicionará suporte para autenticação de
token na seção 6.1.1.

Listagem 6.1 O armazenamento de token JSON

pacote com.manning.apisecurityinaction.token;

import org.json.*;
import spark.Request;
import java.time.Instant;
importar java.util.*;
importar estático java.nio.charset.StandardCharsets.UTF_8;

public class JsonTokenStore implementa TokenStore {


@Sobrepor
public String create(Solicitação de solicitação, Token token) {
var json = new JSONObject();
json.put("sub", token.username); ❶
json.put("exp", token.expiry.getEpochSecond()); ❶
json.put("attrs", token.attributes); ❶

var jsonBytes = json.toString().getBytes(UTF_8); ❷


return Base64url.encode(jsonBytes); ❷
}

@Sobrepor
public Opcional<Token> read(Request request, String tokenId) {
tentar {
var decodificado = Base64url.decode(tokenId); ❸
var json = new JSONObject(new String(decoded, UTF_8)); ❸
var expiração = Instant.ofEpochSecond(json.getInt("exp")); ❸
var nome de usuário = json.getString("sub"); ❸
var attrs = json.getJSONObject("attrs"); ❸

var token = new Token(expiração, nome de usuário); ❸


for (var key : attrs.keySet()) { ❸
token.attributes.put(key, attrs.getString(key)); ❸
}

return Opcional.of(token);
} catch (JSONException e) {
return Opcional.vazio();
}
}

@Sobrepor
public void revoke(Solicitação de solicitação, String tokenId) {
// TODO ❹
}
}
❶ Converta os atributos do token em um objeto JSON.

❷ Codifique o objeto JSON com codificação Base64 segura para URL.

❸ Para ler o token, decodifique-o e analise o JSON para recuperar os atributos.

❹ Deixe o método de revogação em branco por enquanto.

6.1.1 Protegendo tokens JSON com HMAC

DoClaro, como está, este código é completamente inseguro. Qualquer pessoa pode
fazer login na API e editar o token codificado em seu navegador para alterar seu
nome de usuário ou outros atributos de segurança! Na verdade, eles podem sim-
plesmente criar um novo token sem nunca fazer login. Você pode corrigir isso
reutilizando o HmacTokenStore que você criou no capítulo 5, conforme mostrado
na figura 6.1. Ao anexar uma marca de autenticação calculada com uma chave se-
creta conhecida apenas pelo servidor da API, um invasor é impedido de criar um
token falso ou alterar um existente.

Para habilitar tokens protegidos por HMAC, abra Main.java em seu editor e altere
o código que constrói o DatabaseTokenStore em vez disso, crie um JsonTo-
kenStore :

TokenStore tokenStore = new JsonTokenStore(); ❶


tokenStore = new HmacTokenStore(tokenStore, macKey); ❷
var tokenController = new TokenController(tokenStore);

❶ Construa o JsonTokenStore.

❷ Embrulhe-o em um HmacTokenStore para garantir a autenticidade.

Você pode experimentá-lo para ver seu primeiro token sem estadodentroação:

$ curl -H 'Tipo de conteúdo: aplicativo/json' -u teste:senha \


-X POST https://localhost:4567/sessions
{"token":"eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNTU5NTgyMTI5LCJhdHRycyI6e319.
➥ INFgLC3cAhJ8DjzPgQfHBHvU_uItnFjt568mQ43V7YI"}

questionário

1. Qual das ameaças STRIDE faz o HmacTokenStore proteger contra? (Pode haver
mais de uma resposta correta.)
1. Falsificação
2. adulteração
3. Repúdio
4. Divulgação de informação
5. Negação de serviço
6. Elevação de privilégio

A resposta está no final do capítulo.


Figura 6.1 Uma tag HMAC é calculada sobre as declarações JSON codificadas
usando uma chave secreta. A tag HMAC é codificada em formato Base64 seguro
para URL e anexada ao token, usando um ponto como separador. Como um ponto
não é um caractere válido na codificação Base64, você pode usá-lo para localizar a
tag posteriormente.

6.2 Tokens Web JSON

Autenticadoos tokens do lado do cliente se tornaram muito populares nos últimos


anos, em parte graças à padronização dos JSON Web Tokens em 2015. Os JWTs são
muito semelhantes aos tokens JSON que você acabou de produzir, mas têm muito
mais recursos:

Um formato de cabeçalho padrão que contém metadados sobre o JWT, como


qual MAC ou algoritmo de criptografia foi usado.
Um conjunto de declarações padrão que podem ser usadas no conteúdo JSON
do JWT, com significados definidos, como para exp indicar o tempo de expira-
ção e sub para o assunto, assim como você vem usando.
Uma ampla variedade de algoritmos para autenticação e criptografia, bem
como assinaturas digitais e criptografia de chave pública, abordadas posterior-
mente neste livro.

Como os JWTs são padronizados, eles podem ser usados ​com muitas ferramentas,
bibliotecas e serviços existentes. Atualmente, existem bibliotecas JWT para a mai-
oria das linguagens de programação, e muitas estruturas de API incluem suporte
integrado para JWTs, tornando-as um formato atraente de usar. O OpenID Con-
nectO protocolo de autenticação (OIDC) discutido no capítulo 7 usa JWTs como um
formato padrão para transmitir reivindicações de identidade sobre usuários entre
sistemas.

O zoológico de padrões JWT

Embora o próprio JWT seja apenas uma especificação (


https://tools.ietf.org/html/rfc7519 ), ele se baseia em uma coleção de padrões co-
nhecidos coletivamente comoAssinatura e criptografia de objetos JSON (JOSE). O
próprio JOSE consiste em vários padrões relacionados:

Assinatura da Web JSON(JWS, https://tools.ietf.org/html/rfc7515 ) define como


os objetos JSON podem ser autenticados com HMAC e assinaturas digitais.
Criptografia da Web JSON(JWE, https://tools.ietf.org/html/rfc7516 ) define como
criptografar objetos JSON.
Chave da Web JSON(JWK, https://tools.ietf.org/html/rfc7517 ) descreve um for-
mato padrão para chaves criptográficas e metadados relacionados em JSON.
Algoritmos Web JSON(JWA, https://tools.ietf.org/html/rfc7518 ) especifica os al-
goritmos de assinatura e criptografia a serem usados.

JOSE foi ampliado ao longo dos anos por novas especificações para adicionar no-
vos algoritmos e opções. É comum usar JWT para se referir a toda a coleção de es-
pecificações, embora existam usos de JOSE além dos JWTs.

Um JWT básico autenticado é quase exatamente como os tokens JSON autentica-


dos por HMAC que você produziu na seção 6.1.1, mas com um cabeçalho JSON adi-
cional que indica o algoritmo e outros detalhes de como o JWT foi produzido, con-
forme mostrado na figura 6.2. O formato codificado em Base64url usado para
JWTs é conhecido como JWS Compact Serialization. O JWS também define outro
formato, mas a serialização compacta é a mais utilizada para tokens de API.

Figura 6.2 A JWS Compact Serialization consiste em três partes codificadas em


Base64 seguras para URL, separadas por pontos. Primeiro vem o cabeçalho, de-
pois a carga útil ou declarações e, finalmente, a marca ou assinatura de autentica-
ção. Os valores neste diagrama foram reduzidos para fins de exibição.

A flexibilidade do JWT também é sua maior fraqueza, já que vários ataques foram
encontrados no passado que exploram essa flexibilidade. JOSE é um projeto de kit
de peças, permitindo que os desenvolvedores escolham entre uma ampla varie-
dade de algoritmos, e nem todas as combinações de recursos são seguras. Por
exemplo, em 2015, o pesquisador de segurança Tim McClean descobriu vulnerabi-
lidades em muitas bibliotecas JWT ( http://mng.bz/awKz ) nas quais um invasor
poderia alterar o cabeçalho do algoritmo em um JWT para influenciar como o
destinatário validava o token. Foi até possível mudar para o valor none , o que
instruiu a biblioteca JWT a não validar a assinatura de jeito nenhum! Esses tipos
de falhas de segurança levaram algumas pessoas a argumentar que os JWTs são
inerentemente inseguros devido à facilidade com que podem ser mal utilizados e
à baixa segurança de alguns dos algoritmos padrão.

PASETO: Uma alternativa ao JOSE

A natureza propensa a erros dos padrões levou ao desenvolvimento de formatos


alternativos destinados a serem usados ​para muitas das mesmas finalidades do
JOSE, mas com menos detalhes complicados de implementação e oportunidades
de uso indevido. Um exemplo é o PASETO( https://paseto.io ), que fornece cripto-
grafia autenticada simétrica ou objetos JSON assinados por chave pública, abran-
gendo muitos dos mesmos casos de uso dos padrões JOSE e JWT. A principal dife-
rença do JOSE é que o PASETO permite apenas que um desenvolvedor especifique
uma versão de formato. Cada versão usa um conjunto fixo de algoritmos cripto-
gráficos em vez de permitir uma ampla escolha de algoritmos: a versão 1 requer
algoritmos amplamente implementados, como AES e RSA, enquanto a versão 2 re-
quer algoritmos mais modernos, mas menos amplamente implementados, como
Ed25519. Isso dá ao invasor muito menos espaço para confundir a implementação
e os algoritmos escolhidos têm poucos pontos fracos conhecidos.

Vou deixar você tirar suas próprias conclusões sobre o uso de JWTs. Neste capí-
tulo, você verá como implementar alguns dos recursos dos JWTs desde o início,
para poder decidir se a complexidade extra vale a pena. Existem muitos casos em
que os JWTs não podem ser evitados, então vou apontar as melhores práticas de
segurança e truques para que você possa usá-los com segurança.
6.2.1 As reivindicações JWT padrão

UmUma das partes mais úteis da especificação JWT é o conjunto padrão de pro-
priedades do objeto JSON definido para conter declarações sobre um assunto, co-
nhecido como conjunto de declarações. Você já viu duas declarações JWT padrão,
porque as usou na implementação do JsonTokenStore :

A exp reclamaçãoindica o tempo de expiração de um JWT no horário do UNIX,


que é o número de segundos desde a meia-noite de 1º de janeiro de 1970 no
UTC.
A sub reclamaçãoidentifica o sujeito do token: o usuário. Outras reivindicações
no token geralmente estão fazendo reivindicações sobre este assunto.

O JWT também define várias outras declarações, listadas na tabela 6.1. Para eco-
nomizar espaço, cada declaração é representada com uma propriedade de objeto
JSON de três letras.
Tabela 6.1 Reivindicações JWT padrão

Alegar Nome Propósito

iss Emissor Indica quem criou o JWT. Esta é uma única string e
frequentemente o URI do serviço de autenticação.

aud Público Indica para quem é o JWT. Uma matriz de strings que
identifica os destinatários pretendidos do JWT. Se
houver apenas um único valor, ele pode ser um valor
de string simples em vez de uma matriz. O destinatá-
rio de um JWT deve verificar se seu identificador
aparece na audiência; caso contrário, ele deve rejei-
tar o JWT. Normalmente, este é um conjunto de URIs
para APIs onde o token pode ser usado.

iat Emitido A hora do UNIX em que o JWT foi criado.


em

nbf Não O JWT deve ser rejeitado se usado antes desse


antes período.

exp Termo A hora do UNIX em que o JWT expira e deve ser rejei-
tado pelos destinatários.
sub Sujeito A identidade do sujeito do JWT. Uma linha. Normal-
mente, um nome de usuário ou outro identificador
exclusivo.

jti ID do Um ID exclusivo para o JWT, que pode ser usado para


JWT detectar replay.

Dessas reivindicações, apenas as reivindicações do emissor, emitidas e sujeitas ex-


pressam uma declaração positiva. Todos os campos restantes descrevem restri-
ções sobre como o token pode ser usado em vez de fazer uma reivindicação. Essas
restrições destinam-se a impedir certos tipos de ataques contra tokens de segu-
rança, como ataques de repetiçãoem que um token enviado por uma parte ge-
nuína a um serviço para obter acesso é capturado por um invasor e posterior-
mente repetido para que o invasor possa obter acesso. Definir um tempo de expi-
ração curto pode reduzir a janela de oportunidade para esses ataques, mas não
eliminá-los. O JWT ID pode ser usado para adicionar um valor exclusivo a um
JWT, que o destinatário pode lembrar até que o token expire para evitar que o
mesmo token seja repetido. Os ataques de repetição são amplamente evitados
pelo uso de TLS, mas podem ser importantes se você precisar enviar um token
por um canal inseguro ou como parte de um protocolo de autenticação.

DEFINIÇÃO Um ataque de repetiçãoocorre quando um invasor captura um to-


ken enviado por uma parte legítima e, posteriormente, o reproduz a seu próprio
pedido.

As declarações do emissor e do público podem ser usadas para evitar uma forma
diferente de ataque de repetição, no qual o token capturado é repetido em uma
API diferente do destinatário originalmente pretendido. Se o invasor repetir o to-
ken de volta ao emissor original, isso é conhecido como ataque de reflexão, e pode
ser usado para derrotar alguns tipos de protocolos de autenticação se o destinatá-
rio puder ser induzido a aceitar suas próprias mensagens de autenticação. Ao ve-
rificar se seu servidor de API está na lista de público e se o token foi emitido por
uma parte confiável, esses ataques podem serderrotado.

6.2.2 O cabeçalho JOSE

A maioriada flexibilidade dos padrões JOSE e JWT está concentrada no cabeçalho,


que é um objeto JSON adicional incluído na tag de autenticação e contém metada-
dos sobre o JWT. Por exemplo, o cabeçalho a seguir indica que o token é assinado
com HMAC-SHA-256 usando uma chave com o ID de chave fornecido:

{
"alg": "HS256", ❶
"kid": "hmac-key-1" ❷
}

❶ O algoritmo
❷ O identificador de chave

Embora aparentemente inócuo, o cabeçalho JOSE é um dos aspectos mais propen-


sos a erros das especificações, e é por isso que o código que você escreveu até
agora não gera um cabeçalho, e geralmente recomendo que eles sejam removidos
quando possível para criar (fora do padrão ) JWTs sem comando. Isso pode ser
feito removendo a seção de cabeçalho produzida por uma biblioteca JWT padrão
antes de enviá-la e, em seguida, recriá-la novamente antes de validar um JWT re-
cebido. Muitos dos cabeçalhos padrão definidos pelo JOSE podem abrir sua API
para ataques se você não tomar cuidado, conforme descrito nesta seção.

DEFINIÇÃO Um JWT sem cabeçaé um JWT com o cabeçalho removido. O desti-


natário recria o cabeçalho dos valores esperados. Para casos de uso simples em
que você controla o remetente e o destinatário, isso pode reduzir o tamanho e a
superfície de ataque do uso de JWTs, mas os JWTs resultantes não são padrão.
Onde JWTs sem cabeçalho não podem ser usados, você deve validar rigorosa-
mente todos os valores de cabeçalho.

Os tokens produzidos na seção 6.1.1 são efetivamente JWTs headless e adicionar


um cabeçalho JOSE a eles (e incluí-lo no cálculo do HMAC) os tornaria compatíveis
com os padrões. De agora em diante, você usará uma biblioteca JWT real, em vez
de escrever a sua própria.
O CABEÇALHO DO ALGORITMO

o alg cabeçalhoidentifica o algoritmo criptográfico JWS ou JWE que foi usado


para autenticar ou criptografar o conteúdo. Este também é o único valor de cabe-
çalho obrigatório. O objetivo deste cabeçalho é permitir agilidade criptográfica,
permitindo que uma API altere o algoritmo que usa enquanto ainda processa to-
kens emitidos usando o algoritmo antigo.

DEFINIÇÃO Agilidade criptográficaé a capacidade de alterar o algoritmo usado


para proteger mensagens ou tokens caso pontos fracos sejam descobertos em um
algoritmo ou uma alternativa com melhor desempenho seja necessária.

Embora seja uma boa ideia, o design do JOSE não é o ideal porque o destinatário
deve confiar no remetente para dizer qual algoritmo usar para autenticar a men-
sagem. Isso viola o princípio de que você nunca deve confiar em uma declaração
que não tenha autenticado e, no entanto, não pode autenticar o JWT até que tenha
processado essa declaração! Essa fraqueza foi o que permitiu a Tim McClean con-
fundir as bibliotecas JWT alterando o alg cabeçalho.

Uma solução melhor é armazenar o algoritmo como metadados associados a uma


chave no servidor. Você pode alterar o algoritmo ao alterar a chave, uma metodo-
logia que chamo de agilidade criptográfica orientada por chave. Isso é muito mais
seguro do que gravar o algoritmo na mensagem, porque um invasor não tem ca-
pacidade de alterar as chaves armazenadas em seu servidor. A Chave da Web
JSON (JWK) permite que um algoritmo seja associado a uma chave, conforme
mostrado na Listagem 6.2, usando o alg atributo. JOSE define nomes padrão para
muitos algoritmos de autenticação e criptografia e o nome padrão para HMAC-
SHA256 que você usará neste exemplo é HS256 . Uma chave secreta usada para
HMAC ou AES é conhecida como chave de octeto no JWK, pois a chave é apenas
uma sequência de bytes aleatórios e octeto é uma palavra alternativa para byte. O
tipo de chave é indicado pelo kty atributoem um JWK, com o valor oct usado
para chaves de octeto.

DEFINIÇÃO Em agilidade criptográfica baseada em chave, o algoritmo usado


para autenticar um token é armazenado como metadados com a chave no servi-
dor, e não como um cabeçalho no token. Para alterar o algoritmo, você instala
uma nova chave. Isso evita que um invasor induza o servidor a usar um algo-
ritmo incompatível.

Listagem 6.2 Um JWK com declaração de algoritmo

{
"kty": "out",
"alg": "HS256", ❶
"k": "9ITYj4mt-TLYT2b_vnAyCVurks1r2uzCLw7sOxg-75g" ❷
}

❶ O algoritmo para o qual a chave deve ser usada

❷ Os bytes codificados em Base64 da própria chave


A especificação JWE também inclui um enc cabeçalhoque especifica a cifra usada
para criptografar o corpo JSON. Esse cabeçalho é menos propenso a erros do que
o alg cabeçalho, mas você ainda deve validar se ele contém um valor sensato.
JWTs criptografados são discutidos emseção 6.3.3.

ESPECIFICANDO A CHAVE NO CABEÇALHO

Parapermitem que as implementações alterem periodicamente a chave que usam


para autenticar JWTs, em um processo conhecido como rotação de chave, as espe-
cificações do JOSE incluem várias maneiras de indicar qual tecla foi usada. Isso
permite que o destinatário encontre rapidamente a chave certa para verificar o
token, sem ter que tentar uma chave por vez. As especificações do JOSE incluem
uma maneira segura de fazer isso (o kid cabeçalho) e duas alternativas potenci-
almente perigosas listadas na tabela 6.2.

DEFINIÇÃO A rotação de chaves é o processode alterar periodicamente as cha-


ves usadas para proteger mensagens e tokens. Alterar a chave regularmente ga-
rante que os limites de uso de uma chave nunca sejam atingidos e, se alguma
chave for comprometida, ela será substituída em breve, limitando o tempo em
que o dano pode ser causado.
Tabela 6.2 Indicando a chave em um cabeçalho JOSE

Cabeça- Conteúdo Seguro? Comentários


lho

kid Um ID de Sim Como o ID da chave é apenas um identifica-


chave dor de string, ele pode ser pesquisado com
segurança em um conjunto de chaves do
lado do servidor.

jwk A chave Não Confiar no remetente para lhe fornecer a


completa chave para verificar uma mensagem perde
todas as propriedades de segurança.

jku Uma URL Não A intenção desse cabeçalho é que o destina-


para recu- tário possa recuperar a chave de um end-
perar a point HTTPS, em vez de incluí-la diretamente
chave na mensagem, para economizar espaço. Infe-
completa lizmente, isso tem todos os problemas do
jwk cabeçalho, mas também abre o destina-
tário para ataques SSRF.

DEFINIÇÃO Uma falsificação de solicitação do lado do servidor(SSRF) ocorre


quando um invasor pode fazer com que um servidor faça solicitações de rede de
saída sob o controle do invasor. Como o servidor está em uma rede confiável atrás
de um firewall, isso permite que o invasor investigue e potencialmente ataque
máquinas na rede interna que, de outra forma, não poderiam acessar. Você
aprenderá mais sobre ataques SSRF e como evitá-los no capítulo 10.

Também há cabeçalhos para especificar a chave como um certificado X.509


(usado em TLS). Analisar e validar certificados X.509 é muito complexo, então
você deve evitaressescabeçalhos.

6.2.3 Gerando JWTs padrão

Agoraque você viu a ideia básica de como um JWT é construído, passará a usar
uma biblioteca JWT real para gerar JWTs no restante do capítulo. É sempre me-
lhor usar uma biblioteca bem testada para segurança quando houver uma dispo-
nível. Existem muitas bibliotecas JWT e JOSE para a maioria das linguagens de
programação, e o site https://jwt.io mantém uma lista. Você deve verificar se a bi-
blioteca é mantida ativamente e se os desenvolvedores estão cientes das vulnera-
bilidades históricas do JWT, como as mencionadas neste capítulo. Para este capí-
tulo, você pode usar o Nimbus JOSE + JWT em https://connect2id
.com/products/nimbus-jose-jwt, que é uma biblioteca Java JOSE de software livre
bem mantida (licenciada pelo Apache 2.0). Abra o arquivo pom.xml na pasta raiz
do projeto Natter e adicione a seguinte dependência à seção de dependências
para carregar a biblioteca Nimbus:
<dependência>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.19</version>
</dependência>

A Listagem 6.3 mostra como usar a biblioteca para gerar um JWT assinado. O có-
digo é genérico e pode ser usado com qualquer algoritmo JWS, mas por enquanto
você usará o HS256 algoritmo, que usa HMAC-SHA-256, assim como o existen-
te HmacTokenStore . A biblioteca Nimbus requer um JWSSigner objetopara ge-
ração de assinaturas, e um JWSVerifier por comprová-los. Esses objetos geral-
mente podem ser usados ​com vários algoritmos, então você também deve passar
o algoritmo específico para usar como um JWSAlgorithm objeto separado. Final-
mente, você também deve passar um valor para usar como público para os JWTs
gerados. Geralmente, esse deve ser o URI base do servidor da API, como https: //
localhost:4567. Ao definir e verificar a declaração de público, você garante que
um JWT não possa ser usado para acessar uma API diferente, mesmo que eles
usem a mesma chave criptográfica. Para produzir o JWT, você primeiro cria o
conjunto de declarações, defina a sub declaraçãoao nome de usuário, a exp rei-
vindicaçãoao tempo de expiração do token e a aud reivindicaçãoao valor de pú-
blico obtido do construtor. Em seguida, você pode definir quaisquer outros atribu-
tos do token como uma declaração personalizada, que se tornará um objeto JSON
aninhado no conjunto de declarações. Para assinar o JWT, você define o algoritmo
correto no cabeçalho e usa o JWSSigner objetopara calcular a assinatura. o
serialize() métodoentão produzirá a JWS Compact Serialization do JWT para
retornar como o identificador do token. Crie um novo arquivo chamado
SignedJwtTokenStore.java em
src/main/resources/com/manning/apisecurityinaction/token e copie o conteúdo da
listagem.

Listagem 6.3 Gerando um JWT assinado

pacote com.manning.apisecurityinaction.token;

importar javax.crypto.SecretKey;
import java.text.ParseException;
importar java.util.*;
import com.nimbusds.jose.*;
import com.nimbusds.jwt.*;
import spark.Request;

public class SignedJwtTokenStore implementa TokenStore {


assinante JWSSigner final privado; ❶
verificador JWSVerifier final privado; ❶
algoritmo JWSAlgorithm final privado; ❶
Audiência de String final privada; ❶

public SignedJwtTokenStore(assinante JWSSigner, ❶


verificador JWSVerifier, algoritmo JWSAlgorithm, ❶
audiência String) { ❶
this.signer = signer; ❶
this.verifier = verificador; ❶
this.algorithm = algoritmo; ❶
this.audience = público; ❶
}

@Sobrepor
public String create(Solicitação de solicitação, Token token) {
var ClaimSet = new JWTClaimsSet.Builder() ❷
.subject(token.username) ❷
.audience(audience) ❷
.expirationTime(Date.from(token.expiry)) ❷
.claim("attrs", token.attributes) ❷
. construir(); ❷
var cabeçalho = new JWSHeader(JWSAlgorithm.HS256); ❸
var jwt = new SignedJWT(cabeçalho, ClaimSet); ❸
tentar {
jwt.sign(signatário); ❹
return jwt.serialize(); ❺
} catch (JOSEException e) {
lança nova RuntimeException(e);
}
}

@Sobrepor
public Opcional<Token> read(Request request, String tokenId) {
// FAÇAM
return Opcional.vazio();
}

@Sobrepor
public void revoke(Solicitação de solicitação, String tokenId) {
// FAÇAM
}
}

❶ Transmita o algoritmo, o público e os objetos de assinante e verificador.

❷ Crie o conjunto de declarações JWT com detalhes sobre o token.

❸ Especifique o algoritmo no cabeçalho e crie o JWT.

❹ Assine o JWT usando o objeto JWSSigner.

❺ Converta o JWT assinado na serialização compacta do JWS.

Para usar o novo armazenamento de token, abra o arquivo Main.java em seu edi-
tor e altere o código que constrói o JsonTokenStore e HmacTokenStore em vez
disso, construir um SignedJwtTokenStore . Você pode reutilizar o
mesmo macKey que você carregou para o HmacTokenStore , pois você está
usando o mesmo algoritmo para assinar os JWTs. O código deve se parecer com o
seguinte, usando o MACSigner e MACVerifier classes para assinatura e verifica-
ção usando HMAC:
algoritmo var = JWSAlgorithm.HS256; ❶
var signer = new MACSigner((SecretKey) macKey); ❶
var verificador = new MACVerifier((SecretKey) macKey); ❶
TokenStore tokenStore = new SignedJwtTokenStore( ❷
assinante, verificador, algoritmo, "https://localhost:4567"); ❷
var tokenController = new TokenController(tokenStore);

❶ Construa os objetos MACSigner e MACVerifier com a macKey.

❷ Passe o signatário, verificador, algoritmo e público para SignedJwtTokenStore.

Agora você pode reiniciar o servidor da API, criar um usuário de teste e fazer lo-
gin para ver o JWT criado:

$ curl -H 'Tipo de conteúdo: aplicativo/json' \


-d '{"nome de usuário":"teste","senha":"senha"}' \
https://localhost:4567/users
{"nome de usuário":"teste"}
$ curl -H 'Tipo de conteúdo: aplicativo/json' -u teste:senha \
-d '' https://localhost:4567/sessions
{"token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwiYXVkIjoiaHR0cH
➥ M6XC9cL2xvY2FsaG9zdDo0NTY3IiwiZXhwIjoxNTc3MDA3ODcyLCJhdHRycyI
➥ 6e319.nMxLeSG6pmrPOhRSNKF4v31eQZ3uxaPVyj-Ztf-vZQw"}
Você pode pegar este JWT e colá-lo no depurador em https://jwt.io para validá-lo e
ver o conteúdo do cabeçalho e declarações, conforme mostrado emfigura 6.3.

Figura 6.3 O JWT no depurador jwt.io. Os painéis à direita mostram o cabeçalho


decodificado e a carga útil e permitem que você cole sua chave para validar o
JWT. Nunca cole um JWT ou chave de um ambiente de produção em um site.

AVISO Embora o jwt.io seja uma ótima ferramenta de depuração, lembre-se de


que os JWTs são credenciais, portanto, você nunca deve postar JWTs de um ambi-
ente de produção em nenhum site.

6.2.4 Validando um JWT assinado

Paravalidar um JWT, você primeiro analisa o formato JWS Compact Serialization


e, em seguida, usa o JWSVerifier objetopara verificar a assinatura. o nim-
bo MACVerifier calculará a tag HMAC correta e a comparará com a tag anexada
ao JWT usando uma comparação de igualdade de tempo constante, assim como
você fez no HmacTokenStore . A biblioteca Nimbus também cuida das verifica-
ções básicas de segurança, como garantir que o cabeçalho do algoritmo seja com-
patível com o verificador (evitando os ataques de confusão de algoritmo discuti-
dos na seção 6.2) e que não haja cabeçalhos críticos não reconhecidos. Após a ve-
rificação da assinatura, você pode extrair o conjunto de declarações JWT e verifi-
car quaisquer restrições. Nesse caso, você só precisa verificar se o valor esperado
do público aparece na declaração de público e, em seguida, definir a expiração do
token na declaração de tempo de expiração do JWT. o TokenController garan-
tirá que o token não expirou. A Listagem 6.4 mostra a lógica completa de valida-
ção do JWT. Abra o arquivo SignedJwtTokenStore.java e substitua o read() méto-
docom o conteúdo da listagem.

Listagem 6.4 Validando um JWT assinado


@Sobrepor
public Opcional<Token> read(Request request, String tokenId) {
tentar {
var jwt = SignedJWT.parse(tokenId); ❶

if (!jwt.verify(verifier)) { ❶
throw new JOSEException("Assinatura inválida"); ❶
} ❶

var reivindicações = jwt.getJWTClaimsSet();


if (!claims.getAudience().contains(audience)) { ❷
throw new JOSEException("Público incorreto"); ❷
} ❷

var expiração = reivindicações.getExpirationTime().toInstant(); ❸


var subject = Claims.getSubject(); ❸
var token = new Token(expiração, assunto); ❸
var attrs = Claims.getJSONObjectClaim("attrs"); ❸
attrs.forEach((chave, valor) -> ❸
token.attributes.put(chave, (String) valor)); ❸

return Opcional.of(token);
} catch (ParseException | JOSEException e) {
return Opcional.vazio(); ❹
}
}
❶ Analise o JWT e verifique a assinatura HMAC usando o JWSVerifier.

❷ Rejeite o token se o público não contiver o URI de base de sua API.

❸ Extraia atributos de token das declarações JWT restantes.

❹ Se o token for inválido, retorne uma resposta de falha genérica.

Agora você pode reiniciar a API e usar o JWT para criar um novosocialespaço:

$ curl -H 'Tipo de conteúdo: aplicativo/json' \


-H 'Autorização: Portador eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN
➥ 0IiwiYXVkIjoiaHR0cHM6XC9cL2xvY2FsaG9zdDo0NTY3IiwiZXhwIjoxNTc
➥ 3MDEyMzA3LCJhdHRycyI6e319.JKJnoNdHEBzc8igkzV7CAYfDRJvE7oB2md
➥ 6qcNgc"}espaço de teste: ""'M' proprietário"
https://localhost:4567/spaces

{"nome":"espaço de teste","uri":"/espaços/1"}

questionário

2. Qual declaração JWT é usada para indicar o servidor de API para o qual um
JWT se destina?
1. iss
2. sub
3. iat
4. exp
5. ouvido
6. jti
3. Verdadeiro ou falso: o alg cabeçalho JWT (algoritmo) pode ser usado com se-
gurança para determinar qual algoritmo usar ao validar a assinatura.

As respostas estão no final do capítulo.

6.3 Criptografando atributos confidenciais

UMAO banco de dados em seu datacenter, protegido por firewalls e controles de


acesso físico, é um local relativamente seguro para armazenar dados de token, es-
pecialmente se você seguir os conselhos de proteção do último capítulo. Depois
que você sai de um banco de dados e começa a armazenar dados no cliente, esses
dados ficam muito mais vulneráveis ​à espionagem. Qualquer informação pessoal
sobre o usuário incluída no token, como nome, data de nascimento, cargo, local
de trabalho e assim por diante, pode estar em risco se o token vazar acidental-
mente pelo cliente ou for roubado por meio de um ataque de phishing ou XSS ex-
filtração. Alguns atributos também podem precisar ser mantidos em sigilo do pró-
prio usuário, como quaisquer atributos que revelem detalhes da implementação
da API. No capítulo 7, você também considerará aplicativos clientes de terceiros
que podem não ser confiáveis ​para saber detalhes sobre quem é o usuário.

A criptografia é um tópico complexo com muitas armadilhas potenciais, mas pode


ser usado com sucesso se você se ater a algoritmos bem estudados e seguir algu-
mas regras básicas. O objetivo da criptografia é garantir a confidencialidade de
uma mensagem, convertendo-a em uma forma obscura, conhecida como texto ci-
frado., usando uma chave secreta. O algoritmo é conhecido como cifra. O destina-
tário pode então usar a mesma chave secreta para recuperar a mensagem de
texto sem formatação original. Quando o remetente e o destinatário usam a
mesma chave, isso é conhecido como criptografia de chave secreta. Existem tam-
bém algoritmos de criptografia de chave públicaem que o remetente e o destina-
tário têm chaves diferentes, mas não abordaremos isso com muitos detalhes neste
livro.

Um princípio importante da criptografia, conhecido comoPrincípio de Kerckhoff,


diz que um esquema de criptografia deve ser seguro mesmo que todos os aspectos
do algoritmo sejam conhecidos, desde que a chave permaneça secreta.

OBSERVAÇÃO Você deve usar apenas algoritmos que foram projetados por
meio de um processo aberto com revisão pública por especialistas, como os algo-
ritmos que você usará neste capítulo.

Existem vários algoritmos de criptografia segura em uso atual, mas o mais impor-
tante é o Advanced Encryption Standard(AES), que foi padronizado em 2001 após
uma competição internacional e é amplamente considerado muito seguro. AES é
um exemplo de cifra de bloco, que usa uma entrada de tamanho fixo de 16 bytes e
produz uma saída criptografada de 16 bytes. As chaves AES têm tamanho de 128
bits, 192 bits ou 256 bits. Para criptografar mais (ou menos) de 16 bytes com AES,
você usa um modo de operação de cifra de bloco. A escolha do modo de operação
é crucial para a segurança conforme demonstrado na figura 6.4, que mostra a
imagem de um pinguim criptografado com a mesma chave AES, mas com dois
modos de operação diferentes. 1 OLivro de Código Eletrônico(ECB) é completa-
mente inseguro e vaza muitos detalhes sobre a imagem, enquanto o modo mais
seguroModo de Contador(CTR) elimina todos os detalhes e parece ruído aleatório.

Figura 6.4 Uma imagem do mascote do Linux, Tux, que foi criptografada pelo AES
no modo ECB. A forma do pinguim e muitos recursos ainda são visíveis, apesar da
criptografia. Por outro lado, a mesma imagem criptografada com AES no modo
CTR é indistinguível de ruído aleatório. (Imagem original de Larry Ewing e The
GIMP, https://commons.wikimedia.org/wiki/File:Tux.svg. )
DEFINIÇÃO Uma cifra de blococriptografa um bloco de entrada de tamanho
fixo para produzir um bloco de saída. o AESA cifra de bloco opera em blocos de 16
bytes. Um modo de operação de cifra de blocopermite que uma cifra de bloco de
tamanho fixo seja usada para criptografar mensagens de qualquer tamanho. O
modo de operação é crítico para a segurança do processo de criptografia.

6.3.1 Criptografia autenticada

Muitosos algoritmos de criptografia apenas garantem a confidencialidade dos da-


dos que foram criptografados e não pretendem proteger a integridade desses da-
dos. Isso significa que um invasor não poderá ler nenhum atributo confidencial
em um token criptografado, mas poderá alterá-los. Por exemplo, se você souber
que um token é criptografado com o modo CTR e (quando descriptografado) co-
meça com a string user=brian , você pode alterar isso para leitura
user=admin por simples manipulação do texto cifrado, mesmo que não possa
descriptografar o token. Embora não haja espaço para entrar em detalhes aqui,
esse tipo de ataque costuma ser abordado em tutoriais de criptografia sob o no-
meataque de texto cifrado escolhido.

DEFINIÇÃO Um ataque de texto cifrado escolhidoé um ataque contra um es-


quema de criptografia no qual um invasor manipula o texto cifrado
criptografado.

Em termos de modelos de ameaças do capítulo 1, a criptografia protege contra


ameaças de divulgação de informações, mas não contra falsificação ou adultera-
ção. Em alguns casos, a confidencialidade também pode ser perdida se não hou-
ver garantia de integridade porque um invasor pode alterar uma mensagem e ver
qual mensagem de erro é gerada quando a API tenta descriptografá-la. Isso geral-
mente vaza informações sobre o que a mensagem foi descriptografada.

SAIBA MAIS Você pode aprender mais sobre como os algoritmos de criptografia
modernos funcionam e os ataques contra eles, em uma introdução atualizada ao
livro de criptografia, como Serious Cryptography de Jean-Philippe Aumasson (No
Starch Press, 2018).

Para se proteger contra ameaças de falsificação e adulteração, você sempre deve


usar algoritmos que forneçam criptografia autenticada. Algoritmos de criptogra-
fia autenticados combinam um algoritmo de criptografia para ocultar dados con-
fidenciais com um algoritmo MAC, como HMAC, para garantir que os dados não
possam ser alterados ou falsificados.

DEFINIÇÃO A criptografia autenticada combina um algoritmo de criptografia


com um MAC. A criptografia autenticada garante a confidencialidade e integri-
dade das mensagens.
Uma maneira de fazer isso seria combinar um esquema de criptografia seguro
como AES no modo CTR com HMAC. Por exemplo, você pode fazer
um EncryptedTokenStore que criptografa dados usando AES e combina isso
com o existente HmacTokenStore para autenticação. Mas há duas maneiras de
combinar esses dois armazenamentos: primeiro criptografando e depois apli-
cando o HMAC ou aplicando primeiro o HMAC e depois criptografando o token e a
tag juntos. Acontece que apenas o primeiro é geralmente seguro e é conhecido
comoEncrypt-then-MAC(EtM). Como é fácil errar, os criptógrafos desenvolveram
vários modos dedicados de criptografia autenticada, comoModo
Galois/Contador(GCM) para AES. O JOSE oferece suporte aos modos de criptogra-
fia GCM e EtM, que você examinará na seção 6.3.3, mas começaremos exami-
nando uma forma mais simplesalternativo.

6.3.2 Criptografia autenticada com NaCl

Porque Embora a criptografia seja complexa com muitos detalhes sutis para acer-
tar, uma tendência recente tem sido as bibliotecas de criptografia fornecerem
APIs de alto nível que ocultam muitos desses detalhes dos desenvolvedores. A
mais conhecida delas é a Networking and Cryptography Library (NaCl;
https://nacl.cr.yp.to ) projetada por Daniel Bernstein. NaCl (pronuncia-se “sal”,
como em cloreto de sódio) fornece operações de alto nível para criptografia au-
tenticada, assinaturas digitais e outras primitivas criptográficas, mas oculta mui-
tos dos detalhes dos algoritmos usados. Usar uma biblioteca de alto nível proje-
tada por especialistas como NaCl é a opção mais segura ao implementar proteções
criptográficas para suas APIs e pode ser significativamente mais fácil de usar com
segurança do que alternativas.

DICA Outras bibliotecas criptográficas projetadas para serem difíceis de usar in-
devidamente incluem Google's Tink ( https://github.com/google/tink ) e Themis da
Cossack Labs ( https://github.com/cossacklabs/themis ). A biblioteca Sodium (
https://libsodium.org ) é um clone amplamente usado de NaCl em C que fornece
muitas extensões adicionais e uma API simplificada com ligações para Java e ou-
tras linguagens.

Nesta seção, você usará uma implementação Java pura de NaCl chamada Salty
Coffee ( https://github.com/NeilMadden/salty-coffee ), que fornece uma API muito
simples e compatível com Java com desempenho aceitável. 2 Para adicionar a bi-
blioteca ao projeto Natter API, abra o arquivo pom.xml na pasta raiz do projeto
Natter API e adicione as seguintes linhas à seção de dependências:

<dependência>
<groupId>software.pando.crypto</groupId>
<artifactId>café salgado</artifactId>
<version>1.0.2</version>
</dependência>

A Listagem 6.5 mostra uma EncryptedTokenStore SecretBox implementado


usando a classe da biblioteca Salty Coffee, que fornece criptografia autenticada.
Como o HmacTokenStore , você pode delegar a criação do token para outra loja,
permitindo que isso seja agrupado no JsonTokenStore ou outro formato. A crip-
tografia é então executada com o SecretBox.encrypt() método. Esse método
retorna um SecretBox objeto, que possui métodos para obter o texto cifrado
criptografado e a marca de autenticação. O toString() método codifica esses
componentes em uma string segura de URL que você pode usar diretamente como
o ID do token. Para descriptografar o token, você pode usar o
SecretBox.fromString() método para recuperar o SecretBox da string codi-
ficada e, em seguida, usar o decryptToString() métodopara descriptografá-lo e
recuperar o ID do token original. Navegue até a pasta
src/main/java/com/manning/apisecurityinaction/token novamente e crie um novo
arquivo chamado EncryptedTokenStore.java com o conteúdo da listagem 6.5.

Listagem 6.5 Um EncryptedTokenStore

pacote com.manning.apisecurityinaction.token;

importar java.security.Key;
import java.util.Opcional;

importar software.pando.crypto.nacl.SecretBox;
import spark.Request;

public class EncryptedTokenStore implementa TokenStore {

delegado TokenStore final privado;


chave final privada criptografiaChave;
public EncryptedTokenStore(TokenStore delegado, Chave de criptografiaKey) {
this.delegate = delegado;
this.encryptionKey = criptografiaKey;
}

@Sobrepor
public String create(Solicitação de solicitação, Token token) {
var tokenId = delegado.create(pedido, token); ❶
return SecretBox.encrypt(encryptionKey, tokenId).toString(); ❷
}

@Sobrepor
public Opcional<Token> read(Request request, String tokenId) {
var box = SecretBox.fromString(tokenId); ❸
var originalTokenId = box.decryptToString(encryptionKey); ❸
return delegate.read(request, originalTokenId); ❸
}

@Sobrepor
public void revoke(Solicitação de solicitação, String tokenId) {
var box = SecretBox.fromString(tokenId); ❸
var originalTokenId = box.decryptToString(encryptionKey); ❸
delegado.revoke(solicitação, originalTokenId); ❸
}
}
❶ Chame o delegado TokenStore para gerar o ID do token.

❷ Use o método SecretBox.encrypt() para criptografar o token.

❸ Decodifique e descriptografe a caixa e use o ID do token original.

Como você pode ver, o EncryptedTokenStore usando SecretBox é muito curto


porque a biblioteca cuida de quase todos os detalhes para você. Para usar o novo
armazenamento, você precisará gerar uma nova chave para usar na criptografia
em vez de reutilizar a chave HMAC existente.

PRINCÍPIO Uma chave criptográfica deve ser usada apenas para um único pro-
pósito. Use chaves separadas para diferentes funcionalidades ou algoritmos.

Porque keytool o comando do Javanão oferece suporte à geração de chaves para


o algoritmo de criptografia SecretBox usado, você pode, em vez disso, gerar
uma chave AES padrão e convertê-la, pois os dois formatos de chave são idênticos.
SecretBox suporta apenas chaves de 256 bits, então execute o seguinte comando
na pasta raiz do projeto Natter API para adicionar uma nova chave AES ao
keystore existente:

keytool -genseckey -keyalg AES -keysize 256 \


-alias aes-key -keystore keystore.p12 -storepass changeit
Você pode então carregar a nova chave na Main classeassim como você fez para a
chave HMAC no capítulo 5. Abra Main.java em seu editor e localize as linhas que
carregam a chave HMAC do keystore e adicione uma nova linha para carregar a
chave AES:

var macKey = keyStore.getKey("hmac-key", keyPassword); ❶


var encKey = keyStore.getKey("aes-key", keyPassword); ❷

❶ A chave HMAC existente

❷ A nova chave AES

Você pode converter a chave no formato correto com o SecretBox.key() mé-


todo, passando os bytes da chave bruta, que você pode obter
chamando encKey.getEncoded () . Abra o arquivo Main.java novamente e atu-
alize o código que constrói o TokenController para converter a chave e usá-la
para criar um EncryptedTokenStore , envolvendo um JsonTokenStore , em
vez da implementação anterior baseada em JWT:

var naclKey = SecretBox.key(encKey.getEncoded()); ❶


var tokenStore = new EncryptedTokenStore( ❷
new JsonTokenStore(), naclKey); ❷
var tokenController = new TokenController(tokenStore); ❷

❶ Converta a chave para o formato correto.


❷ Construa o EncryptedTokenStore envolvendo um JsonTokenStore.

Agora você pode reiniciar a API e fazer login novamente para obter uma nova se-
nha criptografada símbolo.

6.3.3 JWTs criptografados

NaCl's SecretBox é difícil de superar em termos de simplicidade e segurança,


mas não há um padrão para como os tokens criptografados são formatados em
strings e bibliotecas diferentes podem usar formatos diferentes ou deixar isso
para o aplicativo. Isso não é um problema quando os tokens são consumidos ape-
nas pela mesma API que os gerou, mas pode se tornar um problema se os tokens
forem compartilhados entre muitas APIs, desenvolvidas por equipes separadas
em diferentes linguagens de programação. Um formato padrão como o JOSE
torna-se mais atraente nesses casos. O JOSE oferece suporte a vários algoritmos de
criptografia autenticados no padrão JSON Web Encryption (JWE).

Um JWT criptografado usando JWE Compact Serialization se parece superficial-


mente com os JWTs HMAC da seção 6.2, mas há mais componentes que refletem a
estrutura mais complexa de um token criptografado, mostrado na figura 6.5. Os
cinco componentes de um JWE são:
1. O cabeçalho JWE, que é muito parecido com o cabeçalho JWS, mas com dois
campos adicionais: enc , que especifica o algoritmo de criptografia e zip , que
especifica um algoritmo de compactação opcional a ser aplicado antes da
criptografia.
2. Uma chave criptografada opcional. Isso é usado em alguns dos algoritmos de
criptografia mais complexos. Ele está vazio para o algoritmo de criptografia si-
métrica direta abordado neste capítulo.
3. O vetor de inicializaçãoou nonceusado ao criptografar a carga útil. Dependendo
do método de criptografia usado, será um valor binário aleatório de 12 ou 16
bytes que foi codificado em Base64url.
4. O texto cifrado criptografado.
5. A etiqueta de autenticação MAC.

Figura 6.5 Um JWE em serialização compacta consiste em 5 componentes: um ca-


beçalho, uma chave criptografada (em branco neste caso), um vetor de inicializa-
ção ou nonce, o texto cifrado criptografado e, em seguida, a marca de autentica-
ção. Cada componente é codificado em Base64 seguro para URL. Os valores foram
truncados para exibição.
DEFINIÇÃO Um vetor de inicialização (IV) ou nonce (número-usado-uma vez) é
um valor exclusivo fornecido à cifra para garantir que o texto cifrado seja sempre
diferente, mesmo que a mesma mensagem seja criptografada mais de uma vez. O
IV deve ser gerado usando um java.security.SecureRandom ou outro gerador
de números pseudo-aleatórios criptograficamente seguro(CSPRNG). 3 Um IV não
precisa ser mantido em segredo.

JWE divide a especificação do algoritmo de criptografia em duas partes:

o enc cabeçalhodescreve o algoritmo de criptografia autenticado usado para


criptografar a carga útil do JWE.
o alg cabeçalhodescreve como o remetente e o destinatário concordam com a
chave usada para criptografar o conteúdo.

Há uma grande variedade de algoritmos de gerenciamento de chaves para JWE,


mas neste capítulo você se limitará à criptografia direta com uma chave secreta.
Para criptografia direta, o cabeçalho do algoritmo é definido como dir (direto).
Existem atualmente duas famílias de métodos de criptografia disponíveis no JOSE,
ambas fornecendo criptografia autenticada:

A128GCM , A192GCM , e A256GCM use AES no modo Galois Counter(GCM).


A128CBC-HS256 , A192CBC-HS384 , e A256CBC-HS512 use AES emEncadea-
mento de blocos cifrados(CBC) junto com HMAC em uma configuração EtM con-
forme descrito na seção 6.3.1.
DEFINIÇÃO Todos os algoritmos de criptografia permitem que o cabeçalho JWE
e IV sejam incluídos na tag de autenticação sem serem criptografados. Estes são
conhecidos comocriptografia autenticada com dados associados(AEAD)
algoritmos.

O GCM foi projetado para uso em protocolos como o TLS, onde uma chave de ses-
são exclusiva é negociada para cada sessão e um contador simples pode ser usado
para o nonce. Se você reutilizar um nonce com GCM, quase toda a segurança será
perdida: um invasor pode recuperar a chave MAC e usá-la para forjar tokens, o
que é catastrófico para tokens de autenticação. Por esse motivo, prefiro usar CBC
com HMAC para JWTs criptografados diretamente, mas para outros algoritmos
JWE, o GCM é uma escolha excelente e muito rápida.

O CBC exige que a entrada seja preenchida com um múltiplo do tamanho do bloco
AES (16 bytes), e isso historicamente levou a uma vulnerabilidade devastadora co-
nhecida comoataque de oráculo de preenchimento, que permite que um invasor
recupere o texto sem formatação completo apenas observando as diferentes men-
sagens de erro quando uma API tenta descriptografar um token adulterado. O uso
de HMAC no JOSE evita esse tipo de adulteração e elimina em grande parte a pos-
sibilidade de padding de ataques oracle, sendo que o padding possui alguma
segurançabenefícios.

AVISO Você deve evitar revelar o motivo da falha na descriptografia aos chama-
dores de sua API para evitar ataques de oráculo como o ataque de oráculo de pre-
enchimento CBC.
Que tamanho de chave você deve usar?

O AES permite que as chaves tenham um dos três tamanhos diferentes: 128 bits,
192 bits ou 256 bits. Em princípio, adivinhar corretamente uma chave de 128 bits
está muito além da capacidade até mesmo de um invasor com enormes quantida-
des de poder de computação. Tentar todos os valores possíveis de uma chave é co-
nhecido como ataque de força brutae deve ser impossível para uma chave desse
tamanho. Há três exceções em que essa suposição pode se mostrar errada:

Uma fraqueza no algoritmo de criptografia pode ser descoberta, o que reduz a


quantidade de esforço necessária para quebrar a chave. Aumentar o tamanho
da chave fornece uma margem de segurança contra tal possibilidade.
Novos tipos de computadores podem ser desenvolvidos para executar pesqui-
sas de força bruta muito mais rapidamente do que os computadores existentes.
Acredita-se que isso seja verdade para computadores quânticos, mas não se
sabe se algum dia será possível construir um computador quântico grande o su-
ficiente para que isso seja uma ameaça real. Dobrar o tamanho da chave pro-
tege contra ataques quânticos conhecidos para algoritmos simétricos como
AES.
Teoricamente, se cada usuário tiver sua própria chave de criptografia e você ti-
ver milhões de usuários, pode ser possível atacar todas as chaves simultanea-
mente com menos esforço do que você esperaria ao tentar ingenuamente que-
brá-las uma de cada vez. Isso é conhecido como um ataque em lotee é descrito
mais detalhadamente em https://blog.cr.yp.to/ 20151120-batchattacks.html .
No momento da redação deste artigo, nenhum desses ataques é prático para
AES e, para tokens de autenticação de curta duração, o risco é significativa-
mente menor, portanto, as chaves de 128 bits são perfeitamente seguras. Por
outro lado, as CPUs modernas têm instruções especiais para criptografia AES,
portanto, há muito pouco custo extra para chaves de 256 bits, se você quiser eli-
minar qualquer dúvida.

Lembre-se de que o JWE CBC com métodos HMAC usa uma chave com o dobro do
tamanho normal. Por exemplo, o A128CBC-HS256 métodorequer uma chave de
256 bits, mas na verdade são duas chaves de 128 bits unidas em vez de uma ver-
dadeira chave de 256 bits.
6.3.4 Usando uma biblioteca JWT

Vencimentodevido à relativa complexidade de produzir e consumir JWTs cripto-


grafados em comparação com o HMAC, você continuará usando a biblioteca Nim-
bus JWT nesta seção. A criptografia de um JWT com o Nimbus requer algumas
etapas, conforme mostrado na Listagem 6.6.

Primeiro, você cria um conjunto de declarações JWT usando a


JWTClaimsSet.Builder classe conveniente.
Você pode então criar um JWEHeader objetopara especificar o algoritmo e o
método de criptografia.
Finalmente, você criptografa o JWT usando um DirectEncrypter objetoinici-
alizado com a chave AES.
o serialize() métodono EncryptedJWT objetoretornará a Serialização Com-
pacta JWE. Navegue até src/main/java/com/manning/apisecurityinaction/token e
crie um novo nome de arquivo EncryptedJwtTokenStore.java. Digite o conteúdo
da listagem 6.6 para criar o novo armazenamento de token e salve o arquivo.
Quanto ao JsonTokenStore , deixe o revoke métodoem branco por enquanto.
Você corrigirá isso na seção 6.6.

Listagem 6.6 O EncryptedJwtTokenStore

pacote com.manning.apisecurityinaction.token;

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;
import spark.Request;

importar javax.crypto.SecretKey;
import java.text.ParseException;
importar java.util.*;

public class EncryptedJwtTokenStore implementa TokenStore {

final privada SecretKey encKey;

public EncryptedJwtTokenStore(SecretKey encKey) {


this.encKey = encKey;
}

@Sobrepor
public String create(Solicitação de solicitação, Token token) {
var ClaimBuilder = new JWTClaimsSet.Builder() ❶
.subject(token.username) ❶
.audience("https://localhost:4567") ❶
.expirationTime(Date.from(token.expiry)); ❶
token.attributes.forEach(claimsBuilder::claim); ❶
var header = new JWEHeader(JWEAlgorithm.DIR, ❷
EncryptionMethod.A128CBC_HS256); ❷
var jwt = new EncryptedJWT(cabeçalho, ClaimBuilder.build()); ❷

tentar {
var criptografador = novo DirectEncrypter(encKey); ❸
jwt.encrypt(criptografador); ❸
} catch (JOSEException e) {
lança nova RuntimeException(e);
}

return jwt.serialize(); ❹
}

@Sobrepor
public void revoke(Solicitação de solicitação, String tokenId) {
}
}
❶ Crie o conjunto de declarações JWT.

❷ Crie o cabeçalho JWE e monte o cabeçalho e as reivindicações.

❸ Criptografe o JWT usando a chave AES no modo de criptografia direta.

❹ Retorne a serialização compacta do JWT criptografado.

Processar um JWT criptografado usando a biblioteca é tão simples quanto criar


um. Primeiro, você analisa o JWT criptografado e depois o descriptografa usando
um DirectDecrypter inicializado com a chave AES, conforme a Listagem 6.7. Se
a validação da marca de autenticação falhar durante a descriptografia, a biblio-
teca lançará uma exceção. Para reduzir ainda mais a possibilidade de preenchi-
mento de ataques oracle no modo CBC, você nunca deve retornar nenhum detalhe
sobre o motivo da falha na descriptografia para o usuário, portanto, apenas re-
torne um vazio Optional aqui como se nenhum token tivesse sido fornecido.
Você pode registrar os detalhes da exceção em um registro de depuração que só
pode ser acessado por administradores de sistema, se desejar. Depois que o JWT
for descriptografado, você poderá extrair e validar as declarações do JWT. Abra
EncryptedJwtTokenStore.java em seu editor novamente e implemente o método
read como na Listagem 6.7.

Listagem 6.7 O método de leitura do JWT


@Sobrepor
public Opcional<Token> read(Request request, String tokenId) {
tentar {
var jwt = EncryptedJWT.parse(tokenId); ❶

var decryptor = new DirectDecrypter(encKey); ❷


jwt.decrypt(decryptor); ❷

var reivindicações = jwt.getJWTClaimsSet();


if (!claims.getAudience().contains("https://localhost:4567")) {
return Opcional.vazio();
}
var expiração = reivindicações.getExpirationTime().toInstant();
var subject = Claims.getSubject(); ❸
var token = new Token(expiração, assunto); ❸
var ignore = Set.of("exp", "sub", "aud"); ❸
for (var attr : Claims.getClaims().keySet()) { ❸
if (ignore.contains(attr)) continue; ❸
token.attributes.put(attr, Claims.getStringClaim(attr)); ❸
}
return Opcional.of(token);
} catch (ParseException | JOSEException e) {
return Opcional.vazio(); ❹
}
}
❶ Analisar o JWT criptografado.

❷ Descriptografe e autentique o JWT usando o DirectDecrypter.

❸ Extraia quaisquer declarações do JWT.

❹ Nunca revele a causa de uma falha de descriptografia ao usuário.

Agora você pode atualizar o método principal para alternar para usar o
método EncryptedJwtTokenStore , substituindo o
anterior EncryptedTokenStore . Você pode reutilizar a chave AES gerada na se-
ção 6.3.2, mas precisará convertê-la para a javax.crypto.SecretKey classe
mais específicaque a biblioteca Nimbus espera. Abra Main.java e atualize o código
para criar o controlador de token novamente:

TokenStore tokenStore = new EncryptedJwtTokenStore(


(SecretKey) encKey); ❶
var tokenController = new TokenController(tokenStore);

❶ Transmita a chave para a classe SecretKey mais específica.

Reinicie a API e tenteistoFora:

$ curl -H 'Tipo de conteúdo: aplicativo/json' \


-u teste:senha -X POST https://localhost:4567/sessions
{"token":"eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIn0..hAOoOsgfGb8yuhJD
➥ .kzhuXMMGunteKXz12aBSnqVfqtlnvvzqInLqp83zBwUW_rqWoQp5wM_q2D7vQxpK
➥ TaQR4Nuc-D3cPcYt7MXAJQ.ZigZZclJPDNMlP5GM1oXwQ"}

Tokens compactados

O JWT criptografado é um pouco maior do que um simples token HMAC ou os to-


kens NaCl da seção 6.3.2. O JWE oferece suporte à compactação opcional do JWT
Claims Set antes da criptografia, o que pode reduzir significativamente o tamanho
de tokens complexos. Mas combinar criptografia e compactação pode levar a fa-
lhas de segurança. A maioria dos algoritmos de criptografia não oculta o compri-
mento da mensagem de texto simples que foi criptografada e a compactação re-
duz o tamanho de uma mensagem com base em seu conteúdo. Por exemplo, se
duas partes de uma mensagem forem idênticas, ela pode combiná-las para remo-
ver a duplicação. Se um invasor puder influenciar parte de uma mensagem, ele
poderá adivinhar o restante do conteúdo vendo o quanto ele comprime. O crimee
VIOLAÇÃOataques ( http://breachattack.com ) contra o TLS foram capazes de ex-
plorar esse vazamento de informações da compactação para roubar cookies de
sessão de páginas HTTP compactadas. Esses tipos de ataques nem sempre são um
risco, mas você deve considerar cuidadosamente a possibilidade antes de ativar a
compactação. A menos que você realmente precise economizar espaço, deixe a
compactação desativada.

  

questionário
4. Contra quais ameaças STRIDE a criptografia autenticada protege? (Existem vá-
rias respostas corretas.)
1. Falsificação
2. adulteração
3. Repúdio
4. Divulgação de informação
5. Negação de serviço
6. Elevação de privilégio
5. Qual é o propósito do vetor de inicialização (IV) em um algoritmo de
criptografia?
1. É um lugar para adicionar seu nome às mensagens.
2. Ele retarda a descriptografia para evitar ataques de força bruta.
3. Aumenta o tamanho da mensagem para garantir a compatibilidade com dife-
rentes algoritmos.
4. Ele garante que o texto cifrado seja sempre diferente, mesmo que uma men-
sagem duplicada seja criptografada.
6. Verdadeiro ou falso: Um IV sempre deve ser gerado usando um gerador de nú-
meros aleatórios seguro.

As respostas estão no final do capítulo.

6.4 Usando tipos para design de API seguro

Imagineque você implementou o armazenamento de tokens usando o kit de peças


desenvolvido neste capítulo, criando um JsonTokenStore e envolvendo-o em
um EncryptedTokenStore para adicionar criptografia autenticada, fornecendo
confidencialidade e autenticidade de tokens. Mas seria fácil para alguém remover
acidentalmente a criptografia se simplesmente comentasse o
EncryptedTokenStore wrapper no método principal, perdendo ambas as pro-
priedades de segurança. Se você desenvolveu o EncryptedTokenStore uso de
um esquema de criptografia não autenticado, como o modo CTR, e o combinou
manualmente com o HmacTokenStore , o risco seria ainda maior porque nem
todas as formas de combinar essas duas lojas são seguras, como você aprendeu na
seção 6.3.1.

A abordagem de kit de peças para o design de software costuma ser atraente para
os engenheiros de software, porque resulta em um design organizado com sepa-
ração adequada de interesses e capacidade máxima de reutilização. Isso foi útil
quando você pode reutilizar o HmacTokenStore , originalmente projetado para
proteger tokens com base em banco de dados, para também proteger tokens JSON
armazenados no cliente. Mas um projeto de kit de peças se opõe à segurança se
houver muitas maneiras inseguras de combinar as peças e apenas algumas que
sejam seguras.

PRINCÍPIO O design seguro da API deve tornar muito difícil escrever código in-
seguro. Não basta apenas tornar possível escrever código seguro, porque os de-
senvolvedores cometerão erros.
Figura 6.6 Você pode usar interfaces de marcador para indicar as propriedades de
segurança de seus armazenamentos de tokens individuais. Se uma loja fornecer
apenas confidencialidade, ela deverá implementar a interface ConfidentialTo-
kenStore. Você pode então definir um SecureTokenStore subdigitando a combina-
ção desejada de propriedades de segurança. Nesse caso, garante confidenciali-
dade e autenticação.
Você pode dificultar o uso indevido de um projeto de kit de peças usando tipos
para impor as propriedades de segurança necessárias, conforme mostrado na Fi-
gura 6.6. Em vez de todas as lojas de token individuais implementando uma To-
kenStore interface genérica, você pode definir interfaces de marcadorque des-
crevem as propriedades de segurança da implementação. UMA Confidential-
TokenStore garante que o estado do token seja mantido em segredo, enquanto
um AuthenticatedTokenStore garante que o token não pode ser adulterado ou
falsificado. Podemos então definir um SecureTokenStore esse é um subtipo de
cada uma das propriedades de segurança que queremos impor. Nesse caso, você
deseja que o controlador de token use um armazenamento de token que seja con-
fidencial e autenticado. Você pode atualizar o TokenController para exigir um
SecureTokenStore , garantindo que uma implementação insegura não seja
usada por engano.

DEFINIÇÃO Uma interface de marcadoré uma interface que não define novos
métodos. É usado apenas para indicar que a implementação tem certas proprie-
dades desejáveis.

Navegue até src/main/java/com/manning/apisecurityinaction/token e adicione as


três novas interfaces de marcador, conforme mostrado na Listagem 6.8. Crie três
arquivos separados, ConfidentialTokenStore.java, AuthenticatedTokenStore.java e
SecureTokenStore.java para manter as três novas interfaces.

Listagem 6.8 As interfaces de marcador seguro


pacote com.manning.apisecurityinaction.token; ❶

interface pública ConfidentialTokenStore estende TokenStore { ❶


} ❶

pacote com.manning.apisecurityinaction.token; ❷

public interface AuthenticatedTokenStore extends TokenStore { ❷


} ❷

pacote com.manning.apisecurityinaction.token; ❸

interface pública SecureTokenStore estende ConfidentialTokenStore, ❸


AuthenticatedTokenStore { ❸
} ❸

❶ A interface do marcador ConfidentialTokenStore deve ir para


ConfidentialTokenStore.java.

❷ O AuthenticatedTokenStore deve ir para AuthenticatedTokenStore.java.

❸ O SecureTokenStore os combina e vai para SecureTokenStore.java.

Agora você pode alterar cada um dos armazenamentos de token para implemen-
tar uma interface apropriada:
Se você presumir que o armazenamento de cookies de back-end é seguro con-
tra injeção e outros ataques, o CookieTokenStore pode ser atualizado para
implementar a SecureTokenStore interface.
Se você seguiu o conselho de endurecimento do capítulo 5, o DatabaseTo-
kenStore também pode ser marcado como SecureTokenStore . Se você qui-
ser garantir que ele seja sempre usado com o HMAC para proteção extra contra
adulteração, marque-o como apenas confidencial.
o JsonTokenStore é completamente inseguro por conta própria, então deixe-o
implementando a TokenStore interface base.
o SignedJwtTokenStore não fornece confidencialidade para declarações no
JWT, portanto, deve implementar apenas a AuthenticatedTokenStore inter-
face.
o HmacTokenStore vira qualquer TokenStore em um AuthenticatedTo-
kenStore . Mas se o armazenamento subjacente já for confidencial, o resultado
será um arquivo SecureTokenStore . Você pode refletir essa diferença no có-
digo tornando o HmacTokenStore construtor privado e fornecendo dois méto-
dos de fábrica estáticos, conforme mostrado na Listagem 6.9. Se o armazena-
mento subjacente for confidencial, o primeiro método retornará um arquivo
SecureTokenStore . Para qualquer outra coisa, o segundo método será cha-
mado e retornará apenas um AuthenticatedTokenStore .
o EncryptedTokenStore e EncryptedJwtTokenStore ambos podem ser alte-
rados para implementar SecureTokenStore porque ambos fornecem cripto-
grafia autenticada que atinge os objetivos de segurança combinados, indepen-
dentemente do armazenamento subjacente transmitido.
Listagem 6.9 Atualizando o HmacTokenStore

public class HmacTokenStore implementa SecureTokenStore { ❶

delegado TokenStore final privado;


chave final privada macKey;

private HmacTokenStore(TokenStore delegado, Key macKey) { ❷


this.delegate = delegado;
this.macKey = macKey;
}
public static SecureTokenStore wrap(armazenamento ConfidentialTokenStore,
Key macKey) { ❸
return new HmacTokenStore(store, macKey); ❸
} ❸
public static AuthenticatedTokenStore wrap(TokenStore store, ❹
Key macKey) { ❹
return new HmacTokenStore(store, macKey); ❹
} ❹

❶ Marque o HmacTokenStore como seguro.

❷ Torne o construtor privado.

❸ Ao passar por um ConfidentialTokenStore, retorna um SecureTokenStore.


❹ Ao passar por qualquer outro TokenStore, retorna um AuthenticatedTokenStore.

Agora você pode atualizar a TokenController classeexigir um SecureTokenS-


tore ser passado para ele. Abra TokenController.java em seu editor e atualize o
construtor para obter um SecureTokenStore :

public TokenController(SecureTokenStore tokenStore) {


this.tokenStore = tokenStore;
}

Essa alteração torna muito mais difícil para um desenvolvedor passar acidental-
mente em uma implementação que não atende às suas metas de segurança, por-
que o código falhará na verificação de tipo. Por exemplo, se você tentar passar em
uma planície JsonTokenStore , o código falhará ao compilar com um erro de
tipo. Essas interfaces de marcador também fornecem uma documentação valiosa
das propriedades de segurança esperadas de cada implementação e um guia para
revisores de código e auditorias de segurança para verificar se eles atingemeles.

6.5 Lidando com a revogação do token

apátridatokens independentes, como JWTs, são ótimos para mover o estado para
fora do banco de dados. Aparentemente, isso aumenta a capacidade de escalar a
API sem precisar de hardware de banco de dados adicional ou topologias de im-
plantação mais complexas. Também é muito mais fácil configurar uma nova API
com apenas uma chave de criptografia em vez de precisar implantar um novo
banco de dados ou adicionar uma dependência a um existente. Afinal, um banco
de dados de token compartilhado é um ponto único de falha. Mas o calcanhar de
Aquiles dos tokens sem estado é como lidar com a revogação do token. Se todo o
estado estiver no cliente, fica muito mais difícil invalidar esse estado para revogar
um token. Não há banco de dados do qual excluir o token.

Existem algumas maneiras de lidar com isso. Primeiro, você pode simplesmente
ignorar o problema e não permitir que os tokens sejam revogados. Se seus tokens
tiverem vida curta e sua API não lidar com dados confidenciais ou executar ope-
rações privilegiadas, talvez você se sinta confortável com o risco de não permitir
que os usuários façam logout explicitamente. Mas poucas APIs se encaixam nessa
descrição; quase todos os dados são confidenciais para alguém. Isso deixa várias
opções, quase todas envolvendo o armazenamento de algum estado no servidor,
afinal:

Você pode adicionar algum estado mínimo ao banco de dados que lista um ID
exclusivo associado ao token. Para revogar um JWT, exclua o registro corres-
pondente do banco de dados. Para validar o JWT, agora você deve executar
uma pesquisa no banco de dados para verificar se o ID exclusivo ainda está no
banco de dados. Se não for, o token foi revogado. Isso é conhecido como lista de
permissões. 4
Uma reviravolta no esquema acima é armazenar apenas o ID exclusivo no
banco de dados quando o token é revogado, criando uma lista de bloqueiode to-
kens revogados. Para validar, verifique se não há um registro correspondente
no banco de dados. O ID exclusivo só precisa ser bloqueado até que o token ex-
pire, momento em que será inválido de qualquer maneira. Usar tempos de ex-
piração curtos ajuda a manter a lista de bloqueio pequena.
Em vez de bloquear tokens individuais, você pode bloquear determinados atri-
butos de um conjunto de tokens. Por exemplo, é uma prática de segurança co-
mum invalidar todas as sessões existentes de um usuário quando ele altera sua
senha. Os usuários geralmente alteram sua senha quando acreditam que outra
pessoa pode ter acessado sua conta, portanto, invalidar qualquer sessão exis-
tente expulsará o invasor. Como não há registro das sessões existentes no servi-
dor, você pode registrar uma entrada no banco de dados informando que todos
os tokens emitidos para o usuário Mary antes da hora do almoço na sexta-feira
devem ser considerados inválidos. Isso economiza espaço no banco de dados ao
custo de maior complexidade da consulta.
Por fim, você pode emitir tokens de curta duração e forçar o usuário a se auten-
ticar novamente regularmente. Isso limita o dano que pode ser feito com um to-
ken comprometido sem a necessidade de nenhum estado adicional no servidor,
mas fornece uma experiência de usuário insatisfatória. No capítulo 7, você
usará tokens de atualização OAuth2 para fornecer uma versão mais transpa-
rente desse padrão.
6.5.1 Implementando tokens híbridos

oexistir DatabaseTokenStore pode ser usado para implementar uma lista de


JWTs válidos e esse é o padrão mais simples e seguro para a maioria das APIs. Em-
bora isso envolva desistir da natureza sem estado pura de uma arquitetura JWT e
possa inicialmente parecer oferecer o pior dos dois mundos - confiança em um
banco de dados centralizado junto com a natureza arriscada do estado do lado do
cliente - na verdade, oferece muitas vantagens sobre cada estratégia de armaze-
namento por conta própria:

Os tokens do banco de dados podem ser revogados fácil e imediatamente. Em


setembro de 2018, o Facebook foi atingido por um ataque que explorou uma
vulnerabilidade em algum código de manipulação de token para obter acesso
rápido às contas de muitos usuários (
https://newsroom.fb.com/news/2018/09/security-update / ). Após o ataque, o Fa-
cebook revogou 90 milhões de tokens, forçando esses usuários a se autentica-
rem novamente. Em uma situação de desastre, você não quer esperar horas
para que os tokens expirem ou encontrar problemas de escalabilidade de re-
pente com sua lista de bloqueio ao adicionar 90 milhões de novas entradas.
Por outro lado, tokens simples de banco de dados podem ser vulneráveis ​a
roubo e falsificação de token se o banco de dados for comprometido, conforme
descrito na seção 5.3 do capítulo 5. Nesse capítulo, você fortaleceu tokens de
banco de dados usando o método HmacTokenStore para evitar falsificações. O
agrupamento de tokens de banco de dados em um JWT ou outro formato de to-
ken autenticado obtém as mesmas proteções.
Menos operações críticas de segurança podem ser executadas com base apenas
nos dados do JWT, evitando uma consulta ao banco de dados. Por exemplo,
você pode decidir permitir que um usuário veja de quais espaços sociais do
Natter ele é membro e quantas mensagens não lidas ele tem em cada um deles
sem verificar o status de revogação do token, mas exigir uma verificação do
banco de dados quando ele realmente tentar leia um deles ou poste uma nova
mensagem.
Os atributos de token podem ser movidos entre o JWT e o banco de dados, de-
pendendo de quão sensíveis eles são ou da probabilidade de serem alterados.
Você pode querer armazenar algumas informações básicas sobre o usuário no
JWT, mas armazenar um último tempo de atividade para implementar tempos
limite ociososno banco de dados porque ele mudará com frequência.

DEFINIÇÃO Um tempo limite ocioso(ou logout por inatividade) revoga automa-


ticamente um token de autenticação se ele não for usado por um determinado pe-
ríodo de tempo. Isso pode ser usado para desconectar automaticamente um usuá-
rio se ele parou de usar sua API, mas se esqueceu de desconectar manualmente.
A Listagem 6.10 mostra o EncryptedJwtTokenStore atualizado para listar to-
kens válidos no banco de dados. Ele faz isso tomando uma instância do Databa-
seTokenStore como um argumento do construtor e usa isso para criar um token
fictício sem atributos. Se você deseja mover atributos do JWT para o banco de da-
dos, pode fazer isso aqui preenchendo os atributos no token do banco de dados e
removendo-os do token JWT. O ID do token retornado do banco de dados é arma-
zenado dentro do JWT como a jti declaração JWT ID ( ) padrão. Abra
JwtTokenStore.java em seu editor e atualize-o para permitir a lista de tokens no
banco de dados como na listagem.

Listagem 6.10 Lista de permissões de JWTs no banco de dados

public class EncryptedJwtTokenStore implementa SecureTokenStore {

final privada SecretKey encKey;


private final DatabaseTokenStore tokenAllowlist; ❶

public EncryptedJwtTokenStore(SecretKey encKey,


DatabaseTokenStore tokenAllowlist) { ❶
this.encKey = encKey;
this.tokenAllowlist = tokenAllowlist; ❶
}
@Sobrepor
public String create(Solicitação de solicitação, Token token) {
var allowlistToken = new Token(token.expiry, token.username); ❷
var jwtId = tokenAllowlist.create(request, allowlistToken); ❷
var ClaimsBuilder = new JWTClaimsSet.Builder()
.jwtID(jwtId) ❸
.subject(token.username)
.audience("https://localhost:4567")
.expirationTime(Date.from(token.expiry));
token.attributes.forEach(claimsBuilder::claim);

var cabeçalho = new JWEHeader(JWEAlgorithm.DIR,


EncryptionMethod.A128CBC_HS256);
var jwt = new EncryptedJWT(cabeçalho, ClaimBuilder.build());

tentar {
var criptografador = new DirectEncrypter(encKey);
jwt.encrypt(criptografador);
} catch (JOSEException e) {
lança nova RuntimeException(e);
}

return jwt.serialize();
}

❶ Injetar um DatabaseTokenStore no EncryptedJwtTokenStore para usar na lista de


permissões.

❷ Salve uma cópia do token no banco de dados, mas remova todos os atributos para
economizar espaço.
❸ Salve o ID do token do banco de dados no JWT como a declaração de ID do JWT.

Para revogar um JWT, basta excluí-lo do armazenamento de token do banco de


dados, conforme mostrado na Listagem 6.11. Analise e descriptografe o JWT como
antes, o que validará a marca de autenticação e, em seguida, extrairá o ID do JWT
e o revogará do banco de dados. Isso removerá o registro correspondente do
banco de dados. Enquanto você ainda tem o JwtTokenStore.java aberto em seu
editor, adicione a implementação do método revoke da listagem.

Listagem 6.11 Revogando um JWT na lista de permissões do banco de dados

@Sobrepor
public void revoke(Solicitação de solicitação, String tokenId) {
tentar {
var jwt = EncryptedJWT.parse(tokenId); ❶
var decryptor = new DirectDecrypter(encKey); ❶
jwt.decrypt(decryptor); ❶
var reivindicações = jwt.getJWTClaimsSet(); ❶

tokenAllowlist.revoke(request, Claims.getJWTID()); ❷
} catch (ParseException | JOSEException e) {
throw new IllegalArgumentException("token inválido", e);
}
}

❶ Analisar, descriptografar e validar o JWT usando a chave de descriptografia.


❷ Extraia o JWT ID e revogue-o da lista de permissões DatabaseTokenStore.

A parte final da solução é verificar se o token da lista de permissões não foi revo-
gado ao ler um token JWT. Como antes, analise e descriptografe o JWT usando a
chave de descriptografia. Em seguida, extraia o JWT ID e execute uma pesquisa
no DatabaseTokenStore . Se a entrada existir no banco de dados, o token ainda
será válido e você poderá continuar validando as outras declarações JWT como
antes. Mas se o banco de dados retornar um resultado vazio, o token foi revogado
e, portanto, é inválido. Atualize o read() métodoem JwtTokenStore.java para im-
plementar essa verificação de adição, conforme mostrado na Listagem 6.12. Se
você moveu alguns atributos para o banco de dados, também pode copiá-los para
o resultado do token neste caso.

Listagem 6.12 Verificando se um JWT foi revogado

var jwt = EncryptedJWT.parse(tokenId); ❶


var decryptor = new DirectDecrypter(encKey); ❶
jwt.decrypt(decryptor); ❶

var reivindicações = jwt.getJWTClaimsSet();


var jwtId = reivindicações.getJWTID(); ❷
if (tokenAllowlist.read(request, jwtId).isEmpty()) { ❷
return Optional.empty(); ❸
}
// Valida outras declarações JWT
❶ Analisar e descriptografar o JWT.

❷ Verifique se o JWT ID ainda existe na lista de permissões do banco de dados.

❸ Caso contrário, o token é inválido; caso contrário, prossiga com a validação de ou-
tras declarações JWT.

Respostas para perguntas do questionário

1. a e b. O HMAC impede que um invasor crie tokens de autenticação falsos (spoo-


fing) ou adultere os existentes.
2. e. A declaração aud (público) lista os servidores pelos quais um JWT deve ser
usado. É crucial que sua API rejeite qualquer JWT que não seja destinado a esse
serviço.
3. Falso. O cabeçalho do algoritmo não é confiável e deve ser ignorado. Em vez
disso, você deve associar o algoritmo a cada chave.
4. a, b e d. A criptografia autenticada inclui um MAC, portanto, protege contra
ameaças de falsificação e adulteração, como o HMAC. Além disso, esses algorit-
mos protegem dados confidenciais contra ameaças de divulgação de
informações.
5. d. O IV (ou nonce) garante que cada texto cifrado seja diferente.
6. Verdadeiro. IVs devem ser gerados aleatoriamente. Embora alguns algoritmos
permitam um contador simples, eles são muito difíceis de sincronizar entre os
servidores da API e a reutilização pode ser catastróficaparasegurança.
Resumo

O estado do token pode ser armazenado no cliente codificando-o em JSON e


aplicando a autenticação HMAC para evitar adulterações.
Atributos de token confidenciais podem ser protegidos com criptografia, e algo-
ritmos de criptografia autenticados eficientes podem remover a necessidade de
uma etapa HMAC separada.
As especificações JWT e JOSE fornecem um formato padrão para tokens auten-
ticados e criptografados, mas historicamente são vulneráveis ​a vários ataques
sérios.
Quando usado com cuidado, o JWT pode ser uma parte eficaz de sua estratégia
de autenticação de API, mas você deve evitar as partes mais propensas a erros
do padrão.
A revogação de JWTs sem estado pode ser obtida mantendo uma lista de per-
missões ou uma lista de bloqueio de tokens no banco de dados. Uma estratégia
de lista de permissões é um padrão seguro que oferece vantagens sobre tokens
sem estado puros e banco de dados não autenticadotokens.

1.
Este é um exemplo muito famoso conhecido como ECB Penguin. Você encontrará o
mesmo exemplo em muitos livros introdutórios de criptografia.

2.
Escrevi Salty Coffee, reutilizando código criptográfico da biblioteca Tink do Goo-
gle, para fornecer uma solução Java pura e simples. As ligações ao libsodium são
geralmente mais rápidas se você puder usar uma biblioteca nativa.

3.
Um nonce só precisa ser único e pode ser um contador simples. No entanto, sin-
cronizar um contador em muitos servidores é difícil e sujeito a erros, portanto, é
melhor sempre usar um valor aleatório.

4.
Os termos lista de permissões e lista de bloqueio agora são preferidos em relação
aos termos antigos lista de permissões e lista negra devido a conotações negativas
associadas aos termos antigos.

Você também pode gostar