Some applications rely on JAAS to be portable in multiple environment and bypass Servlet 3.0 authentication. This can be useful is you need to run in a Servlet container but also an OSGi one like Karaf or even a plain standalone application. However when in your Servlet container it requires some advanced setup and prevent you to rely on your container security mecanism which is quite a pitfall.

Reminder: Servlet security API vs JAAS

As a quick reminder here is what Servlet 3.x proposes you:

httpRequest.login(user, password);
Principal pcp = httpRequest.getUserPrincipal();

On another side JAAS requires a LoginModule:

public interface LoginModule {
  void initialize(Subject subject, CallbackHandler callbackHandler,
                  Map<String, ?> sharedState, Map<String, ?> options);
  boolean login() throws LoginException;
  boolean commit() throws LoginException;
  boolean abort() throws LoginException;
  boolean logout() throws LoginException;
}

The bridge

The bridge idea is simple: capture the subject and handlers in initialize() then call the request login() method in login() and in commit() add the principal to the subject - of course abort() and logout() would cleanup the state if needed.

Issue is: how do I get the request from JAAS? You can write a filter, set the request in a ThreadLocal<HttpServletRequest> and just use that reference but if you run in a CDI container like TomEE or Meecrowave for instance, you can just lookup the request from CDI:

final BeanManager beanManager = CDI.current().getBeanManager();
final HttpServletRequest request =
        HttpServletRequest.class.cast(
                beanManager.getReference(
                        beanManager.resolve(beanManager.getBeans(HttpServletRequest.class)),
                        HttpServletRequest.class,
                        beanManager.createCreationalContext(null)));

So globally here is your login module:

public class ServletLoginModule implements LoginModule {
    private Subject subject;
    private CallbackHandler callbackHandler;

    private Principal principal;

    @Override
    public void initialize(final Subject subject, final CallbackHandler callbackHandler,
                           final Map<String, ?> sharedState, final Map<String, ?> options) {
        this.subject = subject;
        this.callbackHandler = callbackHandler;
    }

    @Override
    public boolean login() throws LoginException {
        final Callback[] callbacks = new Callback[2];
        callbacks[0] = new NameCallback("Username: ");
        callbacks[1] = new PasswordCallback("Password: ", false);
        try {
            callbackHandler.handle(callbacks);
        } catch (IOException | UnsupportedCallbackException e) {
            throw new LoginException(e.toString());
        }

        final BeanManager beanManager = CDI.current().getBeanManager();
        final HttpServletRequest request =
                HttpServletRequest.class.cast(
                        beanManager.getReference(
                                beanManager.resolve(beanManager.getBeans(HttpServletRequest.class)),
                                HttpServletRequest.class,
                                beanManager.createCreationalContext(null)));

        try {
            request.login(NameCallback.class.cast(callbacks[0]).getName(), new String(PasswordCallback.class.cast(callbacks[1]).getPassword()));
        } catch (final ServletException e) {
            throw new LoginException(e.getMessage());
        }

        principal = request.getUserPrincipal();

        return principal != null;
    }

    @Override
    public boolean commit() throws LoginException {
        if (!subject.getPrincipals().contains(principal)) {
            subject.getPrincipals().add(principal);
            if (GenericPrincipal.class.isInstance(principal)) {
                final String roles[] = GenericPrincipal.class.cast(principal).getRoles();
                for (final String role : roles) {
                    subject.getPrincipals().add(new GenericPrincipal(role, null, null));
                }

            }
        }
        return true;
    }

    @Override
    public boolean abort() throws LoginException {
        principal = null;
        return true;
    }

    @Override
    public boolean logout() throws LoginException {
        subject.getPrincipals().remove(principal);
        return true;
    }
}

Here in commit we have a little trick using Tomcat GenericPrincipal to get back the roles but it is not always needed - depends your application.

Setup this realm

To setup this realm you need to configure it in your JAAS config file:

myrealm {
   com.rmannibucau.blog.ServletLoginModule required;
};

Then if you login using the realm myrealm you will use our servlet login module and therefore servlet security.

However you can go further if you manage the boot of your application.

Suppose we extend javax.security.auth.login.Configuration in a class called MyConfig this way:

public class MyConfig extends Configuration {
    private final AppConfigurationEntry[] entries = new AppConfigurationEntry[]{
      new AppConfigurationEntry(ServletLoginModule.class.getName(), AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, new HashMap<>())
    };

    @Override
    public AppConfigurationEntry[] getAppConfigurationEntry(final String name) {
        return "myrealm".equals(name) ? entries : null;
    }
}

and if you set the Security property login.configuration.provider to be this class:

java.security.Security.setProperty(key, MyConfig.class.getName());

Then you don't need to set any JAAS system property or use any login.config file. Everything is in memory :).

Conclusion

JAAS is not the easiest API ever but it is flexible enough to match today's embedded applications (microservices?). It is also not very hard to bridge it with servlet containers to have a portable accross most environments security layer.

Of course this code only works if authentication is done with a request otherwise you need a fallback but this is a rare usage.

From the same author:

In the same category: