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.
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.
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 📚
-
Usando JWT RBAC. Disponível em: https://quarkus.io/guides/security-jwt
-
Alex Soto Bueno; Jason Porter; Quarkus Cookbook: Kubernetes-Optimized Java Solutions. Editora: O’Reilly Media, 2020.
CC BY 4.0 DEED