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); } } }