대형 고객이라면 언젠가 반드시 이런 질문을 합니다. 우리 직원들이 회사 계정으로 당신들의 애플리케이션에 로그인할 수 있나요? 이는 또 다른 사용자 이름과 비밀번호를 의미하는 것이 아닙니다. 그들이 이미 다른 모든 곳에서 사용하고 있는 Microsoft Entra ID, Okta 또는 AD FS 로그인을 의미하며, 여기에는 자체 비밀번호 정책, 자체 2단계 인증, 그리고 누군가 퇴사하는 날 계정을 한 곳에서 비활성화할 수 있는 체계가 포함됩니다.
고객의 ID 팀이 기대하는 답은 SAML 2.0입니다. 새로운 로그인 컴포넌트 개요에서 SAML은 한 단락만 다뤄졌습니다. 이 글은 전체 흐름을 다룹니다. TsgcSAMLServiceProvider가 하는 일, 로그인 페이지와 Assertion Consumer Service의 코드, 일반적인 identity provider에 애플리케이션을 등록하는 방법, 그리고 어디에도 계정 없이 오늘 바로 전부 테스트하는 방법까지 말입니다.
SAML 로그인의 작동 방식
세 주체가 참여합니다. 애플리케이션은 service provider(SP)입니다. 고객의 디렉터리는 identity provider(IdP)입니다. 브라우저가 둘 사이에서 메시지를 전달하므로, 서버와 IdP는 직접 통신하지 않습니다.
- 사용자가 로그인 URL을 엽니다. 애플리케이션은 AuthnRequest를 만들어 브라우저를 IdP로 리디렉션합니다.
- IdP는 회사가 정한 비밀번호, MFA, 조건부 액세스 규칙에 따라 사용자를 로그인시킵니다.
- IdP는 서명된 SAMLResponse로 응답하고, 브라우저는 이를 Assertion Consumer Service(ACS) URL로 POST합니다.
- 애플리케이션은 응답을 검증하고, 그 안에 지정된 사용자를 위해 자체 세션을 생성합니다.
네 번째 단계가 바로 SAML 구현이 잘못되기 쉬운 지점이며, 이 컴포넌트가 여러분을 대신해 처리해 주는 부분입니다.
Service Provider, 단계별로 살펴보기
TsgcSAMLServiceProvider는 HTTP 서버가 아닙니다. SAML 메시지를 구성하고 검사하는 역할을 하며, 애플리케이션이 이미 가지고 있는 서버의 요청 핸들러에서 이를 호출합니다. 예를 들어 TsgcWebSocketHTTPServer나 TsgcHTTPServer가 있습니다.
- 애플리케이션을 설명합니다. 애플리케이션의 고유 이름(보통 메타데이터 URL)인
EntityID와 응답이 도착하는 https URL인AssertionConsumerServiceURL을 설정합니다. - identity provider를 설명합니다. IdP 메타데이터 문서로
LoadIdPMetadata를 호출합니다. 이 메서드는 IdP의 entity ID, 로그인 URL과 binding, 그리고 모든 서명 인증서를 읽어들입니다. 메타데이터가 없다면IdPEntityID,IdPSSOURL,IdPCertificates를 직접 설정합니다. - 요청을 전송합니다.
GetAuthnRequestRedirectURL은 브라우저를 리디렉션할 URL을 반환합니다. HTTP-POST binding만 지원하는 IdP의 경우,GetAuthnRequestPostForm이 대신 요청을 POST하는 페이지를 반환합니다. - 요청 id를 보관합니다. 두 메서드 모두 새 AuthnRequest의 id를 반환합니다. 이를 무작위 RelayState나 세션 쿠키를 키로 하여 서버에 저장하고, 응답이 도착하면 제거하여 각 요청이 한 번만 응답받도록 합니다.
- 응답을 처리합니다. ACS URL에서 전달된 SAMLResponse, RelayState, 저장된 요청 id로
ProcessResponse를 호출합니다.True를 반환하면TsgcSAMLResult에NameID,SessionIndex, IdP가 보낸 모든 속성이 담깁니다.False를 반환하면ErrorMessage가 이유를 설명하고OnSAMLError가 발생합니다.
시작 시 EntityID, AssertionConsumerServiceURL, LoadIdPMetadata를 한 번 설정해 두면, 로그인 페이지와 ACS를 하나의 요청 핸들러 안에 담을 수 있습니다.
uses
sgcAuth_SAML_SP;
procedure TMyApp.OnCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
vRelayState, vRequestID: string;
oResult: TsgcSAMLResult;
begin
if ARequestInfo.Document = '/saml/login' then
begin
// 1. send the browser to the identity provider
vRelayState := NewRelayState;
AResponseInfo.Redirect(FSAML.GetAuthnRequestRedirectURL(vRelayState,
vRequestID));
// 2. keep the request id, the response must answer it
AddPendingRequest(vRelayState, vRequestID);
end
else if (ARequestInfo.Document = '/saml/acs') and
SameText(ARequestInfo.Command, 'POST') then
begin
// 3. the browser posts SAMLResponse and RelayState back
vRelayState := ARequestInfo.Params.Values['RelayState'];
vRequestID := TakePendingRequest(vRelayState);
oResult := TsgcSAMLResult.Create;
try
if FSAML.ProcessResponse(ARequestInfo.Params.Values['SAMLResponse'],
vRelayState, vRequestID, oResult) then
begin
// 4. signed in: create your own session for this user
CreateUserSession(AResponseInfo, oResult.NameID, oResult.SessionIndex);
AResponseInfo.Redirect('/');
end
else
AResponseInfo.ResponseNo := 403; // log oResult.ErrorMessage
finally
oResult.Free;
end;
end;
end;
NewRelayState, AddPendingRequest, TakePendingRequest, CreateUserSession은 여러분 자신의 코드를 나타냅니다. 즉 GUID, RelayState를 키로 하여 각 요청 id를 한 번만 내어주는 thread safe 리스트, 그리고 애플리케이션의 세션 쿠키입니다. 속성은 Name=Value 형태의 줄로 도착하므로 oResult.Attributes.Values['email']로 이름을 지정해 하나를 읽을 수 있습니다. Entra ID는 http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress와 같은 claim URI로 속성 이름을 지정합니다.
RelayState는 IdP 서명의 보호 대상이 아닙니다. 이를 여러분 자신의 대기 중인 요청을 찾는 키로만 사용하고, 확인 없이 리디렉션하는 URL로는 절대 사용하지 마십시오.
Identity Provider에 애플리케이션 등록하기
GetMetadata는 service provider의 메타데이터를 반환합니다. 즉 entity ID와 HTTP-POST binding을 사용하는 ACS URL입니다. 이를 /saml/metadata와 같은 URL로 제공하거나 파일로 저장하여 IdP에 전달합니다. 모든 identity provider는 SP의 entity ID와 ACS URL이라는 동일한 두 값을 요구하므로, 아래 설명은 주로 각 콘솔이 이를 어디에 보관하는지에 관한 것입니다. 모든 경우에 어설션 암호화는 꺼둡니다.
- Microsoft Entra ID. Enterprise applications, New application, Create your own application (non-gallery). Single sign-on에서 SAML을 선택한 다음 SP 메타데이터를 업로드하거나 Identifier (Entity ID)와 Reply URL을 입력합니다. 사용자나 그룹을 할당하고, SAML Certificates에 표시된 App Federation Metadata Url을 불러옵니다.
- Okta. Applications, Create App Integration, SAML 2.0. Single sign-on URL은 여러분의 ACS URL이며, “Use this for Recipient URL and Destination URL”을 체크하고, Audience URI는 여러분의 entity ID입니다. email, firstName, lastName 같은 attribute statements를 추가하고, 사람이나 그룹을 할당한 다음, Sign On 탭에서 Metadata URL을 불러옵니다.
- AD FS. claims aware Relying Party Trust를 추가하고 SP 메타데이터를 가져옵니다. AD FS는 https 엔드포인트만 허용합니다. Name ID를 전송하는 claim rules를 추가합니다. 예를 들어 E-Mail-Addresses를 E-Mail Address로 전송한 다음, E-Mail Address를 Name ID로 변환합니다. IdP 메타데이터는
https://<adfs-host>/FederationMetadata/2007-06/FederationMetadata.xml에 있습니다. - Google Workspace. Admin console, Apps, Web and mobile apps, Add custom SAML app. IdP 메타데이터를 다운로드하고, ACS URL과 entity ID를 입력하고, Name ID(예: 기본 이메일)를 선택한 다음 사용자를 위해 앱을 켭니다.
- Keycloak. Client ID가 여러분의 entity ID인 SAML client를 만들거나 SP 메타데이터를 가져옵니다. Keycloak은 기본적으로 문서 전체에 서명하므로, Sign assertions도 함께 활성화합니다. Client signature required가 켜져 있다면
SignAuthnRequests,SPCertificate,SPPrivateKey를 설정합니다. IdP 메타데이터는https://<host>/realms/<realm>/protocol/saml/descriptor에 있습니다.
어떤 IdP든 마지막 단계는 동일합니다. IdP의 메타데이터를 LoadIdPMetadata에 전달합니다. 문서가 여러 엔티티를 설명하는 경우, 두 번째 매개변수로 여러분의 것을 선택합니다.
ProcessResponse가 검사하는 것
SAML 응답은 서명된 XML 문서이며, 잘 알려진 SAML 취약점 대부분은 service provider가 실제로 서명된 것과 다른 내용을 읽도록 만드는 방법입니다. 응답은 다음 검사를 모두 통과해야만 수락됩니다.
- IdP 인증서에 대한 서명 검증. 응답은
IdPCertificates에 있는 인증서로만 검증됩니다. 메시지에 포함된 인증서는 결코 신뢰되지 않습니다. 공격자도 마찬가지로 인증서를 포함시킬 수 있기 때문입니다. 기본값인WantAssertionsSigned를 사용하면 어설션 자체도 자신의 서명을 가져야 합니다. - Signature wrapping 방어. 서명은 문서 내에서 ID가 고유한 요소를 참조해야 하며, 검증 후에는 서명된 요소만 읽습니다. 서명된 어설션 옆에 끼워 넣은 서명되지 않은 어설션은 절대 확인되지 않습니다.
- 단일 어설션. 응답은 response 바로 아래에 정확히 하나의 어설션만 포함해야 합니다.
- Audience와 recipient. audience는 여러분의
EntityID여야 하고 recipient는 여러분의AssertionConsumerServiceURL이어야 하며, 이를 통해 다른 애플리케이션을 위해 발급된 어설션은 거부됩니다. - 시간 창. NotBefore와 NotOnOrAfter는 UTC를 기준으로, 기본값 2분인
ClockSkew초의 허용 오차와 함께 검사됩니다.MaxAssertionAge로 어설션의 최대 경과 시간도 제한할 수 있습니다. - InResponseTo. 응답은 여러분이 저장한 요청 id에 대해 응답해야 합니다.
AllowIdPInitiated를 설정하지 않는 한, 요청하지 않은 IdP 시작 응답은 거부됩니다. - 재전송 방지 캐시. 수락된 각 어설션의 ID는 만료될 때까지 보관되므로, 동일한 응답이 두 번 전송되면 거부됩니다. 이 캐시는 thread safe이며 메모리에 존재합니다. 여러 서버가 로그인을 공유하는 경우,
DoAddToReplayCache를 오버라이드하여 ID를 공유 저장소에 보관합니다. - SHA-1은 기본적으로 비활성화. RSA-SHA1 서명과 SHA-1 다이제스트는 여전히 이를 필요로 하는 IdP에 대해
AllowSHA1을 설정하지 않는 한 거부됩니다.
파서는 또한 DOCTYPE 선언을 거부하므로 외부 엔티티가 존재하지 않으며, 문서의 크기와 중첩 깊이도 제한합니다. issuer는 여러분이 구성한 IdP여야 합니다. 첫 번째로 실패한 검사가 검증을 중단시키며, 그 이유는 ErrorMessage에 담깁니다. 이를 로그로 남기고 사용자에게는 단순한 “로그인 실패” 페이지를 보여주십시오.
계정 없이 시험해 보기
SAML이 작동하는 모습을 보기 위해 Entra ID 테넌트가 필요하지는 않습니다. Mock SAML은 mocksaml.com에서 제공하는 무료 테스트용 identity provider입니다. 어떤 service provider든 받아들이며, AuthnRequest에서 audience와 ACS URL을 그대로 가져오므로 등록할 것이 없습니다.
데모 Demos\26.Authentication\03.SAML_ServiceProvider는 TsgcWebSocketHTTPServer 위에 만들어진 완전한 service provider이며, http://localhost:8090에서 /login, /acs, /metadata 엔드포인트를 제공합니다.
- 데모를 빌드하고 libcrypto-3.dll과 libssl-3.dll을 실행 파일 옆에 둡니다. 이 파일들은 데모 폴더에 있으며, OpenSSL이 RSA 서명을 검증하는 데 사용합니다.
- Load IdP metadata를 클릭합니다. 기본 소스는 mocksaml.com의 메타데이터 URL입니다.
- Start를 클릭한 다음 Open Browser를 클릭하고, 로그인 링크를 따라갑니다.
- mocksaml.com에서 example.com 도메인의 아무 사용자 이름과 아무 비밀번호를 입력합니다.
- 브라우저가 ACS로 돌아오고, 페이지에 NameID, SessionIndex, 그리고 id, email, firstName, lastName 속성이 표시됩니다.
이것이 정상적으로 동작하면 http://localhost:8090/metadata를 열어 실제 IdP에 등록하고, 데모에서 IdP 메타데이터를 불러온 다음 다시 로그인해 보십시오. AD FS의 경우 AD FS가 https만 허용하므로 먼저 SSL로 데모를 실행하십시오.
현재의 제한 사항
- 암호화된 어설션은 지원하지 않습니다. EncryptedAssertion이나 암호화된 NameID가 포함된 응답은 거부됩니다. IdP에서는 어설션 암호화를 비활성화된 상태로 두십시오. 어설션은 여전히 서명되며 https로 전송됩니다.
- Single Logout은 지원하지 않습니다. SLO는 구현되어 있지 않습니다. 애플리케이션이 자체 세션을 종료하고 자체 로그아웃을 구성할 수 있도록
SessionIndex가 반환됩니다.
문서
받는 방법
TsgcSAMLServiceProvider는 Delphi와 C++ Builder용 sgcWebSockets의 Enterprise 및 All-Access 에디션에 포함되어 있으며, 동일한 컴포넌트가 sgcWebSockets .NET에도 포함되어 있습니다. 인증 기능만 필요하다면 sgcAuth 패키지가 다른 로그인 컴포넌트와 함께 이를 제공합니다. 유닛 이름은 sgcAuth_SAML_SP이며, 컴포넌트를 폼에 올려놓기 전까지는 기존 애플리케이션에서 아무것도 바뀌지 않습니다.
다음 읽을거리
영상으로 보기
eSeGeCe 채널에 “SAML single sign-on in Delphi with Entra ID, Okta and AD FS”라는 제목의 짧은 영상이 있습니다. IDE에서의 코드와 mocksaml.com을 상대로 한 데모의 실시간 로그인을 보여줍니다.
질문이나 피드백이 있으신가요? identity provider 연결에 도움이 필요하신가요? 문의하기. 코드를 작성한 팀이 직접 답변해 드립니다.
