Protegendo sua API Rest via Shared Key Authentication

Atualmente todos nós usamos a internet diariamente e com muita frequência. Seja para fins profissionais, acadêmicos ou pessoais. E na web existem diversos sites como Dropbox, GitHub, PayPal, Slack, SlideShare e LinkedIn, que nos fornecem serviços para resolver problemas comuns do dia a dia.

A maioria desses sites fornece também uma API Rest, para que desenvolvedores possam interagir com os serviços e recursos disponibilizados, e até mesmo desenvolver novos serviços baseados nestes.

Mas ao criar um serviço web e disponibilizá-lo por meio de uma API Rest, devemos ter uma preocupação especial em relação a segurança. Afinal, os clientes do nosso serviço esperam que seus recursos estejam protegidos e não sejam acessados e/ou modificados por terceiros não autorizados.

Como fazer então para proteger uma API Rest?

Uma primeira abordagem seria aplicar o conceito de autenticação, baseado em login/senha, por exemplo. Assim, a cada chamada feita à API Rest, deve-se passar estes dados para que o provedor do serviço verifique se quem está chamando a API realmente está cadastrado como cliente.

E para que um cliente não tenha acesso aos recursos de outro cliente, o provedor pode aplicar o conceito de autorização, restringindo o acesso aos recursos apenas ao seu proprietário ou a algum terceiro que tenha sido autorizado por ele. Nesse caso o protocolo OAuth se encaixa muito bem na maioria dos casos.

Mas apenas autenticação/autorização não basta, ainda mais no ambiente web onde estamos sujeitos a diversos tipos de ataques, sendo um deles o Man In The Middle, onde um hacker intercepta a comunicação entre o cliente e o servidor, e consegue ter acesso às informações sendo trafegadas pela rede, podendo até mesmo modificá-las. Temos que garantir também a Confidencialidade e Integridade das informações.

Para proteger as informações sendo trafegadas pela rede podemos utilizar técnicas como criptografia e assinatura digital, e isto é fácil de conseguir se utilizarmos o protocolo HTTPS juntamente com um certificado digital.

Até aqui, para quem já desenvolve aplicações web, não tem nada de muito novo. Essas mesmas técnicas e ferramentas geralmente também são utilizadas ao desenvolvermos aplicações web.
Mas se engana quem acha que devemos parar por aqui. Existem ainda diversos outros tipos de ataques, sendo outros bem comuns: SQL Injection, Parameter Injection, Cross-site Scripting(XSS) e Cross-site Request Forgery(CSRF).

E mesmo utilizando o HTTPS ainda podemos ter problemas, pois ele não evita o ataque Man In The Middle. Mas como toda requisição é criptografada e assinada digitalmente, o hacker não poderá modificá-la e nem conseguirá entendê-la.

Porém, às vezes podemos ter algum tipo de restrição técnica no projeto que nos impede de utilizar o HTTPS, e com isso devemos ter uma preocupação maior, já que nesse cenário, mesmo utilizando criptografia e assinatura digital, um hacker pode enviar uma réplica de alguma requisição ao servidor.

Esse é mais um tipo de ataque, conhecido como Replay Attack, onde um hacker, ao interceptar as requisições feitas pelo cliente ao servidor, as reenvia uma ou mais vezes, sendo que o servidor as aceita, pois acredita que as mesmas foram originadas pelo cliente legítimo.

Como nos proteger desse tipo de ataque?

Para nos proteger, devemos fazer com que cada requisição seja única. Sendo assim, quando um hacker enviar uma réplica de uma requisição, o servidor deve rejeitá-la, pois detecta que uma requisição idêntica a esta já foi enviada anteriormente.

Uma técnica utilizada para isso é conhecida como Shared Key Authentication. Essa técnica é utilizada por provedores de cloud computing como Amazon e Azure.

A ideia consiste na elaboração de um algoritmo que será responsável por gerar uma identificação única para cada requisição, devendo utilizar para isso informações da própria requisição como URI, Método HTTP e Data. Todos os clientes do serviço devem utilizar esse mesmo algoritmo para a geração da identificação de cada requisição a ser feita ao servidor, e enviá-la como parâmetro para que o servidor faça a validação.

Traduzindo isso para código, seria algo como:

public class GeradorIdentificacaoRequisicao {

    private static final String DELIMITADOR = "\n";
    private final Criptografia criptografia;

    public GeradorIdentificacaoRequisicao(Criptografia criptografia) {
        this.criptografia = criptografia;
    }

