Thursday, October 28, 2010

Basic Authentication and Authorization

When building commercial applications the twin issues of authentication and authorization are almost certain to arise at some point, the topic becomes even livelier when you throw a legacy infrastructure into the mix and want everything to work seamlessly.

So what’s a really nice and simple way of doing this? Let’s look at Apache Shiro and see how it simplifies things for us.

What is Apache Shiro? I think they say it best themselves
Apache Shiro is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application

Right - so let’s get our hands dirty, and see how we can leverage Shiro.


Create a Web application


New Project -> Maven -> Maven Web Application


Create a new Stateless Session Bean


HelloResource class

@Stateless
@Path("/message")
public class HelloResource {
    
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String sayHello() {
        return "hello someone";
    }

} 

Note that under your PersonalHello web project Netbeans has created a RESTful web services node, under which your HelloResource lives.

Right click the RESTful Web Services node and Select REST Resources configuration.


Change the name of the REST Resources Path, this will make NetBeans generate the web.xml file with your servlet adaptor.

Run your web app and you should see the Standard ‘Hello World!’ index page.

Go to http://localhost:8080/PersonalHello/rest/message and you should see your ‘hello someone’ message.


So now we know that our simple web service with no authentication is working, lets add Shiro into the mix and see how complicated things become!


Adding Shiro 

Add  the following dependencies to your projects pom.xml

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.0.0-incubating</version>
        </dependency>
        
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.6.1</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.6.1</version>
            <scope>runtime</scope>
        </dependency>

Note: Shiro has just graduated from the Apache Incubator, so expect a non-incubating release sometime soon.


Go to your web.xml and add the following



     <filter>
        <filter-name>ShiroFilter</filter-name>
        <filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
        <init-param>
            <param-name>config</param-name>
            <param-value>

                # The IniShiroFilter configuration is very powerful and flexible, while still remaining succinct.
                # Please read the org.apache.shiro.web.servlet.IniShiroFilter JavaDoc for information.

                # Quick Tip: Instead of having this configuration here in web.xml, you can instead
                # move all of this to a 'shiro.ini' file at the root of the classpath and remove
                # the 'config' init-param. Or you can specify the 'configPath' init-param and specify the
                # path to a resource at any location (url, file or classpath). This may be desired if the
                # config gets long and you want to keep web.xml clean.

                [users]
                # format: username = password, role1, role2, ..., roleN
                root = secret,admin
                guest = guest,guest
                presidentskroob = 12345,president,admin
                darkhelmet = ludicrousspeed,darklord,schwartz
                lonestarr = vespa,goodguy,schwartz

                [roles]
                # format; roleName = permission1, permission2, ..., permissionN
                admin = *
                schwartz = lightsaber:*
                goodguy = winnebago:drive:eagle5

                [urls]
                /rest/** = authcBasic

            </param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>ShiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


Go New File -> Other -> properties



And add the following text to the file

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#
# This file is used to format all logging output
log4j.rootLogger=TRACE, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %-5p [%c]: %m%n

# =============================================================================
# 3rd Party Libraries
# OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL
# =============================================================================
# ehcache caching manager:
log4j.logger.net.sf.ehcache=WARN

# Most all Apache libs:
log4j.logger.org.apache=WARN

# Quartz Enterprise Scheular (java 'cron' utility)
log4j.logger.org.quartz=WARN

# =============================================================================
# Apache Shiro
# =============================================================================
# Shiro security framework
log4j.logger.org.apache.shiro=TRACE
#log4j.logger.org.apache.shiro.realm.text.PropertiesRealm=INFO
#log4j.logger.org.apache.shiro.cache.ehcache.EhCache=INFO
#log4j.logger.org.apache.shiro.io=INFO
#log4j.logger.org.apache.shiro.web.servlet=INFO
log4j.logger.org.apache.shiro.util.ThreadContext=INFO








And finally let’s make our hello a little more personal. Change the sayHello method to the following


    public String sayHello() {

        StringBuilder builder = new StringBuilder();
        builder.append("Hello ");
        builder.append(SecurityUtils.getSubject().getPrincipal().toString());

        return builder.toString();
    }


Run your web-app again and go to http://localhost:8080/PersonalHello/rest/message you will now see a login prompt. Use ‘lonestarr’ as the user and ‘vespa’ as the password (or any of the other combinations that are in the config that you placed in the web.xml)
You will now be greeted by a personalised Hello, pretty nifty me think!

What have we done to achieve this?
1.    Added the dependencies to our project
2.    Made sure that log4j was configured
3.    Modified our web.xml with the Shiro specific config

  •     The important things to note here are:
         1. The Filter that in our case says every request must go through Shiro
         2. /rest/** = authcBasic  - This says that the urls here require basic authentication
         3. The users section

So not much work to achieve the authentication at all then.


Adding Authorization


Create a new class in your Web App:

import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
import org.apache.shiro.SecurityUtils;


public class AuthorizationInterceptor {

    @AroundInvoke
    public Object authenticate(InvocationContext context) throws Exception{

        if (!SecurityUtils.getSubject().hasRole("admin")){
            StringBuilder exBuilder = new StringBuilder();
            exBuilder.append("User: \'");
            exBuilder.append(SecurityUtils.getSubject().getPrincipal().toString());
            exBuilder.append("\' is not authorized to use this resource");


            throw new SecurityException(exBuilder.toString());
        }

        return context.proceed();
    }
}

And add the @Interceptors({AuthorizationInterceptor.class})  Annotation to your HelloResource class

package sparg.tim.personalhello;

import javax.ejb.Stateless;
import javax.interceptor.Interceptors;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.apache.shiro.SecurityUtils;

/**
 *
 * @author tsparg
 */
