개발공부/주저리

Spring 의 OAuth2 Client: 핵심 인터페이스와 클래스

siotMan 2025. 6. 1. 12:53

이 글은 다음 가이드를 정리한 내용이다. Reactive 어플리케이션은 이 가이드를 참조해야한다. 기술할 인터페이스 및 클래스는 아래와 같다.

ClientRegistration

OIDC 1.0 제공자나 OAuth 2.0 으로 등록된 클라이언트의 표현이며 아래와 같은 내용을 담는다.

public final class ClientRegistration {
	private String registrationId;	
	private String clientId;	
	private String clientSecret;	
	private ClientAuthenticationMethod clientAuthenticationMethod;	
	private AuthorizationGrantType authorizationGrantType;	
	private String redirectUri;	
	private Set<String> scopes;	
	private ProviderDetails providerDetails;
	private String clientName;	

	public class ProviderDetails {
		private String authorizationUri;	
		private String tokenUri;	
		private UserInfoEndpoint userInfoEndpoint;
		private String jwkSetUri;	
		private String issuerUri;	
		private Map<String, Object> configurationMetadata;  

		public class UserInfoEndpoint {
			private String uri;	
			private AuthenticationMethod authenticationMethod;  
			private String userNameAttributeName;	

		}
	}

	public static final class ClientSettings {
		private boolean requireProofKey; 
	}
}

  • registrationId: ClientRegistration 의 고유 아이디
  • clientId / clientSecret: 클라이언트 아이디와 시크릿
  • clientAuthenticationMethod:
    • client_secret_basic
    • client_secret_post
    • private_key_jwt
    • client_secret_jwt
    • none (public clients)
  • authorizationGrantType: OAuth 2.0 의 4가지 인가 타입
    • authorization_code
    • client_credentials
    • password
    • urn:ietf:params:oauth:grant-type:jwt-bearer 의 확장 타입
  • redirectUri: 클라이언트가 등록한 리디렉트 URI
  • scopes: openid, email, profile 과 같이 클라이언트가 인증 요청 흐름에서 요청한 범위
  • clientName: 클라이언트를 보일 이름
  • authorizationUri: 인가서버의 엔드포인트
  • tokenUri: 인가서버의 토큰 엔드포인트
  • jwkSetUri: JSON Web Key (JWK) 세트를 얻기위한 인가서버의 엔드포인트. 이는 아이디 토큰과 추가적 유저 정보 응답의 JWS 를 검증하기 위해 사용
  • issuerUri: ODIC 혹은 OAUth 2.0 인가 서버의 발급자 식별자 URI
  • configurationMetadata: The OpenID Provider Configuration Information. ...provider.[providerId].issuerUri 가 설정되었을 때 활용 가능함
  • userInfoEndpoint.uri: 인증된 엔드 유저의 클레임/속성에 접근하기 위해 사용하는 유저 정보 엔드포인트
  • userInfoEndpoint.authenticationMethod: UserInfo 엔드포인트에 어세스 토큰을 보낼 때 사용할 인증 방식
    • header
    • form
    • query
  • userNameAttributeName: 엔드 유저의 이름 혹은 식별자를 참조하는 UserInfo 응답속 속성의 이름
  • requireProofKey: 이 값이 true 거나 authorizationGrantType 가 none 인 경우 PKCE 가 default 로 활성화

하나의 ClientRegistration 은 OpenID COnnect Provider 의 Configuration endpoint 혹은 인가 서버의  Metadata endpoint 로 초기화 될 수 있다.

ClientRegistrations 는 위 방식의 초기화 메소드를 제공한다.

val clientRegistration = ClientRegistrations.fromIssuerLocation("<https://idp.example.com/issuer>").build()

상기 쿼리는 아래를 순차적으로 호출한다.

OpenID Connect Provider 만의 엔드포인트를 호출하기 위해선 그 대안으로 lientRegistrations.fromOidcIssuerLocation() 를 볼 수 있다.

ClientRegistrationRepository

ClientRegistration 들의 리포지토리이다.

