OAuth 2.0 Client는 크게 3가지의 핵심 기능을 가지고 있다.
- Authorization Grant
- Client Authentication
- HTTP Client
HttpSecurity.oauth2Client()를 이용하여 이러한 핵심 특징들을 커스터마이징할 수 있으며, Authorization Code grant와 관련된 수정도 가능하다.
@Configuration
@EnableWebSecurity
public class OAuth2ClientSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Client(oauth2 -> oauth2
.clientRegistrationRepository(this.clientRegistrationRepository())
.authorizedClientRepository(this.authorizedClientRepository())
.authorizedClientService(this.authorizedClientService())
.authorizationCodeGrant(codeGrant -> codeGrant
.authorizationRequestRepository(this.authorizationRequestRepository())
.authorizationRequestResolver(this.authorizationRequestResolver())
.accessTokenResponseClient(this.accessTokenResponseClient())
)
);
return http.build();
}
}
위 코드는 Configuration파일에서 HttpSecurity.oauth2Client()를 이용하여 설정할 수 있는 모든 내용을 보여준다.

OAuth2 Login을 통해 최종적으로 생성되는 클래스 타입으로, 인증된 사용자 자체를 나타낸다.
ClientRegistration와 그에 해당하는 Token들로 구성되어있다
OAuth2AuthorizedClientRepository: 웹 요청간에OAuth2AuthorizedClient를 유지하기 위한 저장소
OAuth2AuthorizedClientService: 애플리케이션 수준에서 저장된OAuth2AuthorizedClient접근을 위한 관리개발자의 관점에서 보자면 보호된 리소스 접근을 위해 필요한 access token을 관리하고 있는 클래스라고 볼 수 있다.
Spring Boot 자동설정에 의해 이 클래스들은 Bean으로 등록되며, 원한다면 커스텀 클래스를 등록할 수도 있다.
기본적으로 OAuth2AuthorizedClientService는 InMemoryOAuth2AuthorizedClientService가 사용되며 DB를 사용하여 관리하고 싶다면 JdbcOAuth2AuthorizedClientService를 이용할 수 있다.
만약, DB를 이용하여 사용자 정보들을 관리하고 싶다면 아래의 스키마를 따라야한다.
CREATE TABLE oauth2_authorized_client (
client_registration_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
access_token_type varchar(100) NOT NULL,
access_token_value blob NOT NULL,
access_token_issued_at timestamp NOT NULL,
access_token_expires_at timestamp NOT NULL,
access_token_scopes varchar(1000) DEFAULT NULL,
refresh_token_value blob DEFAULT NULL,
refresh_token_issued_at timestamp DEFAULT NULL,
created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (client_registration_id, principal_name)
);
OAuth2AuthorizedClientManager:OAuth2AuthorizedClient의 전반적인 관리
OAuth2AuthorizedClientProvider: OAuth 2.0 Client의 인증/재인증 전략( ex: authorization grant type ) 을 구현
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
기본적으로 사용되는 OAuth2AuthorizedClientManager는 DefaultOAuth2AuthorizedClientManager 이며, OAuth2AuthorizedClientProvider와 함께 사용된다.
인가시도가 성공하게 되면 OAuth2AuthorizationSuccessHandler 에서 OAuth2AuthorizedClient를 저장하는 등의 작업을 진행한다.
만약, 재인가가 실패하게되면 RemoveAuthorizedClientOAuth2AuthorizationFailureHandler 에서 저장했던 사용자 정보를 삭제한다.
setAuthorizationSuccessHandler()와 setAuthorizationFailureHandler()를 사용하여 성공/실패 시의 동작을 커스터마이징한 핸들러를 등록할 수 있다.
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// contextAttributesMapper 등록
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
return authorizeRequest -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = new HashMap<>();
// OAuth2AuthorizeRequest로 부터 속성 값을 얻어내어 저장
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return contextAttributes;
};
}
이 외에도 DefaultOAuth2AuthorizedClientManager는 Function<OAuth2AuthorizeRequest, Map<String, Object>> 타입의 contextAttributesMapper 와도 함께 사용된다. OAuth2AuthorizeRequest에 매핑된 속성값들을 OAuth2AuthorizationContext와 연관지어 Map의 형태로 저장하는 역할을 한다.
DefaultOAuth2AuthorizedClientManager는HttpServletRequest와 함께 사용하도록 설계되었다.
HttpServletRequest외부에서의 작업은AuthorizedClientServiceOAuth2AuthorizedClientManager가 사용된다.
AuthorizedClientServiceOAuth2AuthorizedClientManager 는 사용자와의 상호작용이 없는 서비스 애플리케이션에서 주로 사용된다.
권한 부여 방식이 client_credentials로 설정된 애플리케이션은 서비스 애플리케이션으로 취급된다.

