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.

Advertisements

Setting up GIT with Apache Smart HTTP/S and LDAP

I recently was put on a project to explore how we could use GIT over HTTP and integrate with our existing LDAP for authnz.  The reason for HTTP is that it is pretty easy to set-up and you can encrypt the content transfer with SSL.  Also, HTTP/S is firewall friendly.  The downside is that HTTP is a “dumb” protocol. The information here consolidates some information I found on the web to accomplish this.  I am using RHEL 6, Apache 2.2, OpenLDAP and msysgit for my GIT client on my Windows machine.

First off, HTTP wasn’t necessarily the fastest protocol to use with GIT until they added a mod called git-http-backend, or SMART-HTTP, as of GIT 1.6.6. This article from the Pro GIT author, @chacon, details this and from my experience I cut my download times by two-thirds using this approach.  Moreover, github is also supporting this.  Basically what you need to do is as follows:

  1. Confirm you have Apache 2.2 installed: rpm -q httpd (install it with yum otherwise)
  2. Clone your GIT repo to Apache by doing the following (as per Pro GIT book):
    $ cd /var/www/html/git (mkdir if necessary)
    $ git clone  --bare /path/to/git_project gitproject.git
    $ cd gitproject.git
    $ mv  hooks/post- update.sample  hooks/post- update
    $ chmod a+x  hooks/post- update 
  3. Update your httpd.conf to include this:
    SetEnv GIT_PROJECT_ROOT /var/www/html/git
    SetEnv GIT_HTTP_EXPORT_ALL
    ScriptAlias /git/ /usr/libexec/git-core/git-http-backend/
  4. Add LDAP Authentication (in this case, any valid LDAP user will have access to the git location) as follows:
    <LocationMatch "^/git/.*/git-receive-pack$">
            SSLRequireSSL
            Order deny,allow
            Deny from All
            AuthName "GIT Repo"
            AuthType Basic
            AuthBasicProvider ldap
            AuthzLDAPAuthoritative off
            AuthLDAPURL "ldap://ldap-server.company.com:389/ou=users,o=company?uid"
            Require valid-user
    </LocationMatch>
  5. Restart httpd: /etc/init.d/httpd restart

For LDAP authorization, of course you may have several different repos running off the same host, all which require certain users or groups access to the given location. This site explains this in detail, but here is an example I used so that I could bind a particular repo location to an LDAP group with SSL in place:

<LocationMatch "/git/gitproject*">
        SSLRequireSSL
        Order deny,allow
        Deny from All
        AuthName "GIT Repo"
        AuthType Basic
        AuthBasicProvider ldap
        AuthzLDAPAuthoritative on
        LDAPTrustedGlobalCert CA_BASE64 /etc/pki/tls/http/rootCA.crt
        AuthLDAPURL "ldaps://ldap-server.company.com:636/ou=users,o=company?uid"
        AuthLDAPGroupAttribute member
        AuthLDAPGroupAttributeIsDN on
        Require ldap-group cn=my.group,ou=groups,o=company
        Satisfy any
</LocationMatch>

In this example, I’m binding project.git (use wildcard for LocationMatch in-case users forget to add .git extension) to any member in the LDAP group “my.group”. Note that you may need to define a different LDAP group attribute to match the field that will contain the DN of your users .  If you are not storing DN, then you can set AuthLDAPGroupAttributeIsDN to off.

The last step is enable SSL on your Apache server.  We use self-signed CERTs internally so you’re going to have to add those certs to your GIT clients unless of course your using a well known root CA like Verisign.  To do that with msysgit, you open the $MYSYSGIT_INSTALL/bin/curl-ca-bundle.crt and add the base-64 encoded text of your keys to the end of this file.  Then run the following command from the GIT BASH:

$ git config --global http.sslcainfo c:\\apps\\Git\\bin\\curl-ca-bundle.crt

That’s pretty much it!  Now you can simply connect to your repo from msysgit with SSL (no SSH keys req’d) and LDAP authorization:

$ git clone https://mygitserver.company.com/git/project.git
Cloning Project...
Username: [put user name that's in the LDAP group]
Password: [password]
remote: Counting objects: 17, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 17 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (17/17), done.