0. 준비작업

이전 포스팅

https://jinn-o.tistory.com/321

 

[SAML 2.0] IdP 와 SP 인증을 직접 구현해보자. (1) - IdP 설정

먼저 SAML 2.0 의 구조나 개념, 동작방식에 대해서 예습해오자.https://jinn-o.tistory.com/320 [SAML 2.0] Security Assertion Markup LanguageSAML 2.0 이란?OAuth 2.0 과 마찬가지로 인증/인가와 관련된 규칙들을 묶어서 표

jinn-o.tistory.com

 

먼저 SAML 2.0 의 구조나 개념, 동작방식에 대해서 예습해오자.

https://jinn-o.tistory.com/320

 

[SAML 2.0] Security Assertion Markup Language

SAML 2.0 이란?OAuth 2.0 과 마찬가지로 인증/인가와 관련된 규칙들을 묶어서 표준화한 방식이다.다만 이상한 점이 있다. MarkUp Language 라고 정의되어 있다. 먼저, OAuth 2.0 은 주로 인가(Authorization)와 관

jinn-o.tistory.com

 

해당 포스팅에서 SP-SpringBoot를 구현해 놓은 코드는 다음 Github 에 올려놓았다.

 

(시행착오 - 오류남)

https://github.com/SMJin/Security-SAML

 

GitHub - SMJin/Security-SAML: Spring Security SAML 2.0

Spring Security SAML 2.0 . Contribute to SMJin/Security-SAML development by creating an account on GitHub.

github.com

 

(추가 - 성공)

https://github.com/SMJin/SpringBoot-SAML

 

GitHub - SMJin/SpringBoot-SAML: Spring Boot 를 활용하여 SAML 2.0 방식의 SP *(Service Provider) 구축

Spring Boot 를 활용하여 SAML 2.0 방식의 SP *(Service Provider) 구축 - SMJin/SpringBoot-SAML

github.com

 

 

 

본격적으로 시작해보자.

1. Gradle Dependency 추가

implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security
implementation 'org.springframework.security:spring-security-saml2-service-provider' // SAML 2.0

 

 

2. Security 테스트

 

비밀번호는 실행 콘솔 창에 나와있다. 이 비밀번호는 실행할때마다 달라진다.

 

다음과 같이 오류 페이지가 뜨면 잘 된것이다. 왜냐하면 아무 컨트롤러도 만들어주지 않았기 때문.

Spring Security library 가 잘 implements 되었다.

 

 

3. spring-security-saml2-service-provider 라이브러리 오류

버전에 맞는 opensaml-saml-api 라이브러리가 없다는 오류가 발생했다.

 

하지만 maven repository 에 가보니 opensaml-saml-api:4.3.2 버전은 없었다.

 

 

그렇다면 spring-security-saml2-service-provider 라이브러리의 버전을 낮춰서 implements 하자.

implementation 'org.springframework.security:spring-security-saml2-service-provider:5.8.12' // SAML 2.0

 

 

4. IdP 엔지니어에게 받은 metadata.xml 파일을 다운로드한다.

metadata.xml 파일을 /src/main/resources/ 하위에 저장한다.

 

그전에 metadata.xml 파일에서 수정해야할 부분이 있다.

entityID를 keycloak Client ID 로 지정한 값으로 맞춰주어야 한다.

<EntityDescriptor entityID="http://localhost:8080/spring-saml-sp">

 

그리고 해당 경로를 application.yml 에 명시해줘야 한다.

spring:
  security:
    saml2:
      relyingparty:
        registration:
          spring-saml-sp:
            identityprovider:
              metadata-location: classpath:/saml/idp-metadata.xml # 메타데이터.xml 경로
            assertion-consumer-service-location: http://localhost:8080/login/saml2/sso/spring-saml-sp # Assertion (SAML Response) 받을 경로 (ACS URL)

 

 

5. 임시 유저를 생성하자.

 

비밀번호도 설정완료!

 

 

6. IDP-Initiated SSO 로 테스트해보자.

[형식]
http://<Keycloak-Host>/realms/<Realm-Name>/protocol/saml/clients/<Client-ID>

[예시]
http://localhost:8081/realms/identity-provider/protocol/saml/clients/spring-saml-sp

 

 

 