기본설정으로 사용되는 권한 부여 방식이다. 앞서 OAuth2 Login 에서 살펴본 로그인 흐름이 이 방식을 사용한다.
동작 시작은 OAuth2AuthorizationRequestRedirectFilter에서 OAuth2AuthorizationRequestResolver를 이용하여 이뤄진다.

resolver를 이용하여 OAuth2AuthorizationRequest를 얻어내고 redirect가 이뤄진다
기본적으로 사용되는 resolver는 DefaultOAuth2AuthorizationRequestResolver이며, registrationId를 추출해낸다.
추출된 registrationId를 이용하여 ClientRegistration을 얻어내고, 이를 바탕으로 redirect uri를 생성해낸다.
최종적으로는 OAuth2AuthorizationRequest를 생성해낸다.
이후에는 OAuth2AuthorizationRequestRedirectFilter에서 redirect를 실행한다.
OAuth2AuthorizationRequestRedirectFilter는AuthorizationCodeOAuth2AuthorizedClientProvider에서 발생한 예외를 처리하여 redirect를 발생시키기도 한다.
만약, 믿을 수 없는 환경에서 클라리언트 애플리케이션이 동작한다면 사용자 자격증명의 기밀성을 유지할 수 없기 때문에 자동적으로 Proof Key for Code Exchange가 사용된다
public final class DefaultAuthorizationCodeTokenResponseClient
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
// ...
private ResponseEntity<OAuth2AccessTokenResponse> getResponse(RequestEntity<?> request) {
try {
return this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
}
catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: "
+ ex.getMessage(),
null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}
}
// ...
사용자 권한 승인을 받은 이후, authorization code를 이용하여 access token을 발급받는다.
토큰 발급에 사용되는 클래스 타입은 OAuth2AccessTokenResponseClient이며, DefaultAuthorizationCodeTokenResponseClient 가 기본값으로 사용된다. 토큰 교환 과정에서 RestOperations를 사용한다. 이 클래스는 토큰 요청 전의 사전동작과 토큰 응답 후의 사후동작을 커스터마이징 할 수 있다.
앞선
OAuth2Service는 사용자 정보를 가져오는데 사용된다면OAuth2AccessTokenResponseClient는 accessToken을 가져오는데 사용된다
사전동작과 사후동작의 커스터마이징 예시는 공식 문서를 참고하자
처음 사용자가 OAuth2 로그인 인증을 하는 과정에서 Authorization Code를 이용하여 AccessToken을 획득하게 된다.
이 과정에서 RefreshToken이 함께 발급되기도 한다. ( 본인의 테스트 환경인 Google OAuth2에서는 access_type 파라미터를 추가해야 발급된다 )
AccessToken은 Resource Server로의 요청에 반드시 필요하다.
로그인 이후에도 Resource Server로 요청을 하려면 AccessToken이 유효해야한다
RefreshToken은 발급 받은 AccessToken이 만료되었을 때,
이를 갱신하기 위한 용도로 사용되며 기본으로 사용되는 클래스는 DefaultRefreshTokenTokenResponseClient다.
만약, AccessToken이 만료되었다면 DefaultRefreshTokenTokenResponseClient의 RestOperations을 이용하여 Token 엔드포인트로 재발급 요청을 보내게된다. 이때, 요청의 전후과정에서 이뤄지는 동작을 커스터마이징 하고싶으면 관련된 메서드를 이용할 수 있다.
요청 전에 이뤄지는 동작은 setRequestEntityConverter, 그 과정에서 파라미터를 추가/변경하고 싶다면 새로 등록하는 Converter의 setParametersConverter와 addParametersConverter를 이용하면 된다.
요청 후에 이뤄지는 동작은 DefaultRefreshTokenTokenResponseClient의 setRestOperations을 이용하면 된다.
좀 더 자세한 내용은 공식 문서를 참고하자

