SAML 2.0 A to Z — 개념부터 Spring Boot SSO 실습까지

이번 포스팅에서는 엔터프라이즈 SSO의 사실상 표준인 SAML 2.0에 대해서 처음 접하는 개발자의 눈높이로 정리하고자 한다. SAML은 이름부터 XML까지 낯설어 보이지만, 핵심 아이디어는 단순하다. “로그인은 신뢰하는 한 곳(IdP)에서만 하고, 나머지 앱들은 그 결과를 서명된 문서로 넘겨받아 믿는다”는 것이다. 회사에서 계정 하나로 메일·위키·경비 시스템에 자동 로그인되는 그 경험이 대부분 SAML이다. 구성요소부터 SSO 흐름, assertion과 metadata의 역할, 그리고 OAuth와 무엇이 다른지 짚은 뒤, Spring Boot로 SAML 로그인을 실제로 구현해 실행까지 확인한다.

SAML 2.0이 푸는 문제 — 왜 이 표준이 필요한가

SAML 2.0(Security Assertion Markup Language)은 여러 앱이 로그인을 한 곳에 위임하고, 그 인증 결과를 서명된 XML로 주고받는 표준이다. 목적은 SSO(Single Sign-On), 즉 사용자가 한 번만 로그인하면 연결된 여러 서비스에 다시 로그인하지 않고 접근하게 하는 것이다. 2005년에 표준화된 만큼 오래됐지만, 지금도 엔터프라이즈 SSO에서 가장 널리 쓰인다.

문제 상황을 떠올리면 왜 필요한지 분명해진다. 회사에 사내 앱이 열 개 있는데 각각 따로 로그인하면 계정 관리가 지옥이 된다. SAML은 인증을 IdP(Identity Provider) 한 곳으로 몰아 이 문제를 푼다. 각 앱은 스스로 비밀번호를 검증하지 않고, “이 사용자는 IdP가 이미 인증했다”는 서명된 증명(assertion)만 받아 신뢰한다.

한 가지 특징을 짚고 넘어가야 한다. SAML은 XML 기반이고, 신뢰의 근거가 디지털 서명이다. IdP가 개인키로 서명한 XML을 앱이 IdP의 공개키(certificate)로 검증해, “정말 그 IdP가 보냈고 내용이 위조되지 않았음”을 확인한다. 이 서명 검증이 SAML 2.0 보안의 뼈대다. 표준 개념은 OASIS SAML 2.0 기술 개요에 정의돼 있다.

SAML 2.0의 세 등장인물 — User, SP, IdP

SAML 2.0에는 세 주체가 있고, 이 셋의 관계만 잡으면 전체 흐름이 보인다. 로그인하는 User, 사용자가 접근하려는 앱인 SP(Service Provider), 그리고 인증을 실제로 수행하는 IdP(Identity Provider)다. OAuth를 알고 있다면 “권한 위임” 대신 “인증 위임”이라는 점만 다르다고 보면 된다.

주체역할회사 예시
User로그인하는 사용자직원
SP (Service Provider)사용자가 쓰려는 앱. 인증을 IdP에 위임사내 위키·경비 시스템
IdP (Identity Provider)신원을 인증하고 assertion을 발급회사 SSO 서버(Okta·Keycloak 등)

놓치면 안 되는 건 SP가 비밀번호를 직접 다루지 않는다는 점이다. SP는 “사용자를 IdP로 보내서 로그인시키고, IdP가 돌려준 서명된 assertion만 검증”한다. 비밀번호는 오직 IdP만 알고, SP는 그 결과만 신뢰한다. 덕분에 앱이 열 개든 스무 개든 비밀번호 관리는 IdP 한 곳에서만 이뤄진다. 이 구조가 SAML 2.0이 엔터프라이즈에서 사랑받는 이유다. 그렇다면 이 셋이 실제로 어떤 순서로 대화하는지, 다음 절의 SSO 흐름에서 본다.

SAML 2.0 SSO 흐름 — SP-initiated 로그인 한 바퀴

