Spring/Spring Security

[Spring Security] 7-2. OAuth2 Client: Authorization Grant Support

noahkim_ 2025. 7. 19. 09:02

1. Authorization Code

Initiating the Authorization Request

OAuth2AuthorizationRequestRedirectFilter
  • 로그인 요청을 시작하는 필터
  1. OAuth2AuthorizationRequestResolver를 사용해서 OAuth2AuthorizationRequest 생성
  2. user-agent를 Authorization Sever의 authorization endpoint로 리디렉션함

 

OAuth2AuthorizationRequestResolver
  • 웹 요청 정보에서 OAuth2AuthorizationRequest를 구성해주는 역할

 

PKCE (Proof Key for Code Exchange)
  • OAuth 2.0 Authorization Code Grant 흐름의 보안 강화를 위해 도입된 추가 인증 메커니즘
항목 내용
목적 Authorization Code 탈취 방지
취약점 - code는 redirect_uri를 통해 전달되므로 브라우저 주소창에 노출됨
- 악성 앱이나 프록시가 이 인가 코드를 가로챌 수 있음
사용 대상
authorization_code grant type을 사용하는 Public Client (SPA, Mobile 앱 등)
주요 흐름 1. client: code_verifier 생성
2. user-agent: 인가 요청 시 code_challenge를 authorization server에 보냄
3. client: 토큰 요청 시 code_verifier를 authorization server에 보냄
4. authorization server가 둘이 일치하는지 확인 후 토큰 발급
용어
- code_verifier: 클라이언트가 생성한 랜덤 문자열
- code_challenge: code_verifier를 SHA256 후 base64url 인코딩한 값
- code_challenge_method: 보통 "S256" (해시 방식 지정)

 

Customizing the Authorization Request

  • OAuth2 인가 요청 외에 추가 파라미터를 인가 요청에 포함시키고 싶을 때 사용
  • OAuth2AuthorizationRequestResolver를 커스터마이징해서 처리

 

커스터마이징) authorizationRequestResolver

더보기
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
        .oauth2Login(oauth2 -> oauth2
            .authorizationEndpoint(endpoint -> endpoint
                .authorizationRequestResolver(
                    customAuthorizationRequestResolver(clientRegistrationRepository)
                )
            )
        );
    return http.build();
}
private OAuth2AuthorizationRequestResolver customAuthorizationRequestResolver(
        ClientRegistrationRepository clientRegistrationRepository) {

    DefaultOAuth2AuthorizationRequestResolver resolver =
        new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, "/oauth2/authorization");

    resolver.setAuthorizationRequestCustomizer(
        customizer -> customizer
            .additionalParameters(params -> params.put("prompt", "consent"))
    );

    return resolver;
}

 

Storing the Authorization Request

  • AuthorizationRequestRepository: 인가 요청과 인가 응답 사이에 요청 정보를 저장하고 복구하는 역할
  • HttpSessionOAuth2AuthorizationRequestRepository (기본) : HttpSession에 OAuth2AuthorizationRequest 저장

 

커스터마이징) AuthorizationRequestRepository

더보기
@Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {
    return new CustomOAuth2AuthorizationRequestRepository(); // 직접 구현한 저장소
}
  • 저장소, 쿠키 등 구현 가능

 

설정) SecurityFilterChain

더보기
.oauth2Login(oauth2 -> oauth2
    .authorizationEndpoint(endpoint -> endpoint
        .authorizationRequestRepository(authorizationRequestRepository())
    )
)

.oauth2Client(oauth2 -> oauth2
    .authorizationCodeGrant(codeGrant -> codeGrant
        .authorizationRequestRepository(authorizationRequestRepository())
    )
)
  • login, server to server에서 사용하므로 두 곳에 설정해주기

 

Requesting an Access Token

OAuth2AccessTokenResponseClient
  • authorization_code로 받은 code를 이용해 token_endpoint로 HTTP 요청을 보내는 역할을 하는 객체
구현체 설명
DefaultAuthorizationCodeTokenResponseClient 기본 구현 (기존 RestTemplate 기반)
RestClientAuthorizationCodeTokenResponseClient 새로운 구현 (RestClient 기반, WebClient 스타일)

 

설정) RestClientAuthorizationCodeTokenResponseClient

