Discourse, Keycloak, SAML — разве невозможно настроить?

Привет!
Я занимаюсь этим уже почти неделю, но не могу настроить! Перепробовал миллион вариантов! Пожалуйста, помогите…

Итак, у меня есть тестовый экземпляр Keycloak по адресу http://10.5.40.19:8081. Там я создал realm CUBA и SAML-клиент:


Как видно из скриншотов, есть тестовый форум Discourse по адресу https://cuba-test-forum-en.demo.haulmont.com/discuss.
Согласно документации плагина discourse-saml, URL для подключения к SSO в моём тестовом форуме будет выглядеть так: https://cuba-test-forum-en.demo.haulmont.com/discuss/auth/saml/callback.

Настройки SAML в файле app.yml:

DISCOURSE_RELATIVE_URL_ROOT: /discuss

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

Я взял сертификат отсюда:

Экспортировал его в формат PKCS12:

Импортировал в связку ключей (KeyChain) моего MacBook и затем экспортировал в формат PEM. По сути, он остался тем же самым, только с надписью “BEGIN …” в начале и “END …” в конце.

Когда я пытаюсь войти через SAML, меня перенаправляет на страницу Keycloak:

Но там я вижу ошибку:

Что я делаю не так? Я настроил авторизацию OpenID, она работает. Но! По ряду причин мне нужно обеспечить работу и SAML.
Очень прошу о помощи!

Сервер Keycloak имеет частный IP-адрес. Доступен ли этот IP-адрес из тех мест, откуда к нему необходимо обращаться?

Конечно! Иначе меня бы не перекинуло на страницу Keycloak:

Ping:

Добавлю. Keycloak работает в контейнере Docker. Вот лог этого контейнера при переключении на SAML из Discourse:

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

Я изменил ClientID в Keycloak на тот, который указан в логах:

Теперь в логах Keycloak появляется следующая ошибка:


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

Ваша последняя ошибка показывает, что Keycloak требует подписи SAML-запросов.

Поэтому вам нужно либо настроить:

  • DISCOURSE_SAML_SP_CERTIFICATE — сертификат провайдера услуг SAML
  • DISCOURSE_SAML_SP_PRIVATE_KEY — закрытый ключ провайдера услуг SAML
  • DISCOURSE_SAML_AUTHN_REQUESTS_SIGNED — по умолчанию false
  • DISCOURSE_SAML_WANT_ASSERTIONS_SIGNED — по умолчанию false
  • DISCOURSE_SAML_LOGOUT_REQUESTS_SIGNED — по умолчанию false
  • DISCOURSE_SAML_LOGOUT_RESPONSES_SIGNED — по умолчанию false

(для этого вам понадобится сертификат с закрытым ключом для Discourse, выступающего в роли провайдера услуг).

Или вы можете отключить настройку, касающуюся подписанных запросов, в Keycloak (я не знаком с Keycloak, но, судя по вашему скриншоту, отключение опции «Требуется подпись клиента» должно помочь…)

Будьте осторожны: отключение подписи запросов клиента имеет определённые последствия для безопасности (в частности, другие стороны могут подписывать ваши пользователей — насколько я помню). Лучше самостоятельно изучить этот вопрос и не полагаться только на мои слова :wink:

Привет, @nahimov,
Помогло ли это? Вам удалось настроить Discourse + Keycloak SAML?
Можете поделиться своим опытом?
Заранее спасибо!

Я вижу так много вопросов о связке Discourse + Keycloak.
Эта тема (Keycloak with Discourse - #20 by mahcr) имеет 5,2 тыс. просмотров.
Возможно, кому-то стоит написать пошаговое руководство по её настройке.
Я бы лично очень-очень это оценил.