Discourse, Keycloak, SAML - 구성이 불가능한가요?

안녕하세요!
거의 일주일째 이것을 시도해 보고 있지만 설정을 할 수 없습니다! 수백 가지 옵션을 시도해 보았어요! 제발 도와주세요…

Keycloak 테스트 인스턴스가 있으며, 주소는 http://10.5.40.19:8081입니다. 거기서 CUBA 리얼름과 SAML 클라이언트를 만들었습니다:


스크린샷에서 볼 수 있듯이 https://cuba-test-forum-en.demo.haulmont.com/discuss에 테스트용 Discourse 포럼이 있습니다.
discourse-saml 플러그인 문서에 따르면, 제 테스트 포럼에서 SSO 연결을 위한 URL은 다음과 같아야 합니다 - https://cuba-test-forum-en.demo.haulmont.com/discuss/auth/saml/callback

app.yml 파일의 SAML 설정:

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로 내보냈습니다:

Macbook의 KeyChain에 가져온 후 PEM으로 내보냈습니다. 실제로는 처음과 끝에 "BEGIN …"과 "END …"라는 단어가 있는 것 외에는 동일하게 나왔습니다.

SAML을 사용하여 로그인하려고 하면 Keycloak 페이지로 이동합니다:

하지만 거기서 오류를 확인합니다:

제가 무엇을 잘못하고 있는 건가요? OpenID 인증은 설정했고 작동합니다. 하지만! 여러 가지 이유로 SAML도 제공해야 합니다.
도와주시길 간절히 부탁드립니다!

Keycloak 서버가 사설 IP를 사용하고 있습니다. 해당 IP가 필요한 위치에서 접근 가능한가요?

물론이죠! 그렇지 않았다면 Keycloak 페이지로 리다이렉트되지 않았을 테니까요:

핑:

추가로 말씀드릴 점은, Keycloak가 Docker 컨테이너에서 실행 중이라는 것입니다. 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

Keycloak에서 ClientID를 로그에 정의된 것으로 변경했습니다:

이제 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(SP)에는 개인 키가 포함된 인증서가 필요합니다.)

또는 Keycloak에서 서명된 요청에 대한 설정을 비활성화할 수도 있습니다. (저는 Keycloak에 대해 잘 알지 못하지만, 스크린샷을 보면 "Client signature required"를 끄면 해결될 것 같습니다 …)
주의하세요 - 클라이언트 요청 서명을 끄면 특정 보안상의 함의가 있습니다(기억이 정확하지는 않지만 다른 당사자가 사용자를 대신하여 서명할 수 있게 됩니다) - 해당 부분에 대해 스스로 조사해 보시고 제 말에만 의존하지 마세요 :wink:

안녕하세요 @nahimov,

도움이 되었나요? Discourse + Keycloak SAML 설정을 완료하셨나요?

경험을 공유해 주실 수 있을까요?

미리 감사드립니다!

Discourse와 Keycloak에 대한 질문을 정말 많이 보게 됩니다.
이 글( Keycloak with Discourse - #20 by mahcr )만 해도 조회 수가 5,200회나 되네요.
누군가 설정 방법을 단계별로 정리해 주면 좋겠다는 생각이 듭니다.
저라면 정말 정말 감사히 읽을 것 같습니다.