Issue with SAML Response Parsing: XML Parsing Error in Keycloak 22.0.4 - The entity name must immediately follow the '&' in the entity reference

Hi,

I’m experiencing a challenging issue with SAML response parsing in Keycloak version 22.0.4 and would greatly appreciate any insights or suggestions.

Issue Description: When receiving a SAML response from the German portal “Elster” / “Mein Unternehmenskonto” (acting as the identity provider), Keycloak logs a fatal XML parsing error, which i received from the logs:

[Fatal Error] :1:24: The entity name must immediately follow the '&' in the entity reference.
2023-12-21 09:53:34,838 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (executor-thread-5) Uncaught server error: org.keycloak.broker.provider.IdentityBrokerException: Could not process response from SAML identity provider.
More verbose error message

[Fatal Error] :1:24: The entity name must immediately follow the ‘&’ in the entity reference.

2023-12-21 10:48:24,621 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (executor-thread-22) Uncaught server error: org.keycloak.broker.provider.IdentityBrokerException: Could not process response from SAML identity provider
at org.keycloak.broker.saml.SAMLEndpoint$Binding.handleLoginResponse(SAMLEndpoint.java:596)
at org.keycloak.broker.saml.SAMLEndpoint$Binding.handleSamlResponse(SAMLEndpoint.java:679)
at org.keycloak.broker.saml.SAMLEndpoint$Binding.execute(SAMLEndpoint.java:278)
at org.keycloak.broker.saml.SAMLEndpoint.postBinding(SAMLEndpoint.java:189)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:154)
at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:118)
at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:560)
at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:452)
at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$2(ResourceMethodInvoker.java:413)
at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:321)
at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:415)
at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:378)
at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:174)
at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:142)
at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:168)
at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:131)
at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:33)
at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:429)
at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:240)
at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:154)
at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:321)
at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:157)
at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:229)
at io.quarkus.resteasy.runtime.standalone.RequestDispatcher.service(RequestDispatcher.java:82)
at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.dispatch(VertxRequestHandler.java:147)
at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:84)
at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:44)
at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1284)
at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:177)
at io.vertx.ext.web.impl.RoutingContextWrapper.next(RoutingContextWrapper.java:200)
at io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers$1.handle(HttpServerCommonHandlers.java:58)
at io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers$1.handle(HttpServerCommonHandlers.java:36)
at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1284)
at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:177)
at io.vertx.ext.web.impl.RoutingContextWrapper.next(RoutingContextWrapper.java:200)
at org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter.lambda$createBlockingHandler$0(QuarkusRequestFilter.java:82)
at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:576)
at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:833)

Caused by: java.lang.RuntimeException: Could not parse xml element
at org.keycloak.broker.saml.mappers.XPathAttributeMapper.lambda$applyXPath$4(XPathAttributeMapper.java:219)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:762)
at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:276)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:762)
at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:276)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.HashMap$KeySpliterator.forEachRemaining(HashMap.java:1707)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
at org.keycloak.broker.saml.mappers.XPathAttributeMapper.findAttributeValuesInContext(XPathAttributeMapper.java:241)
at org.keycloak.broker.saml.mappers.XPathAttributeMapper.preprocessFederatedIdentity(XPathAttributeMapper.java:147)
at org.keycloak.services.resources.IdentityBrokerService.lambda$authenticated$0(IdentityBrokerService.java:524)
at java.base/java.util.HashMap$KeySpliterator.forEachRemaining(HashMap.java:1707)
at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:762)
at org.keycloak.services.resources.IdentityBrokerService.authenticated(IdentityBrokerService.java:521)
at org.keycloak.broker.saml.SAMLEndpoint$Binding.handleLoginResponse(SAMLEndpoint.java:592)

… 46 more

Caused by: ParsingException [location=null]org.keycloak.saml.common.exceptions.ParsingException: PL00074: Parsing Error:The entity name must immediately follow the ‘&’ in the entity reference
at org.keycloak.saml.common.DefaultPicketLinkLogger.parserError(DefaultPicketLinkLogger.java:486)
at org.keycloak.saml.common.util.DocumentUtil.getDocument(DocumentUtil.java:142)
at org.keycloak.broker.saml.mappers.XPathAttributeMapper.lambda$applyXPath$4(XPathAttributeMapper.java:213)

