A Trip Through Spring Security
What can Spring do to help me secure my application?
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:
- Authentication: Verifying you are who you say you are.
- 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:
- Return an
Authentication
, usually using a trueauthenticated
flag if it the provided input represents a valid Principal. A principal essentially refers to the current client. - Throw a runtime
AuthenticationException
. Note, we will need to implement a way of dealing with this, but without an explicittry/ catch
block. - 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 AuthenticationProvider
s. 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!
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 AccessDecisionVoter
s, 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).
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:
- Vote: This accepts an
Authentication
, anObject
we are looking to check access for, and a list ofConfigAttributes
. It can return three values defined statically by Spring,ACCESS_GRANTED
,ACCESS_DENIED
, andACCESS_ABSTAIN
. The final value is used when we don’t want to make a authorisation decision. - Supports
ConfigAttribute
: This is used to see if the particular voter can be used in conjunction with a particularConfigAttribute
. - Supports
Class
: The same as above, but with a given class, representing the class of the argument for thevote
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 ConfigAttribute
s.
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.
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.
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:
From here we import the project into IntelliJ and set up the below:
Some of these classes should look familiar:
- SecurityConfig: This contains the Spring configuration to enable and set up security.
- ApiKeyAuthenticationFilter: This is the filter we will put in the filter chain.
- ApiKeyAuthenticationToken: This is the authentication token we will be passing, representing the access of the client.
- ApiKeyAuthenticationProvider: This is the provider we will be using to authenticate the endpoint.
- Order: This is the object we will be returning from the controller.
- 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!
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.