Spring/Spring Boot

[Spring Boot] Google OAuth 2.0 API 요청 시 invalid grant와 malformed token 응답 해결하기

jundyu 2024. 10. 26. 01:37
Spring Boot

 

들어가며

반려동물의 쇼핑몰 제작 프로젝트에서 구글 소셜 로그인을 맡았습니다. Google OAuth 2.0로 로그인해서 인가 코드를 받아 사용자 인증을 하고, JWT를 생성해서 회원을 관리하려 했습니다. Spring Boot에서 api 구현을 끝내고 포스트맨으로 테스트를 하려니까 자꾸만 인가 코드가 유효하지 않다고 나오는 invalid grant와 malformed token 에러 메세지.. 인가 코드로 Google OAuth 2.0 API로 액세스 토큰을 요청해야 하는데 이 에러 해결하느라 하루를 다 써버렸습니다.
 
이번 글은 invalid grant와 malformed token에 대한 트러블 슈팅입니다. 저처럼 인가 코드가 유효하지 않다는 응답을 받는 분들에게 이 글이 많은 도움이 되었으면 좋겠습니다. 우선 트러블 슈팅 이전에 invalid grant와 malformed token이 어떤 에러인지 간단하게 확인하겠습니다.
 


 

invalid grant와 malformed token

유효하지 않은 인가 코드 메세지

invalid grant와 malformed token 모두 Google OAuth 2.0에 잘못된 인가 코드를 보낸 경우에 생기는 에러 메세지입니다. RestTemplate이나 WebClient로 Http 요청을 보낼 때 클라이언트 측에서 생기는 HttpClientErrorException 예외 입니다. 인가 코드가 유효하지 않은 경우엔 invalid token을 반환하고 형식이 맞지 않는 경우엔 malformed token을 반환합니다. (위 사진에서 두 에러 메세지가 보이지 않는 이유는 캡쳐하는 시점에 프로젝트에 글로벌 예외 처리를 해놨기 때문입니다.)
 

invalid grant

우선 invalid grant가 발생할 수 있는 경우는 다음과 같습니다.

  • 만료된 인가 코드 사용
  • 구글의 인가 코드는 발급 후 대략 5분이 지나면 유효 기간이 끝납니다. 따라서 구글로 로그인하고 인가 코드가 만료되기 전에 구글 서버에게 요청을 끝내야합니다.
  • 이미 사용된 인가 코드 사용
  • 구글의 인가 코드는 한 번 밖에 사용하지 못합니다. 인가 코드를 통해 구글에게 토큰 요청을 하는 순간 성공 여부에 상관없이 해당 인가 코드는 사용한 인가 코드가 됩니다.
  • 유효하지 않은 인가 코드
  • 그냥 인가 코드 자체가 잘못된 경우에도 invalid grant 에러가 발생할 수 있습니다. 이 경우는 인가 코드의 형식이 잘못된 경우입니다.

malformed token

다음으로 malformed token입니다. malformed은 "흉한", "기형의"라는 의미이고 malformed token이 발생하는 경우는 다음과 같습니다.

  • Google OAuth 2.0 API 요청 시 인가 코드 누락
  • 말 그대로 https://oauth2.googleapis.com/token으로 API 요청할 때 요청 파라미터에 인가 코드를 누락한 경우입니다.
  • 잘못된 인가 코드의 형식
  • 인가 코드는 URL이 인코딩된 상태로 전달되기 때문에 인가 코드 내에 예상치 못한 특수 문자가 포함될 수 있습니다.

 

Trouble Shooting

[TS #1] 인가 코드를 RequestParam으로 받기

당연하게도 RequestBody에서 발생하던 에러가 RequestParam으로 바꿨다고 고쳐질 리는 없었습니다.

[TS #2] Postman에서 인가 코드를 텍스트로 전달하기

@PostMapping("/auth/google")
public ResponseEntity<?> googleCallback(
    @RequestBody String code, HttpServletResponse httpServletResponse) {
	// .. //
}

생각해보니 위의 코드처럼 AuthController에서 인가 코드를 String 값으로 받고 있는데 PostMan에서 JSON 형식으로 POST 요청하고 있었습니다. 그래서 바로 PostMan에서 TEXT로 바꿔 다시 POST 요청을 해봤는데 결과는 같았습니다.

[TS #3] 인가 코드를 Map 객체에 저장하기

@PostMapping("/google")
public ResponseEntity<?> googleCallback(
    @RequestBody Map<String, String> authorizationCode, HttpServletResponse httpServletResponse) {
	String code = authorizationCode.get("code");
	// .. //
}

이번엔 반대로 인가 코드의 요청 형식을 TEXT로 바꾸는 게 아니라 서버에서 JSON으로 받되, 그걸 String이 아닌 Map 객체에 저장하는 걸로 시도해봤습니다. 역시나 invalid grant와 malformed token 에러가 떴습니다.
 
여기서부터 저도 의문이 생겼습니다. code를 System.out.println으로 출력해보면 분명 제대로 값이 나오는데 왜 인가 코드가 유효하지 않다는 걸까.. 이때부터 저는 인가 코드 값 자체에 의문을 품고 열심히 구글링을 했습니다. 그 결과 인가 코드의 앞에 위치한 %2F는 슬래시(/)가 인코딩된 값이라는 것을 알게 되었습니다.

Authorization Code 예시

[TS #4] 인가 코드를 URLDecoder로 디코딩

@PostMapping("/google")
public ResponseEntity<?> googleCallback(
    @RequestBody AuthorizationCode authorizationCode, HttpServletResponse httpServletResponse) {
   	String code = authorizationCode.getCode();
	String decodedCode = URLDecoder.decode(code, StandardCharsets.UTF_8);
	// .. //
}

클라이언트에서 JSON으로 인가 코드를 전달해주면 백엔드에선 인가 코드를 URLDecoder로 디코딩 해줬습니다. 그랬더니 바로 나타나는 액세스 토큰..(API 호출하면 액세스 토큰까지 발급하게 구현)

디코딩한 인가 코드로 요청해서 성공한 모습

 
 

결론

위에서 malformed token 메세지가 생기는 이유 중 하나가 잘못된 인가 코드 형식이라고 했습니다. 클라이언트에선 URL을 그대로 가져와서 인가 코드만 뽑아내서 서버로 전달해주기 때문에 인가 코드 또한 인코딩 되어있는 상태입니다. 따라서 인코딩된 값으로 Google OAuth 2.0 API를 호출했더니 자꾸 malformed token 에러 메세지가 생긴 것입니다. 또한, 이미 한 번 사용한 인가 코드는 다시 사용하지 못하는데 까먹고 계속 같은 코드로 사용했던 저의 실수도 있었습니다.
 

.
.
.

 

마치며

아무래도 처음으로 백엔드에서 로그인을 구현하다보니 시행착오가 많았던 것 같습니다. 이번 에러를 해결하면서 OAuth2와 JWT를 이용한 회원 관리 방식을 더욱(더더더욱) 깊이 이해할 수 있었습니다. 추가로 에러 해결해보겠다고 포스트맨에서 써보지 않았던 기능도 이것저것 써보다가 포스트맨도 너무 익숙해질 수 있었습니다!
 
에러 발생할때마다 캡쳐를 해두지 못한게 아쉽습니다. 다음 트러블 슈팅에선 더욱 친절한 글로 돌아오겠습니다. 앞으로 상품 조회, 결제 등등 수많은 과제가 남았지만 소셜 로그인 구현에 성공하면서 더욱 재밌어졌습니다. 남은 프로젝트 2주 화이팅!
 
피드백이나 질문 환영합니다!