소개배경
이번에 Refresh Rotation 전략을 구성하며 상당히 바보 같은 에러를 자초하여 이를 기록하고자 간단하고 짧게 포스팅하게 되었다. 가장 크게 문제가 되었던 것은 RefreshToken으로 새로 발급 받은 AccessToken을 통하여 API의 응답이 이루어지지 않는 이슈였다.
Refresh Rotation 로직 자체에 문제가 발생한 것은 아니나, 해당 로직에 관련 부분이 문제를 일으키게 되었다.
혹시라도, Refresh Rotation을 알지 못한다면 나의 블로그에서 JWT를 정리하였던 글 또는 타 블로그를 참조하는 것이 좋을 것같다.
Trouble(문제원인)
현재 프로젝트에서 RefreshToken의 용도는 토큰을 재발급 받는 용도로만 사용하게 되었다.
현재 프로젝트에선 Custom Filter를 제작하였고 모든 URL 기반의 API 요청에서 RefreshToken의 유무를 판별한다.
만약, RefreshToken이 존재한다면 이는 토큰 재발급을 위한 요청으로 판단하여, 바로 관련 로직을 호출한다.
이때 Refresh Rotation 특성상 AccessToken과 RefreshToken 모두 재발급하여 클라이언트에게 Response(응답)을 주게된다.
여기서 문제가 발생하게 되었다.
본 프로젝트의 경우 OAtuh2.0 환경을 구성하고 있다. 흐름 상 OAuth 2.0의 인가 과정이 끝나면 OAuthSuccessHandler로 넘어가게 되고 그 시점에 AccessToken과 RefreshToken을 유저에게 설정해준다.
이때 Github OAuth환경에서 Member Entity의 Name 필드를 구성하는 `login`이라는 것을 OAuthAttributes에서 꺼내어 사용한다. 한마디로 로그인이 인가과정이 완료된 후에는 Name을 토큰 클레임으로 구성하여 액세스 토큰을 생성한다.
하지만 RefreshToken 재발급 로직에서는 이를 MemberEntity의 Email 필드를 가져와 액세스 토큰을 생성하였기에 다른 토큰으로 인식되는 것이다.
JWT 토큰에서의 세개의 구조 중 Payload에는 토큰의 유효기간 및 외부에 노출이 가능한(유출되어도 문제를 일으킬 수 없는 데이터) 유저정보를 담게된다.
여기서 유저정보 클레임을 각각 다른 정보로 생성하게 된 경우 서로 다른 클레임을 가지기에 별개의 토큰으로 인식하는 것이다.
기본적으로 AccessToken을 생성할 때 , 애플리케이션에서 지정한 특정 알고리즘을 이용해 인코딩을 거치게된다. 하지만 구성되는 회원 클레임 정보가 다르면 최종적으로 서로 다른 클레임이 생성되어 이를 인식하지 못하고 AccessToken을 정상적으로 사용할 수 없던 것이다.
원인 코드
정말 단순하게 에러를 Fix하였기에 살짝은 부끄러운 수준이다.
우선 OAuth 인가 과정 후 토큰을 발급해주는 메소드를 보자
private void loginSuccess(HttpServletResponse response, CustomOAuth2User oAuth2User) throws IOException {
String accessToken = jwtService.createAccessToken((String) oAuth2User.getAttributes().get("login"));
String refreshToken = jwtService.createRefreshToken();
System.out.println("accessToken="+accessToken);
System.out.println("refreshToken="+refreshToken);
response.addHeader(jwtService.getAccessHeader(), "Bearer " + accessToken);
response.addHeader(jwtService.getRefreshHeader(), "Bearer " + refreshToken);
jwtService.sendAccessAndRefreshToken(response, accessToken, refreshToken);
jwtService.updateRefreshToken((String) oAuth2User.getAttributes().get("login"), refreshToken);
}
해당 메소드에서는 CustomOAuth2User 객체에서 attributes의 login을 사용하여 AccessToken을 발급한다.(이는 Member Entity의 Name 필드로 저장)
이후 재발급 받는 로직을 살펴보자
public void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, String refreshToken) {
memberRepository.findByRefreshToken(refreshToken)
.ifPresent(member -> {
String reIssuedRefreshToken = reIssueRefreshToken(member);
jwtService.sendAccessAndRefreshToken(response, jwtService.createAccessToken(member.getEmail()),
reIssuedRefreshToken);
});
}
요청이 들어오면 AccessToken과 RefreshToken을 모두 재발급 해주는 코드이다. 여기서 jwtService의 createAccessToken을 이용하는데, 이미 Name을 클레임으로 토큰을 생성하였는데 email을 사용한 것이 직접적으로 문제가 되었다.
Trouble Shooting(문제 해결)
단순하게도 이를 name으로 변경하여주자 에러를 고칠 수 있었다.
jwtService.createAccessToken(member.getNickname())
전체적인 코드흐름을 보고 싶다면 아래의 링크를 참조하도록하자
#25 이슈 해결 - RefreshToken Rotation Issue Solved · Issue #26 · Mogakco-web/BE-Mogakco
원인 분석 기존 토큰 발급 전략의 경우 RefreshToken의 사용 용도를 토큰 재발급으로 규정 이때 기존 OAuthLogin이 성공한 후의 토큰 발급 로직과 RefreshToken Rotation의 로직의 차이점 발견 기존 로직의 경
github.com
결론
정말 간단한 이슈로 인한 에러였지만 이를 트러블 슈팅하기 위해 수많은 로깅 작업을 진행했다..
코드의 변경점이 생긴다면 다음으로 미루지말고 바로 작업하며, 개발 단계에서의 log는 웬만해서 실배포 환경이전까지는 남기는 습관을 들여야겠다..
'Trouble-Shooting' 카테고리의 다른 글
Jackson InvalidDefinitionException (0) | 2023.05.17 |
---|---|
NoClassDefFoundError, Command is too long (0) | 2023.04.23 |
[JPA] Service Layer 간의 순환 참조 (1) | 2023.04.14 |
[JPA] @OneToOne - 주인 결정하기(양방향 참조) (0) | 2023.03.26 |
406 Error - Not Acceptable (0) | 2023.03.13 |