Preloader image

Este é um exemplo básico sobre como configurar e utilizar o MicroProfile JWT no TomEE.

Execute testes para diferentes cenários relacionados à validação JWT

mvn clean test

Configuração no TomEE

A classe MoviesMPJWTConfigurationProvider.java fornece ao TomEE as configurações necessárias para a validação do JWT.

package org.superbiz.moviefun.rest;

import org.apache.tomee.microprofile.jwt.config.JWTAuthContextInfo;

import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.Produces;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Optional;

@Dependent
public class MoviesMPJWTConfigurationProvider {

    @Produces
    Optional<JWTAuthContextInfo> getOptionalContextInfo() throws NoSuchAlgorithmException, InvalidKeySpecException {
        JWTAuthContextInfo contextInfo = new JWTAuthContextInfo();

        // todo use MP Config to load the configuration
        contextInfo.setIssuedBy("https://server.example.com");

        final String pemEncoded = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq" +
                "Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR" +
                "TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e" +
                "UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9" +
                "AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn" +
                "sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x" +
                "nQIDAQAB";
        byte[] encodedBytes = Base64.getDecoder().decode(pemEncoded);

        final X509EncodedKeySpec spec = new X509EncodedKeySpec(encodedBytes);
        final KeyFactory kf = KeyFactory.getInstance("RSA");
        final RSAPublicKey pk = (RSAPublicKey) kf.generatePublic(spec);

        contextInfo.setSignerKey(pk);

        return Optional.of(contextInfo);
    }

    @Produces
    JWTAuthContextInfo getContextInfo() throws InvalidKeySpecException, NoSuchAlgorithmException {
        return getOptionalContextInfo().get();
    }
}

Usando o MicroProfile JWT no TomEE

