Let The Right One In…With Spring Security Annotations
On the Advertising Applications team here at GumGum, the bulk of the job consists of fleshing out features for a number of web-applications designed to help users with efficient advertising campaign setup, publisher on-boarding/setup, or provide easy-to-digest metrics to measure various KPIs. In the background, a SpringBoot REST API aids in storing, updating, and accessing MySQL and Snowflake databases that hold relative data for the many different companies that partner with GumGum. Ensuring the security of all that company data by securing the API is the main focus of this article, but since it’s close to Halloween, and bad analogies are fun, let’s talk about vampires for a minute.
Remember, with vampires, one is generally safe inside one’s home. Vampires can try the front door, a window, or maybe a side door from the garage, but as long as they aren’t invited inside, they’re just somebody else’s problem. More often than not, software engineers inevitably end up fighting their own “vampires”, people trying to gain access to parts of an application they have no business accessing. Hopefully the tricks that follow will help keep the vampires out on the porch where they belong.
The Setup
Straight out of the box, SpringBoot has a lot of built-in security features that users can utilize to secure their applications. From authentication to role-based authorization, there’s a good base to secure any application. A lot of these features can be further extended to satisfy myriad use-cases, but this article will primarily focus on endpoint authorization, with an emphasis on keeping a user’s eyes on their own data. This will be done by constructing custom Spring Expression Language (SpEL) expressions for use with Spring’s method security annotations, so click on the preceding links if a refresher is needed.
For the examples, the following schema will be assumed, where a user can belong to many companies (can’t forget the contractors), and a company can have many users:
Note that for this Spring project, the following Java and Kotlin classes will help with the example alongside CrudRepository interfaces for all three entities (repository classes omitted for brevity):
Notice that in the service class there is a glaring opportunity for some vampirism. The endpoint that calls this method only passes in a company id to the service’s getCompanyData
method. Assuming a user has a valid JWT and has authenticated themselves properly, nothing so far prevents them from looking up the API endpoint with their browser’s dev tools and accessing the endpoint with other company ids. For example, going to https://example-api.com/companies/{companyId}/data
can return company data to them with any companyId
they pass in. Currently, the API is saying “Yes, Dracula, right this way. After you.”
Shutting The Door
So, as it stands, the service layer is wide open. To close it, validation must occur that the current user has access to the company. Assuming that the email can be obtained from retrieving the current user’s details, Spring can retrieve their respective User
object and ensure that their list of companies includes the company being queried. In this case, it is assumed there is a SecurityUtils class that allows retrieval of the current user from the SecurityContext, casts to a concrete UserDetails
class, and retrieves the user’s email. So, knowing that access to the current user’s email is available, something like the following can be done:
The byCompanyId
method will return true if any of the User’s companies’ ids match the companyId
parameter. Otherwise, it will return false. Note that the @Component annotation contains a specified name for this class. This will be referenced in the next step where SpEL is used in conjunction with the @PreAuthorize annotation to add the authorization in the service layer, which looks like this:
Recall, SpEL allows us to access a class method with the @
operator and parameters via the #
operator, so what this is doing is telling the @PreAuthorize annotation to call the method created in CompanyUserAuthorization
, using the companyId
from CompanyDataServiceImpl.getCompanyData(Integer companyId)
method. Since this is a pre-authorization, this is actually performed prior to the annotated method being called. If the pre-authorization returns true, getCompanyData
will be called and company data will be returned to the user. If it returns false, the user will get a 403 and the method will not be called at all. In other words, there is no longer need to worry about the vampires.
Expanding on this
The vampires are outside the door now, but what happens when they try the window? The best part of how this @PreAuthorize annotation was constructed is that it is extremely reusable. Any method in the service layers that take in a companyId
parameter can have this annotation applied! The CompanyUserAuthorization
class could also be expanded to include methods that handle request objects, lists of company ids, etc. All that would need to be done is to construct their respective SpEL strings.
The main advantage to these annotations resides in their flexibility, as they can be as granular as needed. In this case, the authorizations were only checking against the User object, but it’s possible that the authorization could be much more complex. For instance, if only some internal users can access all company data, then one might need to also do role-based authorization in addition to the companyId
check for external users.
Wrapping Up
Securing an application’s endpoints can be accomplished in a number of ways, but as shown in this article, @PreAuthorize can be used in conjunction with re-usable custom methods and SpEL expressions to secure method calls at the service level. Similar methods can be constructed using a variety of parameter types or even alongside @PostAuthorize if the object being compared for authorization resides in the return object.
At GumGum, the Advertising Applications’ API is currently closed to our internal users only, but recent use-cases have arisen that will require us to be more and more external-user facing in the future. The team is planning on using Spring functionality like that in the preceding example (albeit a bit more complicated) to help ensure the vampires aren’t invited inside. When the applications are only letting the right ones in, software engineers can have a happy Halloween, assured that their API is a little less spooky.