더보기
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
    return new RestClientAuthorizationCodeTokenResponseClient();
}
  • 이 Bean을 등록해두면, OAuth2AuthorizedClientManager 또는 로그인 필터가 자동으로 사용함

 

커스터마이징) RestClientAuthorizationCodeTokenResponseClient

더보기
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
    RestClientAuthorizationCodeTokenResponseClient client =
        new RestClientAuthorizationCodeTokenResponseClient();

    // 필요에 따라 RequestEntityConverter, ResponseConverter 등을 설정 가능
    client.setRequestEntityConverter(customRequestConverter());
    client.setRestClient(customRestClient());

    return client;
}
private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> customRequestConverter() {
    return request -> {
        OAuth2AuthorizationCodeGrantRequest authRequest = request;
        ClientRegistration registration = authRequest.getClientRegistration();

        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
        form.add("grant_type", "authorization_code");
        form.add("code", authRequest.getAuthorizationExchange().getAuthorizationResponse().getCode());
        form.add("redirect_uri", authRequest.getAuthorizationExchange().getAuthorizationRequest().getRedirectUri());

        // ✅ 기본 방식은 client_id + client_secret
        // 필요하면 여기에 다른 custom 파라미터도 추가 가능
        form.add("client_id", registration.getClientId());

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

        return new RequestEntity<>(form, headers, HttpMethod.POST, URI.create(registration.getProviderDetails().getTokenUri()));
    };
}
private RestClient customRestClient() {
    return RestClient.builder()
        .requestInterceptor((request, body, execution) -> {
            // ✅ 예: 로그 출력 또는 추가 헤더 삽입
            request.getHeaders().add("X-Custom-Header", "MyValue");
            return execution.execute(request, body);
        })
        .build();
}

 

Customizing the Access Token Request

  • Access Token 요청의 헤더 및 파라미터 커스터마이징
  • RestClientAuthorizationCodeTokenResponseClient 설정을 통해 가능

 

커스터마이징) 요청 헤더

더보기

addHeadersConverter()

accessTokenResponseClient.addHeadersConverter(grantRequest -> {
    HttpHeaders headers = new HttpHeaders();
    if (grantRequest.getClientRegistration().getRegistrationId().equals("spring")) {
        headers.set(HttpHeaders.USER_AGENT, "my-user-agent");
    }
    return headers;
});
  • 필요한 헤더만 추가됨 (기본 헤더 유지)

 

setHeadersConverter()

DefaultOAuth2TokenRequestHeadersConverter headersConverter =
    new DefaultOAuth2TokenRequestHeadersConverter();
headersConverter.setEncodeClientCredentials(false); // 기본 Basic 인증 제거

accessTokenResponseClient.setHeadersConverter(headersConverter);
  • 특정 헤더 제거 가능
  • 전체 헤더 새로 구성 가능

 

커스터마이징) 파라미터

더보기

addParametersConverter()

accessTokenResponseClient.addParametersConverter(grantRequest -> {
    MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
    if (grantRequest.getClientRegistration().getRegistrationId().equals("keycloak")) {
        parameters.set(OAuth2ParameterNames.AUDIENCE, "my-audience");
    }
    return parameters;
});
  • 필요한 파라미터만 추가 (기본 파라미터 유지)

 

setParametersConverter()

accessTokenResponseClient.setParametersConverter(grantRequest -> {
    LinkedMultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
    if (grantRequest.getClientRegistration().getRegistrationId().equals("okta")) {
        parameters.set(OAuth2ParameterNames.CLIENT_ID, "my-client");
    }
    return parameters;
});
  • 직접 기본 파라미터도 모두 넣어야 함

 

setParametersCustomizer()

accessTokenResponseClient.setParametersCustomizer(parameters -> {
    if (parameters.containsKey(OAuth2ParameterNames.CLIENT_ASSERTION)) {
        parameters.remove(OAuth2ParameterNames.CLIENT_ID);
    }
});
  • Spring Security가 구성한 기본 파라미터 리스트에 대해 후처리 (제거 및 조건부 수정 가능)

 

Customizing the Access Token Response

  • OAuth2 Access Token 응답을 커스터마이징할 수 있음

 

기본) RestClient

