Monday, December 6, 2021

Hello JMS!

Jakarta Messaging - antes conhecida apenas como Java Message Service - (JMS) é a API que especifica como aplicações Java Enterprise enviam e recebem mensagens através de um Message Oriented Middleware (MOM). MOMs são um componente essencial para integração de operações entre sistemas corporativos diferentes.

Mensagens

O conceito de mensagem possui uma definição bastante ampla na computação. No contexto do JMS, mensagens são requisições assincronas ou eventos que são produzidos ou consumidos pelas aplicações. Essas mensagens geralmente contêm informações vitais necessárias para a coordenação entres os diferentes sistemas.

Arquitetura do JMS

Em um alto nível, a arquitetura do JMS consiste nos seguintes componentes:

  • Provider: JMS é apenas uma API, então ela precisa de uma implementação que efetivamente direcione as mensagens, ou seja, o provider, também conhecido como message broker
  • Client: a aplicação que produz ou consome mensagens atraves de algum provider
  • Messages: os objetos que os clientes enviam ou recebem dos providers
  • Administered Objects: objetos disponibilizados pelos brokers ao cliente (conexão e destino)




Os providers habilitam a comunicação assíncrona disponibilizando um destino, que é o lugar onde as mensagens ficam até que sejam consumidas pelo cliente. Com base no destino, há 2 tipos de modelos de  envio de mensagens:
  • Point to Point (P2P): 1 mensagem destinada a um único consumidor
  • Publish-subscribe (pub-sub): 1 mensagem para N consumidores

modelo P2P



modelo pub-sub

Hello JMS

Criemos agora um exemplo de uma aplicação JEE que:
  1.  através de um timer, envia uma mensagem para uma fila chamada HelloQueue a cada 3 segundos
  2. um tópico chamado PurchaseTopic que é observado pelo Operador de Cartão de Crédito e pelo Departamento Financeiro, ou seja, toda vez que uma operação de cartão de crédito chega nesse Tópico, as respectivas áreas são notificadas.

Como servidor de aplicação, usaremos Payara Application Server, que tem como message broker default o OpenMQ instalado na pasta <PAYARA_HOME>/mq/bin

Para iniciar o payara executamos o binário asadmin localizado em

<PAYARA_HOME>/glassfish/bin

depois o comando start-domain

O console administrativo do payara pode ser acessado por padrão na porta 4848




No painel esquerdo, clique em JMS resources, depois em destination resources



Clique em new. Vamos criar um destino do tipo fila chamado HelloQueue. Preencha os campos conforme a imagem abaixo e clique em save



De forma analoga, criamos um tópico chamado PurchaseTopic




Agora temos agora 2 Destination Resources. Seus respectivos JNDI names serão usados por qualquer aplicação que queira se conectar aos mesmos e receber as mensagens que chegarem



Enviando Mensagens para jms/HelloQueue


Vamos chamar de TimedProducer o producer que envia mensagens para jms/HelloQueue a cada 3 segundos.

@Stateless
@LocalBean
public class TimedQueueProducer {

    @Inject
    private JMSContext jmsContext;

    @Resource(lookup = "jms/HelloQueue")
    private Queue queue;

    @Schedule(hour = "*", minute = "*", second = "*/3", info = "Every 3 seconds", timezone = "UTC", persistent = false)
    public void sendToQueue() {

        TextMessage textMessage = jmsContext.createTextMessage("New Text Message");
        JMSProducer producer = jmsContext.createProducer();

        producer.send(queue, textMessage);
    }
}

TimedProducer é um EJB, o método sendToQueue é anotado com @Schedule cujos parâmetros configuram para que seja chamado a cada 3 segundos. Esse método cria uma mensagem do tipo texto e envia para a queue.

Depois criamos uma classe que consome cada mensagem enviada para HelloQueue. Chamamos de QueueReceiver.

@MessageDriven(activationConfig = {
        @ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/HelloQueue")
})
public class QueueReceiver implements MessageListener {

    private final static Logger LOG = Logger.getLogger(QueueReceiver.class.getName());

    @Override
    public void onMessage(Message message) {

        TextMessage textMessage = (TextMessage) message;
        try {
            LOG.info(">>> received: " + textMessage.getText());
        }
        catch (JMSException e) {
            e.printStackTrace();
        }
    }
}

QueueReceiver é anotada com @MessageDriven, o que a torna um consumidor assíncrono, isto é, o método onMessage é chamado pelo container sempre quando uma nova mensagem chega na fila especificada. 

Quando a mensagem chega, ela é registrada nos logs do payara, localizado em <PAYARA_HOME>/glassfish/domain/domain1logs/server.log



Enviando Mensagens para jms/PurchaseTopic


Nesse próximo exemplo, criamos uma servlet simulando o envio de dados de um cartão de crédito para o tópico chamado jms/PruchaseTopic o qual é observado pelo microserviço do Departamento financeiro e pela operadora de cartão (nesse exemplo são 2 classes dentro do projeto, mas elas poderiam estão em outra aplicação, em outro servidor).

@WebServlet(urlPatterns = "/hello")
public class ViewServelt extends HttpServlet {

    @EJB
    private TopicProducerService topicProducer;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        String cvv = req.getParameter("cvv");
        String number = req.getParameter("number");

        topicProducer.sendMessage(new CreditCard(cvv, number));