스프링 부트 자동 설정은 spring.security.oauth2.client.registration.*[registrationId]* 이하 프로퍼티 값을 각 ClientRegistration 에 바인드하고, 각 ClientRegistration 인스턴스를 하나의 ClientRegistrationRepository 에 구성한다.

ClientRegistrationRepository 의 기본 구현체는 InMemoryClientRegistrationRepository 이다. 자동설정은 ClientRegistrationRepository 빈으로 등록한다.

OAuth2AuthorizedClient

인가된 클라이언트의 표현이다. OAuth2AuthorizedClient 는 OAuth2AccessToken (and optional OAuth2RefreshToken) 와 ClientRegistration (client) 및 이를 허가한 Principal 된 엔드유저인 리소스 소유자와 연관짓는 것을 담당한다.

OAuth2AuthorizedClientRepository and OAuth2AuthorizedClientService

OAuth2AuthorizedClientRepository 는 웹 요청사이 OAuth2AuthorizedClient(s) 들을 영속할 책임을 갖는다. 한편 OAuth2AuthorizedClientService는 어플리케이션 레벨에서 OAuth2AuthorizedClient(s) 들을 관리한다.

개발자 관점에서 OAuth2AuthorizedClientRepository or OAuth2AuthorizedClientService 는 클라이언트와 연관된 OAuth2AccessToken 를 룩업할 능력을 제공한다. 이는 보호된 리소스 요청을 초기화할 때 사용될 수 있다.

@Controller
class OAuth2ClientController {

    @Autowired
    private lateinit var authorizedClientService: OAuth2AuthorizedClientService

    @GetMapping("/")
    fun index(authentication: Authentication): String {
        val authorizedClient: OAuth2AuthorizedClient =
            this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName());
        val accessToken = authorizedClient.accessToken

        ...

        return "index";
    }
}

스프링 자동설정은 이들을 빈으로 등록한다. 그러나 어플리케이션이 이들의 오버라이드와 등록을 결정할 수 있다.

OAuth2AuthorizedClientService 의 기본 구현체는 InMemoryOAuth2AuthorizedClientService 이며 OAuth2AuthorizedClient 를 인 메모리에 저장한다.

다르게, JdbcOAuth2AuthorizedClientService 를 사용하여 OAuth2AuthorizedClient 를 디비에 저장할 수 있다. 이는 OAuth 2.0 Client Schema 에 서술된 테이블 정의에 의존한다.

OAuth2AuthorizedClientManager and OAuth2AuthorizedClientProvider

OAuth2AuthorizedClientManager 는 OAuth2AuthorizedClient 들의 전체적인 관리를 담당한다.

우선적인 책임은 다음을 포함한다.

  • OAuth2AuthorizedClientProvider 를 사용하여, OAuth 2.0 Client 를 인가 혹은 재인가
  • OAuth2AuthorizedClient 의 영속화를 위임한다. 일반적으로 OAuth2AuthorizedClientService 혹은 OAuth2AuthorizedClientRepository 를 활용한다.
  • OAuth 2.0 Client 의 인가 혹은 재인가가 성공 혹은 실패 시 요청을 아래로 위임한다.
    • 성공 시: OAuth2AuthorizationSuccessHandler
    • 실패 시: OAuth2AuthorizationFailureHandler

OAuth2AuthorizedClientProvider 는 OAuth 2.0 Client 를 인가 혹은 재인가하기 위한 전략을 구현한다. (authorization grant type such as authorization_code, client_credentials, and others.)

OAuth2AuthorizedClientManager 의 기본 구현체는 DefaultOAuth2AuthorizedClientManager 이다. 이는 위임 베이스 구성을 통해 다중 인가 타입을 지원할 수 있는 OAuth2AuthorizedClientProvider 와 연관된다. OAuth2AuthorizedClientProviderBuilder 를 통해 위임 베이스 구성을 설정하고 빌드할 수 있다.

이하 예제는 OAuth2AuthorizedClientProvider 를 설정하고 빌드하는 예시이다. 이하에서는 authorization_code, refresh_token, client_credentials, and password 인가 그랜트 타입을 구성한다.