가장 흔한 SP-initiated SSO는 다섯 단계로 돈다. 사용자가 SP에 접근하면 SP가 IdP로 보내고, IdP에서 로그인하면 서명된 assertion이 SP로 돌아오며, SP가 그 서명을 검증한 뒤 세션을 만든다. 순서대로 짚으면 이렇다.

  1. 접근 시도: User가 SP의 보호된 페이지에 접근한다. 아직 로그인 세션이 없다.
  2. AuthnRequest 생성·리다이렉트: SP가 “이 사용자를 인증해 달라”는 AuthnRequest(XML)를 만들어 User를 IdP로 리다이렉트한다.
  3. IdP 인증: User가 IdP에서 로그인한다(이미 IdP 세션이 있으면 이 단계는 생략되고, 이게 SSO의 핵심이다).
  4. SAML Response(assertion) 반환: IdP가 인증 결과를 담은 서명된 assertion을 만들어, User의 브라우저를 통해 SP의 ACS(Assertion Consumer Service) 주소로 POST한다.
  5. 검증·세션 생성: SP가 assertion의 서명을 IdP 공개키로 검증하고, 유효하면 사용자 세션을 만든다. 이후 User는 SP에 로그인된 상태가 된다.

여기서 SSO가 빛나는 지점은 3단계다. User가 다른 SP에 접근해도 IdP 세션이 살아 있으면 로그인 화면 없이 바로 assertion이 발급된다. 한 번 IdP에 로그인해 두면 연결된 모든 SP에 자동 로그인되는 것이다. 이 흐름은 OAuth의 authorization code와 겉모습이 비슷하지만, 오가는 것이 code·token이 아니라 서명된 XML assertion이라는 점이 다르다. Spring Security의 SAML2 처리 과정은 Spring Security SAML2 공식 문서에 정리돼 있다.

assertion과 metadata — SAML을 굴리는 두 XML

SAML 2.0을 실제로 다루면 두 XML 문서를 계속 만난다. assertion은 “인증 결과”를 담은 문서이고, **metadata는 “SP와 IdP가 서로를 신뢰하기 위한 명함”**이다. 이 둘의 역할을 구분하면 설정이 훨씬 쉬워진다.

assertion은 IdP가 발급하는 서명된 XML로, 크게 세 가지를 담는다. 사용자를 식별하는 NameID(이메일이나 사용자명), 인증이 언제 어떻게 이뤄졌는지(AuthnStatement), 그리고 이름·부서·역할 같은 추가 속성(AttributeStatement)이다. SP는 이 assertion의 서명을 검증해 “IdP가 보증한 사용자 정보”로 신뢰한다.

metadata는 SP와 IdP가 연동 전에 교환하는 설정 문서다. 여기에는 서로의 entity ID(고유 식별자), 엔드포인트 주소(SSO URL, ACS URL), 그리고 서명 검증에 쓸 **공개키(certificate)**가 들어간다. 실무에서 “SAML 연동”의 절반은 이 metadata를 교환하는 일이다. SP는 자기 metadata를 IdP에 등록하고, IdP metadata를 받아 자기 쪽에 설정한다. 뒤에서 Spring Boot 설정을 보면, IdP의 metadata-uri 하나만 지정하면 Spring이 그 XML을 읽어 엔드포인트와 공개키를 자동으로 파악하는 것을 확인할 수 있다.

SAML vs OAuth/OIDC — 언제 무엇을 쓰나

이 글을 읽는 개발자라면 “그래서 OAuth랑 뭐가 다른가”가 궁금할 것이다. 한 줄로 답하면, SAML은 XML 기반 엔터프라이즈 SSO, OAuth 2.0/OIDC는 JSON·JWT 기반 현대적 인가·로그인이다. 둘 다 “인증을 한 곳에 위임”하지만 태생과 강점이 다르다.

항목SAML 2.0OAuth 2.0 / OIDC
등장20052012 / 2014
포맷XMLJSON / JWT
주 목적인증·SSO인가(OAuth) / 인증(OIDC)
강한 영역엔터프라이즈 웹 SSO모바일·SPA·API
신뢰 근거서명된 XML assertion서명된 JWT / 토큰

선택 기준은 “누가 쓰느냐”다. 사내 직원이 여러 웹 앱을 오가는 엔터프라이즈 SSO라면 SAML이 여전히 표준이고, 대부분의 상용 IdP(Okta·Azure AD·Keycloak)가 SAML을 기본 지원한다. 반면 모바일 앱·SPA·API 접근이나 소셜 로그인이라면 OAuth 2.0/OIDC 쪽이 가볍고 현대적이다. 실무에서는 둘 중 하나만 고르는 게 아니라 사내 SSO는 SAML, API·모바일은 OAuth로 병행하는 경우가 많다(SAML vs OAuth 비교). OAuth의 토큰·JWT 구조가 궁금하다면 앞서 정리한 OAuth 글을 함께 보면 대비가 선명해진다.