        resp.getWriter().println("<h1<Credit Card sent to the card operator topic...</h<");
    }
}

A servlet recebe no path os parametros CVV e number e em seguida os envia para o TopicProducer,  o qual por sua vez publica os dados do cartão no tópico o que faz com que o provider notifique todas  as aplicações envolvidas, como no exemplo:

@Stateless
@LocalBean
public class TopicProducerService {

    @Inject
    private JMSContext context;

    @Resource(lookup = "jms/PurchaseTopic")
    private Topic helloTopic;

    public void sendMessage(CreditCard creditCard) {

        JMSProducer producer = context.createProducer();
        producer.send(helloTopic, creditCard);
    }
}

As classes CardOperator e FinancialDepartament observam o tópico PurchaseTopic e logam os dados do cartão toda vez que eles chegam no tópico:

@MessageDriven(activationConfig = {
        @ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/PurchaseTopic")
})
public class CardOperator implements MessageListener {

    private static final Logger LOG = Logger.getLogger(CardOperator.class.getName());

    @Override
    public void onMessage(Message message) {

        try {
            CreditCard cc = message.getBody(CreditCard.class);
            LOG.info(String.format("Received cc: %s", cc ));
        }
        catch (JMSException e) {
            e.printStackTrace();
        }

    }
}

E a classe FinancialDepartment:

@MessageDriven(activationConfig = {
        @ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/PurchaseTopic")
})
public class FinancialDepartament implements MessageListener {

    private static final Logger LOG = Logger.getLogger(FinancialDepartament.class.getName());

    @Override
    public void onMessage(Message message) {

        try {
            CreditCard cc = message.getBody(CreditCard.class);
            LOG.info(String.format("Received cc: %s", cc ));
        }
        catch (JMSException e) {
            e.printStackTrace();
        }

    }
}

Ao chamarmos a url da servlet, os dados do cartão são registrados nos logs pelos consumidores do tópico

http://localhost:8080/hello-jms/hello?cvv=789&number=333333333333



o consumo dos tópicos é registrado nos logs



O código completo se encontra no github.



       

Friday, May 28, 2021

Hello, Java + Kubernetes!

Nesse artigo  fizemos um Hello World de Docker com Java e abordamos os conceitos de imagens e containers. Entendemos que aplicações na forma de containers possuem caracteristicas peculiares que possibilitam sua replicação e distribuição entre os diferentes ambientes com muito mais agilidade. Porém elas também apresentam novos desafios.

Containers no mundo real

Com o comando docker run podemos criar e iniciar um novo container e nossa aplicação está no ar. Porém temos apenas uma única instancia da aplicação em um único docker host. O que acontece se:

  • o número de usuários crescer acima da capacidade suportada pela aplicação?
  • precisarmos monitorar o estado de dezenas ou centenas de containers?
  • um ou vários containers param por qualquer motivo?
  • o host no qual rodam os containers pára?
Esses são apenas alguns dos desafios introduzidos pelo uso massivo de aplicações em containers.

Essas questões são específicas de um problema mais genérico conhecido como Orquestração de Containers.

Neste artigo exploramos uma das soluções mais usadas para o problema da Orquestração de Container, que é o Kubernetes. Criamos uma aplicação Java e vamos containerizá-la e em seguida replicar o container via kubernetes.

Kubernetes (K8)

Aplicação que iremos conteinerizar já está pronta. É a que usamos nesse artigo que apresenta uma introdução à autenticação JWT em Java. O que a aplicação faz não é revelevante por agora. O que importa é que ela já possui uma suite testes de integração automatizados apontando para localhost

Após conteinerizá-la e replicar os containers, vamos rodar os testes apontando para o load-balancer do cluster kubernetes, o qual irá distribuir a carga entre as réplicas criadas.

Para realizar esse teste precisamos instalar o minikube (utilitário que simula um cluster na sua máquina) com o seguinte comando:


curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube

Agora instalamos o utilitário kubectl que permite gerencial o cluster kubernetes por linha de comando:


$ curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
$ sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
$ kubectl version --client

O primeiro passo para conteinerizar a aplicação é criar seu Dockerfile, no qual definimos todo o script para seu build e deploy. 

Nosso Dockerfile começa declarando que o build começa em uma imagem que possui o JDK 11 e o maven instalados:

  • copiamos a pasta /src e o arquivo pom.xml para uma imagem que já contenha JDK 11 e Maven configurados (maven:3.6.3-openjdk-11-slim)
  • buildamos a aplicação dentro da imagem com mvn clean package (esse passo gera o .war)
  • copiamos o artefato gerado (.war) no passo anterior para uma outra imagem jboss/wildfly e descartamos a imagem do Maven
  • expomos a porta 8080 do container jboss/wildfly

FROM maven:3.6.3-openjdk-11-slim AS Builder
LABEL key="https://finalexception.blogspot.com"

COPY ./pom.xml /app/pom.xml
COPY ./src /app/src
WORKDIR /app
RUN mvn clean package

FROM jboss/wildfly
EXPOSE 8080
COPY --from=Builder /app/target/hello-authentication.war /opt/jboss/wildfly/standalone/deployments/

Com o Dockerfile pronto, criamos a imagem:


$ docker build -t rafaelnasc1mento/hello-authentication:latest .

Em seguida publico a imagem em alguma conta docker-hub com o padrão <prefixo-conta>/hello-authentication:latest:

