토리맘의 한글라이즈 프로젝트 logo 토리맘의 한글라이즈 프로젝트

스프링 시큐리티 공식 레퍼런스를 한글로 번역한 문서입니다.

전체 목차는 여기에 있습니다.

목차:


13.1. SAML 2.0 Login

SAML 2.0 로그인 기능 saml2Login()을 사용하면 어플리케이션의 사용자를 외부 SAML 2.0 Identity Provider (Okta, ADFS 등) 계정으로 로그인할 수 있다.

SAML 2.0 로그인은 SAML 2 Profiles에 명시된 대로 웹 브라우저 SSO 프로파일 (Web Browser SSO Profile) 방식을 사용한다. 현재 스프링 시큐리티는 간단한 인증 스킴만 지원한다.

13.1.1. SAML 2 Support in Spring Security

relying party라고도 하는 SAML 2 Service Provider(SP)는 2009년부터 독립 프로젝트로 지원했다. 현재도 1.0.x 브랜치를 사용하고 있으며, SP 기반 SAML 2.0 Identity Provider 구현체를 함께 제공하는 Cloud Foundry User Account and Authentication Server에서도 사용하고 있다.

2018년에는 실험적으로 최신 Service Provider와 Identity Provider를 모두 지원하는 독립형 라이브러리를 개발했었다. 오랜 심도있는 고민 끝에, 스프링 시큐리티는 이 프로젝트를 중단하기로 결정했다. 이 프로젝트로 새 독립형 1.0.x 라이브러리를 만들긴 했지만, 다른 라이브러리 위에서 동작하는 라이브러리를 개발해야 한다고는 생각하지 않았었다.

대신 스프링 시큐리티 프로젝트에서 프레임워크로써 SAML 2 인증을 지원하기로 결정했다.

13.1.2. Saml 2 Login - High Level Concepts

saml2Login()의 목표는 SAML 2 기능 셋을 지원하는 것으로, Service Provider(SP), 즉 relying party가 되어 인증하는 것과, Identity Provider, 즉 asserting party에서 XML assertion을 받는 것에 초점을 두고 있다.

SAML2 로그인 또는 SAML2 인증이라고 하면, SP가 IDP로부터 assertion이라는 XML 메시지를 받아 검증한다는 뜻이다.

현재 지원하는 인증 플로우는 두 가지다.

  1. IDP-Initiated 플로우 - 예시: Okta에 직접 로그인한 뒤 이 계정으로 인증할 웹 어플리케이션을 선택한다. Okta(IDP)는 웹 어플리케이션(SP)으로 assertion을 전송한다.
  2. SP-Initiated 플로우 - 예시: 웹 어플리케이션에 접속하면 어플리케이션(SP)이 IDP에 assertion을 달라고 인증 요청을 보낸다. IDP에서 인증에 성공하면 SP에 assertion을 전송한다.

13.1.3. Saml 2 Login - Current Feature Set

  1. Service Provider(SP/Relying Party)는 entityId = {baseUrl}/saml2/service-provider-metadata/{registrationId}로 식별한다
  2. {baseUrl}/login/saml2/sso/{registrationId}에서 Http-POST 또는 Http-Redirect를 통해 SAML 응답에 있는 assertion을 수신한다
  3. 서명한 응답이 아니라면 assertion을 서명해야 한다.
  4. 암호화된 assertion 지원
  5. 암호화된 NameId 요소 지원
  6. Converter<Assertion, Collection<? extends GrantedAuthority>>를 사용해서 assertion 속성을 권한으로 추출할 수 있다
  7. GrantedAuthoritiesMapper를 사용해서 권한을 매핑하고 화이트리스트를 관리할 수 있다.
  8. java.security.cert.X509Certificate 형식의 공개키.
  9. AuthNRequest를 통한 SP-Initiated 인증

Saml 2 Login - Not Yet Supported

  1. assertion 조건과 속성을 세션 기능에 매핑 (타임아웃, 트래킹 등)
  2. 싱글 로그아웃
  3. 동적인 메타데이터 생성
  4. 독립형 assertion (응답 객체로 감싸지 않은) 수신과 검증

13.1.4. Saml 2 Login - Introduction to Java Configuration

스프링 시큐리티 필터 체인에 saml2Login()을 추가하려면, 자바 설정에선 최소한 SAML 설정을 저장하는 RelyingPartyRegistrationRepository가 필요하며, HttpSecurity.saml2Login() 메소드를 호출해야 한다:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
        //SAML configuration
        //Mapping this application to one or more Identity Providers
        return new InMemoryRelyingPartyRegistrationRepository(...);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(withDefaults())
        ;
    }
}

빈 정의는 편리하긴 하지만 필수는 아니다. 메소드에 직접 레포지토리를 연결해도 된다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .relyingPartyRegistrationRepository(...)
            )
        ;
    }
}

RelyingPartyRegistration

RelyingPartyRegistration 객체로 어플리케이션(SP)과 asserting party(IDP)를 매핑한다.

URI Patterns

