Michael Coxon
Michael Coxon

Reputation: 3535

Spring Boot Test: Spring Security with custom auth provider not visible on springsecurityfilterchain

I have rather special requirements for authentication (being a username, password and device or just device to enter). This made me conclude that the usual UsernamePasswordAuthenticationFilter wasn't going to work, so I set up my own filter, provider and token, which are shown below. First the provider:

@Service(value="customAuthenticationProvider")
public class DeviceUsernamePasswordAuthenticationProvider implements AuthenticationProvider {
    private static final Logger LOG = LoggerFactory.getLogger(DeviceUsernamePasswordAuthenticationProvider.class);

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private DeviceDetailsService deviceDetailsService;

    @Override
    public boolean supports(Class<? extends Object> authentication) {
        return authentication.equals(DeviceUsernamePasswordAuthenticationToken.class);
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        LOG.info("Authenticating device and user - assigning authorities...");
        DeviceUsernamePasswordAuthenticationToken auth = (DeviceUsernamePasswordAuthenticationToken) authentication;
        String name = auth.getName();
        String password = auth.getCredentials().toString();

        boolean isDeviceRequest = (name == null && password == null);
        LOG.debug("name is {}, password is {}", name, password);

        // (a) nothing, (b) hasToken|<token encoding>, or (c) getToken|<base64 encoded device request>
        String deviceToken = auth.getDeviceAuthorisation();

        if (deviceToken == null) {
            // very bad - set as anonymous
            LOG.error("missing.device.token");
            throw new BadCredentialsException("missing.device.token");
        }

        LOG.debug("deviceToken is {}", deviceToken);
        String[] deviceInformation = StringUtils.split(deviceToken,"|");


        DeviceDetails device = null;

        if(deviceInformation[0].equals("getToken")) {
            LOG.debug("getToken");
            // we expect the array to be of length 3, if not, the request is malformed
            if (deviceInformation.length < 3) {
                LOG.error("malformed.device.token");
                throw new BadCredentialsException("malformed.device.token");
            }

            device = deviceDetailsService.loadDeviceByDeviceId(deviceInformation[1]);

            if (device == null) {
                LOG.error("missing.device");
                throw new BadCredentialsException("missing.device");
            } else {
                // otherwise, get the authorities
                auth = new DeviceUsernamePasswordAuthenticationToken(null, null,
                        device.getDeviceId(), device.getAuthorities());

                //also we need to set a new token into the database

                String newToken = Hashing.sha256()
                        .hashString("your input", Charsets.UTF_8)
                        .toString();


                deviceDetailsService.setToken(device.getDeviceId(),newToken);

                // and put it into the response headers
                auth.setDeviceTokenForHeaders(newToken);

            }
        } else if(deviceInformation[0].equals("hasToken")) {
            LOG.debug("hasToken");
            if (deviceInformation.length < 3) {
                LOG.error("malformed.device.token");
                throw new BadCredentialsException("malformed.device.token");
            }

            // check that there is a token and that the token has not expired
            String token = deviceDetailsService.getToken(deviceInformation[1]);

            if (token == null) {
                // we got a token in the request but the token we have no stored token
                LOG.error("mismatched.device.token");
                throw new BadCredentialsException("mismatched.device.token");
            } else if(!token.equals(deviceInformation[2])) {
                // we got a token in the request and its not the same as the token we have stored
                LOG.error("mismatched.device.token");
                throw new BadCredentialsException("mismatched.device.token");
            } else if ( deviceDetailsService.hasTokenExpired(deviceInformation[1])) {
                // we got a token in the request and its not the same as the token we have stored
                LOG.error("expired.device.token");
                throw new BadCredentialsException("expired.device.token");
            } else {
                // token was in the request, correctly formed, and matches out records
                device = deviceDetailsService.loadDeviceByDeviceId(deviceInformation[1]);
                auth = new DeviceUsernamePasswordAuthenticationToken(null, null,
                        device.getDeviceId(), device.getAuthorities());
            }


        } else {
            LOG.error("malformed.device.token");
            throw new BadCredentialsException("malformed.device.token");
        }

        if (!isDeviceRequest) {

            UserDetails user = customUserDetailsService.loadUserByUsername(name);
            auth = new DeviceUsernamePasswordAuthenticationToken(name, password, device.getDeviceId(), device.getAuthorities());
        }

        return auth;
    }

}

