We'll see how to configure a Java Servlet based application so it can be secure with Keycloak.
Keycloak is an Open Source Identity and Access Management that can be used to delegate entirely the security of an application.
1. Keycloak configuration
The Keycloak documentation is really easy to follow. You can see for yourself here the section about the configuration of your Keycloak instance: https://www.keycloak.org/docs/latest/authorization_services/#_getting_started_hello_world_create_realm
You need to configure:
- A realm
- A user with role
user
, we'll see later how it's used - A Client. It's a representation of your Java application
- Client protocol: openid-connect
- Access Type: public
- Valid Redirect URIs: the url of your development environment or
*
for the time being
2. Tomcat security-constraint
We're using the Tomcat security-constraint
that enable a security verification at the application level on Tomcat.
The Keycloak team developed a convenient Valve for the Tomcat Security system that handle the redirect to and from the Keycloak login page.
2.1. You need to add the following to the context.xml
of your application:
<Context>
<Valve className="org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve"/>
</Context>
2.2. Install the Keycloak Valve libraries into the ${tomcat}/lib
directory on your Tomcat server
2.3. You need to copy the keycloak.json
config file into /WEB-INF/keycloak.json
You can download the file in your Client installation tab:
2.4. Add security-constraint
in your web.xml
<security-constraint>
<web-resource-collection>
<web-resource-name>Private area</web-resource-name>
<url-pattern>/esp_privat/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Public area</web-resource-name>
<url-pattern>/api/*</url-pattern>
</web-resource-collection>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>this is ignored currently</realm-name>
</login-config>
<security-role>
<role-name>user</role-name>
</security-role>
Here we defined 2 URL patterns:
-
/esp_privat/*
that require a user to be connected with a roleuser
-
/api/*
that require no authentification
2.5. Results
So when you try accessing any route under /esp_privat/
in your application Keycloak valve now automatically redirect you to the login page in your Keycloak instance.
When successfuly logged in Keycloak redirects you to the asked page.
What we need to do now is to identify the user logged in thank's to the token Keycloak is adding to the cookies of the web navigator.
3. Intercept Keycloak access token to log the user into your app
3.1. Keycloak dependencies
Add the following to the pom.xml
of your webapp
application:
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>9.0.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
<version>9.0.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-spi</artifactId>
<version>9.0.2</version>
<scope>provided</scope>
</dependency>
Notice the scope = provided
since we will be using the libraries added previously into the tomcat library folder. We don't want to override it with another version of the libraries.
3.2. Read the token
The following snippet will extract the token from the request and verify if it's lifetime is expired. It returns true in case the token is valid.
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.TokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.representations.AccessToken;
...
/**
* Verify if user is logged in keycloak by validating token in request
*/
public boolean isLoggedInKeycloak(HttpServletRequest request) throws VerificationException {
KeycloakSecurityContext keycloakSecurityContextToken = getKeycloakSecurityContextToken(request);
if (keycloakSecurityContextToken == null) {
return false;
}
return !isTokenExpired(keycloakSecurityContextToken);
}
private boolean isTokenExpired(KeycloakSecurityContext keycloakSecurityContextToken) throws VerificationException {
AccessToken token = TokenVerifier.create(keycloakSecurityContextToken.getTokenString(), AccessToken.class).getToken();
if (token.isExpired()) {
logger.warn("User token is expired..." + token);
return true;
}
return false;
}
In our case we also needed to verify if the user is a member of the correct group so we added the following method check:
private void handleGroupMembership(@Nonnull KeycloakSecurityContext keycloakSecurityContext, String keycloakPreferredUsername) {
Object groups = keycloakSecurityContext.getToken().getOtherClaims().getOrDefault("groups", new ArrayList<>());
if (groups == null) {
throw new GenericRuntimeException("Fail to read groups from the token of the user " + keycloakPreferredUsername);
}
((List<String>) groups)
.stream()
.filter(s -> s.equalsIgnoreCase("/my-group"))
.findFirst()
.orElseThrow(() -> new GenericRuntimeException("User \"" + keycloakPreferredUsername + "\" is not a member of /my-group"));
}
We then called the previous method in a pre-action hook into all the call received by our servlets so that it can be catched by any servlet like so:
boolean isUserLoggedIn = request.getSession().getAttribute(USER_SESSION) != null;
if (isLoggedInKeycloak(request) && !isUserLoggedIn) {
logger.info("User logged in Keycloak but not logged in the app. Logging in the user...");
new KeycloakLoginService().login(request, getKeycloakSecurityContextToken(request));
}
else if (!isLoggedInKeycloak(request) && isUserLoggedIn) {
logger.info("User not logged in Keycloak but logged in the app. Logging out the user...");
sessionLogout.logout(request, response);
return;
}
3.3. Logout
To logout a user from Keycloak you can use the request.logout()
method. We use the following method:
public void logout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
request.logout();
request.getSession(true); // create a new session
response.sendRedirect(request.getContextPath());
}
4. Maven profiles to compile versions with and without keycloak login
In one of our project we needed to be able to deploy a version of the app that doesn't use the Keycloak login feature but our previous login mechanism.
Of course we wanted to keep a unique codebase with the less difference as possible.
We identify that the only thing preventing us from working as before was the security-constraint
section in the web.xml
config file.
We will be using the Maven filtering solution with a little hack we found on SO: https://stackoverflow.com/questions/3298763/maven-customize-web-xml-of-web-app-project/8593041#8593041
It consists in adding 2 variables in your web.xml like so:
${enable.security.start}
<security-constraint>
...
// all of the XML that you need, in a completely readable format
...
</login-config>
${enable.security.end}
And have it replaced by comment block start <!--
and end ->
in the profile where you don't want to use Keycloak.
So in our default ci
profile we defined the following properties:
<enable.security.start></enable.security.start>
<enable.security.end></enable.security.end>
and in the without-keycloak
profile:
<enable.security.start><!--</enable.security.start>
<enable.security.end>--></enable.security.end>
Top comments (3)
Can you please provide source of this sample project, many things are abstract in your implementation
Can you put the project on github , it will be really useful and i would appreciate it!
How can I intercept the login event? Can you provide source of this sample project? Thanks!