keycloak / keycloak

Open Source Identity and Access Management For Modern Applications and Services
https://www.keycloak.org
Apache License 2.0
23.33k stars 6.76k forks source link

SAML decryption fails if keycloak.saml.deprecated.encryption flag is set #24652

Closed rtheys closed 11 months ago

rtheys commented 1 year ago

Before reporting an issue

Area

identity-brokering

Describe the bug

Keycloak 21 included changes to the SAML SP metadata to require a different encryption key. To keep backwards compatibility with identity providers that were using the older metadata, the keycloak.saml.deprecated.encryption flag could be set.

If this flag is set, and an identity provider is updated to use the new metadata (and the new key for encryption), authentication fails and the following message is logged:

2023-11-09 09:55:08,616 WARN  [org.keycloak.broker.saml.SAMLEndpoint] (executor-thread-1) Not possible to decrypt SAML assertion. Please check realm keys of usage ENC in the realm 'test' and make sure there is a key able to decrypt the assertion encrypted by identity provider 'testidp': org.keycloak.saml.common.exceptions.ProcessingException: PL00102: Processing Exception:
        at org.keycloak.saml.common.DefaultPicketLinkLogger.processingError(DefaultPicketLinkLogger.java:165)
        at org.keycloak.saml.processing.core.util.XMLEncryptionUtil.decryptElementInDocument(XMLEncryptionUtil.java:308)
        at org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil.decryptAssertion(AssertionUtil.java:612)
        at org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil.decryptAssertion(AssertionUtil.java:588)
        at org.keycloak.broker.saml.SAMLEndpoint$Binding.handleLoginResponse(SAMLEndpoint.java:446)
        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:840)
Caused by: java.lang.RuntimeException: Cannot decrypt element in document
        at org.keycloak.saml.processing.core.util.XMLEncryptionUtil.decryptElementInDocument(XMLEncryptionUtil.java:282)
        ... 49 more
        Suppressed: org.apache.xml.security.encryption.XMLEncryptionException: Key is too long for unwrapping
Original Exception was java.security.InvalidKeyException: Key is too long for unwrapping
                at org.apache.xml.security.encryption.XMLCipher.decryptKey(XMLCipher.java:1495)
                at org.keycloak.saml.processing.core.util.XMLEncryptionUtil.decryptElementInDocument(XMLEncryptionUtil.java:295)
                ... 49 more
        Caused by: java.security.InvalidKeyException: Key is too long for unwrapping
                at java.base/com.sun.crypto.provider.RSACipher.engineUnwrap(RSACipher.java:453)
                at java.base/javax.crypto.Cipher.unwrap(Cipher.java:2590)
                at org.apache.xml.security.encryption.XMLCipher.decryptKey(XMLCipher.java:1493)
                ... 50 more

If keycloak is restarted without the flag set, authentication works as expected.

To work around this we would have to make sure all identity providers are updated at the same time, after which we can remove the flag. If multiple identity providers need to be contacted, this can take quite a while and depending on the state of the flag, some of them will be broken until all of them are using the new metadata and the flag is removed.

I believe keycloak should also try to decrypt the saml assertions using the new key (for encryption) if the flag is set (and not just the key for signing).

This is also discussed in https://github.com/keycloak/keycloak/discussions/19672

Version

22.0.5

Expected behavior

Even with -Dkeycloak.saml.deprecated.encryption enabled, decryption works with both the new key (with usage encryption) and the key that should be used for signing.

Actual behavior

Authentication fails for identity provides that are using the new key (usage encryption). No output is produced in the browser and the following message is logged in the keycloak log:

2023-11-09 09:55:08,616 WARN  [org.keycloak.broker.saml.SAMLEndpoint] (executor-thread-1) Not possible to decrypt SAML assertion. Please check realm keys of usage ENC in the realm 'test' and make sure there is a key able to decrypt the assertion encrypted by identity provider 'testidp': org.keycloak.saml.common.exceptions.ProcessingException: PL00102: Processing Exception:
        at org.keycloak.saml.common.DefaultPicketLinkLogger.processingError(DefaultPicketLinkLogger.java:165)
        at org.keycloak.saml.processing.core.util.XMLEncryptionUtil.decryptElementInDocument(XMLEncryptionUtil.java:308)
        at org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil.decryptAssertion(AssertionUtil.java:612)
        at org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil.decryptAssertion(AssertionUtil.java:588)
        at org.keycloak.broker.saml.SAMLEndpoint$Binding.handleLoginResponse(SAMLEndpoint.java:446)
        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:840)
Caused by: java.lang.RuntimeException: Cannot decrypt element in document
        at org.keycloak.saml.processing.core.util.XMLEncryptionUtil.decryptElementInDocument(XMLEncryptionUtil.java:282)
        ... 49 more
        Suppressed: org.apache.xml.security.encryption.XMLEncryptionException: Key is too long for unwrapping
