본문 바로가기
개발

[OAuth2] X(구 트위터) 로그인 API 사용기

by hwan-da 2024. 11. 17.

X 소셜 로그인을 뚜복 프로젝트에 적용하면서 시행 착오를 겪었다.

일반 소셜 로그인과 다른 부분이 한 가지 있었고, 이를 정리한 블로그가 많이 없어 정리를 해두려고 한다.

X는 소셜 로그인에 PKCE(Proof Key for Code Exchange)를 사용한다.

💡 PKCE
OAuth2.0의 보안을 강화하기 위한 확장 인증 방식
- 클라이언트가 랜덤한 문자열(code verifier) 생성
- 이를 해시하여 code challenge 생성
- 인증 요청 시 code challenge를 함께 전송
- 인증 코드를 받은 후, 엑세스 토큰 요청 시 원래의 code verifier 전송
- 서버는 전송받은 code verifier를 해시하여 처음 받았던 code challenge와 비교 검증

 

나는 이 PKCE를 Spring Security를 사용한 소셜 로그인에 적용하기 위해 Resolver를 구현하여 사용하였다.

@Bean
    public OAuth2AuthorizationRequestResolver customAuthorizationRequestResolver(
        ClientRegistrationRepository clientRegistrationRepository) {

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

        resolver.setAuthorizationRequestCustomizer(
            builder -> {
                Map<String, Object> attributes = builder.build().getAttributes();
                String registrationId = (String) attributes.get(OAuth2ParameterNames.REGISTRATION_ID);


        	// x만 PKCE를 사용하므로 x로 로그인할 때만 적용
                // 1. Twitter인지 확인
		if ("x".equals(registrationId)) {
			// 2. code_verifier 생성 (클라이언트가 보관)
			String codeVerifier = generateCodeVerifier();
								    
			// 3. code_verifier로 code_challenge 생성
			String codeChallenge = generateCodeChallenge(codeVerifier);
								    
			// 4. code_challenge를 authorization request에 추가
			builder.additionalParameters(params -> {
				params.put("code_challenge", codeChallenge);
				params.put("code_challenge_method", "S256");
			});
								    
			// 5. code_verifier를 나중에 사용하기 위해 저장
			builder.attributes(attrs -> 
				attrs.put("code_verifier", codeVerifier)
			);
		}
    	});

        return resolver;
    }
    
    private String generateCodeVerifier() {
        SecureRandom secureRandom = new SecureRandom();
        // 96byte를 권장
        byte[] codeVerifier = new byte[96];
        secureRandom.nextBytes(codeVerifier);
        return Base64.getUrlEncoder().withoutPadding()
            .encodeToString(codeVerifier);
    }

    private String generateCodeChallenge(String codeVerifier) {
        try {
            byte[] bytes = codeVerifier.getBytes(StandardCharsets.US_ASCII);
            // SHA-256으로 해싱
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(bytes);
            return Base64.getUrlEncoder().withoutPadding()
                .encodeToString(hash);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Failed to generate code challenge", e);
        }
    }

 

인가 요청 생성 단계에서 이 Resolver가 작동하여

  • 사용자가 /api/oauth2/authorization/x 엔드포인트로 X 로그인 시도
  • Spring Security OAuth2 로직이 동작하면서 Authorization Request를 생성하기 전에 resolver 실행됨
  • Resolver 내부 동작
  • X로 리다이렉트되면서 code-chellenge와 code_chellenge-method 전송
  • 사용자가 X에서 인증을 완료하고 서비스로 돌아올 때, X는 authorization code 전달
  • 이 authorization code와 앞서 저장해둔 code_verifer를 사용하여 엑세스 토큰 요청

이 단계를 거친다.

 

5번에 대한 추가 적인 설명을 하자면

builder.attributes(attrs -> 
	attrs.put("code_verifier", codeVerifier)
);
  • Authorization Request 속성에 저장된 code_verifier는 Spring Security의 OAuth2AuthorizationRequest에 저장됨
  • Spring Security는 이 Authorization Request를 세션에 저장하고 있다가 콜백으로 돌아왔을 때 다시 불러옴
  • OAuth2LoginAuthentificationFilter가 Access Token을 요청할 때, 저장된 것을 Spring Security가 자동으로 처리함

이렇게 두지 않으면 codeVerifier를 따로 저장해둘 수가 없었다.

구현 과정에서 발생한 예외

처음 코드에서는 Security Config에서 Authentification을 명시적으로 빈으로 등록했다.

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
    return configuration.getAuthenticationManager();
}

 

다른 소셜 로그인에서는 오류가 나지 않았는데 X 소셜 로그인을 할 때에만 오류가 발생했다.

Spring AOP에서 순환 참조가 발생해서 StackOverflow가 난 것이 원인이었다.

  1. authenticate 메소드 호출
  2. AOP 프록시가 이를 가로챔
  3. 프록시가 보안 체크를 위해 다시 AuthenticationManager를 사용
  4. 이것이 다시 authenticate 메소드 호출을 유발
  5. 2-4 과정이 무한 반복

해결방법은 해당 코드를 제거하는 단순한 방법이었다.

명시하지 않아도 Spring Security가 내부적으로 AuthenticationManger를 관리하기 때문에 프록시 없이 구현체를 사용하므로 순환 참조가 발생하지 않은 것이다.

왜 X만 에러가 나는가?

  • 기술적으로는 모든 소셜 로그인이 동일한 OAuth2 인증 흐름을 따르고 동일한 AuthentificationManager를 사용하기 때문에 동일한 문제가 다른 소셜 로그인에서 발생해야 할 것 같지만, X에서만 발생
  • 이는.. 해결되지 않았다.. 그냥 X가 문제다..
  • X는 소셜 로그인 중에 날 가장 힘들게 했다..

다른 사람들은 나처럼 시행착오를 겪지 않게 하기 위해 이 글을 작성한다.

X는 redirect Uri부터 X로 바꿔라.. twitter라서 헷갈렸다..

'개발' 카테고리의 다른 글

Spring Batch란?  (0) 2024.11.24
Docker 공부하기 (기초)  (1) 2024.09.01
JWT 심화  (1) 2024.08.11
OAuth2.0 + SpringSecurity  (0) 2024.08.04
스프링 시큐리티(Spring Security) + JWT  (0) 2024.07.21