Sunday, 26 February 2017

PicketLink Authentication and Authorization with Custom Simple Schema

I was looking for a modern CDI enabled JEE security framework and found a very good one in PicketLink(PL) as it has pretty much everything. What's more, it combines with Apache DeltaSpike for authorization which is cool.

However, I have one or two problems asociated with PicketLink's IDM structure which we use for this:
a) The ID in the IDM User class is defined as a String and not an Integer/Long. This is a problem. In most cases that we encounter, the ID is a numeric type and is referred in several other tables. Being a numeric type comes in handy particularly when writing raw queries - which is needed when debugging production issues. The String in the IDM is also a generated one and results in a long hash - which is very difficult to track and use in these types of situations.

b) Secondly, how do I use PicketLink + DeltaSpike authorization with all its goodies to an existing database - which mostly has a numeric ID field?

c) Thirdly, could I have a much simpler table structure to deal with authentication and authorization? PicketLink's IDM is robust, but, we may not need it in all cases.

There have been some successful solutions used by other people. The most common is to have a numeric ID and tie it as an attribute in the IDM scheme of things and then refer to that numeric value in other places.

In this post, I use a custom solution which I found useful and simple - which might come in handy for small application(s).

I define a typical model for user management - which is not related to the IDM schems, such as:



A simple one, with a table to store the user info, a separate related table to store password (this is useful because, when bringing back user from DB, I don't need to bring back the password related info). A master table for role and another table to associate the roles for a user.

I write corresponding JPA entities for these tables. The numeric ID field becomes the primary key and is also auto-generated via a sequence. Importantly, I don't have a relation from AppUser to UserPassword entity (it's the other way around) - simply because, as mentioned above, I don't want to bring back password information when I load user. Same applies in case of roles too. One of the basic concepts to remember about security is that, information should not be provided unless and until it is asked for (strictly on a need-to-know basis). 

Let us first create sample users and roles. I use the @Initializer class (as it is done in many PL quickstarts). First, create the records for the 'role_master' and then create users with roles. For an user, this would insert 1 record into user table, 1 record into the password table and 1 into the 'user_role' table. 

First things first, how do we encrypt the password and store the same in DB? Had we been using PL IDM schema, the PL API would have taken care of the same. 
I do almost the same as what IDM does (benefits of open source!) - make use of the org.picketlink.idm.credential.encoder.SHAPasswordEncoder class to encrypt the password (and then use JPA to store the same). This class simply uses the java.security.MessageDigest class to get the SHA implementation (so, there is no big danger of depending on a PL implementation). We pass an argument of 512 to indicate the strength of the SHA algorithm.

(sidescript: the createUser method of the @Initializer class can be used to create user when you want to add user via the UI and this method can be placed in a transaction).

Now, how do we authenticate when the user wants to log-in to the application? We have the usual login.xhtml JSF page which is same as in any PL example. For authentication, as the default PL uses IDM, we cannot rely on that. We need to write our own authenticator and wire this up with the PL ecosystem. A nice example is given at https://github.com/jboss-developer/jboss-picketlink-quickstarts/tree/master/picketlink-authentication-jsf

I follow the same and write the CustomAuthenticator class. Instead of IDM schema, I use simple JPA queries to query the user and the password tables with the user entered user id and password. As the user id is the login name in my example, I use the findByLoginName query to check against the user table (note that the numeric id that we spoke about in the beginning is obviously not used as the login id by the real users). 

As for the password, we again use org.picketlink.idm.credential.encoder.SHAPasswordEncoder instance and call the verify method after we get back the password hash from the table using JPA. 

Now comes the most important part. If the authentication is successful, we set the status as AuthenticationStatus.SUCCESS. Once we set the status as success and the page is loaded, the identity.isLoggedin would be true and the user and admin links would be displayed.

And next, we have to set the account object. This account object should be a type of org.picketlink.idm.model.basic.Account. This is in a way enforced by the API. Setting this account is both important and useful as this is the account that is retrieved via the Identity object (which can be injected). This is same as setting the user info in the session.

Now, because we are forced to use the IDM Account object (as the setAccount method takes that type), we will create an instance of org.picketlink.idm.model.basic.User class - as User extends Account. Now, we have to set the values of the User object (fn, ln, email etc.). The most important information is the ID. As outlined earlier, the ID within this IDM User object is a String. The ID that we use in our AppUser is a Long. So, we simply convert the Long to String and set it into the User's ID. 

