이 페이지에서는 SAML(Security Assertion Markup Language) 2.0 빌딩 블록의 일반적인 개요와 SAML 인증 제공자를 위한 일반적인 단일 로그인 문제 및 문제 해결 기술을 제공합니다.

어떤 이유로든 업데이트된/새로운 IdP 메타데이터 XML 파일이 SAML 인증 제공자에 대한 'ID 제공자 설정' 섹션의 'SAML 인증 설정' 페이지에 있는 Blackboard Learn GUI에서 업로드된 경우 SAML 인증 제공자는 '활성' 상태로 두고 SAML B2 및 해당 SAML 인증 제공자도 '비활성'/'사용 가능'으로 전환하여 캐시된 모든 IdP 메타데이터가 지워졌으며 업데이트된 IdP 메타데이터가 완전히 활용되고 있음을 확인해야 합니다.


주요 용어

이 가이드에서 사용된 용어 및 약어는 다음과 같습니다.

  • SAML: Security Assertion Markup Language
  • IdP: ID 제공자
  • SP: 서비스 제공자
  • ADFS: Active Directory Federation Services
  • GUI: 시각적 사용자 인터페이스. Blackboard Learn의 맥락에서 이는 소프트웨어 내부에서 작업 중인 것을 의미합니다.

SAML 구성 설정 수정

SAML 인증 문제를 해결하는 데 도움을 주기 위해 SAML 빌딩 블록은 다음의 구성 설정 및 옵션을 포함하도록 릴리즈 3200.2.0에서 업데이트되었습니다.

  • SAML 세션 수명 제한 정의
  • 서명 알고리즘 유형 선택
  • 인증서 재생성
  • ResponseSkew 값 변경

SAML 빌딩 블록에서 설정을 구성하는 방법 자세히 알아보기


오류 및 예외

SAML과 관련된 오류/예외는 다음 로그에 캡처됩니다.

  • /usr/local/blackboard/logs/bb-services-log.txt
  • /usr/local/blackboard/logs/tomcat/stdout-stderr-<date>.log
  • /usr/local/blackboard/logs/tomcat/catalina-log.txt

보고된 SAML 인증 문제를 조사할 때 이러한 로그를 항상 검색해야 합니다.


SAML 추적 프로그램

SAML 2.0 인증 문제 해결을 반복하면서 특정 시점에 인증 프로세스 동안 IdP에서 실제로 릴리즈되고 Learn에 전송되고 있는 특성을 확인하고 볼 수 있어야 합니다. IdP의 특성이 SAML 응답에서 암호화되지 않은 경우 Firefox 브라우저의 SAML 추적 프로그램 애드온 또는 Chrome SAML Message Decoder를 사용하여 특성을 확인할 수 있습니다.


특성이 적절하게 매핑되지 않음

userName이 포함된 특성이 Blackboard Learn GUI의 SAML 인증 설정 페이지에 있는 SAML 특성 매핑 섹션의 원격 사용자 ID 필드에 지정된 대로 적절하게 매핑되지 않은 경우, SAML 인증을 통해 Blackboard Learn에 로그인하려고 시도할 때 다음 이벤트가 bb-services 로그에 로깅됩니다.

2016-06-28 12:48:12 -0400 - userName is null or empty

다음과 같이 유사한 로그인 오류입니다! 메시지가 브라우저에 표시됩니다. Blackboard Learn에서 현재 단일 로그인을 사용하여 계정에 로그인할 수 없습니다. 지원을 받으려면 Blackboard 관리자에게 문의하십시오.

다음과 같이 인증 실패 항목이 bb-services 로그에 나타납니다.

2016-06-28 12:48:12 -0400 - BbSAMLExceptionHandleFilter - javax.servlet.ServletException: Authentication Failure
    at blackboard.auth.provider.saml.customization.handler.BbAuthenticationSuccessHandler.checkAuthenticationResult(BbAuthenticationSuccessHandler.java:81)
    at blackboard.auth.provider.saml.customization.handler.BbAuthenticationSuccessHandler.onAuthenticationSuccess(BbAuthenticationSuccessHandler.java:57)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:331)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:245)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
[SNIP]

해결 방법

이 문제를 해결하기 위한 옵션으로는 두 가지가 있습니다. 먼저 Blackboard Learn GUI의 SAML 인증 설정 페이지에서 시스템에 없는 경우 계정 생성 옵션을 선택합니다. 또는 특성이 암호화되지 않은 경우 SAML 추적 프로그램 또는 디버그 로깅을 통해 IdP에서 릴리즈한 특성 값을 확인하려고 시도할 수 있습니다.

<saml2:Attribute Name="urn:oid:0.9.2342.19200300.100.1.3">
    <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                          xsi:type="xs:anyType"
                          >bbuser_saml2@bbchjones.net</saml2:AttributeValue>
</saml2:Attribute>

그리고 원하는 AttributeValue를 가진 Attribute Name을 Blackboard Learn GUI의 SAML 인증 설정 페이지에 있는 원격 사용자 ID에 매핑할 수 있습니다.


호환되는 데이터 소스가 선택되지 않음

Blackboard Learn GUI의 SAML 인증 설정 페이지에 있는 서비스 제공자 설정 > 호환되는 데이터 소스 섹션에서 사용자에 대해 데이터 소스를 선택하지 않은 경우, 사용자가 SAML 인증을 통해 Blackboard Learn에 로그인할 수 없습니다. 다음 이벤트는 SAML 인증을 통해 Blackboard Learn에 로그인하려고 시도할 때 bb-services 로그에 로깅됩니다.

2016-09-23 12:33:13 -0500 - userName is null or empty

로그인 오류입니다! 메시지가 브라우저에 나타나고 인증 실패가 bb-services 로그에 나타납니다.

2016-09-23 12:33:13 -0500 - BbSAMLExceptionHandleFilter - javax.servlet.ServletException: Authentication Failure
    at blackboard.auth.provider.saml.customization.handler.BbAuthenticationSuccessHandler.checkAuthenticationResult(BbAuthenticationSuccessHandler.java:82)
    at blackboard.auth.provider.saml.customization.handler.BbAuthenticationSuccessHandler.onAuthenticationSuccess(BbAuthenticationSuccessHandler.java:58)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:331)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:245)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter (SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor3399.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
        [SNIP]

해결 방법

  1. 로그인할 수 없는 사용자의 사용자명을 확보합니다.
  2. Blackboard Learn GUI에서 시스템 관리자 > 사용자로 이동하여 사용자를 검색합니다.
  3. 사용자의 데이터 소스 키를 복사합니다.
  4. 시스템 관리자 > 인증 > "제공자 이름" > SAML 설정 > 호환되는 데이터 소스로 이동합니다.
  5. 이름 열에서 해당 데이터 소스 옆에 확인 표시를 하고 제출을 선택합니다.

"지정된 URL의 형식이 잘못되었습니다." 오류 메시지

OneLogin이 Blackboard Learn의 SAML 인증 제공자에 대한 IdP로 구성된 경우, Blackboard Learn에 로그인하려고 시도할 때 OneLogin 자격 증명을 입력한 후 지정된 URL의 형식이 잘못되었습니다. 오류 메시지가 이 페이지에 표시될 수 있습니다.

bb-services-log에 다음 내용이 함께 표시됩니다.

2016-09-16 09:43:40 -0400 - Given URL is not well formed<P><span class="captionText">For reference, the Error ID is 17500f44-7809-4b9f-a272-3bed1d1af131.</span&gt; - java.lang.IllegalArgumentException: Given URL is not well formed
    at org.opensaml.util.URLBuilder.<init>(URLBuilder.java:120)
    at org.opensaml.util.SimpleURLCanonicalizer.canonicalize(SimpleURLCanonicalizer.java:87)
    at org.opensaml.common.binding.decoding.BasicURLComparator.compare(BasicURLComparator.java:57)
    at org.opensaml.common.binding.decoding.BaseSAMLMessageDecoder.compareEndpointURIs(BaseSAMLMessageDecoder.java:173)
    at org.opensaml.common.binding.decoding.BaseSAMLMessageDecoder.checkEndpointURI(BaseSAMLMessageDecoder.java:213)
    at org.opensaml.saml2.binding.decoding.BaseSAML2MessageDecoder.decode(BaseSAML2MessageDecoder.java:72)
    at org.springframework.security.saml.processor.SAMLProcessorImpl.retrieveMessage(SAMLProcessorImpl.java:105)
    at org.springframework.security.saml.processor.SAMLProcessorImpl.retrieveMessage(SAMLProcessorImpl.java:172)
    at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:80)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    [SNIP]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.net.MalformedURLException: no protocol: {recipient}
    at java.net.URL.<init>(URL.java:593)
    at java.net.URL.<init>(URL.java:490)
    at java.net.URL.<init>(URL.java:439)
    at org.opensaml.util.URLBuilder.<init>(URLBuilder.java:77)
        ... 203 more