@Interceptors({AuthorizationInterceptor.class})
@Stateless
@Path("/message")
public class HelloResource {
    
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String sayHello() {

        StringBuilder builder = new StringBuilder();
        builder.append("Hello ");
        builder.append(SecurityUtils.getSubject().getPrincipal().toString());


        return builder.toString();
    }

}

Run your application and go to http://localhost:8080/PersonalHello/rest/message and login with ‘guest’ username ‘guest’ password (or any user that is not a part of the admin group).

You should now see an error page telling you that you are not authenticated to view this page.

If you log in with any of the other user codes that are members of the admin role (look in your web.xml to see which) you will proceed to the normal screen.


A couple of things to note:
  • Shiro has some very nifty hooks into both Authentication and Authorization so that you can leverage any existing infrastructure that you may have.
  • With the Way I have done the Authorization the App Server returns a HTTP 500(Internal Error), it would be nice if I could figure out how to return a HTTP 401 (Unauthorized)
  • You should most probably make the AuthorizationInterceptor a Default Interceptor, meaning you wouldn't have to annotate your classes for authorization.


I quite like this approach, as the authorization and authentication can be quite clearly moved away from any domain specific code. 

4 comments:

  1. Hi Tim,

    Great post! Thanks for sharing. Seeing this makes me think that we should support the JAX RS Interceptor mechanism directly in Shiro. That way, you might be able to use Shiro's existing annotations (@RequiresRoles, @RequiresPermissions, etc) directly.

    Anyway, good stuff!

    Cheers,

    Les

    ReplyDelete
  2. To return 401 status, throw a WebApplicationException: http://jsr311.java.net/nonav/releases/1.0/javax/ws/rs/WebApplicationException.html

    ReplyDelete
  3. Very Very Good!! Would be nice if you post an example using DB backend for permissions, roles, user etc.

    Congrats and Thanks

    ReplyDelete
  4. for exception handling you could also check this:

    http://bhaveshthaker.com/blog/184/technical-article-customize-handling-server-side-exceptions-with-error-codes-using-exceptionmapper-with-jersey-jax-rs-in-java/

    ReplyDelete