Discourse, Keycloak, SAML - est-il impossible de configurer ?

Bonjour !
Je fais cela depuis presque une semaine, mais je n’arrive pas à le configurer ! J’ai essayé un million d’options ! S’il vous plaît, aidez-moi…

Donc, il y a une instance de test de Keycloak, adresse - http://10.5.40.19:8081. J’y ai créé un realm CUBA et un client SAML :


Comme vous pouvez le voir sur les captures d’écran, il y a un forum de test Discourse à l’adresse https://cuba-test-forum-en.demo.haulmont.com/discuss
Selon la documentation du plugin discourse-saml, l’URL pour la connexion SSO sur mon forum de test sera comme ceci - https://cuba-test-forum-en.demo.haulmont.com/discuss/auth/saml/callback

Paramètres SAML dans le fichier app.yml :

DISCOURSE_RELATIVE_URL_ROOT: /discuss

DISCOURSE_SAML_TITLE: "Exemple SAML"
DISCOURSE_SAML_TARGET_URL: "http://10.5.40.19:8081/auth/realms/CUBA/protocol/saml"
DISCOURSE_SAML_AUTO_CREATE_ACCOUNT: 1
DISCOURSE_SAML_CERT_FINGERPRINT: "0D:9A:46:68:6B:CA:A6:41:7A:F5:08:18:C4:01:47:33:75:AD:2C:EF"
DISCOURSE_SAML_CERT: "-----BEGIN CERTIFICATE-----
MIICmzCCAYMCBgF0wEV2uzANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZlbnNhbWwwHhcNMjAwOTI0MTMxODMxWhcNMzAwOTI0MTMyMDExWjARMQ8wDQYDVQQDDAZlbnNhbWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCclA1o6Q3jcpqpxHtx79nrDCBfUIHUhZP+4+wLlV+7nel7Kts5Tas6jSToeptueWxRLKadWtV5hN3ommGAN5/u61UXP1JX6c/vTkZZgS+xBO5wekCjZVnbOL2NA6n0PVq7//zinaqP6KsiUo8xKJKe1mrehVVpCUQW4a9rqzj8mTwE3t5t+X5GidvYDdqH075q47uTqO5WbiDJGPNH3qT8g07bzt5y3hTtu1aohCkAiChognRxVb1WDn35oC7Tn/1Hckbxie18tREXOha35Dq2jWAS5CyLf1pTHURpWS7JMp6cu8ZHE39hJpo6MKHkhtQgd9I6DBDoP1gTShU6eFCTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAfJQNOygoL+Z16iLg8bA8l81Q/5VTmMFu2EKadefkHU611tZZm9D+5gug5RRHsaQn/NoL9gz1gDoUhBLgptklFHmiIIhGQmKdFeoNi5twIb6LFMe4ZhukWVlZuAfEHge0iE9M6/wQ54FYgQQ2aZjK+RnxBEaCRTxzJ/FzAjB/4Fp4DC2xQ8ceFdvnQoc8I+4K5SHIVqJdwKe44xIKLaLU/xKQcVeAe5drXPGOoDn4uBcrlDl6RstIIgkyIlp4RM0ofxOdUVj7PUo9NsxS9Zl9+MbHsydXTN8GhQFSoLW0zaXNFfAT0d7JJCR0QZAd9iP6YCJYBz3y5KjEeIREUUfTM=
-----END CERTIFICATE-----"

J’ai récupéré le certificat ici :

Je l’ai exporté au format PKCS12 :

Je l’ai importé dans le trousseau de mon MacBook et exporté au format PEM. En fait, il s’est avéré identique, avec simplement les mots “BEGIN …” au début et “END …” à la fin.

Lorsque j’essaie de me connecter via SAML, cela m’envoie vers la page Keycloak :

Mais j’y vois une erreur :

Que fais-je mal ? J’ai configuré l’autorisation OpenID, cela fonctionne. Mais ! Pour plusieurs raisons, je dois également proposer SAML.
Je vous prie vivement de m’aider !

Le serveur Keycloak est sur une adresse IP privée. Cette adresse est-elle accessible depuis l’endroit où elle doit l’être ?

Bien sûr ! Sinon, je n’aurais pas été redirigé vers la page Keycloak :

Ping :

Je vais ajouter. Keycloak s’exécute dans un conteneur Docker. Voici le journal de ce conteneur lors du passage de Discourse à SAML :

10:21:53,379 WARN  [org.keycloak.events] (default task-198) type=LOGIN_ERROR, realmId=CUBA, clientId=https://cuba-test-forum-en.demo.haulmont.com/discuss, userId=null, ipAddress=172.27.0.1, error=client_not_found

I changed the ClientID in Keycloak to the one defined in the logs:

Now in the Keycloak logs this error:


11:24:32,750 ERROR [org.keycloak.protocol.saml.SamlService] (default task-211) request validation failed: org.keycloak.common.VerificationException: SigAlg was null

	at org.keycloak.keycloak-services@11.0.2//org.keycloak.protocol.saml.SamlProtocolUtils.verifyRedirectSignature(SamlProtocolUtils.java:137)

	at org.keycloak.keycloak-services@11.0.2//org.keycloak.protocol.saml.SamlProtocolUtils.verifyRedirectSignature(SamlProtocolUtils.java:127)

	at org.keycloak.keycloak-services@11.0.2//org.keycloak.protocol.saml.SamlService$RedirectBindingProtocol.verifySignature(SamlService.java:592)

	at org.keycloak.keycloak-services@11.0.2//org.keycloak.protocol.saml.SamlService$BindingProtocol.handleSamlRequest(SamlService.java:268)

	at org.keycloak.keycloak-services@11.0.2//org.keycloak.protocol.saml.SamlService$BindingProtocol.execute(SamlService.java:537)

	at org.keycloak.keycloak-services@11.0.2//org.keycloak.protocol.saml.SamlService.redirectBinding(SamlService.java:635)

	at jdk.internal.reflect.GeneratedMethodAccessor940.invoke(Unknown Source)

	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

	at java.base/java.lang.reflect.Method.invoke(Method.java:566)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:138)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:543)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:432)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$0(ResourceMethodInvoker.java:393)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.interception.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:358)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:395)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:364)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:150)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:104)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:440)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:229)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:135)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.interception.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:358)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:138)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:215)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:245)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:61)

	at org.jboss.resteasy.resteasy-jaxrs@3.12.1.Final//org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:56)

	at javax.servlet.api@2.0.0.Final//javax.servlet.http.HttpServlet.service(HttpServlet.java:590)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)

	at org.keycloak.keycloak-wildfly-extensions@11.0.2//org.keycloak.provider.wildfly.WildFlyRequestFilter.lambda$doFilter$0(WildFlyRequestFilter.java:41)

	at org.keycloak.keycloak-services@11.0.2//org.keycloak.services.filters.AbstractRequestFilter.filter(AbstractRequestFilter.java:43)

	at org.keycloak.keycloak-wildfly-extensions@11.0.2//org.keycloak.provider.wildfly.WildFlyRequestFilter.doFilter(WildFlyRequestFilter.java:39)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)

	at org.wildfly.extension.undertow@20.0.1.Final//org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78)

	at io.undertow.core@2.1.3.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:132)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)

	at io.undertow.core@2.1.3.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)

	at io.undertow.core@2.1.3.Final//io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)

	at io.undertow.core@2.1.3.Final//io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)

	at io.undertow.core@2.1.3.Final//io.undertow.security.handlers.NotificationReceiverHandler.handleRequest(NotificationReceiverHandler.java:50)

	at io.undertow.core@2.1.3.Final//io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)

	at io.undertow.core@2.1.3.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)

	at org.wildfly.extension.undertow@20.0.1.Final//org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)

	at io.undertow.core@2.1.3.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)

	at org.wildfly.extension.undertow@20.0.1.Final//org.wildfly.extension.undertow.deployment.GlobalRequestControllerHandler.handleRequest(GlobalRequestControllerHandler.java:68)

	at io.undertow.core@2.1.3.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:269)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler.access$100(ServletInitialHandler.java:78)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:133)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:130)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)

	at org.wildfly.extension.undertow@20.0.1.Final//org.wildfly.extension.undertow.security.SecurityContextThreadSetupAction.lambda$create$0(SecurityContextThreadSetupAction.java:105)

	at org.wildfly.extension.undertow@20.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1530)

	at org.wildfly.extension.undertow@20.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1530)

	at org.wildfly.extension.undertow@20.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1530)

	at org.wildfly.extension.undertow@20.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1530)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:249)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:78)

	at io.undertow.servlet@2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:99)

	at io.undertow.core@2.1.3.Final//io.undertow.server.Connectors.executeRootHandler(Connectors.java:370)

	at io.undertow.core@2.1.3.Final//io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:830)

	at org.jboss.threads@2.3.3.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)

	at org.jboss.threads@2.3.3.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1982)

	at org.jboss.threads@2.3.3.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)

	at org.jboss.threads@2.3.3.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1377)

	at java.base/java.lang.Thread.run(Thread.java:834)


11:24:32,750 WARN  [org.keycloak.events] (default task-211) type=LOGIN_ERROR, realmId=CUBA, clientId=null, userId=null, ipAddress=172.27.0.1, error=invalid_signature

Votre dernière erreur indique que Keycloak exige que les requêtes SAML soient signées.
Vous devez donc configurer l’une des options suivantes :

  • DISCOURSE_SAML_SP_CERTIFICATE : Certificat du fournisseur de services (SP) SAML
  • DISCOURSE_SAML_SP_PRIVATE_KEY : Clé privée du fournisseur de services (SP) SAML
  • DISCOURSE_SAML_AUTHN_REQUESTS_SIGNED : par défaut à false
  • DISCOURSE_SAML_WANT_ASSERTIONS_SIGNED : par défaut à false
  • DISCOURSE_SAML_LOGOUT_REQUESTS_SIGNED : par défaut à false
  • DISCOURSE_SAML_LOGOUT_RESPONSES_SIGNED : par défaut à false

(pour cela, vous avez besoin d’un certificat avec une clé privée pour Discourse, qui agit en tant que SP).

Sinon, vous pouvez désactiver le paramètre relatif aux requêtes signées dans Keycloak (je ne connais pas bien Keycloak, mais d’après votre capture d’écran, je pense que désactiver « Signature du client requise » devrait suffire…).
Attention : désactiver la signature des requêtes du client comporte certaines implications en matière de sécurité (notamment, d’autres parties pourraient signer pour vos utilisateur(s) - si ma mémoire est bonne) - il vaut mieux faire vos propres recherches et ne pas vous fier uniquement à moi :wink:

Bonjour @nahimov,
Est-ce que cela t’a aidé ? As-tu réussi à configurer Discourse + Keycloak SAML ?
Pourrais-tu partager ton expérience ?
Merci d’avance !

Je vois tellement de questions sur Discourse + Keycloak.
Celui-ci (Keycloak with Discourse - #20 by mahcr) a 5,2 k vues.
Peut-être que cela suffirait pour que quelqu’un écrive un guide étape par étape sur la façon de le configurer.
Je sais que j’apprécierais vraiment, vraiment beaucoup.