JSON Web Token 🔑

Um JSON Web Token (JWT) é um padrão para a criação de tokens, sequências de caracteres normalmente criptografadas, capazes de transportar dados no formato JSON. A principal utilização desse padrão se da na geração de tokens para controlar o acesso aos métodos de serviços. Do ponto de vista prático, um JWT é uma String codificada que possui três trechos separados por um ponto (.): cabeçalho, carga (payload) de declarações (claims) e assinatura do JWT.

O cabeçalho normalmente contém duas informações, o tipo do token (nesse caso JWT) e o algoritmo de assinatura que está sendo utilizado, como por exemplo, HMAC, SHA256 ou RSA. A carga (payload) de declarações (claims) é a segunda parte de um token. As declarações são dados específicos do sistema em questão, como por exemplo, declarações sobre um usuário, nome, e-mail, papel (role), entre outros. Finalmente, a assinatura se constitui como a terceira parte de um JWT, trata-se da concatenação de hashes gerados a partir do cabeçalho e da carga com o objetivo de garantir a integridade do token.

💡 Para saber mais e também conseguir visualizar as três partes de um JWT de forma prática visite o site jwt.io e assista ao vídeo. Além disso, existe um segundo vídeo que compara, por meio de analogias, os métodos de autenticação por sessão e token (se necessitar, coloque as legendas em português e assista aos vídeos pausadamente).

Como funciona? 🤔

A Figura 1 ilustra o funcionamento básico de um JWT. Inicialmente, o cliente envia suas credenciais para o servidor, que por sua vez, verifica as credenciais e gera um JWT. O JWT é enviado de volta ao cliente, que o envia em cada requisição subsequente. O servidor verifica a assinatura do JWT para autorizar as solicitações.

Funcionamento do JWT
Figura 1 - Funcionamento básico de um JWT.

Por que utilizar JWT? 🤔

  • Segurança: o JWT é um padrão seguro e amplamente utilizado para autenticação e autorização de usuários.

  • Escalabilidade: o JWT é um padrão leve e eficiente, que pode ser facilmente integrado em qualquer aplicação.

  • Interoperabilidade: o JWT é um padrão aberto e amplamente suportado por diversas linguagens de programação e frameworks.

  • Flexibilidade: o JWT permite a inclusão de informações adicionais no token, como por exemplo, o nome do usuário, o papel (role) e outras informações específicas da aplicação.

Formato de um JWT 📝

Um JWT é uma String codificada que possui três partes separadas por um ponto (.): cabeçalho, carga (payload) de declarações (claims) e assinatura do JWT.

Um JWT tem o seguinte formato:

    xxxxx.yyyyy.zzzzz
  • Cabeçalho: contém duas informações, o tipo do token (nesse caso JWT) e o algoritmo de assinatura que está sendo utilizado.

  • Carga (payload) de declarações (claims): contém informações específicas do sistema em questão, como por exemplo, declarações sobre um usuário, nome, e-mail, papel (role), entre outros.

  • Assinatura: é a concatenação de hashes gerados a partir do cabeçalho e da carga com o objetivo de garantir a integridade do token.

Uma dica para visualizar as três partes de um JWT de forma prática é visitar o site jwt.io.

Como implementar no Quarkus? 🤓

Para criar um serviço no Quarkus com suporte ao JWT necessitamos de duas extensões smallrye-jwt e smallrye-jwt-build, por exemplo:

mvn io.quarkus.platform:quarkus-maven-plugin:2.5.1.Final:create \
    -DprojectGroupId=dev.rpmhub \
    -DprojectArtifactId=jwt \
    -DclassName="dev.rpmhub.TokenSecuredResource" \
    -Dpath="/secured" \
    -Dextensions="resteasy,resteasy-jackson,smallrye-jwt,smallrye-jwt-build"
  • smallrye-jwt: fornece suporte para a validação de tokens JWT.
  • smallrye-jwt-build: fornece suporte para a construção de tokens JWT.

Chaves públicas e privadas 🔐

Inicialmente é necessário gerar um par de chaves pública e privada para poder assinar e validar os tokens. Para isso, podemos utilizar o OpenSSL, que forneces um conjunto de ferramentas de código aberto para criptografia. No caso do JWT, a assinatura é feita por meio de chaves assimétricas, ou seja, a chave privada é utilizada para assinar o token e a chave pública é utilizada para validar a assinatura.

💡 Veja o vídeo para entender mais sobre criptografia assimétrica.

# Para criar uma chave privada
openssl genrsa -out rsaPrivateKey.pem 2048

# Converter a chave privada para o formato PKCS#8
openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out privateKey.pem