    public String geraIdDaRequisicao(HttpRequest request) {
        String uri = request.getURI();
        HttpMethod metodo = request.getMethod();
        LocalDateTime data = LocalDateTime.now();

        String id = uri + DELIMITADOR + metodo + DELIMITADOR + data;
        return criptografia.criptografa(id);
    }

}

Repare que no código anterior concatenamos as informações da requisição, utilizando o /n como delimitador, para gerar o id dela. Além disso, o id da requisição que foi gerado não é devolvido em texto puro, mas sim criptografado utilizando algum algoritmo de criptografia qualquer, como SHA2. Esse id deve ser enviado como parâmetro da requisição e o servidor utilizará o mesmo algoritmo para gerá-lo novamente, e então fará a comparação para identificar se a requisição é válida.

Devemos ainda melhorar o algoritmo, pois no momento ele pode gerar id iguais para clientes distintos. Como todos os clientes utilizarão o mesmo algoritmo, se dois ou mais deles fizerem uma requisição para a mesma URI, na mesma hora e utilizando o mesmo método HTTP, os ids gerados para ambas as requisições serão idênticos.

É aqui que entra a ideia do Shared Key. Nosso algoritmo precisa ser alterado para levar mais uma informação em consideração ao gerar o id da requisição: a chave compartilhada do cliente. Essa chave é na verdade uma senha gerada pelo servidor para o cliente. Cada cliente tem a sua chave distinta, e ela deve ser compartilhada apenas entre o servidor e o cliente. Adaptando nosso código anterior:

public class GeradorIdentificacaoRequisicao {

    private final Criptografia criptografia;
    private static final String DELIMITADOR = "\n";

    public GeradorIdentificacaoRequisicao(Criptografia criptografia) {
        this.criptografia = criptografia;
    }

    public String geraIdDaRequisicao(HttpRequest request, String sharedKey) {
        String uri = request.getURI();
        HttpMethod metodo = request.getMethod();
        LocalDateTime data = LocalDateTime.now();

        String id = uri + DELIMITADOR + metodo + DELIMITADOR + data + DELIMITADOR + sharedKey;
        return criptografia.criptografa(id);
    }

}

Agora duas requisições iguais, mas originadas de clientes distintos, produzirão ids distintos. Nesse caso o cliente precisa também enviar na requisição o seu login como parâmetro, para que o servidor possa recuperar sua chave compartilhada ao receber a requisição, e com isso consiga utilizar o algoritmo para gerar o id da requisição e assim validá-lo.

Existem outros detalhes para se melhorar ainda mais a segurança ao utilizar essa técnica, mas a ideia desse post é apenas mostrar o seu funcionamento de forma geral.

Já utilizou essa técnica em algum projeto? Conte-nos como foi a experiência.