Original Exception was java.security.InvalidKeyException: Key is too long for unwrapping
                at org.apache.xml.security.encryption.XMLCipher.decryptKey(XMLCipher.java:1495)
                at org.keycloak.saml.processing.core.util.XMLEncryptionUtil.decryptElementInDocument(XMLEncryptionUtil.java:295)
                ... 49 more
        Caused by: java.security.InvalidKeyException: Key is too long for unwrapping
                at java.base/com.sun.crypto.provider.RSACipher.engineUnwrap(RSACipher.java:453)
                at java.base/javax.crypto.Cipher.unwrap(Cipher.java:2590)
                at org.apache.xml.security.encryption.XMLCipher.decryptKey(XMLCipher.java:1493)
                ... 50 more

How to Reproduce?

  1. Set up a keycloak realm and configure an identity provider for brokering.
  2. Restart keycloak and set the -Dkeycloak.saml.deprecated.encryption=true flag
  3. Try to log in on a service that uses this keycloak instance and select to authenticate using the brokered identity provider.
  4. Log in on the brokered identity provider

When the response from the brokered identity provider is sent to keycloak, nothing is shown in the browser and the keycloak log contains the message mentioned above.

Anything else?

No response

rmartinc commented 12 months ago

@rtheys Yes, it's like you say, checking the code it's one or the other way.

@mhajas Maybe we should always try with the signing key if the compatibility flag is set... WDYT?

rmartinc commented 12 months ago

I have prepared a simple fix in this branch: https://github.com/rmartinc/keycloak/tree/issue-24652 The old RSA key is always added at the end if the deprecated mode is enabled. CI passes OK.

bizkaipc commented 11 months ago

Good morning, I am new to the world of keycloak, I have recently updated the company keycloak from version 17.0.1 to version 22.0.5.

I am having exactly the problem described here, I saw that there is a fix and I have downloaded it, but I don't know how I can install it, could you help me.

Thank you very much in advance.

rmartinc commented 11 months ago

There is no fix for this exact problem, it's just an idea. If you have upgraded you probably just need to put the system property -Dkeycloak.saml.deprecated.encryption=true at startup. Read the release notes to understand what is happening with the certs.

This issue is about the problem you face when you start re-assigning the certificates for encryption, and you have clients with the new enc cert and other clients with the old signing keys.

bizkaipc commented 11 months ago

Good afternoon,

I have set the parameter -Dkeycloak.saml.deprecated.encryption=true and it doesn't work either, could you tell me what tests I can do, I can also send the configuration of the broker realm and the client realm in case there is something missing to configure.

rmartinc commented 11 months ago

@bizkaipc The change done in issue https://github.com/keycloak/keycloak/issues/13606 and it's since version 21.0.0. The idea is that now the key that identity provider (SP) uses to decrypt the SAML is the one specified by the encryption alg in the document. Previously it was the current RSA key, the same one used for signing (RS256). The system prop reverts to do the same than before.

So you have to ask to the IDP what key/certificate they are using to encrypt the requests/responses to your SP and check the alg in the SAML document too. Then check where you have defined it in your realm. But if the IDP has not changed anything, it should be using the same old RS256 key, and that's why I said that the system prop was necessary.

mhajas commented 11 months ago

I have prepared a simple fix in this branch: https://github.com/rmartinc/keycloak/tree/issue-24652 The old RSA key is always added at the end if the deprecated mode is enabled. CI passes OK.

@rmartinc I like the idea. We didn't consider a case when we need to combine both the deprecated and the new approach to encryption.

@rtheys Any chance you could try the fix from @rmartinc in your environment so we can check whether the fix works?

One more thing, this flag is about to be removed in Keycloak 24 (https://github.com/keycloak/keycloak/issues/16726) which means this issue needs to be fixed only for 23.0.x.

rtheys commented 11 months ago

I have prepared a simple fix in this branch: https://github.com/rmartinc/keycloak/tree/issue-24652 The old RSA key is always added at the end if the deprecated mode is enabled. CI passes OK.

@rmartinc I like the idea. We didn't consider a case when we need to combine both the deprecated and the new approach to encryption.

@rtheys Any chance you could try the fix from @rmartinc in your environment so we can check whether the fix works?

@mhajas I've applied @rmartinc his proposed fix on 22.0.5 and I can confirm that it works:

with -Dkeycloak.saml.deprecated.encryption=true NOT set:

with -Dkeycloak.saml.deprecated.encryption=true set:

Regards, Rik

mhajas commented 11 months ago

Thank you @rtheys! @rmartinc Could you please send a PR with the changes?