11

Preface

First of all, my sincerest apologies for this question being extremely long, but I honestly have no idea on how to shorten it, since each part is kind of a special case. Admittedly, I may be blind on this since I am banging my head against the wall for a couple of days now and I am starting to get desperate.

My utmost respect and thankfulness to all of you who read through it.

The aim

I would like to be able to map Shiro's AuthenticationException and it's subclasses to JAX-RS Responses by using Jersey ExceptionMappers, set up using a Guice 3.0 Injector which creates an embedded Jetty.

The environment

  • Guice 3.0
  • Jetty 9.2.12.v20150709
  • Jersey 1.19.1
  • Shiro 1.2.4

The setup

The embedded Jetty is created using a Guice Injector

// imports omitted for brevity
public class Bootstrap {

    public static void main(String[] args) throws Exception {

      /*
       * The ShiroWebModule is passed as a class
       * since it needs a ServletContext to be initialized
       */
        Injector injector = Guice.createInjector(new ServerModule(MyShiroWebModule.class));

        Server server = injector.getInstance(Server.class);

        server.start();
        server.join();
    }
}

The ServerModule binds a Provider for the Jetty Server:

public class ServerModule extends AbstractModule {

    Class<? extends ShiroWebModule> clazz;

    public ServerModule(Class <?extends ShiroWebModule> clazz) {
        this.clazz = clazz;
    }

    @Override
    protected void configure() {
        bind(Server.class)
         .toProvider(JettyProvider.withShiroWebModule(clazz))
         .in(Singleton.class);
    }

}

The JettyProvider sets up a Jetty WebApplicationContext, registers the ServletContextListener necessary for Guice and a few things more, which I left in to make sure no "side effects" may be hidden:

public class JettyProvider implements Provider<Server>{

    @Inject
    Injector injector;

    @Inject
    @Named("server.Port")
    Integer port;

    @Inject
    @Named("server.Host")
    String host;

    private Class<? extends ShiroWebModule> clazz;

    private static Server server;

    private JettyProvider(Class<? extends ShiroWebModule> clazz){
        this.clazz = clazz;
    }

    public static JettyProvider withShiroWebModule(Class<? extends ShiroWebModule> clazz){
        return new JettyProvider(clazz);
    }

    public Server get() {       

        WebAppContext webAppContext = new WebAppContext();
        webAppContext.setContextPath("/");

        // Set during testing only
        webAppContext.setResourceBase("src/main/webapp/");
        webAppContext.setParentLoaderPriority(true);

        webAppContext.addEventListener(
          new MyServletContextListener(injector,clazz)
        );

        webAppContext.addFilter(
          GuiceFilter.class, "/*",
          EnumSet.allOf(DispatcherType.class)
        );

        webAppContext.setThrowUnavailableOnStartupException(true);

        QueuedThreadPool threadPool = new QueuedThreadPool(500, 10);

        server = new Server(threadPool);

        ServerConnector connector = new ServerConnector(server);
        connector.setHost(this.host);
        connector.setPort(this.port);

        RequestLogHandler requestLogHandler = new RequestLogHandler();
        requestLogHandler.setRequestLog(new NCSARequestLog());

        HandlerCollection handlers = new HandlerCollection(true);

        handlers.addHandler(webAppContext);
        handlers.addHandler(requestLogHandler);

        server.addConnector(connector);
        server.setStopAtShutdown(true);
        server.setHandler(handlers);
        return server;
    }

}

In MyServletContextListener, I created a child injector, which gets initialized with the JerseyServletModule:

public class MyServletContextListener extends GuiceServletContextListener {

    private ServletContext servletContext;

    private Injector injector;

    private Class<? extends ShiroWebModule> shiroModuleClass;
    private ShiroWebModule module;

    public ServletContextListener(Injector injector,
            Class<? extends ShiroWebModule> clazz) {
        this.injector = injector;
        this.shiroModuleClass = clazz;
    }

    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {

        this.servletContext = servletContextEvent.getServletContext();
        super.contextInitialized(servletContextEvent);

    }

