πŸ‡ͺπŸ‡ΈEnhancements of Authorization Lab

🎯 Objective

Learn about the latest innovations in Authorization features of Spring Security. Details on Spring Security Authorization can be found in the Spring Security Reference Documentation.

Authorization Layers

This implements a well-known security principle called Defense in depth


Step 1: Get to know the provided application

The provided application is a simple Spring Boot application that demonstrates the latest Authorization features of Spring Security. The use case is a simple online banking application with bank accounts owned by different users.

This is the BankAccount entity class that represents a bank account in the application (see Spring Data JPA Reference if you are not familiar with JPA and/or Spring Data JPA):

The application uses a simple in-memory database (H2) to store the bank accounts. The BankAccount entity class extends the AbstractPersistable class, which provides a simple way to manage the entity's ID and version.

πŸ’‘ if Spring Security 6.3 is used: As we will use Jackson (for JSON), we need to set the annotation @JsonSerialize(as = BankAccount.class) on top of the class. This is due to how Jackson works with CGLIB proxies (Spring Security needs proxies to make enhanced authorizations work). Otherwise, this may result in a serialization error like the following:com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle

The application provides a simple REST API that allows users to create, view and update their bank accounts.

Endpoint
Description

GET /api/accounts

Administrative Endpoint to get all existing accounts

GET /api/accounts/{id}

Retrieve a single account by its unique identifier

POST /api/accounts

Create a new account for currently authenticated user

POST /api/accounts/{id}

Increase the account balance by given amount

Here is the code for the REST API:

The Rest API delegates all calls to the BankAccountService class, which is responsible for the business logic and bridge to data access.

The BankAccountService class is annotated with @Transactional(readOnly = true) to indicate that all methods are read-only by default. The save and update methods are annotated with @Transactional to indicate that they are write operations.

Finally, we approach the data access layer. The BankAccountRepository class is a simple Spring Data JPA repository that provides CRUD operations for the BankAccount entity.

Please notice the updateBankAccount method. It contains a custom query that updates the balance of a bank account only if the owner of the account matches the currently authenticated user. This is basically done by using the ?#{principal?.username} expression in the query. This expression extension is enabled through the corresponding Spring Data Security extension defined in class WebSecurityConfiguration.

And it also requires an additional dependency in the pom.xml that is already included in the provided application:

Let's start the application with:

and test the API with the following credentials:

Username
Password
Role(s)

user

secret

USER

admin

secret

USER, ADMIN

accountant

secret

USER, ACCOUNTANT

To test the application, you can use one of the provided HTTP client files in the requests folder of the module:

  • api-call.http: The IntelliJ Http client

  • api-call.httpie: The command line Httpie client

  • api-call.curl: The command line Curl client

What do you think about the responses of the different API calls? It looks like there are issues with the authorization of the API calls. So there is work to do!

But first let's have a look at the (new and enhanced) method authorization features in Spring Security.


Step 2: Get to know the new and enhanced method Authorization Features

An application can use Spring Security to secure the REST API and restrict access to the method (service) layer. This is basically done by using annotations like @PreAuthorize, @PostAuthorize and @AuthorizeReturnObject on the service methods or domain objects.

Summary of Annotations

Annotation
When it runs
Use case

@PreAuthorize

Before method call

Guard method based on roles or parameters

@PostAuthorize

After method call

Guard method based on return value

@PreFilter

Before method call

Filter input collections

@PostFilter

After method call

Filter returned collections

@AuthorizeReturnObject

After return (6.3+)

Object-level security on returned object

@HandleAuthorizationDenied

Interception (6.3+)

Custom handling of authorization failures

Annotation Details

βœ… @PreAuthorize

Runs before the method and prevents execution if the expression is false.

🧠 Common use cases:

  • Role-based access

  • Ownership checks: @PreAuthorize("#userId == authentication.name")

βœ… @PostAuthorize

Runs after the method and prevents return if the expression is false. This allows checking the returned object.

🧠 Use this when you need to inspect the returned domain object to enforce access control.

βœ… @PreFilter

Filters collection input parameters before the method executes.

🧠 filterObject is a special variable that refers to each item in the collection.

βœ… @PostFilter

Filters the collection result after method execution.

🧠 This is useful when returning a list of domain objects, and you want to hide some from the caller.

βœ… @AuthorizeReturnObject (Spring Security 6.3+)

A new alternative to @PostAuthorize, more structured and composable

This can be combined with @PreAuthorize on parts of the domain object to enforce a bit more fine-grained object security.