URI 패턴은 요청을 기반으로 자동으로 URI를 생성할 때 자주 사용한다. saml2Login에선 URI 패턴에 다음 변수를 사용할 수 있다.

예시: {baseUrl}/login/saml2/sso/{registrationId}

Relying Party

http://localhost:8080/saml2/service-provider-metadata/my-test-configuration

이 설정 옵션이 꼭 패턴이어야 하는 건 아니다. 고정 URI 값일 수도 있다.

수신하는 메세지는 항상 서명돼 있으며, 시스템에선 첫 번째로 인덱스 [0]에 있는 certificate로 서명을 검증해 본다. 첫 번째 검증에 실패했을 때만 두 번째 credential로 이동한다.

이와 유사하게, 복호화에 사용할 SP 개인키도 같은 순서로 시도해 본다. IDP로 전송할 메세지를 서명할 때는 첫 번째 SP credential(type=SIGNING)을 사용한다.

Duplicated Relying Party Configurations

어플리케이션이 identity provider를 여러 개 사용한다면 두 RelyingPartyRegistration 객체에 있는 일부 설정은 반드시 중복되기 마련이다.

설정이 중복되면 안 좋은 점도 있지만, 백엔드에 있는 설정 저장소엔 이 데이터 저장 모델을 이중으로 관리하지 않아도 된다.

좋은 점도 있다. credential은 일부 identity provider 간에 더 쉽게 교대해가며 사용할 수 있다. 이 객체 모델을 사용하면 여러 IDP를 사용할 때 설정이 달라져서 모든 identity provider에서 credential을 교대하지 않을 때도 문제되지 않는다.

Service Provider Metadata

스프링 시큐리티 SAML 2 구현체는 아직 SP 메타데이터를 XML 형식으로 다운로드하는 엔드포인트를 지원하지 않는다. 최소한 다음 정보를 교환해야 한다.

Authentication Requests - SP Initiated Flow

웹 어플리케이션에서 인증을 시작하려면 다음으로 리다이렉트하면 된다:

{baseUrl}/saml2/authenticate/{registrationId}

이 엔드포인트에선 RelyingPartyRegistration에 따라 Redirect 또는 POST로 AuthNRequest를 생성할 것이다.

Customizing the AuthNRequest

AuthNRequest를 원하는대로 설정하려면, Saml2AuthenticationRequestFactory 인스턴스를 만들면 된다.

예를 들어 AuthNRequest로 IDP에 REDIRECT로 SAML Assertion을 전송해 달라는 요청을 만들고 싶다면 다음과 같이 설정할 수 있다:

@Bean
public Saml2AuthenticationRequestFactory authenticationRequestFactory() {
    OpenSamlAuthenticationRequestFactory authenticationRequestFactory =
        new OpenSamlAuthenticationRequestFactory();
    authenticationRequestFactory.setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
    return authenticationRequestFactory;
}

Delegating to an AuthenticationRequestFactory

아니면 AuthenticationRequestFactory에서 전송할 파라미터를 더 세세하게 컨트롤해야 한다면 위임 전략을 사용하면 된다:

@Component
public class IssuerSaml2AuthenticationRequestFactory implements Saml2AuthenticationRequestFactory {
    private OpenSamlAuthenticationRequestFactory delegate = new OpenSamlAuthenticationRequestFactory();

    @Override
    public String createAuthenticationRequest(Saml2AuthenticationRequest request) {
        return this.delegate.createAuthenticationRequest(request);
    }

    @Override
    public Saml2PostAuthenticationRequest createPostAuthenticationRequest
        (Saml2AuthenticationRequestContext context) {

        String issuer = // ... calculate issuer

        Saml2AuthenticationRequestContext customIssuer = Saml2AuthenticationRequestContext.builder()
                .assertionConsumerServiceUrl(context.getAssertionConsumerServiceUrl())
                .issuer(issuer)
                .relayState(context.getRelayState())
                .relyingPartyRegistration(context.getRelyingPartyRegistration())
                .build();

        return this.delegate.createPostAuthenticationRequest(customIssuer);
    }

    @Override
    public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest
        (Saml2AuthenticationRequestContext context) {

        throw new UnsupportedOperationException("unsupported");
    }
}

13.1.5. Customizing Authentication Logic

기본적으로 스프링 시큐리티는 OpenSamlAuthenticationProvider가 SAML 2 응답과 assertion을 검증하고 파싱하도록 설정한다. 이 provider엔 세 가지 설정 옵션이 있다.

  1. 권한 extractor - assertion에서 그룹 정보를 파싱한다
  2. 권한 mapper - 추출한 그룹 정보를 내부 권한으로 매핑한다
  3. 응답 시간 검증 오차 범위 - 시간 동기화 이슈가 있을 수 있기 때문에 기본적으로 타임스탬프를 검증할 땐 어느 정도 오차를 허용해야 한다.

한 가지 커스텀 전략은 이 구현체가 생성한 객체를 수정할 수 있는 ObjectPostProcessor를 사용하는 것이다. 다른 옵션은 SAMLResponse를 가로채는 필터에서 사용할 인증 매니저를 재정의하는 것이다.

