5 Modern Token-Based Authentication - API Security in Action
5 Modern Token-Based Authentication - API Security in Action
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.
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:
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
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.
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
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.
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.
pacote com.manning.apisecurityinaction;
importar faísca.*;
importar java.util.*;
importar spark.Spark.* estático;
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); ❸
}
}
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.
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
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):
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
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.
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.
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.
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.
❷ Codifique o resultado com codificação Base64 segura para URL para criar uma
string.
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.*;
@Sobrepor
public String create(Solicitação de solicitação, Token token) {
var tokenId = randomId(); ❷
var attrs = new JSONObject(token.attributes).toString(); ❸
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);
}
@Sobrepor
public void revoke(Solicitação de solicitação, String tokenId) {
database.update("DELETE FROM tokens WHERE token_id = ?", ❺
tokenId); ❺
}
}
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.
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
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.
tokenStore.revoke(solicitação, tokenId);
resposta.status(200);
retornar novo JSONObject();
}
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:
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 ❷
}
})
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:
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 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.
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:
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.
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.
@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.
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',
❷ Construa um elemento img com o elemento src apontando para um site contro-
lado pelo 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.
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
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
@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)); ❶
}
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.
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.
pacote com.manning.apisecurityinaction.token;
import spark.Request;
importar javax.crypto.Mac;
import java.nio.charset.StandardCharsets;
importar java.security.*;
importar java.util.*;
@Sobrepor
public String create(Solicitação de solicitação, Token token) {
var tokenId = delegado.create(pedido, token); ❷
var tag = hmac(tokenId); ❷
@Sobrepor
public Opcional<Token> read(Request request, String tokenId) {
return Opcional.vazio(); // A ser escrito
}
}
❷ Chame o TokenStore real para gerar o ID do token e use o HMAC para calcular a
tag.
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.
@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();
}
GERANDO A CHAVE
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:
EXPERIMENTANDO
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
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)
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.