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.



       

Wednesday, April 15, 2020

Segurança com Java: Certificados

Nesse primeiro post falamos um pouco sobre criptografia com Java. Agora vamos nos aprofundar e falar sobre certificados.

Se você quiser que sua aplicação web (e-commerce, página estática, back-end, etc) viabilize a comunicação HTTPS com seu cliente, você precisa instalar um certificado TLS/SSL no seu web-server. Dessa forma toda a sessão entre a aplicação web e o cliente poderá ser estabelecida de forma segura, isto é, criptografada por meio de chaves públicas e privadas.



TLS/SSL

Os protocolos TLS (Transport Layer Security) e SSL (Secure Socket Layer) são frequentemente usados para se referir à mesma coisa: comunicação HTTPS. Ambos visam a segurança, integridade e privacidade durante a comunicação cliente-servidor. O SSL porém é considerado depreciado e sua versão moderna é o TLS 1.3.

A espinha dorsal do TLS é uma infraestrutura de chaves públicas que viabiliza a confiança mútua entre duas partes (cliente e servidor) que nunca trabalharam juntas. Isso tudo é realizado por um sistema de Certificate Authorities (CAs) que assinam certificados que passam a servir de credenciais para servidores, atestando sua identidade. Assim, quando um cliente tenta se conectar a esse servidor via HTTPS (TLS), esse servidor apresenta seu certificado ao cliente. Se a autoridade que assinou o certficado desse servidor estiver na lista de autoridades confiáveis do cliente, então um canal de comunicação segura é estabelecido entre esse cliente e o respectivo servidor.

Quando o cliente é uma aplicação java, a lista de autoridades confiáveis é chamada de truststore. Do lado do servidor [quando ele também é uma aplicação java], seu certificado (e as respectivas chaves) fica armazenado em uma keystore.

Geralmente, quando você é um servidor, trabalha mais com o keystore; quando você é cliente, está mais interessado no seu truststore.

O server apresenta seu certificado ao cliente, que confere no seu truststore se o certificado é confiável

Truststores do JDK

O Truststore na prática é um conjunto de certificados armazenados no JDK que a aplicação utiliza para determinar se confia ou não no servidor TLS que ela está acessando. Todo JDK vem com uma lista de CAs pré-definidos no Truststore.

O keytool é um programa em linha de comando que permite trabalhar com truststores e keystores.  Como dissemos, você trabalha mais com o keystore se for um server; ou trabalha mais com o truststore, se for um cliente.

O seguinte comando lista todas as autoridades pré-aceitas no JDK (o password default é changeit para rodar o comando):

keytool -list -keystore $JAVA_HOME/jre/lib/security/cacerts


Como exemplo, no browser Mozila, podemos checar o certificado de docs.amazon.aws clicando no icone do cadeado:


Em seguida clicando em more information:


Na pop-up que aparecer clique em view certficate:


Selecione o fingerprint desse certificado:


E confira se o JDK confia nesse CA:

keytool -list -keystore $JAVA_HOME/jre/lib/security/cacerts | grep ' fingerprint '


Como esse CA consta no truststore do JDK, podemos acessar essa url via código sem problemas:

String hostURL = "https://docs.aws.amazon.com";
URL url = new URL(hostURL);
HttpsURLConnection conn;
conn = (HttpsURLConnection)url.openConnection();
InputStream is = conn.getInputStream();

Experimente trocar por uma url cujo certificado não confiável, como por exemplo https://self-signed.badssl.com, e o código acima lança uma exception

Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
 at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1946)
 at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:316)
 at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:310)
 at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1639)
 at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:223)
 at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1037)
 at sun.security.ssl.Handshaker.process_record(Handshaker.java:965)
 at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1064)
 at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
 at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
 at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
 at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
 at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
 at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1570)
 at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1498)
 at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:268)
 at ssl.Main03.main(Main03.java:17)

 javax.net.ssl.SSLHandshakeException Basicamente nos diz que o certificado desse host não é reconhecido pelo truststore default do JDK e que, portanto, não é possível estabelecer uma conexão segura com ele via TLS.

