Reputation: 3535
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
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
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