… 73 more

Caused by: org.xml.sax.SAXParseException; lineNumber: 1; columnNumber: 24; The entity name must immediately follow the ‘&’ in the entity reference
at java.xml/com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:262)
at java.xml/com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:342)
at org.keycloak.saml.common.util.DocumentUtil.getDocument(DocumentUtil.java:138)

… 74 more

Context:

  • The data sent from the identity provider includes various fields, some with special characters like ‘ö’, ‘ü’, ‘ß’ and ‘&’. For instance, the street name contains ‘ö’ and ‘ß’, and the company name includes ‘&’.
  • I have already ensured that the JVM is using UTF-8 encoding (JAVA_OPTS set to -Dfile.encoding=UTF-8).
  • Keycloak version: 22.0.4.
  • Keycloak is deployed in a kubernetes cluster using a helm configuration.

Information that should be sent from the IdP

This data should be sent from the german portal “Elster” / “Mein Unternehmenskonto”, which is the identity provider which sends the response. I replaced some regular, non special characters with abc or 123.

Firmenname: abcde - abcd & Abcdefgh_12345
Registernummer: 12345678901
Registerart: HRB
Registergericht: Amtsgericht Bersenbrück
Rechtsform: Ges. mit beschr. Haftung und Co.KG
Anschrift: Schönmannstraße 123, 12345 Town, Deutschland
Steuernummer (wird nicht weitergegeben): 1234567890123
Herkunft der Daten: Finanzamt

In English:
Company name: abcde - abcd & Abcdefgh_12345
Register number: 12345678901
Register type: HRB
Register court: Amtsgericht Bersenbrück
Legal form: Ges. mit beschr. Haftung und Co.KG
Legal address: Schönmannstraße 123, 12345 Town, Deutschland
Tax number (will not be passed on): 1234567890123
Origin of the data: Finanzamt

SAML Response Details:

  • I used SAML-tracer for Chrome to capture the SAML response.
  • The response includes standard SAML and XML signatures, status, and an encrypted assertion.

this is my saml response that i recorded with saml-tracer for chrome.

<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
                 Destination="mydest/muk-saml/endpoint"
                 ID="myid"
                 InResponseTo="ID_myid"
                 IssueInstant="2023-12-21T09:48:24.022Z"
                 Version="2.0"
                 >
    <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://e4k-portal.een.elster.de</saml2:Issuer>
    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:SignedInfo>
            <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            <ds:SignatureMethod Algorithm="http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1" />
            <ds:Reference URI="#_myReferenceUri">
                <ds:Transforms>
                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                </ds:Transforms>
                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
                <ds:DigestValue>myDigestValue</ds:DigestValue>
            </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>mysigval</ds:SignatureValue>
        <ds:KeyInfo>
            <ds:X509Data>
                <ds:X509Certificate>mycert</ds:X509Certificate>
            </ds:X509Data>
        </ds:KeyInfo>
    </ds:Signature>
    <saml2p:Status>
        <saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
    </saml2p:Status>
    <saml2:EncryptedAssertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
        <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"
                            Id="myId"
                            Type="http://www.w3.org/2001/04/xmlenc#Element"
                            >
            <xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes256-gcm" />
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <xenc:EncryptedKey Id="myId">
                    <xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#rsa-oaep">
                        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
                        <xenc11:MGF xmlns:xenc11="http://www.w3.org/2009/xmlenc11#"
                                    Algorithm="http://www.w3.org/2009/xmlenc11#mgf1sha256"
                                    />
                    </xenc:EncryptionMethod>
                    <xenc:CipherData>
                        <xenc:CipherValue>myciphervalue</xenc:CipherValue>
                    </xenc:CipherData>
                </xenc:EncryptedKey>
            </ds:KeyInfo>
            <xenc:CipherData>
                <xenc:CipherValue>

Questions:

Are there recommended configurations or approaches to handle special characters in SAML responses, especially when dealing with non-English characters and symbols like ‘&’?

Has anyone experienced similar XML parsing issues with special characters in SAML responses in Keycloak?

Any advice on further diagnosing or logging this specific issue to isolate the problematic part of the SAML response?

Thank you in advance for any help.
Let me know if there’s any more information i can provide.

Best regards,
Lukas

1 Like