Tuesday, October 14, 2014

Implementing JSR-250 hierarchical roles in SpringSecurity

Get the code: https://github.com/iainporter/oauth2-provider

In a previous post I walked through setting up an OAuth2 provider service using Spring Security. I used JSR-250 role-based annotations to protect access to resources. An example of this is the /v1.0/me API to return user details:

    public ApiUser getUser(final @Context SecurityContext securityContext) {
        User requestingUser = loadUserFromSecurityContext(securityContext);
        if(requestingUser == null) {
            throw new UserNotFoundException();
        return new ApiUser(requestingUser);

What happens when we create a new role for admin users called ROLE_ADMIN?
This user should have the same access rights as ROLE_USER as well as other restrictive rights not available to regular users. The temptation would be to simply add another role to the list of allowed roles:

@RolesAllowed({"ROLE_USER", "ROLE_ADMIN"})

The better solution would be to keep the access as ROLE_USER but allow anyone with a role that extends ROLE_USER to access the resource. To do that we would have to implement hierarchical roles and the access decision mechanism in Spring Security would need to know how to handle this hierarchy.

Creating an access controlled resource

I'll create a simple service that can be accessed by anyone with the role of ROLE_GUEST. Users with a higher level of access will have a role of ROLE_USER and can access anything that ROLE_GUEST users can. The url of the resource will be /v1.0/samples.

public class SampleResource extends BaseResource {

    public Response getSample(@Context SecurityContext sc) {
        User user = loadUserFromSecurityContext(sc);
        return Response.ok().entity("{\"message\":\"" + user.getEmailAddress() + 
                  " is authorized to access\"}").build();

The resource is in a package that Jersey does not yet know about so it has to be registered with the com.porterhead.RestResourceApplication class

packages("com.porterhead.resource", "com.porterhead.user.resource", "com.porterhead.sample");

Next Spring needs to know about the class in order to apply method level security so we need to add the package to component scanning in application-context.xml:

<context:component-scan base-package="com.porterhead.sample"/>

Writing a failing functional test

Before implementing the changes I'll add a failing functional test that will register a user and then attempt to access the service.
This should result in a 401 status as users are assigned a ROLE_USER role on registration and the system does not know anything about ROLE_GUEST.

    public void testHierarchicalRole() {

        //sign up a user with role of ROLE_USER
        def username = createRandomUserName()
        httpSignUpUser(getCreateUserRequest(username, TEST_PASSWORD))

        //login and get the oauth token
        def loginResponse = httpGetAuthToken(username, TEST_PASSWORD)

        //get the resource that requires a role of ROLE_GUEST
        def sampleResponse = getRestClient().get(path: "/v1.0/samples", 
              contentType: ContentType.JSON, headers: ['Authorization': "Bearer " 
              + loginResponse.responseData["access_token"])

        assertEquals(200, sampleResponse.status)

        return Response.ok().entity("{\"message\":\"" + username.toLowerCase() 
              + " is authorized to access\"}").build();

Execute the tests:

./gradlew integrationTest

This will result in ONE failure which you can view at


You can also try it out with curl
Start the application by executing ./gradlew tomcatRun and ensure you have mongodb running

First create a user:

   curl -v -X POST -H "Content-Type: application/json" \
   -H "Authorization: Basic MzUzYjMwMmM0NDU3NGY1NjUwNDU2ODdlNTM0ZTdkNmE6Mjg2OTI0Njk3ZTYxNWE2NzJhNjQ2YTQ5MzU0NTY0NmM=" \
   -d '{"user":{"emailAddress":"foo@example.com"}, "password":"password"}' \

Extract the access token from the response and send the request for samples:

curl -v -X GET \    -H "Content-Type: application/json" \
   -H "Authorization: Bearer <the access token from user registration>" \

You should see a response similar to this:

   HTTP/1.1 401 Unauthorized
   Server Apache-Coyote/1.1 is not blacklisted
   Server: Apache-Coyote/1.1
   Content-Type: application/json
   Content-Length: 168
   Date: Tue, 14 Oct 2014 19:10:26 GMT

   {"errorCode":"401","consumerMessage":"You do not have the appropriate privileges to access this resource","applicationMessage":"Access is denied","validationErrors":[]}

The Role hierarchy class

We only need a simple hierarchy with ROLE_ADMIN extending ROLE_USER which in turn extends ROLE_GUEST.
Fortunately we don't have to do too much work as Spring already has an implementation that is fit for this purpose. We just need to add a bean definition with our role hierarchies to security-configuration.xml.

    <bean id="roleHierarchy"
        <property name="hierarchy">
                ROLE_ADMIN > ROLE_USER
                ROLE_USER > ROLE_GUEST

Customising JSR-250 Voter class

The spring implementation of JSR-250 is almost there but does not know how to handle hierarchies. Spring has another class (RoleHierarchyVoter) that knows how to handle hierarchical roles so we can use some of that to extract the roles.

So if we subclass Jsr250Voter and modify the vote method so that it extracts the hierarchical roles that should do the trick.

public class HierarchicalJsr250Voter extends Jsr250Voter {

    private RoleHierarchy roleHierarchy = null;

    public HierarchicalJsr250Voter(RoleHierarchy roleHierarchy) {
        Assert.notNull(roleHierarchy, "RoleHierarchy must not be null");
        this.roleHierarchy = roleHierarchy;

    public int vote(Authentication authentication, Object object, 
                     Collection<ConfigAttribute> definition) {
        boolean jsr250AttributeFound = false;

        for (ConfigAttribute attribute : definition) {
            if (Jsr250SecurityConfig.PERMIT_ALL_ATTRIBUTE.equals(attribute)) {
                return ACCESS_GRANTED;

            if (Jsr250SecurityConfig.DENY_ALL_ATTRIBUTE.equals(attribute)) {
                return ACCESS_DENIED;

            if (supports(attribute)) {
                jsr250AttributeFound = true;
                // Attempt to find a matching granted authority
                for (GrantedAuthority authority : extractAuthorities(authentication)) {
                    if (attribute.getAttribute().equals(authority.getAuthority())) {
                        return ACCESS_GRANTED;

        return jsr250AttributeFound ? ACCESS_DENIED : ACCESS_ABSTAIN;

    Collection<? extends GrantedAuthority> extractAuthorities(
           Authentication authentication) {
        return roleHierarchy.getReachableGrantedAuthorities(

Wire the new class into the application context

Adding the bean definition to security-configuration.xml:

    <bean id="roleVoter" class="com.porterhead.security.HierarchicalJsr250Voter">
        <constructor-arg ref="roleHierarchy" />

And the final piece of the puzzle is to change the bean that is being referenced by the AccessDecisionManager:

    <bean id="accessDecisionManager" 
        <property name="decisionVoters">
                <ref bean="roleVoter"/>

Testing the changes

First make sure the integration test passes by executing ./gradlew integrationTest

Testing with curl should produce a response similar to this:

   HTTP/1.1 200 OK
   Server Apache-Coyote/1.1 is not blacklisted
   Server: Apache-Coyote/1.1
   Content-Type: application/json
   Content-Length: 42
   Date: Tue, 14 Oct 2014 19:54:28 GMT

  {"message":"You are authorized to access"}

Other posts in this series


  1. Dear Iain Porter,

    I have read your code. It is a very good demo. I found that you are using AJAX if I want to POST, PUT, GET, something to the server. But if I want to to the old method, by using the "FORM POST" action, how can I modify the code to do so?

  2. Hi Iain, thank you very much for the series.
    In the comments from the previous post you mentioned that next on your list was to discuss the flow with 3rd party OAuth providers (Facebook etc). Was wondering if you still plan on doing that.