Spring Authorization Server의 OIDC Logout 문제 해결하기

Spring Authorization Server의 OIDC Logout 문제 해결하기

December 1, 2024·kangwoo
kangwoo

Spring Authorization Server의 OIDC Logout 문제 해결하기

Spring Authorization Server를 운영하면서 OIDC(OpenID Connect) Logout 기능 구현 시 발생한 문제와 해결 과정을 공유하고자 합니다. 이 경험이 비슷한 상황에 직면한 다른 개발자들에게 도움이 되길 바랍니다.

배경 설명

OpenID Connect의 Logout 기능은 사용자가 안전하게 로그아웃할 수 있도록 표준화된 방법을 제공합니다. 이 과정에서 인증 서버는 사용자의 세션을 종료하고, 연결된 모든 클라이언트 애플리케이션에도 로그아웃을 전파해야 합니다. Spring Authorization Server는 이러한 OIDC 표준을 구현한 강력한 인증 서버 프레임워크입니다.

문제 상황

현재 Spring Authorization Server 1.2.4 버전을 사용하여 2대의 인스턴스로 인증 서버를 운영하고 있습니다. 고가용성을 위해 로드 밸런서를 통해 트래픽을 분산하고 있죠. OIDC Logout 기능을 구현하던 중 간헐적으로 다음과 같은 에러가 발생했습니다:

o.s.s.o.s.a.o.w.OidcLogoutEndpointFilter : Logout request failed: [invalid_token] OpenID Connect 1.0 Logout Request Parameter: sid
  
org.springframework.security.oauth2.core.OAuth2AuthenticationException: OpenID Connect 1.0 Logout Request Parameter: sid
at org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationProvider.throwError(OidcLogoutAuthenticationProvider.java:198) ~[spring-security-oauth2-authorization-server-1.2.4.jar:1.2.4]
at org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationProvider.authenticate(OidcLogoutAuthenticationProvider.java:159) ~[spring-security-oauth2-authorization-server-1.2.4.jar:1.2.4]
...

이 에러는 사용자가 로그아웃을 시도할 때 약 15-20% 정도의 확률로 발생했으며, 사용자 경험을 저해하는 중요한 문제였습니다.

원인 분석

문제의 근본 원인을 파악하기 위해 OidcLogoutAuthenticationProvider 코드를 자세히 분석했습니다.

// Check for active session  
if (StringUtils.hasText(oidcLogoutAuthentication.getSessionId())) {  
    SessionInformation sessionInformation = findSessionInformation(  
          currentUserPrincipal, oidcLogoutAuthentication.getSessionId());  
    if (sessionInformation != null) {  
       String sessionIdHash;  
       try {  
          sessionIdHash = createHash(sessionInformation.getSessionId());  
       } catch (NoSuchAlgorithmException ex) {  
          OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,  
                "Failed to compute hash for Session ID.", null);  
          throw new OAuth2AuthenticationException(error);  
       }  
  
       String sidClaim = idToken.getClaim("sid");  
       if (!StringUtils.hasText(sidClaim) ||  
             !sidClaim.equals(sessionIdHash)) {  
          throwError(OAuth2ErrorCodes.INVALID_TOKEN, "sid");  
       }  
    }  
}

코드 분석을 통해 다음과 같은 중요한 사실들을 발견했습니다:

  1. Spring Authorization Server는 기본적으로 SessionRegistryImpl을 사용하여 세션 정보를 관리합니다. 이 구현체는 메모리에 세션 정보를 저장하는 방식을 사용합니다.

  2. 로그아웃 과정에서 서버는 ID 토큰의 ‘sid’ 클레임과 현재 세션 ID의 해시값을 비교합니다. 이 비교는 보안을 위한 중요한 검증 단계입니다.

  3. 2대의 서버로 구성된 우리 환경에서는, 사용자의 요청이 서로 다른 인스턴스로 전달될 수 있습니다. 예를 들어, 로그인은 서버 A에서 처리되었는데 로그아웃은 서버 B에서 처리될 수 있습니다.

  4. 메모리 기반 세션 저장소를 사용하면 각 인스턴스가 독립적으로 세션을 관리하게 되어, 인스턴스 간 세션 정보가 동기화되지 않습니다. 이로 인해 한 서버에서는 유효한 세션이 다른 서버에서는 찾을 수 없는 상황이 발생합니다.

해결 방안

이 문제를 해결하기 위해서는 모든 인스턴스가 동일한 세션 정보를 공유할 수 있는 중앙 집중식 세션 저장소가 필요했습니다. Spring Session 프로젝트의 SpringSessionBackedSessionRegistry를 활용하여 다음과 같이 구현했습니다:

  1. 먼저 MongoDB를 세션 저장소로 선택했습니다. MongoDB는 높은 성능과 확장성을 제공하며, 세션 데이터의 영구 저장에 적합합니다.

  2. Spring Session MongoDB 지원을 활성화하기 위해 다음 의존성을 추가했습니다:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-mongodb</artifactId>
</dependency>
  1. MongoDB 세션 저장소를 활성화하기 위해 설정 클래스에 @EnableMongoHttpSession 어노테이션을 추가했습니다:
@Configuration
@EnableMongoHttpSession
public class SessionConfig {
    // 설정 내용
}
  1. SpringSessionBackedSessionRegistry를 구성하여 Spring Security와 통합했습니다:
@Bean  
public SessionRegistry sessionRegistry(MongoIndexedSessionRepository mongoIndexedSessionRepository) {  
    return new SpringSessionBackedSessionRegistry(mongoIndexedSessionRepository);  
}

이 설정으로 인해 모든 인스턴스가 동일한 세션 정보를 공유할 수 있게 되었고, OIDC Logout 과정에서 발생하던 세션 불일치 문제가 해결되었습니다.

결론

Spring Authorization Server를 클러스터 환경에서 운영할 때는 세션 관리 방식에 특별한 주의가 필요합니다. 기본으로 제공되는 메모리 기반의 세션 레지스트리는 단일 서버 환경에서는 잘 작동하지만, 다중 인스턴스 환경에서는 세션 불일치 문제를 일으킬 수 있습니다.

이러한 문제는 Spring Session과 같은 분산 세션 관리 솔루션을 도입함으로써 해결할 수 있습니다.

Last updated on