the token:

public class DeviceUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
    private String deviceAuthorisation;
    private String deviceTokenForHeaders;

    public DeviceUsernamePasswordAuthenticationToken(Object principal, Object credentials, String deviceAuthorisation) {
        super(principal, credentials);
        this.deviceAuthorisation = deviceAuthorisation;
    }

    public DeviceUsernamePasswordAuthenticationToken(Object principal, Object credentials, String deviceAuthorisation, List<GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
        this.deviceAuthorisation = deviceAuthorisation;
    }

    public String getDeviceAuthorisation() {
        return deviceAuthorisation;
    }


    public void setDeviceAuthorisation(String deviceAuthorisation) {
        this.deviceAuthorisation = deviceAuthorisation;
    }

    public String getDeviceTokenForHeaders() {
        return deviceTokenForHeaders;
    }

    public void setDeviceTokenForHeaders(String deviceTokenForHeaders) {
        this.deviceTokenForHeaders = deviceTokenForHeaders;
    }

    @Override
    public String toString() {
        return "DeviceUsernamePasswordAuthenticationToken{" +
                "deviceAuthorisation='" + deviceAuthorisation + '\'' +
                '}';
    }
}

and the filter:

public class DeviceUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public static final String SPRING_SECURITY_FORM_DEVICE_KEY = "device";
    private String deviceParameter = SPRING_SECURITY_FORM_DEVICE_KEY;
    private boolean postOnly = true;

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        String username = obtainUsername(request);
        String password = obtainPassword(request);
        String device = obtainDevice(request);

        if(username != null) {
            username = username.trim();
        }

        DeviceUsernamePasswordAuthenticationToken authRequest = new DeviceUsernamePasswordAuthenticationToken(username, password, device);


        // TODO: check an see if I need to do any additional work here.
        setDetails(request, authRequest);

        response.addHeader("X-AUTH-TOKEN", authRequest.getDeviceTokenForHeaders());

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    protected String obtainDevice(HttpServletRequest request) {

        String token = "hasToken|" + request.getHeader("X-AUTH-TOKEN");

        if(token == null) {
            String deviceInformation = request.getParameter(deviceParameter);
            if(deviceInformation != null) {
                token = "getToken|" + StringUtils.newStringUtf8(
                        Base64.decodeBase64(deviceInformation));
            }
        }
        return token;
    }
}

Now, I have a security config which looks like this:

@Configuration
@EnableWebMvcSecurity
@ComponentScan({
        "com.xxxxxcorp.xxxxxpoint.security",
        "com.xxxxxcorp.xxxxxpoint.service",
        "com.xxxxxcorp.xxxxxpoint.model.dao"})
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    DeviceUsernamePasswordAuthenticationProvider customAuthenticationProvider;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        System.out.println( "we are getting the custom config right?" );

        auth
                .authenticationProvider(customAuthenticationProvider);
    }

    @Configuration
    @Order(1)
    public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .antMatcher("/api/**")
                    .authorizeRequests()
                .anyRequest().hasRole("ADMIN")
                    .and()
                    .httpBasic();
        }
    }

    @Order(2)
    @Configuration
    public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .csrf().disable()
                .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                .loginPage("/login")
                    .failureUrl("/login?error=1")
                    .permitAll()
                    .and()
                .logout()
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/");
        }
    } 
}

and finally, the test context (note the springSecurityFilterChain autowiring)

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestApplicationConfig.class,TestPersistenceConfig.class,MvcConfig.class,SecurityConfig.class},loader=AnnotationConfigWebContextLoader.class)
@WebAppConfiguration
@Transactional
public class ApplicationIntegrationTest {

