PAC4J + JAX-RS (Jersey) + Custom ExceptionMapper

594 views
Skip to first unread message

Sebastián Garcia

unread,
Jan 27, 2017, 9:35:30 AM1/27/17
to pac4j-users
Server: Payara 4.1

Hi again, I have a JAX-RS project where I have a custom exception mapper for exceptions.
My project have two Pac4j Clients: DirectFormClient for first use to get a JWT token and ParameterClient thas use the JWT token as authenticator.
When I secure my endpoints ang get 401 Unauthorized, my exception mapper is not executed and instead I receive a default 401 page (But I want to send a JSON instead, thats why I wrote an ExceptionMapper)

My Custom ExceptionMapper

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package ar.com.pucaratech.rest.exceptionmappers;

import ar.com.pucaratech.dtos.ResponseComprobantes;
import java.util.Arrays;
import java.util.List;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Variant;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import org.pac4j.core.exception.TechnicalException;

/**
 *
 * @author Seba
 */

@Provider
public class ErrorExceptionMapper extends Throwable implements ExceptionMapper<Exception> {

   
@Context
   
private javax.inject.Provider<Request> request;

   
@Override
   
public Response toResponse(Exception exception) {        

       
Response.ResponseBuilder response = Response.status(Response.Status.INTERNAL_SERVER_ERROR);

       
if (exception instanceof TechnicalException) {
            response
= Response.status(Response.Status.UNAUTHORIZED);
       
} else if (exception instanceof BadRequestException) {
            response
= Response.status(Response.Status.BAD_REQUEST);
       
} else if (exception instanceof NotAuthorizedException) {
            response
= Response.status(Response.Status.UNAUTHORIZED);
       
} else if (exception instanceof NotFoundException) {
            response
= Response.status(Response.Status.NOT_FOUND);
       
} else {
            response
= Response.status(Response.Status.INTERNAL_SERVER_ERROR);
       
}

       
// Entity
       
final List<Variant> variants = Variant.mediaTypes(
               
MediaType.APPLICATION_JSON_TYPE,
               
MediaType.APPLICATION_XML_TYPE
       
).build();
       
final Variant variant = request.get().selectVariant(variants);
       
if (variant != null) {
            response
.type(variant.getMediaType());
       
} else {
           
/*
                 * default media type which will be used only when none media type from {@value variants} is in
                 * accept header of original request.
             */

            response
.type(MediaType.TEXT_PLAIN_TYPE);
       
}

       
ResponseComprobantes rc = new ResponseComprobantes();
        rc
.setResultado(ResponseComprobantes.RESULTADO_ERROR);

       
if (exception instanceof TechnicalException && exception.getMessage()
               
.equals("Cannot decrypt / verify JWT")) {
            rc
.setErrores(Arrays.asList("Token inválido. No se pudo verificar el token"));
       
} else {
            rc
.setErrores(Arrays.asList(exception.getMessage()));
       
}

        response
.entity(
               
new GenericEntity<ResponseComprobantes>(
                        rc
,
                       
new GenericType<ResponseComprobantes>() {
                       
}.getType()
               
)
       
);
       
return response.build();
   
}

}


Any Ideas?
The funny thing is that If I call and endpoint with an invalid JWT token My exception mapper is called.

Example:

1) Without the token parameter I receive 401 Unauthorized (Default 401 page)
2) With an invalid token My exception mapper is called

victo...@gmail.com

unread,
Jan 27, 2017, 11:08:12 AM1/27/17
to pac4j-users
Hi,

I think you hit a limitation of the current implementation of jax-rs-pac4j.

Basically, in case of 401 by pac4j, there is no Exception thrown, so your ExceptionMapper is never called.
I will have to investigate if it is possible to actually throw an Exception in the Filter: usually ExceptionMappers are for Exceptions in Resources, but it may work :)

Could you please open an issue in jax-rs-pac4j with the use case? I will see this week-end what can be done, or if there is an alternative way of doing the same thing!

Thanks :)

Sebastián Garcia

unread,
Jan 27, 2017, 4:49:13 PM1/27/17
to pac4j-users
Victor:
 Thanks for the reply. The issue was created https://github.com/pac4j/jax-rs-pac4j/issues/13