É comum que muitas vezes queiramos ignorar essa advertência e acessar o host mesmo assim (cenário comum em ambientes de teste). Neste caso podemos criar nossa própria truststore e   adicionar manualmente qualquer certficado.

O código abaixo cria uma nova TrsutStore e adiciona nela o certificado de https://self-signed.badssl.com, fazendo com que a aplicação consiga acessar a URL via HTTPS sem erros:

public class AdicionaCertificado {

    public static void main(String args[]) throws Exception {

        String hostURL = "https://self-signed.badssl.com";
        //String hostURL = "https://docs.aws.amazon.com";
        loadNewCertificate("/home/rafael/Library/Blog/Certs/badssl-com.pem");

        URL url = new URL(hostURL);
        HttpsURLConnection conn;
        conn = (HttpsURLConnection)url.openConnection();
        InputStream is = conn.getInputStream();
    }

    static void loadNewCertificate(String newCertficateFile) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {

        String trustStorePass = "123456";
        String clientTrutStorePath = "/home/rafael/Library/Blog/Certs/myNewTrustStore";
        String alias = "badssl.com";

        // criando uma nova trustStore (diferente da default cacerts)
        KeyStore clientTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());

        char[] password = trustStorePass.toCharArray();

        // para criar uma nova truststore vazia, passe null no 1o argumento...
        clientTrustStore.load(null, password);

        // ...e crie o arquivo fisico vazio
        FileOutputStream fos = new FileOutputStream(clientTrutStorePath);
        clientTrustStore.store(fos, password);
        fos.close();

        // como a keystore já existe, desta vez não passamos null ao metodo load
        FileInputStream in = new FileInputStream(clientTrutStorePath);
        clientTrustStore.load(in, password);
        in.close();

        // CertficateFactory é usada para gerar novos certificados
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        InputStream serverCertstream = new FileInputStream(newCertficateFile);

        // gera um objeto certificado e o inicializa com base nos dados do InputStream
        Certificate serverCertificate =  cf.generateCertificate(serverCertstream);

        // adiciona o novo certificado a nova keystore, com um apelido
        clientTrustStore.setCertificateEntry(alias, serverCertificate);

        // salvamos as alteracoes na nova keystore
        FileOutputStream out = new FileOutputStream(clientTrutStorePath);
        clientTrustStore.store(out, password);
        out.close();

        // dinamicamente alteramos o trsutstore default desta App para a nova trsutStore criada
        System.setProperty("javax.net.ssl.trustStore", clientTrutStorePath);
        System.setProperty("javax.net.ssl.trustStorePassword", trustStorePass);
    }
}

Você pode criar seu próprio certificado para posteriormente apresentá-lo aos clientes de sua aplicação. O comando a seguir cria uma chave privada e um certificado contendo a chave pública:
keytool -genkeypair -alias meu-alias -keyalg RSA -validity 7 -keystore minha-keystore

O próximo comando extrai o certificado do arquivo minha-keystore com a extensão .cer:
keytool -exportcert -alias meu-alias -keystore minha-keystore -rfc -file meu-certificado.cer

Nota: um detalhe para se estar ciente e que pode causar confusão é que, da perspectiva do keytool e do Java, keystores e truststores são arquivos keystore. Eles apenas contêm diferentes tipos de chaves. Tanto que a referência a clientTrustore no código foi representada pela classe java.security.KeyStore.


"Os fariseus perguntaram a Jesus sobre o momento em que chegaria o Reino de Deus. Jesus respondeu: 'O Reino de Deus não vem ostensivamente. Nem se poderá dizer: 'Está aqui' ou: 'está alí', porque o Reino de Deus está no meio de vocês.'"

Lucas 17:20-21

       

Friday, February 21, 2020

AWS: API Gateway e Lambda: parte 2

