카카오소셜로그인 KOE320에러가 발생합니다

문의 시, 사용하시는 개발환경과 디벨로퍼스 앱ID를 알려주세요.


개발환경 : MAC OS
앱ID : 1211731

배포를 아직 안한상태입니다.

https://kauth.kakao.com/oauth/authorize?client_id=.a...&redirect_uri=http://localhost:8080/oauth/callback/kakao&response_type=code

라고 브라우저에서 url을 직접 입력하면 http://localhost:8080/oauth/callback/kakao?code=AE…라는 url로 리다이렉트 되면서 화면에 token값이 잘 뜨는데
postman으로 OAuth2 2.0 토큰을 발급받아 Access Token을http://localhost:8080/oauth/callback/kakao?code=에 입력할 시 400오류가 발생합니다. 코드를 발급받아서 바로 사용하는데 왜 오류가 발생하는건지 잘 모르겠습니다ㅠㅠ

참고 정보

KOE320에러는 동일한 인가 코드를 두 번 이상 사용하거나, 이미 만료된 인가 코드를 사용한 경우, 혹은 인가 코드를 찾을 수 없는 경우 발생합니다.

액세스 토큰 관련 에러


FAQ. KOE320 (An authorization code must be supplied, authorization code not found) 에러가 발생할 때

[@tim.l @woody.ho]

안녕하세요. 처음 구현하는 소셜로그인이라 지식이 많이 부족한지 동일한 인가 코드를 두 번 이상 사용하는 부분을 도저히 찾을수가없습니다…ㅠㅠ 프론트와 협업을 하여 restAPI로 소셜 로그인을 진행하는것인데 프론트에서
https://kauth.kakao.com/oauth/authorize?client_id=...&redirect_uri=http://localhost:8080/oauth/callback/kakao&response_type=code
를 통해서 인가코드를 받아 백엔드에 파라미터로 code를 넘겨주는 것 아닌가요?

그러면 백엔드는 http://localhost:8080/oauth/callback/kakao로 들어오는 요청을 프론트로부터 인가코드를 넘겨받아서 jwt를 생성 후 프론트로 넘겨주는걸로 생각했는데 어느 부분에서 인가 코드가 두 번 이상 사용되었다는건지 잘 모르겠습니다…ㅠㅠ 제가 알고있는 지식이 잘못된걸까요 ? 혹시 몰라서 아래 코드를 같이 첨부하였습니다. 한 번만 봐주시면 감사하겠습니다…ㅠㅠ

@Service
@RequiredArgsConstructor
public class OauthServiceImpl implements OauthService {

    private final JwtProvider jwtProvider;
    private final ParentRepository parentRepository;
    private final RestTemplate restTemplate = new RestTemplate();  // RestTemplate 사용

    @Value("${spring.security.oauth2.client.provider.kakao.token-uri}")
    private String kakaoTokenUri;
    @Value("${spring.security.oauth2.client.provider.kakao.user-info-uri}")
    String userInfoReqUri;
    @Value("${spring.security.oauth2.client.registration.kakao.client-id}")
    String kakaoClientId;
    @Value("${spring.security.oauth2.client.registration.kakao.client-secret}")
    private String kakaoClientSecret;
    @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
    private String kakaoRedirectUri;