Sebastián Garcia

unread,
Jan 31, 2017, 7:49:22 AM1/31/17
to pac4j-users
For the record, I tried ContainerResponseFilter to customize 401 Unauthorized and 403 Forbidden and works like a charm. Thanks Victor and leleuj for the help.

victo...@gmail.com

unread,
Feb 1, 2017, 3:20:44 AM2/1/17
to pac4j-users
Good to know, thanks for the final feedback :)

Richard Walker

unread,
Mar 27, 2017, 2:40:24 AM3/27/17
to pac4j-users

On Tuesday, January 31, 2017 at 11:49:22 PM UTC+11, Sebastián Garcia wrote:
For the record, I tried ContainerResponseFilter to customize 401 Unauthorized and 403 Forbidden and works like a charm. Thanks Victor and leleuj for the help.

For the record ... would you mind posting your working version?
Because I want to do what you are doing.

Richard Walker

unread,
Mar 27, 2017, 3:22:32 AM3/27/17
to pac4j-users

OK, so I tried myself.
It is not hard at all. Here is a "first draft", that matches 403s and adds an entity in an appropriate format (which for me, is either JSON or XML). (If you do not already have HttpStatus in your project, use some other convenient constant equal to 403.)

Is there now a better way?


package <your package goes here>;

import java.io.IOException;
import java.util.List;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.Provider;

import org.apache.http.HttpStatus;

import <your package goes here>.ErrorResult;

@Provider
public class AuthenticationErrorResponseFilter
   
implements ContainerResponseFilter {

   
/** Intercept outgoing 403s, and add an entity to the response
     * in an appropriate MediaType.
     */

   
@Override
   
public void filter(final ContainerRequestContext requestContext,
           
final ContainerResponseContext responseContext)
                   
throws IOException {
       
if (responseContext.getStatus() == HttpStatus.SC_FORBIDDEN) {
           
MediaType requestMediaType = requestContext.getMediaType();
           
List<MediaType> acceptableMediaTypes =
                    requestContext
.getAcceptableMediaTypes();
           
MediaType responseMediaType;
           
if (acceptableMediaTypes == null
                   
|| acceptableMediaTypes.isEmpty()
                   
|| acceptableMediaTypes.contains(requestMediaType)) {
                responseMediaType
= requestMediaType;
           
} else {
                responseMediaType
= acceptableMediaTypes.get(0);
           
}
            responseContext
.setEntity(new ErrorResult("Forbidden"),
                   
null, responseMediaType);
       
}
   
}
}



ErrorResult is a simple POJO with a JAXB annotation. E.g.,:

package <your package goes here>;

import javax.xml.bind.annotation.XmlRootElement;

/** Class for representing an error result for an API call. */
@XmlRootElement(name = "error")
public class ErrorResult {

   
/** The text of the error message. */
   
private String message;

   
/** Default constructor.
     */

   
public ErrorResult() {
   
}

   
/** Constructor that takes the text of the error message as a parameter.
     * @param aMessage The text of the error message.
     */

   
public ErrorResult(final String aMessage) {
        message
= aMessage;
   
}

   
/** Set the text of error message.
     * @param aMessage The text of the error message.
     */

   
public void setMessage(final String aMessage) {
        message
= aMessage;
   
}

   
/** Get the text of the error message.
     * @return The text of the error message.
     */

   
public String getMessage() {
       
return message;
   
}

}



It's a bit yucky. Since I am already using jax-rs-pac4j, I would prefer to be able to have this as a feature that I register with context.register(...).

victo...@gmail.com

unread,
Mar 27, 2017, 3:38:37 AM3/27/17
to pac4j-users
Maybe you would have more help asking on a JAX-RS or Jersey-related mailing list (even though the official jersey ml is not very active…), because there is not so much advanced users of these APIs here I think… :)

Are you sure you need to bother with the media type like you do? I wouldn't have bothered with it personally, I would have just called setEntity and let Jersey handle things from there… not sure it is the best though ^^

Sorry for not being much helpful!

Richard Walker