25 Comentários

  1. Rafael Ponte 12/08/2015 at 15:22 #

    Oi Rodrigo,

    Muito bacana o post! Apesar de trabalhar há um bom tempo com desenvolvimento Web eu tenho que confessar, minha experiência e conhecimento sobre segurança é bem restrito. Sei o básico do básico! Mas a verdade é que acho essa área muito interessante. E sei que é um assunto muito, mas muito amplo e até complexo em muitos casos.

    Se eu entendi bem, toda requisição enviada ao servidor deverá enviar essa ID gerada, certo? Normalmente essa informação vai onde? No cabeçalho HTTP ou no corpo da requisição mesmo?

    Outro detalhe, se não estou enganado a maioria dos serviços nas nuvens nos fornecem essa Shared Key antes de usarmos a API REST de integração, deles, certo? Entre sistemas de uma empresa, é comum utilizar outro método para obter essa Shared Key? Por exemplo, o cliente envia a 1a requisição para obter a shared key e, só depois, é que o serviço é utilizado.

    Um abraço e parabéns pelo post!

  2. Abner Carleto 12/08/2015 at 17:51 #

    Olá Rodrigo,

    Muito bom o post, mais tenho uma dúvida. No ID da requisição você usa a data e hora em que a requisição foi gerada pelo cliente (LocalDateTime.now()). Como o servidor deve gerar a mesma chave para comparar com a chave enviada pelo cliente, como o servidor irá saber qual a hora exata que o cliente gerou a chave?

    Lembrando que o objeto do tipo LocalDateTime, armazena a data com precisão de milissegundos, e que a requisição nunca chegará ao servidor no mesmo milissegundo que foi enviada.

    https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html

  3. Rodrigo Ferreira 12/08/2015 at 18:35 #

    Oi Rafael,

    Realmente a área de segurança é bem extensa, e o número de tipos de ataque também =D

    O cliente deve gerar o id e enviá-lo em todas as requisições, e geralmente ele é enviado no cabeçalho mesmo.
    Sobre a Shared Key, geralmente os providers de cloud nos fornecem assim que nos cadastramos na plataforma.

    Para sistemas internos acho que seria viável sim usar essa abordagem do cliente obter a Shared Key via 1a requisição. O problema é que a Shared Key vai trafegar pela rede, o que pode ser perigoso 😀

    Valeu! Abraços!

  4. Rodrigo Ferreira 12/08/2015 at 18:41 #

    Oi Abner,

    Essa questão da data realmente é um problema, pois precisaríamos nos certificar de que o cliente e o servidor estão utilizando as mesmas configurações e formatos.

    E por conta disso, o que os providers de cloud fazem é criar uma “janela” de tempo para considerar a requisição válida.
    No Amazon e Azure, por exemplo, essa janela é de 15 minutos.

    E na verdade a data também é enviada como parâmetro no cabeçalho da requisição.

    Abraços!

  5. Paulo Júnior 12/08/2015 at 20:39 #

    Parabéns pelo post, Rodrigo.

    Gostaria de saber se há algum livro sobre segurança para aplicações web que você recomende. Algum que comente sobre esses possíveis ataques que você mencionou no post e mostre como se proteger deles.

    Desde já, obrigado.

  6. Rodrigo Ferreira 13/08/2015 at 09:21 #

    Oi Paulo,

    Pior que não conheço livros sobre essa área =/
    Com certeza tem, eu é que não cheguei a ler nenhum 😀

    Pretendo escrever um próximo post aqui no blog falando sobre esses ataques mais comuns e como se proteger deles. Fique acompanhando o blog 🙂

    Abraços!

  7. Marcus 13/08/2015 at 12:27 #

    Interessante, já usei uma técnica parecida. Quando li sobre o assunto, acabei descobrindo o HMAC, que faz um hash (usando algum outro algoritmo, como SHA2) e adiciona segurança contra a concatenação de dados a uma mensagem pré-existente e contra colisões de hash. Algoritmos de HMAC já vêm com o Java (ver javax.crypto.Mac)

    ps: aquele delimitador “/n” era para ser “/n” mesmo ou deveria ser uma barra invertida?

  8. Rodrigo Ferreira 13/08/2015 at 13:18 #

    Oi Marcus,

    Realmente errei a “barra” no delimitador 😀
    Valeu!

    Abraços!

  9. Paulo Júnior 15/08/2015 at 08:09 #

    Ok Rodrigo, obrigado. Continuarei acompanhando sim.

  10. Raphael Lacerda 19/08/2015 at 14:16 #

    Bom demais Rodrigo!

    Eu tinha lido esse post aqui

    http://www.developerscrappad.com/1814/java/java-ee/rest-jax-rs/java-ee-7-jax-rs-2-0-simple-rest-api-authentication-authorization-with-custom-http-header/

    um esquema fazendo autenticação via http header

    só não consegui ainda colocar pra funcionar

  11. Jocélio Otávio 31/08/2015 at 09:00 #

    Rodrigo, parabéns pelo POST! A classe UUID do java não poderia faciliar a geração da chave única?

    http://docs.oracle.com/javase/7/docs/api/java/util/UUID.html

  12. PH 01/09/2015 at 09:25 #

    Show de bola Rodrigo!

    Estou um pouco envolvido com isso para fazer a autenticação e autorização de um cliente SPA e um servidor de resources independente!

    Realmente esse mundo da segurança é muito extenso!

    Vlw!

  13. Rafael Reynoud 01/09/2015 at 13:29 #

    Para quem estava perguntando a respeito de livros, eu recomento este:
    SOA aplicado – Integrando com web services e além, encontrei na casa do código.
    (http://www.casadocodigo.com.br/products/livro-soa-webservices)
    e se alguém tiver outro para indicar?

  14. Jayr Motta 02/09/2015 at 09:53 #

    Algo que sempre surge nas conversas em assuntos relacionados a esse é que muitos sites, como o da Caixa Econômica Federal, possuem certificados expirados e portanto forçam o usuário a deixar de usar (o que é improvável pois as pessoas PRECISAM de seus serviços bancários) ou então aceitar certificados expirados dizendo isso explicitamente nos navegadores.

    Isso cria uma cultura de aceitar certificados expirados, inválidos e suspeitos que abrem brechas para ataques Man in The Middle até mesmo de serviços que se comunicam via HTTPS. Qual sua visão a respeito do assunto?

    Em um projeto que trabalhei recentemente implementei o OAuth2 Token Bearer e achei o conceito bastante seguro pois além de enviar informações a respeito do usuário e um identificador do cliente HTTP (que permite a aplicação do outro lado atender várias sessões de um mesmo usuário em dispositivos diferentes) ele também utiliza o conceito de token rolante que em toda requisição recebe um token, valida o token, gera um novo token e retorna ele no response, isso promove um encadeamento de requisições e respostas que impede replay attacks, csrf entre outras diversas proteções interessantes.

    Dicas de leitura:

    https://github.com/lynndylanhurley/ng-token-auth#identifying-users-on-the-server
    https://tools.ietf.org/html/rfc6750

    Abs

  15. Leandro 17/09/2015 at 17:15 #

    Fiquei com uma dúvida: Você disse que em alguns cenários não podemos utilizar o Https devido a alguma restrição técnica, e por isso devemos ter este cuidado e usar esta solução do seu post. Então, caso eu use o Https eu estou protegido e não preciso usar um shared key authentication? Obrigado.

  16. Rodrigo Ferreira 17/09/2015 at 21:54 #

    Oi @Jocelio,

    O problema de usar a classe UUDI é que o servidor, ao receber a requisição, precisa gerar novamente a chave para verificar se bate com a enviada pelo cliente. Dai não teria como ele gerar a mesma chave que o cliente gerou, já que é totalmente aleatório.

    Abraço!

  17. Rodrigo Ferreira 17/09/2015 at 21:58 #

    Oi @Jayr,

    Acredito que mesmo com certificado expirado, é melhor do que não tê-lo. Mas claro, cada usuário deve ter confiança no site que vai aceitar digitar informações sigilosas com um certificado expirado ou gerado na mão…

    Interessante o OAuth2 Token Bearer, vai na mesma ideia de tornar cada request único, protegendo contra replay attack,,e de quebra contra CSRF.

    Abraço!

  18. Rodrigo Ferreira 17/09/2015 at 22:01 #

    Oi @Leandro,

    Mesmo usando o HTTPS você pode sofrer ataques do tipo Replay Attack.
    Dai a técnica do Shared Key Authentication é para impedir esse tipo de ataque, já que cada requisição será válida apenas uma vez.

    Abraço!

  19. Fernando Frazão 07/10/2015 at 21:19 #

    Olá Rodrigo! Tudo blz.

    O caminho é esse, são várias possibilidades de ataque e tem que se pensar em tudo mesmo!

    O problema é que a implementação não é tão simples. Muito bacana seria se pudéssemos trabalhar (fuçar) com um exemplo.

    Falando nisso, vc possui algum esqueleto para esta solução? Se sim, poderia compartilhá-la?

    Estou tentando implementar um exemplo básico com Spring Security, através de autenticação via Token, mas não consegui lograr êxito ainda (só funciona se o projeto estiver num mesmo contexto, em contextos separados “com a utilização de um filtro CORS para habilitar requisição cruzada” não funciona).

    Abraços e parabéns pelo texto,

    Frazão

  20. Raphael Lacerda 30/10/2015 at 14:17 #

    ótimo artigo Rodrigão.

    Só tive tempo de lê-lo hoje!

    muito bom

  21. Juliano Sena da Silva Carlos 03/04/2016 at 10:31 #

    Bom dia Rodrigo, tudo bom?
    A passagem da data da requisição e do login por parâmetro não daria a chance da cópia da requisição do mesmo jeito? Como faria para que nenhum hacker pudesse copiar essa requisição e reenviá-la novamente?

  22. Walter 29/07/2016 at 11:48 #

    Olá,

    Muito bom esse artigo, parabens! Estou criando um sistema web e logo vou criar o app dele para android e essa dica vai me ajudar mto.
    Já li esse outro artigo http://blog.caelum.com.br/protegendo-sua-aplicacao-web-contra-cross-site-request-forgerycsrf/ e implementei na mão um esquema de sessionToken para o sistema web (Ficou melhor que o do spring security hehehe). Então, fiquei com uma dúvida, já que existe esse problema de sincronia da data/hora cliente-servidor, será que seria uma boa ao invés de usar a data/hora, usar um token diferente para cada requisição para rquisições rest? Pensei em a cada requisição retornar um token diferente, ai na proxima requisição a chave deverá ser uma cripitografia, como a do exemplo, mas ao invés da data/hora, seria o token. Isso seria uma boa? O que acham?

    Obrigado.

Deixe uma resposta