해결 방법

  1. Firefox 브라우저의 SAML 추적 프로그램을 켜고 로그인 문제를 재연합니다.
  2. SAML POST 이벤트의 시작 부분을 검토합니다.

    <samlp:Response Destination="{recipient}"
            ID="R8afbfbfee7292613f98ad4ec4115de7c6b385be6"
            InResponseTo="a3g2424154bb0gjh3737ii66dadbff4"
            IssueInstant="2016-09-16T18:49:09Z"
            Version="2.0"
            xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
            xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
            >
        <saml:Issuer>https://app.onelogin.com/saml/metadata/123456</saml:Issuer>
        [SNIP]

  3. 응답 내용이 나와 있는 첫 번째 줄의 경우 Destination=이 수신자로만 설정되었는지 확인합니다.
  4. 고객이 OneLogin IdP의 구성 섹션에 접근하도록 합니다.
  5. 수신자 필드가 비어 있는지 확인합니다.
  6. ACS(소비자) URL의 값을 복사하여 수신자 필드에 붙여넣고 저장을 선택합니다.

IdP/SP 문제 시나리오

  1. IdP의 로그인 페이지로 리디렉션하기 전에 오류가 나타나면 IdP의 메타데이터가 유효하지 않은 것일 수 있습니다.
  2. 오류가 IdP의 페이지에 로그인한 후에 나타난다면 다음과 같은 이유 때문일 수 있습니다.
    1. SP와 IdP 간의 특성 매핑이 잘못되었거나 IdP가 유효한 원격 사용자 ID를 반환하지 않았습니다.
    2. IdP의 SAML 응답 유효성을 SP가 검사하지 않았습니다. 이는 다음과 같은 이유 때문일 수 있습니다.
      • IdP가 유효한 인증 기관에서 발급하지 않은 인증서를 사용하여 SAML 응답에 서명하며, SP의 키 저장소에 이 인증서가 들어 있지 않습니다.
      • SP의 시스템 시계가 잘못되었습니다.

ADFS(Active Directory Federation Services)

특성 이름은 Blackboard Learn GUI의 SAML 인증 설정 페이지에 있는 SAML 특성 매핑 섹션에서 대/소문자를 구분합니다. So if the Remote User ID has sAMAccountName for the Attribute Name on the settings page and the actual SAML POST from the IdP has this for the Attribute Name in the AttributeStatement:

<AttributeStatement>
    <Attribute Name="SamAccountName>
        <AttributeValue>Test-User</AttributeValue>
    </Attribute>
</AttributeStatement>

사용자는 로그인할 수 없습니다. SAML 인증 설정 페이지의 원격 사용자 ID 특성 이름 값을 sAMAccountName에서 SamAccountName으로 변경해야 합니다.

"리소스를 찾을 수 없습니다." 또는 "로그인 오류입니다!" 경고

이 섹션에는 지정된 리소스를 찾을 수 없거나 해당 리소스에 접근할 권한이 없습니다. 또는 로그인 오류입니다! 메시지가 Blackboard Learn GUI에 표시되는 경우 ADFS와 함께 SAML 인증을 통해 사용자가 Learn에 로그인하는 것을 방해할 수 있는 일반적인 문제가 나와 있습니다.

문제 1

ADFS 로그인 페이지에서 로그인 자격 증명을 입력하면 Blackboard Learn GUI로 리디렉션된 후에 다음과 같은 오류가 표시될 수 있습니다. 지정된 리소스를 찾을 수 없거나 해당 리소스에 접근할 권한이 없습니다.

stdout-stderr 로그에 다음과 같은 메시지가 함께 나타납니다.

INFO | jvm 1 | 2016/06/22 06:08:33 | - No mapping found for HTTP request with URI [/auth-saml/saml/SSO] in DispatcherServlet with name 'saml'

이 문제는 noHandlerFound() 메서드가 DispatcherServlet.java 코드에서 사용되고 HTTP 단일 로그인 요청을 찾거나 매핑할 수 없기 때문에 발생합니다.

/**
 * No handler found -&gt; set appropriate HTTP response status.
 * @param request current HTTP request
 * @param response current HTTP response
 * @throws Exception if preparing the response failed
 */
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
 if (pageNotFoundLogger.isWarnEnabled()) {
  pageNotFoundLogger.warn("No mapping found for HTTP request with URI [" + getRequestUri(request) +
    "] in DispatcherServlet with name '" + getServletName() + "'");
 }
 if (this.throwExceptionIfNoHandlerFound) {
  throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
    new ServletServerHttpRequest(request).getHeaders());
 }
 else {
  response.sendError(HttpServletResponse.SC_NOT_FOUND);
 }
}

해결 방법

이 문제는 일반적으로 Blackboard Learn GUI에서 구성된 SP에 대한 엔터티 ID가 올바르지 않기 때문에 발생합니다. 이 문제는 시스템 관리자 > 인증 > SAML 인증 설정 > 서비스 제공자 설정으로 이동하여 엔터티 ID를 업데이트하여 해결할 수 있습니다. ADFS의 경우 엔터티 ID의 기본 구성은 https://[Learn Server Hostname]/auth-saml/saml/SSO입니다.

학교에서 URL을 기본 https://school.blackboard.com에서 https://their.school.edu로 변경하는 경우, 'SAML 인증 설정' 페이지의 Blackboard Learn GUI에 있는 '엔터티 ID'를 https://their.school.edu/auth-saml/saml/SSO로 업데이트해야 합니다.

문제 2

ADFS 로그인 페이지에서 로그인 자격 증명을 입력하면 Blackboard Learn GUI로 리디렉션된 후에 다음과 같은 오류가 표시될 수 있습니다. 지정된 리소스를 찾을 수 없거나 해당 리소스에 접근할 권한이 없습니다.

stdout-stderr 로그에 다음과 같은 메시지가 함께 나타납니다.

INFO  | jvm 1  | 2016/06/22 06:08:33 | - No mapping found for HTTP request with URI [/auth-saml/saml/SSO] in DispatcherServlet with name 'saml'

또한 catalina 로그에는 다음과 같은 메시지가 나타납니다.

ERROR 2016-06-27 10:47:03,664 connector-6: userId=_2_1, sessionId=62536416FB80462298C92064A7022E50 org.opensaml.xml.encryption.Decrypter - Error decrypting the encrypted data element
org.apache.xml.security.encryption.XMLEncryptionException: Illegal key size
Original Exception was java.security.InvalidKeyException: Illegal key size
    at org.apache.xml.security.encryption.XMLCipher.decryptToByteArray(XMLCipher.java:1822)
    at org.opensaml.xml.encryption.Decrypter.decryptDataToDOM(Decrypter.java:596)
    at org.opensaml.xml.encryption.Decrypter.decryptUsingResolvedEncryptedKey(Decrypter.java:795)
    at org.opensaml.xml.encryption.Decrypter.decryptDataToDOM(Decrypter.java:535)
    at org.opensaml.xml.encryption.Decrypter.decryptDataToList(Decrypter.java:453)
    at org.opensaml.xml.encryption.Decrypter.decryptData(Decrypter.java:414)
    at org.opensaml.saml2.encryption.Decrypter.decryptData(Decrypter.java:141)
    at org.opensaml.saml2.encryption.Decrypter.decrypt(Decrypter.java:69)
    at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:199)
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:82)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
        [SNIP]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.security.InvalidKeyException: Illegal key size
    at javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1039)
    at javax.crypto.Cipher.init(Cipher.java:1393)
    at javax.crypto.Cipher.init(Cipher.java:1327)
    at org.apache.xml.security.encryption.XMLCipher.decryptToByteArray(XMLCipher.java:1820)
        ... 205 more

그리고 bb-services 로그에는 다음과 같은 메시지가 표시됩니다.

2016-06-27 10:47:03 -0400 - unsuccessfulAuthentication - org.springframework.security.authentication.AuthenticationServiceException: Error validating SAML message
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:100)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
    at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor3422.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:277)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:274)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
    at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:309)
    at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:249)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:237)
    at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:55)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:191)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:187)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:186)
    at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:30)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at sun.reflect.GeneratedMethodAccessor3421.invoke(Unknown Source)
        [SNIP]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.common.SAMLException: Response doesn't have any valid assertion which would pass subject validation
    at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:229)
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:87)
        ... 229 more

이 문제는 기본적으로 ADFS가 전송하는 특성을 AES-256을 사용하여 암호화하고 Blackboard Learn에서 사용하는 Java 런타임이 AES-256을 바로 지원하지 않기 때문에 발생합니다.

해결 방법

보편적인 해결 옵션은 ADFS 서버에서 PowerShell을 열고 Blackboard Learn에 대해 생성된 신뢰 당사자가 암호화되지 않은 상태로 특성을 전송하도록 설정하는 것입니다. 전체 통신이 SSL을 통과하므로 이로 인해 인증 보안의 수준이 감소하지 않습니다. 또한 Firefox 브라우저 SAML 추적 프로그램 애드온과 같은 디버깅 도구를 사용하여 특성을 확인할 수 있으며 Blackboard Learn 시스템을 다시 시작할 필요가 없으므로 모든 문제를 쉽게 디버깅할 수 있습니다. Blackboard Learn에 대해 생성된 신뢰 당사자가 암호화되지 않은 상태로 특성을 전송하도록 설정하려면 PowerShell을 열고 다음 명령을 실행하여 TargetName트러스트 관계 > 신뢰 당사자 트러스트의 'ADFS 관리 콘솔'에 있는 신뢰 당사자 트러스트의 이름으로 바꾸십시오.

set-ADFSRelyingPartyTrust –TargetName "yourlearnserver.blackboard.com" –EncryptClaims $False

이렇게 변경한 후에 ADFS 서비스를 다음 명령으로 다시 시작해야 합니다. Restart-Service ADFSSRV

문제 3

