Sylvain Lemoine
Another dev blog in the nexus…

Migrate to Spring Security

· by Sylvain Lemoine · Read in about 11 min · (2277 Words)
Java Spring Framework Spring Security

Quite some time has passed since my latest blog post… I keep learning cool stuff like haskell and japanese. Still enjoying learning Java too :). Beyond that, I’m refactoring an application at work. A matter of splitting a monolith in smaller running pieces and several side-kick libraries. I could have called it building a micro-services architecture in order to sell it to the higher-ups or to make my linkedin profile shines bright like diamond, but let’s start whispering from now: there’s an underground of living on the dark side of the moon systems between monolith and micro-services architecture.

But enough digressing now…

A good part of my (more or less interesting) job is migrating a bunch of xml, apache cxf, tests and plain java code to a unified up-to-date springframework - spring boot code base.

When it comes to http security, I chose for the sake of uniformity and simplicity to migrate the plain java authentication mechanism to Spring Security. A cool stuff to blog about, as setting up Spring Security seems somewhat intimidating around me. Oh and it gave me the taste for blogging again which was not a small issue…

The topic.


Nothing exotic about it, the authentication mechanism has the following specs:

It’s a custom http header, token based and stateless authentication scheme. An authentication token (not JWT) is passed through a specific header (labeled Auth-Token) on each request. The token existence and validity is asserted through database and the Spring Security context is set up accordingly. Besides, there is other authentication possibility in the app, so failing with this kind of authentication is an option and does not mean breaking the whole security filter chain.

Once upon a time a filter…


A good Spring Security tale always starts with a filter for the incoming requests. So I thought that looking at the build-in basic authentication filter could be a good starting point. I picked the things I need from here and dropped the others. I comment in some of my thoughts like this: «SLE: blablabla» in the following source code:

//SLE: I'm okay with the contract of OncePerRequestFilter.
public class BasicAuthenticationFilter extends OncePerRequestFilter {

	/**
	 * SLE: The following comment is exactly what I need.
	 *
	 * Creates an instance which will authenticate against the supplied
	 * {@code AuthenticationManager} and which will ignore failed authentication attempts,
	 * allowing the request to proceed down the filter chain.
	 *
	 * @param authenticationManager the bean to submit authentication requests to
	 */
	public BasicAuthenticationFilter(AuthenticationManager authenticationManager) {
		Assert.notNull(authenticationManager, "authenticationManager cannot be null");
		this.authenticationManager = authenticationManager;
		this.ignoreFailure = true;
	}

	/**
	 * SLE: NO NEED 
	 *
	 * Creates an instance which will authenticate against the supplied
	 * {@code AuthenticationManager} and use the supplied {@code AuthenticationEntryPoint}
	 * to handle authentication failures.
	 *
	 * @param authenticationManager the bean to submit authentication requests to
	 * @param authenticationEntryPoint will be invoked when authentication fails.
	 * Typically an instance of {@link BasicAuthenticationEntryPoint}.
	 */
	public BasicAuthenticationFilter(AuthenticationManager authenticationManager,
			AuthenticationEntryPoint authenticationEntryPoint) {
		Assert.notNull(authenticationManager, "authenticationManager cannot be null");
		Assert.notNull(authenticationEntryPoint,
				"authenticationEntryPoint cannot be null");
		this.authenticationManager = authenticationManager;
		this.authenticationEntryPoint = authenticationEntryPoint;
	}

	// ~ Methods
	//
	//
	//SLE: NO NEED
	@Override
	public void afterPropertiesSet() {
		Assert.notNull(this.authenticationManager,
				"An AuthenticationManager is required");

		if (!isIgnoreFailure()) {
			Assert.notNull(this.authenticationEntryPoint,
					"An AuthenticationEntryPoint is required");
		}
	}

