Saturday, October 22. 2011
Certificate Security in JavaEE - Custom Solution
This entry is the second chapter of the JavaEE certificate security series. The previous post explained how all the environment was configured (OpenSSL demo CA and glassfish application server). Now it is time to deploy an application in the container and start using the certificates. This entry will deal with custom security, custom means that the application will use a repository for users and groups by its own, implementing the login classes and not JavaEE standard.
Before commenting the application let's explain how a web application usually works with certificates. If you remember the first entry, the client certificate is validated by the application server (or the web server in general) before any code of the application is executed (in the video the certificate validation is performed even no application was already deployed in glassfish). Therefore it is concluded that when the request reaches the application the certificate is valid. Here the only problem that remains is who is the user which corresponds to this valid certificate. You have to think that usually a company is going to trust in some CA certs (for example, the national eID, DNIe in case of Spain), but the registered users will be stored in a company repository (LDAP, DDBB or whatever). At this point the application needs someway to map certificates and users, usually some data in the issued certificate is used to match the user: some part of the certificate subject (usually the CN or the Email part), an attribute extension (some extra data included in the cert),... It does not matter what field, but the idea is some information stored in the cert will be used to find the real user in the repository (in the previous example of Spainsh eID the ID number, DNI number, would be a good candidate for matching users, obviously the DNI should be a compulsory field at customer/user registration process).
The other question is when to perform this match between the certificate and the real user in the store, but this problem is solved as in any other login. In a web application with custom security any request should be intercepted to check if the user is logged in, if the request has no user information the user is redirected to the login page. In Java the user information is always stored in a session attribute (the application checks that the username and any other relevant information are inside the session) and the interceptor technique depends on the framework used (a typical servlet filter can be the simplest one). If our application is going to use certificates, the interceptor code will also be responsible of matching the user, in case of a successful match the user will be automatically logged in without password prompting.
In summary custom security needs to map the certificate against a real user inside the typical login interceptor code. My sample application uses an OpenLDAP repository (LDAP is the most common repository for users and groups) and it is implemented using JSF (Java Server Faces) and CDI (Context Dependency Injection). Instead of a filter it will use a JSF phase listener, if there is a solution in the chosen framework for a specified problem it will always be the recommended way to go (do not mix technologies). The main files in the solution are the following.
The LoginBean is a JSF session bean which is used to store the logged user information. As I said Java applications usually use the session to save the information associated with the logged user, in case of JSF, a session Bean is the direct solution.
This bean has some important methods:
login: The login action method performs typical login. The user introduces username and password in a login page and this page calls this method which finds and binds the user against the LDAP repository (the user is found and then the password is checked). This is the method executed in normal (non-certificate) login. I have used the ldapbp library (typical Sun, now Oracle, LDAP library) in order to get the user and the groups easily. All the LDAP stuff is executed inside a Singleton annotated EJB (Enterprise JavaBean) but forget this, it is just a way to acess the sample repository.
public String login() { userEntry = ldap.findUser(username, password.toCharArray()); groupsCache.clear(); if (userEntry != null) { return "index"; } else { FacesUtils.errorMessage("loginForm:username", "login_invalid_username_or_password"); return "login"; } }
trustedLogin: Other method of the bean which performs the match between certificates and users. It receives the X509 certificate of the client and with the CN part of the Subject (I have implemented a very simple match condition) it searches the LDAP repository for a matching user. If a valid user is found the method automatically fulfills all the variables in the session bean. This method only executes the login process if the user is not previously logged in (avoiding useless searches and validations).
public String trustedLogin(X509Certificate cert) { try { // only check if user not logged in if (!isLoggedIn()) { // get CN part => it's the last one LdapName name = new LdapName(cert.getSubjectDN().getName()); String certName = name.getRdn(name.size() - 1).getValue().toString(); // validate via PKIX if configured boolean certOK = !validatePKIX || validatePKIX(cert); if (certOK) { // login without password LdapEntry entry = ldap.findUser(certName, null); if (entry != null) { username = certName; userEntry = entry; groupsCache.clear(); } } } } catch (Exception e) { logger.error("Error performing the login", e); } return username; }
isLoggedIn: Method that returns if there is an active session of some user. It only checks that the username and userEntry variables are filled (both vars are initialized by the two previous methods).
public boolean isLoggedIn() { return username != null && userEntry != null; }
validatePKIX: This method validates the certificate using OCSP and/or CRL programmatically. You know that in my demo environment this is already done by the container, but I decided to include this code here just in case of an hypothetical container which does not support these validations. The bean can be configured to perform this extra validation in previous trustedLogin method, this way the container would do normal validation (trusted CA and valid dates) and the Java application would perform OCSP or CRL (this method uses exactly the same APIs than JSSE/glassfish).
isUserInGroup: The bean performs custom security, this is an equivalent method to standard isUserInRole. It returns true if the user belongs to the specified LDAP group (static and dynamic groups can be passed because ldapbp is used). Group membership is cached inside the LoginBean.
public boolean isUserInGroup(String groupname) { // check cache String key = groupname.toLowerCase(); if (groupsCache.containsKey(key)) { return groupsCache.get(key); } else { // read from ldap and set into cache boolean res = ldap.isUserInGroup(this.userEntry.getDn(), groupname); groupsCache.put(key, res); return res; } }
logout: Simple method to delete the session associated to this user.
public String logout() { HttpSession session = (HttpSession) FacesContext.getCurrentInstance( ).getExternalContext().getSession(false); if (session != null) { session.invalidate(); } return "logout"; }
When using CDI for JSF Beans the @ManagedProperty annotation cannot be used (and I used it a lot to initialize the beans before). In this sample application I tried to figure out how to initialize beans with Injection but I realized there is no direct solution, finally I used a properties file as it is commented in this post.
The CertLoginPhaseListener interceptor. As I commented previously, using JSF the recommended solution for the login interceptor stuff is a JSF Phase Listener (see the link I presented before for further information). These listeners are always executed associated with a JSF request/response phase and therefore can be used as filters. The listener performs the following steps:
It recovers the certificate from the request (this is the normal way of getting the associated certificate in a Java container):
X509Certificate[] certs = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
If there is at least one certificate (the client is accessing via the secure https port) the listener calls trustedLogin to automatically log the user in if it is the case. To do that programmatic EL (Expression Language) is used.
if (certs != null && certs.length > 0) { try { // login silently without password via certificate, the trustedLogin // performs the login if there is no previous session, certificate is valid // and the user is mapped against the ldap ExpressionFactory ef = facesContext.getApplication().getExpressionFactory(); MethodExpression me = ef.createMethodExpression(facesContext.getELContext(), "#{loginBean.trustedLogin}", null, new Class[]{X509Certificate.class}); me.invoke(facesContext.getELContext(), new Object[]{certs[0]}); } catch (Exception e) { logger.error("Error checking certificate", e); } }
After that the listener calls isUserLoggedIn just to check if there is an active session, again EL is used.
ExpressionFactory ef = facesContext.getApplication().getExpressionFactory(); ValueExpression ve = ef.createValueExpression(facesContext.getELContext(), "#{loginBean.loggedIn}", Boolean.class); boolean isLoggedIn = (Boolean) ve.getValue(facesContext.getELContext());
Finally this boolean variable can be true (the user previously logged in or he has been automatically logged in by the certificate method) or false (the user has never logged in and there is no certificate or it matched no user). Based on this variable the listener permits the user to continue or redirects him to the login page. It is quite interesting the case of a registered user who tries to access the login page, if this case happens the user is also redirected, but this time to main application page (avoiding any login). Besides if the client has a valid certificate but it did not match any user login page will be presented through the secure port.
if (!loginPage && !isLoggedIn) { // drive the user to the login page facesContext.getApplication().getNavigationHandler( ).handleNavigation(facesContext, null, "login"); } else if (loginPage && isLoggedIn) { // already login => go to main page facesContext.getApplication().getNavigationHandler( ).handleNavigation(facesContext, null, "index"); }
The login.xhtml and index.xhtml are the facelets pages for the login and the main page. Login is the typical username and password page and the main one simply shows the user logged in and if it belongs to an LDAP static and a dynamic group. These groups were previously loaded into the OpenLDAP repo (SampleClient1 belongs to both groups and SampleClient2 to none of them).
So today the video exemplifies all possible login methods. If the user accesses the application using non-secure port (8080) the listener detects no certificate and no logged user and it presents a common login page. In this page I log in using SampleClient2 (although his certificate is revoked he is a valid user in the LDAP repository, so he can access the application using normal login and password way). But if I access the https port (8181) and I use the revoked certificate glassfish denies the access. The second time I try with SampleClient1, now the listener detects my certificate, matches it against the LDAP user (the Subject of this cert is CN=SampleClient1,O=demo.kvm,C=ES and the CN part is used to find the associated user) and it fulfills the LoginBean data to silently log me in. Finally the listener redirects the request to the index main page, SampleClient1 user and his groups are shown in the page. As I am using certificate login I cannot logout (the certificate is always detected and the user is logged when he has not an active session). In order to delete my login I clean my cookies and active logins in the browser and I try with another certificate, I created a new certificate for SampleClient3 but this user does not exists in the LDAP repository, this time the listener does not match any user to this new Subject CN and, therefore, login page is presented in the secure port. I enter again as SampleClient2.
In summary using certificates as a login method in a custom security application is not very complicated. Mapping the certificate to a real application user is the main part, and, in a custom security application, it is done in the same place in which login interception is done. My sample application NetBeans project can be downloaded and it just performs all of this with JSF and a phase listener, the users and groups are placed in an OpenLDAP server and the ldapbp library is used to retrieve group membership easily, the application maps the certificate via the CN part of the subject. Although the application is very simple I think it exemplifies perfectly a certificate login using custom security. Some days ago (when I had already implemented this demo application but not published the entry) I faced a customer who had tried to do the same in a very very weird way (I better do not comment this), that reassured me in publishing this series. Take into account that, although the entry implements the certificate security in Java, it rules in any web application (PHP, Rails, Python or whatever).
Remember to be smart!
I'm getting a PropertyNotFoundException in the CertLoginPhaseListener at the point where it tires to invoke a method on the loginBean (it's null). I don't wuite get where the LoginBean is instantiated. Have you seen this before? Thanks, Ricky.
Comments