더보기
RestClient restClient = RestClient.builder()
    .messageConverters(converters -> {
        converters.clear();
        converters.add(new FormHttpMessageConverter()); // request
        converters.add(new OAuth2AccessTokenResponseHttpMessageConverter()); // response
    })
    .defaultStatusHandler(new OAuth2ErrorResponseErrorHandler()) // error
    .build();
  • FormHttpMessageConverter: access token 요청에 사용됨 (x-www-form-urlencoded 전송)
  • OAuth2AccessTokenResponseHttpMessageConverter: token 응답을 Java 객체로 변환
  • OAuth2ErrorResponseErrorHandler: 오류 응답 처리 (400, 401 등)

 

커스터마이징) RestClient

더보기
OAuth2AccessTokenResponseHttpMessageConverter accessTokenResponseMessageConverter =
    new OAuth2AccessTokenResponseHttpMessageConverter();

accessTokenResponseMessageConverter.setAccessTokenResponseConverter(parameters -> {
    // 이곳에서 응답 파라미터 직접 가공 가능
    String accessToken = parameters.get("access_token").toString();

    return OAuth2AccessTokenResponse.withToken("custom-" + accessToken)
        .tokenType(OAuth2AccessToken.TokenType.BEARER)
        .expiresIn(3600)
        .build();
});
  • JSON 파라미터 → OAuth2AccessTokenResponse로 가공하는 Converter를 직접 정의

 

커스터마이징) RestClient - errorHandler converter

더보기
OAuth2ErrorHttpMessageConverter errorConverter = new OAuth2ErrorHttpMessageConverter();

errorConverter.setErrorConverter(parameters -> {
    return new OAuth2Error(
        "custom_error_code",
        "Custom error description",
        "https://example.com/docs/errors#custom"
    );
});

OAuth2ErrorResponseErrorHandler errorHandler = new OAuth2ErrorResponseErrorHandler();
errorHandler.setErrorConverter(errorConverter);
  • 인증 서버의 오류를 원하는 객체로 변환하고 싶을 때 사용

 

Customize using the DSL

설정) OAuth2AccessTokenResponseClient

더보기
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .oauth2Client(oauth2 -> oauth2
            .authorizationCodeGrant(codeGrant -> codeGrant
                .accessTokenResponseClient(this.accessTokenResponseClient())
                // ...
            )
        );
    return http.build();
}

 

2. Refresh Token

Refreshing an Access Token

RestClientRefreshTokenTokenResponseClient
  • 기본 구현체
  • reactive 방식의 요청

 

설정) RestClientRefreshTokenTokenResponseClient

더보기
@Bean
public OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient() {
	return new RestClientRefreshTokenTokenResponseClient();
}

 

Customizing the Access Token Request

커스터마이징) 헤더 추가

더보기
accessTokenResponseClient.addHeadersConverter(grantRequest -> {
    ClientRegistration clientRegistration = grantRequest.getClientRegistration();
    HttpHeaders headers = new HttpHeaders();
    if (clientRegistration.getRegistrationId().equals("spring")) {
        headers.set(HttpHeaders.USER_AGENT, "my-user-agent"); // 사용자 정의 User-Agent 추가
    }
    return headers;
});

 

커스터마이징) 헤더 재정의

더보기
DefaultOAuth2TokenRequestHeadersConverter headersConverter =
	new DefaultOAuth2TokenRequestHeadersConverter();
headersConverter.setEncodeClientCredentials(false); // 기본 헤더에서 client 자격증명 form으로 전송

accessTokenResponseClient.setHeadersConverter(headersConverter);

 

커스터마이징) 파라미터 추가

더보기
accessTokenResponseClient.addParametersConverter(grantRequest -> {
    ClientRegistration clientRegistration = grantRequest.getClientRegistration();
    MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
    if (clientRegistration.getRegistrationId().equals("keycloak")) {
        parameters.set(OAuth2ParameterNames.AUDIENCE, "my-audience");
    }
    return parameters;
});

 

커스터마이징) 파라미터 오버라이드

더보기
accessTokenResponseClient.setParametersConverter(grantRequest -> {
    ClientRegistration clientRegistration = grantRequest.getClientRegistration();
    LinkedMultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
    if (clientRegistration.getRegistrationId().equals("okta")) {
        parameters.set(OAuth2ParameterNames.CLIENT_ID, "my-client");
    }
    return parameters;
});

 