OpenSamlAuthenticationProvider ObjectPostProcessor

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ObjectPostProcessor<OpenSamlAuthenticationProvider> processor = new ObjectPostProcessor<>() {
            @Override
            public <O extends OpenSamlAuthenticationProvider> O postProcess(O provider) {
                provider.setResponseTimeValidationSkew(RESPONSE_TIME_VALIDATION_SKEW);
                provider.setAuthoritiesMapper(AUTHORITIES_MAPPER);
                provider.setAuthoritiesExtractor(AUTHORITIES_EXTRACTOR);
                return provider;
            }
        };

        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
               .addObjectPostProcessor(processor)
            )
        ;
    }
}

Configure OpenSamlAuthenticationProvider as an Authentication Manager

authenticationManager 메소드 하나로 디폴트 OpenSamlAuthenticationProvider를 재정의하거나 커스텀할 수 있다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        OpenSamlAuthenticationProvider authProvider = new OpenSamlAuthenticationProvider();
        authProvider.setResponseTimeValidationSkew(RESPONSE_TIME_VALIDATION_SKEW);
        authProvider.setAuthoritiesMapper(AUTHORITIES_MAPPER);
        authProvider.setAuthoritiesExtractor(AUTHORITIES_EXTRACTOR);
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(asList(authProvider)))
            )
        ;
    }
}

Custom Authentication Manager

보안 필터에서 사용할 인증 매니저는 커스텀 AuthenticationManager를 구현해서 재정의할 수도 있다. 이 인증 매니저는 SAML 2 응답 XML 데이터를 가지고 있는 Saml2AuthenticationToken 객체를 처리해야 한다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(authenticationManager)
            )
        ;
    }
}

13.1.6. Spring Boot 2.x Sample

우리는 지금도 스프링 시큐리티 SAML 로그인 자동 설정을 위해 스프링 부트 팀과 함께하고 있다. 그 사이 Yaml 설정을 지원하는 스프링 부트 샘플을 제공해 왔다.

샘플을 실행해보려면, 아래 세 단계를 따라해라

  1. 스프링 부트 어플리케이션을 기동해라
    • ./gradlew :spring-security-samples-boot-saml2login:bootRun
  2. 브라우저를 켜라
    • http://localhost:8080/
  3. identity provider로 이동하면 아래 계정으로 로그인해라:
    • User: user
    • Password: password

Multiple Identity Provider Sample

provider를 여러 개 사용하는 것도 간단하지만, 주의하지 않으면 문제가 될 수 있는 몇 가지 디폴트 설정들이 있다. SAML 설정이 있는 RelyingPartyRegistration 객체의 SP 엔터티 ID의 디폴트 값은 {baseUrl}/saml2/service-provider-metadata/{registrationId}다.

따라서 2개의 provider 설정을 사용하면 시스템은 다음과 같이 설정된다.

registration-1 (Identity Provider 1) - Our local SP Entity ID is:
http://localhost:8080/saml2/service-provider-metadata/registration-1

registration-2 (Identity Provider 2) - Our local SP Entity ID is:
http://localhost:8080/saml2/service-provider-metadata/registration-2

이 설정에선 아래 나와있는 대로 실제로 동일한 어플리케이션에서 호스팅하는 두 개의 가상 Service Provider ID를 생성한다.

spring:
  security:
    saml2:
      login:
        relying-parties:
          - entity-id: &idp-entity-id https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php
            registration-id: simplesamlphp
            web-sso-url: &idp-sso-url https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php
            signing-credentials: &service-provider-credentials
              - private-key: |
                  -----BEGIN PRIVATE KEY-----
                  MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE
                  ...................SHORTENED FOR READ ABILITY...................
                  INrtuLp4YHbgk1mi
                  -----END PRIVATE KEY-----
                certificate: |
                  -----BEGIN CERTIFICATE-----
                  MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC
                  ...................SHORTENED FOR READ ABILITY...................
                  RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B
                  -----END CERTIFICATE-----
            verification-credentials: &idp-certificates
              - |
                -----BEGIN CERTIFICATE-----
                MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD
                ...................SHORTENED FOR READ ABILITY...................
                lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk
                -----END CERTIFICATE-----
          - entity-id: *idp-entity-id
            registration-id: simplesamlphp2
            web-sso-url: *idp-sso-url
            signing-credentials: *service-provider-credentials
            verification-credentials: *idp-certificates

이 설정을 원하지 않는다면, 아래처럼 수동으로 로컬 SP 엔터티 ID를 재정의할 수 있다.

localEntityIdTemplate = {baseUrl}/saml2/service-provider-metadata

로컬 SP 엔터티 ID를 이 값으로 변경하더라도, 각 identity provider엔 등록 ID를 기반으로 정확한 싱글사인온 URL (assertion 컨슈머 서비스 URL)을 제공해야 한다. {baseUrl}/login/saml2/sso/{registrationId}


전체 목차는 여기에 있습니다.

<< >>