# Para criar uma chave pública
openssl rsa -pubout -in rsaPrivateKey.pem -out publicKey.pem

Se você tiver dificuldades para gerar as chaves, por favor, baixe as chaves de exemplo: privateKey.pem e publicKey.pem.

Atualmente o JWT suporta chaves no formato:

  • Public Key Cryptography Standards #8 (PKCS#8) PEM
  • JSON Web Key (JWK)
  • JSON Web Key Set (JWKS)
  • JSON Web Key (JWK) Base64 URL encoded
  • JSON Web Key Set (JWKS) Base64 URL encoded

Depois de gerar as chaves, devemos indicar a chave privada por meio da propriedade smallrye.jwt.sign.key.location no arquivo de application.properties, veja o exemplo abaixo:

    smallrye.jwt.sign.key.location=privateKey.pem

Gerando um JSON Web Token (JWT) 🏭

Depois de criarmos e configurarmos as chaves, podemos escrever um código para gerar um JWT. Como visto anteriormente, um JWT nada mais é que uma String codificada que possui três: cabeçalho, carga (payload) de declarações (claims) e assinatura. Para gerar e assinar um token podemos utiliza a classe io.smallrye.jwt.build.Jwt, veja um exemplo:

@POST
@Path("/getJwt")
@PermitAll
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
public String generate(final String fullName, final String email) {
    return Jwt.issuer(ISSUER)
            .upn(email)
            .groups(new HashSet<>(Arrays.asList("User", "Admin")))
            .claim(Claims.full_name, fullName)
            .sign();
}

No exemplo acima o token é construído por meio do método issuer, o assunto ou usuário (upn), os papeis do usuário (groups) e um conjunto de propriedades específicas da aplicação (Claim). Note, o método sign é utilizado no final da criação do token para assinar e efetivamente construir o token.

🚨 Note que o método do exemplo acima utiliza a anotação @PermitAll para liberar o acesso ao método.

Restringindo o Acesso 🚪

Para restringir o acesso a um método devemos utilizar a anotação @RolesAllowed. Logo, temos que informar quais são as roles que poderão acessar aquele método, observe o exemplo abaixo:

@Inject
@RestClient
IPayment paymentService;

@POST
@Path("/buy")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed("User")
public Invoice buy(@FormParam("cardNumber") String cardNumber,
                        @FormParam("value") String value){
    logger.info("Confirms the payment");

    return paymentService.pay(cardNumber, value);
}

No exemplo, podemos também observar que as informações contidas no token podem ser recuperadas por intermédio da anotação @Claim. Além disso, o método buy foi decorado com a anotação @RolesAllowed({ "User" }), assim, o método está estrito para requisições que encaminhem tokens que contenham o papel “User”. Apesar do exemplo não mostrar, também é possível injetar o token diretamente por meio de um objeto da classe org.eclipse.microprofile.jwt.JsonWebToken que, por sua vez, possui métodos para você recuperar informações sobre o token, como por exemplo, o nome de um usuário: token.getName().

💡 Para saber mais sobre recuperação de informações de um JWT acesse: Using the JsonWebToken and Claim Injection

Validando um JWT

Quando um serviço deseja validar um token, ele deve saber quem é o emissor (Issuer) do JWT. Assim, no Quarkus/Microprofile devemos que adicionar nos serviços que recebem os tokens duas configurações no arquivo application.properties: (1) mp.jwt.verify.issuer - que indica a url do emissor do token e (2) mp.jwt.verify.publickey.location - que indica a chave pública, veja o exemplo abaixo:

    mp.jwt.verify.issuer=http://localhost:8080
    mp.jwt.verify.publickey.location=publicKey.pem

🚨 Uma observação importante, no caso de desenvolvimento de um serviço nativo (GraalVM) a propriedade mp.jwt.verify.publickey.location deve ser substituída por quarkus.native.resources.includes=publicKey.pem.

Sign e Encrypt

Quando o payload (claims) possuir dados sensíveis, como por exemplo, um número de cartão de crédito, é recomendável criptografar o JWT. Neste caso, o JWT pode assinado e criptografado, o que garante a integridade e a confidencialidade, por meio dos métodos innerSign() e encrypt(). O método innerSign() é utilizado para assinar o token e o método encrypt() é usado para criptografar o token. Observem o exemplo abaixo:

@POST
@Path("/getJwt")
@PermitAll
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
public String generate(final String fullName) {
    return Jwt.issuer("http://localhost:8080")
        .upn("rodrigo@rpmhub.dev")
        .groups(new HashSet<>(Arrays.asList("User", "Admin")))
        .claim(Claims.full_name, fullName)
        .innerSign()
        .encrypt();
}

