SAML 로그인은 되는데 로그아웃만 깨진다. 로그에는 다음 두 가지가 번갈아 나온다.
unsupported binding urn:oasis:names:tc:SAML:2.0:bindings:SOAP
func=xmlSecOpenSSLEvpSignatureExecute:file=evp_signatures.c
error:rsa_ossl_private_encrypt: missing private key
그리고 로그아웃 후 사용자가 원래 화면이 아니라 낯선 오류 페이지로 떨어진다.
SAML 2.0 의 단일 로그아웃(SLO)은 로그인과 대칭이다. 서비스 제공자(SP)가 LogoutRequest 를 만들어 신원 제공자(IdP)에 보내고, IdP 가 LogoutResponse 를 돌려준다. 이 두 메시지 모두 보낸 쪽의 개인키로 서명하고 받는 쪽이 상대의 공개 인증서로 검증한다.
로그인만 되고 로그아웃이 안 되는 구성은 대개 다음 상태다. 로그인 시 SP 는 IdP 가 서명한 Assertion 을 검증만 하면 되므로 IdP 인증서만 있으면 된다. 반대로 로그아웃에서는 SP 가 서명을 만들어야 하므로 SP 개인키가 필요하다. 그 개인키가 없거나 읽히지 않으면 위의 missing private key 가 난다.
확인할 것은 세 가지다.
첫째, SP 설정의 개인키 경로가 실제 파일을 가리키는지, 프로세스 계정이 읽을 수 있는지 본다.
ls -l /etc/app/saml/sp.key
openssl rsa -in /etc/app/saml/sp.key -check -noout
둘째, 인증서와 개인키가 짝인지 본다. 두 값이 같아야 한다.
openssl x509 -noout -modulus -in /etc/app/saml/sp.crt | openssl md5
openssl rsa -noout -modulus -in /etc/app/saml/sp.key | openssl md5
셋째, 키가 암호로 잠겨 있으면 라이브러리가 열지 못한다. xmlsec 계열은 비밀번호를 따로 넘겨야 하므로, 서비스 계정이 쓰는 키는 보통 암호를 푼 형태로 둔다.
openssl rsa -in sp-encrypted.key -out sp.key
chmod 600 sp.key
SAML 바인딩은 메시지를 어떤 전송 수단에 실을지를 정한 규약이다. 로그아웃에서 실제로 쓰이는 것은 세 가지다.
| 바인딩 URN | 전달 방식 | 쓰임 |
|---|---|---|
...bindings:HTTP-Redirect |
브라우저 302, 파라미터에 서명 | 프런트 채널 로그아웃 |
...bindings:HTTP-POST |
브라우저 자동 제출 폼 | 프런트 채널 로그아웃 |
...bindings:SOAP |
SP↔IdP 서버 간 직접 호출 | 백 채널 로그아웃 |
unsupported binding ... SOAP 는 상대가 백 채널 로그아웃을 구현하지 않았다는 뜻이다. SOAP 바인딩은 브라우저를 거치지 않고 SP 와 IdP 가 서로 HTTP 로 직접 호출해야 해서, 망이 분리된 환경에서는 애초에 성립하지 않는다. 지원 범위는 양쪽 메타데이터에 적혀 있다.
curl -s https://idp.example.com/FederationMetadata/2007-06/FederationMetadata.xml \
| grep -o 'SingleLogoutService[^>]*'
메타데이터에 SingleLogoutService 가 Redirect · POST 만 있으면 SP 쪽 설정을 그 둘 중 하나로 바꾼다. 양쪽이 지원하는 바인딩이 하나도 겹치지 않으면 SLO 자체를 포기하고 SP 로컬 로그아웃만 하도록 설계를 바꾸는 편이 낫다.
로그아웃이 성공했는데 화면이 이상하다면 순서가 어긋난 것이다. 정상 흐름은 다음과 같다.
SP 가 자기 세션과 쿠키를 지운다. 이어서 IdP 의 SLO 엔드포인트로 LogoutRequest 를 보낸다. IdP 가 IdP 세션을 끊고 LogoutResponse 를 SP 의 SLO 응답 엔드포인트로 돌려보낸다. SP 가 그 응답을 받고 나서야 최종 착지 페이지로 리다이렉트한다.
자주 어긋나는 지점은 두 곳이다. SP 메타데이터의 SingleLogoutService Location 이 응답을 받을 수 있는 실제 경로가 아니거나, IdP 에 등록된 로그아웃 후 허용 URL 목록에 착지 주소가 빠져 있는 경우다. 허용 목록에 없는 주소로 보내려 하면 IdP 가 자기 오류 페이지를 띄운다.
AD FS 를 쓰는 환경이라면 WS-Federation 의 로그아웃 주소와 SAML 의 SLO 주소가 서로 다르다는 점에 주의한다.
WS-Federation : https://adfs.example.com/adfs/ls/?wa=wsignout1.0
SAML 2.0 SLO : https://adfs.example.com/adfs/ls/
SAML 로 붙여 놓고 로그아웃만 wsignout1.0 으로 던지면 IdP 세션은 끊기지만 SP 는 LogoutResponse 를 못 받아 흐름이 끊긴다.
브라우저 개발자 도구에서 네트워크 기록을 보존한 채 로그아웃을 눌러, SAMLRequest · SAMLResponse 파라미터를 잡는다. 이 값은 Base64 이고 Redirect 바인딩이면 DEFLATE 압축까지 돼 있다.
echo '<SAMLRequest 값>' | base64 -d | python3 -c \
"import sys,zlib;print(zlib.decompress(sys.stdin.buffer.read(),-15).decode())"
풀린 XML 에서 Destination · Issuer · NameID 가 IdP 가 기대하는 값과 같은지 확인한다. NameID 형식이 로그인 때와 다르면 IdP 가 세션을 찾지 못하고 조용히 실패한다.
SLO 는 표준 준수율이 낮다. 상용 제품 사이에서도 절반은 제대로 맞물리지 않으므로, 설계 단계에서 "SP 로컬 로그아웃만 보장하고 IdP 세션은 만료에 맡긴다"는 선택지를 열어 둔다.
서명 알고리즘도 맞춰야 한다. 최근 IdP 는 SHA-1 서명을 거부하므로 SP 쪽 설정을 rsa-sha256 으로 둔다.