A Trip Through Spring Security

What can Spring do to help me secure my application?

James Collerton
The Startup
8 min readFeb 28, 2021

--

Adding a security layer to Spring projects

Audience

This article is aimed at developers with a solid understanding of the basics of Spring and web security. It marries up the two concepts, concluding with a worked example of using Spring Security with Spring Boot.

Argument

Spring Security is centred round two core concepts:

  1. Authentication: Verifying you are who you say you are.
  2. Authorisation: Once we know who you are, what are you allowed to do?

Authentication

Authentication is handled by the AuthenticationManager interface:

public interface AuthenticationManager {  Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}

This can be implemented differently depending on how we intend to authenticate. In our later example we will be authenticating using API Keys, but we may well use a database of user credentials.

From the method signature we can see that the authenticate method has three behaviours:

  1. Return an Authentication , usually using a true authenticated flag if it the provided input represents a valid Principal. A principal essentially refers to the current client.
  2. Throw a runtime AuthenticationException. Note, we will need to implement a way of dealing with this, but without an explicit try/ catch block.
  3. Return null if no decision is made.

We have our authentication interface, but what about the implementation? The most common solution is a ProviderManager, which delegates to a chain of AuthenticationProviders. We can see how this would be useful in the case where we had multiple different methods of authenticating.

The AuthenticationProvider has a method boolean supports(Class<?> authentication), which is used to check if we support the current authentication instance type.

public interface AuthenticationProvider {  Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}

To clarify, let’s think about a customAuthenticationProvider that uses a username and password to check if a user should be allowed into our application:

@Component 
public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider {
// Method for authentication
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
if (shouldAuthenticateUsernamePassword()) {
// Do authentication work

return new UsernamePasswordAuthenticationToken(
username,
password,
new ArrayList<>()
);
} else {
return null;
}}
// Checking the type of authentication is appropriate
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}

The use of tokens will be covered in more detail in the example, so don’t worry about it too much for the time being. As long as you get the gist it’s OK!

A ProviderManager has an optional parent, which it can fall back to if all providers return null. Let’s use another example to clarify.

Say we have an application, where by default we would like all of our endpoints secured using a username and password. However, for some endpoints we are happy just using an API key, and for others we want to use a completely different authentication again!

An example of provider managers and authentication providers

The top ProviderManager is our global default, and this insists we use the username and password authentication (amongst others). On the lower left we see a ProviderManager that uses an API Key. However, if none of the AuthenticationProvider s in the chain offers a valid authentication, it will fall back to the parent and use the username and password!

To ease the process of building AuthenticationManager s Spring provides some helper functionality, including the AuthenticationManagerBuilder.

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
@Autowired
public void initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
... configure builder here
}

Don’t worry too much about the Configuration this sits in, again we will expand on it in the example.

An important point to note is how we configure a global default authentication manager (like the one with username and password authentication) vs. how we configure any others.

By using the Autowired annotation we are creating a Spring bean, which will act as the global default. If instead we used Override we would create another AuthenticationManager . Spring Boot provides a secure, single user, default global AuthenticationManager unless you provide your own.

Authorisation

We have now covered how Spring Security authenticates a user. So once we know they are who they say they are, how do we decide what they’re allowed to do?

This uses theAccessDecisionManager, which delegate to a chain of AccessDecisionVoters, which look at an Authentication as passed from the authentication layer and decides if a user (principal) can access a resource (with a web resource or a Java class method being the two most common).

An Access Decision Manager calling each of the Access Decision Voters to determine Authorisation

The resource is represented by a secure Object, which has been decorated with ConfigAttributes.

A ConfigAttribute is an interface, with a single method returning a string. A typical ConfigAttribute is the name of a user role, often having given formats (like the ROLE_ prefix) or representing Spring Expression Language expressions that need to be evaluated.

Let’s look at an example class:

public class UserVoter implements AccessDecisionVoter {  @Override
public int vote(
Authentication authentication,
Object object,
Collection collection) {
return authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.filter(r -> "ROLE_USER".equals(r))
.findAny()
.map(s -> ACCESS_GRANTED)
.orElseGet(() -> ACCESS_DENIED);
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class clazz) {
return true;
}
}

In the above example we have three methods:

  1. Vote: This accepts an Authentication, an Object we are looking to check access for, and a list of ConfigAttributes. It can return three values defined statically by Spring, ACCESS_GRANTED, ACCESS_DENIED, and ACCESS_ABSTAIN. The final value is used when we don’t want to make a authorisation decision.
  2. Supports ConfigAttribute: This is used to see if the particular voter can be used in conjunction with a particular ConfigAttribute.
  3. Supports Class: The same as above, but with a given class, representing the class of the argument for the vote method.

We can see that we are granting access to any user. Both our supports methods are returning true , as we essentially ignore the incoming Object and ConfigAttributes.

In the final part of this section we will look at how Spring implements Web Security.

When a request from a client comes into an application it must first go through a number of filters before it reaches the servlet. Spring Security has a concrete type of FilterChainProxy, which can replace a single filter in the chain, as demonstrated below.