$ docker login
$ docker push rafaelnasc1mento/hello-authentication:latest

Agora que temos a imagem devidamente publicada no hub, é hora do kubernetes entrar em ação.

Deployment.yaml

Este arquivo contem a descrição de como nosso cluster irá operar e qual é o estado desejado do mesmo:

  1. Nosso serviço se chama hello-authentication-service
  2. Queremos um Load-Balancer para distribuir a carga entre os containers
  3. Queremos 3 réplicas do container
Como os containers não poderão ser acessados diretamente, o Load-Balancer expõe a porta 30001 para a qual serão encaminhados os requests, também chamada de nodePort

Como o container expoe a porta 8080, então definimos a targetPort para 8080, assim o Load-Balancer encaminhará as requisições que chegam até ele para a porta 8080 de algumas das réplicas. Abaixo o script completo:

---
kind: Service
apiVersion: v1
metadata:
  name: hello-authentication-service
spec:
  selector:
    app: hello-authentication
  ports:
    - protocol: "TCP"
      # port accessible inside cluster
      port: 8081
      # port to forward to inside pod
      targetPort: 8080
      # port accessible outside the cluster
      # whenever I hit this port, it will forward the request to the 'targetPort', which is the port to forward to the pod specified in 'selector.app'
      nodePort: 30001
  type: LoadBalancer

---
apiVersion: apps/v1
# a deployment defines the desired state of our application
kind: Deployment
metadata:
  name: hello-authentication-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      # name that's refereed by the load-balancer or other components inside the cluster
      app: hello-authentication
      tier: backend
  template:
    metadata:
      labels:
        app: hello-authentication
        tier: backend
    spec:
      containers:
        # container name
        - name: hello-authentication
          # docker image at docker-hub
          image: rafaelnasc1mento/hello-authentication:1.0
          ports:
            - containerPort: 8080


Isso é tudo! Para subirmos o cluster basta iniciar o minikube e criar o deployment:


$ minikube start
$ kubectl create -f deployment.yml

Use apply ao invez de create se o cluster já existe e você quer apenas atualizar alguma diretriz, editando o deployment.yaml.

Nesse ponto nosso cluster minimamente resiliente com 3 réplicas do container Hello-Authentication e um load balancer está totalmente operacional. Para confirmar, execute:

$ kubectl get pods


PODs são a unidade básica de deploy em um cluster. Neste caso podemos ver 3 instâncias de cada um dos containers.

Conforme dito, o Load Balancer é a porta de entrada dos serviços, umas vez que os containers não ficam expostos diretamente ao mundo exterior. Para descobrir o IP do Load Balancer execute:

$ minikube ip


Pelo output do comando, podemos ver que o IP do seriço Load Balancer, através do qual acessamos o cluster é 192.168.49.2.


Testes Automatizados

No pacote de testes do projeto Hello Atuhtentication, temos a classe Constants.java e nela o campo BASE_URI. Vamos colocar o IP do minikube como valor desse campo:


Agora, ao rodar os testes, as requisições serão direcionadas para o IP do cluster 192.168.49.2 na porta 30001. Depois da porta, colocamos a url dos serviços conforme definido na aplicação java.

 Clique com o botão direito na classe ServiceApiTest.java para rodar os testes:



Podemos ver que as replicas recebem as requisições normalmente.

Outro recurso interessante do minikube é o dashborad. Execute:

$ minikube addons enable metrics-server

$ minikube dashboard




O dashboard é uma interface web de administração que te permite ter uma visão geral do cluster, podemos inclusive remover ou adicionar mais containers, ver o consumo de CPU e memória em cada réplica e muito mais!


       

Saturday, April 17, 2021

Wildfly: Habilitando HTTPS para suas Aplicações 2

Após habilitarmos e configurarmos o TLS no Wildfly, qualquer endpoint que publicarmos nesse servidor poderá ser acessado via HTTPS. Para tanto, a aplicação cliente precisa baixar o certificado do servidor, o qual está disponível em https://localhost:8443/.


Testando o acesso

Com o TLS configurado, vamos publicar um endpoint. Em seguida criar um cliente que tenta acessar esse endpoint com e sem o certificado.

O endpoint GET terá a seguinte interface: /service/{name} e retorna um Hello name!!!


@Path("/service")
@Produces(TEXT_PLAIN)
public interface Services {

    @GET
    @Path("/{name}")
    public String getMessage(@PathParam("name") String name);


Após fazer o deploy deste serviço no Wildfly, realizamos o teste. Com JUnit 5, criamos uma classe chamada TlsTest que envia uma requisição ao ao endpoint. Nesse teste, para fazer a requisição, usamos a recente API HttpClient nativa do Java introduzida na versão 11:


public class TlsTest {
    @Test
    public void shouldSuccessOverTLS() throws IOException, InterruptedException {

        HttpClient httpClient = HttpClient.newBuilder().version(HTTP_1_1).build();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://localhost:8443/hello-tls/api/service/Rafael"))
                .header("Content-Type", "application/json")
                .GET()
                .build();

        HttpResponse<String> resp = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        Assertions.assertEquals(resp.statusCode(), HTTP_OK);

}

Como ainda não adicionamos o certificado do servidor ao TrustStore do cliente, recebemos o seguinte erro ao rodar o teste:


java.io.IOException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