    @Override
    protected Injector getInjector() {
        /*
         * Since we finally have our ServletContext
         * we can now instantiate our ShiroWebModule
         */
        try {
            module = shiroModuleClass.getConstructor(ServletContext.class)
                    .newInstance(this.servletContext);
        } catch (InstantiationException | IllegalAccessException
                | IllegalArgumentException | InvocationTargetException
                | NoSuchMethodException | SecurityException e) {
            e.printStackTrace();
        }

    /*
     * Now, we create a child injector with the JerseyModule
     */
        Injector child = injector.createChildInjector(module,
                new JerseyModule());

        SecurityManager securityManager = child
                .getInstance(SecurityManager.class);
        SecurityUtils.setSecurityManager(securityManager);

        return child;
    }

}

The JerseyModule, a subclass of JerseyServletModule now put everything together:

public class JerseyModule extends JerseyServletModule {

    @Override
    protected void configureServlets() {
        bindings();
        filters();
    }

    private void bindings() {

        bind(DefaultServlet.class).asEagerSingleton();
        bind(GuiceContainer.class).asEagerSingleton();
        serve("/*").with(DefaultServlet.class);
    }

    private void filters() {
        Map<String, String> params = new HashMap<String, String>();

    // Make sure Jersey scans the package
        params.put("com.sun.jersey.config.property.packages",
                "com.example.webapp");

        params.put("com.sun.jersey.config.feature.Trace", "true");

        filter("/*").through(GuiceShiroFilter.class,params);
        filter("/*").through(GuiceContainer.class, params);

        /* 
         * Although the ExceptionHandler is already found by Jersey
         * I bound it manually to be sure
         */
        bind(ExceptionHandler.class);

        bind(MyService.class);

    }

}

The ExceptionHandler is extremely straightforward and looks like this:

@Provider
@Singleton
public class ExceptionHandler implements
        ExceptionMapper<AuthenticationException> {

    public Response toResponse(AuthenticationException exception) {
        return Response
                .status(Status.UNAUTHORIZED)
                .entity("auth exception handled")
                .build();
    }

}

The problem

Now everything works fine when I want to access a restricted resource and enter correct principal/credential combinations. But as soon as enter a non-existing user or a wrong password, I want an AuthenticationException to be thrown by Shiro and I want it to be handled by the above ExceptionHandler.

Utilizing the default AUTHC filter provided by Shiro in the beginning, I noticed that AuthenticationExceptions are silently swallowed and the user is redirected to the login page again.

So I subclassed Shiro's FormAuthenticationFilter to throw an AuthenticationException if there is one:

public class MyFormAutheticationFilter extends FormAuthenticationFilter {

    @Override
    protected boolean onLoginFailure(AuthenticationToken token,
            AuthenticationException e, ServletRequest request,
            ServletResponse response) {
        if(e != null){
            throw e;
        }
        return super.onLoginFailure(token, e, request, response);
    }
}

And I also tried it with throwing the exception e wrapped in a MappableContainerException.

Both approaches cause the same problem: Instead of the exception being handled by the defined ExceptionHandler, a javax.servlet.ServletException is thrown:

  javax.servlet.ServletException: org.apache.shiro.authc.AuthenticationException: Unknown Account!
    at org.apache.shiro.web.servlet.AdviceFilter.cleanup(AdviceFilter.java:196)
    at org.apache.shiro.web.filter.authc.AuthenticatingFilter.cleanup(AuthenticatingFilter.java:155)
    at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:148)
    at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
    at org.apache.shiro.guice.web.SimpleFilterChain.doFilter(SimpleFilterChain.java:41)
    at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449)
    at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
    at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
    at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
    at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:383)
    at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362)
    at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
    at com.google.inject.servlet.FilterDefinition.doFilter(FilterDefinition.java:163)
    at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:58)
    at com.google.inject.servlet.ManagedFilterPipeline.dispatch(ManagedFilterPipeline.java:118)
    at com.google.inject.servlet.GuiceFilter.doFilter(GuiceFilter.java:113)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:585)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:577)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:223)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1127)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:515)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
    at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:110)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97)
    at org.eclipse.jetty.server.Server.handle(Server.java:499)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:310)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:257)
    at org.eclipse.jetty.io.AbstractConnection$2.run(AbstractConnection.java:540)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:635)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:555)
    at java.lang.Thread.run(Thread.java:744)
