Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(Auth): reissue, logout 구현 #41

Merged
merged 10 commits into from
Jul 29, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class JwtHandler {
private final Key refreshKey;
private final Integer accessExpiredMin;
private final Integer refreshExpiredDay;
private final String APP_ISSUER = "jabiseo";
private static final String APP_ISSUER = "jabiseo";

public JwtHandler(JwtProperty jwtProperty) {
byte[] accessEncodeByte = Base64.getEncoder().encode((jwtProperty.getAccessKey().getBytes()));
Expand All @@ -36,7 +36,6 @@ public JwtHandler(JwtProperty jwtProperty) {
public String createAccessToken(Member member) {
Instant accessExpiredTime = Instant.now()
.plus(this.accessExpiredMin, ChronoUnit.MINUTES);

Map<String, Object> payload = new HashMap<>();

return Jwts.builder()
Expand All @@ -58,20 +57,6 @@ public String createRefreshToken() {
}
InHyeok-J marked this conversation as resolved.
Show resolved Hide resolved


public boolean validateAccessToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(accessKey)
.build()
.parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
throw new AuthenticationBusinessException(AuthenticationErrorCode.EXPIRED_APP_JWT);
} catch (Exception e) {
throw new AuthenticationBusinessException(AuthenticationErrorCode.INVALID_APP_JWT);
}
}

public void validateRefreshToken(String refreshToken) {
try {
Jwts.parserBuilder()
Expand Down Expand Up @@ -101,11 +86,17 @@ public Claims getClaimFromExpiredAccessToken(String accessToken) {
}

public Claims getClaimsFromAccessToken(String token) {
return Jwts
.parserBuilder()
.setSigningKey(accessKey)
.build()
.parseClaimsJws(token)
.getBody();
try {
return Jwts
.parserBuilder()
.setSigningKey(accessKey)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
throw new AuthenticationBusinessException(AuthenticationErrorCode.EXPIRED_APP_JWT);
} catch (Exception e) {
throw new AuthenticationBusinessException(AuthenticationErrorCode.INVALID_APP_JWT);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package com.jabiseo.auth.application.usecase;

import com.jabiseo.cache.RedisCacheRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogoutUseCase {
public void execute() {

private final RedisCacheRepository redisCacheRepository;

public void execute(String memberId) {
redisCacheRepository.deleteToken(memberId);
}
morenow98 marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,42 @@
package com.jabiseo.auth.application.usecase;

import com.jabiseo.auth.application.JwtHandler;
import com.jabiseo.auth.dto.LoginResponse;
import com.jabiseo.auth.dto.ReissueRequest;
import com.jabiseo.auth.dto.ReissueResponse;
import com.jabiseo.auth.exception.AuthenticationBusinessException;
import com.jabiseo.auth.exception.AuthenticationErrorCode;
import com.jabiseo.cache.RedisCacheRepository;
import com.jabiseo.member.domain.Member;
import com.jabiseo.member.domain.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ReissueUseCase {

public LoginResponse reissue(String refreshToken) {
return new LoginResponse("access_token", "refresh_token");
private final MemberRepository memberRepository;
private final RedisCacheRepository redisCacheRepository;
private final JwtHandler jwtHandler;

public ReissueResponse execute(ReissueRequest request, String memberId) {
Member member = memberRepository.getReferenceById(memberId);

jwtHandler.validateRefreshToken(request.refreshToken());

String savedToken = redisCacheRepository.findToken(memberId)
.orElseThrow(() -> new AuthenticationBusinessException(AuthenticationErrorCode.REQUIRE_LOGIN));

if (!savedToken.equals(request.refreshToken())) {
throw new AuthenticationBusinessException(AuthenticationErrorCode.NOT_MATCH_REFRESH);
}

String accessToken = jwtHandler.createAccessToken(member);
return new ReissueResponse(accessToken);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import com.jabiseo.auth.application.usecase.LogoutUseCase;
import com.jabiseo.auth.application.usecase.ReissueUseCase;
import com.jabiseo.auth.application.usecase.WithdrawUseCase;
import com.jabiseo.auth.dto.ReissueRequest;
import com.jabiseo.auth.dto.ReissueResponse;
import com.jabiseo.config.auth.AuthMember;
import com.jabiseo.config.auth.AuthenticatedMember;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -34,14 +38,14 @@ public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest logi
}

@PostMapping("/reissue")
public ResponseEntity<LoginResponse> reissue(String refreshToken) {
LoginResponse result = reissueUseCase.reissue(refreshToken);
public ResponseEntity<ReissueResponse> reissue(@Valid @RequestBody ReissueRequest request, @AuthenticatedMember AuthMember member) {
ReissueResponse result = reissueUseCase.execute(request, member.getMemberId());
return ResponseEntity.ok(result);
}

@PostMapping("/logout")
public ResponseEntity<Void> logout() {
logoutUseCase.execute();
public ResponseEntity<Void> logout(@AuthenticatedMember AuthMember member) {
logoutUseCase.execute(member.getMemberId());
return ResponseEntity.noContent().build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.jabiseo.auth.dto;

import jakarta.validation.constraints.NotBlank;

public record ReissueRequest(@NotBlank String refreshToken) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.jabiseo.auth.dto;

public record ReissueResponse(String accessToken) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,29 @@
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtHandler jwtHandler;
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String HEADER_PREFIX = "Bearer ";

private static final String REISSUE_REQUEST = "/api/auth/reissue";
morenow98 marked this conversation as resolved.
Show resolved Hide resolved

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = extractTokenFromRequest(request);

if (StringUtils.hasText(token) && jwtHandler.validateAccessToken(token)) {
Claims claims = jwtHandler.getClaimsFromAccessToken(token);
if (StringUtils.hasText(token)) {
Claims claims = getClaimsFromToken(token, request);
setAuthenticationToContext(claims);
}

filterChain.doFilter(request, response);
}

private Claims getClaimsFromToken(String token, HttpServletRequest request) {
if (request.getRequestURI().equals(REISSUE_REQUEST)) {
return jwtHandler.getClaimFromExpiredAccessToken(token);
}
return jwtHandler.getClaimsFromAccessToken(token);
}

InHyeok-J marked this conversation as resolved.
Show resolved Hide resolved
private void setAuthenticationToContext(Claims jwtClaim) {
String memberId = jwtClaim.getSubject();
Set<GrantedAuthority> authorities = new HashSet<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.jabiseo.auth.application.usecase;

import com.jabiseo.auth.application.JwtHandler;
import com.jabiseo.auth.dto.ReissueRequest;
import com.jabiseo.auth.dto.ReissueResponse;
import com.jabiseo.auth.exception.AuthenticationBusinessException;
import com.jabiseo.auth.exception.AuthenticationErrorCode;
import com.jabiseo.cache.RedisCacheRepository;
import com.jabiseo.member.domain.Member;
import com.jabiseo.member.domain.MemberRepository;
import fixture.MemberFixture;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.BDDMockito.given;

@ExtendWith(MockitoExtension.class)
class ReissueUseCaseTest {

@InjectMocks
ReissueUseCase reissueUseCase;

@Mock
MemberRepository memberRepository;

@Mock
RedisCacheRepository redisCacheRepository;

@Mock
JwtHandler jwtHandler;

ReissueRequest request;

@BeforeEach
void setUp() {
request = new ReissueRequest("refresh");
}

@Test
@DisplayName("저장된 토큰이 없는 경우 예외를 반환한다")
void savedTokenIsNullThrownException() {
//given
String memberId = "id";
given(memberRepository.getReferenceById(memberId)).willReturn(MemberFixture.createMember(memberId));
given(redisCacheRepository.findToken(memberId)).willReturn(Optional.empty());

//when then
assertThatThrownBy(() -> reissueUseCase.execute(request, memberId))
.isInstanceOf(AuthenticationBusinessException.class);
}

@Test
@DisplayName("다른 refreshToken으로 요청하면 예외를 반환한다")
void otherTokenRequestThrownException() {
//given
String memberId = "id";
String otherToken = "tokens";
given(memberRepository.getReferenceById(memberId)).willReturn(MemberFixture.createMember(memberId));
given(redisCacheRepository.findToken(memberId)).willReturn(Optional.of(otherToken));

//when then
assertThatThrownBy(() -> reissueUseCase.execute(request, memberId))
.isInstanceOf(AuthenticationBusinessException.class)
.hasMessage(AuthenticationErrorCode.NOT_MATCH_REFRESH.getMessage());
}

@Test
@DisplayName("정상 요청의 경우 새로운 access Token을 발급한다.")
void requestSuccessReturnNewAccessToken(){
//given
String memberId = "id";
Member member = MemberFixture.createMember(memberId);
String newAccessToken = "accessToken";
given(memberRepository.getReferenceById(memberId)).willReturn(member);
given(redisCacheRepository.findToken(memberId)).willReturn(Optional.of(request.refreshToken()));
given(jwtHandler.createAccessToken(member)).willReturn(newAccessToken);

//when
ReissueResponse execute = reissueUseCase.execute(request, memberId);

//then
assertThat(execute.accessToken()).isEqualTo(newAccessToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ void containsInvalidTokenPassProcess(String headerValue) throws ServletException
jwtAuthenticationFilter.doFilterInternal(request, response, chain);

//then
verify(jwtHandler, never()).validateAccessToken(any());
verify(jwtHandler, never()).getClaimsFromAccessToken(any());
verify(jwtHandler, never()).getClaimFromExpiredAccessToken(any());
verify(chain, times(1)).doFilter(request, response);
}

Expand All @@ -76,14 +77,14 @@ void containsTokenStartJwtValidating() throws ServletException, IOException {
String headerValue = "Bearer " + token;
request.addHeader("Authorization", headerValue);

given(jwtHandler.validateAccessToken(token)).willReturn(true);
given(jwtHandler.getClaimsFromAccessToken(token)).willReturn(claims);

// when
jwtAuthenticationFilter.doFilterInternal(request, response, chain);

//then
verify(jwtHandler, times(1)).validateAccessToken(token);
verify(jwtHandler, times(1)).getClaimsFromAccessToken(token);
verify(jwtHandler, times(0)).getClaimFromExpiredAccessToken(token);
}

@Test
Expand All @@ -96,7 +97,6 @@ void jwtValidSuccessSaveAuthenticationInfo() throws ServletException, IOExceptio
String memberId = "memberId";


given(jwtHandler.validateAccessToken(token)).willReturn(true);
given(claims.getSubject()).willReturn(memberId);
given(jwtHandler.getClaimsFromAccessToken(token)).willReturn(claims);

Expand All @@ -109,5 +109,29 @@ void jwtValidSuccessSaveAuthenticationInfo() throws ServletException, IOExceptio
assertThat(authentication.getPrincipal().toString()).isEqualTo(memberId);
}

@Test
@DisplayName("reissue 요청 시 expired 검사안하고 Claim을 가져와 context에 저장한다.")
void reissueNotCheckExpired() throws ServletException, IOException {
//given
String token = "tokens";
String headerValue = "Bearer " + token;
request.addHeader("Authorization", headerValue);
request.setRequestURI("/api/auth/reissue");
String memberId = "memberId";

given(claims.getSubject()).willReturn(memberId);
given(jwtHandler.getClaimFromExpiredAccessToken(token)).willReturn(claims);

//when
jwtAuthenticationFilter.doFilterInternal(request, response, chain);

//then
verify(jwtHandler, never()).getClaimsFromAccessToken(token);
verify(jwtHandler, times(1)).getClaimFromExpiredAccessToken(token);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
assertThat(authentication).isNotNull();
assertThat(authentication.getPrincipal().toString()).isEqualTo(memberId);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public enum AuthenticationErrorCode implements ErrorCode {
EXPIRED_APP_JWT("만료된 jwt 토큰 입니다", "AUTH_004", ErrorCode.UNAUTHORIZED),
INVALID_APP_JWT("잘못된 jwt 토큰입니다", "AUTH_005", ErrorCode.UNAUTHORIZED),
GET_JWK_FAIL("jwk 획득 실패", "AUTH_006", ErrorCode.INTERNAL_SERVER_ERROR),
REQUIRE_LOGIN("로그인이 필요합니다","AUTH007", ErrorCode.UNAUTHORIZED);
REQUIRE_LOGIN("로그인이 필요합니다","AUTH007", ErrorCode.UNAUTHORIZED),
NOT_MATCH_REFRESH("refresh token이 일치하지 않습니다", "AUTH_008", ErrorCode.UNAUTHORIZED);

private final String message;
private final String errorCode;
Expand Down
Loading
Loading