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!
Sunday, November 10. 2019
Testing eclipse/mojarra JSF implementation
These days I have been involved in several issues about the old Java Server Faces (JSF) technology. Currently wildfly is using the former Oracle driven mojarra implementation that, as the rest of the JavaEE specification, now is controlled by the eclipse foundation. Working with the code the major issue I have faced has been launching the tests. Although it is explained in the project, I spent some time to fully understand the process, so better if I collect the commands here.
As the documentation says the glassfish javaee server is needed, but the version is specially important when testing the different branches. For example if you want to test branch 2.3 (2.3.14 right now) you can install glassfish 5.0, but if you want to test the master branch (future 3.0 version) you need glassfish 5.1 from the same eclipse project.
Let's assume we want to test the master branch. Then the glassfish 5.1 should be installed in your computer.
unzip glassfish-5.1.0.zip mv glassfish5 glassfish5.1
Now just clone and compile the project (default maven usage).
mvn clone https://github.com/eclipse-ee4j/mojarra.git cd mojarra mvn clean install
In order to launch a specific app test the documentation can just be followed, using the properties to locate the glassfish instance. The test application test/javaee7/cdiInitDestroyEvent is used as example.
cd test/javaee7/cdiInitDestroyEvent
Copy the compiled JSF jar file into the glassfish.
mvn -N -Pglassfish-patch -Dglassfish.cargo.home=/home/rmartinc/glassfish5.1 -Dglassfish.patch.home=/home/rmartinc/glassfish5.1 validate
Start the domain with the new JSF version.
mvn -N -Pglassfish-cargo -Dglassfish.cargo.home=/home/rmartinc/glassfish5.1 -Dglassfish.patch.home=/home/rmartinc/glassfish5.1 cargo:start
Deploy the application for the test.
mvn -Pglassfish-cargo -Dglassfish.cargo.home=/home/rmartinc/glassfish5.1 -Dglassfish.patch.home=/home/rmartinc/glassfish5.1 cargo:redeploy
Launch the tests against the app just deployed.
mvn -Pintegration -Dglassfish.cargo.home=/home/rmartinc/glassfish5.1 -Dglassfish.patch.home=/home/rmartinc/glassfish5.1 verify
If everything goes OK you can stop the server.
mvn -N -Pglassfish-cargo -Dglassfish.cargo.home=/home/rmartinc/glassfish5.1 -Dglassfish.patch.home=/home/rmartinc/glassfish5.1 cargo:stop
The documentation also comments that all the tests inside a directory can be executed. To do that the same steps are done but in the upper directory. For example the javaee7 directory is used to run all the tests in that version of the JavaEE standard (the -fn parameter is passed to not stop tests on error, I want all of them executed even if there are errors):
cd test/javaee7 mvn -N -Pglassfish-patch -Dglassfish.cargo.home=/home/rmartinc/glassfish5.1 -Dglassfish.patch.home=/home/rmartinc/glassfish5.1 validate mvn -N -Pglassfish-cargo -Dglassfish.cargo.home=/home/rmartinc/glassfish5.1 -Dglassfish.patch.home=/home/rmartinc/glassfish5.1 cargo:start mvn -Pglassfish-cargo -Dglassfish.cargo.home=/home/rmartinc/glassfish5.1 -Dglassfish.patch.home=/home/rmartinc/glassfish5.1 cargo:redeploy mvn -fn -Pintegration -Dglassfish.cargo.home=/home/rmartinc/glassfish5.1 -Dglassfish.patch.home=/home/rmartinc/glassfish5.1 verify
Finally there are some test directories that do not run OK. But then I realized that, in the automatic tests that are launched when a Pull Request is received, only the following folders are executed: htmlunitaware, javaee6, javaee6web, javaee7, javaee8, servlet31, servlet40. I can confirm that servlet30 also runs without any problem but all the others report an issue at some point (I think just because they are not used and therefore not maintained, except the cluster folder, that needs several instances and it is not explained how to test it, maybe it just needs a glassfish cluster deployed in some specific ports but I did not check it deeply).
And that's all. Very simple entry. The documentation also comments that the tests can be run with different options (different JSF configurations). Indeed my main problem was knowing what glassfish to use for the tests, because I initially tried with 5.0 and that does not work for the master branch (you need the new version 5.1 to be compatible). Maybe I'm just repeating the same statements that there are in the documentation, but I prefer to write a summary here with the exact commands I used, just in case I need to work again on the mojarra implementation.
Happy testing!
Comments