7. 문제발생. 왜 ClientId 가 "null" 로 받아와질까? SP 에서 설정 다시 검토.

① Client Id 확인해보기

 

keycloack 에 등록된 Client Id: spring-saml-sp

 

 

metadata.xml 에 지정한 entidyID 도 맞고 ...

 

 

application.yml 에서도 clientID를 잘 지정했는데 !

 

 

.. 어디서 Client ID 를 잘못 지정한거지?

라고 생각했는데 문제는 keycloak 에서 있었다.

 

설정을 이렇게하면 안된다! name 이니까 ClientID 를 지정해놓아야 한다!!!!!

 

 

 

1편에 작성해놓았던 설정파일을 수정해야겠다.. 총총

 

8. 다시 IDP-Initiated SSO 로 테스트해보자.

 

접속성공!

 

 

keycloak 에서 미리 작성하지 못했던 필요한 정보를 묻는다. 착실히 작성해준다.

 

SP (Spring) 페이지로 리다이렉트....... 

 

..성공?

 

9. SecurityConfig 클래스 작성해주기

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .saml2Login(Customizer.withDefaults())
                .authorizeHttpRequests(auth -> auth
                        .anyRequest().authenticated()
                )
                .build();
    }

}

 

Spring Security 에게 SAML 2.0 으로 로그인할 것이라고 명시해주어야 한다.

 

오류 발생!!

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Unsatisfied dependency expressed through method 'setFilterChains' parameter 0: Error creating bean with name 'securityFilterChain' defined in class path resource [spring/sso/common/config/SecurityConfig.class]: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'securityFilterChain' threw exception with message: org/springframework/security/saml2/provider/service/web/OpenSamlAuthenticationTokenConverter

 

뭔가 SAML 클래스 OpenSamlAuthenticationTokenConverter가 없다고 하니 라이브러리를 추가해주자.

아니면 버전이 안맞나?

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.security:spring-security-saml2-service-provider:6.1.2'
	implementation 'org.opensaml:opensaml-saml-api:4.0.1' // 추가
	implementation 'org.opensaml:opensaml-saml-impl:4.0.1' // 추가
	implementation 'org.opensaml:opensaml-core:4.0.1' // 추가
}

 

 

 

그러나 ...........................

치명적인 현재 상황 발생....

현재 상황 요약

  1. Spring Security 6.x 이상은 OpenSAML 4.3.x 이상을 요구
  2. Maven Central에는 OpenSAML 4.0.1까지만 제공되고, OpenSAML 4.3.x는 공식적으로 아직 릴리스되지 않았다....
  3. Spring Boot 3.x 이상 환경에서는 Spring Security 6.x와 호환성을 유지해야 하지만, OpenSAML의 부재로 인해 일부 기능에서 제약이 발생하고 있다 !!!!!!!.

 

결론.

현재 Spring Security 를 이용해서 SAML2.0 을 구현하는 건 포기해야 된다. ㅠㅠ

 

 

10. RequestURL 생성 로직부터.. 다시 해보자 ^^

다음 url 에는 RequestURL 명세에 대해서 정말 잘 나와있다.

https://developers.worksmobile.com/kr/docs/sso-idp-request

 

NAVER WORKS Developers

 

developers.worksmobile.com

 

Controller

@GetMapping("/sso/login")
public RedirectView redirectToIdP() {
    String samlRequest = samlApiService.createSamlRequest();
    String redirectUrl = samlApiService.getRedirectUrl(samlRequest);
    return new RedirectView(redirectUrl);
}

 

Service

package spring.saml.sp.service;

import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.UUID;

@Service
public class SamlApiService {

    public String getRedirectUrl(String samlRequest) {
        String redirectUrl = "http://localhost:8081/realms/identity-provider/protocol/saml/clients/spring-saml-sp"
                + "?SAMLRequest=" + URLEncoder.encode(samlRequest, StandardCharsets.UTF_8)
                + "&RelayState=" + URLEncoder.encode("/dashboard", StandardCharsets.UTF_8);

        System.out.println("Redirect to: " + redirectUrl);
        return redirectUrl;
    }

