springboot:spring_simple_ajax_authentication

This is an old revision of the document!


Spring Simple Ajax Authentication

Assume you have already read https://wiki.chongtin.com/springboot/spring_simple_username-password_authentication_with_h2. If not, do it because we need to use h2 database to store our users.

Unless you want to put the username, and password in your HTTP header every time, otherwise, Ajax authentication is usually use for the very beginning of the JWT authentication when the front-end use their login:password for exchanging the JWT access token, and refresh token.

In this example, we use Ajax Authentication for exchanging JWT token. The JWT part will be in the https://wiki.chongtin.com/springboot/spring_jwt_authentication.

  1. Create an AjaxAuthenticationFilter that extends UsernamePasswordAuthenticationFilter
  2. Create public class WebMvcConfig extends WebSecurityConfigurerAdapter {…} for configuration
  3. For default setting UsernamePasswordAuthenticationFilter take HTTP form input with username and password for the username, and password.
  4. Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {…} to do your stuff. In our case, generate some JWT tokens.

Here we create our own AuthenticationFilter that extends UsernamePasswordAuthenticationFilter. By default, UsernamePasswordAuthenticationFilter's attemptAuthentication method takes a HTTP form input with name username, and password for path /login, and try to use the supplied AuthenticationManager to authenticate it. So UsernamePasswordAuthenticationFilter has already done the heavy lifting for us, all we need to do is to @Override successfulAuthentication(…){…} method to do our stuff.

There is another possible way to do Ajax authentication with OncePerRequestFilter, but we are NOT going to talk about it here.

public class AjaxAuthFilter extends UsernamePasswordAuthenticationFilter {

    public AjaxAuthFilter(AuthenticationManager authenticationManager) {
        this.setAuthenticationManager(authenticationManager);
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        super.doFilter(req, res, chain);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        if (authResult.getPrincipal() instanceof UserDetails) {
            UserDetails user = (UserDetails) authResult.getPrincipal();
            String role = user.getAuthorities().iterator().next().getAuthority();
            //create the JWT access token, refresh token
            //push it to the response as token, token-refresh
            String token = JWT.create()
                    .withSubject(user.getUsername())
                    .withExpiresAt(new Date(System.currentTimeMillis() + SecurityConstants.ACCESS_TOKEN_EXPIRATION_TIME))
                    .withClaim(SecurityConstants.ROLE, role)
                    .sign(HMAC512(SecurityConstants.SECRET.getBytes()));

            String refreshToken = JWT.create()
                    .withSubject(user.getUsername())
                    .withExpiresAt(new Date(System.currentTimeMillis() + SecurityConstants.REFRESH_TOKEN_EXPIRATION_TIME))
                    .withClaim(SecurityConstants.ROLE, role)
                    .sign(HMAC512(SecurityConstants.SECRET.getBytes()));

            response.addHeader(SecurityConstants.ACCESS_HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token);
            response.addHeader(SecurityConstants.REFRESH_HEADER_STRING, SecurityConstants.TOKEN_PREFIX + refreshToken);
        } else {
            super.successfulAuthentication(request, response, chain, authResult);
        }
    }
}

Forget about the JWT stuff for now, the key point here are:

  1. auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
  2. .addFilterBefore(new AjaxAuthFilter(this.authenticationManager()), UsernamePasswordAuthenticationFilter.class)

The userDetailsService is used by our AjaxAuthFilter, so we need to register it. We have already talked UserDetailsServiceImpl in the other page (https://wiki.chongtin.com/springboot/spring_simple_username-password_authentication_with_h2), so we skip it here.

For Spring to know, and use our filter, we need to register it too. We supply the this.authenticationManager() since we need to set it for our filter that extends UsernamePasswordAuthenticationFilter, and we want our filter to be caller before the spring UsernamePasswordAuthenticationFilter (i.e the spring default login page).

@Configuration
public class WebMvcConfig extends WebSecurityConfigurerAdapter {

    private UserDetailsServiceImpl userDetailsService;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    @Autowired
    private JwtAuthenticationProvider jwtAuthenticationProvider;

    public WebMvcConfig(UserDetailsServiceImpl userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
        auth.authenticationProvider(jwtAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        List<String> jwtSkipPaths = new ArrayList<>();
        jwtSkipPaths.add("/login");
        jwtSkipPaths.add("/error");

        http.cors().and().csrf().disable().authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .addFilterBefore(new AjaxAuthFilter(this.authenticationManager()), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JwtAuthFilter(new SkipPathRequestMatcher(jwtSkipPaths, "/**"), this.authenticationManager()), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

Run the application, and use curl: (Remember to use HTTPS for production server)

curl --noproxy "*" -v  http://127.0.0.1:8080/login  -d "username=user&password=password"

Here is the result. We put the access token and refresh token in the response header. The front-end should take it and store in browser cookie, or local storage, and it should use a HttpInterceptor (for Angular for example) to put the token in the HTTP header in each of it message to the back-end server.

*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST /login HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.61.0
> Accept: */*
> Content-Length: 31
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 31 out of 31 bytes
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< token: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJST0xFIjoiVVNFUiIsInN1YiI6InVzZXIiLCJleHAiOjE1OTE4NDgzNDB9.wsM6psLzJZm5VbuiY6ynZMBNonWz99X-K8NEaBKQHYeVQv34Y7KUSLlIqWqzD3ZVbZbMlXCAntIuN7alwnCMSQ
< token-refresh: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJST0xFIjoiVVNFUiIsInN1YiI6InVzZXIiLCJleHAiOjE1OTE4NTQ2NDB9.epI8No4In3AkzVqsqoyE_GFcuMSNe9BE-OnKiI6FR82qUV5LEUv5jCmCpxgbqwXEXNF4D1rjsi25PgedO-OVGw
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Length: 0
< Date: Thu, ** *** **** **:**:** GMT
<
* Connection #0 to host 127.0.0.1 left intact
  • springboot/spring_simple_ajax_authentication.1591847978.txt.gz
  • Last modified: 2020/06/11 11:59
  • by chongtin