	//SLE: I should rewrite the method body to suit my needs. 
	@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain)
					throws IOException, ServletException {
		final boolean debug = this.logger.isDebugEnabled();

	    //SLE: Should reuse this for my own header
		String header = request.getHeader("Authorization");

		if (header == null || !header.toLowerCase().startsWith("basic ")) {
			chain.doFilter(request, response);
			return;
		}

		try {
			//SLE: Nothing to extract and decode, it's just a token.
			String[] tokens = extractAndDecodeHeader(header, request);
			assert tokens.length == 2;

			String username = tokens[0];

			if (debug) {
				this.logger
						.debug("Basic Authentication Authorization header found for user '"
								+ username + "'");
			}

	        //SLE: My auth is always required.
			if (authenticationIsRequired(username)) {
				UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
						username, tokens[1]);
				authRequest.setDetails(
						this.authenticationDetailsSource.buildDetails(request));
				Authentication authResult = this.authenticationManager
						.authenticate(authRequest);

				if (debug) {
					this.logger.debug("Authentication success: " + authResult);
				}

				SecurityContextHolder.getContext().setAuthentication(authResult);

                //SLE: My auth is stateless so i'll skip this.
				this.rememberMeServices.loginSuccess(request, response, authResult);

				onSuccessfulAuthentication(request, response, authResult);
			}

		}
		catch (AuthenticationException failed) {
			SecurityContextHolder.clearContext();

			if (debug) {
				this.logger.debug("Authentication request for failed: " + failed);
			}

			this.rememberMeServices.loginFail(request, response);

			onUnsuccessfulAuthentication(request, response, failed);

	        //SLE: Always ignore failure in my case
			if (this.ignoreFailure) {
				chain.doFilter(request, response);
			}
			else {
				this.authenticationEntryPoint.commence(request, response, failed);
			}

			return;
		}

		chain.doFilter(request, response);
	}

	/**
	 * Decodes the header into a username and password.
	 *
	 * @throws BadCredentialsException if the Basic header is not present or is not valid
	 * Base64
	 */
	private String[] extractAndDecodeHeader(String header, HttpServletRequest request)
			throws IOException {
		//...
	}

	//SLE: ALWAYS REQUIRED FOR ME
	private boolean authenticationIsRequired(String username) {
		//...
	}

	protected void onSuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, Authentication authResult) throws IOException {
	}

	protected void onUnsuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException failed)
					throws IOException {
	}

   //SLE: Skip the remaining part...
}

The filter


Here is our filter class:

public class AuthTokenFilter extends OncePerRequestFilter {

    private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();

    private final AuthenticationManager authenticationManager;

    /**
     * {@inheritDoc}
     */
    public AuthTokenFilter(AuthenticationManager authenticationManager) {
        Assert.notNull(authenticationManager, "authenticationManager cannot be null");
        this.authenticationManager = authenticationManager;
    }

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        final boolean debug = this.logger.isDebugEnabled();

        String header = request.getHeader("Auth-Token");

        if (header == null) {
            chain.doFilter(request, response);
            return;
        }

        try {
            if (debug) {
                this.logger
                        .debug("Authentication-Token header found for '" + request.getRemoteUser() + "'");
            }

            AuthTokenAuthenticationToken authRequest = new AuthTokenAuthenticationToken(
                    request.getRemoteUser(), header);

            authRequest.setDetails(
                    this.authenticationDetailsSource.buildDetails(request));

            Authentication authResult = this.authenticationManager
                    .authenticate(authRequest);

            if (debug) {
                this.logger.debug("Authentication success: " + authResult);
            }

            SecurityContextHolder.getContext().setAuthentication(authResult);

            onSuccessfulAuthentication(request, response, authResult);

        } catch (AuthenticationException failed) {
            SecurityContextHolder.clearContext();

            if (debug) {
                this.logger.debug("Authentication request for failed: " + failed);
            }

            onUnsuccessfulAuthentication(request, response, failed);

            chain.doFilter(request, response);

            return;
        }

        chain.doFilter(request, response);
    }

    protected void onSuccessfulAuthentication(HttpServletRequest request,
                                              HttpServletResponse response, Authentication authResult) throws IOException {
    }

    protected void onUnsuccessfulAuthentication(HttpServletRequest request,
                                                HttpServletResponse response, AuthenticationException failed)
            throws IOException {
    }


}

We kept the constructor with sole parameter AuthenticationManager. Nothing peculiar here. We should pursue de filter chain even if authentication failed.

public AuthTokenFilter(AuthenticationManager authenticationManager) {
    Assert.notNull(authenticationManager, "authenticationManager cannot be null");
    this.authenticationManager = authenticationManager;
}

The doFilterInternal is much more interesting:

 String header = request.getHeader("Auth-Token");

    if (header == null) {
        chain.doFilter(request, response);
        return;
    }

We extract the Auth-Token header from the request. If it’s not present, we pursue the filter chain. If it is, we store the token in the header variable. I remove the extractAndDecode stuff form BasicAuthenticationFilter as it is not relevant in our case.

AuthTokenAuthenticationToken authRequest = new AuthTokenAuthenticationToken(request.getRemoteUser(), header);

The authentication instance


Here comes the second class we have to create: AuthTokenAuthenticationToken. The class implements the Authentication interface (by extending the abstract class AbstractAuthenticationToken).

public class AuthorizationTokenAuthenticationToken extends AbstractAuthenticationToken {


    private final Object principal;

    private Object credentials;