Spring Boot로 SAML 로그인 구현하기 — 의존성과 설정

Spring Boot에서 SAML 2.0 로그인은 의존성 하나와 relying party 설정이면 동작한다. SAML2 스타터를 추가하고, application.yml에 IdP의 metadata-uri만 지정하면 Spring이 엔드포인트와 공개키를 자동으로 읽어 온다. 아래는 Spring Boot 4.1 기준 구성이다.

// build.gradle (Spring Boot 4.1)
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    // SAML 2.0 Service Provider 기능
    implementation 'org.springframework.boot:spring-boot-starter-security-saml2'
}
Groovy

Spring Boot 4.1 기준 SAML2 스타터 이름은 spring-boot-starter-security-saml2다(start.spring.io에서 실제로 생성해 확인한 값이다). 이 스타터가 SP 역할에 필요한 AuthnRequest 생성, assertion 검증, metadata 발행을 모두 처리한다. 다음은 IdP 등록 정보를 담는 설정이다.

# application.yml
spring:
  security:
    saml2:
      relyingparty:
        registration:
          my-idp:                          # registrationId (URL에 들어감)
            # SP 서명 자격증명 (IdP가 AuthnRequest 서명을 요구할 때 필요)
            signing:
              credentials:
                - private-key-location: classpath:sp-private.key
                  certificate-location: classpath:sp-cert.crt
            assertingparty:
              metadata-uri: https://mocksaml.com/api/saml/metadata  # 공개 테스트 IdP metadata
YAML

눈여겨볼 건 metadata-uri 한 줄이다. Spring Security는 이 주소에서 IdP metadata(XML)를 읽어, SSO 엔드포인트 주소와 서명 검증용 공개키(certificate)를 자동으로 구성한다. 덕분에 엔드포인트나 인증서를 일일이 적을 필요가 없다. 위 예시에서는 공개 테스트 IdP인 Mock SAML을 썼는데, 실무에서는 회사의 Okta·Keycloak metadata URL이 이 자리에 들어간다. signing.credentials도 함께 넣었는데, 이는 IdP metadata에 WantAuthnRequestsSigned="true"가 있으면 SP가 AuthnRequest에 서명해야 하기 때문이다. 이 경우 SP 서명용 키(private key)와 인증서가 필요하며, 없으면 부팅 시 “Signing credentials must not be empty” 에러가 난다. 여기서 두 파일의 역할이 갈린다. 개인키(sp-private.key)는 SP가 AuthnRequest에 서명을 생성하는 데 쓰고, 인증서(sp-cert.crt)는 그 서명을 IdP가 검증하도록 SP metadata에 실려 IdP로 전달된다. 반대로 IdP가 돌려준 assertion을 SP가 검증할 때 쓰는 공개키는 이 설정이 아니라 위의 metadata-uri에서 자동으로 로드된다. IdP 쪽에도 우리 SP를 등록해야 하는데, 그 방법은 다음 절에서 짚는다.

{registrationId}는 내가 지은 이름이 URL에 그대로 박히는 것이다

{registrationId}는 Spring이 정해 놓은 예약어가 아니라, 바로 위 application.yml에서 registration. 아래에 내가 붙인 이름이다. 예시에서 my-idp로 지었으니, 앞으로 나오는 모든 SAML 엔드포인트의 {registrationId} 자리에는 my-idp가 그대로 치환된다. 즉 문서 표기 /login/saml2/sso/{registrationId}는 이 프로젝트에서 실제로 /login/saml2/sso/my-idp가 된다.

문서 표기(템플릿)my-idp로 지었을 때 실제 주소역할
/login/saml2/sso/{registrationId}/login/saml2/sso/my-idpACS — IdP가 보낸 assertion을 받는 주소
/saml2/service-provider-metadata/{registrationId}/saml2/service-provider-metadata/my-idpSP metadata 발행(IdP에 등록할 명함)
/saml2/authenticate/{registrationId}/saml2/authenticate/my-idpSP-initiated 로그인 시작(AuthnRequest 생성)

이 이름은 순전히 논리적 별칭이라 okta, keycloak, azure-ad처럼 IdP를 알아볼 수 있는 값으로 자유롭게 바꿔도 된다. 다만 이름을 바꾸면 위 세 URL의 마지막 세그먼트도 함께 바뀌고, IdP에 SP를 등록할 때 넘긴 ACS 주소와 정확히 일치해야 로그인 응답이 제 등록으로 라우팅된다. IdP를 여러 개 붙일 때는 registration. 아래에 이름을 여러 개 두는데, 이때 registrationId가 곧 “어느 IdP로 로그인했는가”를 가르는 식별자가 된다. 실제로 뒤의 컨트롤러에서 principal.getRelyingPartyRegistrationId()로 읽는 값이 바로 이 이름이다.

