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:






 