Caused by: org.apache.shiro.authc.AuthenticationException: Unknown Account!
    at com.example.webapp.security.MyAuthorizingRealm.doGetAuthenticationInfo(MyAuthorizingRealm.java:27)
    at org.apache.shiro.realm.AuthenticatingRealm.getAuthenticationInfo(AuthenticatingRealm.java:568)
    at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:180)
    at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:267)
    at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)
    at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)
    at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:270)
    at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:256)
    at org.apache.shiro.web.filter.authc.AuthenticatingFilter.executeLogin(AuthenticatingFilter.java:53)
    at org.apache.shiro.web.filter.authc.FormAuthenticationFilter.onAccessDenied(FormAuthenticationFilter.java:154)
    at org.apache.shiro.web.filter.AccessControlFilter.onAccessDenied(AccessControlFilter.java:133)
    at org.apache.shiro.web.filter.AccessControlFilter.onPreHandle(AccessControlFilter.java:162)
    at org.apache.shiro.web.filter.PathMatchingFilter.isFilterChainContinued(PathMatchingFilter.java:203)
    at org.apache.shiro.web.filter.PathMatchingFilter.preHandle(PathMatchingFilter.java:178)
    at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:131)
    ... 32 more

The question, after all

Given that the environment can't be changed, how can I achieve that a server instance still can be requested via Guice, while Shiro's exceptions are handled with Jersey's auto discovered ExceptionMappers?

Markus W Mahlberg
  • 19,711
  • 6
  • 65
  • 89

2 Answers2

3

This question is much too complicated for me to reproduce on my side, but I saw a problem that I think is the answer and I'll delete this answer if I turn out to be wrong.

You do this:

@Provider
@Singleton
public class ExceptionHandler implements
        ExceptionMapper<AuthenticationException> {

Which is correct, you are supposed to bind with both of those annotations as in this question. However, what you do differently is this:

/* 
 * Although the ExceptionHandler is already found by Jersey
 * I bound it manually to be sure
 */
bind(ExceptionHandler.class);

The annotations in a class definition have lower priority than that in a module's configure() method, meaning you are erasing the annotations when you bind "it manually just to be sure". Try erasing that line of code and see if that fixes your problem. If it doesn't fix the problem, leave it deleted anyway, because I am certain that it is at least part of the problem - that statement erases those essential annotations.

Community
  • 1
  • 1
durron597
  • 31,968
  • 17
  • 99
  • 158
  • First of all thanks for your answer. Sadly, this did not change anything ;) Tracing the log files, it seems to me that the `@Providers` found either by scanning or through binding are both registered with Jersey. As far as I tried, there does not seem to be any functional difference between the approaches, and even using both approaches do not seem to change any behavior. I tested this both with the exception handler and the service. – Markus W Mahlberg Aug 08 '15 at 08:16
  • @MarkusWMahlberg Another dumb, low hanging fruit attempt: Possible duplicate of this? http://stackoverflow.com/questions/27982948/jersey-exceptionmapper-doesnt-map-exceptions – durron597 Aug 08 '15 at 14:23
0

I've not found a way to do this either. It looks like the Jersey filters/handlers aren't active on the Shiro servlet stack during authentication. As a work-around specifically for the AuthenticationException I opted to override the AdviceFilter::cleanup(...) method on my AuthenticatingFilter and return a custom message directly.

public class MyTokenAuthenticatingFilter extends AuthenticatingFilter {

protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
    // regular auth/token creation
}

@Override
protected void cleanup(ServletRequest request, ServletResponse response, Exception existing) throws ServletException, IOException {

    HttpServletResponse httpResponse = (HttpServletResponse)response;
    if ( null != existing ) {
        httpResponse.setContentType(MediaType.APPLICATION_JSON);
        httpResponse.getOutputStream().write(String.format("{\"error\":\"%s\"}", existing.getMessage()).getBytes());
        httpResponse.setStatus(Response.Status.FORBIDDEN.getStatusCode());
        existing = null; // prevent Shiro from tossing a ServletException
    }
    super.cleanup(request, httpResponse, existing);
}
}

When authentication is successful the ExceptionMappers work fine for exceptions thrown within the context of the Jersey controllers.

Jeff
  • 3,549
  • 1
  • 14
  • 11