How to secure microservice applications with role-based access control (2/7)?

March 27, 2023

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.

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 PartImplementation OptionDescription
This blogHTTP Query ParamThis is the most basic module where the “role” is transferred as a HTTP Query Parameter. The server validates the role programmatically.
(3/7)Basic AuthenticationA 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:

  1. Go to code.quarkus.io
  2. Configure your application:
    • Group: org.acme
    • Artifact: serviceA
    • Build Tool: Maven
    • Version: 1.0.0-SNAPSHOT
    • Java Version: 11
    • Starter Code: Yes



  3. Add the extensions:
    • RestEasy Reactive (required for exposing REST end-points)
    • REST Client Reactive (required for calling REST end-points)
  4. Generate the application & download it
  5. Open the project in your favorite IDE (e.g. VS Codium)
  6. Start the service in quarkus developer mode:
    • Open a Terminal
    • cd into the root directory of the project
    • Enter: mvn quarkus:dev
  7. 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:

  1. Go to code.quarkus.io
  2. Configure your application:
    • Group: org.acme
    • Artifact: serviceB
    • Build Tool: Maven
    • Version: 1.0.0-SNAPSHOT
    • Java Version: 11
    • Starter Code: Yes
  3. Add the extensions:
    • RestEasy Reactive (required for exposing REST end-points)
  4. Generate the application & download it
  5. Open the project in your favorite IDE (e.g. VS Codium)
  6. Start the Application quarkus developer Mode:
    • Open a Terminal
    • cd into the root directory of the project
    • Enter: mvn quarkus:dev
  7. 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

  1. Start all the services in quarkus:dev mode:
  2. You can test the end-points by clicking on the URL (above the method signature):


  3. The publicService should return:

    Hello from Service B: This is the public End-point!
  4. 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!

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.

Leave a Reply

close

Subscribe to our newsletter.

Please select all the ways you would like to hear from Open Sourcerers:

You can unsubscribe at any time by clicking the link in the footer of our emails. For information about our privacy practices, please visit our website.

We use Mailchimp as our newsletter platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.