Para gerar um JWT com esses métodos innerSign() e encrypt() se faz necessário configurar o Quarkus com as seguintes propriedades:

smallrye.jwt.sign.key.location=privateKey.pem
smallrye.jwt.encrypt.key.location=publicKey.pem

Por outro lado, para poder validar o JWT e também descriptografar:

mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.decrypt.key.location=privateKey.pem

No momento que você configura o Quarkus com essas propriedades, o JWT é gerado com a assinatura e criptografia. Por outro lado, o Quarkus, por meio das propriedades mp.jwt.verify.publickey.location e mp.jwt.decrypt.key.location, consegue validar e descriptografar o token.

🚨 Para saber mais detalhes, sobre esse processo de assinatura e criptografia, por favor acesse: https://smallrye.io/docs/smallrye-jwt/generate-jwt.html

Propagação de JWT 🔌

Em uma arquitetura de micro serviços, é bastante comum que necessitemos propagar os tokens entre os serviços de maneira automática. Para fazermos isso no Quarkus inicialmente temos que adicionar a extensão quarkus-oidc-token-propagation no arquivo pom.xml. Em seguida, devemos anotar o Rest Client com @AccessToken, pois, isto irá permitir que os Rest Clients reencaminhe os tokens recebidos de um serviço para o outro. Veja o exemplo abaixo:

@RegisterRestClient(baseUri = "https://localhost:8445/payment")
@AccessToken
public interface IPayment {

Hyper Text Transfer Protocol Secure (HTTPS)

Um dos problemas do JWT é que o token pode ser capturado, nesse caso, se faz necessário utilizar Hyper Text Transfer Protocol Secure (HTTPS) para fazer com que o JWT trafegue sempre numa conexão criptografada. Assim, pare gerar uma chave privada e um certificado utilize o comando:

    keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -storepass password -validity 365 -keysize 2048

Se você tiver dificuldades para gerar o certificado, por favor, baixe o certificado de exemplo.

🚨 Nota, o formato keystore.jks armazena tanto o certificado quanto a sua chave privada.

Para informar o caminho do arquivo keystore.jks adicione a seguinte propriedades do arquivo application.properties do Quarkus:

    quarkus.http.ssl-port=8443
    quarkus.http.ssl.certificate.key-store-file=keystore.jks
    quarkus.http.ssl.certificate.key-store-password=password

A propriedade quarkus.http.ssl-port é utilizada para informar a porta que o serviço irá escutar as requisições HTTPS que por padrão é a porta 8443. Para acessar o serviço utilize o endereço https://localhost:8443. A propriedade quarkus.http.ssl.certificate.key-store-file é utilizada para informar o caminho do arquivo keystore.jks e a propriedade quarkus.http.ssl.certificate.key-store-password é utilizada para informar a senha do arquivo keystore.jks, que foi informada no momento da criação do certificado.

🚨 Nota, quando você estiver utilizando Rest Client se faz necessário utilizar a propriedade quarkus.tls.trust-all para que o cliente confie em certificados não homologados por uma unidade certificadora. Assim, adicione a seguinte linha no arquivo de properties do serviço que utiliza um Rest Client:

    quarkus.tls.trust-all=true

Exemplo de código 🖥️

O código do exemplo abaixo, ilustra um trecho de uma arquitetura de micro serviços para um sistema de comércio eletrônico. Nesse caso, temos um serviço de “Users”, que é responsável por gerar um token JWT, e dois serviços, “Chekout” e “Payment”, que são protegidos por esse token. Como exemplo, o diagrama de componentes da Figura 2 ilustra os serviços e suas relações.

Exemplo de arquitetura de micro serviços
Figura 2 - Exemplo de arquitetura de micro serviços.

O JWT do exemplo é utilizado para proteger os métodos dos serviços dos serviços “Checkout” e “Payment”. Desta maneira, é necessário se obter um token por meio do serviço de “Users” para depois conseguir acessar os demais serviços. Para baixar o código desse exemplo utilize os seguintes comandos:

git clone -b dev https://github.com/rodrigoprestesmachado/pw2
cd pw2/exemplos/store

Exercício Prático 🏋️

Com base no exercício anterior, sobre da arquitetura de micro serviços para uma rede social de empréstimo de livros, adicione a segurança por meio de JWT. Para isso, crie um serviço “Users” que seja responsável por gerar um tokens JWT.

Referências 📚

Rodrigo Prestes Machado
CC BY 4.0 DEED

Copyright © 2024 RPM Hub. Distributed by CC-BY-4.0 license.