SP 서명 키·인증서 만들기 — self-signed로 충분하다

signing.credentials에 넣을 개인키와 인증서는 CA에서 발급받을 필요 없이 자체 서명(self-signed)으로 충분하다. 이 인증서는 브라우저 TLS 신뢰용이 아니라 “이 AuthnRequest를 정말 그 SP가 서명했는가”를 IdP가 검증하기 위한 것이라, IdP에 등록한 인증서와 SP가 실제 서명에 쓴 키쌍만 일치하면 된다. openssl 한 줄로 개인키와 자체 서명 인증서를 함께 만든다.

# RSA 2048비트 개인키 + 자체 서명 인증서(유효기간 10년)를 한 번에 생성
openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout sp-private.key -out sp-cert.crt \
  -days 3650 -subj "/CN=saml-demo-sp"
ShellScript

-newkey rsa:2048가 새 RSA 키쌍을 만들고, -x509가 그 키로 자기 자신을 서명해 인증서로 내보낸다. -nodes는 개인키를 암호화 없이 저장하라는 뜻으로, Spring이 부팅할 때 암호 입력 없이 읽게 해 준다. OpenSSL 3.x는 개인키를 Spring Security가 요구하는 PKCS#8 형식(-----BEGIN PRIVATE KEY-----)으로 출력하므로, 생성된 두 파일을 src/main/resources/에 두면 classpath:sp-private.key·classpath:sp-cert.crt로 바로 참조된다.

Spring Security의 키 로더는 PKCS#8 PEM 개인키만 읽는다. 구버전 OpenSSL에서 -----BEGIN RSA PRIVATE KEY-----(PKCS#1) 형식으로 나오면 아래처럼 PKCS#8로 변환해 헤더가 -----BEGIN PRIVATE KEY-----가 되도록 맞춘다.

# PKCS#1 → PKCS#8 변환 (Spring Security가 요구하는 형식)
openssl pkcs8 -topk8 -nocrypt -in sp-private.key -out sp-private-pkcs8.key
ShellScript

이렇게 만든 SP 인증서(sp-cert.crt)는 뒤에서 IdP(Mock SAML·Okta 등)에 SP를 등록할 때 그대로 올려, IdP가 우리 SP의 서명을 검증할 수 있게 한다.

SecurityConfig와 사용자 정보 조회 — 실행 가능한 전체 코드

보안 설정과 로그인한 사용자 정보를 확인하는 컨트롤러를 붙이면 SP 쪽 구현은 끝이다. 아래 두 클래스는 그대로 합치면 컴파일·실행되는 완결 코드다. saml2Login() 한 줄이 앞서 설명한 SAML SSO 흐름 전체를 활성화한다.

package com.example.samldemo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()   // 모든 요청은 인증 필요
            )
            // 이 한 줄이 SAML2 로그인(SP-initiated SSO) 전체를 활성화한다
            .saml2Login(login -> login.defaultSuccessUrl("/me", true))
            // SP metadata 엔드포인트를 노출 (IdP에 등록할 명함)
            .saml2Metadata(metadata -> {});
        return http.build();
    }
}
Java

saml2Login()을 호출하면 Spring Security가 /login/saml2/sso/{registrationId}(assertion을 받는 ACS)와 AuthnRequest 진입점을 자동으로 만든다. saml2Metadata()는 우리 SP의 metadata를 /saml2/service-provider-metadata/{registrationId} 주소로 발행해, IdP에 등록할 때 이 URL을 넘기면 된다. 다음은 로그인한 사용자 정보를 꺼내는 컨트롤러다.

package com.example.samldemo;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class MeController {

    // SAML 로그인 결과는 Saml2AuthenticatedPrincipal로 주입된다
    @GetMapping("/me")
    public Map<String, Object> me(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal) {
        return Map.of(
            "nameId", principal.getName(),                  // assertion의 NameID (사용자 식별자)
            "attributes", principal.getAttributes(),        // AttributeStatement의 속성들
            "registrationId", principal.getRelyingPartyRegistrationId()  // 어떤 IdP로 로그인했는지
        );
    }
}
Java