    MockMvc mockMvc;

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    private UserDao userDao;

    @Autowired
    private ClientDao clientDao;

    @Autowired
    private RoleDao roleDao;


    UUID key = UUID.fromString("f3512d26-72f6-4290-9265-63ad69eccc13");


    @Before
    public void setup() {

        mockMvc = MockMvcBuilders.webAppContextSetup(wac).addFilter(springSecurityFilterChain).build();


        List<Client> clients = new ArrayList<Client>();

        List<Role> roles = new ArrayList<Role>();
        Role roleUser = new Role();
        roleUser.setRole("user");
        Role roleUserDomain = roleDao.save(roleUser);
        roles.add(roleUserDomain);

        Role roleAdmin = new Role();
        roleAdmin.setRole("admin");
        Role roleAdminDomain = roleDao.save(roleAdmin);
        roles.add(roleAdminDomain);

        Client clientEN = new Client();
        clientEN.setDeviceId("444444444");
        clientEN.setLanguage("en-EN");
        clientEN.setAgentId("444444444|68:5b:35:8a:7c:d0");
        clientEN.setRoles(roles);
        Client clientENDomain = clientDao.save(clientEN);
        clients.add(clientENDomain);

        User user = new User();
        user.setLogin("user");
        user.setPassword("password");
        user.setClients(clients);
        user.setRoles(roles);

        userDao.save(user);

    }

    @Test
    public void thatViewBootstrapUsesHttpNotFound() throws Exception {

        MvcResult result = mockMvc.perform(post("/login")
                .param("username", "user").param("password", "password")
                .header("X-AUTH-TOKEN","NDQ0NDQ0NDQ0fDY4OjViOjM1OjhhOjdjOmQw")).andReturn();
        Cookie c = result.getResponse().getCookie("my-cookie");

        Cookie[] cookies = result.getResponse().getCookies();
        for (int i = 0; i < cookies.length; i++) {
            System.out.println("cookie " + i + " name: " + cookies[i].getName());
            System.out.println("cookie " + i + " value: " + cookies[i].getValue());
        }
        //assertThat(c.getValue().length(), greaterThan(10));

        // No cookie; 401 Unauthorized
        mockMvc.perform(get("/")).andExpect(status().isUnauthorized());

        // With cookie; 200 OK
        mockMvc.perform(get("/").cookie(c)).andExpect(status().isOk());

        // Logout, and ensure we're told to wipe the cookie
        result = mockMvc.perform(delete("/session")).andReturn();
        c = result.getResponse().getCookie("my-cookie");
        assertThat(c.getValue().length(), is(0));
    }

}

What's basically happening is that the login request is being intercepted by the normal UsernamePasswordAuthenticationFilter and not my custom authentication. I would have thought that the SecurityConfig would have ensured that the right substitutions were made, but it seems that the usage of:

@Autowired
private FilterChainProxy springSecurityFilterChain;

overrides that? does anyone know why?

Upvotes: 2

Views: 9030

Answers (2)

Michael Coxon
Michael Coxon

Reputation: 3535

Ultimately, it turns out that if you are overriding the UsernamePasswordAuthenticationFilter, Provider and Token, you need to go back to XML configuration. It seems that Spring Security does not properly publish the overridden vanilla spring SecurityFilterChain as a bean, so this is what you get the vanilla version back no matter what you try to configure.

Perhaps when Spring Security goes to version 4.0.0, we'll be able to do this by Java Configuration.

Upvotes: 1

Dave Syer
Dave Syer

Reputation: 58094

You have 3 filter chains (as far as we can tell). One default one (the outer WebConfigurerAdapter), and 2 custom ones (one has explicit httpBasic() and the other has formLogin()). The default one has order=0 I think, and protects everything, so that's the one that you will meet if you send a request into the whole filter. So that's probably a problem. And none of them (as far as I can see) installs your device details filter, so the authentication provider you added will never be used. That's another problem.

Upvotes: 1

Related Questions