	at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:565)
	at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:119)
	at tls.TlsTest.shouldSuccessOverTLS(TlsTest.java:38)
    ...
Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target


SSLHandshakeException indica que o cliente e o servidor não chegaram a um acordo sobre o nível de segurança desejado, e portanto a conexão será abandonada. Neste caso específico, a JVM não encontrou no TrustStore default ($JAVA_HOME/lib/security/cacerts) o certificado apresentado pelo host localhost:8443.

Então temos que baixar o certificado do servidor e adicioná-lo ao TrustStore do cliente. Baixe o certificado conforme descrito na parte 1

Vamos criar uma classe chamada CertificateLoader que carrega uma nova TrustStore para o cliente e nela adiciona o certficado de localhost. Criamos o metodo loadCertificate() passando como parâmetros o caminho do certificado e o caminho desejado do novo TrsutStore que, caso não exista, será criado:


public class CertificateLoader {

    static void loadCertificate(Path serverCertificatePath, Path clientTrustStorePath) throws Exception {

        if (!Files.exists(clientTrustStorePath))
            createClientTrustStore(clientTrustStorePath);

        String alias = "wildfly23.localhost";
        String password = "changeit";

        // to load a new truststore other than default cacerts
        KeyStore clientTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        InputStream in = Files.newInputStream(clientTrustStorePath);
        clientTrustStore.load(in, password.toCharArray());
        in.close();

        // CertficateFactory to create a new reference to the server certificate file
        CertificateFactory cf = CertificateFactory.getInstance("X.509");

        // read the server certificate
        InputStream serverCertstream = Files.newInputStream(serverCertificatePath);

        // certificate instance
        Certificate serverCertificate =  cf.generateCertificate(serverCertstream);

        // add the server certificate to our newly truststore
        clientTrustStore.setCertificateEntry(alias, serverCertificate);

        // save modifications
        OutputStream out = Files.newOutputStream(clientTrustStorePath);
        clientTrustStore.store(out, password.toCharArray());
        out.close();

        // dynamically set default truststore for this application from cacerts to newly client.truststore
        System.setProperty("javax.net.ssl.trustStore", clientTrustStorePath.toString());
        System.setProperty("javax.net.ssl.trustStorePassword", password);
    }


E modificamos a nossa classe de teste para carregar o certificado antes da execução do teste:


public class TlsTest {

    private static final Path CLIENT_TRUST_STORE = Paths.get("/home/rafael/Library/Practice/_02_httpsLocalHost/client.truststore");
    private static final Path LOCALHOST_CERTIFICATE = Paths.get("/home/rafael/Library/Practice/_02_httpsLocalHost/localhost");

    @BeforeAll
    public static void init() throws Exception {

        CertificateLoader.loadCertificate(LOCALHOST_CERTIFICATE, CLIENT_TRUST_STORE);
    }

    @Test
    public void shouldSuccessOverTLS() throws IOException, InterruptedException {

        HttpClient httpClient = HttpClient.newBuilder().version(HTTP_1_1).build();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://localhost:8443/hello-tls/api/service/Rafael"))
                .header("Content-Type", "application/json")
                .GET()
                .build();

        HttpResponse<String> resp = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        Assertions.assertEquals(resp.statusCode(), HTTP_OK);
    }
}

Agora sim podemos rodar o teste, e a requisição será bem sucedida uma vez que o certificado do servidor foi adicionado ao TrustStore do cliente:


O código completo do cliente e do endpoint encontram-se no gitHub.



Monday, March 29, 2021

Autorização JWT Puro Java: parte 2

Na parte 1 delineamos o fluxo geral que as API que realizam autenticação utilizando JSON Web Token (JWT) deve seguir. Podem haver variações, mas o fluxo básico é aquele.

Neste artigo colocamos em prática o fluxo criando uma API com os seguintes serviços:


Nossa solução utiliza somente a JAX-RS 2.0 API, evitando completamente qualquer solução específica de terceiros, assim sendo, ela deve funcionar qualquer que seja a implementação JAX-RS utilizada (Jersey, Resteasy, Apache CXF, etc).


Projeto

O projeto chamado HelloAuthentication possui a seguinte estrutura:


Em primeiro lugar criamos uma anotação Name Binding chamada @Secured. Name Binding é um decorator para algum interceptor (neste caso um filtro de requests) de modo que todos os endpoints que forem marcados com @Secured terão seu acesso interceptado pelo filtro para checagens de segurança.


@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured {

}

Conforme destacado em azul na estrutura do projeto, 3 classes são protagonistas nesse cenário:

1 AuthenticationFilter

Perceba que ela é decorada com a anotação que criamos @Secured:


@Secured
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {

    static final Logger LOG = Logger.getLogger(AuthenticationFilter.class.getName());
    private static final String SCOPE = "scope";
    private static final String AUTHENTICATION_SCHEME = "Bearer";

    @Override
    public void filter(ContainerRequestContext requestContext) {

        LOG.info("authentication filter");

        String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);

        if (!isTokenBasedAuthentication(authorizationHeader)) {
            LOG.info("Not a token based authentication! Aborting request...");
            abortWithUnauthorized(requestContext);
            return;
        }

        try {
            validateToken(authorizationHeader);
        }
        catch (Exception e) {
            e.printStackTrace();
            abortWithUnauthorized(requestContext);
        }
    }