이 권한 부여 방식은 클라이언트 애플리케이션의 자격증명을 이용하여 Resource Server로 요청을 보낼 때 사용된다.
즉, Authorization Code 방식이 사용자의 로그인을 통해 애플리케이션이 각 사용자를 대신하여 Resource Server로 요청을 보낸다면 Client Credentials는 현재 애플리케이션이 자신의 자격증명을 이용하여 직접 Resource Server로 요청을 보내는 방식이다.
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id // 자격 증명
client-secret: okta-client-secret // 자격 증명
authorization-grant-type: client_credentials
scope: read, write
provider:
okta:
token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token
따라서 이 권한 부여 방식은 Client registration를 생성할 때 입력한 자격증명 정보를 이용한다.
Authorization Code 방식도 자격증명 정보를 필요로 한다.
인가 서버로 요청을 통해 생성되는 클래스 타입은 DefaultClientCredentialsTokenResponseClient 이며,
토큰 요청의 전후동작을 커스터마이징 할 수 있다.
요청 전에 이뤄지는 동작은 setRequestEntityConverter, 그 과정에서 파라미터를 추가/변경하고 싶다면 새로 등록하는 Converter의 setParametersConverter와 addParametersConverter를 이용하면 된다.
요청 후에 이뤄지는 동작은 DefaultRefreshTokenTokenResponseClient의 setRestOperations을 이용하면 된다.

사용자의 자격증명을 클라이언트 애플리케이션에 직접 제공하여, 이를 바탕으로 인증/인가가 진행되는 방식이다.
현재 테스트 중인 Google OAuth2 에서는 아예 지원하지 않는 권한 부여 방식이다.
인가 서버로 요청을 통해 생성되는 클래스 타입은 DefaultPasswordTokenResponseClient 이며,
토큰 요청의 전후동작을 커스터마이징 할 수 있다.
요청 전에 이뤄지는 동작은 setRequestEntityConverter, 그 과정에서 파라미터를 추가/변경하고 싶다면 새로 등록하는 Converter의 setParametersConverter와 addParametersConverter를 이용하면 된다.
요청 후에 이뤄지는 동작은 DefaultRefreshTokenTokenResponseClient의 setRestOperations을 이용하면 된다.

상세 설명은 JWT Authorization Grant (RFC 7523 2.1)을 참고하자
인가 서버로 요청을 통해 생성되는 클래스 타입은 DefaultJwtBearerTokenResponseClient 이며,
토큰 요청의 전후동작을 커스터마이징 할 수 있다.
요청 전에 이뤄지는 동작은 setRequestEntityConverter, 그 과정에서 파라미터를 추가/변경하고 싶다면 새로 등록하는 Converter의 setParametersConverter와 addParametersConverter를 이용하면 된다.
요청 후에 이뤄지는 동작은 DefaultRefreshTokenTokenResponseClient의 setRestOperations을 이용하면 된다.