O recurso JAX-RS MoviesRest.java contém vários endpoints que estão protegidos usando a anotação padrão @RolesAllowed. O MicroProfile JWT é responsável por validar solicitações recebidas com o cabeçalho `Authorization' que fornece um token de acesso assinado.

   package org.superbiz.moviefun.rest;

   import org.superbiz.moviefun.Movie;
   import org.superbiz.moviefun.MoviesBean;

   import jakarta.annotation.security.RolesAllowed;
   import jakarta.inject.Inject;
   import jakarta.ws.rs.*;
   import jakarta.ws.rs.core.MediaType;
   import java.util.List;

   @Path("cinema")
   @Produces(MediaType.APPLICATION_JSON)
   @Consumes(MediaType.APPLICATION_JSON)
   public class MoviesRest {

       @Inject
       private MoviesBean moviesBean;

       @GET
       @Produces(MediaType.TEXT_PLAIN)
       public String status() {
           return "ok";
       }

       @GET
       @Path("/movies")
       @RolesAllowed({"crud", "read-only"})
       public List<Movie> getListOfMovies() {
           return moviesBean.getMovies();
       }

       @GET
       @Path("/movies/{id}")
       @RolesAllowed({"crud", "read-only"})
       public Movie getMovie(@PathParam("id") int id) {
           return moviesBean.getMovie(id);
       }

       @POST
       @Path("/movies")
       @RolesAllowed("crud")
       public void addMovie(Movie newMovie) {
           moviesBean.addMovie(newMovie);
       }

       @DELETE
       @Path("/movies/{id}")
       @RolesAllowed("crud")
       public void deleteMovie(@PathParam("id") int id) {
           moviesBean.deleteMovie(id);
       }

       @PUT
       @Path("/movies")
       @RolesAllowed("crud")
       public void updateMovie(Movie updatedMovie) {
           moviesBean.updateMovie(updatedMovie);
       }

   }

 @Inject
 @ConfigProperty(name = "java.runtime.version")
 private String javaVersion;

Sobre a arquitetura de teste

Os casos de teste para este projeto são construídos com o Arquillian. A configuração do Arquillian pode ser encontrada em src/test/resources/arquillian.xml

A classe TokenUtils.java é usada durante o teste para atuar como um servidor de Autorização que gera Access Tokens com base nos arquivos de configuração privateKey.pem,publicKey.pem, Token1.json e Token2.json.

nimbus-jose-jwt é a biblioteca usada para a geração do JWT durante os testes.

Token1.json

{
    "iss": "https://server.example.com",
    "jti": "a-123",
    "sub": "24400320",
    "upn": "jdoe@example.com",
    "preferred_username": "jdoe",
    "aud": "s6BhdRkqt3",
    "exp": 1311281970,
    "iat": 1311280970,
    "auth_time": 1311280969,
    "groups": [
        "group1",
        "group2",
        "crud",
        "read-only"
    ]
}

Token2.json

{
  "iss": "https://server.example.com",
  "jti": "a-123",
  "sub": "24400320",
  "upn": "alice@example.com",
  "preferred_username": "alice",
  "aud": "s6BhdRkqt3",
  "exp": 1311281970,
  "iat": 1311280970,
  "auth_time": 1311280969,
  "groups": [
    "read-only"
  ]
}

Cenários de teste

MovieTest.java contém 4 cenários OAuth2 para diferentes combinações de JWT.

package org.superbiz.moviefun;

import org.apache.cxf.feature.LoggingFeature;
import org.apache.cxf.jaxrs.client.WebClient;
import org.apache.johnzon.jaxrs.JohnzonProvider;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.superbiz.moviefun.rest.ApplicationConfig;
import org.superbiz.moviefun.rest.MoviesMPJWTConfigurationProvider;
import org.superbiz.moviefun.rest.MoviesRest;

import jakarta.ws.rs.core.Response;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.logging.Logger;

import static java.util.Collections.singletonList;
import static org.junit.Assert.assertTrue;

@RunWith(Arquillian.class)
public class MoviesTest {

    @Deployment(testable = false)
    public static WebArchive createDeployment() {
        final WebArchive webArchive = ShrinkWrap.create(WebArchive.class, "test.war")
                                                .addClasses(Movie.class, MoviesBean.class, MoviesTest.class)
                                                .addClasses(MoviesRest.class, ApplicationConfig.class)
                                                .addClass(MoviesMPJWTConfigurationProvider.class)
                                                .addAsWebInfResource(new StringAsset("<beans/>"), "beans.xml");

        System.out.println(webArchive.toString(true));

        return webArchive;
    }

    @ArquillianResource
    private URL base;


    private final static Logger LOGGER = Logger.getLogger(MoviesTest.class.getName());

    @Test
    public void movieRestTest() throws Exception {

        final WebClient webClient = WebClient
                .create(base.toExternalForm(), singletonList(new JohnzonProvider<>()),
                        singletonList(new LoggingFeature()), null);


        //Testing rest endpoint deployment (GET  without security header)
        String responsePayload = webClient.reset().path("/rest/cinema/").get(String.class);
        LOGGER.info("responsePayload = " + responsePayload);
        assertTrue(responsePayload.equalsIgnoreCase("ok"));


        //POST (Using token1.json with group of claims: [CRUD])
        Movie newMovie = new Movie(1, "David Dobkin", "Wedding Crashers");
        Response response = webClient.reset()
                                     .path("/rest/cinema/movies")
                                     .header("Content-Type", "application/json")
                                     .header("Authorization", "Bearer " + token(1))
                                     .post(newMovie);
        LOGGER.info("responseCode = " + response.getStatus());
        assertTrue(response.getStatus() == 204);


        //GET movies (Using token1.json with group of claims: [read-only])
        //This test should be updated to use token2.json once TOMEE- gets resolved.
        Collection<? extends Movie> movies = webClient
                .reset()
                .path("/rest/cinema/movies")
                .header("Content-Type", "application/json")
                .header("Authorization", "Bearer " + token(1))
                .getCollection(Movie.class);
        LOGGER.info(movies.toString());
        assertTrue(movies.size() == 1);


        //Should return a 403 since POST require group of claims: [crud] but Token 2 has only [read-only].
        Movie secondNewMovie = new Movie(2, "Todd Phillips", "Starsky & Hutch");
        Response responseWithError = webClient.reset()
                                              .path("/rest/cinema/movies")
                                              .header("Content-Type", "application/json")
                                              .header("Authorization", "Bearer " + token(2))
                                              .post(secondNewMovie);
        LOGGER.info("responseCode = " + responseWithError.getStatus());
        assertTrue(responseWithError.getStatus() == 403);


        //Should return a 401 since the header Authorization is not part of the POST request.
        Response responseWith401Error = webClient.reset()
                                                 .path("/rest/cinema/movies")
                                                 .header("Content-Type", "application/json")
                                                 .post(new Movie());
        LOGGER.info("responseCode = " + responseWith401Error.getStatus());
        assertTrue(responseWith401Error.getStatus() == 401);

    }


    private String token(int token_type) throws Exception {
        HashMap<String, Long> timeClaims = new HashMap<>();
        if (token_type == 1) {
            return TokenUtils.generateTokenString("/Token1.json", null, timeClaims);
        } else {
            return TokenUtils.generateTokenString("/Token2.json", null, timeClaims);
        }
    }

}