Wednesday, November 9. 2011
Certificate Security in JavaEE - Standard Solution (Glassfish)
Today I am going to present the last entry of the series about certificate security in a JavaEE application. If you remember the first post dealt with the environment setup (not very interesting in terms of development but a nice guide about a quick CA and secure server installation) and the second one commented custom security using client certificates (the application itself mapped the certificates and real users and groups). This last entry will present client certificates authentication but using standard JavaEE security.
JavaEE uses security based on roles, each user will have some roles, and depending these roles he can or cannot perform the specific application actions. Talking about what can be protected JavaEE adds security to two types of resources in a direct way: any web resource (pages, servlets,...) and Enterprise Java Beans (EJB) (the whole bean and/or specific methods of the bean). The security can be mapped via the deployment descriptor, web.xml in case of the web resources (security-constraint directive) and ejb-jar.xml in case of EJBs (method-permission and so on), or using annotations (@DeclareRoles, @RolesAllowed, @PermitAll or @DenyAll). The JavaEE API also offers methods to know the roles of a user and perform a programmatic security (isUserInRole in HttpServletRequest and isCallerInRole in EJBContext). So the idea is pretty clear, based on the roles of the user the application has to manage its own security which can be declarative (assembler files or annotations), programmatic (API) or both.
So now that JavaEE security is understood we need to know how the roles are assigned to a specific user. Basically the roles are defined for the application (again assembler file descriptors or annotations) and then they are mapped against users and groups inside some repository. JavaEE manages the concept of realms, which are stores for users and groups associated to the container. The containers perform the mapping between them and the application roles via a proprietary file descriptor (glassfish-web.xml, weblogic.xml,...) and/or using the container management console (case of IBM WebSphere). Each application server manages the realms in its own way, they can be more or less powerful (for example weblogic has clearly a much more customizable realms than glassfish or tomcat).
The last part of theory which needs to be commented is the login issue. Once the JavaEE application has added role restrictions it needs to establish a login method. The standard configures the login inside the login-config part of the web.xml. It permits the following methods for authentication: NONE, BASIC, DIGEST, FORM or CLIENT-CERT. This login is also associated to a realm in the container. So now it is clear, when an application establishes JavaEE security, it defines a realm, a login method and different roles; the users who try to access the application will need to login first and, in this process, they get all the groups from the selected realm (in JavaEE jargon the user and groups obtained from the realm are called principals); these groups are mapped against the app roles; finally the roles are used to implement security (declarative or programmatic) inside the application. As you see the circle is now closed.
JavaEE is sometimes quite overelaborated, but be sure that the ideas behind it are very very simple. You can read the JavaEE tutorial (current version 6) for further information about its security model.
Going back to our particular case of client certificates, it is clear that CLIENT-CERT authentication login method has to be selected. And, as it is a standard defined login method, everything should work smoothly, but Glassfish is not the case. What I want you to have in mind is that now all this stuff is in the hands of your container, we are talking about JavaEE, therefore the way login and mapping are handled does not depend on the application but on the container itself.
I chose glassfish v3 as my application server for this series and it has a special certificate realm, this realm is always used when CLIENT-CERT login is specified for an application (this part is not configurable). By default it only gets the subject of the certificate and sets it as the user principal, but nothing else, so forget about ldap, groups or whatever. Nevertheless glassfish joins realm implementations to a JAAS (Java Authentication and Authorization Service) login module context. A JAAS context can specify several (and custom) login modules, this way a custom certificate login module can be implemented to get the user from the certificate and match him to a user in the company repository. This way I defined a new login module inside the domains/domain1/config/login.conf:
certLdapRealm { sample.loginmodule.CertLdapLoginModule required realm=ldap; };
I created a CertLdapLoginModule.java which extends AppservCertificateLoginModule, an abstract class that glassfish explicitly provides to implement custom certificate login modules. My new class only has to implement the authenticateUser method, in here, my idea is clear: getting the user from the subject (same as in custom security, the user login is the CN part of the certificate subject) and querying the ldap with it in order to obtain the user and groups.
I first thought reusing the glassfish LDAPRealm for all the ldap stuff but this realm has all methods declared as private and I could not find a way of searching the user. The class is declared final so it cannot be extended either. Finally I copied the class code and a new LDAPRealm.java was created. It is almost the same code except the method findAndBind, which now receives a new boolean parameter checkPassword, if it is false no bind is performed and the user and groups are retrieved without password.
You have noticed that the certLdapRealm JAAS definition defines a realm option parameter, this parameter marks the realm which implements the new LDAPRealm class. The code inside my custom login class (autheticateUser method) is the following:
protected void authenticateUser() throws LoginException { String realm = (String) _options.get(REALM_OPTION); if (realm == null) { throw new LoginException( "LoginModule has not 'realm' option to get the LDAP realm!"); } try { // get the user from the certificate LdapName name = new LdapName(this.getX500Principal().getName()); String certName = name.getRdn(name.size() - 1).getValue().toString(); // get the realm LDAPRealm ldap = (LDAPRealm) Realm.getInstance(realm); if (ldap == null) { throw new LoginException("Realm'" + realm + "' does not exists!"); } String[] grpList = ldap.findAndBind(certName, null, false); // add the short user as a user principal // LoginContextDriver sets the cert subject as the name of the user // this let the user to set roles based on user (uid) names getSubject().getPrincipals().add(new PrincipalImpl(certName)); // add groups => commit sets them as group principals commitUserAuthentication(grpList); } catch (Exception e) { throw new LoginException(e.getMessage()); } }
The method gets the LDAPRealm using the specified option. The CN part of the certificate subject is read in order to query the ldap. The custom findAndBind method is used with false argument (do not bind the user). If everything goes well the groups that have been returned are passed to be transformed into principals. Check that the user himself is added as a principal before, this is done cos the certificate realm uses the complete DN as username (now both ways, complete DN or just the UID part can be used in the role mapping file).
Finally the certificate realm inside glassfish has to be configured to use the new JAAS module. This was done directly in the domain.xml file (asadmin can also be used for this configuration):
<auth-realm name="certificate" classname="com.sun.enterprise.security.auth.realm.certificate.CertificateRealm"> <property name="jaas-context" value="certLdapRealm"></property> <property name="assign-groups" value="allusers"></property> </auth-realm>
The jaas-context property specifies the JAAS context to be used (it was set to certLdapRealm). The other property assign-groups are groups that are directly assigned to any user who successfully logs in this realm (they are used in web.xml for specifying any authenticated user).
Finally the LDAPRealm (used inside the custom login module) has to be configured in glassfish too:
<auth-realm name="ldap" classname="sample.realm.LDAPRealm"> <property name="directory" value="ldap://debian.demo.kvm:389"></property> <property name="base-dn" value="dc=demo,dc=kvm"></property> <property name="jaas-context" value="myLdapRealm"></property> <property name="search-bind-dn" value="cn=admin,dc=demo,dc=kvm"></property> <property name="search-bind-password" value="****"></property> <property name="assign-groups" value="allusers"></property> </auth-realm>
This is a normal configuration for a glassfish LDAPRealm (except that my class is used instead of the glassfish default). Please check that the realm name is ldap, exactly the same name which is passed to my certLdapRealm JAAS context in the login.conf file. The extra realm and login module classes were packaged into a jar and placed inside domains/domain1/lib directory (see this article for more information about custom realms and login modules in glassfish).
I know that all of this is quite confusing, so I am going to repeat the idea. The default certificate realm now depends on a custom certLdapRealm JAAS context. This context executes a custom CertLdapLoginModule which uses another realm ldap (custom LDAPRealm but with minimal differences from the glassfish default) to search the user and his groups in the LDAP repository. In my opinion glassfish realms and login modules are too dependent on each other, they always generate cross references, and that generates some mess.
So now everything is correctly configured to deploy a JavaEE security application which uses client certificates. I deployed the same application that was used in the custom security entry, but now it does not need all ldap stuff so it is almost an empty app (only some facelets pages and little helper Java classes). The interesting part of this application is inside the descriptor (web.xml) file. It defines the following constraint:
<security-constraint> <display-name>CertSecurityJavaEE constraint</display-name> <web-resource-collection> <web-resource-name>Servlet Application</web-resource-name> <url-pattern>/faces/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>allusers</role-name> </auth-constraint> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint>
All faces pages (pattern /faces/*) are declared inside the constraint. The role allusers is declared as needed to access the application (any user authenticated in the realm). Besides the constraint declares the data as confidential, this means that the application has to be used securely (over HTTPS). The login part is as follows:
<login-config> <auth-method>CLIENT-CERT</auth-method> <realm-name>ldap</realm-name> <form-login-config> <form-login-page>/faces/login.xhtml</form-login-page> <form-error-page>/faces/login.xhtml</form-error-page> </form-login-config> </login-config>
I declared CLIENT-CERT which means the user needs to present a certificate to get access (as I said glassfish uses the certificate realm for this configuration, and all the previous configuration comes into stage). And finally some roles are defined for the application:
<security-role> <description>Users Role</description> <role-name>allusers</role-name> </security-role> <security-role> <description>StaticGroup1 Role</description> <role-name>StaticGroup1</role-name> </security-role> <security-role> <description>DynGroupPreSales Role</description> <role-name>DynGroupPreSales</role-name> </security-role> <security-role> <description>SampleClient1 Role</description> <role-name>SampleClient1</role-name> </security-role>
The application manages four roles:
- allusers: All the users of the application (any authenticated user).
- StaticGroup1: The static group of the ldap (I used it in the previous entry).
- DynGroupPreSales: The dynamic group of the ldap (I also used it before).
- SampleClient1: This is an example user role, only the SampleClient1 is part of this role. I added this role to check if my login module handles user short login name (UID in LDAP) correctly.
As I said glassfish used a specific glassfish-web.xml descriptor file to map the roles against the users and groups in the realm, here it is my mapping:
<security-role-mapping> <role-name>allusers</role-name> <group-name>allusers</group-name> </security-role-mapping> <security-role-mapping> <role-name>StaticGroup1</role-name> <group-name>StaticGroup1</group-name> </security-role-mapping> <security-role-mapping> <role-name>DynGroupPreSales</role-name> <group-name>DynGroupPreSales</group-name> </security-role-mapping> <security-role-mapping> <role-name>SampleClient1</role-name> <principal-name>SampleClient1</principal-name> </security-role-mapping>
Basically I mapped all roles to the so-called LDAP group (remember allusers is a nonexistent ldap group, which is automatically added by the realm when a user is authenticated, it was defined in the assign-groups property of both realms) except SampleClient1 which is mapped against the same username (I used the short CN name, the one that is added by my custom login module).
Now the application is ready, but please think about it, how does it work? When the user access to the application he needs to present a certificate (it is compulsory) and there is no other choice. As you can imagine this is too much restrictive in common situations. In other JavaEE containers a fallback authentication method can be configured (CLIENT-CERT,FORM for example in weblogic) but not in glassfish. So I decided to deploy the same application twice, one with CLIENT-CERT authentication (/CertSecurityJavaEE-cert context) and the other with FORM (/CertSecurityJavaEE context). If you check the previously presented login part of the web.xml the ldap realm is specified (remember that glassfish uses certificate realm for CLIENT-CERT authorization no matter the configuration, so this ldap realm is only used when FORM authentication is selected). In order to use my custom LDAPRealm class here I needed to implement a custom LDAPLoginModule.java that extends glassfish default (because the damned cross references), but you can ignore this, it is too confusing even for me who implemented the solution.
The CLIENT-CERT option makes unnecessary the client authentication configuration we did in the secure listener of glassfish (check the first entry of this series). If CLIENT-CERT authentication method is established for an application the app server automatically requests a client certificate for its context in the secure port. So I unchecked this option and now a user without certificate is able to access the FORM defined application through the same secure port (the other /CertSecurityJavaEE context). Besides an HTML error page for 401 error code (Unauthorized) was configured for the application. What does it mean? Now a user who presents a certificate in /CertSecurityJavaEE-cert but has no mapped user in the LDAP server will receive a custom 401 error page, this page will show a link against the FORM authenticated application. This is configured (standard way) in the web.xml file.
<error-page> <error-code>401</error-code> <location>/resources/errorpages/401.html</location> </error-page>
Here it is the video. Now when I access to the certificate application (/CertSecurityJavaEE-cert) a certificate is requested. Same as before if I select the revoked certificate (03) firefox requests me to choose another one because it is revoked, if I pick up the valid one (02) I just enter as SampleClient1 (see all the roles are granted and the long certificate subject is used as username). Now I cannot logout cos once you have selected a certificate for a site the browser uses always it for this site (it is the same case than in custom security). Then I clear my sessions in firefox (this logs me out to try again) and I try to enter with a new certificate, created for SampleClient3 with serial number 05 but without a mapped real user, so this time my login module fails to match a user and the 401 page is reached. There I can click the presented link and I access the non-certificate (/CertSecurityJavaEE) application. Finally I log in using SampleClient2 (remember this user has a revoked certificate but he is a valid user in the LDAP server). He has no role assigned as expected and now the user name is the short one.
The moral for standard JavaEE certificate login is that it depends on the container implementation, the application just needs to configure CLIENT-CERT authentication. As you see glassfish realms (and the certificate one particularly) are quite confusing, there is a lot of mess between realms and login modules and it is specially painful using or extending its default classes (I do not know why but they have a lot of private methods and they are usually declared as final). The best I could do was implementing a custom login module which was associated to the certificate realm, this login class performs the mapping between certificate and user (groups included). Besides the application was deployed twice, one requests client certificates and the other uses typical username and password login, this way users without a certificate have a less secure but functional way to login. For this entry two projects were developed, the library project with realms and login modules and the Web Application project. My personal conclusion is that all of this is so particular for glassfish that a different entry like this one is needed for every JavaEE container.
You win some, you lose some. Cheerio!
Comments