상세 설명은 RFC 8693 OAuth 2.0 Token Exchange을 참고하자
인가 서버로 요청을 통해 생성되는 클래스 타입은 DefaultTokenExchangeTokenResponseClient 이며,
토큰 요청의 전후동작을 커스터마이징 할 수 있다.
요청 전에 이뤄지는 동작은 setRequestEntityConverter, 그 과정에서 파라미터를 추가/변경하고 싶다면 새로 등록하는 Converter의 setParametersConverter와 addParametersConverter를 이용하면 된다.
요청 후에 이뤄지는 동작은 DefaultRefreshTokenTokenResponseClient의 setRestOperations을 이용하면 된다.
public final class ClientRegistration implements Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private String registrationId;
private String clientId;
private String clientSecret;
private ClientAuthenticationMethod clientAuthenticationMethod; // << 클라이언트 인증 방법
private AuthorizationGrantType authorizationGrantType;
private String redirectUri;
private Set<String> scopes = Collections.emptySet();
private ProviderDetails providerDetails = new ProviderDetails();
private String clientName;
//...
properties 또는 yml 파일을 통해 입력한 설정값으로 생성되는 ClientRegistration 에는 인가서버로의 요청을 위해 클라이언트 애플리케이션을 인증하기위한 방법이 표시된 필드가 있다.

해당 클래스에는 여러가지 인증방식을 지원한다. ( none은 별도의 인증방식을 이용한다는 의미다 )
설정파일에 작성한 Client Credentials 정보로 인가서버에 클라이언트 인증을 하는 방식이다.
HTTP Basic을 활용한 클라이언트 인증 방식( Authorization 헤더를 통해 전송 )이며, 기본으로 사용되는 설정이다.
기본 구현은 DefaultOAuth2TokenRequestHeadersConverter에 의해 제공된다.
ClientRegistration에 저장된 client-id와 client-secret을 통해 인가서버로 인증요청을 한다.

