In todays modern IT architecture, domains are quite the thing. Domains are supposed to be as much isolated from each other as possible. If you have an application that manages sensitive data (and really, when do you not have sensitive data?), you need to think about security.
Programmatic security
In many cases, you can get away with role-based security (e.g. you are an admin or a user) and be done with it. In Java EE applications, you simply add some <security-constraint> in the web.xml or annotate functions with @RolesAllowed. But in many other cases, there is a lot more business logic involved. You might be able to edit data for one location but not for another, some operations are not location-bound while others are. Maybe it depends on the moon phase or the day of the week whether or not you are allowed to do something or not. This is called programmatic security in JEE.
Let’s say that we have a printer maintenance company. Customers are authorised to edit the printer name for specific locations. We also have mechanics that can edit all printer configurations for all locations. How would this work?
One solution would be to work role-based (declarative security). For editing the printer name, we could do something like this:
@Inject private Principal principal; @RolesAllowed({"customer", "mechanic"}) public void editPrinterName(long id, String name) { ... }
This would allow only customers and mechanics to edit the printer’s name. It is fine for the mechanics (they are allowed to edit any printer), but customers should only be allowed to edit printers at their own location. So we need to check at which location the printer is, and if the customer is authorised for that location:
@Inject private Principal principal; @RolesAllowed({"customer", "mechanic"}) public void editPrinterName(long id, String name) { Location location = printerService.findLocationByPrinterId(id); if (userService.isAuthorisedForLocation(location, principal)) { .. } }
This feels weird for two reasons. First, we have two levels of checks: the @RolesAllowed and our own isAuthorisedForLocation(). Maybe that could be translated to a more business-logic related function like isLocatedAtLocation() which would make it a bit less weird. But that assumes that isLocatedAt is the same as isAuthorisedFor. And that is not necessarily the case.
Secondly, we are tying together a couple of domains here. It is highly likely that the printerService is part of some PrinterConfiguration domain. The userService is in most cases situated in a completely different domain. So this goes against the principle of isolated domains: we can’t build the printerService in a secure way without another domain.
Tokens
Most likely, if you are using a services oriented architecture, you already faced the issue of single sign on. Identity manager like Keycloak allow you to pass proof of authentication (a token) along the different services, making sure that a user only has to log in once. We could use those tokens to pass along data. If we could add e.g. the authorised locations to the token we could use that to skip the call to the user service. So how would that work?
Add the custom attribute
First, we need to configure a custom attribute for the client that needs to receive the information. In our case, that would be the printer domain client that is configured in Keycloak.
- If you select the client in Keycloaks admin panel (e.g. http://localhost:8080/auth), you can add a new Mapper.
- Select the User Attribute type and give it a name (e.g. “authorised-locations”).
- Use the same name in the user attribute and in the token claim name.
- Set the Claim JSON type to String so we can put whatever we want in there.
- Check the add to ID token and add to access token as well so that the printer domain client will receive this new attribute.
Write the custom attribute
Now, we need to make sure that our user accounts are updated when they are authorised (or de-authorised) for a location. As described in Programmatically adding users in Keycloak we can use the admin client for that in our user domain:
Keycloak kc = Keycloak.getInstance("http://localhost:8080/auth", "master", "admin", "admin", "security-admin-console"); UserResource userResource = kc.realm("OurRealm").users().get("xxx-xxx-xxx-xxx-xxx"); UserRepresentation user = userResource.toRepresentation(); Map<String, Object> map = new HashMap<>(); map.put("authorised-locations", Arrays.asList("23", "25")); user.setAttributes(map); userResource.update(user);
The only thing our user domain needs to know is that locations have an id.
Read the custom attribute
With this, Keycloak will add the authorised-locations as-is to each token for our printer domain. So how can we retrieve it? The properties are accessible from the otherClaims property found in the Token. As it is already validated by the Keycloak adapter, we only need to grab the data from it:
@Resource private SessionContext ctx; @RolesAllowed({"customer", "mechanic"}) public void editPrinterName(long id, String name) { Location location = printerService.findLocationByPrinterId(id); if (isAuthorisedForLocation(location.getId(), ctx.getCallerPrincipal())) { .. } } public boolean isAuthorisedForLocation(long locationId, Principal p) { KeycloakPrincipal p = (KeycloakPrincipal) ctx.getCallerPrincipal(); Map<String, Object> otherClaims = p.getKeycloakSecurityContext().getToken().getOtherClaims(); String authorisedLocations = otherClaims.get("authorised-locations"); return checkLocation(locationId, authorisedLocations); }
Summary
You might argue that we still are sharing state between the user domain and the printer domain. Well, yes: both have to have knowledge of locations. But they don’t have to have the same perspective on locations: they could assign different properties and have their own “truth” of which locations exist. For the user domain, it could be sufficient to know that a location exists and that we can authorise users for it. The printer domain has a different perspective: it knowns that locations can have printers. Secondly, in this scenario, locations are less volatile than authorisations-for-a-location. Using the token mechanism, the most volatile part is managed while we can think of less frequent synchronisation for locations if necessary.
We do need to share this (minimal) security logic between domains which is less clean as we would like to see it. However, we might be able to find a cleaner way to access the token, making the domains only depend on the keycloak-admin-client and not on a shared library.
Finally, we still have the two layers of security. If we are going the way of programmatic security, we might as well go all the way and remove the @RolesAllowed annotation and replace it with some logic of our own. If we want to be fancy, we can hide the logic again behind our own annotations and add static code checking for it. But that’s for another blog post.
Read more:
[edit: otherClaims is being filled which for some reason did not work for me the first few times. Fixed the post to simply use that.]