Tuesday, July 8, 2014

Web Service using Spring-WS with HTTP Digest Authentication

We have seen 'A Simple Web Service Using Spring-WS' in the previous post.
Now we need to secure the service from unauthorized access. We are going to apply HTTP Digest Authentication[1][2] for this.

Note that we are not implementing Message Signing or Encryption here. We will only ensure 'Authentication' using Spring XwsSecurityInterceptor[3], so that only authenticated users can access the add service we defined in the previous post.

First modify the ws-servlet.xml file and add the following bean definitions:

  • XwsSecurityInterceptor bean (inside the <sws:interceptors/> tag),
  • callBackHandlerDigest bean definition,
  • userDetailsService


ws-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
 xmlns:sws="http://www.springframework.org/schema/web-services"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/web-services http://www.springframework.org/schema/web-services/web-services-2.0.xsd">

 <sws:annotation-driven />

 <!-- To detect @Endpoint, @Service, @Component etc -->
 <context:component-scan base-package="com.test.ws" />

 <!-- To generate dynamic wsdl -->
 <sws:dynamic-wsdl id="wstest" portTypeName="add"
  locationUri="/services/add" targetNamespace="http://develop-for-fun.blogspot.com/spring-ws">
  <sws:xsd location="/WEB-INF/spring/service-definitions.xsd" />
 </sws:dynamic-wsdl>

 <!-- For validating your request and response -->
 <!-- So that you don't send a string instead of an integer -->

 <sws:interceptors>
  <bean id="validatingInterceptor"
   class="org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor">
   <property name="schema" value="/WEB-INF/spring/service-definitions.xsd" />
   <property name="validateRequest" value="true" />
   <property name="validateResponse" value="true" />
  </bean>
  <bean id="wsSecurityInterceptor"
              class="org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor">
            <property name="policyConfiguration" value="/WEB-INF/spring/securityPolicy.xml"/>
            <property name="callbackHandlers">
                <list>
                    <ref bean="callbackHandlerDigest"/>
                </list>
            </property>
        </bean>
 </sws:interceptors>

    <bean id="callbackHandlerDigest" class="org.springframework.ws.soap.security.xwss.callback.SpringDigestPasswordValidationCallbackHandler">
        <property name="userDetailsService" ref="userDetailsService"/>
    </bean>
    <bean id="userDetailsService" class="com.test.ws.services.CustomUserDetailsService" />

</beans>

security-policy.xml

<xwss:securityconfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    <xwss:requireusernametoken noncerequired="true" passworddigestrequired="true">
</xwss:requireusernametoken></xwss:securityconfiguration>

Add a Role enumeration to define User's role:

com.test.ws.enumeration.Role

package com.test.ws.enumeration;



public enum  Role {

    ROLE_ADMIN(1),

    ROLE_MEMBER(2);



    private final int value;



    Role(int p) {

        this.value = p;

    }



    public int getValue() {

        return this.value;

    }

}

Add the domain(/model/entity) class:

com.test.ws.domain.User

package com.test.ws.domain;

import com.test.ws.enumeration.Role;

/**
 * The User entity
 */

public class User {
    private int id;
    private String username;
    private String password;
    private String email;
    private String name;
    private Role role;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Role getRole() {
        return role;
    }

    public void setRole(Role role) {
        this.role = role;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Now add the dao class (with dummy user population):

com.test.ws.dao.UserDao

package com.test.ws.dao;

import com.test.ws.domain.User;
import com.test.ws.enumeration.Role;

public class UserDao {

    private static final String DEFAULT_ADMIN_PASS = "adminpass";
    private static final String DEFAULT_MEMBER_PASS = "memberpass";
    private static final String DUMMY_ADMIN_EMAIL = "admin@domain.com";
    private static final String DUMMY_MEMBER_EMAIL = "member@domain.com";

    public User getUser(String email) {
        /** We are only allowing now two dummy users here
         * Actually here we have to retrieve the users by the email address from Database **/

        User user = new User();
        if(email.equals(DUMMY_ADMIN_EMAIL)) {
            user.setId(1);
            user.setName("Administrator");
            user.setPassword(DEFAULT_ADMIN_PASS);
            user.setRole(Role.ROLE_ADMIN);
        } else if(email.equals(DUMMY_MEMBER_EMAIL)) {
            user.setId(2);
            user.setName("Member");
            user.setPassword(DEFAULT_MEMBER_PASS);
            user.setRole(Role.ROLE_ADMIN);
        }
        user.setEmail(email);
        return user;
    }
}



We need to create a CustomUserDetailsService which implements UserDetailsService

com.test.ws.services.CustomUserDetailsService

package com.test.ws.services;

import com.test.ws.dao.UserDao;
import com.test.ws.domain.User;
import com.test.ws.enumeration.Role;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    UserDao userDao = new UserDao();

    @Override
    public UserDetails loadUserByUsername(String email) {
        try {
            // Retrieve the user by email
            User user = userDao.getUser(email);

            int roleId = user.getRole().getValue();
            return new org.springframework.security.core.userdetails.User (
                    user.getEmail(), user.getPassword(), true, true, true, true, getAuthorities(roleId)
            );
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }

    }

    public Collection<? extends GrantedAuthority> getAuthorities(Integer role) {
        List<GrantedAuthority> authList = getGrantedAuthorities(getRoles(role));
        return authList;
    }

    public List<String> getRoles(Integer role) {

        List<String> roles = new ArrayList<String>();

        if (role.intValue() == Role.ROLE_ADMIN.getValue()) {
            roles.add(String.valueOf(Role.ROLE_ADMIN));
        } else if (role.intValue() == Role.ROLE_MEMBER.getValue()) {
            roles.add(String.valueOf(Role.ROLE_MEMBER));
        }
        return roles;
    }

    public static List<GrantedAuthority> getGrantedAuthorities(List<String> roles) {
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();

        for (String role : roles) {
            authorities.add(new SimpleGrantedAuthority(role));
        }
        return authorities;
    }
}

This is how the project-structure should look like (underlined items are newly added over the project in the previous post):

Project Structure



The project can be downloaded using git from spring-ws-test project (secured_user_auth_digest branch):
git clone https://github.com/tariqmnasim/spring-ws-test.git -b secured_user_auth_digest

References:
[1] HTTP Authentication: Basic and Digest Access Authentication
[2] Wikipedia: Digest access Authentication
[3] Securing your web services using Spring-WS