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.