So, now, later on, wherever we need the ID, we can simply @Inject the Identity object and get the value. For example, to get the user name, we can do:
((User)identity.getAccount()).getLoginName()


Now, to the next important part - authorization:

Authorization checks would be needed at two levels. One is at the UI side and the other at method-level checks on the server-side. Apache DeltaSpike provides a CDI enabled authorization module which blends nicely with PL. Some examples can be found in the PL quickstarts for the same.

We are going to deal with the UI side authorization checks - this again would be needed at two levels. First is at the displayed UI, where a part of the UI is hidden/disabled for certain roles. Second, at the URL level.

Within the UI, there are a few types of authorization needs. One is that showing/hiding or enabling/disabling parts of the UI for specific roles. Let's see how this works. This is actually very simple. Login as 'user1' and goto the common page - you should see the 'Save' button in disabled state. Now, login as 'admin1' and goto the common page. Now, you should see the 'Save' button enabled. 
This is achieved simply using JSF EL. The JSF EL uses a named bean 'authChecker' (AuthorizationChecker.java) on the isAdmin method. The isAdmin method in turn queries the 'UserRole' table using a JPA query to check if the user has the specified role. 

This is cool. Likewise, you can navigate to the admin page. The link to the admin page (menu) shown on the home page can also be shown/hidden based on the role. But, what if the user copies the admin url and pastes it into the address bar after being logged in as a normal user 'user1'? So, we need URL level authorization to happen here.

PL does supports URL level authorization in a simple manner. However, there are some issues here. Since our whole model is customized and not based on IDM, the custom authorization of URLs for a specific role does not work. For example, the following does not work:

When we build the security configuration, we can make use of the forPath() and authorizeWith() methods to specify URL authorizations. For example, we use the following in our code:
  .forPath("/faces/admin/*")
  .authorizeWith().role(AppRole.ADMIN.toString())

But, how and where do we hook the role into PL? Remember that in the custom authenticator that we wrote, we set the status and the account. But, nowhere did we set the roles for the logged in user. And, there is no method on the PL Account class to set the roles for the user.

This is usually the norm. Roles are not set anywhere. Security works on a need-to-know and deny-first principles and so the hasRole and such methods are provided. Still, we need to figure out on how this will work.

We can write a custom authorizer for URL level authorization too as follows:
.forPath("/faces/admin/*")
.authorizeWith().role(AppRole.ADMIN.toString()).authorizer(CustomPathAuthorizer.class)


This class needs to override the authorize method and return true/false. So, we can write our custom auth checks inside this method and return the value accordingly.This method takes a PathConfiguration as a parameter and we can get the roles with the following call:

pc.getAuthorizationConfiguration().getAllowedRoles();

And then with that, we can do our custom auth check. More info can be found from org.picketlink.authorization.DefaultAuthorizationManager source.

However, when I implement this and run, no matter what role I am logged on to, going to the admin URL directly simply fails and I get a 401/403. I began wondering if this was a bug and posted in the forum (https://developer.jboss.org/thread/272838)
Let's take a step back and see how the default PL works first. There are 4 authorizers built by default and added which are called one by one to do the check (authorize method which returns a boolean). If any of them return false, authorization will be denied.

When we add the custom authorizer, even though it is added as a 5th one, it seems, one of the default 4 returns false and so we continue to get access denied. And all the default authorizers are tied to the IDM schema.

I found a workaround thanks to a tip in one of the forum messages. The tip was to not use the 'role()' method at all. So, I tried that and the authorizer is working fine with the same now. Just use:
.authorizeWith().authorizer(CustomPathAuthorizer.class)

So, where do I specify the roles for the URL? Inside the authorizer, I can get the pathInfo from the request and then use it to check with my roles. For example, in the authorize method, I can get the path and then check against a List/Map to verify if this path is allowed for the role of the logged in user. As an example, I have used a trivial sample map with path and list of roles. For more robust ones, we can propably have this data also in the database.

In the current sample, I just loop against the paths to match and then see if the logged-in user has the requisite role to go to the URL. To test this, login as a user and then paste the admin URL directly in the browser. You should see an access denied page.

The complete sample code is available in my github repo.

2 comments: