Discourse, Keycloak, SAML - è impossibile configurare?

Ciao!
Lo sto cercando di fare da quasi una settimana, ma non riesco a configurarlo! Ho provato un milione di opzioni! Aiutatemi per favore…

Quindi, c’è un’istanza di test di Keycloak, indirizzo - http://10.5.40.19:8081. Lì ho creato un realm CUBA e un client SAML:


Come si può vedere dagli screenshot, c’è un forum di test Discourse all’indirizzo https://cuba-test-forum-en.demo.haulmont.com/discuss
Secondo la documentazione del plugin discourse-saml, l’URL per la connessione SSO nel mio forum di test sarà simile a questo - https://cuba-test-forum-en.demo.haulmont.com/discuss/auth/saml/callback

Impostazioni SAML nel file app.yml:

DISCOURSE_RELATIVE_URL_ROOT: /discuss

DISCOURSE_SAML_TITLE: "Esempio 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-----"

Ho preso il certificato da qui:

L’ho esportato in PKCS12:

L’ho importato nel Portachiavi del mio MacBook ed esportato in PEM. In realtà, è risultato identico, solo con le parole “BEGIN …” all’inizio e “END …” alla fine.

Quando provo ad accedere tramite SAML, mi reindirizza alla pagina di Keycloak:

Ma lì vedo un errore:

Cosa sto facendo sbagliato? Ho configurato l’autorizzazione OpenID, funziona. Ma! Per una serie di motivi, devo fornire anche SAML.
Vi chiedo davvero aiuto!

Il server Keycloak si trova su un indirizzo IP privato. Quell’indirizzo è raggiungibile da dove deve essere raggiunto?

Certo! Altrimenti non sarei stato reindirizzato alla pagina di Keycloak:

Ping:

Lo aggiungerò. Keycloak è in esecuzione in un container Docker. Ecco il log di questo container durante il passaggio da Discourse a 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

Ho modificato il ClientID in Keycloak con quello definito nei log:

Ora nei log di Keycloak appare questo errore:


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

L’ultimo errore mostra che Keycloak richiede che le richieste SAML siano firmate.
Quindi devi configurare:

  • DISCOURSE_SAML_SP_CERTIFICATE: Certificato del Service Provider SAML
  • DISCOURSE_SAML_SP_PRIVATE_KEY: Chiave privata del Service Provider SAML
  • DISCOURSE_SAML_AUTHN_REQUESTS_SIGNED: valore predefinito false
  • DISCOURSE_SAML_WANT_ASSERTIONS_SIGNED: valore predefinito false
  • DISCOURSE_SAML_LOGOUT_REQUESTS_SIGNED: valore predefinito false
  • DISCOURSE_SAML_LOGOUT_RESPONSES_SIGNED: valore predefinito false

(per questo è necessaria una chiave privata con certificato per Discourse, ovvero per l’SP).

In alternativa, puoi disabilitare l’impostazione relativa alle richieste firmate in Keycloak (non conosco Keycloak, ma dalla tua schermata penso che disattivare “Firma del client obbligatoria” dovrebbe risolvere il problema…)
Attenzione: disattivare la firma delle richieste del client comporta alcune implicazioni per la sicurezza (ovvero, altre parti potrebbero firmare i tuoi utenti, se ricordo bene) - è meglio fare le tue ricerche al riguardo e non fare affidamento su di me :wink:

Ciao @nahimov,
Ti è stato d’aiuto? Sei riuscito a configurare Discourse + Keycloak SAML?
Potresti condividere la tua esperienza?
Grazie in anticipo!

Vedo così tante domande su Discourse + Keycloak.
Questa (Keycloak with Discourse - #20 by mahcr) ha 5.200 visualizzazioni.
Forse sarebbe sufficiente che qualcuno scrivesse una guida passo passo su come configurarlo.
Lo apprezzerei moltissimo.