봐야 할 건 @AuthenticationPrincipal Saml2AuthenticatedPrincipal이다. SAML 로그인이 끝나면 principal이 이 타입으로 주입되고, assertion에서 파싱된 NameID(사용자 식별자)와 속성들을 바로 읽을 수 있다. OAuth에서 OidcUser로 id token 클레임을 읽던 것과 같은 자리에, SAML에서는 이 Saml2AuthenticatedPrincipal이 온다고 보면 된다. 이 코드를 실제로 띄우면 어떤 엔드포인트가 생기는지 다음 절에서 실행으로 확인한다.

한 가지 짚어둘 것은 /me가 SAML이 정한 표준 엔드포인트가 아니라, 이 예제에서 직접 만든 보호 페이지이자 로그인 결과를 눈으로 확인하는 창이라는 점이다. 그래서 /me는 한 흐름에서 세 번 등장한다. 먼저 로그인하지 않은 채 /me에 접근하면 위 anyRequest().authenticated() 규칙에 걸려 Spring이 AuthnRequest를 만들고 IdP로 리다이렉트한다 — SSO를 시작시키는 방아쇠다. 다음으로 IdP 인증을 마치고 assertion이 ACS(/login/saml2/sso/my-idp)로 돌아와 검증되면, 앞의 defaultSuccessUrl("/me", true) 설정에 따라 사용자가 로그인 성공 후 착지하는 지점이 된다(두 번째 인자 true는 원래 가려던 페이지가 있어도 무조건 /me로 보내라는 뜻이다). 마지막으로 이제 인증된 상태에서 /me를 열면 이 컨트롤러가 실행되어 로그인 결과(NameID·속성·registrationId)를 돌려준다. 데모라 이 자리에 /me를 뒀을 뿐, 실무에서는 대시보드나 마이페이지 같은 실제 보호 페이지가 그 자리에 온다.

실행으로 확인하기 — SP metadata와 SSO 리다이렉트

위 코드를 Spring Boot 4.1 프로젝트에 넣고 실제로 띄워, SAML SP가 제대로 구성됐는지 확인해 봤다. 로그인 없이 두 가지를 볼 수 있다. 하나는 우리 SP의 metadata(IdP에 등록할 명함)이고, 다른 하나는 보호된 페이지에 접근했을 때 IdP로 리다이렉트되며 만들어지는 AuthnRequest다. 이 둘이 앞서 설명한 흐름의 2단계와 metadata 개념을 그대로 증명한다.

/saml2/service-provider-metadata/my-idp에 접근하면 SP metadata XML이 나오고, 그 안에 우리 SP의 entity ID와 ACS 주소(/login/saml2/sso/my-idp)가 담겨 있다. 보호된 페이지에 접근하면 Spring이 AuthnRequest를 만들어 IdP의 SSO URL로 리다이렉트한다.

캡처의 두 번째 흐름이 이 글의 SSO 1~2단계를 그대로 증명한다. 보호 페이지(/me)에 접근하자 Spring이 IdP의 SSO URL(mocksaml.com/api/saml/sso)로 리다이렉트하며, 거기에 SAMLRequest와 SP 개인키로 만든 Signature가 붙어 나간다. SP metadata의 ACS 주소(/login/saml2/sso/my-idp)가 바로 IdP에 등록해야 하는 값이다. 이 출력이 확인되면 남은 건 IdP에 우리 SP metadata를 등록하고 실제 로그인하는 것뿐이다.

실무에서 자주 막히는 지점 — 신뢰 교환과 시간 동기화

SAML 2.0을 처음 붙일 때 가장 많이 부딪히는 문제는 양방향 신뢰 등록이다. SP에 IdP metadata를 설정하는 것으로 끝이 아니라, IdP에도 우리 SP를 등록해야 한다. IdP 입장에서 “이 SP는 내가 assertion을 보내도 되는 신뢰 대상”임을 알아야 하기 때문이다. 그러니 SP metadata(/saml2/service-provider-metadata/{id})를 IdP 관리자에게 넘겨 등록시키는 절차가 반드시 필요하다. 한쪽만 설정하면 로그인 시도 때 “unknown SP” 류의 에러가 난다.

두 번째 함정은 시간 동기화다. assertion에는 유효 시간(NotBefore, NotOnOrAfter)이 들어 있고, SP는 이를 검증한다. SP와 IdP 서버의 시계가 몇 분만 어긋나도 “assertion이 아직 유효하지 않다” 또는 “이미 만료됐다”며 인증이 거부된다. 배포 환경에서 SAML 로그인이 간헐적으로 실패하면 서버 시간(NTP 동기화)부터 의심하는 편이 빠르다.