How filters interact with a client request, including Spring Security

This diagram shows a single filter chain in Spring Security, however there can be multiple. In practice this means that we match a request format to a filter chain, and so can use separate security measures for separate requests.

Examples of routing to different filter chains depending on requests

In the above we see we can route to different filter chains depending if the request goes to path /a/** , /b/** and /**. The final chain is the default, and catches all requests that have not been matched otherwise.

An Example

The full code can be found in the repository here. We first use the Spring Initializr to generate a project with the required dependencies:

Generating a Spring project for use with Spring Security

From here we import the project into IntelliJ and set up the below:

An example class structure

Some of these classes should look familiar:

  1. SecurityConfig: This contains the Spring configuration to enable and set up security.
  2. ApiKeyAuthenticationFilter: This is the filter we will put in the filter chain.
  3. ApiKeyAuthenticationToken: This is the authentication token we will be passing, representing the access of the client.
  4. ApiKeyAuthenticationProvider: This is the provider we will be using to authenticate the endpoint.
  5. Order: This is the object we will be returning from the controller.
  6. OrderController: This is the endpoint we will secure using an API key.

Now let’s deep dive into each class.

SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired private ApiKeyAuthenticationProvider apiKeyAuthenticationProvider;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.addFilterBefore(
new ApiKeyAuthenticationFilter(authenticationManager()),
AnonymousAuthenticationFilter.class);
}

@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Collections.singletonList(apiKeyAuthenticationProvider));
}
}

The first thing to notice is the configure method. This is used to set up our Spring Security layer to make sure all requests are authenticated. You can see we’re adding our API Key authentication filter into the filter chain at a certain point.

Additionally, we’re setting up our authentication manager with a single provider, the API Key Authentication provider. So far this is all looking quite familiar!

ApiKeyAuthenticationFilter

public class ApiKeyAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

public ApiKeyAuthenticationFilter(AuthenticationManager authenticationManager) {
super("/**");
this.setAuthenticationManager(authenticationManager);
}

@Override
public Authentication attemptAuthentication(
HttpServletRequest request, HttpServletResponse response) {

Optional<String> apiKeyOptional = Optional.ofNullable(request.getHeader("x-api-key"));

ApiKeyAuthenticationToken token =
apiKeyOptional.map(ApiKeyAuthenticationToken::new).orElse(new ApiKeyAuthenticationToken());

return getAuthenticationManager().authenticate(token);
}

@Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult)
throws IOException, ServletException {

SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}

This is the filter we add to the chain. Notice how we create the object using the authentication manager, then later retrieve the API Key and use the authentication manager to authenticate the request.

ApiKeyAuthenticationToken

@Transient
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {

private String apiKey;

public ApiKeyAuthenticationToken(String apiKey, boolean authenticated) {
super(AuthorityUtils.NO_AUTHORITIES);
this.apiKey = apiKey;
setAuthenticated(authenticated);
}

public ApiKeyAuthenticationToken(String apiKey) {
super(AuthorityUtils.NO_AUTHORITIES);
this.apiKey = apiKey;
setAuthenticated(false);
}

public ApiKeyAuthenticationToken() {
super(AuthorityUtils.NO_AUTHORITIES);
setAuthenticated(false);
}

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

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

This is the token used across requests. Points to note:

  • In the constructor we are passing AuthorityUtils.NO_AUTHORITIES . This is referring to Granted Authorities, which are essentially permissions. We can use this to pass rights around with a token once a client has been authenticated. In our case we have no need to do this.
  • The principal is the identity of the principal being authenticated. In our case it is the API Key.

ApiKeyAuthenticationProvider

@Component
public class ApiKeyAuthenticationProvider implements AuthenticationProvider {

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String apiKey = (String) authentication.getPrincipal();

if (ObjectUtils.isEmpty(apiKey)) {
throw new InsufficientAuthenticationException("No API key in request");
} else {
if ("ValidApiKey".equals(apiKey)) {
return new ApiKeyAuthenticationToken(apiKey, true);
}
throw new BadCredentialsException("API Key is invalid");
}
}

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

This is the authentication provider. We see here we authenticate the request using the API key. We also need to tell the authentication manager which authentications we support, hence the support method.

Order and OrderController

@Controller
public class OrderController {

@GetMapping("/order")
public ResponseEntity<Order> getOrder() {

Order order = Order.builder().name("Order One").description("Order One Description").build();

return ResponseEntity.ok(order);
}
}

This is a regular controller class, used only to demonstrate how they can be secured.

@Builder
@Data
public class Order {
private String name;
private String description;
}

Similarly, this small model class is returned in order to demonstrate a working endpoint.

In Postman we can see that by making a request without a valid x-api-key header, we receive a 401. With a correctly implemented API Key we can access our order!

A successfully authenticated request!

Conclusion

In conclusion, application security can be reduced to: authentication (are you who you say you are?) and authorization (what can you do?). Spring Security implements both of these within the Spring framework.

--

--

James Collerton
The Startup

Senior Software Engineer at Spotify, Ex-Principal Engineer at the BBC