ADFS 로그인 페이지에서 로그인 자격 증명을 입력하면 Blackboard Learn GUI로 리디렉션된 후에 다음과 같은 오류가 표시될 수 있습니다. 지정된 리소스를 찾을 수 없거나 해당 리소스에 접근할 권한이 없습니다. 또는 로그인 오류입니다!

둘 중 한 가지 오류와 함께 stdout-stderr 로그에 다음과 유사한 SAML 관련 이벤트가 나타납니다.

INFO   | jvm 1    | 2016/09/06 20:33:04 | - /saml/login?apId=_107_1&redirectUrl=https%3A%2F%2Fbb.fraser.misd.net%2Fwebapps%2Fportal%2Fexecute%2FdefaultTab at position 1 of 10 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
INFO   | jvm 1    | 2016/09/06 20:33:04 | - No HttpSession currently exists
INFO   | jvm 1    | 2016/09/06 20:33:04 | - No SecurityContext was available from the HttpSession: null. A new one will be created.
INFO   | jvm 1    | 2016/09/06 20:33:04 | - /saml/login?apId=_107_1&redirectUrl=https%3A%2F%2Fbb.fraser.misd.net%2Fwebapps%2Fportal%2Fexecute%2FdefaultTab at position 2 of 10 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
INFO   | jvm 1    | 2016/09/06 20:33:04 | - /saml/login?apId=_107_1&redirectUrl=https%3A%2F%2Fbb.fraser.misd.net%2Fwebapps%2Fportal%2Fexecute%2FdefaultTab at position 3 of 10 in additional filter chain; firing Filter: 'HeaderWriterFilter'
INFO   | jvm 1    | 2016/09/06 20:33:04 | - /saml/login?apId=_107_1&redirectUrl=https%3A%2F%2Fbb.fraser.misd.net%2Fwebapps%2Fportal%2Fexecute%2FdefaultTab at position 4 of 10 in additional filter chain; firing Filter: 'FilterChainProxy'
INFO   | jvm 1    | 2016/09/06 20:33:04 | - Checking match of request : '/saml/login'; against '/saml/login/**'
INFO   | jvm 1    | 2016/09/06 20:33:04 | - /saml/login?apId=_107_1&redirectUrl=https%3A%2F%2Fbb.fraser.misd.net%2Fwebapps%2Fportal%2Fexecute%2FdefaultTab at position 1 of 1 in additional filter chain; firing Filter: 'SAMLEntryPoint'
INFO   | jvm 1    | 2016/09/06 20:33:04 | - Request for URI http://www.w3.org/2000/09/xmldsig#rsa-sha1
INFO   | jvm 1    | 2016/09/06 20:33:04 | - Request for URI http://www.w3.org/2000/09/xmldsig#rsa-sha1
INFO   | jvm 1    | 2016/09/06 20:33:04 | - SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
INFO   | jvm 1    | 2016/09/06 20:33:04 | - SecurityContextHolder now cleared, as request processing completed
INFO   | jvm 1    | 2016/09/06 20:33:07 | - /saml/SSO at position 1 of 10 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - HttpSession returned null object for SPRING_SECURITY_CONTEXT
INFO   | jvm 1    | 2016/09/06 20:33:07 | - No SecurityContext was available from the HttpSession: [email protected] A new one will be created.
INFO   | jvm 1    | 2016/09/06 20:33:07 | - /saml/SSO at position 2 of 10 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - /saml/SSO at position 3 of 10 in additional filter chain; firing Filter: 'HeaderWriterFilter'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - /saml/SSO at position 4 of 10 in additional filter chain; firing Filter: 'FilterChainProxy'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Checking match of request : '/saml/sso'; against '/saml/login/**'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Checking match of request : '/saml/sso'; against '/saml/logout/**'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Checking match of request : '/saml/sso'; against '/saml/bbsamllogout/**'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Checking match of request : '/saml/sso'; against '/saml/sso/**'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - /saml/SSO at position 1 of 1 in additional filter chain; firing Filter: 'SAMLProcessingFilter'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Authentication attempt using org.springframework.security.saml.SAMLAuthenticationProvider
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Forwarding to /
INFO   | jvm 1    | 2016/09/06 20:33:07 | - DispatcherServlet with name 'saml' processing POST request for [/auth-saml/saml/SSO]
INFO   | jvm 1    | 2016/09/06 20:33:07 | - No mapping found for HTTP request with URI [/auth-saml/saml/SSO] in DispatcherServlet with name 'saml'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Successfully completed request
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Skip invoking on
INFO   | jvm 1    | 2016/09/06 20:33:07 | - SecurityContextHolder now cleared, as request processing completed

또는 bb-services 로그에 다음과 유사한 SAML 예외가 표시됩니다.

2016-11-29 09:04:24 -0500 - unsuccessfulAuthentication - org.springframework.security.authentication.AuthenticationServiceException: Error validating SAML message
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:100)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
    at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor853.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:282)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:279)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
    at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:314)
    at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:253)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
    at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:46)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:148)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:144)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:143)
    at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:30)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at sun.reflect.GeneratedMethodAccessor853.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        [SNIP]
    at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:677)
    at blackboard.tomcat.valves.LoggingRemoteIpValve.invoke(LoggingRemoteIpValve.java:44)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:349)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:1110)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:785)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1425)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.common.SAMLException: Response issue time is either too old or with date in the future, skew 60, time 2016-11-29T14:03:16.634Z
    at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:126)
    at blackboard.auth.provider.saml.customization.consumer.BbSAMLWebSSOProfileConsumerImpl.processAuthenticationResponse(BbSAMLWebSSOProfileConsumerImpl.java:40)
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:87)
        ... 230 more

이 문제는 ADFS 서버와 Blackboard Learn 애플리케이션 서버에 기본값인 60초에 가깝거나 이를 초과하는 시간 추이가 있는 경우 발생합니다.

해결 방법

이 문제를 해결하는 옵션으로는 다음과 같이 두 가지가 있습니다.

  1. Blackboard Learn 애플리케이션 서버와 ADFS 서버의 시계를 수동으로 동기화합니다. Blackboard Learn의 경우 Blackboard Learn URL의 끝에 /webapps/portal/healthCheck를 추가하여 서버의 현재 시간 및 표준 시간대를 웹 브라우저에서 확인할 수 있습니다.

    예: https://mhtest1.blackboard.com//webapps/portal/healthCheck

    Hostname: ip-10-145-49-11.ec2.internal
    Status: Active - Database connectivity established
    Running since: Sat, Dec 3, 2016 - 05:39:11 PM EST
    Time of request: Thu, Dec 8, 2016 - 05:12:43 PM EST

    교육기관은 위의 URL을 사용하여 Blackboard Learn 시스템의 표준 시간대와 시계를 ADFS 서버의 표준 시간대 및 시계와 비교한 다음, 필요에 따라 ADFS 서버에서 해당 항목을 조정하여 Blackboard Learn 사이트와 동기화 상태가 되게 할 수 있습니다.

  2. securityContext.xml에서 왜곡된 부분을 조정하여 문제를 해결할 수 있습니다.
    1. 파일의 백업 사본 만들기:

      /usr/local/blackboard/content/vi/BBLEARN/plugins/bb-auth-provider-saml/webapp/WEB-INF/config/saml/securityContext.xml

    2. 변경 - 변경 전:

      <!-- SAML 2.0 WebSSO Assertion Consumer -->
      <bean id="webSSOprofileConsumer" class="org.springframework.security.saml.websso.WebSSOProfileConsumerImpl"/>

      변경 후:

      <!-- SAML 2.0 WebSSO Assertion Consumer -->
      <bean id="webSSOprofileConsumer" class="org.springframework.security.saml.websso.WebSSOProfileConsumerImpl">
      <property name="responseSkew"value="60000"/><!-- 100 hours -->
      </bean>

문제 4

ADFS 로그인 페이지에서 로그인 자격 증명을 입력하면 Blackboard Learn GUI로 리디렉션된 후에 다음과 같은 오류가 표시될 수 있습니다. 지정된 리소스를 찾을 수 없거나 해당 리소스에 접근할 권한이 없습니다. 또는 로그인 오류입니다!

bb-services 로그에 다음과 같은 예외가 함께 표시됩니다.

2016-11-01 12:47:19 -0500 - unsuccessfulAuthentication - org.springframework.security.authentication.AuthenticationServiceException: Error validating SAML message
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:100)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
    at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor929.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:282)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:279)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
    at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:314)
    at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:253)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
    at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:46)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:148)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:144)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:143)
    at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:30)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
 [SNIP]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.common.SAMLException: Response has invalid status code urn:oasis:names:tc:SAML:2.0:status:Responder, status message is null
    at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:113)
    at blackboard.auth.provider.saml.customization.consumer.BbSAMLWebSSOProfileConsumerImpl.processAuthenticationResponse(BbSAMLWebSSOProfileConsumerImpl.java:40)
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:87)
        ... 230 more
 2016-11-01 12:47:19 -0500 - BbSAMLExceptionHandleFilter - javax.servlet.ServletException: Unsuccessful Authentication
         at blackboard.auth.provider.saml.customization.filter.BbSAMLProcessingFilter.unsuccessfulAuthentication(BbSAMLProcessingFilter.java:31)
         at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:235)
         at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
         at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
         at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
         at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
         at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
         at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
         at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
         at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
         at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
         at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
         at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
         at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
         at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
         at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
         at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
         at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
         at sun.reflect.GeneratedMethodAccessor929.invoke(Unknown Source)
         at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
         at java.lang.reflect.Method.invoke(Method.java:498)
         at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:282)
         at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:279)
         at java.security.AccessController.doPrivileged(Native Method)
         at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
         at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:314)
         at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:253)
         at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
         at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:46)
         at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:148)
         at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:144)
         at java.security.AccessController.doPrivileged(Native Method)
         at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:143)
         at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:30)
 [SNIP]

