Sunday, January 22, 2017

Spring Security By Example


This post presents some possible use cases of Spring Security (4.2.1.RELEASE). A zip archive containing an eclipse dynamic web project is provided for each use case.

Mainly we consider a web application with a page (secret.html) that needs to be secured: access to this page should only be granted to authorized users.

The first 3 projects do not use Spring Security, they try to implement the security contract described above relying on different techniques such as HTTP servlets (using sessions) and filters. The 5 remaining projects implement scenarios using Spring Security ranging over: plain login form, database, LDAP and X509 certificate authentication.

The 8 projects are intended to be run under a light container, Apache Tomcat (9.0.0.M15), for instance.

  1. 0-BadAuthentication.zip: in this project we implement a login page where a user can type username and password token. Access will be granted to secret.html only when the token isg/isg is provided. The problem with this example is that if the user try to access secret.html even without being previously logged in, access will be granted. This example is described here to point out a common mistake when it comes to implementing access control: over-securing front doors and neglecting potential back doors ...

  2. 1-BetterAuthentication.zip: the flaw of the previous project is here fixed by moving secret.html to the WEB-INF/ sub-folder, which is "naturally" protected: no access addressing resources in this sub-folder will be ever granted unless it is done through a forwarding within the container itself.

  3. 2-FilterBasedAuthentication.zip: this projects uses a filter to enforce security. This filter intercepts all access requests addressing secret.html and authorize it, if and only if, the user has been successfully authenticated before. To keep track of a successful authentication, we use a session attribute in which we store the username. The filter has only to check whether this attribute is valuated or not (null).

  4. 3-SpringSecurityAuthenticationSimple.zip: we use Spring Security here to implement the security policy. Except for the page to be secured and configuration files no extra code is actually needed. Even the login form is provided by Spring Security. The configuration files are listed below:

    web.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID_WebApp_ID_3-SpringSecurityAuthenticationSimple" version="3.1">
     <display-name>3-SpringSecurityAuthenticationSimple</display-name>
     <welcome-file-list>
      <welcome-file>secret.html</welcome-file>
     </welcome-file-list>
     <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring-security.xml </param-value>
     </context-param>
     <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>
     <listener>
      <listener-class>
       org.springframework.web.context.ContextLoaderListener
      </listener-class>
     </listener>
    </web-app>
    

    The container is asked to forward the user to secret.html. A filter (springSecurityFilterChain) provided by Spring Security is used to control the access to all application resources (/* pattern) according to the policy described in the configuration file spring-security.xml, listed below. A listener is used to initiate Spring (and Spring Security) when the application starts.

    spring-security.xml
    <b:beans xmlns="http://www.springframework.org/schema/security"
     xmlns:b="http://www.springframework.org/schema/beans"
     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.xsd
      http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"
    >
     <http auto-config="true">
      <csrf disabled="true"/>
      <intercept-url
       pattern="/secret.html"
       access="hasRole('ROLE_ISG')"
      />
     </http>
     <user-service>
      <user name="isg" password="isg" authorities="ROLE_ISG" />
     </user-service>
    </b:beans>
    

    This file contains the policy (<intercept-url /> tag) that specifies that a user has to be authenticated and must have a privilege named ROLE_ISG to be granted access to secret.html. The file also specifies the known users database which is hard-coded inside the <user-service /> tag: only one user named isg identified by isg and possessing a privilege named ROLE_ISG will be recognized.

  5. 4-SpringSecurityAuthenticationSimpleCustomForm.zip: Here we add a small customization to the result obtained in previous project: we provide our own login form page (index.html) and tell Spring Security to use it in stead. This is done through the use of the <form-login /> tag.

    spring-security.xml
    <b:beans xmlns="http://www.springframework.org/schema/security"
     xmlns:b="http://www.springframework.org/schema/beans"
     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.xsd
      http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"
    >
     <http auto-config="true">
      <csrf disabled="true"/>
      <form-login
       login-processing-url="/login"
       login-page="/index.html"
       username-parameter="username"
       password-parameter="password"
      />
      <intercept-url
       pattern="/secret.html"
       access="hasRole('ROLE_ISG')"
      />
     </http>
     <user-service>
      <user name="isg" password="isg" authorities="ROLE_ISG" />
     </user-service>
    </b:beans>
    

  6. 5-SpringSecurityAuthenticationJdbc.zip: here we provide database authentication. We assume that known users and their privileges are stored in tables. The SQL (MySQL dialect) script below provides more details:

    SecurityTables.sql
    create table USERS (
     USERNAME VARCHAR(36) not null,
     PASSWORD VARCHAR(36) not null,
     ENABLED  smallint not null
    );
    
    alter table USERS
     add constraint USER_PK primary key (USERNAME);
    
    create table USER_ROLES (
     ROLE_ID   VARCHAR(50) not null,
     USERNAME  VARCHAR(36) not null,
     USER_ROLE VARCHAR(30) not null
    );
     
    alter table USER_ROLES
     add constraint USER_ROLE_PK primary key (ROLE_ID);
    
    alter table USER_ROLES
     add constraint USER_ROLE_UK unique (USERNAME, USER_ROLE);
    
    insert into USERS values('isg', 'isg', 1);
    insert into USER_ROLES values('1', 'isg', 'ISG');
    commit;
    


    In spring-security.xml configuration file (listed below) we still stick to the same access control policy. We provide in addition the database properties and the requests to be performed to authenticate users and to get an authenticated user's privileges. Don't forget to change database properties (database user, password, schema, server and port ...) to fit your settings.

    spring-security.xml
    <b:beans xmlns="http://www.springframework.org/schema/security"
     xmlns:b="http://www.springframework.org/schema/beans"
     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.xsd
      http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"
    >
     <http auto-config="true">
      <csrf disabled="true"/>
      <intercept-url
       pattern="/secret.html"
       access="hasRole('ROLE_ISG')"
      />
     </http>
     <b:bean
      id="myDataSource"
      class="org.springframework.jdbc.datasource.DriverManagerDataSource"
     >
      <b:property
       name="driverClassName"
       value="com.mysql.jdbc.Driver"
      />
      <b:property
       name="url"
       value="jdbc:mysql://localhost:3306/spring_jdbc"
      />
      <b:property
       name="username"
       value="root"
      />
      <b:property
       name="password"
       value="*******"
      />
     </b:bean>
     <authentication-manager>
      <authentication-provider>
       <jdbc-user-service
        data-source-ref="myDataSource"
        users-by-username-query="select USERNAME, PASSWORD, ENABLED from USERS where USERNAME=?"
        authorities-by-username-query="select USERNAME, concat('ROLE_', USER_ROLE) USER_ROLE from USER_ROLES where USERNAME=?"
       />
      </authentication-provider>
     </authentication-manager>
    </b:beans>
    

  7. 6-SpringSecurityAuthenticationLdap.zip: we switch in this project to LDAP-based authentication. For instance we use the test-purpose online LDAP server described here. This directory includes a set of entries dispatched in different groups mathematicians, chemists ... The access control policy we choose to enforce here states that only members of group mathematicians can access secret.html (see <intercept-url /> tag).

    spring-security.xml
    <b:beans xmlns="http://www.springframework.org/schema/security"
     xmlns:b="http://www.springframework.org/schema/beans"
     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.xsd
      http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"
    >
     <http auto-config="true">
      <csrf disabled="true"/>
      <access-denied-handler
       error-page="/denied.html"
      />
      <intercept-url
       pattern="/secret.html"
       access="hasRole('ROLE_MATHEMATICIANS')"
      />
     </http>
     <ldap-server 
      id="contextSource" 
      url="ldap://ldap.forumsys.com:389/"
      manager-dn="cn=read-only-admin,dc=example,dc=com"
      manager-password="password"
     />
     
     <authentication-manager>
      <ldap-authentication-provider
       user-search-base="dc=example,dc=com"
       user-search-filter="(uid={0})"
       group-search-base="ou=mathematicians,dc=example,dc=com"
       group-search-filter="(uniqueMember={0})"
       role-prefix="ROLE_"
      >
      </ldap-authentication-provider>
     </authentication-manager>
    </b:beans>
    

  8. 7-SpringSecurityAuthenticationX509.zip: here we switch to X509 authentication. This setting implies extra-configuration in the container itself (Apache Tomcat). We must provide keystores to Apache Tomcat:  the first one (server-keystore.jks) contains its own public and private key, the second (server-truststore.jks) contains the public keys of trusted clients. Note that the project contains a folder named keystores/ holding the formerly described stores (under sub-folder server/) but also a set of 3 client profiles with their own pairs of keys (under sub-folder client/). All passwords for the keystores are cn$$cn$$. First update to make to the container configuration is to add the SSL connector to its server.xml file.

    server.xml
    <Connector
     SSLEnabled="true"
     acceptCount="100"
     clientAuth="true"
     disableUploadTimeout="true"
     enableLookups="false"
     keystoreFile="$PROJECT_PATH/keystores/server/server-keystore.jks"
     keystorePass="cn$$cn$$"
     maxHttpHeaderSize="8192"
     maxSpareThreads="75"
     maxThreads="150"
     minSpareThreads="25"
     port="8443"
     scheme="https"
     secure="true"
     sslProtocol="TLS"
     truststoreFile="$PROJECT_PATH/keystores/server/server-truststore.jks"
     truststorePass="cn$$cn$$"
    />
    

    Below the web.xml configuration file of our project. Updates here are that we tell the container that transport security is delegated to Spring Security and that SSL has to be used.

    web.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID_7-SpringSecurityAuthenticationX509" version="3.1">
     
     <display-name>7-SpringSecurityAuthenticationX509</display-name>
     
     <welcome-file-list>
      <welcome-file>secret.html</welcome-file>
     </welcome-file-list>
      
     <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring-security.xml </param-value>
     </context-param>
     <security-constraint>
      <web-resource-collection>
       <web-resource-name>Secured</web-resource-name>
       <url-pattern>/*</url-pattern>
      </web-resource-collection>
      <user-data-constraint>
       <transport-guarantee>CONFIDENTIAL</transport-guarantee>
      </user-data-constraint>
     </security-constraint>
     <login-config>
      <auth-method>CLIENT-CERT</auth-method>
      <realm-name>certificate</realm-name>
     </login-config>
     <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>
    
     <listener>
      <listener-class>
       org.springframework.web.context.ContextLoaderListener
      </listener-class>
     </listener>
     
    </web-app>
    

    Lets take a look to the spring-security.xml configuration file. The main bean is x509Filter. This bean uses 2 other beans: myPrincipalExtractor and authManager.

    spring-security.xml
    <b:beans xmlns="http://www.springframework.org/schema/security"
     xmlns:b="http://www.springframework.org/schema/beans"
     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.xsd
      http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"
    >
     <b:bean
      id="x509Filter"
      class="org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter"
     >
      <b:property
       name="authenticationManager"
       ref="authManager"
      />
      <b:property
       name="principalExtractor"
       ref="myPrincipalExtractor"
      />
     </b:bean>
     <b:bean
      id="myPrincipalExtractor"
      class="tn.rnu.isg.security.MyPrincipalExtractor"
     />
     <b:bean
      id="preauthAuthenticationProvider"
      class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider"
     >
      <b:property
       name="preAuthenticatedUserDetailsService"
       ref="authenticationUserDetailsService"
      />
     </b:bean>
     <b:bean
      id="myUserDetailsService"
      class="tn.rnu.isg.security.MyUserDetailsService"
     />
     <b:bean
      id="authenticationUserDetailsService"
      class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper"
     >
      <b:property
       name="userDetailsService"
       ref="myUserDetailsService"
      />
     </b:bean>
     <b:bean
      id="forbiddenAuthEntryPoint"
         class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint"
        />
     <http use-expressions="true" entry-point-ref="forbiddenAuthEntryPoint" >
      <csrf disabled="true"/>
      <access-denied-handler
       error-page="/denied.html"
      />
      <intercept-url
       pattern="/secret.html"
       access="hasRole('ROLE_ISG')"
      />
      <custom-filter
       ref="x509Filter"
       position="X509_FILTER"
      />
     </http>
     
        <authentication-manager alias="authManager">
            <authentication-provider ref="preauthAuthenticationProvider" />
        </authentication-manager>
    </b:beans>
    

    myPrincipalExtractor specifies which attributes have to be extracted from the certificate. Here we choose to extract the hole DN (distinguished name) of the client.

    MyPrincipalExtractor.java
    package tn.rnu.isg.security;
    
    import java.security.cert.X509Certificate;
    
    import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
    
    public class MyPrincipalExtractor implements X509PrincipalExtractor {
     @Override
     public Object extractPrincipal(X509Certificate x509Certificate) {
      return x509Certificate.getSubjectX500Principal().getName();
     }
    }
    

    authManager relies on myUserDetailsService bean to authenticate (or not) the user but also to specify what privileges are assigned to the successfully authenticated user. In the implementation below we choose to successfully authenticate all the clients but to grant the role ROLE_ISG only to user client3.

    MyUserDetailsService.java
    package tn.rnu.isg.security;
    
    import java.util.ArrayList;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    
    public class MyUserDetailsService implements UserDetailsService {
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      if ((username == null) || (username.isEmpty())) {
       throw new UsernameNotFoundException("Username not found !!!");
      } else {
       if ("CN=CLIENT3,OU=DEI,O=CNSS,L=TUNIS,ST=TUNIS,C=TN".equals(username.toUpperCase())) {
        return (
         new User(
          username,
          "",
          AuthorityUtils.createAuthorityList(
           "ROLE_ISG"
          )
         )
        );
       } else {
        return (
         new User(
          username,
          "", 
          new ArrayList<grantedauthority>()
         )
        );
       }
      }
     }
    }
    

Keywords: Spring Security 4, Authentication, Plain Login Form, LDAP, X509, JDBC