HATEOAS + Spring Security: Why Some Links Are Missing for Certain Roles
ATEOAS (Hypermedia As The Engine Of Application State) is a core constraint of REST application architecture that makes REST APIs more self-descriptive. Combined with Spring Security, it allows us to dynamically shape API responses based on user roles. But developers often encounter a frustrating problem: some hypermedia links are missing depending on the authenticated user’s role.
In this article, we’ll explore why this happens, how to diagnose and fix it, and provide real-world examples to make the integration of HATEOAS and Spring Security smoother and more predictable.
🔍 Understanding HATEOAS in Spring Boot
Spring HATEOAS allows us to enrich REST API responses with navigable links. This makes it easier for clients to understand what actions are available without out-of-band information.
For example, a typical response might look like this:
{ "id": 123, "name": "John Doe", "_links": { "self": { "href": "/users/123" }, "delete": { "href": "/users/123/delete" } } }
But what if the “delete” link is missing for some users? That’s where Spring Security comes in.
🔐 Where Spring Security Comes In
Spring Security provides authentication and authorization. If a user doesn’t have the authority to perform a certain action (e.g., ROLE_ADMIN
to delete a user), the system should ideally not even expose that link.
This is a best practice in secure API design: don’t tell users about operations they’re not allowed to perform (OWASP Principle of Least Privilege).
🤔 Why Are Links Missing?
Here are the most common reasons HATEOAS links go missing:
1. Manual Link Filtering Based on Roles
Many developers write code like:
if (SecurityContextHolder.getContext().getAuthentication().getAuthorities().contains("ROLE_ADMIN")) { userModel.add(linkTo(methodOn(UserController.class).deleteUser(user.getId())).withRel("delete")); }
This means the delete
link will only be added if the user is an admin. That’s expected, but if you forget to include this conditional logic for other links, they might not appear.
2. Missing or Incorrect Role Checks
Sometimes developers forget to wrap the logic with proper role checks. For example:
if (user.isAdmin()) { // But what if user.isAdmin() doesn’t reflect Spring Security context correctly? }
Instead, use Spring Security’s @PreAuthorize
or SecurityContextHolder
.
3. Static Link Definitions
If your links are statically defined in a representation model and not dynamically built per request, they won’t reflect different user roles. For HATEOAS to work well with Spring Security, links should be built dynamically based on the current authentication context.
✅ A Better Approach: Role-Aware Link Building
Let’s write a proper example using Spring HATEOAS and Spring Security together.
📁 Domain Model
public class User { private Long id; private String name; private boolean isAdmin; }
🧠 Resource Assembler with Role Check
@Component public class UserModelAssembler implements RepresentationModelAssembler<User, EntityModel<User>> { @Override public EntityModel<User> toModel(User user) { EntityModel<User> model = EntityModel.of(user); model.add(linkTo(methodOn(UserController.class).getUser(user.getId())).withSelfRel()); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); boolean isAdmin = auth.getAuthorities().stream() .anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_ADMIN")); if (isAdmin) { model.add(linkTo(methodOn(UserController.class).deleteUser(user.getId())).withRel("delete")); } return model; } }
Now the links reflect the current user’s authorities, and only show what’s appropriate.
🧪 Testing with Mock Users
Spring Boot makes it easy to test this behavior using @WithMockUser
.
@Test @WithMockUser(username = "admin", roles = {"ADMIN"}) public void shouldContainDeleteLinkForAdmin() { // Perform a GET and check if "_links.delete" exists } @Test @WithMockUser(username = "user", roles = {"USER"}) public void shouldNotContainDeleteLinkForRegularUser() { // Perform a GET and assert "_links.delete" is absent }
This ensures your links adapt correctly and securely based on user roles.
💡 Pro Tip: Use AuthorizationManager
for Clean Separation
Spring Security 6 introduced AuthorizationManager
, which can help you externalize access logic. Instead of checking roles manually, define an authorization policy.
@Bean public AuthorizationManager<RequestAuthorizationContext> deleteUserAuth() { return (auth, context) -> { return auth.get().getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")) ? AuthorizationDecision.PERMIT : AuthorizationDecision.DENY; }; }
This makes your HATEOAS logic cleaner and reusable.
🔗 Real-World Practices and References
- Spring HATEOAS Documentation
- Spring Security Guide
- Baeldung: Securing REST APIs with Spring Security
- REST API Design Best Practices
🧵 Conclusion
When HATEOAS and Spring Security are used together, missing links aren’t bugs — they’re often intentional results of secure design. But developers must be explicit and aware of how links are conditionally rendered based on roles.
Use the SecurityContext
properly, build links dynamically, and test thoroughly. That way, your API will be both secure and self-documenting — the best of both worlds.