    public String createSamlRequest() {
        try {
            Document authnRequestDoc = getAuthnRequest();

            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            DOMSource source = new DOMSource(authnRequestDoc);
            StringWriter writer = new StringWriter();
            transformer.transform(source, new StreamResult(writer));

            return Base64.getEncoder().encodeToString(writer.toString().getBytes(StandardCharsets.UTF_8));
        } catch (Exception e) {
            throw new RuntimeException("Failed to create SAML request - AuthnRequest", e);
        }
    }

    private Document getAuthnRequest() {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            DocumentBuilder builder = factory.newDocumentBuilder();

            Document document = builder.newDocument();
            Element authnRequest = document.createElementNS("urn:oasis:names:tc:SAML:2.0:protocol", "samlp:AuthnRequest");
            authnRequest.setAttribute("AssertionConsumerServiceURL", "http://localhost:8080/saml/consume");
            authnRequest.setAttribute("ID", "_" + UUID.randomUUID());
            authnRequest.setAttribute("IssueInstant", Instant.now().toString());
            authnRequest.setAttribute("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
            authnRequest.setAttribute("ProviderName", "spring-saml-sp");
            authnRequest.setAttribute("Version", "2.0");

            Element issuerElement = document.createElementNS("urn:oasis:names:tc:SAML:2.0:assertion", "saml:Issuer");
            issuerElement.setTextContent("http://localhost:8080/saml");
            authnRequest.appendChild(issuerElement);

            Element nameIDPolicyElement = document.createElementNS("urn:oasis:names:tc:SAML:2.0:protocol", "samlp:NameIDPolicy");
            nameIDPolicyElement.setAttribute("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified");
            authnRequest.appendChild(nameIDPolicyElement);

            document.appendChild(authnRequest);
            return document;
        } catch (Exception e) {
            throw new RuntimeException("Failed to create SAML request - AuthnRequest", e);
        }
    }
}

 

 

11. CORS 오류를 해결하자

그런데 왜인지 cors 오류가 났다. 일단 결론만 말하자면

Spring MVC 에서 기본적으로 제공하는 cors 필터만으로는 부족하다.

cors 필터를 커스텀해야 한다.

package spring.saml.sp.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.List;

@Configuration
public class CorsConfig {

    @Bean
    public FilterRegistrationBean<CorsFilter> corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true); // 쿠키와 세션 허용
        config.setAllowedOrigins(List.of("http://localhost:8081", "http://localhost:3000"));
        config.setAllowedOriginPatterns(List.of("null")); // 명시적으로 null 허용
        config.setAllowedHeaders(List.of("*"));
        config.setAllowedMethods(List.of("GET", "POST", "OPTIONS"));
        source.registerCorsConfiguration("/**", config);

        FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
        bean.setOrder(0); // 가장 먼저 실행되도록 우선순위 설정
        return bean;
    }
}

 

오류에 대한 자세한 내용은 다음을 참고해보자.

https://jinn-o.tistory.com/352

 

[Spring MVC] WebMvcConfigurer로 cross-origin 설정을 해도 cors 오류가 나는 이유 (304 응답에서)

이 문제 때문에 며칠을 애먹었는지 모른다. 아무튼 결론부터 말하겠다. Spring MVC는 3xx 응답(특히 304)에는 CORS 헤더를 자동으로 추가하지 않는다 ... 아무리 내가 다음과 같이 지정했다고 해도 말

jinn-o.tistory.com

 

 

12. http://localhost:8080/saml/login 으로 이동하자. (SAML Request 전송)

 

13. acs url 에 SAML Response 가 잘담겨있다

 

 

 

14. 다음으로

React 까지 연결해보자.

https://jinn-o.tistory.com/353

 

[SAML 2.0] IdP 와 SP 인증을 직접 구현해보자. (3) - SP 설정 : React편

0. 준비작업이전 포스팅 1편 - IdP 구성https://jinn-o.tistory.com/321 [SAML 2.0] IdP 와 SP 인증을 직접 구현해보자. (1) - IdP 설정0. 준비작업먼저 SAML 2.0 의 구조나 개념, 동작방식에 대해서 예습해오자.https:/

jinn-o.tistory.com

 

+ Recent posts