Escolar Documentos
Profissional Documentos
Cultura Documentos
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.
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.
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.
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;
@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"); ❸
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.
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 :
❶ Construa o JsonTokenStore.
Você pode experimentá-lo para ver seu primeiro token sem estadodentroação:
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
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.
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.
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.
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 :
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
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.
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.
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.
{
"alg": "HS256", ❶
"kid": "hmac-key-1" ❷
}
❶ O algoritmo
❷ O identificador de chave
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.
{
"kty": "out",
"alg": "HS256", ❶
"k": "9ITYj4mt-TLYT2b_vnAyCVurks1r2uzCLw7sOxg-75g" ❷
}
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.
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;
@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
}
}
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);
Agora você pode reiniciar o servidor da API, criar um usuário de teste e fazer lo-
gin para ver o JWT criado:
if (!jwt.verify(verifier)) { ❶
throw new JOSEException("Assinatura inválida"); ❶
} ❶
return Opcional.of(token);
} catch (ParseException | JOSEException e) {
return Opcional.vazio(); ❹
}
}
❶ Analise o JWT e verifique a assinatura HMAC usando o JWSVerifier.
Agora você pode reiniciar a API e usar o JWT para criar um novosocialespaço:
{"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.
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.
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).
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>
pacote com.manning.apisecurityinaction.token;
importar java.security.Key;
import java.util.Opcional;
importar software.pando.crypto.nacl.SecretBox;
import spark.Request;
@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.
PRINCÍPIO Uma chave criptográfica deve ser usada apenas para um único pro-
pósito. Use chaves separadas para diferentes funcionalidades ou algoritmos.
Agora você pode reiniciar a API e fazer login novamente para obter uma nova se-
nha criptografada símbolo.
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:
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
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.*;
@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.
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:
Tokens compactados
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.
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.
pacote com.manning.apisecurityinaction.token; ❷
pacote com.manning.apisecurityinaction.token; ❸
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
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.
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
tentar {
var criptografador = new DirectEncrypter(encKey);
jwt.encrypt(criptografador);
} catch (JOSEException e) {
lança nova RuntimeException(e);
}
return jwt.serialize();
}
❷ 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.
@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);
}
}
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.
❸ Caso contrário, o token é inválido; caso contrário, prossiga com a validação de ou-
tras declarações JWT.
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.