세 번째는 certificate 관리다. IdP가 서명 키를 교체하면 SP가 가진 검증용 인증서도 갱신돼야 한다. metadata-uri로 IdP를 설정하면 Spring이 metadata에서 최신 인증서를 읽어오므로 이 문제가 줄지만, 인증서를 파일로 직접 박아 둔 경우엔 교체 때마다 수동 갱신이 필요하다. 가능하면 인증서를 하드코딩하지 말고 metadata URL로 참조하는 편이 유지보수에 유리하다.

FAQ

SAML과 OAuth는 뭐가 다르고 언제 쓰나요?

SAML 2.0은 XML 기반 표준으로 엔터프라이즈 SSO(사내 여러 웹 앱에 한 번 로그인으로 접근)에 강하고, OAuth 2.0/OIDC는 JSON·JWT 기반이라 모바일·SPA·API와 소셜 로그인에 강하다. 사내 직원용 웹 앱 SSO나 상용 IdP(Okta·Azure AD) 연동이면 SAML을, API 권한 위임이나 소셜 로그인이면 OAuth/OIDC를 쓴다. 실무에서는 둘을 병행하는 경우가 많다.

SAML assertion과 metadata는 각각 무엇인가요?

assertion은 IdP가 발급하는 서명된 XML로, 사용자 식별자(NameID)·인증 정보·속성을 담은 “인증 결과 문서”다. metadata는 SP와 IdP가 서로를 신뢰하기 위해 교환하는 “설정 명함”으로, entity ID·엔드포인트 주소·서명 검증용 공개키가 들어간다. 실무의 SAML 연동은 대부분 이 metadata를 양쪽에 교환·등록하는 일이다.

Spring Boot에서 SAML 로그인에 최소로 필요한 설정은 무엇인가요?

spring-boot-starter-security-saml2 의존성(Spring Boot 4.x 기준)과 application.ymlrelyingparty.registration.{id}.assertingparty.metadata-uri 한 줄이면 시작할 수 있다. Spring Security가 이 metadata URL에서 IdP의 SSO 엔드포인트와 서명 검증용 인증서를 자동으로 읽어 구성한다. 그다음 saml2Login()을 SecurityFilterChain에 추가하면 SSO 흐름이 활성화된다.

IdP 없이 로컬에서 SAML을 테스트할 수 있나요?

공개 테스트 IdP를 쓰면 가능하다. Mock SAML(mocksaml.com) 같은 무료 테스트 IdP의 metadata URL을 metadata-uri에 넣으면, 앱이 부팅하며 그 metadata를 읽어 SP를 구성하고 SP metadata·AuthnRequest 리다이렉트까지 실제로 만들어 낸다. 다만 그 IdP가 AuthnRequest 서명을 요구하면 SP 서명 키도 설정해야 한다. 실제 로그인까지 하려면 그 테스트 IdP에 우리 SP metadata를 등록하면 된다.

SAML 로그인이 자꾸 실패하는데 왜 그런가요?

가장 흔한 원인은 셋이다. 첫째, IdP에 우리 SP를 등록하지 않은 경우(양방향 신뢰가 필요하다). 둘째, SP와 IdP 서버의 시계가 어긋난 경우(assertion의 유효 시간 검증이 깨진다). 셋째, IdP가 서명 인증서를 교체했는데 SP가 옛 인증서를 들고 있는 경우다. 시간 동기화와 metadata 기반 인증서 참조로 대부분 예방된다.

마치며

지금까지 SAML 2.0을 개념부터 구성요소, SSO 흐름, assertion과 metadata, 그리고 Spring Boot 실습까지 정리해 보았다. 개인적으로 SAML을 처음 만났을 때 XML과 인증서에 질려서 “OAuth보다 훨씬 복잡하다”고 느꼈는데, 막상 뜯어보니 핵심은 “서명된 문서를 주고받으며 신뢰한다”는 한 문장이었다. 오히려 진짜 고생은 개념이 아니라 IdP에 SP를 등록하고 시계를 맞추는 운영 쪽에 있었다. 실습에서도 metadata를 양쪽에 교환하는 절차만 넘기면 나머지는 Spring Security가 조용히 처리해 준다. 처음이라면 Mock SAML 같은 테스트 IdP로 SP metadata가 어떻게 생겼는지 눈으로 보고, assertion 하나를 직접 디코드해 보길 권한다. XML의 낯섦은 그 순간 대부분 사라진다.