Foto Source: Ron Lach (www. pixels.com)
Option: HTTP Query Param
In Part 2 of our 7-part blog series “How to secure microservice applications with role-based access control”, we will build the basic services and establish a connection. Later, we are going to implement a basic Role-based Access Control (RBAC) by transmitting the role information via HTTP query parameters and programatically checking the existence and correctness of the role. This option is (admittedly) unrealistic, but sets the scene for further options & concepts.
In Part 1, we’ve provided the context for the whole blog series. We recommend you read it first because otherwise, you miss out on the context.
Overview of the Options & Links to Blogs
In Part 1, we’ve provided the context for the whole blog series. We recommend you read it first because otherwise, you miss out on the context.
You find below an overview of the content of these blog series. Just click on the link to jump directly to the respective blog part.
Blog Part | Implementation Option | Description |
This blog | HTTP Query Param | This is the most basic module where the “role” is transferred as a HTTP Query Parameter. The server validates the role programmatically. |
(3/7) | Basic Authentication | A user agent uses Basic Authentication to transfer credentials. |
(4/7) | JWT | A JSON Web Token (JWT) codifies claims that are granted and can be objectively validated by the receiver. |
(5/7) | OpenID and Keycloak | For further standardization, OpenID Connect is used as an identity layer. Keycloak acts as an intermediary to issue a JWT token. |
(6/7) | Proxied API Gateway (3Scale) | ServiceB uses a proxied gateway (3Scale) which is responsible for enforcing RBAC. This is useful for legacy applications that can’t be enabled for OIDC. |
(7/7) | Service Mesh | All services are managed by a Service Mesh. The JWT is created outside and enforced by the Service Mesh. |
What do we want to achieve in this blog part?
We are implementing the 2 services with multiple end-points:
- publicEP: Endpoint that can be accessed by everybody
- userEP: Endpoint that can only be accessed by persons who have the role “users”.
- adminEP: Endpoint that can only accessed by persons who have the role “admins”.
The blog explains step-by-step how to accomplish the final state.
If you have any issues you can also clone the git repository https://github.com/sa-mw-dach/micro-service-rbac and find the resources in the directory http-query.
Prerequisites
We are using the following tools:
Bootstrapping ServiceA
Let’s start by bootstrapping a Quarkus application:
- Go to code.quarkus.io
- Configure your application:
- Group: org.acme
- Artifact: serviceA
- Build Tool: Maven
- Version: 1.0.0-SNAPSHOT
- Java Version: 11
- Starter Code: Yes
- Add the extensions:
- RestEasy Reactive (required for exposing REST end-points)
- REST Client Reactive (required for calling REST end-points)
- Generate the application & download it
- Open the project in your favorite IDE (e.g. VS Codium)
- Start the service in quarkus developer mode:
- Open a Terminal
- cd into the root directory of the project
- Enter:
mvn quarkus:dev
- Test the endpoint
http://localhost:8080/hello
You should get:Hello from RestEasy Reactive
Congratulations! You have a running Quarkus REST application.
Implementing ServiceA
In the GreetingResource class, we are adding 3 end-points to the service that correspond to the 3 service endpoints.
package org.acme;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.inject.RestClient;
@Path("/serviceA")
public class GreetingResource {
@Inject
@RestClient
ExternalService externalService;
@GET
@Path("/publicEP")
@Produces(MediaType.TEXT_PLAIN)
public String call_publicEP() {
return externalService.publicEP();
}
@GET
@Path("/userEP")
@Produces(MediaType.TEXT_PLAIN)
public String call_userEP(@QueryParam("role") String role) {
return externalService.userEP(role);
}
@GET
@Path("/adminEP")
@Produces(MediaType.TEXT_PLAIN)
public String call_adminEP(@QueryParam("role") String role) {
return externalService.adminEP(role);
}
}
Code language: CSS (css)
Some remarks:
- The ExternalService is an interface (annotated with @RestClient) that we will generate later on.
- @QueryParam is injected into the method header. This is the variable that will be populated with the HTTP query parameter.
Bootstrapping ServiceB
Let’s now bootstrap a Quarkus application for ServiceB:
- Go to code.quarkus.io
- Configure your application:
- Group: org.acme
- Artifact: serviceB
- Build Tool: Maven
- Version: 1.0.0-SNAPSHOT
- Java Version: 11
- Starter Code: Yes
- Add the extensions:
- RestEasy Reactive (required for exposing REST end-points)
- Generate the application & download it
- Open the project in your favorite IDE (e.g. VS Codium)
- Start the Application quarkus developer Mode:
- Open a Terminal
- cd into the root directory of the project
- Enter:
mvn quarkus:dev
- Test the endpoint
http://localhost:8080/hello
Implementing ServiceB
In the ResourceClass that has been automatically generated, we are adding 3 end-points to the service that correspond to the 3 service endpoints.
package org.acme;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
@Path("/serviceB")
public class GreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/publicEP")
public String publicService(@QueryParam("role") String role) {
return "I don't care which role you have. I always greet you!";
}
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/userEP")
public String helloUser(@QueryParam("role") String role) {
if (role.equals("user")) {
return "I greet you because you are a user!";
} else {
return "I don't greet you because you are not a user!";
}
}
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/adminEP")
public String helloAdmin(@QueryParam("role") String role) {
if (role.equals("admin")) {
return "I greet you because you are a admin!";
} else {
return "I don't greet you because you are not a admin!";
}
}
}
Code language: JavaScript (javascript)
As you can see, we are only checking the appropriate role with an “if” – statement.
Connecting ServiceA and Service B
We are now adding an interface class to ServiceA that mimics the Service end-points of ServiceB. Quarkus offers a very convenient way to use this interface to be called via a RestClient.
package org.acme;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@RegisterRestClient
@Path("/serviceB")
public interface ExternalService {
@GET
@Path("publicEP")
String publicEP ();
@GET
@Path("userEP")
String userEP (@QueryParam("role") String role);
@GET
@Path("adminEP")
String adminEP (@QueryParam("role") String role);
}
Code language: CSS (css)
Now, ServiceA can just call methods from the Interface class (e.g. “userEP”). We will later bind the interface class to the actual location of ServiceB.
Before that, we need to bind both services to a URL. As we currently run all 2 services locally, we need to bind them to different ports. This can be accomplished by setting the quarkus.http.port environment variable in the application.properties file (located in the src/main/resources/ folder).
We are using the following ports throughout the whole blog series:
ServiceA: port 8000
ServiceB: port 9000
So, for ServiceA, we specify in the application.properties:
%dev.quarkus.http.port=8000
And for ServiceB, we specify in the application.properties
:
%dev.quarkus.http.port=9000
(Remark: The %dev prefix adds a scope to the respective environment variable. For the moment, we are working in the %dev scope which is automatically associated by Quarkus if starting in dev mode”)
For the services to “find” each other, we also need to add another environment variable to the application.properties
file of ServiceA which points to the URL of ServiceB:
quarkus.rest-client."org.acme.ExternalService".url=http://localhost:9000
Testing the connection
- Start all the services in quarkus:dev mode:
- You can test the end-points by clicking on the URL (above the method signature):
- The publicService should return:
Hello from Service B: This is the public End-point!
- For the userService and adminService:
- If you send the request without a http query param:
The REST end-point expects a http query param and thus throws a ClientWebApplicationException! - If you send the request with a http query param and the wrong role, e.g:
http://localhost:8080/client/adminService?role=user
:
I don't greet you because you are not a admin!
- If you send the request with a http query param and the right role, e.g: http://localhost:8080/client/adminService?role=admin:
I greet you because you are a admin!
- If you send the request without a http query param:
Congratulations! You have established the basic connection and already some – even tough fake – RBAC.
Conclusion
We have now achieved a basic service-to-service connection. The securing of the REST end-points is done programmatically via an if-statement.
Advantages:
- The solution can be implemented very fast, without any 3rd party components.
Disadvantages (amongst others):
- It is very easy to call the REST end-points with any assumed role.
- There is no trust relationship between client and server established.
- There is no validation of username / password.