diff --git a/src/main/java/com/genius/gitget/challenge/user/domain/User.java b/src/main/java/com/genius/gitget/challenge/user/domain/User.java index aefd1ec9..b339a91d 100644 --- a/src/main/java/com/genius/gitget/challenge/user/domain/User.java +++ b/src/main/java/com/genius/gitget/challenge/user/domain/User.java @@ -116,6 +116,10 @@ public long updatePoints(Long amount) { return this.point; } + public boolean isRegistered() { + return this.role != Role.NOT_REGISTERED; + } + @Override public Optional getFiles() { return Optional.ofNullable(this.files); diff --git a/src/main/java/com/genius/gitget/global/security/config/CustomCorsConfigurationSource.java b/src/main/java/com/genius/gitget/global/security/config/CustomCorsConfigurationSource.java index 635a69d2..3c7d475f 100644 --- a/src/main/java/com/genius/gitget/global/security/config/CustomCorsConfigurationSource.java +++ b/src/main/java/com/genius/gitget/global/security/config/CustomCorsConfigurationSource.java @@ -1,5 +1,8 @@ package com.genius.gitget.global.security.config; +import static com.genius.gitget.global.security.constants.JwtRule.ACCESS_HEADER; +import static com.genius.gitget.global.security.constants.JwtRule.ACCESS_REISSUED_HEADER; + import jakarta.servlet.http.HttpServletRequest; import java.util.Collections; import java.util.List; @@ -24,6 +27,10 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { config.setAllowedMethods(ALLOWED_METHODS); config.setAllowCredentials(true); config.setAllowedHeaders(Collections.singletonList("*")); + + config.setExposedHeaders(Collections.singletonList(ACCESS_HEADER.getValue())); + config.addExposedHeader(ACCESS_REISSUED_HEADER.getValue()); + config.setMaxAge(3600L); return config; } diff --git a/src/main/java/com/genius/gitget/global/security/config/SecurityConfig.java b/src/main/java/com/genius/gitget/global/security/config/SecurityConfig.java index 2bd56770..80a5eae7 100644 --- a/src/main/java/com/genius/gitget/global/security/config/SecurityConfig.java +++ b/src/main/java/com/genius/gitget/global/security/config/SecurityConfig.java @@ -7,7 +7,7 @@ import com.genius.gitget.global.security.handler.OAuth2FailureHandler; import com.genius.gitget.global.security.handler.OAuth2SuccessHandler; import com.genius.gitget.global.security.service.CustomOAuth2UserService; -import com.genius.gitget.global.security.service.JwtService; +import com.genius.gitget.global.security.service.JwtFacade; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -32,7 +32,7 @@ public class SecurityConfig { private static final String PERMITTED_ROLES[] = {"USER", "ADMIN"}; private final CustomCorsConfigurationSource customCorsConfigurationSource; private final CustomOAuth2UserService customOAuthService; - private final JwtService jwtService; + private final JwtFacade jwtFacade; private final UserService userService; private final OAuth2SuccessHandler successHandler; private final OAuth2FailureHandler failureHandler; @@ -57,7 +57,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // JWT 검증 필터 추가 - .addFilterBefore(new JwtAuthenticationFilter(jwtService, userService), + .addFilterBefore(new JwtAuthenticationFilter(jwtFacade, userService), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new ExceptionHandlerFilter(), JwtAuthenticationFilter.class) diff --git a/src/main/java/com/genius/gitget/global/security/constants/JwtRule.java b/src/main/java/com/genius/gitget/global/security/constants/JwtRule.java index 8892419d..0492a016 100644 --- a/src/main/java/com/genius/gitget/global/security/constants/JwtRule.java +++ b/src/main/java/com/genius/gitget/global/security/constants/JwtRule.java @@ -6,10 +6,15 @@ @RequiredArgsConstructor @Getter public enum JwtRule { - JWT_ISSUE_HEADER("Set-Cookie"), - JWT_RESOLVE_HEADER("Cookie"), - ACCESS_PREFIX("access"), - REFRESH_PREFIX("refresh"); + + ACCESS_HEADER("Authorization"), + ACCESS_PREFIX("Bearer "), + ACCESS_REISSUED_HEADER("token-reissued"), + + REFRESH_PREFIX("refresh"), + + REFRESH_ISSUE("Set-Cookie"), + REFRESH_RESOLVE("Cookie"); private final String value; } diff --git a/src/main/java/com/genius/gitget/global/security/controller/AuthController.java b/src/main/java/com/genius/gitget/global/security/controller/AuthController.java index d3377ede..9640b41f 100644 --- a/src/main/java/com/genius/gitget/global/security/controller/AuthController.java +++ b/src/main/java/com/genius/gitget/global/security/controller/AuthController.java @@ -1,13 +1,15 @@ package com.genius.gitget.global.security.controller; +import static com.genius.gitget.global.util.exception.ErrorCode.NOT_AUTHENTICATED_USER; import static com.genius.gitget.global.util.exception.SuccessCode.SUCCESS; import com.genius.gitget.challenge.user.domain.User; import com.genius.gitget.challenge.user.service.UserService; import com.genius.gitget.global.security.domain.UserPrincipal; import com.genius.gitget.global.security.dto.AuthResponse; -import com.genius.gitget.global.security.dto.SignupResponse; -import com.genius.gitget.global.security.service.JwtService; +import com.genius.gitget.global.security.dto.TokenRequest; +import com.genius.gitget.global.security.service.JwtFacade; +import com.genius.gitget.global.util.exception.BusinessException; import com.genius.gitget.global.util.response.dto.CommonResponse; import com.genius.gitget.global.util.response.dto.SingleResponse; import jakarta.servlet.http.HttpServletResponse; @@ -27,18 +29,20 @@ @RequestMapping("/api") public class AuthController { private final UserService userService; - private final JwtService jwtService; + private final JwtFacade jwtFacade; @PostMapping("/auth") public ResponseEntity> generateToken(HttpServletResponse response, - @RequestBody SignupResponse tokenRequest) { - User requestUser = userService.findUserByIdentifier(tokenRequest.identifier()); - jwtService.validateUser(requestUser); + @RequestBody TokenRequest tokenRequest) { + User user = userService.findUserByIdentifier(tokenRequest.identifier()); + if (!user.isRegistered()) { + throw new BusinessException(NOT_AUTHENTICATED_USER); + } - jwtService.generateAccessToken(response, requestUser); - jwtService.generateRefreshToken(response, requestUser); + jwtFacade.generateAccessToken(response, user); + jwtFacade.generateRefreshToken(response, user); - AuthResponse authResponse = userService.getUserAuthInfo(requestUser.getIdentifier()); + AuthResponse authResponse = userService.getUserAuthInfo(user.getIdentifier()); return ResponseEntity.ok().body( new SingleResponse<>(SUCCESS.getStatus(), SUCCESS.getMessage(), authResponse) @@ -49,7 +53,7 @@ public ResponseEntity> generateToken(HttpServletRes public ResponseEntity logout( @AuthenticationPrincipal UserPrincipal userPrincipal, HttpServletResponse response) { - jwtService.logout(userPrincipal.getUser(), response); + jwtFacade.logout(response, userPrincipal.getUser().getIdentifier()); return ResponseEntity.ok().body( new CommonResponse(SUCCESS.getStatus(), SUCCESS.getMessage()) diff --git a/src/main/java/com/genius/gitget/global/security/dto/TokenRequest.java b/src/main/java/com/genius/gitget/global/security/dto/TokenRequest.java new file mode 100644 index 00000000..d3a0f485 --- /dev/null +++ b/src/main/java/com/genius/gitget/global/security/dto/TokenRequest.java @@ -0,0 +1,6 @@ +package com.genius.gitget.global.security.dto; + +public record TokenRequest( + String identifier +) { +} diff --git a/src/main/java/com/genius/gitget/global/security/filter/ExceptionHandlerFilter.java b/src/main/java/com/genius/gitget/global/security/filter/ExceptionHandlerFilter.java index 46226654..4e648f7c 100644 --- a/src/main/java/com/genius/gitget/global/security/filter/ExceptionHandlerFilter.java +++ b/src/main/java/com/genius/gitget/global/security/filter/ExceptionHandlerFilter.java @@ -8,18 +8,20 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.filter.OncePerRequestFilter; @Slf4j +@RequiredArgsConstructor public class ExceptionHandlerFilter extends OncePerRequestFilter { + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { filterChain.doFilter(request, response); } catch (BusinessException e) { - log.error("[ERROR]" + e.getMessage(), e); setErrorResponse(response, e); } } diff --git a/src/main/java/com/genius/gitget/global/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/genius/gitget/global/security/filter/JwtAuthenticationFilter.java index e9054f65..d1e6778e 100644 --- a/src/main/java/com/genius/gitget/global/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/genius/gitget/global/security/filter/JwtAuthenticationFilter.java @@ -2,10 +2,9 @@ import static com.genius.gitget.global.security.config.SecurityConfig.PERMITTED_URI; -import com.genius.gitget.global.security.constants.JwtRule; -import com.genius.gitget.global.security.service.JwtService; import com.genius.gitget.challenge.user.domain.User; import com.genius.gitget.challenge.user.service.UserService; +import com.genius.gitget.global.security.service.JwtFacade; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -19,7 +18,7 @@ @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { - private final JwtService jwtService; + private final JwtFacade jwtFacade; private final UserService userService; @Override @@ -32,26 +31,27 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; } - String accessToken = jwtService.resolveTokenFromCookie(request, JwtRule.ACCESS_PREFIX); - if (jwtService.validateAccessToken(accessToken)) { + String accessToken = jwtFacade.resolveAccessToken(request); + if (jwtFacade.validateAccessToken(accessToken)) { setAuthenticationToContext(accessToken); filterChain.doFilter(request, response); return; } - String refreshToken = jwtService.resolveTokenFromCookie(request, JwtRule.REFRESH_PREFIX); + String refreshToken = jwtFacade.resolveRefreshToken(request); User user = findUserByRefreshToken(refreshToken); - if (jwtService.validateRefreshToken(refreshToken, user.getIdentifier())) { - String reissuedAccessToken = jwtService.generateAccessToken(response, user); - jwtService.generateRefreshToken(response, user); + if (jwtFacade.validateRefreshToken(refreshToken, user.getIdentifier())) { + String reissuedAccessToken = jwtFacade.generateAccessToken(response, user); + jwtFacade.generateRefreshToken(response, user); + jwtFacade.setReissuedHeader(response); setAuthenticationToContext(reissuedAccessToken); filterChain.doFilter(request, response); return; } - jwtService.logout(user, response); + jwtFacade.logout(response, user.getIdentifier()); } private boolean isPermittedURI(String requestURI) { @@ -63,12 +63,12 @@ private boolean isPermittedURI(String requestURI) { } private User findUserByRefreshToken(String refreshToken) { - String identifier = jwtService.getIdentifierFromRefresh(refreshToken); + String identifier = jwtFacade.getIdentifierFromRefresh(refreshToken); return userService.findUserByIdentifier(identifier); } private void setAuthenticationToContext(String accessToken) { - Authentication authentication = jwtService.getAuthentication(accessToken); + Authentication authentication = jwtFacade.getAuthentication(accessToken); SecurityContextHolder.getContext().setAuthentication(authentication); } } diff --git a/src/main/java/com/genius/gitget/global/security/service/CustomUserDetailsService.java b/src/main/java/com/genius/gitget/global/security/service/CustomUserDetailsService.java index 3d69a7bc..782d2699 100644 --- a/src/main/java/com/genius/gitget/global/security/service/CustomUserDetailsService.java +++ b/src/main/java/com/genius/gitget/global/security/service/CustomUserDetailsService.java @@ -2,14 +2,13 @@ import static com.genius.gitget.global.util.exception.ErrorCode.MEMBER_NOT_FOUND; -import com.genius.gitget.global.security.domain.UserPrincipal; import com.genius.gitget.challenge.user.domain.User; import com.genius.gitget.challenge.user.repository.UserRepository; +import com.genius.gitget.global.security.domain.UserPrincipal; import com.genius.gitget.global.util.exception.BusinessException; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service @@ -18,7 +17,7 @@ public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + public UserDetails loadUserByUsername(String username) { User user = userRepository.findById(Long.valueOf(username)) .orElseThrow(() -> new BusinessException(MEMBER_NOT_FOUND)); diff --git a/src/main/java/com/genius/gitget/global/security/service/JwtFacade.java b/src/main/java/com/genius/gitget/global/security/service/JwtFacade.java new file mode 100644 index 00000000..b1e64bb3 --- /dev/null +++ b/src/main/java/com/genius/gitget/global/security/service/JwtFacade.java @@ -0,0 +1,28 @@ +package com.genius.gitget.global.security.service; + +import com.genius.gitget.challenge.user.domain.User; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; + +public interface JwtFacade { + String generateAccessToken(HttpServletResponse response, User user); + + String generateRefreshToken(HttpServletResponse response, User user); + + String resolveAccessToken(HttpServletRequest request); + + String resolveRefreshToken(HttpServletRequest request); + + String getIdentifierFromRefresh(String refreshToken); + + boolean validateAccessToken(String accessToken); + + boolean validateRefreshToken(String refreshToken, String identifier); + + void setReissuedHeader(HttpServletResponse response); + + void logout(HttpServletResponse response, String identifier); + + Authentication getAuthentication(String accessToken); +} diff --git a/src/main/java/com/genius/gitget/global/security/service/JwtService.java b/src/main/java/com/genius/gitget/global/security/service/JwtFacadeService.java similarity index 63% rename from src/main/java/com/genius/gitget/global/security/service/JwtService.java rename to src/main/java/com/genius/gitget/global/security/service/JwtFacadeService.java index b8c9315a..17c761b4 100644 --- a/src/main/java/com/genius/gitget/global/security/service/JwtService.java +++ b/src/main/java/com/genius/gitget/global/security/service/JwtFacadeService.java @@ -1,17 +1,16 @@ package com.genius.gitget.global.security.service; +import static com.genius.gitget.global.security.constants.JwtRule.ACCESS_HEADER; import static com.genius.gitget.global.security.constants.JwtRule.ACCESS_PREFIX; -import static com.genius.gitget.global.security.constants.JwtRule.JWT_ISSUE_HEADER; +import static com.genius.gitget.global.security.constants.JwtRule.ACCESS_REISSUED_HEADER; +import static com.genius.gitget.global.security.constants.JwtRule.REFRESH_ISSUE; import static com.genius.gitget.global.security.constants.JwtRule.REFRESH_PREFIX; -import static com.genius.gitget.global.util.exception.ErrorCode.JWT_TOKEN_NOT_FOUND; -import static com.genius.gitget.global.util.exception.ErrorCode.NOT_AUTHENTICATED_USER; +import static com.genius.gitget.global.util.exception.ErrorCode.JWT_NOT_FOUND_IN_COOKIE; +import static com.genius.gitget.global.util.exception.ErrorCode.JWT_NOT_FOUND_IN_HEADER; -import com.genius.gitget.challenge.user.domain.Role; import com.genius.gitget.challenge.user.domain.User; -import com.genius.gitget.global.security.constants.JwtRule; import com.genius.gitget.global.security.constants.TokenStatus; import com.genius.gitget.global.security.domain.Token; -import com.genius.gitget.global.security.repository.TokenRepository; import com.genius.gitget.global.util.exception.BusinessException; import com.genius.gitget.global.util.exception.ErrorCode; import io.jsonwebtoken.Jwts; @@ -31,54 +30,53 @@ @Service @Transactional(readOnly = true) @Slf4j -public class JwtService { +public class JwtFacadeService implements JwtFacade { private final CustomUserDetailsService customUserDetailsService; + private final TokenService tokenService; private final JwtGenerator jwtGenerator; private final JwtUtil jwtUtil; - private final TokenRepository tokenRepository; private final Key ACCESS_SECRET_KEY; private final Key REFRESH_SECRET_KEY; private final long ACCESS_EXPIRATION; private final long REFRESH_EXPIRATION; - public JwtService(CustomUserDetailsService customUserDetailsService, JwtGenerator jwtGenerator, - JwtUtil jwtUtil, TokenRepository tokenRepository, - @Value("${jwt.access-secret}") String ACCESS_SECRET_KEY, - @Value("${jwt.refresh-secret}") String REFRESH_SECRET_KEY, - @Value("${jwt.access-expiration}") long ACCESS_EXPIRATION, - @Value("${jwt.refresh-expiration}") long REFRESH_EXPIRATION) { + public JwtFacadeService(CustomUserDetailsService customUserDetailsService, + TokenService tokenService, + JwtGenerator jwtGenerator, JwtUtil jwtUtil, + @Value("${jwt.access-secret}") String ACCESS_SECRET_KEY, + @Value("${jwt.refresh-secret}") String REFRESH_SECRET_KEY, + @Value("${jwt.access-expiration}") long ACCESS_EXPIRATION, + @Value("${jwt.refresh-expiration}") long REFRESH_EXPIRATION) { this.customUserDetailsService = customUserDetailsService; + this.tokenService = tokenService; this.jwtGenerator = jwtGenerator; this.jwtUtil = jwtUtil; - this.tokenRepository = tokenRepository; this.ACCESS_SECRET_KEY = jwtUtil.getSigningKey(ACCESS_SECRET_KEY); this.REFRESH_SECRET_KEY = jwtUtil.getSigningKey(REFRESH_SECRET_KEY); this.ACCESS_EXPIRATION = ACCESS_EXPIRATION; this.REFRESH_EXPIRATION = REFRESH_EXPIRATION; } - public void validateUser(User requestUser) { - if (requestUser.getRole() == Role.NOT_REGISTERED) { - throw new BusinessException(NOT_AUTHENTICATED_USER); - } - } + @Override public String generateAccessToken(HttpServletResponse response, User requestUser) { String accessToken = jwtGenerator.generateAccessToken(ACCESS_SECRET_KEY, ACCESS_EXPIRATION, requestUser); - ResponseCookie cookie = setTokenToCookie(ACCESS_PREFIX.getValue(), accessToken, ACCESS_EXPIRATION / 1000); - response.addHeader(JWT_ISSUE_HEADER.getValue(), cookie.toString()); + String bearer = ACCESS_PREFIX.getValue() + accessToken; + response.setHeader(ACCESS_HEADER.getValue(), bearer); + response.setHeader(ACCESS_REISSUED_HEADER.getValue(), "False"); return accessToken; } + @Override @Transactional public String generateRefreshToken(HttpServletResponse response, User requestUser) { String refreshToken = jwtGenerator.generateRefreshToken(REFRESH_SECRET_KEY, REFRESH_EXPIRATION, requestUser); ResponseCookie cookie = setTokenToCookie(REFRESH_PREFIX.getValue(), refreshToken, REFRESH_EXPIRATION / 1000); - response.addHeader(JWT_ISSUE_HEADER.getValue(), cookie.toString()); + response.addHeader(REFRESH_ISSUE.getValue(), cookie.toString()); - tokenRepository.save(new Token(requestUser.getIdentifier(), refreshToken)); + tokenService.save(new Token(requestUser.getIdentifier(), refreshToken)); return refreshToken; } @@ -87,46 +85,48 @@ private ResponseCookie setTokenToCookie(String tokenPrefix, String token, long m .path("/") .maxAge(maxAgeSeconds) .httpOnly(true) - .sameSite("None") + .sameSite("Strict") .secure(true) .build(); } + @Override public boolean validateAccessToken(String token) { return jwtUtil.getTokenStatus(token, ACCESS_SECRET_KEY) == TokenStatus.AUTHENTICATED; } + @Override public boolean validateRefreshToken(String token, String identifier) { boolean isRefreshValid = jwtUtil.getTokenStatus(token, REFRESH_SECRET_KEY) == TokenStatus.AUTHENTICATED; + boolean isHijacked = tokenService.isRefreshHijacked(identifier, token); - Token storedToken = tokenRepository.findByIdentifier(identifier); - boolean isTokenMatched = storedToken.getToken().equals(token); - - return isRefreshValid && isTokenMatched; + return isRefreshValid && !isHijacked; } - public String resolveTokenFromCookie(HttpServletRequest request, JwtRule tokenPrefix) { - Cookie[] cookies = request.getCookies(); - if (cookies == null) { - throw new BusinessException(JWT_TOKEN_NOT_FOUND); - } - return jwtUtil.resolveTokenFromCookie(cookies, tokenPrefix); + @Override + public void setReissuedHeader(HttpServletResponse response) { + response.setHeader(ACCESS_REISSUED_HEADER.getValue(), "True"); } - public Authentication getAuthentication(String token) { - UserDetails principal = customUserDetailsService.loadUserByUsername(getUserPk(token, ACCESS_SECRET_KEY)); - return new UsernamePasswordAuthenticationToken(principal, "", principal.getAuthorities()); + @Override + public String resolveAccessToken(HttpServletRequest request) { + String bearerHeader = request.getHeader(ACCESS_HEADER.getValue()); + if (bearerHeader == null || bearerHeader.isEmpty()) { + throw new BusinessException(JWT_NOT_FOUND_IN_HEADER); + } + return bearerHeader.trim().substring(7); } - private String getUserPk(String token, Key secretKey) { - return Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(token) - .getBody() - .getSubject(); + @Override + public String resolveRefreshToken(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + throw new BusinessException(JWT_NOT_FOUND_IN_COOKIE); + } + return jwtUtil.resolveTokenFromCookie(cookies, REFRESH_PREFIX); } + @Override public String getIdentifierFromRefresh(String refreshToken) { try { return Jwts.parserBuilder() @@ -140,13 +140,26 @@ public String getIdentifierFromRefresh(String refreshToken) { } } - public void logout(User requestUser, HttpServletResponse response) { - tokenRepository.deleteById(requestUser.getIdentifier()); + @Override + public Authentication getAuthentication(String accessToken) { + UserDetails principal = customUserDetailsService.loadUserByUsername(getUserPk(accessToken, ACCESS_SECRET_KEY)); + return new UsernamePasswordAuthenticationToken(principal, "", principal.getAuthorities()); + } + + private String getUserPk(String token, Key secretKey) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + } - Cookie accessCookie = jwtUtil.resetToken(ACCESS_PREFIX); - Cookie refreshCookie = jwtUtil.resetToken(REFRESH_PREFIX); + @Override + public void logout(HttpServletResponse response, String identifier) { + tokenService.deleteById(identifier); - response.addCookie(accessCookie); + Cookie refreshCookie = jwtUtil.resetCookie(REFRESH_PREFIX); response.addCookie(refreshCookie); } } diff --git a/src/main/java/com/genius/gitget/global/security/service/JwtGenerator.java b/src/main/java/com/genius/gitget/global/security/service/JwtGenerator.java index 4c2fed46..c17d6d4d 100644 --- a/src/main/java/com/genius/gitget/global/security/service/JwtGenerator.java +++ b/src/main/java/com/genius/gitget/global/security/service/JwtGenerator.java @@ -10,8 +10,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -@Component @Slf4j +@Component public class JwtGenerator { public String generateAccessToken(final Key ACCESS_SECRET, final long ACCESS_EXPIRATION, User user) { diff --git a/src/main/java/com/genius/gitget/global/security/service/JwtUtil.java b/src/main/java/com/genius/gitget/global/security/service/JwtUtil.java index b11d7801..c499af54 100644 --- a/src/main/java/com/genius/gitget/global/security/service/JwtUtil.java +++ b/src/main/java/com/genius/gitget/global/security/service/JwtUtil.java @@ -2,6 +2,7 @@ import static com.genius.gitget.global.util.exception.ErrorCode.INVALID_EXPIRED_JWT; import static com.genius.gitget.global.util.exception.ErrorCode.INVALID_JWT; +import static com.genius.gitget.global.util.exception.ErrorCode.JWT_NOT_FOUND_IN_COOKIE; import com.genius.gitget.global.security.constants.JwtRule; import com.genius.gitget.global.security.constants.TokenStatus; @@ -15,15 +16,11 @@ import java.security.Key; import java.util.Arrays; import java.util.Base64; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.stereotype.Component; @Slf4j -@Service -@Transactional(readOnly = true) -@RequiredArgsConstructor +@Component public class JwtUtil { public TokenStatus getTokenStatus(String token, Key secretKey) { @@ -33,7 +30,7 @@ public TokenStatus getTokenStatus(String token, Key secretKey) { .build() .parseClaimsJws(token); return TokenStatus.AUTHENTICATED; - } catch (ExpiredJwtException | IllegalArgumentException e) { + } catch (ExpiredJwtException e) { log.error(INVALID_EXPIRED_JWT.getMessage()); return TokenStatus.EXPIRED; } catch (JwtException e) { @@ -46,7 +43,7 @@ public String resolveTokenFromCookie(Cookie[] cookies, JwtRule tokenPrefix) { .filter(cookie -> cookie.getName().equals(tokenPrefix.getValue())) .findFirst() .map(Cookie::getValue) - .orElse(""); + .orElseThrow(() -> new BusinessException(JWT_NOT_FOUND_IN_COOKIE)); } public Key getSigningKey(String secretKey) { @@ -58,7 +55,7 @@ private String encodeToBase64(String secretKey) { return Base64.getEncoder().encodeToString(secretKey.getBytes()); } - public Cookie resetToken(JwtRule tokenPrefix) { + public Cookie resetCookie(JwtRule tokenPrefix) { Cookie cookie = new Cookie(tokenPrefix.getValue(), null); cookie.setMaxAge(0); cookie.setPath("/"); diff --git a/src/main/java/com/genius/gitget/global/security/service/TokenService.java b/src/main/java/com/genius/gitget/global/security/service/TokenService.java index 4b4126c6..ccd2ef54 100644 --- a/src/main/java/com/genius/gitget/global/security/service/TokenService.java +++ b/src/main/java/com/genius/gitget/global/security/service/TokenService.java @@ -16,15 +16,23 @@ public class TokenService { private final TokenRepository tokenRepository; - public Token findTokenByIdentifier(String identifier) { + @Transactional + public String save(Token token) { + Token savedToken = tokenRepository.save(token); + return savedToken.getIdentifier(); + } + + public Token findByIdentifier(String identifier) { return tokenRepository.findById(identifier) - .orElseThrow(() -> new BusinessException(ErrorCode.JWT_TOKEN_NOT_FOUND)); + .orElseThrow(() -> new BusinessException(ErrorCode.JWT_NOT_FOUND_IN_DB)); } public boolean isRefreshHijacked(String identifier, String refreshToken) { - Token token = findTokenByIdentifier(identifier); - return token.getToken().equals(refreshToken); + Token token = findByIdentifier(identifier); + return !token.getToken().equals(refreshToken); } - + public void deleteById(String identifier) { + tokenRepository.deleteById(identifier); + } } diff --git a/src/main/java/com/genius/gitget/global/util/exception/ErrorCode.java b/src/main/java/com/genius/gitget/global/util/exception/ErrorCode.java index f5e1c600..50631286 100644 --- a/src/main/java/com/genius/gitget/global/util/exception/ErrorCode.java +++ b/src/main/java/com/genius/gitget/global/util/exception/ErrorCode.java @@ -42,7 +42,10 @@ public enum ErrorCode { INVALID_JWT(HttpStatus.BAD_REQUEST, "JWT가 유효하지 않습니다."), INVALID_PROGRESS(HttpStatus.BAD_REQUEST, "존재하지 않는 정보입니다."), - JWT_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "Cookie에 토큰이 존재하지 않습니다."), + JWT_NOT_FOUND_IN_DB(HttpStatus.NOT_FOUND, "DB에 JWT 정보가 존재하지 않습니다."), + JWT_NOT_FOUND_IN_HEADER(HttpStatus.NOT_FOUND, "Header에 JWT 정보가 존재하지 않습니다."), + JWT_NOT_FOUND_IN_COOKIE(HttpStatus.NOT_FOUND, "Cookie에 JWT 정보가 존재하지 않습니다."), + REFRESH_TOKEN_NOT_MATCH(HttpStatus.BAD_REQUEST, "DB에 저장되어 있는 Refresh token과 일치하지 않습니다."), MULTIPART_FILE_NOT_EXIST(HttpStatus.BAD_REQUEST, "MultipartFile이 전달되지 않았습니다."), FILE_NOT_EXIST(HttpStatus.BAD_REQUEST, "해당 파일(이미지)이 존재하지 않습니다."), diff --git a/src/test/java/com/genius/gitget/admin/topic/controller/TopicControllerTest.java b/src/test/java/com/genius/gitget/admin/topic/controller/TopicControllerTest.java index fbc14eb4..d9713ddb 100644 --- a/src/test/java/com/genius/gitget/admin/topic/controller/TopicControllerTest.java +++ b/src/test/java/com/genius/gitget/admin/topic/controller/TopicControllerTest.java @@ -58,7 +58,7 @@ public void setup() { Topic savedTopic = getSavedTopic(); Long id = savedTopic.getId(); - mockMvc.perform(get("/api/admin/topic/" + id).cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(get("/api/admin/topic/" + id).headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.title").value("title")); @@ -74,7 +74,7 @@ public void setup() { mockMvc.perform(get("/api/admin/topic") .contentType(MediaType.APPLICATION_JSON) - .cookie(tokenTestUtil.createAccessCookie())) + .headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.numberOfElements").value(3)) @@ -91,7 +91,7 @@ public void setup() { Topic savedTopic = getSavedTopic(); Long id = savedTopic.getId(); - mockMvc.perform(delete("/api/admin/topic/" + id).cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(delete("/api/admin/topic/" + id).headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.numberOfElements").doesNotExist()); @@ -106,7 +106,7 @@ public void setup() { Topic savedTopic = getSavedTopic(); Long id = savedTopic.getId(); - mockMvc.perform(delete("/api/admin/topic/" + id + 1).cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(delete("/api/admin/topic/" + id + 1).headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().is4xxClientError()); } diff --git a/src/test/java/com/genius/gitget/challenge/certification/controller/CertificationControllerTest.java b/src/test/java/com/genius/gitget/challenge/certification/controller/CertificationControllerTest.java index 37c61370..a87c8088 100644 --- a/src/test/java/com/genius/gitget/challenge/certification/controller/CertificationControllerTest.java +++ b/src/test/java/com/genius/gitget/challenge/certification/controller/CertificationControllerTest.java @@ -74,7 +74,7 @@ public void should_saveToken_when_tokenValid() throws Exception { //then mockMvc.perform(post("/api/certification/register/token") - .cookie(tokenTestUtil.createAccessCookie()) + .headers(tokenTestUtil.createAccessHeaders()) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andExpect(status().is2xxSuccessful()); @@ -93,7 +93,7 @@ public void should_throwException_when_unregisteredUser() throws Exception { @WithMockCustomUser(role = Role.NOT_REGISTERED) public void should_throwException_when_JWTNonExist() throws Exception { mockMvc.perform(post("/api/certification/register/token") - .cookie(tokenTestUtil.createAccessCookie())) + .headers(tokenTestUtil.createAccessHeaders())) .andExpect(status().is4xxClientError()); } @@ -106,7 +106,7 @@ public void should_throwException_when_accountIncorrect() throws Exception { //when & then mockMvc.perform(post("/api/certification/register/token") - .cookie(tokenTestUtil.createAccessCookie()) + .headers(tokenTestUtil.createAccessHeaders()) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andExpect(status().is4xxClientError()); @@ -125,7 +125,7 @@ public void should_saveToken_when_repositoryValid() throws Exception { //then mockMvc.perform(get("/api/certification/verify/repository?repo=" + targetRepo) - .cookie(tokenTestUtil.createAccessCookie())) + .headers(tokenTestUtil.createAccessHeaders())) .andExpect(status().is2xxSuccessful()); } diff --git a/src/test/java/com/genius/gitget/challenge/home/controller/InstanceHomeControllerTest.java b/src/test/java/com/genius/gitget/challenge/home/controller/InstanceHomeControllerTest.java index 4a53a3a4..73d1918f 100644 --- a/src/test/java/com/genius/gitget/challenge/home/controller/InstanceHomeControllerTest.java +++ b/src/test/java/com/genius/gitget/challenge/home/controller/InstanceHomeControllerTest.java @@ -58,7 +58,7 @@ public void should_returnInstances_when_passUserTags() throws Exception { //when & then mockMvc.perform(get("/api/challenges/recommend") - .cookie(tokenTestUtil.createAccessCookie())) + .headers(tokenTestUtil.createAccessHeaders())) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.numberOfElements").value(3)); } diff --git a/src/test/java/com/genius/gitget/challenge/instance/controller/InstanceControllerTest.java b/src/test/java/com/genius/gitget/challenge/instance/controller/InstanceControllerTest.java index cc041148..4a35f7b3 100644 --- a/src/test/java/com/genius/gitget/challenge/instance/controller/InstanceControllerTest.java +++ b/src/test/java/com/genius/gitget/challenge/instance/controller/InstanceControllerTest.java @@ -67,7 +67,7 @@ public void setup() { @WithMockCustomUser(role = Role.ADMIN) @DisplayName("인스턴스 리스트 조회를 요청하면, 상태코드 200반환과 함께 인스턴스 리스트를 반환한다.") public void 인스턴스_리스트_조회() throws Exception { - mockMvc.perform(get("/api/admin/instance").cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(get("/api/admin/instance").headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.numberOfElements").value(2)); @@ -79,7 +79,7 @@ public void setup() { public void 특정_토픽에_대한_리스트_조회_1() throws Exception { Long id = savedTopic1.getId(); - mockMvc.perform(get("/api/admin/topic/instances/" + id).cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(get("/api/admin/topic/instances/" + id).headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.numberOfElements").value(2)) @@ -93,7 +93,7 @@ public void setup() { public void 특정_토픽에_대한_리스트_조회_2() throws Exception { Long id = savedTopic2.getId(); - mockMvc.perform(get("/api/admin/topic/instances/" + id).cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(get("/api/admin/topic/instances/" + id).headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("data.numberOfElements").value(0)); @@ -107,7 +107,7 @@ public void setup() { Long instanceId = savedInstance2.getId(); - mockMvc.perform(get("/api/admin/instance/" + instanceId).cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(get("/api/admin/instance/" + instanceId).headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("data.topicId").value(topicId)) @@ -120,7 +120,7 @@ public void setup() { public void 인스턴스_삭제_성공() throws Exception { Long instanceId = savedInstance1.getId(); - mockMvc.perform(delete("/api/admin/instance/" + instanceId).cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(delete("/api/admin/instance/" + instanceId).headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.numberOfElements").doesNotExist()); @@ -134,7 +134,7 @@ public void setup() { public void 인스턴스_삭제_성공_실패() throws Exception { Long instanceId = savedInstance1.getId(); - mockMvc.perform(delete("/api/admin/instance/" + instanceId + 1).cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(delete("/api/admin/instance/" + instanceId + 1).headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().is4xxClientError()); } diff --git a/src/test/java/com/genius/gitget/challenge/likes/controller/LikesControllerTest.java b/src/test/java/com/genius/gitget/challenge/likes/controller/LikesControllerTest.java index 8e0b4ca1..24267550 100644 --- a/src/test/java/com/genius/gitget/challenge/likes/controller/LikesControllerTest.java +++ b/src/test/java/com/genius/gitget/challenge/likes/controller/LikesControllerTest.java @@ -94,7 +94,7 @@ public void setup() { likesService.addLikes(user, "kimdozzi", savedInstance1.getId()); mockMvc.perform(get("/api/profile/likes") - .cookie(tokenTestUtil.createAccessCookie()) + .headers(tokenTestUtil.createAccessHeaders()) .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) @@ -109,7 +109,7 @@ public void setup() { public void 좋아요_목록_조회_성공_2() throws Exception { mockMvc.perform(get("/api/profile/likes") - .cookie(tokenTestUtil.createAccessCookie()) + .headers(tokenTestUtil.createAccessHeaders()) .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(jsonPath("$.data.numberOfElements").value(0)) @@ -129,7 +129,7 @@ public void setup() { .build(); mockMvc.perform(post("/api/profile/likes") - .cookie(tokenTestUtil.createAccessCookie()) + .headers(tokenTestUtil.createAccessHeaders()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -148,7 +148,7 @@ public void setup() { .build(); mockMvc.perform(post("/api/profile/likes") - .cookie(tokenTestUtil.createAccessCookie()) + .headers(tokenTestUtil.createAccessHeaders()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -167,7 +167,7 @@ public void setup() { .build(); mockMvc.perform(post("/api/profile/likes") - .cookie(tokenTestUtil.createAccessCookie()) + .headers(tokenTestUtil.createAccessHeaders()) .content(objectMapper.writeValueAsString(request))) .andDo(print()) .andExpect(status().is4xxClientError()); @@ -187,7 +187,7 @@ public void setup() { Long id = likes.getId(); mockMvc.perform(delete("/api/profile/likes/" + id) - .cookie(tokenTestUtil.createAccessCookie())) + .headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()); @@ -205,7 +205,7 @@ public void setup() { .build()); mockMvc.perform(delete("/api/profile/likes/" + 2) - .cookie(tokenTestUtil.createAccessCookie())) + .headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().is4xxClientError()); } diff --git a/src/test/java/com/genius/gitget/global/security/config/SecurityConfigTest.java b/src/test/java/com/genius/gitget/global/security/config/SecurityConfigTest.java index f36cf928..c1ad59d9 100644 --- a/src/test/java/com/genius/gitget/global/security/config/SecurityConfigTest.java +++ b/src/test/java/com/genius/gitget/global/security/config/SecurityConfigTest.java @@ -61,7 +61,7 @@ public void should_status2xx_when_roleIsAdmin() throws Exception { //when & then mockMvc.perform(get("/api/admin/topic") - .cookie(tokenTestUtil.createAccessCookie())) + .headers(tokenTestUtil.createAccessHeaders())) .andExpect(status().is2xxSuccessful()); } @@ -70,7 +70,7 @@ public void should_status2xx_when_roleIsAdmin() throws Exception { @WithMockCustomUser(role = Role.USER) public void should_status4xx_when_roleNotAdmin() throws Exception { mockMvc.perform(get("/api/admin/topic") - .cookie(tokenTestUtil.createAccessCookie())) + .headers(tokenTestUtil.createAccessHeaders())) .andExpect(status().is4xxClientError()); } } \ No newline at end of file diff --git a/src/test/java/com/genius/gitget/global/security/service/JwtFacadeServiceTest.java b/src/test/java/com/genius/gitget/global/security/service/JwtFacadeServiceTest.java new file mode 100644 index 00000000..d365ac80 --- /dev/null +++ b/src/test/java/com/genius/gitget/global/security/service/JwtFacadeServiceTest.java @@ -0,0 +1,274 @@ +package com.genius.gitget.global.security.service; + +import static com.genius.gitget.global.security.constants.JwtRule.ACCESS_HEADER; +import static com.genius.gitget.global.security.constants.JwtRule.ACCESS_PREFIX; +import static com.genius.gitget.global.security.constants.JwtRule.REFRESH_PREFIX; +import static com.genius.gitget.global.util.exception.ErrorCode.INVALID_JWT; +import static com.genius.gitget.global.util.exception.ErrorCode.JWT_NOT_FOUND_IN_COOKIE; +import static com.genius.gitget.global.util.exception.ErrorCode.JWT_NOT_FOUND_IN_HEADER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.genius.gitget.challenge.user.domain.Role; +import com.genius.gitget.challenge.user.domain.User; +import com.genius.gitget.challenge.user.repository.UserRepository; +import com.genius.gitget.global.security.constants.ProviderInfo; +import com.genius.gitget.global.security.domain.Token; +import com.genius.gitget.global.security.domain.UserPrincipal; +import com.genius.gitget.global.security.repository.TokenRepository; +import com.genius.gitget.global.util.exception.BusinessException; +import jakarta.servlet.http.Cookie; +import java.util.Collection; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@Slf4j +@ActiveProfiles({"jwt"}) +class JwtFacadeServiceTest { + User user; + MockHttpServletRequest request; + MockHttpServletResponse response; + + @Autowired + private TokenRepository tokenRepository; + @Autowired + private JwtFacade jwtFacade; + @Autowired + private UserRepository userRepository; + + @BeforeEach + void setUp() { + user = userRepository.save(User.builder() + .providerInfo(ProviderInfo.GITHUB) + .nickname("nickname") + .identifier("identifier") + .role(Role.USER) + .tags("interest1,interest2") + .information("information") + .build()); + response = new MockHttpServletResponse(); + request = new MockHttpServletRequest(); + } + + @AfterEach + void clearMongo() { + tokenRepository.deleteAll(); + } + + + @Nested + @DisplayName("JWT 생성 시") + class describe_create_jwt { + @Nested + @DisplayName("사용자의 정보를 전달하면") + class context_pass_user_info { + @Test + @DisplayName("Access token을 생성하여 Authorization 헤더에 담는다.") + public void it_returns_headers_that_contain_access() { + String accessToken = jwtFacade.generateAccessToken(response, user); + Collection headerNames = response.getHeaderNames(); + + assertThat(headerNames).contains(ACCESS_HEADER.getValue()); + assertThat(response.getHeader(ACCESS_HEADER.getValue())).contains(accessToken); + } + + @Test + @DisplayName("Refresh token을 생성하여 Cookie에 담는다.") + public void it_returns_cookie_that_contain_refresh() { + String refreshToken = jwtFacade.generateRefreshToken(response, user); + Cookie cookie = response.getCookies()[0]; + + assertThat(cookie.getValue()).isEqualTo(refreshToken); + assertThat(cookie.getSecure()).isTrue(); + assertThat(cookie.getPath()).isEqualTo("/"); + } + } + } + + @Nested + @DisplayName("JWT 유효성 확인 시") + class describe_validate_jwt { + @Nested + @DisplayName("Access token을 전달한 경우") + class context_pass_access { + @Test + @DisplayName("유효기간이 만료되지 않았고, 토큰이 유효하다면 true를 반환한다.") + public void it_returns_true_when_token_not_expired_and_valid() { + String accessToken = jwtFacade.generateAccessToken(response, user); + boolean isAccessValid = jwtFacade.validateAccessToken(accessToken); + assertThat(isAccessValid).isTrue(); + } + + @Test + @DisplayName("토큰이 유효하지 않는다면 BusinessException 예외를 발생한다.") + public void it_throws_BusinessException_when_token_invalid() { + String accessToken = "invalid access token"; + assertThatThrownBy(() -> jwtFacade.validateAccessToken(accessToken)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(INVALID_JWT.getMessage()); + } + } + + @Nested + @DisplayName("Refresh token을 전달한 경우") + class context_pass_refresh { + @Test + @DisplayName("토큰이 유효하고, DB에 저장된 토큰과 같다면 true를 반환한다.") + public void it_returns_true_when_token_valid_and_stored_db() { + String refreshToken = jwtFacade.generateRefreshToken(response, user); + boolean isRefreshValid = jwtFacade.validateRefreshToken(refreshToken, user.getIdentifier()); + assertThat(isRefreshValid).isTrue(); + } + + @Test + @DisplayName("토큰이 유효하지 않는다면 BusinessException 예외를 발생한다.") + public void it_throws_BusinessException_when_token_invalid() { + String refreshToken = "invalid refresh token"; + assertThatThrownBy(() -> jwtFacade.validateRefreshToken(refreshToken, user.getIdentifier())) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(INVALID_JWT.getMessage()); + } + + @Test + @DisplayName("DB에 저장된 토큰과 같지 않다면 false를 반환한다.") + public void it_returns_false_when_not_match_db() { + String refreshToken = jwtFacade.generateRefreshToken(response, user); + tokenRepository.save(new Token(user.getIdentifier(), "invalid refresh token")); + + boolean isRefreshValid = jwtFacade.validateRefreshToken(refreshToken, user.getIdentifier()); + assertThat(isRefreshValid).isFalse(); + } + } + } + + @Nested + @DisplayName("HttpServletRequest로부터") + class describe_from_HttpServletRequest { + @Nested + @DisplayName("access token을 추출하는 경우") + class context_resolve_access { + @Test + @DisplayName("Authorization 헤더에 유효한 토큰이 있는 경우 access token을 반환한다.") + public void it_returns_access_token() { + String accessToken = jwtFacade.generateAccessToken(response, user); + request.addHeader(ACCESS_HEADER.getValue(), ACCESS_PREFIX.getValue() + accessToken); + + String resolvedAccessToken = jwtFacade.resolveAccessToken(request); + assertThat(accessToken).isEqualTo(resolvedAccessToken); + } + + @Test + @DisplayName("Authorization 헤더에 빈 문자열이 있는 경우 BusinessException 예외가 발생한다.") + public void it_throws_BusinessException_when_authorization_is_empty() { + jwtFacade.generateAccessToken(response, user); + request.addHeader(ACCESS_HEADER.getValue(), ""); + assertThatThrownBy(() -> jwtFacade.resolveAccessToken(request)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(JWT_NOT_FOUND_IN_HEADER.getMessage()); + } + + @Test + @DisplayName("Authorization 헤더가 null인 경우 BusinessException 예외가 발생한다.") + public void it_throws_BusinessException_when_authorization_is_null() { + assertThatThrownBy(() -> jwtFacade.resolveAccessToken(request)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(JWT_NOT_FOUND_IN_HEADER.getMessage()); + } + } + + @Nested + @DisplayName("refresh token을 추출하는 경우") + class context_resolve_refresh { + @Test + @DisplayName("Cookie에 유효한 토큰이 있는 경우 Refresh token을 반환한다.") + public void it_returns_refresh_token() { + String refreshToken = jwtFacade.generateRefreshToken(response, user); + + request.setCookies(new Cookie(REFRESH_PREFIX.getValue(), refreshToken)); + String resolvedRefreshToken = jwtFacade.resolveRefreshToken(request); + assertThat(refreshToken).isEqualTo(resolvedRefreshToken); + } + + @Test + @DisplayName("Cookie에 refresh 토큰이 없는 경우 BusinessException 예외가 발생한다.") + public void it_throws_businessException_when_token_not_exist() { + assertThatThrownBy(() -> jwtFacade.resolveRefreshToken(request)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(JWT_NOT_FOUND_IN_COOKIE.getMessage()); + } + } + } + + @Nested + @DisplayName("사용자의 식별자를 확인 시") + class describe_check_user_identifier { + @Nested + @DisplayName("Refresh token을 전달했을 때") + class context_pass_refresh_token { + @Test + @DisplayName("토큰이 유효하다면 사용자의 identifier를 반환한다.") + public void it_returns_identifier_when_refresh_valid() { + String refreshToken = jwtFacade.generateRefreshToken(response, user); + String identifier = jwtFacade.getIdentifierFromRefresh(refreshToken); + assertThat(user.getIdentifier()).isEqualTo(identifier); + } + + @Test + @DisplayName("토큰이 유효하지 않으면 BusinessException 예외가 발생한다.") + public void it_throws_businessException_when_refresh_invalid() { + String invalidRefreshToken = "invalid refresh token"; + assertThatThrownBy(() -> jwtFacade.getIdentifierFromRefresh(invalidRefreshToken)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(INVALID_JWT.getMessage()); + } + } + } + + @Nested + @DisplayName("SecurityContext에 저장할 객체를 받으려 할 때") + class describe_try_to_get_authentication { + @Nested + @DisplayName("Access token을 전달한 경우") + class context_pass_access_token { + @Test + @DisplayName("Authentication를 반환받을 수 있다.") + public void it_returns_identifier() { + String accessToken = jwtFacade.generateAccessToken(response, user); + Authentication authentication = jwtFacade.getAuthentication(accessToken); + + String identifier = ((UserPrincipal) authentication.getPrincipal()).getUser().getIdentifier(); + assertThat(identifier).isEqualTo(user.getIdentifier()); + } + } + } + + @Nested + @DisplayName("로그아웃 요청을 받았을 때") + class describe_logout { + @Nested + @DisplayName("사용자의 식별자 정보를 전달하면") + class context_pass_identifier { + @Test + @DisplayName("cookie를 비우고, DB의 토큰 정보도 삭제한다.") + public void it_clear_cookie_and_db() { + jwtFacade.generateRefreshToken(response, user); + jwtFacade.logout(response, user.getIdentifier()); + + assertThat(tokenRepository.findById(user.getIdentifier())).isNotPresent(); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/com/genius/gitget/global/security/service/JwtServiceTest.java b/src/test/java/com/genius/gitget/global/security/service/JwtServiceTest.java deleted file mode 100644 index bbc6dfc3..00000000 --- a/src/test/java/com/genius/gitget/global/security/service/JwtServiceTest.java +++ /dev/null @@ -1,253 +0,0 @@ -package com.genius.gitget.global.security.service; - -import static com.genius.gitget.global.security.constants.JwtRule.ACCESS_PREFIX; -import static com.genius.gitget.global.security.constants.JwtRule.REFRESH_PREFIX; -import static com.genius.gitget.global.util.exception.ErrorCode.INVALID_JWT; -import static com.genius.gitget.global.util.exception.ErrorCode.JWT_TOKEN_NOT_FOUND; -import static com.genius.gitget.global.util.exception.ErrorCode.NOT_AUTHENTICATED_USER; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.genius.gitget.challenge.user.domain.Role; -import com.genius.gitget.challenge.user.domain.User; -import com.genius.gitget.challenge.user.repository.UserRepository; -import com.genius.gitget.global.security.constants.JwtRule; -import com.genius.gitget.global.security.constants.ProviderInfo; -import com.genius.gitget.global.security.repository.TokenRepository; -import com.genius.gitget.global.util.exception.BusinessException; -import com.genius.gitget.global.util.exception.ErrorCode; -import com.genius.gitget.util.TokenTestUtil; -import com.genius.gitget.util.WithMockCustomUser; -import jakarta.servlet.http.Cookie; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.transaction.annotation.Transactional; - -@SpringBootTest -@Transactional -@Slf4j -@ActiveProfiles({"jwt"}) -class JwtServiceTest { - @Autowired - private TokenRepository tokenRepository; - @Autowired - private JwtService jwtService; - @Autowired - private UserRepository userRepository; - @Autowired - private TokenTestUtil tokenTestUtil; - - @AfterEach - void clearMongo() { - tokenRepository.deleteAll(); - } - - @Test - @DisplayName("사용자 정보를 받아서 access-token을 생성할 수 있다.") - public void should_generateAccess_when_passUserInfo() { - //given - User user = getSavedUser(); - MockHttpServletResponse response = new MockHttpServletResponse(); - - //when - String accessToken = jwtService.generateAccessToken(response, user); - Cookie cookie = response.getCookies()[0]; - - //then - assertThat(cookie.getValue()).isEqualTo(accessToken); - assertThat(cookie.getSecure()).isTrue(); - assertThat(cookie.getPath()).isEqualTo("/"); - } - - @Test - @DisplayName("사용자 정보를 받아서 유효한 refresh-token를 생성할 수 있다.") - public void should_generateRefresh_when_passUserInfo() { - //given - User user = getSavedUser(); - MockHttpServletResponse response = new MockHttpServletResponse(); - - //when - String refreshToken = jwtService.generateRefreshToken(response, user); - Cookie cookie = response.getCookies()[0]; - - //then - assertThat(cookie.getValue()).isEqualTo(refreshToken); - assertThat(cookie.getSecure()).isTrue(); - assertThat(cookie.getPath()).isEqualTo("/"); - } - - @Test - @DisplayName("생성한 access-token이 유효하다면 true를 반환한다.") - public void should_returnTrue_when_accessTokenIsValid() { - //given - User user = getSavedUser(); - MockHttpServletResponse response = new MockHttpServletResponse(); - - //when - String accessToken = jwtService.generateAccessToken(response, user); - boolean isValid = jwtService.validateAccessToken(accessToken); - - //then - assertThat(isValid).isTrue(); - } - - @Test - @DisplayName("생성한 access-token가 유효하지 않다면 false를 반환한다.") - public void should_returnFalse_when_accessTokenIsInvalid() { - //given - - //when - String accessToken = "fake access token"; - - //then - assertThatThrownBy(() -> jwtService.validateAccessToken(accessToken)) - .isInstanceOf(BusinessException.class); - } - - @Test - @DisplayName("Cookie에서 access-token을 추출할 수 있다.") - public void should_extractAccessToken_when_passTokenType() { - //given - User user = getSavedUser(); - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - - //when - String accessToken = jwtService.generateAccessToken(response, user); - - request.setCookies(new Cookie(ACCESS_PREFIX.getValue(), accessToken)); - String resolvedToken = jwtService.resolveTokenFromCookie(request, ACCESS_PREFIX); - - //then - assertThat(accessToken).isEqualTo(resolvedToken); - } - - @Test - @DisplayName("Cookie에서 refresh-token을 추출할 수 있다.") - public void should_extractRefreshToken_when_passTokenType() { - //given - User user = getSavedUser(); - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - - //when - String refreshToken = jwtService.generateRefreshToken(response, user); - - request.setCookies(new Cookie(REFRESH_PREFIX.getValue(), refreshToken)); - String resolvedToken = jwtService.resolveTokenFromCookie(request, REFRESH_PREFIX); - - //then - assertThat(refreshToken).isEqualTo(resolvedToken); - } - - @Test - @DisplayName("Access-token은 없고 유효한 Refresh-token만 있을 때, Access-token을 추출한다면 \"\"을 반환해야 한다.") - @WithMockCustomUser - public void should_returnBlank_whenOnlyValidRefreshToken() { - //given - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setCookies(tokenTestUtil.createRefreshCookie()); - - //when - JwtRule accessTokenPrefix = ACCESS_PREFIX; - String resolvedToken = jwtService.resolveTokenFromCookie(request, accessTokenPrefix); - - //then - assertThat(resolvedToken).isEqualTo(""); - } - - @Test - @DisplayName("Cookie에 아무런 JWT 토큰이 존재하지 않는다면 예외를 발생해야 한다.") - public void should_throwException_when_noTokens() { - //given - MockHttpServletRequest request = new MockHttpServletRequest(); - - //when - JwtRule refreshTokenPrefix = REFRESH_PREFIX; - - //then - assertThatThrownBy(() -> jwtService.resolveTokenFromCookie(request, refreshTokenPrefix)) - .isInstanceOf(BusinessException.class) - .hasMessageContaining(JWT_TOKEN_NOT_FOUND.getMessage()); - } - - @Test - @DisplayName("사용자가 아직 가입하지 않은 회원이 JWT 발급을 요청한다면, 예외를 발생시킨다.") - public void should_throwException_when_userIsNotRegistered() { - //given - User user = getUnregisteredUser(); - - //when & then - assertThatThrownBy(() -> jwtService.validateUser(user)) - .isInstanceOf(BusinessException.class) - .hasMessageContaining(NOT_AUTHENTICATED_USER.getMessage()); - } - - @Test - @DisplayName("Refresh-token으로부터 사용자 식별 정보인 Identifier를 받아올 수 있다.") - @WithMockCustomUser(identifier = "testIdentifier") - public void should_getIdentifier_when_passRefreshToken() { - //given - String refreshToken = tokenTestUtil.createRefreshToken(); - - //when - String identifier = jwtService.getIdentifierFromRefresh(refreshToken); - - //then - assertThat(identifier).isEqualTo("testIdentifier"); - } - - @ParameterizedTest(name = "Refresh-token: {0}") - @DisplayName("Refresh-token이 빈 문자열이거나, 유효하지 않은 문자열이라면 예외가 발생해야 한다.") - @ValueSource(strings = {"invalid refresh token", ""}) - public void should_throwException_when_refreshTokenInvalid(String invalidRefreshToken) { - //given - - //when&then - assertThatThrownBy(() -> jwtService.getIdentifierFromRefresh(invalidRefreshToken)) - .isInstanceOf(BusinessException.class) - .hasMessageContaining(ErrorCode.INVALID_JWT.getMessage()); - } - - @Test - @DisplayName("Refresh-token이 유효하지 않는다면 예외가 발생한다.") - @WithMockCustomUser - public void should_throwException_when_refreshTokenInvalid() { - //given - String refreshToken = "invalid refresh token"; - - //when - assertThatThrownBy(() -> jwtService.validateRefreshToken(refreshToken, "identifier")) - .isInstanceOf(BusinessException.class) - .hasMessageContaining(INVALID_JWT.getMessage()); - } - - - private User getSavedUser() { - return userRepository.save(User.builder() - .providerInfo(ProviderInfo.GITHUB) - .nickname("nickname") - .identifier("identifier") - .role(Role.USER) - .tags("interest1,interest2") - .information("information") - .build()); - } - - private User getUnregisteredUser() { - return userRepository.save(User.builder() - .providerInfo(ProviderInfo.GITHUB) - .identifier("identifier") - .role(Role.NOT_REGISTERED) - .build()); - } -} \ No newline at end of file diff --git a/src/test/java/com/genius/gitget/global/security/service/JwtUtilTest.java b/src/test/java/com/genius/gitget/global/security/service/JwtUtilTest.java index 43b8c2f0..07163db9 100644 --- a/src/test/java/com/genius/gitget/global/security/service/JwtUtilTest.java +++ b/src/test/java/com/genius/gitget/global/security/service/JwtUtilTest.java @@ -22,7 +22,7 @@ public void should_returnResetCookie() { //given //when - Cookie cookie = jwtUtil.resetToken(JwtRule.ACCESS_PREFIX); + Cookie cookie = jwtUtil.resetCookie(JwtRule.REFRESH_PREFIX); //then assertThat(cookie.getMaxAge()).isEqualTo(0); diff --git a/src/test/java/com/genius/gitget/global/security/service/TokenServiceTest.java b/src/test/java/com/genius/gitget/global/security/service/TokenServiceTest.java index b2cc263d..22aa3a9e 100644 --- a/src/test/java/com/genius/gitget/global/security/service/TokenServiceTest.java +++ b/src/test/java/com/genius/gitget/global/security/service/TokenServiceTest.java @@ -1,11 +1,16 @@ package com.genius.gitget.global.security.service; +import static com.genius.gitget.global.util.exception.ErrorCode.JWT_NOT_FOUND_IN_DB; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.genius.gitget.global.security.domain.Token; import com.genius.gitget.global.security.repository.TokenRepository; +import com.genius.gitget.global.util.exception.BusinessException; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -14,74 +19,83 @@ @SpringBootTest @Transactional class TokenServiceTest { + private String identifier = "identifier"; + private String refreshToken = "refresh token example"; + private Token token; + @Autowired private TokenService tokenService; @Autowired private TokenRepository tokenRepository; + + @BeforeEach + void setup() { + token = tokenRepository.save(new Token(identifier, refreshToken)); + } + @AfterEach void clearMongo() { tokenRepository.deleteAll(); } - @Test - @DisplayName("특정 리프레시 토큰을 identifier를 통해 DB에서 값을 조회할 수 있어야 한다.") - public void should_findToken_when_findByIdentifier() { - //given - String identifier = "SSung023"; - String refreshToken = "refresh token example"; - Token token = Token.builder() - .identifier(identifier) - .token(refreshToken) - .build(); - - //when - Token savedToken = tokenRepository.save(token); - Token tokenByIdentifier = tokenService.findTokenByIdentifier(identifier); - - //then - assertThat(savedToken.getIdentifier()).isEqualTo(tokenByIdentifier.getIdentifier()); - assertThat(savedToken.getToken()).isEqualTo(tokenByIdentifier.getToken()); - + @Nested + @DisplayName("DB에 저장되어 있는 Token 객체를 찾으려 할 때") + class describe_find_stored_token { + @Nested + @DisplayName("사용자의 식별자(identifier)를 전달하면") + class context_pass_identifier { + @Test + @DisplayName("저장되어 있던 Token 객체를 반환받을 수 있다.") + public void it_returns_stored_Token() { + Token byIdentifier = tokenService.findByIdentifier(identifier); + assertThat(byIdentifier.getIdentifier()).isEqualTo(identifier); + assertThat(byIdentifier.getToken()).isEqualTo(refreshToken); + } + } } - @Test - @DisplayName("리프레시 토큰 요청이 들어왔을 때, identifier-token 짝이 맞게 저장되어 있으면 true를 반환한다.") - public void should_returnTrue_when_tokenValid() { - //given - String identifier = "SSung023"; - String refreshToken = "refresh token example"; - Token token = Token.builder() - .identifier(identifier) - .token(refreshToken) - .build(); + @Nested + @DisplayName("Refresh token 탈취 여부를 확인할 때") + class describe_check_hijack { + @Nested + @DisplayName("사용자의 식별자와 요청받은 토큰을 전달하면") + class context_pass_identifier_and_token { + @Test + @DisplayName("저장되어 있던 토큰와 같으면 false를 반환한다.") + public void it_returns_false_token_same() { + boolean refreshHijacked = tokenService.isRefreshHijacked(identifier, refreshToken); + assertThat(refreshHijacked).isFalse(); + } - //when - tokenRepository.save(token); - boolean isRefreshHijacked = tokenService.isRefreshHijacked(identifier, refreshToken); + @Test + @DisplayName("저장되어 있던 토큰과 다르다면 true를 반환한다.") + public void it_returns_true_token_different() { + String fakeRefreshToken = "fake refresh token"; + tokenRepository.save(new Token(identifier, fakeRefreshToken)); - //then - assertThat(isRefreshHijacked).isTrue(); + boolean refreshHijacked = tokenService.isRefreshHijacked(identifier, refreshToken); + assertThat(refreshHijacked).isTrue(); + } + } } - @Test - @DisplayName("리프레시 토큰 요청이 들어왔을 때, identifier-token 짝이 맞게 저장되어 있으면 true를 반환한다.") - public void should_returnFalse_when_tokenInvalid() { - //given - String identifier = "SSung023"; - String refreshToken = "refresh token example"; - String fakeRefreshToken = "fake refresh token example"; - Token token = Token.builder() - .identifier(identifier) - .token(refreshToken) - .build(); - - //when - tokenRepository.save(token); - boolean isRefreshHijacked = tokenService.isRefreshHijacked(identifier, fakeRefreshToken); + @Nested + @DisplayName("저장되어 있던 토큰을 삭제하고자 할 때") + class describe_delete_token { + @Nested + @DisplayName("사용자의 식별자를 전달하면") + class context_pass_user_identifier { + @Test + @DisplayName("저장되어 있는 토큰을 삭제한다.") + public void it_delete_stored_token() { + tokenService.deleteById(identifier); - //then - assertThat(isRefreshHijacked).isFalse(); + assertThatThrownBy(() -> tokenService.findByIdentifier(identifier)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(JWT_NOT_FOUND_IN_DB.getMessage()); + } + } } } \ No newline at end of file diff --git a/src/test/java/com/genius/gitget/payment/controller/PaymentControllerTest.java b/src/test/java/com/genius/gitget/payment/controller/PaymentControllerTest.java index 8c56692d..1fcf0b4f 100644 --- a/src/test/java/com/genius/gitget/payment/controller/PaymentControllerTest.java +++ b/src/test/java/com/genius/gitget/payment/controller/PaymentControllerTest.java @@ -55,7 +55,7 @@ public void setup() { @DisplayName("결제 내역 조회를 요청하면, 상태코드 200을 반환한다.") public void 결제_내역_조회_성공() throws Exception { - mockMvc.perform(get("/api/payment").cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(get("/api/payment").headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()); } @@ -71,7 +71,7 @@ public void setup() { input.put("pointAmount", 100L); input.put("userEmail", "kimdozzi"); - mockMvc.perform(post("/api/payment/toss").cookie(tokenTestUtil.createAccessCookie()) + mockMvc.perform(post("/api/payment/toss").headers(tokenTestUtil.createAccessHeaders()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(input))) .andDo(print()) @@ -88,7 +88,7 @@ public void setup() { input.put("pointAmount", 100L); input.put("userEmail", "test@gmail.com"); - mockMvc.perform(post("/api/payment/toss").cookie(tokenTestUtil.createAccessCookie()) + mockMvc.perform(post("/api/payment/toss").headers(tokenTestUtil.createAccessHeaders()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(input))) .andDo(print()) @@ -100,7 +100,7 @@ public void setup() { @DisplayName("결제 요청을 실패하면, 상태코드 4xx을 반환한다.") public void 결제_요청_실패_2() throws Exception { - mockMvc.perform(post("/api/payment/toss").cookie(tokenTestUtil.createAccessCookie()) + mockMvc.perform(post("/api/payment/toss").headers(tokenTestUtil.createAccessHeaders()) .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().is4xxClientError()); @@ -116,7 +116,7 @@ public void setup() { input.put("pointAmount", 100L); input.put("userEmail", "test@gmail.com"); - mockMvc.perform(post("/api/payment/toss").cookie(tokenTestUtil.createAccessCookie()) + mockMvc.perform(post("/api/payment/toss").headers(tokenTestUtil.createAccessHeaders()) .content(objectMapper.writeValueAsString(input))) .andDo(print()) .andExpect(status().is4xxClientError()); diff --git a/src/test/java/com/genius/gitget/profile/controller/ProfileControllerTest.java b/src/test/java/com/genius/gitget/profile/controller/ProfileControllerTest.java index 46a2d58c..dba3d842 100644 --- a/src/test/java/com/genius/gitget/profile/controller/ProfileControllerTest.java +++ b/src/test/java/com/genius/gitget/profile/controller/ProfileControllerTest.java @@ -92,7 +92,7 @@ public void setup() { @DisplayName("사용자 상세 정보 조회에 성공하면, 상태 코드 200을 반환한다.") public void 사용자_상세_정보_조회_성공() throws Exception { - mockMvc.perform(get("/api/profile").cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(get("/api/profile").headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()); } @@ -102,7 +102,7 @@ public void setup() { @DisplayName("사용자 상세 정보 조회 시 같은 사용자 정보가 있으면 실패하고, 4xx(IncorrectResultSizeDataAccessException)를 반환한다.") public void 사용자_상세_정보_조회_실패() throws Exception { User user = getSavedUser(); - mockMvc.perform(get("/api/profile").cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(get("/api/profile").headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().is4xxClientError()); } @@ -123,7 +123,7 @@ public void setup() { input.put("userId", id); mockMvc.perform(post("/api/profile") - .cookie(tokenTestUtil.createAccessCookie()) + .headers(tokenTestUtil.createAccessHeaders()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(input))) .andDo(print()) @@ -145,7 +145,7 @@ public void setup() { input.put("userId", id + 1); mockMvc.perform(post("/api/profile") - .cookie(tokenTestUtil.createAccessCookie()) + .headers(tokenTestUtil.createAccessHeaders()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(input))) .andDo(print()) @@ -157,7 +157,7 @@ public void setup() { @WithMockCustomUser(identifier = "kimdozzi") @DisplayName("사용자 관심사 조회에 성공하면, 상태 코드 200을 반환한다.") public void 사용자_관심사_조회_성공() throws Exception { - mockMvc.perform(get("/api/profile/interest").cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(get("/api/profile/interest").headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()); } @@ -171,7 +171,7 @@ public void setup() { Map> input = new HashMap<>(); input.put("tags", new ArrayList<>(Arrays.asList("FE", "BE", "ML"))); - mockMvc.perform(post("/api/profile/interest").cookie(tokenTestUtil.createAccessCookie()) + mockMvc.perform(post("/api/profile/interest").headers(tokenTestUtil.createAccessHeaders()) .content(objectMapper.writeValueAsString(input)) .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) @@ -196,7 +196,7 @@ public void setup() { Map> input = new HashMap<>(); input.put("tags", new ArrayList<>(Arrays.asList("FE", "BE", "ML"))); - mockMvc.perform(post("/api/profile/interest").cookie(tokenTestUtil.createAccessCookie()) + mockMvc.perform(post("/api/profile/interest").headers(tokenTestUtil.createAccessHeaders()) .content(objectMapper.writeValueAsString(input))) .andDo(print()) .andExpect(status().is4xxClientError()); @@ -207,7 +207,7 @@ public void setup() { @DisplayName("사용자 관심사 수정에 실패하면, 상태 코드 4xx을 반환한다.") public void 사용자_관심사_수정_실패_2() throws Exception { - mockMvc.perform(post("/api/profile/interest").cookie(tokenTestUtil.createAccessCookie()) + mockMvc.perform(post("/api/profile/interest").headers(tokenTestUtil.createAccessHeaders()) .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().is4xxClientError()); @@ -219,7 +219,7 @@ public void setup() { @WithMockCustomUser(identifier = "kimdozzi") @DisplayName("사용자 챌린지 현황 조회에 성공하면, 상태 코드 200을 반환한다.") public void 사용자_챌린지_현황_성공() throws Exception { - mockMvc.perform(get("/api/profile/challenges").cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(get("/api/profile/challenges").headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()); } @@ -233,7 +233,7 @@ public void setup() { Map input = new HashMap<>(); input.put("reason", "이용이 불편해서"); - mockMvc.perform(delete("/api/profile").cookie(tokenTestUtil.createAccessCookie()) + mockMvc.perform(delete("/api/profile").headers(tokenTestUtil.createAccessHeaders()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(input))) .andDo(print()) @@ -244,7 +244,7 @@ public void setup() { @WithMockCustomUser(identifier = "kimdozzi") @DisplayName("사용자 탈퇴 사유없이 탈퇴를 요청하면 실패하고, 상태 코드 4xx을 반환한다.") public void 사용자_탈퇴_실패() throws Exception { - mockMvc.perform(delete("/api/profile").cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(delete("/api/profile").headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().is4xxClientError()); } @@ -254,7 +254,7 @@ public void setup() { @WithMockCustomUser(identifier = "kimdozzi") @DisplayName("사용자 포인트 조회에 성공하면, 상태 코드 200을 반환한다.") public void 사용자_포인트_조회_성공() throws Exception { - mockMvc.perform(get("/api/profile/point").cookie(tokenTestUtil.createAccessCookie())) + mockMvc.perform(get("/api/profile/point").headers(tokenTestUtil.createAccessHeaders())) .andDo(print()) .andExpect(status().isOk()); } diff --git a/src/test/java/com/genius/gitget/util/TokenTestUtil.java b/src/test/java/com/genius/gitget/util/TokenTestUtil.java index 16d987ae..44d30ade 100644 --- a/src/test/java/com/genius/gitget/util/TokenTestUtil.java +++ b/src/test/java/com/genius/gitget/util/TokenTestUtil.java @@ -3,31 +3,51 @@ import static com.genius.gitget.global.security.constants.JwtRule.ACCESS_PREFIX; import static com.genius.gitget.global.security.constants.JwtRule.REFRESH_PREFIX; -import com.genius.gitget.global.security.domain.UserPrincipal; -import com.genius.gitget.global.security.service.JwtService; import com.genius.gitget.challenge.user.domain.User; +import com.genius.gitget.global.security.constants.JwtRule; +import com.genius.gitget.global.security.domain.UserPrincipal; +import com.genius.gitget.global.security.service.JwtFacadeService; import jakarta.servlet.http.Cookie; +import java.util.Collections; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; @Component @RequiredArgsConstructor public class TokenTestUtil { - private final JwtService jwtService; + private final JwtFacadeService jwtFacade; - public Cookie createAccessCookie() { + public Cookie createAccessHeader() { UserPrincipal userPrincipal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication() .getPrincipal(); User user = userPrincipal.getUser(); MockHttpServletResponse httpServletResponse = new MockHttpServletResponse(); - String accessCookie = jwtService.generateAccessToken(httpServletResponse, user); + String accessCookie = jwtFacade.generateAccessToken(httpServletResponse, user); return new Cookie(ACCESS_PREFIX.getValue(), accessCookie); } + public HttpHeaders createAccessHeaders() { + UserPrincipal userPrincipal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication() + .getPrincipal(); + User user = userPrincipal.getUser(); + + MockHttpServletResponse httpServletResponse = new MockHttpServletResponse(); + + String accessToken = jwtFacade.generateAccessToken(httpServletResponse, user); + String bearerAccess = ACCESS_PREFIX.getValue() + accessToken; + + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.put(JwtRule.ACCESS_HEADER.getValue(), Collections.singletonList(bearerAccess)); + return HttpHeaders.readOnlyHttpHeaders(headers); + } + public String createAccessToken() { UserPrincipal userPrincipal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication() .getPrincipal(); @@ -35,7 +55,7 @@ public String createAccessToken() { MockHttpServletResponse httpServletResponse = new MockHttpServletResponse(); - return jwtService.generateAccessToken(httpServletResponse, user); + return jwtFacade.generateAccessToken(httpServletResponse, user); } public Cookie createRefreshCookie() { @@ -45,7 +65,7 @@ public Cookie createRefreshCookie() { MockHttpServletResponse httpServletResponse = new MockHttpServletResponse(); - String refreshCookie = jwtService.generateRefreshToken(httpServletResponse, user); + String refreshCookie = jwtFacade.generateRefreshToken(httpServletResponse, user); return new Cookie(REFRESH_PREFIX.getValue(), refreshCookie); } @@ -56,6 +76,6 @@ public String createRefreshToken() { MockHttpServletResponse httpServletResponse = new MockHttpServletResponse(); - return jwtService.generateRefreshToken(httpServletResponse, user); + return jwtFacade.generateRefreshToken(httpServletResponse, user); } }