    public AuthorizationTokenAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
    }

    public AuthorizationTokenAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(true);
    }

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

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        AuthorizationTokenAuthenticationToken that = (AuthorizationTokenAuthenticationToken) o;
        return Objects.equals(principal, that.principal) &&
                Objects.equals(credentials, that.credentials);
    }

    @Override
    public int hashCode() {
        return Objects.hash(principal, credentials);
    }
}

The above class will retain some informations: the principal,the credentials, whether the principal is autenthicated or not, and the authorities (principal roles). See principal definition. In our case I put the request remoteUser attribute in principal and the extracted token in the credentials. Please pay attention to the two different constructors. The first one is used in the filter class and doesn’t set the authenticated value (false by default). The second one will be useful in the AuthenticationProvider class when we are certain that the user is fully authenticated.

Let’s go back to the filter class,

Authentication authResult = this.authenticationManager
                    .authenticate(authRequest);

We call the authenticationManager with the AuthorizationToken instance we just created. The remaining code flow depends on the this.authenticationManager.authenticate(authRequest); result. If the call throws an AuthenticationException, the code will enter the following catch block:

 } catch (AuthenticationException failed) {
            SecurityContextHolder.clearContext();

            if (debug) {
                this.logger.debug("Authentication request for failed: " + failed);
            }

            onUnsuccessfulAuthentication(request, response, failed);

            chain.doFilter(request, response);

            return;
        }

The above will clear the security context (i.e. the ThreadLocal stored info of the authenticated principal), call some handlers and pursue the chain of filters.

Otherwise, if the authenticationManager.authenticate succeeds then

SecurityContextHolder.getContext().setAuthentication(authResult);

onSuccessfulAuthentication(request, response, authResult);

The security context will be set up and the successful handler will run. Note that I have kept the onSuccessfulAuthentication and UnsucessfulAuthentication method empty just like in the Basic filtersource code. I guess it could be useful in case of adding some extra operations by extending the filter.

The provider


By the way, let’s dig into the this.authenticationManager.authenticate(authRequest) which is in charge of… authenticating the principal! The authentication manager delegates to one or more registred AuthenticationProvider. Each AuthenticaionProvider supports one kind of Authentication instance (remember that the AuthTokenAuthentication class implements the Authentication interface). Let’s provide our provider which supports our Authentication instance.

 
public class AuthTokenAuthenticationProvider implements AuthenticationProvider {

    public final AuthTokenService authTokenService;

    public AuthTokenAuthenticationProvider(AuthTokenService authTokenService) {
        this.authTokenService = authTokenService;
    }

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

        Assert.notNull(authentication, "authentication is mandatory.");

        AuthTokenAuthenticationToken tokenAuthentication = (AuthTokenAuthenticationToken) authentication;

        Device device = authTokenService.authenticate((String) tokenAuthentication.getCredentials())
                .orElseThrow(() -> new BadCredentialsException("Failed authentication !"));

        return new AuthTokenAuthenticationToken(authentication.getPrincipal(),
                null,
                device.getAuthorities());

    }

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

The provider implements the AuthenticationProvider contract. We specify that the provider supports only the AuthTokenAuthenticationToken.class instance, so the provider cannot be used by the AuthenticationManager for any other kind of Authentication instance.

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

For authentication itself I usually delegates it to some dedicated service class through constructor dependency injection.

    public final AuthTokenService authTokenService;

    public AuthTokenAuthenticationProvider(AuthTokenService authTokenService) {
        this.authTokenService = authTokenService;
    }
	@Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        Assert.notNull(authentication, "authentication.is.missing");

        AuthTokenAuthenticationToken tokenAuthentication = (AuthTokenAuthenticationToken) authentication;

        Device device = authTokenService.authenticate((String) tokenAuthentication.getCredentials())
                .orElseThrow(() -> new BadCredentialsException("Failed authentication !"));

        return new AuthTokenAuthenticationToken(authentication.getPrincipal(),
                null,
                device.getAuthorities());
    }

The service authenticates the user against the database and custom application logic. It returns an Optional instance. If empty, which means that the authentication does not succeed in the service layer, we throw an AuthenticationException subclass: BadCredentialsException. If the authentication succeeds, this time, we use the second AuthTokenAuthenticationToken contructor.

    public AuthorizationTokenAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(true);
    }

The one which indicates that this time the user is actually authenticated (setAuthenticated(true)) and add the fully loaded principal authorities.

Please not that, this time I erased the credentials info (null value) as it will no longer be necessary to use them.

I provide beyond what could be the AuthTokenService contract and what could be the principal. In our case I chose to authenticate remote devices identified by an hashed token in database. The token should not be present (in real life scenario) in the POJO thus preventing any leaks in the application.

public interface AuthTokenService {

