springboot:adding_customized_pre-authentication

Adding Customized Pre-Authentication

Adding a customized pre-authentication in spring boot takes the following 4 steps:

  1. Create an authentication token that extends AbstractAuthenticationToken
  2. Create an authentication filter that extends AbstractAuthenticationProcessingFilter
  3. Create an authentication provider that implements AuthenticationProvider
  4. Customize the web security configuration by creating a class that extends WebSecurityConfigurerAdapter

Assume our site is http://127.0.0.1:8080. The login path is http://127.0.0.1:8080/login. To make is simple, we take a token from login path like http://127.0.0.1:8080/login?token=THE_TOKEN. If THE_TOKEN is worktimefun, we allow the user login with ROLE_USER, otherwise we redirect the user to a log in failed page.

Our token will store user credential and principal, and a collection of its GrantedAuthority after it is being authenticated.

public class MyToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    private String token;
    private String username;

    public MyToken(String sToken) {
        super(null);
        this.token = sToken;
        setAuthenticated(false);
    }

    public MyToken(String sToken, String sUsername, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.token = sToken;
        this.username = sUsername;
        super.setAuthenticated(true);
    }

    public String getToken() {
        return token != null ? token : "";
    }

    public void setToken(String token) {
        this.token = token;
    }

    public String getUsername() {
        return username;
    }

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

    @Override
    public Object getCredentials() {
        return getToken();
    }

    @Override
    public Object getPrincipal() {
        return getUsername();
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }
        super.setAuthenticated(false);
    }
}

This filter takes two parameters to be constructed. The first one is the login path pattern, and the second one is the authentication manager. We will provide both in step 4.

public class MyFilter extends AbstractAuthenticationProcessingFilter {

    public MyFilter(String pattern, AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher(pattern, "GET"));
        super.setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws
     AuthenticationException, IOException, ServletException {
        String sToken = request.getParameter("token");
        if (sToken == null) {
            sToken = "";
        }

        MyToken token = new MyToken(sToken);
        return this.getAuthenticationManager().authenticate(token);
    }

}

Here we do three things bad to make this example simpler. The first one is the set a hard-coded ROLE_USER on fly. The second one is set a hard-coded username on fly. The third one is to just compare a token with a hard-coded string. All of them are bad.

public class MyProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if (!this.supports(authentication.getClass())) {
            return null;
        } else {
            MyToken myAuthenticationToken = (MyToken) authentication;
            //get the token
            String sToken = myAuthenticationToken.getToken();

            boolean isLogin = false;
            if (sToken.equals("worktimefun")) {
                isLogin = true;
            }

            if (isLogin) {
                GrantedAuthority grantedAuthority = new GrantedAuthority() {
                    @Override
                    public String getAuthority() {
                        return "ROLE_USER";
                    }
                };
                Set<GrantedAuthority> grantedAuthoritySet = new HashSet<>();
                grantedAuthoritySet.add(grantedAuthority);
                MyToken authenticatedToken = new MyToken(sToken, "WorkTimeFunUsername", grantedAuthoritySet);
                return authenticatedToken;
            } else {
                throw new BadCredentialsException("Bad credentials");
            }
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return MyToken.class.isAssignableFrom(authentication);
    }
}

In this configuration, we provide a ProviderManager that has our own authentication provider MyProvider. We also need to add our own authentication filter MyFilter so that when user access the path /login, our filter's attemptAuthentication will be called by the doFilter called by the filter chain. Finally we need to set the authorize requests to tell which URL patterns need to be authenticated, and authorized to whom.

@Configuration
public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
    private AuthenticationManager authenticationManager;

    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        authenticationManager = new ProviderManager(Collections.singletonList(new MyProvider()));
        return authenticationManager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        final String loginPath = "/login";
        final String loginFailedPath = "/loginFailed";
        final String userHome = "/user";

        LogoutSuccessHandler logoutSuccessHandler = (request, response, authentication) -> {
            response.setHeader("Location", "/");
            response.setStatus(302);
        };

        AuthenticationFailureHandler authenticationFailureHandler = (request, response, exception) -> {
            response.setHeader("Location", loginFailedPath);
            response.setStatus(302);
        };

        AuthenticationSuccessHandler successHandler = (request, response, authentication) -> {
            response.setHeader("Location", userHome);
            response.setStatus(302);
        };

        MyFilter myFilter = new MyFilter(loginPath, authenticationManager);
        myFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
        myFilter.setAuthenticationSuccessHandler(successHandler);
        http.addFilterBefore(myFilter, LogoutFilter.class);

        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers(loginPath).permitAll()
                .antMatchers(loginFailedPath).permitAll()
                .antMatchers(userHome + "/**").access("hasRole('ROLE_USER')")
                .antMatchers("/admin").access("hasRole('ROLE_ADMIN')")
                .anyRequest().authenticated()
                .and()
                .logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler).permitAll();
    }
}

Note that we have redirect the user to /user or /loginFailed after they attempted to log in. You need to write your own controller to handle these. It could be easily done by this:

@Controller
public class WelcomeController {
    @ResponseBody
    @RequestMapping("/")
    public String welcome(Map<String, Object> model) {
        return "welcome";
    }

    @ResponseBody
    @RequestMapping(value = "/loginFailed", method = RequestMethod.GET)
    public String loginFailed() {
        return "Login failed!!!";
    }

   @ResponseBody
     @RequestMapping(value = "/user", method = RequestMethod.GET)
    public String userHome(HttpServletRequest request, Model model) {
        return "Login succeed.";
    }
}

After logged in, the authentication token can be obtains by calling:

Authentication auth = SecurityContextHolder.getContext().getAuthentication();

Depending on your authenticate chain, or your will get different type of authentication token that implements Authentication. In our case, it would be MyToken.

  • springboot/adding_customized_pre-authentication.txt
  • Last modified: 2018/10/10 10:16
  • by chongtin