Foto Source: CottonBro Studio (www.pexels.com)
Option: BasicAuthentication
In the previous post (Part 2), we have set the scene by implementing a basic service-to-service communication with 3 REST end-points (publicEP, userEP, adminEP). The checking of the role has been done programmatically against an HTTP query parameter. We have elaborated on the down-sides of this option and will now take it to the next level. In this post (Part 3), we will apply BasicAuthentication.
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 |
(2/7) | 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. |
This blog | 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?
In this blog part (part 3), we are going to implement BasicAuthentication. It is a very basic authentication technique that can be used to enforce access controls to Web resources. It works by transmitting the credentials in an HTTP header in the format of Authorization: Basic <credentials>.
The good thing: It is very simple and widely supported by almost all browser and web server. But it does depend on other security postures (e.g. by using HTTPS) as the credentials are only Base64-encoded.
We will show the implementation of BasisAuthentication with quarkus in 2 flavors. They only differ in the location of the credentials:
- Basic Authentication Embedded: Listing users and roles in theapplication.properties file (just for development / testing)
- Basic Authentication External Database: Referring to an external database which is storing credentials
Prerequisites
We are using the following tools:
Code Base:
You can either:
- continue from the previous blog and clean-up the code as follows:
- In ServiceA, remove all “role” variables from the method headers, e.g.:
public String call_userEP(@QueryParam("role") String role) {
return externalService.userEP(role);
}
- In ServiceB, also remove all “role variables from the method headers. Moreover, remove the if-statements in the methods:
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!";
}
- In ServiceA, remove all “role” variables from the method headers, e.g.:
- or clone the code base from here to have a clean start.
Implementation
We will explain step-by-step how you can achieve multi-service RBAC with Basic Authentication. If you are only interested in the end result, you can clone this from git here.
- For both services you need to add the extension quarkus-elytron-security-properties-file. This can be done either via the command line, by copying the stanza in the pom-file or via the IDE.
The command line command is:
mvn quarkus:add-extension -Dextensions=quarkus-elytron-security-properties
The stanza for copying in the pom-file is:<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-properties-file</artifactId>
</dependency> - Add the following environment variables to the
application.properties
file – again for both services:
quarkus.security.users.embedded.enabled=true
quarkus.security.users.embedded.plain-text=true
quarkus.security.users.embedded.users.user=user_pw
quarkus.security.users.embedded.users.admin=admin_pw
quarkus.security.users.embedded.roles.user=users
quarkus.security.users.embedded.roles.admin=admins
This defines the user names [user,admin], the respective passwords [user_pw, admin_pw] and respective roles [users, admins].
It is a very convenient option for developing and testing, but obviously not a viable fit for production use 🙂 - Now, we have to also define the RBAC rule. With Quarkus, this is very easy. We just need to add an annotation @RolesAllowedto the method, e.g. for the userEP:
@Path("/userEP")
@RolesAllowed("users")
Do this for ServiceA and ServiceB, in order for both services to be RBAC-secured. - Start both services in quarkus dev mode:
mvn quarkus:dev - Now, try to test this by calling any end-point from ServiceA that is protected (e.g. userEP or adminEP):
curl -v http://localhost:8000/serviceA/userEP
You will get a HTTP 401 error (Unauthorized)!
This is expected behavior as the RBAC rule is now enforced. But how can we provide the necessary credentials to satisfy this rule? - As stated in the introduction, Basic Authentication expects a base64-encoded “username:password” credential in the Authorization Header. Let’s try this out with the CLI.
(Remark: You can also test this via a WebBrowser which would then open a simple login form where you can enter “username” and “password. The problem with the browser is that credentials are cached and thus it is inconvenient if you want to test with different credential sets!)
- Encode the credentials from “user” and “admin” and export them to an environment variable:
export USER_CRED=$(echo -n 'user:user_pw' | base64)
export ADMIN_CRED=$(echo -n 'admin:admin_pw' | base64) - Call the ServiceB end-points (port:9000) directly with the Authorization header:
curl -H "Authorization: Basic $USER_CRED" http://localhost:9000/serviceB/userEP -v
- The results should be like this:
- Calling userEP with ADMIN_CRED: HTTP 403
- Calling userEP with USER_CRED: HTTP 200
- Calling adminEP with ADMIN_CRED: HTTP 200
- Calling adminEP with USER_CRED: HTTP 403
- Encode the credentials from “user” and “admin” and export them to an environment variable:
- That’s great, but let’s now extend this to have the credentials propagated from ServiceA to ServiceB to enforce RBAC on both services.
- First, let’s try to call the ServiceA-endpoints with the same Authorization header, e.g:
curl -H "Authorization: Basic $USER_CRED" http://localhost:8000/serviceA/userEP -v
- We now get for all endpoints a HTTP 500 error!
If you read the error message, it actually states that the Server sends back a HTTP 401 (“Unauthenticated”). The reason is very easy. We don’t yet propagate the Authorization header from the client to the server.
- First, let’s try to call the ServiceA-endpoints with the same Authorization header, e.g:
- This can be easily fixed by:
- adding another environment variable in the application.properties file:
org.eclipse.microprofile.rest.client.propagateHeaders=Authorization
- Adding an annotation to the Interface file which will instruct Quarkus to propagate the headers:
@RegisterClientHeaders()
- adding another environment variable in the application.properties file:
- If you try again, everything should work!
Congratulations! You have enabled Basic Authentication for your multi-service application!
CONCLUSION
We have achieved an improvement in multiple aspects:
- We are using a standardized (yet not very secure) authentication.
- The receiving service is checking against credentials (username & password)
Disadvantages:
- Each service needs to have access to the whole credential store.
- There is no explicit trust between the involved services. ServiceB would accept requests from each caller.
BasicAuthentication with External Database
In fact, we have already applied all the principles of BasicAuthentication in the above implementation. But as stated above, (for production use) it is not realistic to put user names, passwords and roles into the application.properties file.
Now, we get to a production-ready implementation of BasicAuthentication which is also much more common. Instead of writing all the credentials in the application.properties file, we are using an external database to store the credentials. The actual enforcement of the RBAC rules is exactly the same as in the previous option.
We will explain step-by-step how you can achieve this. Otherwise you can clone the end-result from git here.
- For this, we need to first create an external database that will then later on populate with credentials. To keep it simple, we will just start a postgres Database locally in a container:
podman run --rm=true --name security-getting-started -e POSTGRES_USER=quarkus \
-e POSTGRES_PASSWORD=quarkus -e POSTGRES_DB=identity_store \
-p 5432:5432 postgres:14.1 - Now, we have to adapt our services to look for credential information in the postgres DB. This of course requires additional extensions for our quarkus projects to establish and manage the connection to the database. Thus, for both services add:
- quarkus-hibernate-orm-panache
- quarkus-security-jpa
- quarkus-jdbc-postgresql
This can be achieved via the following command in the root directory of both services:
mvn quarkus:add-extensions -Dextensions=hibernate-orm-panache,security-jpa,jdbc-postgresql
- Moreover, we need a mapping class that specifies which database attributes correspond to which class attributes for the JPA security. As RBAC shall be enforced by both services, we need this mapping class in ServiceA and ServiceB.
package org.acme;
import javax.persistence.Entity;
import javax.persistence.Table;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.quarkus.security.jpa.Password;
import io.quarkus.security.jpa.Roles;
import io.quarkus.security.jpa.UserDefinition;
import io.quarkus.security.jpa.Username;
@Entity
@Table(name = "test_user")
@UserDefinition
public class User extends PanacheEntity {
@Username
public String username;
@Password
public String password;
@Roles
public String role;
/**
* Adds a new user in the database
* @param username the user name
* @param password the unencrypted password (it will be encrypted with bcrypt)
* @param role the comma-separated roles
*/
public static void add(String username, String password, String role) {
User user = new User();
user.username = username;
user.password = BcryptUtil.bcryptHash(password);
user.role = role;
user.persist();
}
}
Code language: JavaScript (javascript)
4. Also, we have to populate the database with some test users. For this, we create a Startup Class (sufficient to have it only in one service) that adds some users.
package org.acme;
import javax.enterprise.event.Observes;
import javax.inject.Singleton;
import javax.transaction.Transactional;
import io.quarkus.runtime.StartupEvent;
@Singleton
public class Startup {
@Transactional
public void loadUsers(@Observes StartupEvent evt) {
// reset and load all test users
User.deleteAll();
User.add("admin", "admin_pw", "admins");
User.add("user", "user_pw", "users");
}
}
Code language: JavaScript (javascript)
5. Now we configure both services to revert to this postgres database for checking credentials. This can be done with some environment variables in the application.properties:
quarkus.hibernate-orm.enabled=true
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus
quarkus.datasource.password=quarkus
quarkus.datasource.jdbc.url=jdbc:postgresql:identity_store
Code language: JavaScript (javascript)
IMPORTANT: Remove the other settings from the embedded set-up:
<s>quarkus.security.users.embedded.enabled=true
quarkus.security.users.embedded.plain-text=true
quarkus.security.users.embedded.users.user=user_pw
quarkus.security.users.embedded.users.admin=admin_pw
quarkus.security.users.embedded.roles.user=users
quarkus.security.users.embedded.roles.admin=admins</s>
Code language: HTML, XML (xml)
6. Lastly, we have to specify that the database has to be generated on Startup. (The Startup class only adds users, but of course requires that the database is already present!)
Thus, for the service with the Startup Class specify in the application.properties file:
quarkus.hibernate-orm.database.generation=drop-and-create
For the other service specify:
quarkus.hibernate-orm.database.generation=none
That’s it. If you have done this properly for both services, you should be able to properly call the endpoints as before.
Congratulations! You have now taken BasicAuthentication to the next level, leveraging an external database!
CONCLUSION
We have now externalized the credentials and also have established a decent security by encrypting the passwords.
Still there remain some disadvantages:
- Each service needs to have access to the database
- There is no explicit trust between the services.
- The Basic Authentication as such doesn’t provide confidentiality for username and password.
In the next blogs, we will explore some more advanced concepts that address these shortcomings.
Troubleshooting
You can:
- use Quarkus logging to get more insights: https://quarkus.io/guides/logging
- Check the postgres database whether it’s running and has the right content (see below the command flow to accomplish this):
One reply on “How to secure microservice applications with role-based access control? (3/7)”
[…] Link […]