Wednesday, June 28, 2017

Spring Security Custom Authentication Filter

In this post I'd like to share how to write own custom Filter in Spring Security framework to authenticate. If you have your web application built using this security framework this post might be useful  for you.

Well it is very often needed allow user login into the application on the fly without any input forms. For example by link from the email or SMS, by some header in the REST API etc. I take the letter as an example wehere there this the link with parameter:

Hi, dear user!
There is a new comment on your page.
See all the activity in your profile.

And the activity link href is:
http://example.com/activity.htm?token=25390bd438e1f17bee7b0d320a62707f

And your system must be able to authenticate the user just having this token: 25390bd438e1f17bee7b0d320a62707f

First of all you need to know how to restore the user info by this token value and vice versa: you know how to generate this token matching the user info.
The simplest solution is to store them in database providing the expiration info and other checks if needed.

Now we need to write the Filter to find this token in request:

Filter:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;

import javax.servlet.http.HttpServletRequest;

public class TokenFilter extends AbstractPreAuthenticatedProcessingFilter {
  static final String TOKEN_PARAMETER = "token";

  @Override
  protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
    return request.getHeader(TOKEN_PARAMETER);
  }

  @Override
  protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
    return request.getHeader(TOKEN_PARAMETER);
  }
}

Now let's configure the framework: register the filter and code to authenticate.

In Spring Security there is the possibility to provide additional fallback code to provide the user authentication when primary one is failing using user details service. Sample config where we register our filter and authentication manager might look like:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;


import javax.annotation.Resource;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Resource
  private UserDetailsService userDetailsService;
  /*
  Fallback authentication mananger
  */
  @Resource
  private TokenAuthenticationManager tokenAuthenticationManager;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.parentAuthenticationManager(tokenAuthenticationManager);
    auth.userDetailsService(userDetailsService);
  }

  @Override
  public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/assets/**");
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    /*
    This our FILTER
    */
    TokenFilter filter = new TokenFilter();
    filter.setAuthenticationManager(authenticationManager());
    http.addFilterBefore(filter, AbstractPreAuthenticatedProcessingFilter.class);

    http.logout().logoutUrl("/auth/doLogout").logoutSuccessUrl("/auth/login.htm");
    http.rememberMe();
    http.authorizeRequests()
        .antMatchers("/reports/**/*").hasAnyAuthority(new String[]{"ADMIN", "USER"})
        .anyRequest().permitAll()
        .and()
        .formLogin()
        .usernameParameter("username")
        .passwordParameter("password")
        .loginPage("/pages/login.htm")
        .failureUrl("/pages/failure.htm")
        .defaultSuccessUrl("/index.htm")
        .loginProcessingUrl("/auth/doLogin")
        .permitAll();
  }
}

Now lets write this fallback authentication manager:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Objects;

@Component
public class TokenAuthenticationManager implements AuthenticationManager {
  @Resource
  private TokenService tokenService;

  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    if (!(authentication instanceof PreAuthenticatedAuthenticationToken)) {
      throw new BadCredentialsException("Not supported auth");
    }

    final Object principal = authentication.getPrincipal();
    final Object creds = authentication.getCredentials();

    checkFilterProcessed(principal, creds);

    try {
      Token token = tokenService.getByValue(principal.toString());

      if (/* check token is valid: exists, not expired, etc */ false) {
        throw new BadCredentialsException("Bad token");
      }

      User user = tokenService.restoreUserByToken(token); 
      // User implements org.springframework.security.core.userdetails.UserDetails

      UsernamePasswordAuthenticationToken result =
          new UsernamePasswordAuthenticationToken(user, creds, user.getAuthorities());
      result.setDetails(user);
      return result;
    } catch (TokenNotFoundException e) {
      throw new BadCredentialsException(e.getMessage());
    }
  }

  private void checkFilterProcessed(Object principal, Object creds) {
    if (!Objects.equals(principal, creds)) {
      throw new BadCredentialsException("Not supported auth");
    }
  }
}


That's it!
This filter will be invoked only once when user is not logged out.
If you need to logout already authenticated user in the system - you need to create new Filter checking this token and insert it after the standard logout filter same as I did for TokenFilter and logout existing. See all available in org.springframework.security.config.annotation.web.builders.FilterComparator#FilterComparator.

For the case when you have REST API with a header you can modify token filter to extract the token, f.e. get it from the header with Authentication name

No comments:

Post a Comment

Design Patterns Wrapper revisited

 Now lots of software designed and developed following microservice architecture. Components need to communicate with each other via VO (val...