βœ… @HandleAuthorizationDenied (Spring Security 6.3+)

This allows you to intercept and customize the behavior when an authorization check fails at a method level (e.g., in @PreAuthorize, @AuthorizeReturnObject, etc.).

🧠 This is useful when you want to mask or log the failure instead of throwing an exception.

In the next step we will now implement the new and enhanced method authorization features in the provided application.


Step 3: Add the new and enhanced Authorization Features to the application

Now it is the time to add the missing authorization to the application.

Spring Security Annotation Parameters

At first let's create some custom Authorization annotations to show the Annotations Parameters feature introduced in Spring Security 6.3. Later we will add these to the BankAccountService class.

To make method-level authorization generally work, we need to add the @EnableMethodSecurity annotation to the WebSecurityConfiguration class. If you want to use Annotation Parameters, you have to opt in Templating Meta-Annotation Expressions, a new feature introduced in Spring Security 6.3. For this we have to publish an AnnotationTemplateExpressionDefaults bean.

The PreGetBankAccounts and PreWriteBankAccount annotations are custom security annotations that are used to restrict access to the methods based on the user's role and ownership of the bank account. Please create these two classes in the security package.

PreGetBankAccounts:

PreWriteBankAccount:

Let us add these annotations to the BankAccountService class:

βœ… Explanation:

  • @PreGetBankAccounts(role = "ADMIN") is a custom-composed parameterized annotation built on top of @PreAuthorize

  • When findAll() is called, Spring Security will:

    1. Resolve the annotation parameter role = "ADMIN"

    2. Evaluate the SpEL expression hasRole() β†’ becomes hasRole('ADMIN')

    3. Check if the currently authenticated user has the authority ROLE_ADMIN

    4. If the check passes, proceed to call findAll()

    5. Otherwise, throw an AccessDeniedException

  • @PreWriteBankAccount is a custom-composed parameterized annotation built on top of @PreAuthorize

    1. Spring sees @PreWriteBankAccount("#toSave")

    2. The expression #toSave is passed to the account attribute of the annotation

    3. Inside the annotation, {account} is replaced with #toSave

    4. The resulting @PreAuthorize becomes:@PreAuthorize("#toSave?.owner == authentication?.name")

    5. At runtime, before executing save(), Spring evaluates whether: toSave.owner == currentlyAuthenticatedUser.name

Restart the application and test the API again (with the previously introduced credentials above)

Now most Authorizations should now work as expected.

Secure Return Values and Authorization Error Handling

Since Spring Security 6.3+ it's also supported to implement a handler to customize authorization errors. Let's see how this works by defining a new class called MaskMethodAuthorizationDeniedHandler, create this in the security package:

This class implements the MethodAuthorizationDeniedHandler interface and provides a custom implementation for handling authorization failures. In this case, it simply returns a masked value (*****). We will later add this to the BankAccount entity class.

Since Spring Security 6.3+ also supports wrapping any object that is annotated its method security annotations. The simplest way to achieve this is to mark any method that returns the object you wish to authorize with the @AuthorizeReturnObject annotation.

The @AuthorizeReturnObject annotation instructs Spring Security to check the returned object against the security expression. This is a new feature introduced in Spring Security 6.3 as well and allows you to restrict access to the returned domain object based on the user's role and the returned object's properties.

We want to try this now on our domain entity. Here is what the BankAccount entity class looks like after adding the @PreAuthorize and @HandleAuthorizationDenied annotation:

@HandleAuthorizationDenied now also references the previously created MaskMethodAuthorizationDeniedHandler class, which is a custom authorization-denied handler that is used to mask the account number if the user is not authorized to access it.

Finally, we need to activate the validation of authorization checks on our domain object. We achieve this by adding another customized annotation.

The PostReadBankAccount annotation is a custom security annotation that is used to restrict access to the method based on the user's role and the returned object. Create a new class called PostReadBankAccount in the security package:

We also need to add this annotation to the findById method in the BankAccountService class:

Step 4: Start the application and test the API

  1. Restart the application by running the BankAccountApplication class.

    • You can do this by right-clicking on the class and selecting Run or by using the command line:

  2. Re-test the API.

    • All authorizations should work now as expected.

    • If the user has restricted access rights, the account number is being masked out.

βœ… That's it! You have successfully implemented the really important authorization part in your Spring Boot application.

Let's now move on to the next section, where we will look into the OAuth2 Token Exchange.

Last updated