Converter를 살펴보면 기본설정으로 Client Credentials를 URL 인코딩하는 것을 확인할 수 있다.
이는 아래와 같은 설정을 통해 비활성화 할 수 있다.
DefaultOAuth2TokenRequestHeadersConverter<OAuth2AuthorizationCodeGrantRequest> headersConverter =
new DefaultOAuth2TokenRequestHeadersConverter<>();
headersConverter.setEncodeClientCredentials(false);
OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter =
new OAuth2AuthorizationCodeGrantRequestEntityConverter();
requestEntityConverter.setHeadersConverter(headersConverter);
DefaultAuthorizationCodeTokenResponseClient tokenResponseClient =
new DefaultAuthorizationCodeTokenResponseClient();
tokenResponseClient.setRequestEntityConverter(requestEntityConverter);
http
.oauth2Login(oauth2Login -> oauth2Login
.tokenEndpoint(tokenEndpointConfig -> tokenEndpointConfig
.accessTokenResponseClient(tokenResponseClient)
)
)
spring:
security:
oauth2:
client:
registration:
okta:
client-id: client-id
client-secret: client-secret
client-authentication-method: client_secret_post
authorization-grant-type: authorization_code
...
활성화를 위한 설정은 위와 같으며, 요청 본문에 Client Credentials을 담아서 보내는 방식이다.
JWT를 이용하여 인가서버에 클라이언트 인증을 하는 방식이다.
기본 구현은 NimbusJwtClientAuthenticationParametersConverter에 의해 제공된다.
client_assertion 파라미터로 작성된 JWT를 보내 인증을 진행한다
즉, 클라이언트 애플리케이션에서 생성한 JWT를 인가서버로 제출하여 인증을 진행하는 방식이다.
생성되는 JWT는 반드시 iss, sub, aud, jti, iat, exp claims를 보유하고 있어야하며, 추가적인 헤더작성은 아래코드와 같이 가능하다.
Function<ClientRegistration, JWK> jwkResolver = ...
NimbusJwtClientAuthenticationParametersConverter<OAuth2ClientCredentialsGrantRequest> converter =
new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver);
converter.setJwtClientAssertionCustomizer((context) -> {
context.getHeaders().header("custom-header", "header-value");
context.getClaims().claim("custom-claim", "claim-value");
});
JWS 서명에 사용되는 java.security.PrivateKey나 javax.crypto.SecretKey는 com.nimbusds.jose.jwk.JWK resolver에 의해 제공된다.
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-authentication-method: private_key_jwt
authorization-grant-type: authorization_code
...
활성화를 위한 설정은 위와 같다.
Function<ClientRegistration, JWK> jwkResolver = (clientRegistration) -> {
if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
// Assuming RSA key type
RSAPublicKey publicKey = ...
RSAPrivateKey privateKey = ...
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
return null;
};
OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter =
new OAuth2AuthorizationCodeGrantRequestEntityConverter();
requestEntityConverter.addParametersConverter(
new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver));
DefaultAuthorizationCodeTokenResponseClient tokenResponseClient =
new DefaultAuthorizationCodeTokenResponseClient();
tokenResponseClient.setRequestEntityConverter(requestEntityConverter);
위 코드처럼 JWT을 해석하기 위한 resolver를 작성하고 등록할 수 있다.
클라이언트가 개인키로 서명하여 생성된 JWT를 인가서버로 제출하면 인가서버에서 공개키로 검증하는 인증작업이 이뤄진다.
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
client-authentication-method: client_secret_jwt
authorization-grant-type: client_credentials
...
활성화를 위한 설정은 위와 같다.
Function<ClientRegistration, JWK> jwkResolver = (clientRegistration) -> {
if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
SecretKeySpec secretKey = new SecretKeySpec(
clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8),
"HmacSHA256");
return new OctetSequenceKey.Builder(secretKey)
.keyID(UUID.randomUUID().toString())
.build();
}
return null;
};
OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter =
new OAuth2ClientCredentialsGrantRequestEntityConverter();
requestEntityConverter.addParametersConverter(
new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver));
DefaultClientCredentialsTokenResponseClient tokenResponseClient =
new DefaultClientCredentialsTokenResponseClient();
tokenResponseClient.setRequestEntityConverter(requestEntityConverter);
client-secret을 이용하여 secretKey를 생성하고, 이를 이용하여 JWS 서명을 진행한다.
이를 통해 생성된 JWT를 인가서버로 보내 인증을 진행하는 방식이다.
즉, client-secret을 대칭키로 이용하는 방식이다.
OAuth2 로그인이 완료된 사용자는 OAuth2LoginAuthenticationFilter에 의해 OAuth2AuthorizedClientRepository에 OAuth2AuthorizedClient 타입으로 저장되어 관리된다.
이렇게 저장된 OAuth2AuthorizedClient는 컨트롤러에서 @RegisteredOAuth2AuthorizedClient를 이용하여 접근할 수 있다.
@Controller
public class OAuth2ClientController {
@GetMapping("/")
public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) {
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
...
return "index";
}
}
이 애노테이션은 OAuth2AuthorizedClientArgumentResolver를 통해 OAuth2AuthorizedClientManager를 직접적으로 사용하여 인가된 사용자 정보를 얻어온다.
WebClient는 Spring Framework 공식문서에 소개되어 있으며, 간단하게 말하면 애플리케이션에서 HTTP 요청을 만들어내는 도구다.
OAuth 2.0 Client는 ExchangeFilterFunction를 이용하여 WebClient를 지원한다.
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
ServletOAuth2AuthorizedClientExchangeFilterFunction는 OAuth2AuthorizedClient를 사용하여 보호된 리소스로 요청하는 매커니즘을 지원한다. 그 과정에서 OAuth2AuthorizedClientManager가 사용된다.
WebClient를 사용한 요청에서 인가된 사용자정보를 제공하는 방식과 기본값 설정은 공식 문서를 참고하자