@Bean
fun authorizedClientManager(
        clientRegistrationRepository: ClientRegistrationRepository,
        authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager {
    val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
            .authorizationCode()
            .refreshToken()
            .clientCredentials()
            .password()
            .build()
    val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(
            clientRegistrationRepository, authorizedClientRepository)
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
    return authorizedClientManager
}

인가 시도가 성공한 경우:

  • DefaultOAuth2AuthorizedClientManager 가 OAuth2AuthorizationSuccessHandler 로 요청을 위임한다.
  • 이는 기본적으로 OAuth2AuthorizedClientRepository 통해 OAuth2AuthorizedClient 를 저장한다.

재인가 시도가 실패한 경우:

  • 기존에 저장한 OAuth2AuthorizedClient 가 OAuth2AuthorizedClientRepository 에서 삭제된다.
  • 이는 RemoveAuthorizedClientOAuth2AuthorizationFailureHandler 에서 이루어진다.

이러한 동작은 이하 setter 에서 사용자화할 수 있다.

  • setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler)
  • setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)

DefaultOAuth2AuthorizedClientManager는 또한 Function<OAuth2AuthorizeRequest, Map<String, Object>> 유형의 contextAttributesMapper와 연결되며, 이는 OAuth2AuthorizeRequest의 속성을 OAuth2AuthorizationContext에 연결할 속성 맵에 매핑하는 일을 담당한다. 이 기능은 OAuth2AuhorizedClientProvider에 필수(지원되는) 속성을 제공해야 할 때 유용하다. (예: PasswordOAuth2AuthorizedClientProvider는 리소스 소유자의 사용자 이름과 비밀번호가 OAuth2AuthorizationContext.getAttributes()에서 사용 가능해야한다).

이하 예시는 contextAttributesMapper 의 예시를 보인다.

@Bean
fun authorizedClientManager(
        clientRegistrationRepository: ClientRegistrationRepository,
        authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager {
    val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
            .password()
            .refreshToken()
            .build()
    val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(
            clientRegistrationRepository, authorizedClientRepository)
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)

    // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
    // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
    authorizedClientManager.setContextAttributesMapper(contextAttributesMapper())
    return authorizedClientManager
}

private fun contextAttributesMapper(): Function<OAuth2AuthorizeRequest, MutableMap<String, Any>> {
    return Function { authorizeRequest ->
        var contextAttributes: MutableMap<String, Any> = mutableMapOf()
        val servletRequest: HttpServletRequest = authorizeRequest.getAttribute(HttpServletRequest::class.java.name)
        val username: String = servletRequest.getParameter(OAuth2ParameterNames.USERNAME)
        val password: String = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD)
        if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
            contextAttributes = hashMapOf()

            // `PasswordOAuth2AuthorizedClientProvider` requires both attributes
            contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username
            contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password
        }
        contextAttributes
    }
}

DefaultOAuth2AuthorizedClientManager 는 HttpServletRequest 의 맥락에서 사용되도록 설계되었다. HttpServletRequest context 밖에서 조작해야할 경우 AuthorizedClientServiceOAuth2AuthorizedClientManager 를 사용하라.

서비스 어플리케이션은 AuthorizedClientServiceOAuth2AuthorizedClientManager 를 사용하는 일반적인 유즈케이스이다. 서비스 어플리케이션들은 유저 상호작용 없이, 곧잘 백그라운드에서 실행되며 일반적으로 사용자 계정이 아닌 시스템 레벨 계정에서 실행된다. 이 때 OAuth 2.0 Client 는 client_credentials 을 서비스 어플리케이션의 인가 타입으로 설정할 수 있다.

이하 예시는 client_credentials 를 제공하는 AuthorizedClientServiceOAuth2AuthorizedClientManager 설정 예시이다.

@Bean
fun authorizedClientManager(
        clientRegistrationRepository: ClientRegistrationRepository,
        authorizedClientService: OAuth2AuthorizedClientService): OAuth2AuthorizedClientManager {
    val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
            .clientCredentials()
            .build()
    val authorizedClientManager = AuthorizedClientServiceOAuth2AuthorizedClientManager(
            clientRegistrationRepository, authorizedClientService)
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
    return authorizedClientManager