해결 방법
  1. '관리자 패널'로 이동합니다.
  2. 빌딩 블록에서 빌딩 블록을 선택합니다.
  3. 설치된 도구를 선택합니다.
  4. 목록에서 인증 제공자 - SAML을 찾습니다. 메뉴를 열고 설정을 선택합니다.
  5. '서명 알고리즘 설정' 아래의 목록에서 SHA-256 을 선택합니다. 서명 알고리즘 유형을 선택한 후 SAML 빌딩 블록을 다시 시작하여 새 설정을 적용합니다.
  6. 변경사항을 저장하려면 제출을 선택합니다.

문제 5

ADFS 로그인 페이지에서 로그인 자격 증명을 입력하면 Blackboard Learn GUI로 리디렉션된 후에 다음과 같은 오류가 표시될 수 있습니다. 지정된 리소스를 찾을 수 없거나 해당 리소스에 접근할 권한이 없습니다. 또는 로그인 오류입니다!

bb-services 로그에 다음과 같은 예외가 함께 표시됩니다.

2017-01-04 22:52:58 -0700 - unsuccessfulAuthentication - org.springframework.security.authentication.AuthenticationServiceException: Error validating SAML message
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:100)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
    at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor935.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:282)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:279)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
    at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:314)
    at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:253)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
    at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:46)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:148)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:144)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:143)
    at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:30)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    [SNIP]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.common.SAMLException: NameID element must be present as part of the Subject in the Response message, please enable it in the IDP configuration
    at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:252)
    at blackboard.auth.provider.saml.customization.consumer.BbSAMLWebSSOProfileConsumerImpl.processAuthenticationResponse(BbSAMLWebSSOProfileConsumerImpl.java:40)
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:87)
        ... 214 more

위의 SAML 예외에 나와 있듯이 NameID 요소는 응답 메시지의 제목에서 누락됩니다. 이 문제는 일반적으로 NameID가 교육기관의 ADFS IdP에 있는 신뢰 당사자 트러스트에 대한 클레임 규칙에서 발신 클레임 유형으로 설정되지 않은 경우 또는 NameID에 대한 클레임 규칙이 교육기관의 ADFS IdP에 있는 신뢰 당사자 트러스트에 대해 적절한 순서가 아닌 경우 발생하며, 이로 인해 응답 메시지의 제목에서 NameID 요소가 누락됩니다.

예: NameID 요소가 누락됨

<Subject>
    <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <SubjectConfirmationData InResponseTo="a22ai8iig0f75ae22hd28748b12da50"
                                 NotOnOrAfter="2017-01-03T05:57:58.234Z"
                                 Recipient="https://yourschool.blackboard.com/auth-saml/saml/SSO"
                                 />
    </SubjectConfirmation>
</Subject>

예: NameID 요소가 있음

<Subject>
    <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">testadfs</NameID>
    <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <SubjectConfirmationData InResponseTo="a5903d39if463ea87ieiab5135j9ji"
                                 NotOnOrAfter="2017-01-05T04:33:12.715Z"
                                 Recipient="https://yourschool.blackboard.com/auth-saml/saml/SSO"
                                 />
    </SubjectConfirmation>
</Subject>

Firefox SAML 추적 프로그램의 애드온을 사용하여 응답 메시지에서 제목을 확인할 수 있습니다.

해결 방법

이 문제를 해결하기 위한 방법으로는 세 가지가 있습니다.

  1. 다음과 같이 ADFS에 대한 SAML B2 설정 가이드의 단계를 제대로 따랐는지 확인하고 필요에 따라 해당 ADFS IdP에 대한 신뢰 당사자 트러스트를 대상으로 하는 수신 클레임을 변환하도록 변경합니다.
    1. 클레임 규칙 수정을 선택합니다.
    2. 규칙 추가를 선택합니다.
    3. 규칙 템플릿 선택 페이지에서 클레임 규칙 템플릿에 대해 수신 클레임 변환을 선택한 후 다음을 선택합니다.
    4. 규칙 구성 페이지의 클레임 규칙 이름 필드에서 이메일을 이름 ID로 변환을 입력합니다.
    5. 수신 클레임 유형은 SamAccountName이어야 합니다(사용자명을 NameID로 변환 규칙에서 처음으로 생성된 발신 클레임 유형과 일치해야 함).
    6. 발신 클레임 유형이름 ID입니다.
    7. 발신 이름 ID 형식은 이메일입니다.
    8. 모든 클레임 값을 통해 전달이 선택되었는지 확인하고 종료를 선택합니다.
    9. 확인을 선택하여 규칙을 저장하고, 다시 확인을 선택하여 특성 매핑을 완료합니다.
  2. 해당 ADFS IdP에 사용된 클레임 규칙의 순서에서 NameID 요소를 지닌 규칙에 그보다 먼저 발생하는 선택 사항인 규칙이 없는지 확인합니다.
  3. 사용자 지정 특성을 사용 중인 경우 Learn에서는 여전히 해당 ADFS IdP가 NameID 값을 릴리즈할 것으로 계속 예상하므로 NameID 요소가 신뢰 당사자 트러스트에 있는지 확인합니다.

문제 6

SAML 인증을 통해 Blackboard Learn에 로그인할 때, 사용자가 페이지의 왼쪽에 있는 로그아웃 버튼을 클릭하여 로그아웃을 시도한 다음 단일 로그인 세션 종료 버튼을 클릭하면 바로 로그인 오류입니다!가 표시됩니다.

Sign On Error!
Blackboard Learn is currently unable to log into your account using single sign-on. Contact your administrator for assistance.
For reference, the Error ID is [error ID].

bb-services 로그에 다음과 같은 예외가 함께 표시됩니다.

2017-05-08 15:10:46 -0400 - BbSAMLExceptionHandleFilter Error Id: f3299757-8d4e-4fab-98cf-49cd99f4891e - javax.servlet.ServletException: Incoming SAML message failed security validation
    at org.springframework.security.saml.SAMLLogoutProcessingFilter.processLogout(SAMLLogoutProcessingFilter.java:145)
    at org.springframework.security.saml.SAMLLogoutProcessingFilter.doFilter(SAMLLogoutProcessingFilter.java:104)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    [SNIP]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.ws.security.SecurityPolicyException: Validation of request simple signature failed for context issuer
    at org.opensaml.common.binding.security.BaseSAMLSimpleSignatureSecurityPolicyRule.doEvaluate(BaseSAMLSimpleSignatureSecurityPolicyRule.java:139)
    at org.opensaml.common.binding.security.BaseSAMLSimpleSignatureSecurityPolicyRule.evaluate(BaseSAMLSimpleSignatureSecurityPolicyRule.java:103)
    at org.opensaml.ws.security.provider.BasicSecurityPolicy.evaluate(BasicSecurityPolicy.java:51)
    at org.opensaml.ws.message.decoder.BaseMessageDecoder.processSecurityPolicy(BaseMessageDecoder.java:132)
    at org.opensaml.ws.message.decoder.BaseMessageDecoder.decode(BaseMessageDecoder.java:83)
    at org.opensaml.saml2.binding.decoding.BaseSAML2MessageDecoder.decode(BaseSAML2MessageDecoder.java:70)
    at org.springframework.security.saml.processor.SAMLProcessorImpl.retrieveMessage(SAMLProcessorImpl.java:105)
    at org.springframework.security.saml.processor.SAMLProcessorImpl.retrieveMessage(SAMLProcessorImpl.java:172)
    at org.springframework.security.saml.SAMLLogoutProcessingFilter.processLogout(SAMLLogoutProcessingFilter.java:131)
    ... 244 more

이 오류는 SAML 설정 페이지의 단일 로그아웃 서비스 유형 설정으로 인해 발생합니다.

해결 방법

이 설정은 Blackboard Learn 및 ADFS 서버에서 구성해야 합니다.

IdP 역할을 하는 ADFS의 경우 게시 설정만 선택하고 ADFS 서버에서 Learn 인스턴스의 신뢰 당사자 트러스트에 대한 리디렉션 엔드포인트를 제거하십시오.

  1. Learn에서 관리자 > 인증 > (제공자 이름) > SAML 설정 > 단일 로그아웃 서비스 유형으로 이동합니다.
  2. 게시를 선택하고 리디렉션 확인란의 선택을 취소합니다.
  3. ADFS 서버에서 Learn 인스턴스의 '신뢰 당사자 트러스트'로 이동합니다.
  4. 속성 > 엔드포인트를 선택합니다. 두 개의 SAML 로그아웃 엔드포인트가 나열됩니다.
  5. 리디렉션 엔드포인트를 제거합니다. 엔드포인트를 제거하려면 엔드포인트 제거를 선택한 다음 '적용' 및 '확인'을 선택합니다.

Learn 및 ADFS 서버에서 위와 같이 변경을 수행하면 단일 로그인 세션 종료 로그아웃 버튼이 작동하여 사용자가 제대로 로그아웃됩니다.

문제 7

ADFS 로그인 페이지에서 로그인 자격 증명을 입력하고 나면 Learn으로 리디렉션될 때 로그인 오류입니다! 메시지가 표시됩니다.

bb-services 로그에 SAML 예외가 다음과 같이 함께 표시됩니다.

2017-05-26 07:39:30 -0400 - unsuccessfulAuthentication - org.springframework.security.authentication.AuthenticationServiceException: Error validating SAML message
        at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:100)
        at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
        at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
        at blackboard.auth.provider.saml.customization.filter.BbSAMLProcessingFilter.attemptAuthentication(BbSAMLProcessingFilter.java:46)
        at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
        at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
        at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
        at sun.reflect.GeneratedMethodAccessor380.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:282)
        at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:279)
        at java.security.AccessController.doPrivileged(Native Method)
        at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
        at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:314)
        at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:253)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
        at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:46)
        at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:148)
        at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:144)
        at java.security.AccessController.doPrivileged(Native Method)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:143)
        at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:37)
    [SNIP]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.common.SAMLException: Response has invalid status code urn:oasis:names:tc:SAML:2.0:status:Responder, status message is null
        at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:113)
        at blackboard.auth.provider.saml.customization.consumer.BbSAMLWebSSOProfileConsumerImpl.processAuthenticationResponse(BbSAMLWebSSOProfileConsumerImpl.java:56)
        at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:87)
        ... 247 more

해결 방법

Blackboard Learn 3200.0.0부터는 이제 시스템 관리자 > 빌딩 블록 > 인증 제공자 - SAML > 설정 > 인증서 재생성으로 이동하여 SAML 암호화 인증서를 다시 생성하는 옵션이 제공됩니다. 로그인 오류입니다! 문제는 SP 메타데이터가 ADFS 서버에서 Learn 사이트에 대한 신뢰 당사자 트러스트에 이미 업로드된 이후에 인증서 재생성 버튼이 선택된 경우 발생할 수 있습니다. 이 문제를 해결하려면 다음 작업을 수행하십시오.

  1. 시스템 관리자 > 인증 > [SAML 제공자 이름] > SAML 설정으로 이동합니다.
  2. 서비스 제공자 메타데이터 옆에 있는 생성을 선택하여 새 메타데이터 파일을 저장합니다.
  3. ADFS 서버에 접근하고 Learn 사이트에 대한 신뢰 당사자 트러스트에 새 SP 메타데이터를 업로드합니다.

B2 설정에서 새 인증서를 생성하는 경우 변경 내용을 적용하려면 SAML B2를 비활성으로 전환한 다음, 다시 활성으로 전환해야 합니다. 그런 다음, 제공자 설정으로 돌아가 IDP로 가져올 새 메타데이터를 생성할 수 있습니다. 설정을 전환하지 않으면 새 메타데이터를 생성할 때 계속 이전 인증서가 포함될 수 있습니다. IDP가 업데이트되지 않고 다음에 Learn이 다시 시작할 때 새 인증서가 표시됩니다. 이 불일치로 인해 SAML 인증이 해제됩니다.

페더레이션 메타데이터

ADFS(Active Directory Federation Services)를 사용하는 경우 일반적으로 https://[ADFS Server Hostname]/FederationMetadata/2007-06/FederationMetadata.xml에 위치하는 ADFS 페더레이션에 대한 메타데이터에 SAML 2.0과 호환되지 않는 요소가 있으므로 Blackboard Learn GUI에서 SAML 인증 설정 페이지의 ID 제공자 설정 섹션에 메타데이터를 업로드하기 전에 호환되지 않는 요소를 삭제하려면 메타데이터를 수정해야 합니다. 호환되지 않는 요소가 있는 메타데이터가 업로드된 경우 Blackboard Learn 로그인 페이지에서 SAML 로그인 링크를 선택하면 다음과 같은 오류가 발생합니다. 엔터티 [entity] 및 {} 역할에 대한 메타데이터를 찾을 수 없습니다. 참고로, 오류 ID는 [error ID]입니다.

또한 bb-services 로그의 오류 ID에 해당하는 Java 스택 추적에는 다음 내용이 들어 있습니다.

2016-06-21 11:42:51 -0700 - Metadata for entity https://<Learn Server Hostname>/adfs/ls/ and role {urn:oasis:names:tc:SAML:2.0:metadata}SPSSODescriptor wasn&#39;t found<P><span class="captionText">For reference, the Error ID is c99511ae-1162-4941-b823-3dda19fea157.</span&gt; - org.opensaml.saml2.metadata.provider.MetadataProviderException: Metadata for entity https://ulvsso.laverne.edu/adfs/ls/ and role {urn:oasis:names:tc:SAML:2.0:metadata}SPSSODescriptor wasn't found
    at org.springframework.security.saml.context.SAMLContextProviderImpl.populateLocalEntity(SAMLContextProviderImpl.java:319)
    at org.springframework.security.saml.context.SAMLContextProviderImpl.populateLocalContext(SAMLContextProviderImpl.java:216)
    at org.springframework.security.saml.context.SAMLContextProviderImpl.getLocalAndPeerEntity(SAMLContextProviderImpl.java:126)
    at org.springframework.security.saml.SAMLEntryPoint.commence(SAMLEntryPoint.java:146)
    at org.springframework.security.saml.SAMLEntryPoint.doFilter(SAMLEntryPoint.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor1652.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
        [SNIP]

해결 방법

ADFS 페더레이션의 기본 메타데이터 위치가 https://[ADFS server hostname]/FederationMetadata/2007-06/FederationMetadata.xml이므로 다음 작업을 수행하십시오.

  1. 이 파일을 다운로드하고 텍스트 편집기에서 엽니다. <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"&gt; ... </X509Data></KeyInfo>로 시작하고 </ds:Signature>
    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
    로 끝나는 섹션을 주의하여 삭제합니다.

    <ds:SignedInfo>
      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
      <ds:Reference URI="#_43879f32-9a91-4862-bc87-e98b85b51158">
       <ds:Transforms>
        <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
        <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
       </ds:Transforms>
       <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
       <ds:DigestValue>z1H1[SNIP]jaYM=</ds:DigestValue>
      </ds:Reference>
      </ds:SignedInfo>
      <ds:SignatureValue&gt; FVj[SNIP]edrfNKWvsvk5A==
      </ds:SignatureValue>
      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
       <X509Data>
        <X509Certificate>
        FDdd[SNIP]qTNKdk5F/vf1AocDaX
        </X509Certificate>
       </X509Data>
      </KeyInfo>
    </ds:Signature>

  2. Blackboard Learn GUI(SAML 인증 설정 페이지의 ID 제공자 설정 섹션)에서 업데이트된 메타데이터 XML 파일을 업로드합니다.
  3. SAML 인증 제공자를 '활성' 상태로 두고 SAML 인증 제공자 및 SAML B2를 '비활성/사용 가능'으로 전환합니다.

교육기관이 Blackboard Learn 사이트에서 SAML 인증을 테스트 중이며 Blackboard Learn 사이트에서 동일한 기본 ADFS IdP 메타데이터 XML 파일을 공유하는 여러 SAML 인증 제공자를 보유한 경우 다른 SAML 인증 제공자가 '비활성'으로 설정되었더라도 Blackboard Learn GUI(SAML 인증 설정 페이지의 ID 제공자 설정 섹션)에서 업데이트된 메타데이터 XML 파일을 업로드해야 합니다. 그런 다음, SAML 인증 제공자를 '활성' 상태로 두고 SAML B2를 '비활성'/'사용 가능'으로 전환하여 업데이트된 메타데이터 XML 파일이 시스템 전체에서 인식됨을 확인해야 합니다.

잘못된 사용자 조회 방법

ADFS 로그인 페이지에서 로그인 자격 증명을 입력하면 사용자가 Blackboard Learn GUI로 리디렉션되지만 Blackboard Learn에 로그인되지는 않습니다.

bb-services 로그의 유일한 SAML 인증 관련 이벤트는 다음과 같습니다.

2016-10-18 13:03:28 -0600 - userName is null or empty

해결 방법

  1. 기본 Blackboard Learn 내부 인증을 사용하여 Blackboard Learn에 관리자로 로그인합니다.
  2. 시스템 관리자 > "SAML 인증 제공자 이름" > 수정으로 이동합니다.
  3. 사용자 조회 방법배치 UID에서 사용자명으로 변경합니다.

추가 '단일 로그인 세션 종료' 로그아웃 버튼

ADFS는 Blackboard Learn GUI의 오른쪽 상단에서 로그아웃 버튼을 처음 선택하면 표시되는 모든 세션 종료 페이지에서 단일 로그인 세션 종료 로그아웃 버튼을 추가하려고 시도합니다.

이 작업은 다음과 같이 추가 SingleLogoutService를 IdP 메타데이터 파일에 추가하는 방법을 통해 수행됩니다.

<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://your.server.name/adfs/ls/"/>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://your.server.name/adfs/ls/"/>

리디렉션 엔드포인트에서 제공되고 있는 SAML B2 IdP 구성(선택 사항) 및 서명이 잘못되었으므로 모든 세션 종료 페이지에서 추가 단일 로그인 세션 종료 버튼을 선택하면 다음과 같은 오류가 발생합니다. 수신 SAML 메시지가 보안 유효성 검사에 실패했습니다. 컨텍스트 발급자로 인해 요청이 간단한 서명의 유효성을 검사하지 못했습니다. 참고로, 오류 ID는 [error ID]입니다.

또한 bb-services 로그의 오류 ID에 해당하는 Java 스택 추적에는 다음 내용이 들어 있습니다.

2016-10-17 16:57:44 -0400 - Incoming SAML message failed security validation Validation of request simple signature failed for context issuer<P><span class="captionText">For reference, the Error ID is 930c7767-8710-475e-8415-2077152280e0.</span&gt; - org.opensaml.ws.security.SecurityPolicyException: Validation of request simple signature failed for context issuer
    at org.opensaml.common.binding.security.BaseSAMLSimpleSignatureSecurityPolicyRule.doEvaluate(BaseSAMLSimpleSignatureSecurityPolicyRule.java:139)
    at org.opensaml.common.binding.security.BaseSAMLSimpleSignatureSecurityPolicyRule.evaluate(BaseSAMLSimpleSignatureSecurityPolicyRule.java:103)
    at org.opensaml.ws.security.provider.BasicSecurityPolicy.evaluate(BasicSecurityPolicy.java:51)
    at org.opensaml.ws.message.decoder.BaseMessageDecoder.processSecurityPolicy(BaseMessageDecoder.java:132)
    at org.opensaml.ws.message.decoder.BaseMessageDecoder.decode(BaseMessageDecoder.java:83)
    at org.opensaml.saml2.binding.decoding.BaseSAML2MessageDecoder.decode(BaseSAML2MessageDecoder.java:70)
    at org.springframework.security.saml.processor.SAMLProcessorImpl.retrieveMessage(SAMLProcessorImpl.java:105)
    at org.springframework.security.saml.processor.SAMLProcessorImpl.retrieveMessage(SAMLProcessorImpl.java:172)
    at org.springframework.security.saml.SAMLLogoutProcessingFilter.processLogout(SAMLLogoutProcessingFilter.java:131)
    at org.springframework.security.saml.SAMLLogoutProcessingFilter.doFilter(SAMLLogoutProcessingFilter.java:104)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor1652.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
        [SNIP]

해결 방법

  1. ADFS 서버에 접근하여 Blackboard Learn 인스턴스의 신뢰 당사자 트러스트로 이동합니다.
  2. 속성 > 엔드포인트 탭을 선택합니다.
  3. 엔드포인트 탭에는 2개의 SAML 로그아웃 엔드포인트가 있습니다.
  4. 리디렉션 엔드포인트를 제거합니다.
  5. 엔드포인트를 제거하려면 엔드포인트 제거를 선택한 다음 적용확인을 선택합니다.

리디렉션 엔드포인트를 제거하고 나면 단일 로그인 세션 종료 버튼이 제대로 작동하여 사용자가 로그아웃됩니다.

이벤트 뷰어를 통해 애플리케이션 로그 보기

ADFS SAML 인증 문제를 해결할 때 교육기관에서는 통찰력을 더 확보하기 위해 ADFS 서버의 이벤트 뷰어에서 ADFS 애플리케이션 로그도 검토해야 할 수 있습니다. 이 작업은 특히 ADFS 서버의 SAML 응답에 아래에 표시된 것과 같이 요청 거부됨 상태가 있는 경우 수행해야 합니다.

<samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Responder">
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:RequestDenied" />
    </samlp:StatusCode>
</samlp:Status>

SAML 응답은 Firefox 브라우저의 SAML 추적 프로그램 애드온을 사용하여 확인할 수 있습니다.

응답에서 요청 거부됨 상태는 일반적으로 IdP(ADFS)가 응답을 이해하고 SP(Blackboard Learn)가 제공한 결과를 처리하려고 시도했을 때 문제가 발생했음을 나타냅니다.

이벤트 뷰어를 사용하여 ADFS 애플리케이션 로그를 확인하려면 다음 작업을 수행하십시오.

  1. ADFS 서버에서 이벤트 뷰어를 엽니다.
  2. 보기 메뉴에서 분석 및 디버그 로그 표시를 선택합니다.
  3. 콘솔 트리에서 애플리케이션 및 서비스 로그 > AD FS 추적 > 디버그로 이동합니다.

Azure Active Directory

Azure AD는 MS(Microsoft) 클라우드 기반 디렉터리이자 ID 관리 서비스입니다.

이메일의 앞부분 전송

교육기관에서 Azure AD를 IdP로 사용 중이며 Azure AD 이메일 사용자명의 앞부분만 Blackboard Learn 사용자명에 사용하려는 경우 특수한 ExtractMailPrefix() 함수를 사용하여 이메일 또는 사용자 계정 이름에서 도메인 접미사를 제거하도록 Azure AD IdP를 구성하여 사용자명의 앞부분만 전달되도록 할 수 있습니다(예: joesmith@example.com 대신 "joesmith").

Blackboard Learn의 원격 사용자 IDurn:oid:1.3.6.1.4.1.5923.1.1.1.6인 경우 Azure IdP의 특성 설정은 다음과 같을 수 있습니다.

Attribute Name:        urn:oid:1.3.6.1.4.1.5923.1.1.1.6
Attribute Value:    ExtractMailPrefix()
Mail:            user.userprincipalname

joesmith@example.com 이메일 사용자명의 예시처럼 이는 SAML 어설션에서도 Azure IdP에서 Blackboard Learn으로 다음과 같이 전달됩니다.

<Attribute Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6">
    <AttributeValue>joesmith</AttributeValue>

ExtractMailPrefix() 함수를 사용하는 방법에 대한 자세한 내용은 MS Azure 설명서 페이지에서 확인할 수 있습니다.

Azure AD IdP의 인증서 업데이트

MS Azure AD 로그인 페이지에서 로그인 자격 증명을 입력하면 Blackboard Learn GUI로 리디렉션된 이후에 로그인 오류입니다!가 표시될 수 있습니다.

bb-services 로그에 다음과 같은 예외가 함께 표시됩니다.

2016-10-13 12:03:23 +0800 - unsuccessfulAuthentication - org.springframework.security.authentication.AuthenticationServiceException: Error validating SAML message
 at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:100)
 at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
 at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
 at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
 at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
 at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
 at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
 at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
 at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
 at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
 at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
 at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
 at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
 at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
 at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
 at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
 at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
 at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
 at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
 at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
 at sun.reflect.GeneratedMethodAccessor854.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:498)
 at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:282)
 at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:279)
 at java.security.AccessController.doPrivileged(Native Method)
 at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
 at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:314)
 at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:253)
 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
 at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:46)
 at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:148)
 at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:144)
 at java.security.AccessController.doPrivileged(Native Method)
 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:143)
 at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:30)
 [SNIP]
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
 at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
 at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.common.SAMLException: Response doesn't have any valid assertion which would pass subject validation
 at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:229)
 at blackboard.auth.provider.saml.customization.consumer.BbSAMLWebSSOProfileConsumerImpl.processAuthenticationResponse(BbSAMLWebSSOProfileConsumerImpl.java:40)
 at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:87)
 ... 230 more
Caused by: org.opensaml.xml.validation.ValidationException: Signature is not trusted or invalid
 at org.springframework.security.saml.websso.AbstractProfileBase.verifySignature(AbstractProfileBase.java:272)
 at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.verifyAssertionSignature(WebSSOProfileConsumerImpl.java:419)
 at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.verifyAssertion(WebSSOProfileConsumerImpl.java:292)
 at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:214)
 ... 232 more

이 오류는 MS Azure AD IdP에서 인증서를 업데이트하지만 Blackboard Learn SP에서 사용하는 메타데이터 XML이 새로운 인증서를 반영하도록 조정되지 않기 때문에 발생합니다.

해결 방법

  • 새로운 인증서가 포함된 새로운 메타데이터 XML 파일을 Blackboard Learn GUI의 SAML 설정 페이지에서 인증 제공자에 대해 업데이트해야 합니다.
  • 그런 다음, SAML 인증 제공자를 '활성' 상태로 두고 SAML B2 및 인증 제공자를 '비활성'/'사용 가능'으로 전환하여 새로운 인증서가 적용되도록 메타데이터를 업데이트해야 합니다.
  • Blackboard Learn 사이트에 동일한 기본 IdP 엔터티 ID에 대해 동일한 기본 인증서를 공유하는 인증 제공자가 여럿 있는 경우 이러한 인증 제공자를 모두 업데이트해야 합니다.

Microsoft는 이제부터 6주마다 인증서를 업데이트할 것이라고 밝혔으며 해당 업데이트에 대해 공지할 예정입니다.


IdP에서 시작한 단일 로그인

사용자가 사용자 포털에 처음 로그인한 다음 Blackboard Learn 사이트용 앱을 선택하는 경우 새로운 브라우저 탭이 열리고 다음 메시지가 표시됩니다. 지정된 리소스를 찾을 수 없거나 해당 리소스에 접근할 권한이 없습니다.

stdout-stderr.log에 해당하는 SAML 관련 이벤트가 다음과 같이 함께 표시됩니다.