    private void validateToken(String authorizationHeader) throws Exception {

        LOG.info("Validating token...");

        String accessToken = authorizationHeader.substring(7);
        JWT jwt = new JWT(accessToken);

        if (!TokenValidation.isValid(jwt))
            throw new Exception("Invalid JWT");

    }

    private void abortWithUnauthorized(ContainerRequestContext requestContext) {
        requestContext.abortWith(
                    Response.status(HttpStatus.SC_UNAUTHORIZED)
                            .header(HttpHeaders.WWW_AUTHENTICATE,
                                                    AUTHENTICATION_SCHEME + " scope = " + SCOPE)
                            .build());
    }

    private boolean isTokenBasedAuthentication(String authorizationHeader) {

        if (authorizationHeader == null)
            return false;

        return authorizationHeader.startsWith(AUTHENTICATION_SCHEME + " ");
    }
}

Toda vez que algum endpoint anotado com @Secured for chamdo, antes, o método filter() será executado. Dentro dele faremos a checagem do token que deve estar contido dentro do header Authorization do request. 

Após a checagem, podemos decidir liberar o request ou abortá-lo.

2 Validação do Token

Verifica se o token não está expirado. Verifica a assinatura do token. 

A verificação do token não requer acesso ao banco de dados.


public class TokenValidation {

    public static boolean isValid(JWT jwt) {

        JSONObject payload = jwt.getPayload();
        boolean isTimeValid = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) < payload.getLong("exp");
        boolean isSignatureValid = isSignatureValid(jwt);

        return isTimeValid && isSignatureValid;
    }

    private static boolean isSignatureValid(JWT jwt) {

        String b64Data = jwt.getB64Header() + "." + jwt.getB64Payload();
        byte[] bytesSignature = TokenFactory.hs256(b64Data);

        String expectedSignature = encode(bytesSignature);

        return expectedSignature.equals(jwt.getSignature());
    }
}

3 TokenFactory

Essa classe é responsavel por gerar novos tokens, seguindo a especificação RFC 7519.

Uma das subtarefas de se gerar o token é gera a assinatura, portanto a classe TokenFactory acessa a chave secreta por meio da qual geramos o Message Authentication Code (MAC)



public class TokenFactory {

    // secrets should never be placed in code
    final static String SECRET_KEY = "qwertyuiopasdfghjklzxcvbnm0123456789";
    final static String ISSUER = "rafael.senior.engineer";
    final static String HEADER = "{\"alg\":\"HS256\", \"typ\":\"jwt\"}";

    public static JWT issueToken(Credentials credentials) {

        JWT jwt = new JWT();
        fillHeader(jwt);
        fillPayload(jwt, credentials);
        fillSignature(jwt);
        return jwt;
    }

    private static void fillSignature(JWT jwt) {

        byte[] encryptedSignature = hs256(jwt.getB64Header() +"."+ jwt.getB64Payload());
        jwt.setSignature(encode(encryptedSignature));
    }

    static byte[] hs256(String data) {

        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            byte[] secretKeyBytes = SECRET_KEY.getBytes(StandardCharsets.UTF_8);
            SecretKeySpec secretKey = new SecretKeySpec(secretKeyBytes, "HmacSHA256");
            mac.init(secretKey);

            byte[] encryptedData = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return encryptedData;
        }
        catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
            return null;
        }
    }

    private static void fillPayload(JWT jwt, Credentials credentials) {

        JSONObject payload = new JSONObject();
        payload.put("iss", ISSUER);
        payload.put("scope", credentials.getScope());
        payload.put("name", credentials.getUsername());
        payload.put("iat", LocalDateTime.now().toEpochSecond(ZoneOffset.UTC));
        payload.put("exp", LocalDateTime.now().plusMinutes(1).toEpochSecond(ZoneOffset.UTC));
        payload.put("jti", UUID.randomUUID().toString());

        jwt.setPayload(payload);
        jwt.setB64Payload(encode(payload.toString()));
        jwt.setExpires_in(String.valueOf(payload.getLong("exp")));
    }

    private static void fillHeader(JWT jwt) {
        jwt.setB64Header(encode(HEADER));
    }

    static String encode(String data) {
        return encode(data.getBytes(StandardCharsets.UTF_8));
    }

    static String encode(byte[] data) {
        return Base64.getUrlEncoder().withoutPadding().encodeToString(data);
    }
}

O processo de verificar a autenticidade do token consiste simplesmente em regerar a assinatura do token recebido no request e comparar o resultado com a assinatura contida no mesmo token. Como ninguém além do servidor tem acesso a chave secreta, qualquer modificação no JWT realizada por terceiros fará com que o resultado da assinatura regerada pelo servidor seja completamente diferente da assinatura atualmente contida no token, dessa forma, invalidando-o.


Testes de Integração

Após publicar a API no container de aplicações (usamos o Wildfly 23), usamos JUnit 5 e RestAssured, para testar os principais cenários.

A classe AuthenticaitonTest testa os mecanismos de autenticação e geração do Token. Ela possui 4 testes, o quais devem ser executados em uma sequencia pré-definida de acordo anotação @Order:



