Tuesday, March 13, 2012
Java EE 6 Security
Every time that a new major release of the JBoss application server comes out it is always a challenge to see how the new security framework features could fit our needs for the construction of highly secured web applications. In this article I will walk you through every layer of your application and show you how to enable the new security features that JBoss AS 7.1 brings.
Securing the view pages
It all starts with securing the JSF pages of your web application. This can easily be done using JBoss Seam 3.1. The first thing that you want to do is to define the roles that authenticated users can have within your system. In this example we construct a simple @User
role as follows.
import org.jboss.seam.security.annotations.SecurityBindingType; import org.jboss.seam.faces.security.RestrictAtPhase; import org.jboss.seam.faces.event.PhaseIdType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.annotation.ElementType; @SecurityBindingType @RestrictAtPhase(PhaseIdType.RESTORE_VIEW) @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) public @interface User { }
Next we instruct Seam which pages should be protected under the custom defined role. For this we use JBoss Seam 3.1 Faces. In the following example we protect pages under /user/
so that they require an authenticated and @User
authorized user.
import org.jboss.seam.faces.view.config.ViewConfig; import org.jboss.seam.faces.view.config.ViewPattern; import org.jboss.seam.faces.security.LoginView; import org.jboss.seam.faces.security.AccessDeniedView; @ViewConfig public interface MyViewConfig { static enum MyPages { @ViewPattern("/user/*") @User USER_PAGES, @ViewPattern("/*") @LoginView("/login.xhtml") @AccessDeniedView("/denied.xhtml") ALL } }
Via the login.xhtml
page you somehow gather the user's credentials within the PicketBox credentials
bean. The #{identity.login}
action will trigger the Seam Security framework to authenticate the user. This requires an authenticator component to be present. The authenticator will simply use the credentials
and set the identity
after authenticating the user. Here you probably want to use some EJB3 component defined within your model to perform the actual authentication/authorization. In the following example this is done via the MyAuthenticationBean
EJB3 session bean.
import org.jboss.seam.security.BaseAuthenticator; import org.jboss.seam.security.Authenticator; import javax.inject.Inject; import org.jboss.seam.security.Credentials; import org.jboss.seam.security.Identity; import javax.ejb.EJB; import java.util.Set; public class MyAuthenticator extends BaseAuthenticator implements Authenticator { @Inject private Credentials credentials; @Inject private Identity identity; @EJB private MyAuthenticationBean myAuthenticationBean; @Override public void authenticate() { String username = this.credentials.getUsername(); ... credential = credentials.getCredential(); Set<String> roles = this.myAuthenticationBean.authenticate(username, credential); if (null == roles) { setStatus(AuthenticationStatus.FAILURE); return; } for (String role : roles) { this.identity.addRole(role, "USERS", "GROUP"); } setUser(new SimpleUser(username)); setStatus(AuthenticationStatus.SUCCESS); } }
As you can see the authenticator also adds the roles to the identity
.
Securing the controllers
The CDI controllers can now simply use the custom @User
annotations to protect their methods,
...
import javax.inject.Named; @Named public class MyController { @User public String myAction() { ... return "..."; } }
Of course we still need to tell Seam Security how to map from the roles to the custom defined @User
role annotation. For this we define a new CDI component that manages the actual authorizations.
import org.jboss.seam.security.Identity; import org.jboss.seam.security.annotations.Secures; public class MyAuthorization { @Secures @User public boolean authorizeUser(Identity identity) { return identity.hasRole("user", "USERS", "GROUP"); } }
So whenever Seam (CDI) needs a @User
authorization, it will call the authorizeUser
producer method.
Securing the model
For most applications you want to have the EJB3 business logic completely separate from the view/controllers. For example, because you have multiple web applications, or you want to have different interfaces towards the business logic (web application, SOAP web services, JSON). Eventually you want to have the same notion of authenticated/authorized users within your model's EJB3 session beans. An important design principle here is that you never can trust the outer layers of your application's architecture. So you want to have the model to re-verify the user's credentials. Or in a more advanced scheme you can generate a custom token (some HMAC or so) as part of the call to MyAuthenticationBean
that the view can use afterwards towards the model. This is where we need to activate some security domain on your EJB session beans. First of all we have to propagate the user credentials towards the model. For this we need to define a custom security domain within the JBoss AS 7.1 configuration.
<subsystem xmlns="urn:jboss:domain:security:1.1"> ... <security-domains> ... <security-domain name="my-security-domain-client" cache-type="default"> <authentication> <login-module code="org.jboss.security.ClientLoginModule" flag="required"> <module-option name="multi-threaded" value="true"/> <module-option name="restore-login-identity" value="true"/> </login-module> </authentication> </security-domain> </security-domains> </subsystem>
Via the ClientLoginModule
you can pass the user's credentials from the servlet container to the EJB3 container within the JBoss application server. There are several ways possible to login into the my-security-domain-client
security domain. One method is to use a custom servlet filter as shown in the following example.
import javax.servlet.annotation.WebFilter; import javax.servlet.*; import javax.inject.Inject; import org.jboss.seam.security.Identity; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; @WebFilter(urlPatterns = "/*") public class LoginFilter implements Filter { @Inject private Identity identity; @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { LoginContext loginContext; if (this.identity.isLoggedIn()) { ... user = (...) this.identity.getUser(); UsernamePasswordHandler usernamePasswordHandler = new UsernamePasswordHandler(user.getId(), user.getCredential()); try { loginContext = new LoginContext("my-security-domain-client", usernamePasswordHandler); loginContext.login(); } catch (LoginException e) { throw new ServletException(e.getMessage()); } } else { loginContext = null; } try { filterChain.doFilter(servletRequest, servletResponse); } finally { if (null != loginContext) { try { loginContext.logout(); } catch (LoginException e) { throw new ServletException(e.getMessage()); } } } } @Override public void destroy() { } }
Now that the front-end performed a JAAS based authentication we need to use the passed credentials somehow within a security domain dedicated to our EJB3 model. For this we define a custom JAAS login module, which we configure again within the JBoss AS as follows.
<subsystem xmlns="urn:jboss:domain:security:1.1"> ... <security-domains> ... <security-domain name="my-security-domain" cache-type="default"> <authentication> <login-module code="my.package.MyLoginModule" flag="required"/> </authentication> </security-domain> </security-domains> </subsystem>
Despite the fact that we defined the security domain globally, the custom JAAS login module can live within our EJB3 model itself. This custom JAAS login module looks as follows.
import javax.security.auth.spi.LoginModule; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.Subject; import javax.security.auth.login.LoginException; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.Callback; import org.jboss.security.SimplePrincipal; import org.jboss.security.SimpleGroup; public class MyLoginModule implements LoginModule { private CallbackHandler callbackHandler; private Subject subject; private String authenticatedUsername; private Set<String> authorizedRoles; @Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { this.subject = subject; this.callbackHandler = callbackHandler; } @Override public boolean login() throws LoginException { NameCallback nameCallback = new NameCallback("username"); PasswordCallback passwordCallback = new PasswordCallback("password", false); Callback[] callbacks = new Callback[]{nameCallback, passwordCallback}; try { this.callbackHandler.handle(callbacks); } catch (Exception e) { throw new LoginException(e.getMessage()); } String username = nameCallback.getName(); byte[] credential = passwordCallback.getPassword(); Set<String> roles = ... retrieve via some EJB3 session bean via InitialContext ... if (null == roles) { throw new LoginException("invalid login"); } this.authenticatedUsername = username; this.authorizedRoles = roles; return true; } @Override public boolean commit() throws LoginException { if (null != this.authenticatedUsername) { this.subject.getPrincipals().add(new SimplePrincipal(this.authenticatedUsername)); SimpleGroup rolesGroup = new SimpleGroup("Roles"); for (String role : this.authorizedRoles) { rolesGroup.add(new SimplePrincipal(role)); } this.subject.getPrincipals().add(rolesGroup); } return true; } @Override public boolean logout() throws LoginException { this.subject.getPrincipals().clear(); return true; } ... }
As you can see a JAAS login module is using a two-phase commit design. The task here is to populate the subject
using the callbackHandler
. During the login
we authenticate/authorize the user. This operation may fail if the user's credentials are invalid for example. During the commit
we simple commit the authentication transaction. In JBoss AS the EJB3 roles are passed via a Group
which is named Roles
.
Now we can finally use our security domain to protected the EJB3 session beans within our model. An example of such a protected session bean is given below.
import javax.ejb.Stateless; import org.jboss.ejb3.annotation.SecurityDomain; import javax.annotation.security.RolesAllowed; @Stateless @SecurityDomain("my-security-domain") public class MyProtectedBean { @RolesAllowed("my-model-role"); public void myProtectedBusinessMethod() { } }
You can get the caller principal within the session beans via the SessionContext
as shown in the following example code.
import javax.annotation.Resource; import javax.ejb.SessionContext; import java.security.Principal; ... @Resource private SessionContext sessionContext; ... { Principal callerPrincipal = this.sessionContext.getCallerPrincipal(); String callerName = callerPrincipal.getName(); ... }
Context aware input validation
Since Bean Validation is part of Java EE 6, input validation never has been easier before. The only thing that you have to keep in mind is that you have to do the activation of the Bean Validation yourself. As we're interested in input validation on our model session beans, we have to use a custom EJB3 interceptor to activate the Bean Validation on the methods. This is basically a copy of the BeanValidationAppendixInterceptor
of OpenEJB.
import javax.annotation.Resource; import javax.validation.Validator; import javax.interceptor.AroundInvoke; import javax.ejb.SessionContext; import java.lang.reflect.Method; import org.hibernate.validator.method.MethodValidator; import java.util.Set; import javax.validation.ConstraintViolationException; import javax.validation.ConstraintViolation; public class BeanValidationAppendixInterceptor { @Resource private Validator validator; @Resource private SessionContext sessionContext; @AroundInvoke public Object aroundInvoke(final InvocationContext ejbContext) throws Exception { Class<?> bvalClazzToValidate = ejbContext.getTarget().getClass(); if (this.sessionContext != null && ejbContext.getTarget().getClass().getInterfaces().length > 0) { bvalClazzToValidate = this.sessionContext.getInvokedBusinessInterface(); } Method method = ejbContext.getMethod(); if (!bvalClazzToValidate.equals(ejbContext.getTarget().getClass())) { method = bvalClazzToValidate.getMethod(method.getName(), method.getParameterTypes()); } MethodValidator methodValidator = this.validator.unwrap(MethodValidator.class); Set<?> violations = methodValidator.validateAllParameters(ejbContext.getTarget(), ejbContext.getMethod(), ejbContext.getParameters(), new Class[0]); if (violations.size() > 0) { throw new ConstraintViolationException((Set<ConstraintViolation<?>>) violations); } Object returnedValue = ejbContext.proceed(); violations = methodValidator.validateReturnValue(ejbContext.getTarget(), ejbContext.getMethod(), returnedValue, new Class[0]); if (violations.size() > 0) { throw new ConstraintViolationException((Set<ConstraintViolation<?>>) violations); } return returnedValue; } }
Funny that JBoss AS 7.1 doesn't provide this out-of-the-box. This would probably not look good on their benchmarks I guess.
So now we can use the Bean Validation framework to do input validation on our model session beans as follows.
import javax.ejb.Stateless; import javax.interceptor.Interceptors; import javax.validation.constraints.NotNull; @Stateless @Interceptors(BeanValidationAppendixInterceptor.class) public class MySessionBean { public void myMethod(@NotNull String message) { ... } }
Besides the validation annotations that are defined as part of the Bean Validation specification, we can of course also define our own validation annotations and corresponding validators. What we would like to do here is to make our custom validators EJB context aware. So for example, we can do something like:
import javax.ejb.Stateless; import javax.interceptor.Interceptors; @Stateless @Interceptors(BeanValidationAppendixInterceptor.class) public class MySessionBean { public void myMethod(@CheckOwnership MyEntity myEntity) { ... } }
Where the semantics of the @CheckOwnership
annotation is to check whether the caller principal owns the MyEntity
object. The validator corresponding to @CheckOwnership
will need to get access to the caller principal so it can indeed check whether the MyEntity
object is owned by the current caller. So let's define this validation annotation.
import java.lang.annotation.*; import javax.validation.Payload; import javax.validation.Constraint; @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = CheckOwnershipValidatorBean.class) public @interface CheckOwnership { String message() default "ownership error"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Via the @Constraint
annotation the Bean Validation framework knows which validator class it must use for validating parameters annotated with @CheckOwnership
. Here the validator is actually an EJB3 session bean as shown in the following example.
import javax.ejb.Stateless; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import javax.persistence.PersistenceContext; import javax.persistence.EntityManager; import javax.annotation.Resource; import javax.ejb.SessionContext; @Stateless public class CheckOwnershipValidatorBean implements ConstraintValidator<CheckOwnership, MyEntity> { @PersistenceContext private EntityManager entityManager; @Resource private SessionContext sessionContext; @Override public void initialize(CheckOwnership checkOwnership) { } @Override public boolean isValid(MyEntity myEntityParam, ConstraintValidatorContext constraintValidatorContext) { MyEntity myEntity = this.entityManager.find(MyEntity.class, myEntityParam.getId()); String username = this.sessionContext.getCallerPrincipal().getName(); return username.equals(myEntity.getOwnerUsername()); } }
Of course the default validator factory does not support EJB3 session beans for validators.
So we need to define a custom ConstraintValidatorFactory
that is also capable of handling EJB3 session bean validators. In the following example we omitted the exception handling to ease reading.
import javax.validation.ConstraintValidatorFactory; import javax.validation.ConstraintValidator; import javax.ejb.Stateless; import javax.ejb.Stateful; import javax.naming.InitialContext; public class SessionBeanConstraintValidatorFactory implements ConstraintValidatorFactory { @Override public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> tClass) { Stateless statelessAnnotation = tClass.getAnnotation(Stateless.class); Stateful statefulAnnotation = tClass.getAnnotation(Stateful.class); if (null != statelessAnnotation || null != statefulAnnotation) { InitialContext initialContext = new InitialContext(); T validator = (T) initialContext.lookup("java:module/" + tClass.getSimpleName()); return validator; } return tClass.newInstance(); } }
This custom validator factory can be configured within the META-INF/validation.xml
file as follows.
<validation-config xmlns="http://jboss.org/xml/ns/javax/validation/configuration"> <constraint-validator-factory>my.package.SessionBeanConstraintValidatorFactory</constraint-validator-factory> </validation-config>