From 81cc9858ccc0ff3e12546567918e43c86b2be46e Mon Sep 17 00:00:00 2001 From: fromitive <46563149+fromitive@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:46:25 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20access=5Ftoken,=20refresh=5Ftoken=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=20=EC=8B=9C=20=EC=98=A4=EB=A5=98=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EA=B0=92=EC=9D=B4=20=EB=8B=A4=EB=A5=B4=EA=B2=8C=20?= =?UTF-8?q?=EB=82=98=EC=98=A4=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20(#4?= =?UTF-8?q?78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: refreshSecret 키 불일치 수정 * fix: accessToken 만료 시 401 코드를 응답하도록 수정 * chore: 불필요한 파일 삭제 * fix: POSIX 오류 적용 --- backend/.gitignore | 3 ++ .../auth/exception/AuthErrorCode.java | 5 +- .../auth/service/JwtTokenProvider.java | 34 ++++++++++---- .../auth/integration/AuthIntegrationTest.java | 46 +++++++++++++++++-- 4 files changed, 71 insertions(+), 17 deletions(-) diff --git a/backend/.gitignore b/backend/.gitignore index 324c82fa7..484aa34ee 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -41,3 +41,6 @@ out/ ### RestDocs ### openapi3.yaml + +### Intellij IDEA ### +.idea diff --git a/backend/src/main/java/com/zzang/chongdae/auth/exception/AuthErrorCode.java b/backend/src/main/java/com/zzang/chongdae/auth/exception/AuthErrorCode.java index e5ced8fbf..a0833242c 100644 --- a/backend/src/main/java/com/zzang/chongdae/auth/exception/AuthErrorCode.java +++ b/backend/src/main/java/com/zzang/chongdae/auth/exception/AuthErrorCode.java @@ -11,13 +11,14 @@ public enum AuthErrorCode implements ErrorResponse { INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."), - EXPIRED_TOKEN(HttpStatus.FORBIDDEN, "만료된 토큰입니다."), + EXPIRED_REFRESH_TOKEN(HttpStatus.FORBIDDEN, "만료된 토큰입니다."), INVALID_COOKIE(HttpStatus.UNAUTHORIZED, "유효하지 않은 쿠키입니다."), COOKIE_NOT_EXIST(HttpStatus.UNAUTHORIZED, "쿠키가 존재하지 않습니다."), INVALID_PASSWORD(HttpStatus.NOT_FOUND, "가입하지 않은 회원입니다."), DUPLICATED_MEMBER(HttpStatus.CONFLICT, "이미 가입한 회원입니다."), CLIENT_TIME_OUT(HttpStatus.INTERNAL_SERVER_ERROR, "시간이 초과되어 로그인 요청에 실패했습니다."), - KAKAO_LOGIN_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 로그인에 실패했습니다."); + KAKAO_LOGIN_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 로그인에 실패했습니다."), + EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."); private final HttpStatus status; private final String message; diff --git a/backend/src/main/java/com/zzang/chongdae/auth/service/JwtTokenProvider.java b/backend/src/main/java/com/zzang/chongdae/auth/service/JwtTokenProvider.java index e51d3510b..6b21e237b 100644 --- a/backend/src/main/java/com/zzang/chongdae/auth/service/JwtTokenProvider.java +++ b/backend/src/main/java/com/zzang/chongdae/auth/service/JwtTokenProvider.java @@ -55,28 +55,42 @@ private Date calculateExpiredAt(Duration expired) { } public void validateAccessToken(String token) { - getClaims(token, accessSecretKey).getSubject(); + getClaimsAccessToken(token, accessSecretKey).getSubject(); } public Long getMemberIdByAccessToken(String token) { - String memberId = getClaims(token, accessSecretKey).getSubject(); + String memberId = getClaimsAccessToken(token, accessSecretKey).getSubject(); return Long.valueOf(memberId); } + private Claims getClaimsAccessToken(String token, String accessSecretKey) { + try { + return getClaims(token, accessSecretKey); + } catch (ExpiredJwtException e) { + throw new MarketException(AuthErrorCode.EXPIRED_ACCESS_TOKEN); + } catch (JwtException | IllegalArgumentException e) { + throw new MarketException(AuthErrorCode.INVALID_TOKEN); + } + } + + private Claims getClaims(String token, String key) { + return Jwts.parser() + .setSigningKey(key) + .setClock(() -> Date.from(clock.instant())) + .parseClaimsJws(token) + .getBody(); + } + public Long getMemberIdByRefreshToken(String token) { - String memberId = getClaims(token, refreshSecretKey).getSubject(); + String memberId = getClaimsRefreshToken(token, refreshSecretKey).getSubject(); return Long.valueOf(memberId); } - private Claims getClaims(String token, String key) { + private Claims getClaimsRefreshToken(String token, String refreshSecretKey) { try { - return Jwts.parser() - .setSigningKey(key) - .setClock(() -> Date.from(clock.instant())) - .parseClaimsJws(token) - .getBody(); + return getClaims(token, refreshSecretKey); } catch (ExpiredJwtException e) { - throw new MarketException(AuthErrorCode.EXPIRED_TOKEN); + throw new MarketException(AuthErrorCode.EXPIRED_REFRESH_TOKEN); } catch (JwtException | IllegalArgumentException e) { throw new MarketException(AuthErrorCode.INVALID_TOKEN); } diff --git a/backend/src/test/java/com/zzang/chongdae/auth/integration/AuthIntegrationTest.java b/backend/src/test/java/com/zzang/chongdae/auth/integration/AuthIntegrationTest.java index 6f8cc7ddf..255aff60b 100644 --- a/backend/src/test/java/com/zzang/chongdae/auth/integration/AuthIntegrationTest.java +++ b/backend/src/test/java/com/zzang/chongdae/auth/integration/AuthIntegrationTest.java @@ -19,6 +19,7 @@ import io.jsonwebtoken.SignatureAlgorithm; import io.restassured.http.ContentType; import java.time.Duration; +import java.util.Base64; import java.util.Date; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -88,9 +89,9 @@ void should_loginSuccess_when_givenMemberCI() { } } - @DisplayName("토큰 재발급") + @DisplayName("토큰 관리") @Nested - class Refresh { + class ManageToken { List responseHeaderDescriptors = List.of( headerWithName("Set-Cookie").description(""" @@ -111,9 +112,15 @@ class Refresh { @Value("${security.jwt.token.refresh-secret-key}") String refreshSecretKey; + @Value("${security.jwt.token.access-secret-key}") + String accessSecretKey; + @Value("${security.jwt.token.refresh-token-expired}") Duration refreshTokenExpired; + @Value("${security.jwt.token.access-token-expired}") + Duration accessTokenExpired; + MemberEntity member; Date now; @@ -123,6 +130,35 @@ void setUp() { now = Date.from(clock.instant()); } + @DisplayName("만료된 accessToken 경우 예외 발생 후 401 코드를 반환한다.") + @Test + void should_throwException_when_givenExpiredAccessToken() { + Date alreadyExpiredAt = new Date(now.getTime() - accessTokenExpired.toMillis()); + String expiredToken = Jwts.builder() + .setSubject(member.getId().toString()) + .setExpiration(alreadyExpiredAt) + .signWith(SignatureAlgorithm.HS256, Base64.getEncoder().encodeToString(accessSecretKey.getBytes())) + .compact(); + + given(spec).log().all() + .filter(document("access-fail-expired-token", resource(failedSnippets))) + .cookie("access_token", expiredToken) + .when().get("/offerings") + .then().log().all() + .statusCode(401); + } + + @DisplayName("유효하지 않은 accessToken인 경우 예외가 발생한다.") + @Test + void should_throwException_when_givenInvalidAccessToken() { + given(spec).log().all() + .filter(document("refresh-fail-invalid-token", resource(failedSnippets))) + .cookie("access_token", "invalidRefreshToken") + .when().post("/offerings") + .then().log().all() + .statusCode(401); + } + @DisplayName("refreshToken으로 accessToken과 refreshToken을 재발급 한다.") @Test void should_refreshSuccess_when_givenRefreshToken() { @@ -147,14 +183,14 @@ void should_throwException_when_givenInvalidRefreshToken() { .statusCode(401); } - @DisplayName("만료된 refeshToken인 경우 예외가 발생한다.") + @DisplayName("만료된 refeshToken인 경우 예외 발생 후 403 코드를 반환한다.") @Test void should_throwException_when_givenExpiredRefreshToken() { Date alreadyExpiredAt = new Date(now.getTime() - refreshTokenExpired.toMillis()); String expiredToken = Jwts.builder() .setSubject(member.getId().toString()) .setExpiration(alreadyExpiredAt) - .signWith(SignatureAlgorithm.HS256, refreshSecretKey) + .signWith(SignatureAlgorithm.HS256, Base64.getEncoder().encodeToString(refreshSecretKey.getBytes())) .compact(); given(spec).log().all() @@ -162,7 +198,7 @@ void should_throwException_when_givenExpiredRefreshToken() { .cookie("refresh_token", expiredToken) .when().post("/auth/refresh") .then().log().all() - .statusCode(401); + .statusCode(403); } } }