@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AuthenticationTest {

    private static Credentials credentials;
    private JWT jwt;

    @BeforeAll
    public void init() {
        credentials = new Credentials("daniel", "78910", "read_only");
    }

    @IntegrationTest
    @Order(1)
    public void shouldAuthenticate() {

        String path = "/auth/connect/token";

        Response response =
            given().baseUri(BASE_URI)
                .basePath(path)
                .contentType(ContentType.JSON)
                .request()
                .body(credentials)
                .log().all()
                .when().post()
                .then().log().all()
                .extract().response() ;

        jwt = response.jsonPath().getObject("$", JWT.class);
        Assertions.assertNotNull(jwt);
    }


    @IntegrationTest
    @Order(2)
    public void shouldBeAuthorizedByJWT() {

        given().baseUri(BASE_URI)
                .basePath("/service/{name}")
                .pathParam("name", "hello auth")
                .header(AUTHORIZATION, jwt.toStringForRequest())
                .request()
                .log().all()
                .when()
                .get()
                .peek()
                .then().assertThat().statusCode(SC_OK)
                .log().all();
    }

    @IntegrationTest
    @DisplayName("Should not authorize after modifying the token")
    @Order(3)
    public void shouldNotAuthorize() {

        modifyToken();

        given().baseUri(BASE_URI)
                .basePath("/service/{name}")
                .pathParam("name", "hello auth")
                .header(AUTHORIZATION, jwt.toStringForRequest())
                .request()
                .log().all()
                .when()
                .get()
                .peek()
                .then().assertThat().statusCode(SC_UNAUTHORIZED);
    }

    private void modifyToken() {

        String token = jwt.getAccess_token();
        char[] chars = token.toCharArray();
        chars[65] = 'p';
        token = String.valueOf(chars);
        jwt.setAccess_token(token);
    }

    @IntegrationTest
    @DisplayName("Should not authorize because token has expired")
    @Order(4)
    @Disabled("Disbled by default because this test takes 1 minute long, you can optionally enabled it")
    public void shouldNotAuthorizeDueToExpiration() throws InterruptedException {

        shouldAuthenticate();

        Thread.sleep(60000);

        SamplePayload payload = new SamplePayload(1, "Rafael");

        given().baseUri(BASE_URI)
                .basePath("/service/send")
                .header(AUTHORIZATION, jwt.toStringForRequest())
                .contentType(ContentType.JSON)
                .request().body(payload)
                .log().all()
                .when()
                .post()
                .peek()
                .then().assertThat().statusCode(SC_UNAUTHORIZED)
                .log().all();
    }
 }

Neste teste checamos a geração do token após os envio de credenciais:


Neste outro teste, modificamos o token e tantamos acessar o serviço, o qual deve retornar 401 UNAUTHORIZED



A classe ServiceApiTest testa os serviços criados:


package integration.api;

import annotations.IntegrationTest;
import br.com.app.api.model.SamplePayload;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.TestInstance;

import static io.restassured.RestAssured.given;
import static org.apache.http.HttpHeaders.AUTHORIZATION;
import static org.apache.http.HttpStatus.SC_OK;
import static util.Constants.BASE_URI;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class ServiceApiTest extends AbstractTest {

    @BeforeAll
    public void init() {
        setToken();
    }

    @IntegrationTest
    @DisplayName("should return 200 OK when send payload")
    public void test01() {

        SamplePayload payload = new SamplePayload(1, "Rafael");

        given().baseUri(BASE_URI)
                .basePath("/service/send")
                .header(AUTHORIZATION, jwt.toStringForRequest())
                .contentType(ContentType.JSON)
                .request().body(payload)
                .log().all()
                .when()
                .post()
                .peek()
                .then().assertThat().statusCode(SC_OK)
                .log().all();
    }

    @IntegrationTest
    @DisplayName("should return 200 OK when request with name path")
    public void test02() {

        given().baseUri(BASE_URI)
                .basePath("/service/{name}")
                .pathParam("name", "hello auth")
                .header(AUTHORIZATION, jwt.toStringForRequest())
                .request()
                .log().all()
                .when()
                .get()
                .peek()
                .then().assertThat().statusCode(SC_OK)
                .log().all();
    }
}

Neste print testamos o serviço POST /service/send. Repare que o token deve ser colocado no header AUTHORIZARION da requisição:



Ao todo são 7 testes de integração. Para rodá-los de uma vez utilize o comando  mvn integration-test:



O código completo do projeto está no git-hub.


       

Sunday, March 28, 2021

Wildfly: Habilitando HTTPS para suas Aplicações 1

No artigo Segurança com Java: Certificados, fizemos um pequeno apanhado teórico sobre o TLS, vimos o que são TrustStore e KeyStore e vimos também algumas opções sobre gerenciamento de certificados em Java, seja via código ou através do utilitário keytool.


Neste artigo (em 2 partes) criaremos um exemplo prático. Vamos criar um REST endpoint simples, publicá-lo no Wildfly e acessá-lo via HTTPS (TLS). Para tanto precisamos:

  1.  Criar um par de chaves pública/privada e o certificado para o nosso servidor
  2.  Habilitar o sistema de TLS e HTTPS listener do Wildfly com base nas chaves criadas no passo anterior
A versão do Wildfly utilizada neste exemplo é a 23. A do Java é 11.

1 Criando o par de chaves e o certificado para o servidor

Usamos o keytool para gerar as chaves:


keytool -genkeypair -alias localhost -keyalg RSA -keysize 2048 -validity 365 -keystore server.keystore


O comando anterior cria um arquivo chamado server.keystore que contem uma chave privada e um certificado válido por 365 dias o qual contem a chave pública que será apresentado aos clientes das aplicações publicadas no servidor. O domínio é localhost