INFO   | jvm 1    | 2016/08/16 10:49:22 | - /saml/SSO at position 1 of 10 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - HttpSession returned null object for SPRING_SECURITY_CONTEXT
INFO   | jvm 1    | 2016/08/16 10:49:22 | - No SecurityContext was available from the HttpSession: [email protected] A new one will be created.
INFO   | jvm 1    | 2016/08/16 10:49:22 | - /saml/SSO at position 2 of 10 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - /saml/SSO at position 3 of 10 in additional filter chain; firing Filter: 'HeaderWriterFilter'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - /saml/SSO at position 4 of 10 in additional filter chain; firing Filter: 'FilterChainProxy'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Checking match of request : '/saml/sso'; against '/saml/login/**'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Checking match of request : '/saml/sso'; against '/saml/logout/**'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Checking match of request : '/saml/sso'; against '/saml/bbsamllogout/**'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Checking match of request : '/saml/sso'; against '/saml/sso/**'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - /saml/SSO at position 1 of 1 in additional filter chain; firing Filter: 'SAMLProcessingFilter'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Forwarding to /
INFO   | jvm 1    | 2016/08/16 10:49:22 | - DispatcherServlet with name 'saml' processing POST request for [/auth-saml/saml/SSO]
INFO   | jvm 1    | 2016/08/16 10:49:22 | - No mapping found for HTTP request with URI [/auth-saml/saml/SSO] in DispatcherServlet with name 'saml'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Successfully completed request
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Skip invoking on
INFO   | jvm 1    | 2016/08/16 10:49:22 | - SecurityContextHolder now cleared, as request processing completed

SAML 인증 설정 페이지의 서비스 제공자 설정 섹션이 변경되었으며 자동 단일 로그인 활성화 옵션을 선택하여 사용자가 포털에서 Blackboard Learn에 접근할 수 있도록 허용해야 합니다. 이 옵션이 활성화된 경우 ACS URL도 별칭을 포함하도록 변경됩니다.


잘못된 문서 오류

SAML 인증 제공자 로그인 페이지에서 로그인 자격 증명을 입력하면 Blackboard Learn GUI로 리디렉션된 이후에 로그인 오류입니다!가 표시될 수 있습니다.

bb-services 로그에 DOMExceptionWRONG_DOCUMENT_ERR이 다음과 같이 함께 표시됩니다.

2016-11-18 12:27:31 -0600 - WRONG_DOCUMENT_ERR: A node is used in a different document than the one that created it.<P><span class="captionText">For reference, the Error ID is 86ebb81d-d3a3-4da5-95ab-1c94505f4281.</span&gt; - org.w3c.dom.DOMException: WRONG_DOCUMENT_ERR: A node is used in a different document than the one that created it.
    at org.apache.xerces.dom.ParentNode.internalInsertBefore(Unknown Source)
    at org.apache.xerces.dom.ParentNode.insertBefore(Unknown Source)
    at org.apache.xerces.dom.NodeImpl.appendChild(Unknown Source)
    at org.opensaml.xml.encryption.Decrypter.parseInputStream(Decrypter.java:832)
    at org.opensaml.xml.encryption.Decrypter.decryptDataToDOM(Decrypter.java:610)
    at org.opensaml.xml.encryption.Decrypter.decryptUsingResolvedEncryptedKey(Decrypter.java:795)
    at org.opensaml.xml.encryption.Decrypter.decryptDataToDOM(Decrypter.java:535)
    at org.opensaml.xml.encryption.Decrypter.decryptDataToList(Decrypter.java:453)
    at org.opensaml.xml.encryption.Decrypter.decryptData(Decrypter.java:414)
    at org.opensaml.saml2.encryption.Decrypter.decryptData(Decrypter.java:141)
    at org.opensaml.saml2.encryption.Decrypter.decrypt(Decrypter.java:69)
    at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:199)
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:82)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
    at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor1209.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:277)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:274)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
    at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:309)
    at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:249)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:237)
    at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:55)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:191)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:187)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:186)
    at blackboard.platform.servlet.DevNonceFilter.doFilter(DevNonceFilter.java:68)
    [SNIP]

이 문제는 다른 B2/프로젝트에서 시스템 속성인 javax.xml.parsers.DocumentBuilderFactory 값을 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl에서 com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl.로 변경했기 때문에 발생합니다.

임시 해결 방법

수정 프로그램이 릴리즈될 때까지 임시 해결 방법의 옵션은 다음과 같습니다.

  1. 각 노드에서 Bb 서비스를 다시 시작합니다.
  2. IdP 측에서 SAML 응답 암호화를 해제합니다. 전체 통신이 SSL을 통과하므로 이로 인해 인증 보안의 수준이 감소하지 않습니다.

SAML을 제공자 순서에 추가하는 옵션 없음

SAML 인증을 구성할 때 교육기관은 Blackboard Learn GUI의 제공자 순서 섹션에서 SAML 인증 제공자를 추가하는 옵션이 없는 것을 알 수 있습니다(시스템 관리자 > 빌딩 블록: 인증 > 제공자 순서로 이동).

SAML 인증 제공자를 제공자 순서에 추가하는 옵션이 없는 이유는 CAS 및 SAML과 같은 리디렉션 유형 제공자가 인증을 원격 인증 소스에 전달하기 때문입니다. 이러한 제공자는 믿을 수 있는 인증 소스로 간주되며 자신의 인증 실패를 직접 처리하기 때문에 제공자 순서에 나열되지 않습니다.


SAML 연결 테스트

Blackboard Learn의 2016 Q4 릴리즈부터 Blackboard Learn GUI의 인증 섹션에서 SAML 제공자에 대한 연결을 테스트하는 옵션이 제공되고 있습니다. 연결 테스트에서는 다음 항목을 확인합니다.

  • IdP 메타데이터 구문 분석
  • IdP에 대한 연결
  • SAML 응답 수신
  • SAML 응답 구문 분석
  • 원격 사용자 ID 일치
  • Blackboard Learn에 로그인

SAML 인증 제공자에 대한 연결을 테스트하려면 다음 작업을 수행하십시오.

  1. Blackboard Learn에 관리자로 로그인합니다.
  2. 시스템 관리자 > 빌딩 블록: 인증 > "SAML 제공자 이름" > 연결 테스트로 이동합니다.
  3. 메시지가 표시되면 IdP 로그인 자격 증명을 입력합니다.

여러 가지 이유로 연결 테스트 기능은 Blackboard Learn에서 SAML 디버그 로깅을 수동으로 활성화하는 것 대신 사용될 수 있습니다.

연결 테스트 출력 페이지에 표시되는 ID 제공자 엔터티 ID 값은 다음과 같이 사용자가 인증을 받은 다음에 IdP의 SAML POST에 있는 Issuer 요소에서 Blackboard Learn으로 가져오기됩니다.

<Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://bbpdcsi-adfs1.bbpdcsi.local/a...services/trust</Issuer>

SAML 응답 섹션의 연결 테스트 출력 페이지에 표시되는 SAML 특성 값은 다음과 같이 사용자가 인증을 받은 다음에 IdP의 SAML POST에 있는 SubjectAttributeStatement 요소에서 Blackboard Learn으로 가져오기됩니다.

<Subject>
    <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">luke.skywalker</NameID>
    [SNIP]
</Subject>
<AttributeStatement>
    <Attribute Name="SamAccountName">
        <AttributeValue>luke.skywalker</AttributeValue>
    </Attribute>
    <Attribute Name="urn:oid:2.5.4.42">
        <AttributeValue>Luke</AttributeValue>
    </Attribute>
    <Attribute Name="urn:oid:2.5.4.4">
        <AttributeValue>Skywalker</AttributeValue>
    </Attribute>
</AttributeStatement>


테스트용으로 SAML 인증 제공자 및 IdP 생성

Centrify의 무료 단일 로그인 인증 솔루션을 사용하여 IdP(ID 제공자)를 생성하려면 아래의 단계를 따르십시오.

해당 IdP는 다음과 같이 Blackboard Learn SP(서비스 제공자)에서 SAML 인증 제공자로 구성될 수 있습니다.

Blackboard Learn 서비스 제공자

  1. Blackboard Learn GUI에 관리자로 로그인하고 시스템 관리자 > 인증으로 이동합니다.
  2. 제공자 생성 > SAML을 선택합니다.
  3. 다음 설정을 입력합니다.
    • 이름 > SAML 또는 원하는 항목
    • 인증 제공자 > 비활성(당분간)
    • 사용자 조회 방법 > 사용자명
    • 호스트명으로 제한 > 모든 호스트명에 이 제공자 사용
    • 링크 텍스트 > SAML Centrify 로그인
  4. 저장 및 구성을 선택합니다.
  5. 엔터티 ID 필드에서 원하는 대로 설정합니다(단, 이 설정을 변경하는 경우 업데이트된 서비스 제공자 메타데이터를 ID 제공자에게 제공해야 함). ACS URL을 복사하고 붙여넣기만 하면 됩니다.
  6. '서비스 제공자 메타데이터' 아래에서 생성을 선택하고 파일을 데스크톱에 저장합니다.
  7. 데이터 소스 아래에서 이름이 지정된 이 CENTRIFY에 대해 새로운 데이터 소스를 생성하는 것이 좋습니다. 아니면 SYSTEM이나 선택하는 항목을 사용하십시오.
  8. 사용자가 없는 경우 이 SAML 인증 제공자를 통해 로그인하려고 시도할 때 계정이 자동으로 생성되도록 JIT 프로비저닝 활성화 옆의 상자를 선택합니다. JIT 프로비저닝을 선택하지 않은 경우 Blackboard Learn 사용자를 수동으로 생성해야 합니다.
  9. 호환되는 데이터 소스 목록에서 이 인증 제공자와 호환되어야 하는 데이터 소스를 선택합니다.
  10. ID 제공자 유형에 대해 포인트 ID 제공자를 선택합니다.
  11. 지금은 ID 제공자 메타데이터를 건너뜁니다. Centrify IdP 섹션에서 파일을 생성한 후 업로드할 것입니다.
  12. SAML 특성 매핑 섹션에서 원격 사용자 IDNameID를 사용합니다.
  13. 제출을 선택합니다.