Na primeira parte deste artigo conhecemos o serviço Lambda da AWS. Com o AWS Lambda podemos criar funções que são disparadas por eventos diversos, sendo que toda a implementação de infraestrutura fica a cargo do cloud provider (neste caso AWS), assim podemos focar totalmente no código ao inves de questões relacionadas a servidores, etc. Também criamos uma função simples por linha de comando.


Neste artigo vamos explorar um caso de uso muito comum que é expor a função Lambda através de um recurso REST, para tanto, utilizaremos o serviço AWS API Gateway.


AWS API Gateway

Esse serviço permite criar recursos acessíveis via HTTP que aderem ao prtocolo REST, habilitando comunicação client-server usando os métodos padrões GET, PUT, POST, DELETE e PATCH.

O diagrama abaixo ilustra como a API Gateway pode integrar diversos clientes aos diversos serviços de back-end em um ambiente totalmente serverless:



Chamando a função Lambda via API Gateway

Passo 1: no console da API Gateway, encontre a seção REST API e clique em build:



Passo 2: na página seguinte:
  • embaixo de Choose the Protocol, escolha REST
  • embaixo de Create New API, escolha New API
  • em Settings, dê um nome a sua API (neste caso my-api)



Passso 3: na página seguinte, no botão menu Action, clique em Create Method:



Passo 4: na seção Resources, clique na combo box e escolha GET:



Passo 5: clique no método GET e na seção à direita:
  • em integration type, marque Lambda Function, o que significa que esse recurso irá chamar uma função Lambda
  • marque o check-box Use Lambda Proxy integration. Quando essa opção é marcada, todo envolope HTTP que chega à API Gateway é repassado integralmente à função Lambda sob a forma de um JSON. (Se esta opção não for marcada, o desenvolvedor então deve fazer o mapeamento customizado entre o input da API Gateway e o input da função Lambda)
  • em lambda function, digite o nome da função que será chamada e clique em Save.


Vai aparecer uma pop-up dizendo que será criada uma permissão para este recurso invocar a função Lambda, clique OK.


Passo 6: no botão menu Action clique na opção Deploy API. Vai aparecer uma pop up requisitando um stage para esse deployment (coloque stage ou test):


Em stages, clique no nome do stage que você criou. O metodo já está publicado, repare na invoke url:


Copie e cole essa url em uma nova aba do browser, ele vai chamar a função Lambda e exibir o valor que ela retornar:


Integração entre API Gateway e a função Lambda

Existem duas formas de configurar como a API Gateway vai repassar os dados do request para a função Lambda:


  1. Lambda Proxy Integration: é a opção mais simples, todo envelope HTTP recebido pela API Gateway é convertido em um stream de JSON e repassado integralmente ao Lambda
  2. Lambda Proxy Non-Integration (ou AWS): a função Lambda recebe um input diferente do envelope HTTP (geralmente um objeto do modelo de negócios), neste caso antes de chamar a função lambda, deve-se fazer um mapeamento entre o envelope HTTP e o modelo de dados requerido pela função Lmabda.
Exemplo de uma requisição HTTP no formato JSON repassada integralmente pela API Gateway à função Lambda quando se usa a opção Lambda Proxy Integration:

{
 "resource": "Resource path",
 "path": "Path parameter",
 "httpMethod": "Incoming request's method name"
 "headers": {String containing incoming request headers}
 "multiValueHeaders": {List of strings containing incoming request headers}
 "queryStringParameters": {query string parameters }
 "multiValueQueryStringParameters": {List of query string parameters}
 "pathParameters": {path parameters}
 "stageVariables": {Applicable stage variables}
 "requestContext": {Request context, including authorizer-returned key-value pairs}
 "body": "A JSON string of the request payload."
 "isBase64Encoded": "A boolean flag to indicate if the applicable request payload is Base64-encode"
}

"Quem bebe dessa água vai ter sede de novo. Mas aquele que beber da água que eu vou dar, esse nunca mais terá sede. E a água que eu lhe darei, vai se tornar dentro dele uma fonte de água que jorra para a vida eterna."

Jo 4:13-14