unread,
Mar 27, 2017, 3:50:39 AM3/27/17
to pac4j-users
On Monday, March 27, 2017 at 6:38:37 PM UTC+11, victo...@gmail.com wrote:
Are you sure you need to bother with the media type like you do? I wouldn't have bothered with it personally, I would have just called setEntity and let Jersey handle things from there… not sure it is the best though ^^

Oh!

I must have done something wrong the first time, because it wasn't working.
(Maybe I clicked the wrong button in Swagger-UI?)
But you have encouraged me to try again ... and it works perfectly.
Here is the simplified version:

    public void filter(final ContainerRequestContext requestContext,
           
final ContainerResponseContext responseContext)
                   
throws IOException {
       
if (responseContext.getStatus() == HttpStatus.SC_FORBIDDEN) {

            responseContext
.setEntity(new ErrorResult("Forbidden"));
       
}
   
}



Richard Walker

unread,
Mar 27, 2017, 10:37:08 PM3/27/17
to pac4j-users
On Monday, March 27, 2017 at 6:50:39 PM UTC+11, Richard Walker wrote:
On Monday, March 27, 2017 at 6:38:37 PM UTC+11, victo...@gmail.com wrote:
Are you sure you need to bother with the media type like you do? I wouldn't have bothered with it personally, I would have just called setEntity and let Jersey handle things from there… not sure it is the best though ^^

Oh!

I must have done something wrong the first time, because it wasn't working.
(Maybe I clicked the wrong button in Swagger-UI?)
But you have encouraged me to try again ... and it works perfectly.

I really did manage to confuse myself.
In fact, my "simplified" version doesn't work: in my case, it only ever returns JSON, i.e., even if I add a request header "Accept: application/xml".

I think this is because Jersey does its "magic" (determineResponseMediaType() in the MethodSelectingRouter class) before the ContainerResponseFilters are applied. And at that stage, there is still no entity to be returned, so no MediaType is set for the response. And it isn't clear how to apply the "magic" again after the ContainerResponseFilters are applied.

I found another way, by using a modified version of JaxRsHttpActionAdapter:

package <your package goes here>;

import org.apache.http.HttpStatus;
import org.pac4j.core.http.HttpActionAdapter;
import org.pac4j.jax.rs.pac4j.JaxRsContext;


import <your package goes here>.ErrorResult;

/** An HttpActionAdapter based on JaxRsHttpActionAdapter, but which
 * returns an entity if authentication fails.
 */

public class AuthHttpActionAdapter
   
implements HttpActionAdapter<Object, JaxRsContext> {

   
/** Singleton instance of this class. */
   
public static final AuthHttpActionAdapter INSTANCE =
           
new AuthHttpActionAdapter();

   
/** For an authentication failure, insert an error response. */
   
@Override
   
public Object adapt(final int code, final JaxRsContext context) {
       
if (code == HttpStatus.SC_FORBIDDEN) {
        context
.getRequestContext().abortWith(
                context
.getAbortBuilder().
                entity
(new ErrorResult("Forbidden too")).
                build
());
       
} else {
          context
.getRequestContext().abortWith(
                  context
.getAbortBuilder().build());
       
}
       
return null;
   
}

}

And I register this at configuration time:

          configInstance.setHttpActionAdapter(
                 
AuthHttpActionAdapter.INSTANCE);


In this case, the Jersey magic does get applied at the "right" time.

victo...@gmail.com

unread,
Mar 28, 2017, 3:16:28 AM3/28/17
to pac4j-users
Actually, that's quite clever I think to work at that level.
It means it will only be applied to pac4j answers, so it's not always the best fit, but if it is your case, it is better to work at that place yes!

Richard Walker

unread,
Mar 28, 2017, 3:45:44 AM3/28/17
to pac4j-users
On Tuesday, March 28, 2017 at 6:16:28 PM UTC+11, victo...@gmail.com wrote:
Actually, that's quite clever I think to work at that level.
It means it will only be applied to pac4j answers, so it's not always the best fit, but if it is your case, it is better to work at that place yes!

Indeed, it only works for responses that come from within pac4j.

I will have to find another way to deal with empty responses that
come from other places (e.g. 400 (bad request) responses generated
when unmarshalling input data to a @POST method).
So I may have to have a ContainerResponseFilter for those
cases anyway.

Reply all
Reply to author
Forward
0 new messages