Hello, I am trying to create an application where I can mix Java EE
security annotations in JAX-RS endpoints. I am using current TomEE 2
Snapshot and basically I am creating two ContainerRequestFilter.
One for authentication and another for authorization.
@Provider
@Priority(Priorities.AUTHENTICATION)
public class JWTAuthenticationFilter implements ContainerRequestFilter {
private static final List<Class<? extends Annotation>>
securityAnnotations =
Arrays.asList(DenyAll.class, PermitAll.class, RolesAllowed.class);
@Context
private ResourceInfo resourceInfo;
private UserService userservice = new UserService();
@Override
public void filter(ContainerRequestContext request) throws IOException {
if (isSecuredResource()) {
String token = request.getHeaderString("x-access-token");
try {
String username = getUsernameFromToken(token);
final User user = userservice.findUser(username);
request.setSecurityContext(new SecurityContext() {
@Override
public boolean isUserInRole(String role) {
return user.isUserInRole(role);
}
@Override
public boolean isSecure() {
return false;
}
@Override
public Principal getUserPrincipal() {
return user;
}
@Override
public String getAuthenticationScheme() {
return SecurityContext.BASIC_AUTH;
}
});
} catch (java.text.ParseException | JOSEException |
NullPointerException e) {
request.abortWith(Response.status(404).build());
}
}
}
private boolean isSecuredResource() {
for (Class<? extends Annotation> securityClass :
securityAnnotations) {
if (resourceInfo.getResourceMethod().isAnnotationPresent(
securityClass)) {
return true;
}
}
for (Class<? extends Annotation> securityClass :
securityAnnotations) {
if (resourceInfo.getResourceClass().isAnnotationPresent(
securityClass)) {
return true;
}
}
return false;
}
private String getUsernameFromToken(String token)
throws java.text.ParseException, JOSEException {
SignedJWT signedJWT = SignedJWT.parse(token);
JWSVerifier verifier = new MACVerifier(SharedSecret.getSecret());
if (signedJWT.verify(verifier)) {
return signedJWT.getJWTClaimsSet().getSubject();
} else {
throw new JOSEException("Firm is not verified.");
}
}
}
note that I am creating a custom security context and for authorization:
@Provider
@Priority(Priorities.AUTHORIZATION)
public class RolesAllowedFilter implements ContainerRequestFilter {
private static final Response NOT_FOUND = Response.status(
Response.Status.NOT_FOUND).entity("{\"message\": \"Resource Not
Found\"}").build();
@Context
private ResourceInfo resourceInfo;
@Override
public void filter(ContainerRequestContext requestContext)
throws IOException {
Method resourceMethod = resourceInfo.getResourceMethod();
// DenyAll on the method take precedence over RolesAllowed and PermitAll
if (resourceMethod.isAnnotationPresent(DenyAll.class)) {
requestContext.abortWith(NOT_FOUND);
return;
}
// RolesAllowed on the method takes precedence over PermitAll
RolesAllowed ra = resourceMethod.getAnnotation(RolesAllowed.class);
if(assertRole(requestContext, ra)) {
return;
}
// PermitAll takes precedence over RolesAllowed on the class
if (resourceMethod.isAnnotationPresent(PermitAll.class)) {
// Do nothing.
return;
}
if (resourceInfo.getResourceClass().isAnnotationPresent(DenyAll.class))
{
requestContext.abortWith(NOT_FOUND);
}
// RolesAllowed on the class takes precedence over PermitAll
ra = resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class);
if(assertRole(requestContext, ra)) {
return;
}
}
private boolean assertRole(ContainerRequestContext requestContext,
RolesAllowed ra) {
if (ra != null) {
String[] roles = ra.value();
for (String role : roles) {
if (requestContext.getSecurityContext().isUserInRole(role)) {
return true;
}
}
requestContext.abortWith(NOT_FOUND);
}
return false;
}
}
And the business code:
@Stateless
@Path("/app")
public class UiApplication {
@Inject
UserService userService;
@Inject
MovieService moviesService;
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response login(JsonObject jsonObject) throws JOSEException {
JsonString username = (JsonString) jsonObject.get("username");
JsonString password = (JsonString) jsonObject.get("password");
if (authenticate(username.getString(), password.getString())) {
String token = createToken(username.getString(), "example.com");
JsonObject responseDocument = Json.createObjectBuilder()
.add("user", Json.createObjectBuilder().add("username",
username).build())
.add("token", token)
.build();
return Response.ok(responseDocument).build();
}
return Response.status(Status.NOT_FOUND).build();
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed("admin")
public Response movies() {
return Response.ok("{\"message\":\"a\"}").build();
}
private boolean authenticate(String username, String password) {
return this.userService.findUser(username, password);
}
private String createToken(String subject, String issuer) throws
JOSEException {
JWSSigner signer = new MACSigner(SharedSecret.getSecret());
JWTClaimsSet claimsSet = new JWTClaimsSet();
claimsSet.setSubject(subject);
claimsSet.setIssueTime(new Date());
claimsSet.setIssuer(issuer);
SignedJWT signedJWT = new SignedJWT(new
JWSHeader(JWSAlgorithm.HS256), claimsSet);
signedJWT.sign(signer);
return signedJWT.serialize();
}
}
If I run in this way it doesn't work because the endpoint is also an EJB
and because security context of EJB is not shared with the JAX-RS, the
method throws an 403 Forbidden. Is there any clean way to make that this
don't happen? Of course I can make JAXRS class as none EJB, but this would
be my last option.
Also is there a way to use @Inject in ContainerRequestFilter? Currently I
cannot use it because the instance is not injected.
Thank you so much.
Alex.