    /**
     * Try to authenticate a device by its raw authentication token.
     * @param raw raw authentication token.
     * @return An {@link Optional} of {@link Device}. If empty it means that the device cannot be retrieve from the token.
     */
    Optional<Device> authenticate(String raw);
}
public class Device {

    private String deviceId;

    private List<GrantedAuthority> authorities;

    public String getDeviceId() {
        return deviceId;
    }

   
    public Device(String deviceId, List<GrantedAuthority> authorities) {
        this.deviceId = deviceId;
        this.authorities = authorities;
    }

    public Device setDeviceId(String deviceId) {
        this.deviceId = deviceId;
        return this;
    }

    public List<GrantedAuthority> getAuthorities() {
        return authorities;
    }

    public Device setAuthorities(List<GrantedAuthority> authorities) {
        this.authorities = authorities;
        return this;
    }
}

The security config


We almost have everything set up but we still need to tell Spring Security how to use our different classes.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    public final AuthorizationTokenService authorizationTokenService;

    public SecurityConfig(AuthorizationTokenService authorizationTokenService) {
        this.authorizationTokenService = authorizationTokenService;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .anyRequest().authenticated();

        http.addFilterBefore(new AuthorizationTokenFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
    }


    @Bean
    public AuthorizationTokenAuthenticationProvider provider() {
        return new AuthorizationTokenAuthenticationProvider(authorizationTokenService);
    }
}

I supply the AuthenticationProvider as a bean and position the filter in the Security Filter chain. Because it’s a stateless authentication and a backend REST API I set sessionManagement to stateless and disable csrf. Eventually, I protect every requests behind authentication.

Testing


I usually rely on unit testing for filter, provider and authentication service classes.

Besides, Spring-Test dependency provides some handy mock such like MockHttpRequest, MockHttpResponse and MockFilterChain.

Sample Unit test for AuthenticationProvider:

@RunWith(JUnit4.class)
public class AuthTokenHasherAuthenticationProviderTest {

    private static final String RAW = "a13451e0-aac3-4d1f-9ecf-4430816e108a";

    @Test
    public void authenticate() {

        AuthTokenService authTokenService = Mockito.mock(AuthTokenService.class);
        AuthTokenAuthenticationProvider authorizationTokenAuthenticationProvider =
                new AuthTokenAuthenticationProvider(authTokenService);

        AuthTokenAuthenticationToken authenticationToken = new AuthTokenAuthenticationToken("deviceIP", RAW);

        List<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("ROLE_SUPER_DEVICE"));

        Mockito.when(authTokenService.authenticate(RAW)).thenReturn(Optional.of(new Device("device0001",
                authorities)));
        Authentication authenticate = authorizationTokenAuthenticationProvider.authenticate(authenticationToken);
        Assert.assertTrue(authenticate.isAuthenticated());
        Assert.assertEquals(authenticate.getAuthorities(), authorities);
    }

    @Test(expected = BadCredentialsException.class)
    public void authenticateBadCredentials() {

        AuthTokenService authorizationTokenService = Mockito.mock(AuthTokenService.class);
        AuthTokenAuthenticationProvider authorizationTokenAuthenticationProvider =
                new AuthTokenAuthenticationProvider(authorizationTokenService);
        AuthTokenAuthenticationToken authenticationToken = new AuthTokenAuthenticationToken("deviceIP", RAW);
        Mockito.when(authorizationTokenService.authenticate(RAW)).thenReturn(Optional.ofNullable(null));
        authorizationTokenAuthenticationProvider.authenticate(authenticationToken);
    }
}

I also add one @SpringBootTest annotated integration test. I used a Spring Security enabled MockMvc instance (By default, Spring security is disabled for MockMvc).

Sample integration test:

@RunWith(SpringRunner.class)
@SpringBootTest
public class SecurityIT {

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @Before
    public void before() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .apply(SecurityMockMvcConfigurers.springSecurity())
                .build();
    }

    @Test
    public void isAuthenticated() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/")
                .header("Auth-Token", "abcd-efgh"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }

    @Test
    public void accessDeniedWrongToken() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/")
                .header("Auth-Token", "xxxxx-xxxxx-xxxx-xxxx-xxxxxxxx"))
                .andExpect(MockMvcResultMatchers.status().isForbidden());
    }

    @Test
    public void accessDeniedNoToken() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/"))
                .andExpect(MockMvcResultMatchers.status().isForbidden());
    }

}

This kind of test relies on the following dependencies:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>5.1.3.RELEASE</version>
    <scope>test</scope>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

As always


You can find a complete sample app on my github: https://github.com/slem1/spring-security-custom-auth

Thanks for reading !

Comments