Microservices (or microservices architecture) is an architectural style for developing applications. When a microservices-based application responds to a user workflow or request, it may call on multiple external microservices (exposed over the internet), which may call on too few internal microservices (not exposed over the internet). Each microservice has a specific concern, which allows an extensive application to be separated into smaller pieces.
When moving to microservices, securing the microservices needs to be undertaken differently than a monolithic application. A monolith application has a single-user session context shared by all internal components. An architecture based on microservices does not share user context across them, so sharing it requires explicit communication between microservices.
A combination of Authentication (AuthN) and Authorization (AutZ) is commonly used to secure applications.
AuthN – verifies that you are who you claim to be.
AuthZ – whether the user can access information or execute the operation.
Recently, I was developing an authorization workflow for a set of microservices, and the challenge was: -
1. How to enable role-based authorization per API?
2. How to propagate roles received from external microservice to internal microservice?
In this article, we will see how I solved these two problems by developing: -
1. Custom annotation using AspectJ
2. Custom request filter and client interceptor using Spring framework
Let’s get started!
I created a data flow diagram (Figure 1) to explain the problem. There is one UI application, making calls to external microservices and these calls may also be served by other internal microservices. The session manager’s service responsibility is to log in to the user, store the session, and call the AuthZ service. AuthZ service calls the user subscription service to get logged-in user roles. AuthZ service returns the role to the session manager service, and these roles are passed (in the header as JSON array) to all backend external microservices per API call.
Session manager service attaches one request HTTP header “X-Application-Roles” to all API requests.
Example : X-Application-Roles : ["GLOBAL_ADMIN", “TENANT_ADMIN”, “APPLICATION_USER”]
When an API request has this header with given values, it says that the logged-in user ID has these roles assigned per application subscription.
Coming next to original problem statements:-
How do we enable role-based authorization per API? I tried the following approaches to solve this problem: -
I decided to proceed with the second approach as I needed to resolve roles for 30+ backend APIs and the first approach was introducing another hop with each API request.
How do we propagate roles received from external microservice to internal microservice? The solution to the first problem is well suited for external microservices as they were directly receiving the X-Application-Roles header from requests coming from UI, but the challenge was to propagate this header further to internal microservices and use the same custom role annotation. I tried the following approaches to solve this problem: -
I decided to proceed with the second approach as I don’t want to introduce another library dependency and extra configuration.
Step 1: Create and enumeration to hold roles and header name: -
public enum ApplicationRoles {
GLOBAL_ADMIN,
TENANT_ADMIN,
APPLICATION_USER;
public static final String X_APPLICATION_ROLES_HEADER = "X-Application-Roles";
}
Step 2: Create an interface that we’ll use as a method-level annotation on Spring boot’s controller’s APIs: -
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RolesAllowed {
ApplicationRoles[] value();
}
Step 3: Create a custom aspect using the AspectJ library to check whether roles coming in the X-Application-Roles header have the role needed to make this API call. This annotation will serve the API request when roles present in the request header have role applied over API (will show in next step) else it will throw a FORBIDDEN exception.
@Aspect
public class RolesCheckAspect {
// User your package name where RolesAllowed interface exists
@Before("@annotation(com.adp.security.auth.RolesAllowed)")
public void before(JoinPoint joinPoint) throws JsonProcessingException {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// Get expected Roles from annotation
Set<ApplicationRoles> expectedRoles = Arrays.stream(methodSignature.getMethod().getAnnotation(RolesAllowed.class).value()).collect(Collectors.toSet());
// Get all annotations defined in method signature
Annotation[][] parameterAnnotations = methodSignature.getMethod().getParameterAnnotations();
// Get actual arguments from the method
Object[] methodArguments = joinPoint.getArgs();
// Get actual roles from request
Set<ApplicationRoles> actualRoles = getActualRoles(parameterAnnotations, methodArguments);
// Check whether actual roles have expected API role
if (expectedRoles.stream().noneMatch(actualRoles::contains))
throw new ForbiddenException(String.format("Required roles are missing in '%s' header", ApplicationRoles.X_APPLICATION_ROLES_HEADER));
}
private static Set<ApplicationRoles> getActualRoles(Annotation[][] parameterAnnotations, Object[] methodArguments) throws JsonProcessingException {
// Get roles header index in annotations
int rolesHeaderIndex = getRolesHeaderIndex(parameterAnnotations);
ObjectMapper objectMapper = JsonMapper.builder()
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
.build();
return objectMapper.readValue(String.valueOf(methodArguments[rolesHeaderIndex]), new TypeReference<>() {});
}
private static int getRolesHeaderIndex(Annotation[][] parameterAnnotations) {
record HeaderWithIndex(int index, Optional<RequestHeader> requestHeader){}
HeaderWithIndex rolesHeader = IntStream.range(0, parameterAnnotations.length)
.mapToObj(argIndex -> {
Optional<RequestHeader> optionalRequestHeader = Arrays.stream(parameterAnnotations[argIndex])
.filter(RequestHeader.class::isInstance)
.map(RequestHeader.class::cast)
.filter(requestHeader -> requestHeader.value().equalsIgnoreCase(ApplicationRoles.X_APPLICATION_ROLES_HEADER))
.findFirst();
return new HeaderWithIndex(argIndex, optionalRequestHeader);
})
.filter(headerWithIndex -> headerWithIndex.requestHeader().isPresent())
.findFirst()
.orElseThrow(() -> new BadRequestException(String.format("Required header '%s' is not present", ApplicationRoles.X_APPLICATION_ROLES_HEADER)));
return rolesHeader.index();
}
}
Step 4: Configure RolesCheckAspect bean. This step is necessary when your roles aspect class exists in different library projects.
@Configuration
public class RestTemplateConfig {
@Bean
public RolesCheckAspect rolesCheckAspect() {
return new RolesCheckAspect();
}
}
Step 5: Use roles annotation over the API
@GetMapping("/api/v1/hierarchy")
@RolesAllowed(value={ApplicationRoles.APPLICATION_USER})
public ResponseEntity<String> getHierarchy(
@RequestHeader(TENANT) String tenant,
@RequestHeader(ApplicationRoles.X_APPLICATION_ROLES_HEADER) String roles
) {
return ResponseEntity.ok(hierarchyService.getHierarchy(tenant));
}
Roles header propagation from external microservice APIs to internal microservice APIs
Now, we have authorization enabled for external microservice; the remaining problem is propagating the X-Application-Roles header to the internal microservice.
Step 1: Create a class to store roles from incoming API requests. This class will be configured as request scoped bean.
public class RolesInfo {
private String roles;
public String getRoles() {
return roles;
}
public void setRoles(String roles) {
this.roles = roles;
}
}
Step 2: Create a custom request filter to store roles from the header to the RolesInfo object.
public class RolesFilter implements Filter {
private final RolesInfo rolesInfo;
public RolesFilter(RolesInfo rolesInfo) {
this.rolesInfo = rolesInfo;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String roles = httpServletRequest.getHeader(ApplicationRoles.X_APPLICATION_ROLES_HEADER);
// Setting roles in request scoped bean object
rolesInfo.setRoles(roles);
filterChain.doFilter(servletRequest, servletResponse);
}
}
Step 3: Create a client interceptor to fetch roles from the request scoped bean and propagate to internal microservice API requests.
public class RolesInterceptor implements ClientHttpRequestInterceptor {
private final RolesInfo rolesInfo;
public RolesInterceptor(RolesInfo rolesInfo) {
this.rolesInfo = rolesInfo;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// Header propagation : Get roles from request scoped bean object and add with header
String roles = rolesInfo.getRoles();
request.getHeaders().add(ApplicationRoles.X_APPLICATION_ROLES_HEADER, roles);
return execution.execute(request, body);
}
}
Step 4: Configure request scoped bean and add client interceptor to RestTemplate.
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
}
interceptors.add(new RolesInterceptor(rolesInfo()));
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public RolesInfo rolesInfo () {
return new RolesInfo();
}
}
It's better to create a separate library project with roles aspect, request filter, and client interceptor and use it in different microservice code repositories to avoid repeating the same steps and code duplication.
Hope you enjoyed reading this article!