1. Authorization Code
Initiating the Authorization Request
OAuth2AuthorizationRequestRedirectFilter
- 로그인 요청을 시작하는 필터
- OAuth2AuthorizationRequestResolver를 사용해서 OAuth2AuthorizationRequest 생성
- 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;
}
출처
'Spring > Spring Security' 카테고리의 다른 글
[Spring Authorization Server] 1. Configuration Model (0) | 2025.07.20 |
---|---|
[Spring Security] 7-4. OAuth2 Client: Authorized Client Features (0) | 2025.07.20 |
[Spring Security] 7-1. OAuth2 Client: Core Interfaces and Classes (0) | 2025.07.18 |
[Spring Security] 6-4. OAuth2 Login: OIDC Logout (1) | 2025.07.18 |
[Spring Security] 6-3. OAuth2 Login: Advanced Configuration (0) | 2025.07.17 |