    @Override
    public String getKakaoAccessToken(String code) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "authorization_code");
        body.add("client_id", kakaoClientId);
        body.add("client_secret", kakaoClientSecret);
        body.add("redirect_uri", kakaoRedirectUri);
        body.add("code", code);


        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
        ResponseEntity<String> response = restTemplate.postForEntity(kakaoTokenUri, request, String.class);

        if (response.getStatusCode() == HttpStatus.OK) {
            JsonParser parser = new JsonParser();
            JsonElement element = parser.parse(response.getBody());

            return element.getAsJsonObject().get("access_token").getAsString();
        } else {
            throw new RuntimeException("카카오 액세스 토큰 요청 실패: " + response.getStatusCode());
        }
    }
    @Override
    public HashMap<String, Object> getUserKakaoInfo(String accessToken) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + accessToken);
        headers.setContentType(MediaType.APPLICATION_JSON);

        HttpEntity<String> entity = new HttpEntity<>(headers);
        ResponseEntity<String> response = restTemplate.exchange(userInfoReqUri, HttpMethod.POST, entity, String.class);

        if (response.getStatusCode() == HttpStatus.OK) {
            JsonParser parser = new JsonParser();
            JsonElement element = parser.parse(response.getBody());

            HashMap<String, Object> userInfo = new HashMap<>();
            userInfo.put("id", element.getAsJsonObject().get("id").getAsString());

            JsonObject properties = element.getAsJsonObject().get("properties").getAsJsonObject();
            userInfo.put("nickname", properties.get("nickname").getAsString());

            if (element.getAsJsonObject().get("kakao_account").getAsJsonObject().has("email")) {
                userInfo.put("email", element.getAsJsonObject().get("kakao_account").getAsJsonObject().get("email").getAsString());
            }

            return userInfo;
        } else {
            throw new RuntimeException("카카오 사용자 정보 요청 실패: " + response.getStatusCode());
        }
    }


    @Override
    public ParentLoginResponseDto kakaoLogin(String accessToken, HttpServletResponse response) {
        ParentSignUpRequest requestDto = getUserKakaoSignupRequestDto(getUserKakaoInfo(accessToken));
        ParentResponse parentResponse = findByUserKakaoIdentifier(requestDto.id());

        if (parentResponse == null) {
            signUp(requestDto);
            parentResponse = findByUserKakaoIdentifier(requestDto.id());
        }

        String token = jwtProvider.createJwt(parentResponse.email(), parentResponse.roles(), "SOCIAL_KAKAO", null);

        response.addHeader("Authorization", "Bearer " + token);

        Cookie cookie = new Cookie("Authorization", token);
        cookie.setPath("/");
        response.addCookie(cookie);


        return new ParentLoginResponseDto(token, parentResponse.email());
    }

    @Override
    public ParentResponse findByUserKakaoIdentifier(String kakaoIdentifier) {
        List<Parent> parents = parentRepository.findParentByProviderId(kakaoIdentifier).orElse(List.of());

        if (parents.isEmpty()) {
            return null;
        }
        return new ParentResponse(parents.get(0));
    }

    @Override
    @Transactional
    public Long signUp(ParentSignUpRequest requestDto) {
        try {
            System.out.println("회원가입 요청 데이터: " + requestDto);
            return parentRepository.save(requestDto.toEntity(requestDto.email(), requestDto.nickname(), requestDto.id())).getId();
        } catch (Exception e) {
            e.printStackTrace();
            throw new BusinessException(ErrorCode.FAILED_TO_SAVE_UESR);
        }
    }

    private ParentSignUpRequest getUserKakaoSignupRequestDto(HashMap<String, Object> userInfo) {
        return new ParentSignUpRequest(
                (String) userInfo.get("email"),
                (String) userInfo.get("nickname"),
                (String) userInfo.get("id")
        );
    }
}

@RestController
@RequestMapping("/oauth")
@RequiredArgsConstructor
public class OAuthController {

    private final OauthService oauthService;

    @Operation(summary = "카카오 소셜 로그인 콜백 컨트롤러")
    @GetMapping("/callback/kakao")
    public ResponseEntity<?> getKaKaoAuthorizeCode(
            @RequestParam(value = "code") String code,
            HttpServletResponse response) {
        System.out.println("Received Authorization Code: " + code); // ✅ code를 콘솔에 출력

        if (code == null) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("code 파라미터가 없습니다.");
        }
        try {
            String accessToken = oauthService.getKakaoAccessToken(code);
            System.out.println("✅ Access Token: " + accessToken); // ✅ 확인용 로그 추가
            ParentLoginResponseDto loginResponse = oauthService.kakaoLogin(accessToken, response);
            return ResponseEntity.ok(loginResponse);
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("카카오 로그인 처리 중 오류 발생");
        }
    }
}

redirect_uri 로 전달되어야 하는 값은 인가코드 입니다.

인가코드는 접근토큰을 받기 위한 임시 토큰으로 카카오 로그인 과정중 최종적으로 카카오가 전달하는 값입니다.

말씀주신바와 같이 postman에서 발급 받은 접근토큰을 전달하셔서는 안됩니다.

네, 맞습니다.

해당 블로그의 테스트 방식은 사용 불가 합니다.
postman에서 접근토큰 발급까지 자동으로 이루어지기에 인가코드가 즉시 소비 됩니다.

감사합니다!

그러면 포스트맨에서 OAuth2.0으로 토큰을 발급받은 뒤, use Token을 누르면
https://kapi.kakao.com/v2/user/me 요청을 보낼 시, 유저의 정보가 뜨는건 가능한건가요?

액세스 토큰 발급은 되는데 유저의 정보를 조회하려고하면 400 오류가 발생합니다.

  1. use token 하시고
  2. user/me 요청의 Authorization 탭의 Type이 inherit auth … 로 설정하시면됩니다.


아무리 찾아봐도 inherit auth…가 없는데 따로 설정해야하는게있을까요…??

컬렉션 쪽에 구성하신게 아니라 개별 요청에 설정하셨군요
OAuth 2.0 하시고 use token 하신뒤
Headers 에 Authorization 헤더 자동 적용되어 있는지 보시면 될것 같습니다.

답변 감사합니다!
도움주신대로 포스트맨으로 아래 사진과 같이 use token하니 헤더에 엑세스 토큰이 들어갔고, https://kapi.kakao.com/v2/user/me를 입력하니 마지막 사진처럼 로그인 정보를 확인할 수 있었습니다. 정말감사합니다
따로 또 제가 놓친 부분이 있는지 궁금합니다. 카카오 소셜로그인이 성공되었다고 할 수 있는걸까요?

네, 카카오 로그인 및 사용자 정보조회 까지 진행된 경우
온전히 카카오 로그인 되었다고 볼 수 있습니다.

정말 감사합니다 ! 덕분에 잘 해결했습니다!!!