Wednesday, January 30, 2013

Writing REST Services in Java: Part 6 Security & Authorization

Note: Whilst some of the code in this post is still useful the preferred method for authentication is OAuth2 (see my blog post here - http://porterhead.blogspot.co.uk/2014/05/securing-rest-services-with-spring.html)

Previous Post : Part Five: Lost Password
Get the Code: https://github.com/iainporter/rest-java

Part Two dealt with signing up and authenticating users using the conventional email and password approach. Part Four also covered authentication but using the OAuth protocol. This post will cover authorization of services.

To that end what we want to accomplish is:

  • Identify who is making the request
  • Have a high degree of confidence that they are who they say they are
  • Limit access to only those resources that they are allowed to access
  • Prevent malicious tampering of requests or session hijacking


SSL


Ideally all API traffic should be over a secure connection using SSL. If that is not feasible then at the very least all requests that would be open to MITM attacks should be secured, such as account creation, login, etc

Security Filter


All API requests first go through a security filter. An authorization service is registered with the filter on startup. Depending on the level of security you are comfortable with and how much effort you want to subject clients to will determine the choice of service. I'll cover that later in the post. First we need to register the filter with the servlet container.

In src/main/webapp/WEB-INF/web.xml a ResourceFilterFactory is added to the config

        <init-param>
            <param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>
            <param-value>com.porterhead.rest.filter.ResourceFilterFactory</param-value>
        </init-param>


The ResourceFilterFactory extends the Jersey class RolesAllowedResourceFilterFactory. In it we register the security filter and ensure it is the first filter in line.

@Component
@Provider
public class ResourceFilterFactory extends RolesAllowedResourceFilterFactory {

    @Autowired
    private SecurityContextFilter securityContextFilter;

    @Override
    public List<ResourceFilter> create(AbstractMethod am) {
        List<ResourceFilter> filters = super.create(am);
        if (filters == null) {
            filters = new ArrayList<ResourceFilter>();
        }
        List<ResourceFilter> securityFilters = new ArrayList<ResourceFilter>(filters);
        //put the Security Filter first in line
        securityFilters.add(0, securityContextFilter);
        return securityFilters;
    }
}


The SecurityContextFilter class gathers some information from the request and then delegates to the AuthorizationService implementation to handle authorizing the request and loading the user. If the request is valid a user is returned unless the resource is not access controlled in which case no authorization headers will be required. The user is wrapped in a SecurityContext and added to the ContainerRequest.

    public ContainerRequest filter(ContainerRequest request) {
        String authToken = request.getHeaderValue(HEADER_AUTHORIZATION);
        String requestDateString = request.getHeaderValue(HEADER_DATE);
        String nonce = request.getHeaderValue(HEADER_NONCE);
        AuthorizationRequestContext context = new AuthorizationRequestContext(request.getPath(), request.getMethod(),
                            requestDateString, nonce, authToken);
        ExternalUser externalUser = authorizationService.authorize(context);
        request.setSecurityContext(new SecurityContextImpl(externalUser));
        return request;
    }


Role Based Access


Using JAX-RS annotations we can protect resources based on roles. It is the task of the SecurityContext implementation that was added to the Container Request in the above step that will answer the question of whether the user has the role.

    public boolean isUserInRole(String role) {
        if(role.equalsIgnoreCase(Role.anonymous.name())) {
             return true;
        }
        if(user == null) {
            throw new InvalidAuthorizationHeaderException();
        }
        return user.getRole().equalsIgnoreCase(role);
    }


If anyone can access the resource, as in the case of user/login then it is annotated with @PermitAll

    @PermitAll
    @Path("login")
    @POST


If we want to limit access then we use the annotation @RolesAllowed with a list of allowed roles

    @RolesAllowed({"authenticated"})
    @Path("{userId}")
    @GET
    public Response getUser(@Context SecurityContext sc, @PathParam("userId") String userId) {
        ExternalUser userMakingRequest = (ExternalUser)sc.getUserPrincipal();
        ExternalUser user =  userService.getUser(userMakingRequest, userId);
        return Response.ok().entity(user).build();
    }


Note that we can also use annotations to retrieve the user that was wrapped in the SecurityContext as part of the Authorization procedure. We can then check that this user can not only access this resource method but also at the service level whether they can access the user object that is being requested. In this situation we want to check that they can only request getUser on their own instance. We could add "administrative" role access and allow that user to access any user instance.

Session Token Authorization


The simplest method of authorization is to get a session token on login or sign up and pass that back on every request. This is obviously not secure enough to be transmitted over anything other than an SSL connection. It can be useful for testing services quickly and easily with curl without the overhead of signing the request each time.
        public ExternalUser authorize(AuthorizationRequestContext securityContex) {
        String token = securityContext.getAuthorizationToken();
        ExternalUser externalUser = null;
        if(token == null) {
            return externalUser;
        }
        User user =  userRepository.findBySession(token);
        if(user == null) {
            throw new AuthorizationException("Session token not valid");
        }
        AuthorizationToken authorizationToken = user.getAuthorizationToken();
            if (authorizationToken.getToken().equals(token)) {
                externalUser = new ExternalUser(user);
            }
        return externalUser;
    }


We extract the token from the request header and query the User Repository for a User with that Session token.

Testing Session Token Authorization


1. In src/main/resources/properties/app.properties set the following property

security.authorization.requireSignedRequests=false


2. Start the application by executing

gradle tomcatRun


3. Create a user with the curl statement

curl -v -H "Content-Type: application/json" -X POST -d '{"user": {"firstName":"Foo","lastName":"Bar","emailAddress":"foobar@example.com"}, "password":"password"}' http://localhost:8080/java-rest/user


You should receive back an AuthenticatedUserToken similar to this

< HTTP/1.1 201 Created < Server: Apache-Coyote/1.1 < Location: http://localhost:8080/java-rest/user/d26079db-62e9-4819-964a-be954d2c47ed < Content-Type: application/json < Transfer-Encoding: chunked < Date: Tue, 29 Jan 2013 19:48:05 GMT {"userId":"d26079db-62e9-4819-964a-be954d2c47ed","token":"86e8be75-3eac-45aa-acc3-f043333c8608"}


4. For Session token authorization we only need the token part. Now construct a curl statement based on the location url in the response to GET a user similar to the following

curl -v -H "Content-Type: application/json" -X GET -H "Authorization: 86e8be75-3eac-45aa-acc3-f043333c8608" 'http://localhost:8080/java-rest/user/d26079db-62e9-4819-964a-be954d2c47ed'


You should receive a response similar to this one

< HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Content-Type: application/json < Transfer-Encoding: chunked < Date: Tue, 29 Jan 2013 19:58:13 GMT {"id":"d26079db-62e9-4819-964a-be954d2c47ed","firstName":"Foo","lastName":"Bar","emailAddress":"foobar@example.com","socialProfiles":[],"name":"foobar@example.com","verified":false}


Request Signing Authorization


A surer method of securing requests that does not require an SSL connection other than for passing back and forth credentials is to sign requests. This implementation is based somewhat on the OAuth spec, although does not go as far as to require the request body to be signed. That can easily be plugged in though for an added layer of security.

On login or sign up an AuthenticatedUserToken is returned. An AuthenticatedUserToken is comprised of:
  • userId - passed with every role-based request as a means to identify the user.
  • token - used as a shared secret between the client and the application to verify the hash of a token passed with the request.


To generate a hashed token a String is composed of the following

  • The session token
  • A : separator
  • The relative url of the resource (i.e. user/ff7ffcf0-cfe0-4c1e-9971-3b934612b154) followed by ,
  • The HTTP method (GET, POST, PUT, DELETE) followed by ,
  • A time stamp of the request in Iso8061 format followed by ,
  • A unique nonce token generated by the client


This string is then hashed using SHA-256 and then Base64 encoded. The Authorization header is composed of the user token followed by a : separator and then the hashed signature
In order for the server to reconstitute the string the timestamp and nonce must also be passed as headers.

The main methods to authorize signed requests

    public ExternalUser authorize(AuthorizationRequestContext context) {

        ExternalUser externalUser = null;
        if (context.getAuthorizationToken() != null && context.getRequestDateString() != null && context.getNonceToken() != null) {
            String userId = null;
            String hashedToken = null;
            String[] token = context.getAuthorizationToken().split(":");
            if (token.length == 2) {
                userId = token[0];
                hashedToken = token[1];
                //make sure date and nonce is valid
                validateRequestDate(context.getRequestDateString());
                validateNonce(context.getNonceToken());

                User user = userRepository.findByUuid(userId);
                if (user != null) {
                    externalUser = new ExternalUser(user);
                    if (!isAuthorized(externalUser, context, hashedToken)) {
                        throw new AuthorizationException("Request rejected due to an authorization failure");
                    }
                }
            }
        }
        return externalUser;
    }


    private boolean isAuthorized(User user, AuthorizationRequestContext authorizationRequest, String hashedToken) {
        Assert.notNull(user);
        Assert.notNull(authorizationRequest.getAuthorizationToken());
        String unEncodedString = composeUnEncodedRequest(authorizationRequest);
        AuthorizationToken authorizationToken = user.getAuthorizationToken();
        String userTokenHash = encodeAuthToken(authorizationToken.getToken(), unEncodedString);
            if (hashedToken.equals(userTokenHash)) {
                return true;
            }
        LOG.error("Hash check failed for hashed token: {} for the following request: {} for user: {}",
                new Object[]{authorizationRequest.getAuthorizationToken(), unEncodedString, user.getId()});
        return false;
    }


If the request headers are present then we attempt to authorize the request.
The timestamp is checked to ensure it conforms within the configurable boundaries of the server clock.
The nonce value is checked to ensure it is unique
The user is loaded and we iterate over their session tokens and attempt to verify the request token using the shared secret.

Testing Signed Request Authorization



1. In src/main/resources/properties/app.properties set the following property

security.authorization.requireSignedRequests=true


2. Start the application by executing

gradle tomcatRun


3. Create a user with the curl statement

curl -v -H "Content-Type: application/json" -X POST -d '{"user": {"firstName":"Foo","lastName":"Bar","emailAddress":"foobar@example.com"}, "password":"password"}' http://localhost:8080/java-rest/user


4. Construct a signed request to call user/id GET using the returned AuthenticatedUserToken
{"userId":"ff7b93ad-27d0-49f6-90bd-9937951e5fcc","token":"6eebc0ea-b637-4033-925b-3b3cba9880e4"}
First the string to hash:

  • 6eebc0ea-b637-4033-925b-3b3cba9880e4 (the session token)
  • user/ff7b93ad-27d0-49f6-90bd-9937951e5fcc (the resource url)
  • GET (The Http Method)
  • 2013-01-30T10:50:00+00:00 (Timestamp)
  • 22e327d732 (nonce value)

The full string
6eebc0ea-b637-4033-925b-3b3cba9880e4:user/ff7b93ad-27d0-49f6-90bd-9937951e5fcc,GET,2013-01-30T10:50:00+00:00,22e327d732

Hash the string using SHA-256 and then Base64 encode the result. There is a utility class at com.incept5.rest.util.HashUtil that you can use.
This should render a result similar to
ncYoA5n5s2nFSm7qyvf5hDgL4pmmPOUP3zo/UYfaQKg=

5. Construct the curl statement with relevant headers which should look similar to the following

curl -v -H "Content-Type: application/json" -H "Authorization: ff7b93ad-27d0-49f6-90bd-9937951e5fcc:ncYoA5n5s2nFSm7qyvf5hDgL4pmmPOUP3zo/UYfaQKg=" -H "x-java-rest-date:2013-01-30T10:50:00+00:00" -H "nonce:22e327d732" -X GET -d localhost http://localhost:8080/java-rest/user/ff7b93ad-27d0-49f6-90bd-9937951e5fcc


After executing the curl statement you should get back something like:

< HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Content-Type: application/json < Transfer-Encoding: chunked < Date: Wed, 30 Jan 2013 10:48:56 GMT {"id":"ff7b93ad-27d0-49f6-90bd-9937951e5fcc","firstName":"Foo","lastName":"Bar","emailAddress":"foobar@example.com","socialProfiles":[],"name":"foobar@example.com","verified":false}


Tampering with any of the header values or resubmitting the request should result in a failure such as

< HTTP/1.1 403 Forbidden < Server: Apache-Coyote/1.1 < Content-Type: application/json < Transfer-Encoding: chunked < Date: Wed, 30 Jan 2013 10:52:26 GMT {"errorCode":"40301","consumerMessage":"Not authorized","applicationMessage":"Nonce value is not unique"}


The application has some javascript functions for creating signed requests on the client side (see src/main/webapp/js/javarest.js)
That now covers everything that you need to create and manage simple user accounts in a REST API. The next post will focus on configuring the application for production

8 comments:

  1. hello, thank you for this very interesting blog
    I got your git sources and migrated with maven.
    when I execute the generated war, I have the following error.


    INFO: logbak: 08:02:23.119 o.s.web.context.ContextLoader - Context initialization failed
    org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'resourceFilterFactory': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.incept5.rest.filter.SecurityContextFilter com.incept5.rest.filter.ResourceFilterFactory.securityContextFilter; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityContextFilter' defined in file [/home/emmanuel/programmation/SandBox/WSRestSampleJava/rest-java_secur/rest-java-master/target/servicewebmacanaille/WEB-INF/classes/com/incept5/rest/filter/SecurityContextFilter.class]: Unsatisfied dependency expressed through constructor argument with index 1 of type [com.incept5.rest.user.UserService]: : Error creating bean with name 'userService' defined in file [/home/emmanuel/programmation/SandBox/WSRestSampleJava/rest-java_secur/rest-java-master/target/servicewebmacanaille/WEB-INF/classes/com/incept5/rest/user/UserServiceImpl.class]: Unsatisfied dependency expressed through constructor argument with index 0 of type [org.springframework.social.connect.UsersConnectionRepository]: : Error creating bean with name 'com.incept5.rest.user.social.SocialConfig#0': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: or


    Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.security.crypto.encrypt.TextEncryptor] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoSuchBeanDefinitionException(DefaultListableBeanFactory.java:967) ~[spring-beans-3.2.1.RELEASE.jar:3.2.1.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:837) ~[spring-beans-3.2.1.RELEASE.jar:3.2.1.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:749) ~[spring-beans-3.2.1.RELEASE.jar:3.2.1.RELEASE]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:486) ~[spring-beans-3.2.1.RELEASE.jar:3.2.1.RELEASE]
    ... 107 common frames omitted

    Do you have an idea to fix this error?

    ReplyDelete
    Replies
    1. Do you have a dependency for Spring Security core in your pom?


      org.springframework.security
      spring-security-core
      3.1.2.RELEASE

      Delete
  2. Hello Iain, nice post

    I am interested in token session based authentication for a real app and wonder if you might give feew clues about which framework should i use in order to don't reinvent the wheel ... maybe jaas has some classes i can rely on?

    Thanks in advance.

    ReplyDelete
  3. The standard now is to use OAuth2. This means that you have to run all secure requests over SSL.

    I keep meaning to post an example that I have built using Spring security, but other projects have limited my free time.

    ReplyDelete
  4. I understand the concept and would like to implement signed tokens for each session using Jersey REST and AngularJS on the client side. However, I do not understand how the client is storing the token between sessions. Is this through local storage, a page variable that is set on each request? Basically, how does the token get to the client, wait, and then get sent back to the REST api for authentication?

    ReplyDelete
    Replies
    1. I have finally found some free time to write up a blog post on using OAuth2 which is preferred over the this approach. See my most recent post http://porterhead.blogspot.co.uk/2014/05/securing-rest-services-with-spring.html

      Delete
  5. Hello Iain Porter, i am stuck in authentication problem, wiht oauth 2 and spring-security. can you pleae help me. following is the of my problem.
    http://stackoverflow.com/questions/24930830/oauth2-and-spring-security-fail-on-authentication-using-oauth2-and-spring-secur

    ReplyDelete