Using Spring Security with ExtJS for Siteminder/LDAP authentication and authorization

Overview

I’ve been merrily creating my ExtJS 4 application when the inevitable came that we must secure the application. Of course it doesn’t stop at authentication, especially in a client-side where nothing should be assumed secure; the users of course want to show/hide, enable/disable things using role-based security.  In a previous life, I used Spring Security to do this sort of thing for Java applications, so I wondered why I couldn’t just do the same for ExtJS.  This article shares a complete example to do exactly that.  Here I will show you how you can use your existing IT infrastructure to secure your application (in my case LDAP and Siteminder) and how to integrate Spring Security 3.0 with ExtJS 4. Lastly, I would like to thank Dmitry and Rajesh for their contributions on this topic.

Authentication

The first step is to step up authentication to your application. The first approach I’ll show is using LDAP authentication (authn) followed by Siteminder authn. To set-up LDAP authn, you have to extend your HTML/JS/CSS3 app to a Java EE Spring application. There are already several articles on the Web on how to do this and most of today’s IDEs can go this for your automatically. Once you have created your Java EE app, you need to configure your web.xml. The basic gist of it is that you will be using a servlet filter to intercept URLs that invoke the application (see DelegatingFilterProxy). Here is an example one:

<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <display-name>Your App</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>WEB-INF/applicationContext.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <filter>
        <filter-name>charsetFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter> 
    <filter-mapping>
        <filter-name>charsetFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
      <filter-name>springSecurityFilterChain</filter-name>
      <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
      <filter-name>springSecurityFilterChain</filter-name>
      <url-pattern>/*</url-pattern>
    </filter-mapping>

    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

In your Spring applicationContext.xml you’ll want to create and import another spring context configuration that contains security specifics. In the example that follows, I show how you can do LDAP authentication:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-3.0.xsd


http://www.springframework.org/schema/security


http://www.springframework.org/schema/security/spring-security-3.0.3.xsd">

    <security:http auto-config='true' use-expressions="true">
        <security:intercept-url pattern="/**" access="isAuthenticated()" />
        <!--access="hasRole('ROLE_LDAP_GROUP')"/>-->
        <security:http-basic/>
    </security:http>

    <security:global-method-security pre-post-annotations="enabled" />

    <security:ldap-server id="ldapServer" port="636" root="o=company"
                          url="ldaps://myldap.company.com"/>

    <security:authentication-manager alias="company">
        <security:ldap-authentication-provider
                group-search-base="ou=yourgroupou,ou=groups,o=company"
                group-role-attribute="cn"
                user-search-base="ou=people,o=company"
                user-search-filter="uid={0}"
                />

    </security:authentication-manager>
</beans>

Here I do an anonymous bind over the LDAPS port to authenticate based on the users’ credentials they provide (be sure you use SSL and take the time to set up your JVM to support it with our internal certs). The intercept-url includes a pattern (I did everything with “/**”) and the type of access (I did “isAuthenticated()” and defer authorization till later). You’ll see I have commented out access using hasRole(‘ROLE_LDAP_GROUP’) which can be used to limit the users able to open the application using an LDAP group, but I didn’t want to do that. In the next example, I show how you can do the same thing with Siteminder:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-3.0.xsd


http://www.springframework.org/schema/security


http://www.springframework.org/schema/security/spring-security-3.0.3.xsd">

    <bean id="http403EntryPoint" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint">
    </bean>

    <sec:http auto-config="false" entry-point-ref="http403EntryPoint">
        <!--<sec:intercept-url pattern="/**"
                           access="isAuthenticated()"/>-->
        <sec:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" />
    </sec:http>

    <bean id="siteminderFilter" class=
            "org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
        <property name="principalRequestHeader" value="SM_USER"/>
        <property name="authenticationManager" ref="authenticationManager"/>
    </bean>

    <bean id="preauthAuthProvider"
          class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
        <property name="preAuthenticatedUserDetailsService">
            <bean id="userDetailsServiceWrapper"
                  class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
                <property name="userDetailsService" ref="ldapUserDetailsService"/>
            </bean>
        </property>
    </bean>

    <sec:authentication-manager alias="authenticationManager">
        <sec:authentication-provider ref="preauthAuthProvider"/>
    </sec:authentication-manager>

    <!-- Example using LDAP -->
    <sec:ldap-server id="ldapServer" port="636" root="o=company"
                          url="ldaps://dir.company.com"/> 
    <sec:ldap-user-service id="ldapUserDetailsService" server-ref="ldapServer"
                           group-search-base="ou=yourgroupou,ou=groups,o=company"
                           role-prefix="ROLE_" group-role-attribute="cn"
                           user-search-base="ou=people,o=company" user-search-filter="uid={0}"/>
    <security:ldap-server id="ldapServer" port="636" root="o=company"
                          url="ldaps://myldap.company.com"/>
</beans>

While this has been all going on, it is necessary to block the Ext JS app until we have confirmed the identity of the user. In Ext JS we can set up an Ajax request to do that and use the success listener to block the viewport from being created until success is achieved. You’ll notice that my Ajax request is a HTTP POST to user/getUser. This is the mechanism I used to determine the principal from the server-side by parsing the JSON returned into the roles array for the application, which I’ll go into in a little bit.

Ext.application({
    name : 'App',
    controllers : [ 
        'dashboard.TabController' ],
    roles : [ ], 
    version : '1.0',
    onFailure : function(title, response, options) {
        Ext.MessageBox.show({
            title : title,
            msg : response,
            width : 400,
            icon : Ext.Msg.WARNING,
            buttons : Ext.Msg.OK
        });
    },
    hasRole : function(role) {
        return Ext.Array.contains(this.roles, role);
    },
    launch : function() {
        App.app = this;

    
        Ext.Ajax.request({
            url : 'user/getUser',
            method : 'post',
            timeout : '30000',
            defaultHeaders : {
                'Content-Type' : 'application/json; charset=utf-8'
            },
            success : function(response, opts) {
                try {
                    var result = Ext
                            .decode(response.responseText);
                    if (result !== null) {
                        var user = result.userName;
                        var perms = result.permissions;
                        for (var i = 0; i < perms.length; i += 1) {
                            if (perms[i].role
                                    .indexOf("ROLE_ADMIN") === 0) {
                                App.app.roles
                                        .push(perms[i].role);
                            }
                        }
                        if (user !== null) {
                            Ext.QuickTips.init();
                            Ext.create('Ext.container.Viewport',
                            {
                               // Do your normal stuff here
                            });
                        } else {
                            App.app
                                    .onFailure(
                                    'Authorization Failed',
                                    'User does not have the correct permissions',
                                    opts);
                        }
                    } else {
                        App.app
                                .onFailure(
                                'Authorization Failed',
                                'Response returned from service was null.',
                                opts);
                    }
                } catch (e) {
                    App.app.onFailure(
                            'User Serivce Failure', e, opts);
                }
            },
            failure : function(response, opts) {
                App.app.onFailure('User Service Failure',
                        response, opts);
            }
        });

    }
});

Now, let’s take a look at the UserController Java class. This is the class that is responsible for determining the user and retrieving his/her roles. What is really nice about Spring is that I can use their MVC controller mechanism and annotations to intercept URLs and do some sort of logic with a simple POJO as follows:

@Controller
@RequestMapping(value = "/user")
public class UserController {
    private static Logger log = Logger.getLogger(UserController.class);


    @RequestMapping(value = "/getUser", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    public @ResponseBody String getUser() throws IllegalStateException {
        try {
            User user = new User(this.getUserName());


            Collection perms = this.getUserPermissions();
            user.setPermissions(perms);

            Gson gson = new Gson();
            String result = gson.toJson(user, User.class);
            log.info("Sending user object: " + result);

            return result;
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new IllegalStateException(e);
        }
    }


    private String getUserName() {
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String username = "";


        if (principal instanceof UserDetails) {
            username = ((UserDetails)principal).getUsername();
        } else {
            username = principal.toString();
        }
        return username;
    }


    private Collection getUserPermissions() {
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        Collection perms = null;


        if (principal instanceof UserDetails) {
           perms = ((UserDetails)principal).getAuthorities();
        }


        return perms;
    }
}

Notice I use the Spring’s SecurityContextHolder to retrieve the principal as well as his/her authorities (i.e., roles). I populate those into a User bean, convert that to JSON and return the result to the client. Once this information is provided to the client, we can manage roles effectively on the client side.

Authorization

Now that we’ve authenticated the user, collected their roles and parsed them into the Ext.application’s roles array, how can we use that in the application to protect components. The answer is simple really, you just add some conditional logic for the components config for either “hidden” or “disabled”. For instance, I had a settings Tab on a tab panel that only administrators should see. I was able to accomplish that with this bit of JavaScript code:

{
    xtype : 'settingsTab',
    //Example of role-based hiding
    hidden : !App.app.hasRole('ROLE_UI.ADMIN')
}


// function to support above
hasRole : function(role) {
    return Ext.Array.contains(this.roles, role);
}

As you can see from above, I simply have hasRole return true or false based on whether the user has a given role established at the time we invoked the application and made the AJAX call to getUser() from the previous section. If the user is in the LDAP group “UI.ADMIN” (NOTE: Spring automatically prepends ROLE_), they are granted access to see the tab.

Now you might be saying, well I could write a greasemonkey script and hack the JavaScript so I could see the tab and indeed you can, but the approach I demonstrate is only used to change the behavior of the application, not the functionality. Let’s say you applied this to a “save” button instead, that save button is going to call some server-side functionality to, well, save. You should definitely check on the server-side that the user can execute save functions (in the example below, updateDriver). It would be folly to expect a client-side application to verify this. Again, Spring Security to the rescue with a simple annotation, @PreAuthorize, to the server-side method to check the role of the user as follows:

@RequestMapping(method = RequestMethod.PUT)
    @ResponseStatus(HttpStatus.NO_CONTENT)
    @PreAuthorize("hasAuthority('ROLE_UI.ADMIN')")
    public void updateDriver(@RequestBody String json) throws IllegalStateException {
        try {
            // Using Gson to Serialize/Unserialize as example in case post-processing required
            Gson gson = new Gson();
            HosDriverEvent hde = gson.fromJson(json, HosDriverEvent.class);
            restTemplate.put(VEHICLE_DETAILS_SAVE_URL, gson.toJson(hde));
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new IllegalStateException(e);
        }
    }

Even before this method gets invoked, if the user does not have that role, they will get a 403 exception and thus preventing them from accessing the save operation.

Data Security

Another aspect of security for your application to consider should be the data itself. For instance, you may show a grid of data but one user is authorized to see more information than another. This is controlled with identity propagation to the underlying services. This means that with Siteminder, the users secure, time-based identity will be propagated in the HTTP header downstream to the underlying services. The services can validate the users authorities by parsing the header for the principal and querying data appropriately based on that user’s identity. Thus, the data returned will be unique for that user. It’s important to make sure your underlying services do this sort of checking.

Another approach is to use a secure token service (STS) and SAML assertions. This is especially popular in SOA’s. More information can be found in Thomas Erl’s seminal work on the topic.

Conclusion

Security is essential to all business-based Web applications and so it needs to be carefully thought out. Here I hope I’ve provided useful information on how you can implement your customer’s security requirements for an Ext JS application using Spring Security. You have to assume nothing is ever secure on a client-side application, so you must include a server-side component to ensure your application is secure.

About these ads

4 comments on “Using Spring Security with ExtJS for Siteminder/LDAP authentication and authorization

  1. Nice! Have i question though, here you don’t mention anything about the login page. I am assuming your application uses, login.jsp? So if we had to use EXT JS View (for the sake of same look and feel of the app) – a) would it be possible? b) if yes, would the solution change in a way that – you would create the viewport in anycase, but have card layout with login as one of the cards?

    • I’ve used BASIC auth for the most part, but I also have done FORM based auth. In that case, you need to work with your Siteminder to have them give you the standard HTML form that Siteminder uses so you have the right fields/configuration. In our case that’s a generic login form for our company that is hosted on the Siteminder server and just forwards to my app from the HTTP_REFERRER. No need for us to host that in our app as long as the header value is generated with the same key value.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s