Guarde o password para os próximos passos.


2 Configurar o TLS no Wildfly

Há várias formas de se habilitar o TLS para aplicações publicadas no Wildfly. Neste caso o faremos via Elytron, que é um framework de segurança adicionado ao Wildfly. Por meio do Elytron podemos gerenciar configurações de acesso ao próprio servidor e à aplicações nele publicadas. As áreas envolvidas nesse gerenciamento são Autenticação, Autorização, Armazenamento de credenciais e TLS.

Copie o arquivo server.keystore do passo anterior para  JBOSS_HOME/standalone/configuration


Nota: Os comandos que executaremos nos próximos passos serão através do utilitário jboss-cli.sh (ou jboss-cli.bat se o seu ambiente for Windows) localizado em JBOSS_HOME/bin. Execute esse arquivo e depois execute o comando connect.

Agora criamos uma keystore chamada httpsKS dentro do servidor que referencia o nosso arquivo server.keystore:


/subsystem=elytron/key-store=httpsKS:add(path=server.keystore,relative-to=jboss.server.config.dir,credential-reference={clear-text=changeit},type=JKS)


Agora criamos um key-manager para o qual damos o nome httpsKM que referencia a keystore criada no passo anterior:


/subsystem=elytron/key-manager=httpsKM:add(key-store=httpsKS,credential-reference={clear-text=changeit})

Agora configuramos um contexto TLS que vamos chamar de httpsSSC, o qual referencia o httpsKM criado no passo anterior:

/subsystem=elytron/server-ssl-context=httpsSSC:add(key-manager=httpsKM,protocols=["TLSv1.2"])


Como estamos estabelendo essas configuraões de segurança via Elytron, que é um framework novo, precisamos checar se o HTTPS-listener está usando o sitema legado de segurança do Wildfly para configuração do TLS. Execute o seguinte commando:

/subsystem=undertow/server=default-server/https-listener=https:read-attribute(name=security-realm)
{
    "outcome" => "success",
    "result" => "ApplicationRealm"
}

O resultado do comando anterior nos diz que o HTTPS-listener está usando o sistema de segurança legado ApplicationRelam para configuração TLS. 

Undertow não pode referenciar o TLS context no sistema legado e no Elytron ao mesmo tempo. Então temos que remover a referencia para o sistema de segurança legado e atualizar o HTTPS-listener para usar o contexto TLS do Elytron. Para esse procedimento utilizamos a seguinte operação em lote (batch):

batch
/subsystem=undertow/server=default-server/https-listener=https:undefine-attribute(name=security-realm)
/subsystem=undertow/server=default-server/https-listener=https:write-attribute(name=ssl-context,value=httpsSSC)
run-batch

Em seguida reiniciamos o Wildfly com o commando reload.  

Procedimento completo no print abaixo:

Pronto!

HTTPS agora está habilitado para todas as aplicações publicadas nesse servidor. Em termos de configuração, os camandos anteriores equivalem a adicionar essas linhas no arquivo standloane.xml:


<subsystem>
        ...
                <tls>
                     <key-stores>
                         <key-store name="demoKeyStore">
                             <credential-reference clear-text="changeit">
                             <implementation type="JKS">
                             <file path="server.keystore" relative-to="jboss.server.config.dir">
                         </file></implementation></credential-reference></key-store>
                     </key-stores>
                     <key-managers>
                         <key-manager key-store="demoKeyStore" name="demoKeyManager">
                             <credential-reference clear-text="changeit">
                         </credential-reference></key-manager>
                     </key-managers>
                     <server-ssl-contexts>
                         <server-ssl-context key-manager="demoKeyManager" name="demoSSLContext" protocols="TLSv1.2">
                     </server-ssl-context></server-ssl-contexts>
                 </tls>
    </subsystem>
...
<subsystem default-security-domain="other" default-server="default-server" default-servlet-container="default" default-virtual-host="default-host" statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}" xmlns="urn:jboss:domain:undertow:10.0">
                 <buffer-cache name="default">
                 <server name="default-server">
                     <http-listener enable-http2="true" name="default" redirect-socket="https" socket-binding="http">
                     <https-listener enable-http2="true" name="https" socket-binding="https" ssl-context="demoSSLContext">
                     <host alias="localhost" name="default-host">
                         <location handler="welcome-content" name="/">
                         <http-invoker security-realm="ApplicationRealm">
                     </http-invoker></location></host>
                 </https-listener></http-listener></server>
    
...
</buffer-cache></subsystem> 

Agora podemos acessar o console em https://localhost:8443/ e baixar o certificado de localhost. Como o certificado é assinado por nós mesmos, provavelmente seu browser vai emitir um alerta de que não reconhece essa autoridade certificadora. Tal alerta pode ser ignorado. 

Clique no icone do cadeado no canto superior direito, em certificate, depois export certificate: 

Salve este certificado para usarmos em nossa aplicação java que será um cliente desse servidor.




Também podemos checar o certificado atraves do utilitario openssl.


openssl s_client -connect localhost:8443



Na segunda parte deste artigo, publicamos um endpoint no wildfly e o acessamos de forma segura com uma aplicação cliente java que usará o nosso certificado localhost.


       

Wednesday, March 17, 2021

Autorização JWT Puro Java: parte 1

