Sunday, February 16. 2020
Testing Spring Boot (Part I)
Today's entry is about Spring Boot. If you follow the blog you know that I am more engaged to JavaEE, but I wanted to test these new technologies and I decided to start with Spring Boot (maybe the most known framework of this type). I prefer standards (JavaEE) over other technologies (Spring) just because I think this is the way that things should be, but better if I start working with them. In my humble opinion the idea around Spring Boot is nice, particularly how the application is bundle in a single jar with all the needed components in it, really perfect to deploy in a container. So my idea with this series is the following:
Testing Spring Boot but try to limit its functionality to JavaEE specification (mainly servlets and jax-rs, which are the typical frameworks I use to create backend applications). I am going to restrict Spring usage to the minimum.
Including swagger and keycloak as external libraries (other libraries commonly used in my projects).
The app should work in the common Spring Boot configuration (Tomcat and Jersey) and using JBoss implementations (undertow and resteasy).
The app should work like standalone jar executable and container war bundle (inside Wildfly).
The goal is taking advantage of the Spring Boot packaging but developing with common JavaEE technologies. Trying to obtain the best of the two worlds. The last points are just to see how easy is combining those different scenarios (jar and war packaging, changing implementations,...). Please remember this is just my idea, and, of course, there will be other people that prefer the opposite solution (use the Spring framework exclusively).
Adding a Servlet
Let's start with a simple Servlet. Using jax-rs is the final goal, but I prefer to start with the basics. So using the Spring initializer a initial pom.xml was created.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/>
</parent>
<groupId>es.rickyepoderi.springboot</groupId>
<artifactId>springboot-sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-sample</name>
<packaging>jar</packaging>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
And then a simple Hello World servlet is created (The HelloBean is just a POJO used to say hello, it has the greeting and the name properties).
package es.rickyepoderi.springboot.servlet;
import es.rickyepoderi.springboot.HelloBean;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/plain;charset=UTF-8");
try (PrintWriter writer = resp.getWriter()) {
writer.write(new HelloBean(req.getParameter("greeting"), req.getParameter("name")).toString());
}
}
}
In order to make it work with Spring the application is created with an annotation @ServletComponentScan defining the servlets (and other entities of the specification like filters or listeners) that should be initialized.
package es.rickyepoderi.springboot;
import es.rickyepoderi.springboot.servlet.HelloServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan(basePackageClasses = {HelloServlet.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
And it works. You can compile it and make it run with two simple commands:
mvn clean package
java -jar target/springboot-sample-0.0.1-SNAPSHOT.jar
And the Servlet is displayed in the http://localhost:8080/hello URL.
Adding Jax-RS
Now that the simple servlet is working let's move and add the restful web services. The idea is using plain jax-rs, so initially the jersey (jakarta/Oracle implementation) starter is incorporated to the pom.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
The first little drawback was that jersey integration is done using the ResourceConfig class (private class in the Jersey implementation) instead of the standard Application class. This will complicate the change to resteasy unnecessarily. But, it is really easy to use it. The REST application should also be tagged as a @Component to initialize it inside Spring.
package es.rickyepoderi.springboot.rest;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Context;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;
@ApplicationPath("api")
@Component
public class RestApplication extends ResourceConfig {
public RestApplication() {
register(HelloEndpoint.class);
}
}
And finally the REST endpoint is developed. Another Hello World example.
package es.rickyepoderi.springboot.rest;
import es.rickyepoderi.springboot.HelloBean;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
@Path("/hello")
public class HelloEndpoint {
@GET
@Produces("application/json")
public HelloBean sayHello(@QueryParam("greeting") String greeting, @QueryParam("name") String name) {
return new HelloBean(greeting, name);
}
}
And again it works. Just compile and run the application and the rest endpoint is available in http://localhost:8080/api/hello.
Adding Swagger
Now it is time to add swagger to annotate my endpoint. First add the dependency.
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2</artifactId>
<version>2.1.1</version>
</dependency>
The framework will be added like in a common web application, so the previous RestApplication registers the swagger OpenApiResource and initialize the API to include the packages that should be scanned for endpoints.
public RestApplication() {
register(HelloEndpoint.class);
register(OpenApiResource.class);
try {
new JaxrsOpenApiContextBuilder()
.servletConfig(servletConfig)
.application(this)
.openApiConfiguration(new SwaggerConfiguration()
.openAPI(new OpenAPI()
.info(new Info()
.title("Hello App")
.description("Sample Spring Boot Hello App")
.version("0.0.1-SNAPSHOT")))
.prettyPrint(true)
.resourcePackages(Collections.singleton(HelloEndpoint.class.getPackage().getName())))
.buildContext(true);
} catch (OpenApiConfigurationException e) {
throw new RuntimeException(e);
}
}
And now it is turn to annotate all the endpoints and methods. In my case just the sayHello operation.
@GET
@Produces("application/json")
@Operation(summary = "Say Hello",
tags = {"hello"},
description = "Say hello to the given username and greeting",
responses = {
@ApiResponse(description = "The hello bean", content = @Content(schema = @Schema(implementation = HelloBean.class)))
})
public HelloBean sayHello(
@Parameter(description = "The user name") @QueryParam("greeting") String greeting,
@Parameter(description = "The greeting to use") @QueryParam("name") String name) {
return new HelloBean(greeting, name);
}
Finally the swagger-ui.html is added and (I suspect that) it is done in an unusual way for Spring Boot, the needed files were just included in the webapp directory (I just obtained the dist directory from the project and installed in the app directory, the index.html was modified to obtain the hello/openapi.json definition and renamed to swagger-ui.html). And again, everything continues working correctly.
Adding keycloak
The next step is adding keycloak to the restful endpoint. The servlet-filter adapter is going to be used, although there is a Spring and a Spring Boot adapter, the filter adapter is standard and it also works in common web applications. First add the dependency.
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-servlet-filter-adapter</artifactId>
<version>8.0.2</version>
</dependency>
Then a new filter was created extending the default servlet filter.
package es.rickyepoderi.springboot.keycloak;
import javax.servlet.annotation.WebFilter;
import org.keycloak.adapters.servlet.KeycloakOIDCFilter;
@WebFilter(urlPatterns = {"/api/hello", "/keycloak/*"})
public class KeycloakFilter extends KeycloakOIDCFilter {
public KeycloakFilter() {
super(new ClasspathKeycloakConfigResolver());
}
}
The filter is applied to the /api/hello endpoint and the internal /keycloak/* and a configuration resolver is used to obtain the keycloak.json from the classpath.
package es.rickyepoderi.springboot.keycloak;
import java.io.InputStream;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.spi.HttpFacade;
public class ClasspathKeycloakConfigResolver implements KeycloakConfigResolver {
private final KeycloakDeployment deployment;
public ClasspathKeycloakConfigResolver() {
InputStream is = getClass().getResourceAsStream("/keycloak.json");
deployment = KeycloakDeploymentBuilder.build(is);
}
@Override
public KeycloakDeployment resolve(HttpFacade.Request req) {
return deployment;
}
}
And, as it is a common filter, it should also be added to the @ServletComponentScan annotation to be integrated as another servlet class.
@SpringBootApplication
@ServletComponentScan(basePackageClasses = {HelloServlet.class, KeycloakFilter.class})
public class Application {
Finally a index.html was added to be used as a public application with keycloak (I did the trick of using the same client as the public and the bearer-only application, ideally there should be two applications, but the same can be used). And again everything works.
RBAC security
The last thing I wanted to add is some security based on roles. Usually in JavaEE the restful endpoints are annotated as a @Stateless local EJB and RBAC security is easily added to them (@PermitAll, @DenyAll, @RolesAllowed annotations). But enterprise beans is a technology I do not like a lot, the security trick only uses local and stateless and a full JavaeEE application server like wildfly gives that framework out of the box, so it is very easy to use. Nevertheless a similar technique can be achieved using a jax-rs ContainerRequestFilter (a filter that can be added to the rest web services). This entry explains this idea in detail. So the following filter is added.
package es.rickyepoderi.springboot.rest;
import java.io.IOException;
import java.lang.reflect.Method;
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
public class RolesAuthorizationFilter implements ContainerRequestFilter {
@Context
private ResourceInfo resourceInfo;
@Context
private HttpServletRequest httpServletRequest;
private void notAllowed(ContainerRequestContext ctx) {
ctx.abortWith(Response.status(Response.Status.FORBIDDEN).build());
}
private void checkRoles(ContainerRequestContext ctx, String[] rolesAllowed) {
if (rolesAllowed != null) {
for (String role : rolesAllowed) {
if (httpServletRequest.isUserInRole(role)) {
// user is allowed
return;
}
}
}
// user is not of allowed roles => deny
notAllowed(ctx);
}
@Override
public void filter(ContainerRequestContext ctx) throws IOException {
// look for method/class annotations to allow access
Method method = resourceInfo.getResourceMethod();
Class clazz = method.getDeclaringClass();
if (method.isAnnotationPresent(DenyAll.class)) {
// deny at method level
notAllowed(ctx);
} else if (method.isAnnotationPresent(PermitAll.class)) {
// allowed at method level => ok
} else if (method.isAnnotationPresent(RolesAllowed.class)) {
// check roles allowed
checkRoles(ctx, ((RolesAllowed) method.getAnnotation(RolesAllowed.class)).value());
} else if (clazz.isAnnotationPresent(DenyAll.class)) {
// deny at class level
notAllowed(ctx);
} else if (clazz.isAnnotationPresent(PermitAll.class)) {
// allowed at class level => ok
} else if (clazz.isAnnotationPresent(RolesAllowed.class)) {
// check roles allowed in the class
checkRoles(ctx, ((RolesAllowed) clazz.getAnnotation(RolesAllowed.class)).value());
}
// no annotation is like PermitAll => allowed
}
}
This way the endpoint can be annotated (at class and/or method level) to define what users can execute the endpoints. For example in my little hello example the class is annotated to only be executed by the Users role.
@Path("/hello")
@RolesAllowed("Users")
public class HelloEndpoint {
The RolesAuthorizationFilter should also be registered in the RestApplication to be incorporated to the request execution.
Video
And that is all. The application is a typical rest backend application which is annotated using swagger and secured with keycloak and RBAC. The only difference is that this time the app uses Spring Boot. The video shows first the servlet. Then the endpoint is accessed but it is not allowed because it requires authentication. Finally the index page is requested, the javascript keycloak adapter asks for login, and sending the bearer token the user ricky can execute the method (this user belongs to Users role) but admin receives a forbidden response (roles are working as expected). Finally you can see the swagger-ui which reads OK the defining annotations.
As a little summary, this entry presents a modern JavaEE application (rest web services, swagger, OIDC and RBAC) but using Spring Boot instead of a common container. The application is almost a JavaEE app but deployed with Spring Boot. The maven project can be downloaded from here. In a second entry the application will be moved to JBoss implementations (undertow and resteasy) and finally the application will be deployed inside wildfly (instead the full jar bundle). Let's see how complicated that second part is.
Regards!
Sunday, December 8. 2019
Presenting JspC for Wildfly
During the past weeks I have been working in developing the JspC (JSP Compiler) utility tool for Wildfly. The old JBoss AS (JBoss Application Server) used a slightly modified version of Tomcat as its web module which, in turn, used Jasper as the JSP parsing and compiling engine. Therefore in those versions the JSP files could be pre-compiled in order to speed up their first execution just using the Tomcat tools. Tomcat offers an ANT task that let you pre-compile the JSP files of your WAR application. The process really parses each JSP file into a Java Servlet and then compiles it producing the final class file. The JspC tool also gives the possibility of creating the web.xml (or other alternative solution) for mapping the final classes to the initial JSP URL. The benefit of pre-compiling is mainly avoiding all this process the first time the JSP is requested, which, in applications with lots of JSPs, can be a slow task.
When Wildfly substituted the old web implementation with undertow, the JspC original utility stopped working (the generated classes are incompatible and do not execute OK). This is mainly because now the new sub-system uses a modified version of Jasper called Jastow that is incompatible with the resulting classes generated by the current Tomcat counterpart. There is a JIRA about all this that explains the current situation.
Some months ago I decided to try another approach, the idea is using directly the jastow version instead of re-using the Tomcat library for pre-compiling. For that I re-developed the JspC tool but linking to the current Wildfly libraries. This tool in Tomcat is also a command line utility that is used by the ANT task. Only the JspC command is going to be replaced for the moment, I think that the task gives no advantage right now that ANT is obsolete and not very used. This little maven project lets you execute the JspC tool in the command line and pre-compile the JSP files of a WAR application. The project is at very early stages but it is usable in many cases.
- The tool uses the TLDs (tag library descriptors) from the internal Wildfly libs (standard jstl and JSF).
- It also loads custom TLDs from the application libraries.
- Almost all the JspC Tomcat options are replicated with the same functionality.
- Other external libraries (JARs that are available to the application through modules or inside an EAR) can be passed to the tool using the -classpath option.
The tool should more or less work with common WAR applications (there are known cases like web-fragments that are not taken into account) but I really do not see any traction for it internally. So I decided to open the project in my personal github and start advertising it (for example in this blog). Maybe other people are stuck because of this lack of functionality and, in the end, this is a process that is done offline and previous to production. So you can just test the tool, if the JSPs are pre-compiled and work OK, you can modify the WAR app to include them.
The tool has been developed using maven. So the exec-maven-plugin is used to execute it and include all the needed Wildfly libraries. The idea behind the tool is that it can be profiled to work with several wildfly (and EAP) versions. Currently there are two profiles (wildfly18, the default one, and eap72) that define the correct library versions for those servers, for example the wildfly18 profile defines the following dependency versions.
<profile>
<id>wildfly18</id>
<properties>
<version.io.undertow.jastow.jastow>2.0.7.Final</version.io.undertow.jastow.jastow>
<version.org.jboss.metadata.jboss-metadata-web>13.0.0.Final</version.org.jboss.metadata.jboss-metadata-web>
<version.org.jboss.spec.javax.servlet.jsp.jboss-jsp-api>2.0.0.Final</version.org.jboss.spec.javax.servlet.jsp.jboss-jsp-api>
<version.org.jboss.logmanager.log4j-jboss-logmanager>1.2.0.Final</version.org.jboss.logmanager.log4j-jboss-logmanager>
<version.org.apache.taglibs.taglibs-standard-spec>1.2.6-RC1</version.org.apache.taglibs.taglibs-standard-spec>
<version.org.jboss.spec.javax.faces.jboss-jsf-api>3.0.0.Final</version.org.jboss.spec.javax.faces.jboss-jsf-api>
<version.com.sun.faces.jsf-impl>2.3.9.SP03</version.com.sun.faces.jsf-impl>
</properties>
</profile>
So, the JspC needs those libraries from Wildfly to work (mainly jastow, metadata information, servlet spec, log-manager and jstl/jsf). The profile specifies the exact version that the specific Wildfly server bundles. In order to add, for example, the next version 19 a new profile should be added with the new versions for that specific release. This way (while the libraries remain the same) adding a new (or older) version is just defining the list of needed libraries bundled in that version using a different profile.
Let's do a simple test. In order to use a sample application I decided to use a WAR application from the web. It is in Chinese but it does not matter, it is just a JSP application with more than 20 files. I have a video to present how the utility is used.
- First the hotel sample application is built.
- After that the application is deployed into Wildfly 18 using the console.
- When using the application, the JSP files are generated by the server under the tmp, so they are compiled at first use (normal behavior).
- Going to the maven target folder, the WAR app is exploded into another directory.
- Time to use the JspC. It is executed against the app and 21 JSP files are pre-compiled.
- Going to the output directory a JAR is created with the output servlet classes (note that a web fragment is used, -webfrg option, which generates a standard fragment file that is automatically loaded by the Wildfly server, the Servlets listed in that fragment are added to the app).
- Finally the new JAR file is moved into the WEB-INF/lib folder of the application and the WAR is re-created.
- After re-deploying the app, no JSP is generated, now the the pre-compiled classes are being used.
Here I copy the full commands done in the video.
# build the hotel application
mvn clean package
cd target
mkdir JavaWeb0_war
cd JavaWeb0_war
jar xvf ../JavaWeb0.war
# execute jspc and re-build the app
cd ${JSPC_HOME}
mkdir -p /tmp/lala/META-INF
mvn exec:java -Dexec.args="-v -p pre.compiled.jsps -d /tmp/lala -webapp /path/to/hotel-management-system/JavaWeb/target/JavaWeb0_war -webfrg /tmp/lala/META-INF/web-fragment.xml"
cd /tmp/lala
jar cvf precompiled-jsp.jar *
cd /path/to/hotel-management-system/JavaWeb/target/JavaWeb0_war
mv /tmp/lala/precompiled-jsp.jar WEB-INF/lib/
jar cMvf ../JavaWeb0.war *
It is just a simple example, but it is a generic application too, and it works. So, if you have an old JSP application and you are interested in pre-compiling JSPs in a newer Wildfly, you can just try the JspC project. Use the profile for your Wildfly version. If your server is a different version, please create the profile by yourself, you only need to check the dependencies of your Wildfly (look for the library versions in the modules directory), locate them in maven and add them to a new profile. Let's see if people are interested in this project.
Pre-compiled regards!
Saturday, November 30. 2019
SAML assertion replay in keycloak
Today's entry is again about keycloak but this time I am going to use the SAML protocol. This protocol is a very old web Single Sign On (SSO) protocol in which XML information is sent and signed between the peers. The entry is motivated because in the just released version 8.0.0 the SAML assertion can be retrieved from the logged principal and can be replayed. My idea was testing this feature using the assertion to call a CXF endpoint protected with Web Services Security (WSS). The endpoint will be configured to use the SAML assertion to validate the user. If you remember a previous series about CXF/WSS was presented in the blog, but using certificates instead of SAML.
As usual the entry summarizes the steps I followed to perform this PoC (Proof of Concept) in detail.
Download and install the keycloak server.
wget https://downloads.jboss.org/keycloak/8.0.0/keycloak-8.0.0.zip unzip keycloak-8.0.0.zip cd keycloak-8.0.0/bin ./standalone.sh
Go to the default location (http://localhost:8080) and create the initial admin user.
Now the server will be configured to use a self-signed certificate (secure https is a must for SAML). Create the server and trusted key-stores.
cd ../standalone/configuration keytool -genkeypair -keystore keystore.jks -dname "CN=localhost, OU=test, O=test, L=test, C=test" -keypass XXXX -storepass XXXX -keyalg RSA -alias localhost -validity 10000 -ext SAN=dns:localhost,ip:127.0.0.1 keytool -export -keystore keystore.jks -alias localhost -file localhost.cer keytool -import -trustcacerts -alias localhost -file localhost.cer -keystore cacerts -storepass changeit
Configure the server to use the previous certificate using the CLI interface:
cd ../../bin ./jboss-cli.sh --connect /subsystem=elytron/key-store=localhost:add(type=jks, relative-to=jboss.server.config.dir, path=keystore.jks, credential-reference={clear-text=XXXX} /subsystem=elytron/key-manager=localhost-manager:add(key-store=localhost, alias-filter=localhost, credential-reference={clear-text=XXXX}) /subsystem=elytron/server-ssl-context=localhost-context:add(key-manager=localhost-manager, protocols=["TLSv1.2"]) batch /subsystem=undertow/server=default-server/https-listener=https:undefine-attribute(name=security-realm) /subsystem=undertow/server=default-server/https-listener=https:write-attribute(name=ssl-context, value=localhost-context) run-batch
We also configure the self-signed certificate as trusted for the whole JVM using a system property.
/system-property=javax.net.ssl.trustStore:add(value="${jboss.server.config.dir}/cacerts")
Perform the same exact steps for wildfly (installation and certificate, use the same keystores, because both servers are going to run in the same localhost hostname). The wildfly server will be started with an offset of 10000.
./standalone.sh -Djboss.socket.binding.port-offset=10000
At this point we have a keycloak server in 8443 and a wildfly server in 18443 port. Both use https and the same certificate. They also trust in each other. So now the keycloak-adapters for SAML should be installed in the wildfly server.
wget https://downloads.jboss.org/keycloak/8.0.0/adapters/saml/keycloak-saml-wildfly-adapter-dist-8.0.0.zip cd ${WILDFLY_HOME} unzip /path/to/keycloak-saml-wildfly-adapter-dist-8.0.0.zip cd bin ./standalone.sh -Djboss.socket.binding.port-offset=10000 ./jboss-cli.sh --connect controller=localhost:19990 --file=adapter-elytron-install-saml.cli
Now it is the time to configure the SAML client. The idea is simple, we will have a SAML protected application using the keycloak adapter. That application will call a CXF endpoint that will be configured to process the SAML assertion and validate the user. For simplicity I am going to use the same application (the web service endpoint will be located in the same app). Go to the keycloak console and select clients and create a new SAML client. The client ID should be the endpoint location https://localhost:18443/keycloak-cxf-saml/echo-service/echo (later I will explain this limitation). Check the option Sign Assertions to ON, this way the assertion is also signed and it can be replayed in a secure way. My client settings are presented below.
For the configuration of the CXF/wss4j endpoint, the realm certificate will be needed. So go to Realm Settings, select Keys tab and click on the Certificate button of the RSA key. Copy the certificate value and create a file server.cer with the typical certificate header and footer.
-----BEGIN CERTIFICATE----- <copied certificate from keycloak console> -----END CERTIFICATE-----
And finally import it into a JKS as a trusted certificate. This will be the store that should be configured later to validate SAML signatures by the web service endpoint.
keytool -import -trustcacerts -alias saml -file server.cer -keystore server.jks -storepass YYYY
Let's start with the development. The first thing to do is configuring the keycloak SAML SSO. For that just obtain the initial template from the console. Go again to clients, select our client and click on tab Installation. Choose the option Keycloak SAML Adapter keycloak-saml.xml and a template configuration can be downloaded. This configuration should be placed in the file WEB-INF/keycloak-saml.xml inside the WAR application bundle. I customized the configuration file like below.
<keycloak-saml-adapter> <SP entityID="https://localhost:18443/keycloak-cxf-saml/echo-service/echo" sslPolicy="ALL" keepDOMAssertion="true" nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" logoutPage="/logout.jsp"> <Keys> <Key signing="true"> <PrivateKeyPem> MII... </PrivateKeyPem> <CertificatePem> MII... </CertificatePem> </Key> </Keys> <IDP entityID="idp" signatureAlgorithm="RSA_SHA256" signatureCanonicalizationMethod="http://www.w3.org/2001/10/xml-exc-c14n#"> <SingleSignOnService signRequest="true" validateResponseSignature="true" validateAssertionSignature="false" requestBinding="POST" bindingUrl="https://localhost:8443/auth/realms/master/protocol/saml"/> <SingleLogoutService signRequest="true" signResponse="true" validateRequestSignature="true" validateResponseSignature="true" requestBinding="POST" responseBinding="POST" postBindingUrl="https://localhost:8443/auth/realms/master/protocol/saml" redirectBindingUrl="https://localhost:8443/auth/realms/master/protocol/saml"/> </IDP> </SP> </keycloak-saml-adapter>
The web.xml is configured to protect the application but not the endpoint. The CXF web service is secured via wss4j, so no web security should be applied to it.
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <security-constraint> <web-resource-collection> <web-resource-name>Protect all application</web-resource-name> <url-pattern>/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>*</role-name> </auth-constraint> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint> <security-constraint> <web-resource-collection> <web-resource-name>The WS endpoint is public</web-resource-name> <url-pattern>/echo-service/*</url-pattern> </web-resource-collection> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint> <login-config> <auth-method>KEYCLOAK-SAML</auth-method> <realm-name>this is ignored currently</realm-name> </login-config> <security-role> <description>Role required to log in to the Application</description> <role-name>*</role-name> </security-role> <session-config> <session-timeout>30</session-timeout> </session-config> </web-app>
The application is configuring the KEYCLOAK login to use the SSO. The full application (/*) is protected, but the WS endpoint (/echo-service/*) is excluded (everyone can access the endpoint at web level). Besides any authenticated user can access the application (role *) and secure communication (https) is compulsory (transport is defined as confidential).
Time to create the web service endpoint. This part is very similar to the previous entry about WSS and certificates that I commented at the beginning. So you can review it for more information about this subject, because it is really a bit complicated. The simple echo web service is developed like this.
@Stateless @WebService(name = "echo", targetNamespace = "http://es.rickyepoderi.sample/ws", serviceName = "echo-service") @Policy(placement = Policy.Placement.BINDING, uri = "WssSamlV20Token11.xml") @SOAPBinding(style = SOAPBinding.Style.RPC) @EndpointConfig(configFile = "WEB-INF/jaxws-endpoint-config.xml", configName = "Custom WS-Security Endpoint") public class Echo { @WebMethod public String echo(String input) { Message message = PhaseInterceptorChain.getCurrentMessage(); SecurityContext context = message.get(SecurityContext.class); Principal caller = null; if (context != null) { caller = context.getUserPrincipal(); } return (caller == null? "null" : caller.getName()) + " -> " + input; } }
The endpoint is just an echo service but it obtains the user from CXF. If you check it, the web service is configured with a specific WSS policy WssSamlV20Token11.xml file and a configuration file WEB-INF/jaxws-endpoint-config.xml. The next points deal with those files.
- The most complicated part is creating the policy to request a SAML assertion for the web service. For that I checked some of the CXF tests in order to obtain samples of SAML policies and, finally, the following WssSamlV20Token11.xml file is used.
<?xml version="1.0" encoding="UTF-8" ?> <wsp:Policy wsu:Id="SecurityPolicy" xmlns:wsp="http://www.w3.org/ns/ws-policy" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:sp="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy"> <wsp:ExactlyOne> <wsp:All> <sp:TransportBinding> <wsp:Policy> <sp:TransportToken> <wsp:Policy> <sp:HttpsToken> <wsp:Policy/> </sp:HttpsToken> </wsp:Policy> </sp:TransportToken> <sp:Layout> <wsp:Policy> <sp:Lax/> </wsp:Policy> </sp:Layout> <sp:AlgorithmSuite> <wsp:Policy> <sp:Basic256/> </wsp:Policy> </sp:AlgorithmSuite> </wsp:Policy> </sp:TransportBinding> <sp:SupportingTokens> <wsp:Policy> <sp:SamlToken sp:IncludeToken="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy/IncludeToken/AlwaysToRecipient"> <wsp:Policy> <sp:WssSamlV20Token11/> </wsp:Policy> </sp:SamlToken> </wsp:Policy> </sp:SupportingTokens> </wsp:All> </wsp:ExactlyOne> </wsp:Policy>
The policy is requesting https protocol and a SAML version 2.0 token. I know this part is horribly complicated but this is WSS security, not an easy world.
In order to configure the validation of the assertion the file jaxws-endpoint-config.xml is provided.
<?xml version="1.0" encoding="UTF-8"?> <jaxws-config xmlns="urn:jboss:jbossws-jaxws-config:4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:javaee="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="urn:jboss:jbossws-jaxws-config:4.0 schema/jbossws-jaxws-config_4_0.xsd"> <endpoint-config> <config-name>Custom WS-Security Endpoint</config-name> <property> <property-name>ws-security.signature.properties</property-name> <property-value>server.properties</property-value> </property> </endpoint-config> </jaxws-config>
The server.properties contains the properties to access the keystore to validate the signature of the SAML assertion.
org.apache.wss4j.crypto.provider=org.apache.ws.security.components.crypto.Merlin org.apache.wss4j.crypto.merlin.keystore.type=jks org.apache.wss4j.crypto.merlin.keystore.password=YYYY org.apache.wss4j.crypto.merlin.keystore.file=server.jks
And that server.jks is just the previous keystore created in step 8 using the keycloak certificate in the realm. So, in summary, the endpoint is configured to request a SAML token and the certificate used by keycloak is configured as trusted for the validation. This way the wss4j implementation can check the SAML assertion received and validate its signature. If everything is OK the user will be recovered by the echo service and returned.
- And here it comes the final part. How is the SAML assertion retrieved and used to call the endpoint? For that I created a simple EchoServlet that gets the assertion from the special keycloak principal and calls the endpoint.
WSClient client = new WSClient(request); out.println(client.callEcho(((SamlPrincipal) request.getUserPrincipal()).getAssertionDocument(), input));
The actual code in the servlet is bit more complicated because I decided to check the validity of the assertion. The SAML assertion usually has some time constraints to not use the same assertion forever. If the assertion is expired the application forces a re-login of the user. But I decided to not add here the details to not complicate even more the explanation.
The CXF implementation for saml uses a callback handler that should provide the assertion to be sent by the client (the handler fills a SAMLCallback with the assertion). In this case it is extremely easy because it is just there inside the principal. So I created KeycloakSamlCallbackHandler that just wraps the assertion to give it to the CXF system in order to attach it to the SOAP message.
public class KeycloakSamlCallbackHandler implements CallbackHandler { Document assertion; public KeycloakSamlCallbackHandler(Document assertion) { this.assertion = assertion; } @Override public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { if (callbacks != null) { for (Callback callback1 : callbacks) { if (callback1 instanceof SAMLCallback) { SAMLCallback callback = (SAMLCallback) callback1; callback.setAssertionElement(assertion.getDocumentElement()); } } } } }
And the WSClient just puts the callback to the call context. This way the CXF implementation can retrieve the SAML assertion and add it to the SOAP message.
public String callEcho(Document assertion, String input) { EchoService service = new EchoService(url); Echo echo = service.getEchoPort(); // Properties for WS-Security configuration ((BindingProvider)echo).getRequestContext().put(SecurityConstants.SAML_CALLBACK_HANDLER, new KeycloakSamlCallbackHandler(assertion)); // call the endpoint return echo.echo(input); }
Here it is important to add the option keepDOMAssertion to true, because this way the DOM document of the original assertion is stored in the SAML principal and can be recovered by the application to replay it. More information about the SAML configuration for adapters in the keycloak documentation
And that is all. Very long and complicated setup, but it shows that you can replay a SAML assertion. I decided to use CXF/wss4j because it is another complete different SAML implementation (it uses opensaml internally). Here it is a video that shows that it really works. When I access the application the browser is redirected to the keycloak login page. The typical SAML dance is accomplished and finally the browser accesses the application index. The remote user, roles and even the assertion are presented. Check that the assertion is signed and it has some restrictions (time and audience constraints). When the web service is called, the echo works and the message is returned with the user correctly identified by the CXF implementation.
But there are some issues here. At least two new features are needed in order to have a proper assertion replay. The first problem is the time restrictions that I commented before. In keycloak the different times are obtained from the Realm Settings, inside the Tokens tab. The lifespans used are Access Token Lifespan and Client login timeout (the SSO Session Max is also used but this one is very long by default and therefore it is not problematic). Those two times are usually very short (one minute) because of OIDC, and they are too short for SAML. So if you really need to use the assertion replay those values need to be increased to cover your needs. The real problem is that SAML clients cannot override the realm settings (OIDC ones can define a specific access token lifespan).
The second issue is the audience. A SAML assertion can also define which endpoints are allowed to use it. This is done by the audience tag (a list of URLs that are allowed to consume the assertion). By default the keycloak server constructs the assertion with the audience limited to the client ID (only that client can use this assertion). This fact is absolutely limitating the assertion replay. If you remember in step 7 the client was created with a specific ID, which is exactly the URL of the echo endpoint. That was a very nasty trick. This makes both (app and CXF endpoint) use the same ID and both pass the audience validation. But obviously if you want to send the assertion to a second endpoint it would fail, the implementation would check the audience constraint and would complain that its own URL is not in the list. Maybe CXF/wss4j can be configured to not check the audience but that is weird, audience is there for a reason.
Therefore I filed two new feature requests for keycloak (JIRA 12000 and 12001) and I am working on them. There is room for other improvements here but, at least, with those two new settings the assertion replay can be used. You can download the full maven project for the PoC application from here.
Best regards!
Comments