In order to use microservices in multiple projects, I tried to rewrite the Keycloak realm for each request, but realized that it is not realistic to manage users for each realm, and the authentication / authorization that was done with Keycloak I decided to separate the authorization part.
Since I implemented the backend microservices in Spring Boot, I added a filter to the Security Filter Chain of Spring to add new authorization information.
The theory is that if you register a member with the email address as the user's unique identifier for each project, you can identify the user with the email address of the Keycloak JWT token.
In use cases where the same user participates in multiple projects, it is more flexible to separate it from Keycloak because you want to manage authorization information for each project.
In addition, the place to add a new filter seems to be good after SessionManagementFilter.class.
public class CustomKeycloakFilter implements Filter {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final ProjectMemberService service;
@Override
public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
logger.debug("######## CUSTOM KEYCLOAK FILTER INITIALIZED ########");
}
public CustomKeycloakFilter(ProjectMemberService service) {
this.service = service;
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
logger.debug("######## CUSTOM KEYCLOAK FILTER ########");
// extract project id from the request header
String projectId = request.getHeader("project-id");
logger.debug("++++++++ PROJECT ID: {} ++++++++", projectId);
// extract Kycloak authentication token
Principal principal = request.getUserPrincipal();
if (principal != null && principal instanceof KeycloakAuthenticationToken) {
logger.debug("++++++++ AUTHENTICATED BY KEYCLOAK ++++++++", principal.getClass().getName());
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) principal;
KeycloakSecurityContext context = (KeycloakSecurityContext)token.getAccount().getKeycloakSecurityContext();
AccessToken accessToken = context.getToken();
// extract email from the access token as a unique user identifier.
String email = accessToken.getEmail();
logger.debug("++++++++ email: {} ++++++++", email);
if (projectId != null && email != null) {
// retrieve project user permissions from the project database by project id and user email
Collection<? extends GrantedAuthority> authorities = service.getAuthz(UUID.fromString(projectId), email);
List<String> authorityStringList = new ArrayList<>();
authorities.forEach(o -> authorityStringList.add(o.getAuthority()));
logger.debug("++++++++ AUTHOTITIES: {} ++++++++", authorityStringList);
// generates a new authentication token with reloaded user permissions
KeycloakAuthenticationToken newAuthenticationToken = new KeycloakAuthenticationToken(token.getAccount(), false, authorities);
logger.debug("++++++++ NEW AUTHENTICATION TOKEN: {} ++++++++", newAuthenticationToken.toString());
SecurityContext sc = SecurityContextHolder.getContext();
// replaces authentication token with the new one
sc.setAuthentication(newAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
By separating the authorization part of Keycloak's authentication / authorization, it was possible to support multi-project (~~ multi-tenant ~~). Since authorization information can be managed for each project, it seems that the project can be operated with a high degree of freedom.