커스터마이징) 파라미터 재정의

더보기
accessTokenResponseClient.setParametersCustomizer(parameters -> {
    if (parameters.containsKey(OAuth2ParameterNames.CLIENT_ASSERTION)) {
        parameters.remove(OAuth2ParameterNames.CLIENT_ID); // client_assertion이 있을 경우 client_id 제거
    }
});

 

Customizing the Access Token Response

  • 요청을 담당하는 restClient를 set하는 방식
  • restClient에 사용될 converter를 세팅한 후, RestClientRefreshTokenTokenResponseClient에 설정된 restClient를 셋팅

 

커스터마이징) 반환 객체

더보기
OAuth2AccessTokenResponseHttpMessageConverter accessTokenResponseMessageConverter =
	new OAuth2AccessTokenResponseHttpMessageConverter();

accessTokenResponseMessageConverter.setAccessTokenResponseConverter(parameters -> {
	// 파라미터 기반으로 직접 응답 객체 구성
	return OAuth2AccessTokenResponse.withToken("custom-token")
		.tokenType(OAuth2AccessToken.TokenType.BEARER)
		.expiresIn(3600)
		.build();
});

 

커스터마이징) 에러 핸들

더보기
OAuth2ErrorHttpMessageConverter errorConverter = new OAuth2ErrorHttpMessageConverter();
errorConverter.setErrorConverter(parameters -> {
	// 에러 파라미터 기반으로 OAuth2Error 구성
	return new OAuth2Error("custom-error", "custom description", "custom-uri");
});

OAuth2ErrorResponseErrorHandler errorHandler = new OAuth2ErrorResponseErrorHandler();
errorHandler.setErrorConverter(errorConverter);

 

Customize using the Builder

  • OAuth2AccessTokenResponseClient를 Builder 방식으로 구현하는 방법

 

설정) OAuth2AccessTokenResponseClient

더보기
// 사용자 정의 구현체 (예: 커스터마이징한 RestClientRefreshTokenTokenResponseClient)
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient = ...;

// Provider 구성
OAuth2AuthorizedClientProvider authorizedClientProvider =
	OAuth2AuthorizedClientProviderBuilder.builder()
		.authorizationCode()
		.refreshToken(configurer ->
			configurer.accessTokenResponseClient(refreshTokenTokenResponseClient)) // 여기서 설정
		.build();

// Manager에 설정
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
  • OAuth2AuthorizedClientProviderBuilder로 OAuth2AuthorizedClientProvider를 정의함
  • OAuth2AuthorizedClientManager에 셋팅하는 방식

 

6. Token Exchange

  • 기존 토큰을 다른 종류의 토큰으로 교환할 수 있게 해주는 OAuth2.0의 확장 기능

 

Requesting an Access Token

  • RestClientTokenExchangeTokenResponseClient를 사용하여 요청함

 

Using the Access Token

  • token-exchange는 관련 provider 객체를 자동으로 등록해주지 않음
  • 추가적인 설정 필요

 

설정) OAuth2AuthorizedClientManager

더보기
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    // Token Exchange Provider 구성
    TokenExchangeOAuth2AuthorizedClientProvider tokenExchangeProvider =
        new TokenExchangeOAuth2AuthorizedClientProvider();

    // 필요 시 SubjectTokenResolver 설정 (카카오 access_token 전달)
    tokenExchangeProvider.setSubjectTokenResolver(context -> {
        // 카카오 토큰을 subject_token으로 설정
        OAuth2AuthenticationToken kakaoAuth = (OAuth2AuthenticationToken) context.getPrincipal();
        OAuth2AuthorizedClient kakaoClient = context.getAuthorizedClientRepository()
            .loadAuthorizedClient("kakao", kakaoAuth, context.getRequest());

        return kakaoClient.getAccessToken(); // or getIdToken() depending on your use
    });

    // Provider 빌더로 등록
    OAuth2AuthorizedClientProvider authorizedClientProvider =
        OAuth2AuthorizedClientProviderBuilder.builder()
            .provider(tokenExchangeProvider)
            .build();

    // 매니저 설정
    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
        new DefaultOAuth2AuthorizedClientManager(
            clientRegistrationRepository, authorizedClientRepository);

    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

 

 

출처