Este artigo pretende ilustrar a aplicação de autenticação JWT em RESTFUL web services em java simples (sem lib de terceiros). Na parte 2 criamos um exemplo prático

Visão Geral

JWT é uma especificação aberta (RFC 7519) usada para gerenciar autorização e troca de informações de uma maneira segura e stateless, isto é, o servidor não mantem informações relacionadas à sessão do cliente. Este é o cenário ideal no contexto mais utilizado hoje com boa parte da comunicação  ocorrendo via Restful APIs, dessa forma o escalonamento horizontal dos serviços pode ocorrer sem nenhum empecilho (no que tange aos aspectos relacionados à sessão do usuário).



JWT Workflow

O diagrama de sequencia abaixo ilustra de forma geral o fluxo em serviços baseados em autenticações JWT. Um enpoint de autentição recebe as credenciais do cliente, as verifica, caso válidas, emite um token específico com prazo de validade para aquele cliente específico:



Quando for acessar os demais serviços, o cliente deve apresentar o token recebido na requisição anterior, um filtro intercepta a requisição, confere o token, e libera o acesso ao serviço se for o caso.



Segurança

Embora também possa garantir confidencialidade, desde que transmitido via HTTPS, o foco da especificação JWT é validação, ou seja, deve responder à pertgunta: os dados apresentados foram alterados indevidamente? 

Para assegurar essa proposta, a RFC 7519 especifica que deve-se utilizar um Message Authentication Code (MAC) para assinar o token. MACs são reconhecidamente usados para assegurar  integridade e autenticidade durante a troca de mensagens. Há vários algoritmos que implementam o MAC, o que utilizaremos será HMAC-SHA256. 

Um token JWT é composto por 3 objetos JSON concatenados:

  • Header: contém o algoritmo usado para gerar o MAC e o tipo de token
  • Payload: pode conter qualquer informação pertinente as regras de negócio, além de elementos registrados pela RFC 7519, como iss (quem gerou o token), exp (validade), iat (quando foi gerado), sub (assunto), etc.
  • Assinatura: é o resultado da aplicação do MAC ao Header e Payload concatenados.
Antes de processar cada objeto, eles devem ser convertidos em base64 URL safe de modo que o token não seja comrrompido durante o envio pela internet. Exemplo:

HEADER

{"alg":"HS256","typ":"jwt"} 

base64 aplicada
 eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9


PAYLOAD

{"scope":"read_only","iss":"rafael.senior.engineer","name":"daniel","exp":1615718739,"iat":1615718679,"jti":"d970fea6-78ce-4724-b35a-37c49b01a832"} 

base64 aplicada
eyJzY29wZSI6InJlYWRfb25seSIsImlzcyI6InJhZmFlbC5zZW5pb3IuZW5naW5lZXIiLCJuYW1lIjoiZGFuaWVsIiwiZXhwIjoxNjE1NzE4NzM5LCJpYXQiOjE2MTU3MTg2NzksImp0aSI6ImQ5NzBmZWE2LTc4Y2UtNDcyNC1iMzVhLTM3YzQ5YjAxYTgzMiJ9


ASSINATURA

Concatene-se o header e o payload anteriores, aplica-se ao resultado a função MAC:

        HMAC-SHA256(header +"."+ payload)

Converta o resultado da função MAC em base64:
eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJzY29wZSI6InJlYWRfb25seSIsImlzcyI6InJhZmFlbC5zZW5pb3IuZW5naW5lZXIiLCJuYW1lIjoiZGFuaWVsIiwiZXhwIjoxNjE1NzE4NzM5LCJpYXQiOjE2MTU3MTg2NzksImp0aSI6ImQ5NzBmZWE2LTc4Y2UtNDcyNC1iMzVhLTM3YzQ5YjAxYTgzMiJ9.m5zc-JoYEn2fGY44iJfLJjN8si1MPw934My42VAPaFs


JWT final:

    header.payload.assinatura

eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJzY29wZSI6InJlYWRfb25seSIsImlzcyI6InJhZmFlbC5zZW5pb3IuZW5naW5lZXIiLCJuYW1lIjoiZGFuaWVsIiwiZXhwIjoxNjE1NzE4NzM5LCJpYXQiOjE2MTU3MTg2NzksImp0aSI6ImQ5NzBmZWE2LTc4Y2UtNDcyNC1iMzVhLTM3YzQ5YjAxYTgzMiJ9.eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJzY29wZSI6InJlYWRfb25seSIsImlzcyI6InJhZmFlbC5zZW5pb3IuZW5naW5lZXIiLCJuYW1lIjoiZGFuaWVsIiwiZXhwIjoxNjE1NzE4NzM5LCJpYXQiOjE2MTU3MTg2NzksImp0aSI6ImQ5NzBmZWE2LTc4Y2UtNDcyNC1iMzVhLTM3YzQ5YjAxYTgzMiJ9.m5zc-JoYEn2fGY44iJfLJjN8si1MPw934My42VAPaFs

Conforme dito no início, este artigo não pretende ser uma explicação exaustiva sobre JWT, o objetivo é ilustrar sua aplicação prática em java simples (sem lib de terceiros), caso queira se aprofundar, consulte a documentação oficial.

Na parte 2 criaremos uma API no container Wildfly com o seguintes serviços:


Eles só poderão ser acessados através de um token, com validade determinada, segundo os fluxos apresentados no começo do artigo.