I have a number of Wicket-based web applications for various hobby purposes. These applications each serve a specific task, such as keeping track of my financial situation, or to inform me when my favorite YouTube channels have updates I’m interested in. A common feature among these applications is that they have some form of identity management, usually in the form of usernames and passwords.
Having created a number of these login systems over the years, I find the process to be quite tedious. I considered creating my own centralized identity management system to combine the login logic for all these applications when I heard Adam Bien mention Keycloak in one of his Airhacks.tv episodes.
Intrigued, I did some research, and decided to use Keycloak as identity management solution for my next
project, which we’ll refer to as keycloak-client
in this post.
The application is bundled as a pair of WAR files, one for the Wicket-based frontend, and one for the Spring and Resteasy-based backend.
Configuration
Keycloak runs as a Docker container on my home server, and on this installation I created a realm named
“Personal” with a client named keycloak-client
which connects using OpenID Connect. Once
this client was configured, I downloaded the keycloak.json
file from the Installation tab in the client view, which
looked something like this:
{
"realm": "Personal",
"auth-server-url": "https://host/path/to/keycloak",
"ssl-required": "external",
"resource": "keycloak-client",
"public-client": true,
"verify-token-audience": true,
"use-resource-role-mappings": true,
"confidential-port": 0
}
This file was placed in the WEB-INF
folder of both the backend and the frontend. I then added
the Keycloak servlet filter to the pom.xml
of both projects:
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-servlet-filter-adapter</artifactId>
<version>3.1.0.Final</version>
</dependency>
I then used the following configuration for the REST backend:
<filter>
<filter-name>keycloak</filter-name>
<filter-class>org.keycloak.adapters.servlet.KeycloakOIDCFilter</filter-class>
<init-param>
<param-name>keycloak.config.skipPattern</param-name>
<param-value>/health</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>keycloak</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
And the following for the Wicket frontend (which has no /health
service):
<filter>
<filter-name>keycloak</filter-name>
<filter-class>org.keycloak.adapters.servlet.KeycloakOIDCFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>keycloak</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
With this configuration, all URLs (save for the /health
endpoint on the backend) require a valid Keycloak
session, and will automatically redirect you to Keycloak if you’re not logged in.
Accessing Keycloak’s session data from Wicket
Once you’re logged in, you’re probably going to want to know important things such as the user’s ID, and
possibly what roles the user has. This information is stored as part of the servlet request, in a class
of type KeycloakSecurityContext
. Accessing this class from Wicket is simply a matter of getting the current
request from the RequestCycle
:
ServletWebRequest request = (ServletWebRequest) RequestCycle.get().getRequest();
HttpServletRequest containerRequest = request.getContainerRequest();
KeycloakSecurityContext securityContext = (KeycloakSecurityContext) containerRequest.getAttribute(KeycloakSecurityContext.class.getName());
From the KeycloakSecurityContext
you can get the AccessToken
, which contains the User ID in the Subject property:
AccessToken token = securityContext.getToken();
String subject = token.getSubject();
In addition to this data, the KeycloakSecurityContext
contains a wealth of other information, which
I won’t cover in detail. The most important information from a Wicket point of view is probably
the roles a user has, which can be accessed as follows:
// This will probably differ for your implementation. Check field "resource" in keycloak.json
final String CLIENT_ID = "keycloak-client";
AccessToken token = securityContext.getToken();
Map<String, Access> access = token.getResourceAccess();
Set<String> roles = new HashSet<>();
if (access.containsKey(CLIENT_ID)) {
Set<String> userRoles = access.get(CLIENT_ID).getRoles();
if (userRoles != null) {
roles.addAll(userRoles);
}
}
You can then use these roles to define access to various pages, perhaps using a homebrew solution
that uses an IComponentInstantiationListener
, or by adapting an existing solution such as
wicket-auth-roles (though it might
be easier to only partially use the auth-roles functionality since your application will not have its
own login page).
Accessing Keycloak’s session data from JAX-RS
The same logic can be used from a JAX-RS resource, by extracting the KeycloakSecurityContext
from
the HttpServletRequest:
@Path("/example")
public class ExampleResource {
@Context
private HttpServletRequest servletRequest;
@GET
public Response whoAmI() {
KeycloakSecurityContext context = (KeycloakSecurityContext) servletRequest.getAttribute(KeycloakSecurityContext.class);
if (context == null) {
return Response.status(Response.Status.FORBIDDEN).build();
}
return Response.ok(context.getToken().getSubject()).build();
}
}
Accessing the backend from the frontend
My frontend communicates with my backend through REST, using ResteasyClient
to reuse the JAX-RS interfaces
the backend also uses. Since the backend is also secured using Keycloak, you need to pass the access token
as header with every request you make to the backend. This can be done using a ClientRequestFilter
:
public class KeycloakTokenFilter implements ClientRequestFilter {
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
ServletWebRequest request = (ServletWebRequest) RequestCycle.get().getRequest();
HttpServletRequest containerRequest = request.getContainerRequest();
KeycloakSecurityContext securityContext = (KeycloakSecurityContext) containerRequest.getAttribute(KeycloakSecurityContext.class.getName());
requestContext.getHeaders().add("Authorization", "Bearer "+ securityContext.getTokenString());
}
}
This filter is then added to the ResteasyClient
with the register method:
ResteasyClientBuilder builder = new ResteasyClientBuilder();
ResteasyClient client = builder.build();
client.register(KeycloakTokenFilter.class);
ResteasyWebTarget target = client.target(BACKEND_URL);
In conclusion
The amount of configuration needed to setup Keycloak authentication is minimal, and as such is a very useful solution for securing your Wicket applications.