Centrify ID 제공자

  1. https://www.centrify.com/express/identity-service로 이동하고 페이지 하단에 있는 Express 열에서 지금 시작을 선택합니다.
  2. 정보를 입력하여 등록하고 지금 시작을 선택합니다.
  3. 관리자 자격 증명이 포함된 환영 이메일을 받게 됩니다. 이 자격 증명을 사용하여 https://cloud.centrify.com에 로그인합니다.
  4. Centrify ID 서비스 환영 창에서 건너뛰기를 선택합니다.
  5. 페이지 상단의 탭에서 웹 앱 추가 버튼을 선택합니다.
  6. 사용자 지정 탭에서 아래로 스크롤하여 SAML의 추가 버튼을 선택합니다. 를 선택합니다.
  7. 웹 앱 추가 창의 하단에서 닫기를 선택합니다.
  8. 탭으로 이동합니다. 애플리케이션 설정 섹션에서 SP 메타데이터 업로드 버튼을 선택하고 Blackboard Learn SP 섹션의 6단계에서 생성한 파일을 업로드합니다.
  9. 어설션 소비자 서비스 URL은 SP 메타데이터를 업로드한 후에 자동으로 채워져야 합니다.
  10. 어설션 암호화의 선택을 취소합니다. 이렇게 하면 IdP에서 릴리즈되어 Blackboard Learn으로 전송 중인 특성을 Firefox 브라우저 SAML 추적 프로그램의 애드온 또는 Chrome SAML Message Decoder를 사용하여 확인할 수 있습니다. 전체 통신이 SSL을 통과하므로 이로 인해 인증 보안의 수준이 감소하지 않습니다.
  11. 아래로 스크롤하여 ID 제공자 SAML 메타데이터 다운로드를 선택합니다. 파일을 데스크톱에 저장합니다.
  12. 저장을 선택하고 다음 섹션으로 이동합니다.
  13. 설명 섹션의 이름을 입력합니다. 저장을 선택하고 다음 섹션으로 이동합니다.
  14. 사용자 접근 섹션에서 모든 사람시스템 관리자를 선택합니다. 저장을 선택합니다.
  15. 정책 섹션에서는 아무 항목도 선택하지 않습니다.
  16. 계정 매핑 섹션의 경우 userprincipalname디렉터리 서비스 필드 이름에 입력했는지 확인합니다.
  17. 고급 섹션의 경우 애플리케이션에 대한 SAML 어설션을 생성하는 데 사용된 스크립트의 하단에 다음과 같은 줄을 추가합니다.

    전체 스크립트는 다음과 같습니다.

    setIssuer(Issuer);
    setSubjectName(UserIdentifier);
    setAudience('https://YourLearnServer.blackboard.c...saml/saml/SSO');
    setRecipient(ServiceUrl);
    setHttpDestination(ServiceUrl);
    setSignatureType('Assertion');
    setNameFormat('emailaddress');
    setAttribute("NameID", LoginUser.Get("userprincipalname"));

    이 스크립트에서는 Centrify IdP가 SAML POST에 있는 사용자 ID와 함께 AttributeStatement를 릴리즈할 수 있습니다.

    예:

    <AttributeStatement>
        <Attribute Name="NameID"
                   NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
                   >
            <AttributeValue>luke.skywalker@blackboard.com.47</AttributeValue>
        </Attribute>
    </AttributeStatement>

    Centrify SAML 스크립트에서 어설션 요소를 지정하는 방법 자세히 알아보기

  18. 저장을 선택합니다.
  19. 나머지 섹션(앱 게이트웨이, 변경 로그워크플로)은 변경할 필요가 없습니다.
  20. 탭에서 SAML 앱이 자동으로 배포되었는지 확인합니다.
  21. 사용자 탭에서 사용자 추가를 선택하고 사용자의 계정 정보를 입력한 다음 사용자 생성을 선택합니다.
  22. Blackboard Learn GUI에 관리자로 다시 로그인하고 시스템 관리자 > 인증 > SAML 인증 제공자 이름 > SAML 설정 > ID 제공자 설정으로 이동한 다음 13단계에서 데스크톱에 저장한 IdP 메타데이터 파일을 업로드하고 제출을 선택합니다.

생성된 Centrify IdP 사용자는 이제 로그인 페이지에서 인증 제공자를 선택하여 SAML을 통해 Blackboard Learn에 로그인할 수 있고, Blackboard Learn의 오른쪽 상단에서 로그아웃 버튼을 선택한 후에 표시되는 모든 세션 종료 페이지에서 추가 단일 로그인 세션 종료 로그아웃 버튼을 사용하여 Blackboard Learn에서 로그아웃할 수 있습니다.


단일 로그인 세션 종료 로그아웃 페이지에서 텍스트 변경

단일 로그인 세션 종료 로그아웃 페이지에서 텍스트를 변경할 수 있는지 문의하는 교육기관이 있을 수 있습니다. 다음과 같이 언어 팩을 수정하여 단일 로그인 세션 종료 로그아웃 페이지에서 텍스트를 변경할 수 있습니다.

  1. 언어 팩 파일을 엽니다.
  2. auth-provider-saml/src/main/webapp/WEB-INF/bundles/bb-manifest-en_US.properties로 이동합니다.
  3. 다음과 같이 메시지 키를 업데이트합니다.

    saml.single.logout.warning.conent.description // the first line
    saml.single.logout.warning.conent.recommend // second line
    saml.single.logout.warning.endsso.title // third line
    saml.single.logout.warning.endsso.button // the button
    saml.single.logout.warning.backtolearn // the cancel button


IdP 로그인 페이지로 사용자 리디렉션

표준 Blackboard Learn 로그인 페이지에 기본 Learn 내부 인증 제공자에 대한 사용자명 및 비밀번호 필드가 표시됩니다. SAML 인증을 활성화하는 경우 이 페이지 하단에 SAML에 대해 "다음을 사용하여 로그인..."이라는 작은 링크가 나타나므로 사용자가 Learn 로그인 페이지에 접근할 때 IdP의 인증 서버로 자동으로 리디렉션되게 할 수 있습니다.

이 작업을 완료하는 한 가지 방법은 시스템 관리자 > 인증으로 이동하고 기본 Learn 내부 인증을 '비활성'으로 설정하는 것입니다. 이렇게 하면 로그인 페이지가 더 이상 표시되지 않으며 사용자가 SAML 로그인으로 즉시 리디렉션됩니다. 이 방법의 문제는 기본 로그인 URL이 덮어씌워지고 비 SAML 사용자는 모두 로그인할 수 없게 된다는 점입니다.

이 문제를 방지하면서 거의 동일한 결과를 제공하려면 '사용자 지정 로그인 페이지'를 사용하십시오. 그러면 사용자가 SAML 인증 제공자의 IdP 로그인 페이지로 리디렉션되지만 기본 로그인 링크도 사용할 수 있습니다.

  1. 기본 Learn 내부 인증이 활성 상태인지 확인합니다.
  2. 기본 로그인 페이지에서 제공자 리디렉션의 위치(예: 다음을 사용하여 로그인... SAML)를 복사합니다. 링크를 마우스 오른쪽 버튼을 클릭하고 링크 위치 복사를 선택합니다.
  3. 시스템 관리자 > 커뮤니티 > 브랜드 및 테마 > 로그인 페이지 사용자 지정으로 이동합니다.
  4. 기본 로그인 페이지 옆에 있는 다운로드를 선택하여 기본 로그인 JSP 파일을 다운로드합니다.
  5. 텍스트 편집기로 JSP 파일을 엽니다. 다음과 같은 샘플 HTML을 로그인 JSP 파일에 추가하고 URL 텍스트를 2단계에서 복사한 URL로 대체합니다.

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
    <html>
    <head>
    <title>Blackboard Learn - Redirect</title>
    <meta http-equiv="REFRESH" content="0;url=https://URL_Goes_Here"></HEAD>
    <BODY style="font-family: arial,sans-serif;font-size: small; color: grey; padding: 1em; ">
    Redirecting... <a style="color:grey" href="https://URL_Goes_Here">Go to login page</a&gt; if you are not automatically redirected.
    </BODY>
    </HTML>

  6. Learn에서 한 번 더 로그인 페이지 사용자 지정으로 이동합니다. 사용자 지정 페이지 사용을 선택한 다음 업데이트된 로그인 JSP 파일을 업로드합니다.
  7. 변경을 수행한 후에 로그인 페이지 사용자 지정에서 미리 보기를 선택하여 리디렉션이 제대로 작동하는지 확인합니다.

기본 URL로 이동 중인 사용자는 이제 SAML 인증 제공자를 위한 로그인 페이지로 리디렉션됩니다. 관리자는 기본 로그인 페이지인 /webapps/login/?action=default_login 또는/webapps/login/login.jsp를 통해 Learn 내부 인증을 사용하여 계속 로그인할 수 있습니다.

Ultra 환경에서 로그인 페이지를 사용자 지정하는 방법 자세히 알아보기