스프링 시큐리티 공식 레퍼런스를 한글로 번역한 문서입니다.
전체 목차는 여기에 있습니다.
목차:
- 13.1. SAML 2.0 Login
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 메시지를 받아 검증한다는 뜻이다.
현재 지원하는 인증 플로우는 두 가지다.
- IDP-Initiated 플로우 - 예시: Okta에 직접 로그인한 뒤 이 계정으로 인증할 웹 어플리케이션을 선택한다. Okta(IDP)는 웹 어플리케이션(SP)으로 assertion을 전송한다.
- SP-Initiated 플로우 - 예시: 웹 어플리케이션에 접속하면 어플리케이션(SP)이 IDP에 assertion을 달라고 인증 요청을 보낸다. IDP에서 인증에 성공하면 SP에 assertion을 전송한다.
13.1.3. Saml 2 Login - Current Feature Set
- Service Provider(SP/Relying Party)는
entityId = {baseUrl}/saml2/service-provider-metadata/{registrationId}
로 식별한다 {baseUrl}/login/saml2/sso/{registrationId}
에서 Http-POST 또는 Http-Redirect를 통해 SAML 응답에 있는 assertion을 수신한다- 서명한 응답이 아니라면 assertion을 서명해야 한다.
- 암호화된 assertion 지원
- 암호화된 NameId 요소 지원
Converter<Assertion, Collection<? extends GrantedAuthority>>
를 사용해서 assertion 속성을 권한으로 추출할 수 있다GrantedAuthoritiesMapper
를 사용해서 권한을 매핑하고 화이트리스트를 관리할 수 있다.java.security.cert.X509Certificate
형식의 공개키.AuthNRequest
를 통한 SP-Initiated 인증
Saml 2 Login - Not Yet Supported
- assertion 조건과 속성을 세션 기능에 매핑 (타임아웃, 트래킹 등)
- 싱글 로그아웃
- 동적인 메타데이터 생성
- 독립형 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
registrationId
baseScheme
baseHost
basePort
예시: {baseUrl}/login/saml2/sso/{registrationId}
Relying Party
registrationId
- (필수) 설정 매핑을 식별하는 유니크한 값. URI path에서 이 식별자를 사용할 수 있으므로 URI 인코딩이 필요하지 않게 주의해야 한다.localEntityIdTemplate
- (선택) 요청을 기반으로 어플리케이션의 엔티티 ID를 생성하는 URI 패턴. 디폴트 패턴은{baseUrl}/saml2/service-provider-metadata/{registrationId}
이며, 간단한 샘플 어플리케이션에선 다음과 같을 것이다.
http://localhost:8080/saml2/service-provider-metadata/my-test-configuration
이 설정 옵션이 꼭 패턴이어야 하는 건 아니다. 고정 URI 값일 수도 있다.
assertionConsumerServiceUrlTemplate
- (선택) SP-initiated 플로우에서 SP가 IDP로AuthNRequest
전송 요청을 보내면, 이 응답을 처리하는 assertion 컨슈머 서비스 URI를 나타내는 URI 패턴. 이 값은 패턴일 수 있지만 실제 URI는 반드시 SP의 ACS 엔드포인트로 리졸브해야 한다. 디폴트 값은{baseUrl}/login/saml2/sso/{registrationId}
이며,Saml2WebSsoAuthenticationFilter
엔드포인트로 직접 매핑된다.credentials
- credential 리스트로 개인키와 x509 certificate을 가지고 있다. 메세지 서명, 검증, 암호화, 복호화에 사용한다. 리스트에 의미 없는 credential을 넣어서 필요에 따라 교대할 수도 있다. 예를 들어- [0] - X509Certificate{VERIFICATION,ENCRYPTION} - 검증과 암호화에 사용하는 IDP의 첫 번째 공개키.
- [1] - X509Certificate/{VERIFICATION,ENCRYPTION} - 검증에 사용할 IDP의 두 번째 검증키. 암호화는 항상 리스트의 가장 앞에 있는
ENCRYPTION
을 사용한다. - [2] - PrivateKey/X509Certificate{SIGNING,DECRYPTION} - SP의 첫 번째 서명, 복호화 credential.
- [3] - PrivateKey/X509Certificate{SIGNING,DECRYPTION} - SP의 두 번째 복호화 credential. 서명은 항상 리스트의 가장 앞에 있는
SIGNING
키를 사용한다.
ProviderDetails#entityId
- (필수) Identity Provider의 엔티티 ID. 고정 URI 값이나 문자열이어야 하며, 패턴을 사용할 수 없다.ProviderDetails#webSsoUrl
- (필수) SP가AuthNRequest
메세지를 전송할 IDP 싱글사인온 엔드포인트의 고정 URI 값.ProviderDetails#signAuthNRequest
- SP의 개인키로AuthNRequest
를 서명할지 말지 여부를 나타내는 boolean. 디폴트는true
다.ProviderDetails#binding
-AuthNRequest
에서 메세지를 어떻게 바인딩할지를 나타내는Saml2MessageBinding
.REDIRECT
와POST
가 있으며, 디폴트는REDIRECT
다.
수신하는 메세지는 항상 서명돼 있으며, 시스템에선 첫 번째로 인덱스 [0]에 있는 certificate로 서명을 검증해 본다. 첫 번째 검증에 실패했을 때만 두 번째 credential로 이동한다.
이와 유사하게, 복호화에 사용할 SP 개인키도 같은 순서로 시도해 본다. IDP로 전송할 메세지를 서명할 때는 첫 번째 SP credential(type=SIGNING
)을 사용한다.
Duplicated Relying Party Configurations
어플리케이션이 identity provider를 여러 개 사용한다면 두 RelyingPartyRegistration
객체에 있는 일부 설정은 반드시 중복되기 마련이다.
- localEntityIdTemplate
- credentials (모든 SP credential, IDP credential은 달라진다)
- assertionConsumerServiceUrlTemplate
설정이 중복되면 안 좋은 점도 있지만, 백엔드에 있는 설정 저장소엔 이 데이터 저장 모델을 이중으로 관리하지 않아도 된다.
좋은 점도 있다. credential은 일부 identity provider 간에 더 쉽게 교대해가며 사용할 수 있다. 이 객체 모델을 사용하면 여러 IDP를 사용할 때 설정이 달라져서 모든 identity provider에서 credential을 교대하지 않을 때도 문제되지 않는다.
Service Provider Metadata
스프링 시큐리티 SAML 2 구현체는 아직 SP 메타데이터를 XML 형식으로 다운로드하는 엔드포인트를 지원하지 않는다. 최소한 다음 정보를 교환해야 한다.
- entity ID - 디폴트는
{baseUrl}/saml2/service-provider-metadata/{registrationId}
이다. 다른 설정에서도 이 값을 사용한다.- Audience Restriction
- single signon URL - 디폴트는
{baseUrl}/login/saml2/sso/{registrationId}
이다. 다른 설정에서도 이 값을 사용한다.- Recipient URL
- Destination URL
- Assertion Consumer Service URL
- X509Certificate - {SIGNING,DECRYPTION} credential로 설정한 이 증명서는 반드시 Identity Provider와 공유해야 한다
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엔 세 가지 설정 옵션이 있다.
- 권한 extractor - assertion에서 그룹 정보를 파싱한다
- 권한 mapper - 추출한 그룹 정보를 내부 권한으로 매핑한다
- 응답 시간 검증 오차 범위 - 시간 동기화 이슈가 있을 수 있기 때문에 기본적으로 타임스탬프를 검증할 땐 어느 정도 오차를 허용해야 한다.
한 가지 커스텀 전략은 이 구현체가 생성한 객체를 수정할 수 있는 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 설정을 지원하는 스프링 부트 샘플을 제공해 왔다.
샘플을 실행해보려면, 아래 세 단계를 따라해라
- 스프링 부트 어플리케이션을 기동해라
./gradlew :spring-security-samples-boot-saml2login:bootRun
- 브라우저를 켜라
- http://localhost:8080/
- identity provider로 이동하면 아래 계정으로 로그인해라:
- User:
user
- Password:
password
- User:
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}
Next :Protection Against Exploits
서블릿 기반 어플리케이션에서 스프링 시큐리티로 취약점 공격을 방어하는 방법을 설명합니다. 공식 문서에 있는 "Protection Against Exploits in Servlet Application" 챕터를 한글로 번역한 문서입니다.
전체 목차는 여기에 있습니다.