본문 바로가기

Programming/구현

[Spring Security] Access Token Request, Response 커스터마이징 - Tistory OAuth2 로그인 구현하기(1)

작업 환경

  • spring-boot : 2.4.5
  • spring-security : 5.4.6
  • java : 11

 

Tistory API를 사용해 간단한 애플리케이션을 만드려고 했는데 예상치 못한 어려움에 부딪혔다.....

일반적으로 OAuth2 인증 방식은 Authentication Code를 얻은 다음 POST 요청에 담아 다시 전달하면 Access Token이 응답으로 오는데 Tistory는 이를 GET 요청으로만 허용해 응답이 오지 않았다.

 

 

때문에 Token요청을 보내는 Request를 커스텀하는 작업이 필요해졌다.

 

1️⃣ Access Token Request Customizing

DefaultAuthorizationCodeTokenResponseClient.getTokenResponse()

Authentication Code 응답을 받은 Spring Security는 이 Code를 담아 다시 Request를 전달하기 위해 DefaultAuthorizationCodeTokenResponseClient.getTokenResponse()를 호출해 요청을 보내고 응답을 받아 가공한 뒤 AccessToken과 RefreshToken이 담긴 OAuth2AccessTokenResponse를 반환한다.

 

 

이 때, 전달받은 OAuth2AuthorizationCodeGrantRequestResponseEntity로 바꾸는 Converter가 작동하는데 이를 바꾸어주면 요청을 커스텀할 수 있다.

 

 

제일 먼저 SecurityConfig에 설정을 해야한다.

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.oauth2Login()
                .tokenEndpoint(token -> token
                        .accessTokenResponseClient(this.accessTokenResponseClient())
                )
    }

    @Bean
    public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
        var accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
        accessTokenResponseClient.setRequestEntityConverter(new CustomRequestEntityConverter());

        var tokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
        tokenResponseHttpMessageConverter.setTokenResponseConverter(new CustomTokenResponseConverter());

        return accessTokenResponseClient;
    }
}

tokenEndPoint에서 토큰을 요청하는데 사용되는 ResponseClient를 설정해야 한다.

기본적으로 사용하는 DefaultAuthorizationCodeTokenResponseClient를 생성하고, 여기서 RequestEntityConvter를 우리가 직접 커스텀한 Converter를 등록해주면 된다.

 

 

Spring Security가 기본적으로 사용하는 Converter는 org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter 이므로, 이를 참고하여 작성하였다.

 

 

타입을 명시할 수 있지만 Java 10부터 제공되는 var타입을 사용했다...(클래스명이 너무 길어....) 만약 그 이전 버전을 사용하고 있다면 직접 타입을 작성하면 된다.

 

 

public class CustomRequestEntityConverter implements Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {

    private OAuth2AuthorizationCodeGrantRequestEntityConverter defaultConverter;

    public CustomRequestEntityConverter() {
        defaultConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
    }

    @Override
    public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest req) {
        ClientRegistration clientRegistration = req.getClientRegistration();
        HttpHeaders headers = getTokenRequestHeaders(clientRegistration);

        URI uri = buildUriWithQueries(req);
        return new RequestEntity<>(headers, HttpMethod.GET, uri);
    }
    
    private URI buildUriWithQueries(OAuth2AuthorizationCodeGrantRequest req) {
        var clientRegistration = req.getClientRegistration();
        var authorizationExchange = req.getAuthorizationExchange();
        String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
        String state = authorizationExchange.getAuthorizationRequest().getState();

        return UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri())
                      .queryParam(OAuth2ParameterNames.GRANT_TYPE, req.getGrantType().getValue())
                      .queryParam(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode())
                      .queryParam(OAuth2ParameterNames.REDIRECT_URI, redirectUri)
                      .queryParam(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId())
                      .queryParam(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret())
                      .queryParam(OAuth2ParameterNames.STATE, state)
                      .build().toUri();
    }
    
    ...

나는 POST 요청에서 GET요청으로 변경이 필요했기 때문에 이런느낌으로 작성했다.

 

 

2️⃣ Access Token Response Customizing

처음엔 Request만 변경하면 될 줄 알았지만, 여전히 오류가 발생했다. 200응답이 발생했음에도 정상적으로 토큰이 들어오지 않았다.

OAuth2의 응답은 대체로 Token_Type과 만료시간들을 함께 담아 보내주지만...

{
    "token_type":"bearer",
    "access_token":"{ACCESS_TOKEN}",
    "expires_in":43199,
    "refresh_token":"{REFRESH_TOKEN}",
    "refresh_token_expires_in":25184000,
    "scope":"account_email profile"
}

 

 

Tistory는 딱 Access Token만 전달해준다

{
    "access_token":"{ACCESS_TOKEN}",
}

Spring Security에서 Response를 컨버팅 해야하는 OAuth2AccessTokenResponse 클래스의 Token Type은 null이 될 수 없고, 이 때문에 에러가 발생하는 것이였다. 때문에, Response도 커스텀을 해 강제로 타입을 지정해야 했다.

 

 

이전에 설정했던 SecurityConfig.accessTokenResponseClient()에서 이번엔 RestTemplate에서 요청을 받아 가공하는 Converter를 변경해주어야 한다.

@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
    ...

    var tokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
    tokenResponseHttpMessageConverter.setTokenResponseConverter(new CustomTokenResponseConverter());

    RestTemplate restTemplate = new RestTemplate(Arrays.asList(
        new FormHttpMessageConverter(), tokenResponseHttpMessageConverter));
    restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

    accessTokenResponseClient.setRestOperations(restTemplate);
    return accessTokenResponseClient;
}

FormHttpMessageConverter에서 먼저 응답을 받아 Map<String, String> 형태로 변환해 주는데, 다시 이를 OAuth2AccessTokenResponse로 반환하는 Converter를 작성해 주면 된다.

 

 

Spring Security가 기존에 사용하던 Converter는 org.springframework.security.oauth2.client.endpoint.MapOAuth2AccessTokenResponseConverter를 참고해 작성하였다.

public class CustomTokenResponseConverter implements Converter<Map<String, String>, OAuth2AccessTokenResponse> {

    ...

    @Override
    public OAuth2AccessTokenResponse convert(Map<String, String> tokenResponseParameters) {
       ...

        return OAuth2AccessTokenResponse
                .withToken(accessToken)
                .tokenType(TokenType.BEARER)
                .scopes(scopes)
                .additionalParameters(additionalParameters)
                .build();
    }
    ...

아직 고민 중이기는한데 OAuth2AccessTokenResponse의 tokenType에 org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType만 지정해야 한다... 하지만 안에 있는 유일한 타입이 Bearer뿐이고, final 클래스라 상속도 불가능해서 일단 그냥 Bearer 타입을 지정해버렸다. 나중에 아이디어가 생각나면 바꿀 수도....

 

 

 


📚 Reference

Spring Security Reference Docs

https://www.baeldung.com/spring-security-custom-oauth-requests
tistory.github.io/document-tistory-apis/auth/authorization_code.html