diff --git a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java index 48009cc82..9b17214ee 100644 --- a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java +++ b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java @@ -25,8 +25,9 @@ import org.springframework.web.client.RestTemplate; /* - * 애플 인증을 위한 OAuth2 클라이언트 - * https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens + * - 애플 인증을 위한 OAuth2 클라이언트 + * - https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens + * - OAuthClient 인터페이스를 사용하는 전략 패턴으로 구현됨 * */ @Component @RequiredArgsConstructor diff --git a/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java index a25743f7d..b65e70417 100644 --- a/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java +++ b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java @@ -23,10 +23,11 @@ import org.springframework.web.util.UriComponentsBuilder; /* - * 카카오 인증을 위한 OAuth2 클라이언트 - * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code - * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token - * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info + * - 카카오 인증을 위한 OAuth2 클라이언트 + * - https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code + * - https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token + * - https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info + * - OAuthClient 인터페이스를 사용하는 전략 패턴으로 구현됨 * */ @Component @RequiredArgsConstructor diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index f5a30bb2f..9fbd2f225 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -10,10 +10,10 @@ import com.example.solidconnection.auth.dto.oauth.OAuthResponse; import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse; import com.example.solidconnection.auth.service.AuthService; -import com.example.solidconnection.auth.service.EmailSignInService; -import com.example.solidconnection.auth.service.EmailSignUpTokenProvider; -import com.example.solidconnection.auth.service.SignUpService; import com.example.solidconnection.auth.service.oauth.OAuthService; +import com.example.solidconnection.auth.service.signin.EmailSignInService; +import com.example.solidconnection.auth.service.signup.EmailSignUpTokenProvider; +import com.example.solidconnection.auth.service.signup.SignUpService; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; import com.example.solidconnection.common.resolver.AuthorizedUser; diff --git a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java index 7c6f4ec04..6b22aa326 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java +++ b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java @@ -3,11 +3,12 @@ import static com.example.solidconnection.common.exception.ErrorCode.REFRESH_TOKEN_NOT_EXISTS; import com.example.solidconnection.auth.controller.config.RefreshTokenCookieProperties; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.token.config.TokenProperties; import com.example.solidconnection.common.exception.CustomException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.time.Duration; import java.util.Arrays; import lombok.RequiredArgsConstructor; import org.springframework.boot.web.server.Cookie.SameSite; @@ -23,15 +24,17 @@ public class RefreshTokenCookieManager { private static final String PATH = "/"; private final RefreshTokenCookieProperties properties; + private final TokenProperties tokenProperties; public void setCookie(HttpServletResponse response, String refreshToken) { - long maxAge = convertExpireTimeToCookieMaxAge(TokenType.REFRESH.getExpireTime()); - setRefreshTokenCookie(response, refreshToken, maxAge); + Duration tokenExpireTime = tokenProperties.refresh().expireTime(); + long cookieMaxAge = convertExpireTimeToCookieMaxAge(tokenExpireTime); + setRefreshTokenCookie(response, refreshToken, cookieMaxAge); } - private long convertExpireTimeToCookieMaxAge(long milliSeconds) { + private long convertExpireTimeToCookieMaxAge(Duration tokenExpireTime) { // jwt의 expireTime 단위인 millisecond를 cookie의 maxAge 단위인 second로 변환 - return milliSeconds / 1000; + return tokenExpireTime.toSeconds(); } public void deleteCookie(HttpServletResponse response) { diff --git a/src/main/java/com/example/solidconnection/auth/domain/AccessToken.java b/src/main/java/com/example/solidconnection/auth/domain/AccessToken.java new file mode 100644 index 000000000..07df18ff6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/domain/AccessToken.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.auth.domain; + +public record AccessToken( + String token +) implements Token { + +} diff --git a/src/main/java/com/example/solidconnection/auth/domain/RefreshToken.java b/src/main/java/com/example/solidconnection/auth/domain/RefreshToken.java new file mode 100644 index 000000000..aa0680ae7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/domain/RefreshToken.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.auth.domain; + +public record RefreshToken( + String token +) implements Token { + +} diff --git a/src/main/java/com/example/solidconnection/auth/domain/SignUpToken.java b/src/main/java/com/example/solidconnection/auth/domain/SignUpToken.java new file mode 100644 index 000000000..aed55920c --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/domain/SignUpToken.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.auth.domain; + +public record SignUpToken( + String token +) implements Token { + +} diff --git a/src/main/java/com/example/solidconnection/auth/service/Subject.java b/src/main/java/com/example/solidconnection/auth/domain/Subject.java similarity index 50% rename from src/main/java/com/example/solidconnection/auth/service/Subject.java rename to src/main/java/com/example/solidconnection/auth/domain/Subject.java index 15e5c6c75..3a0e29448 100644 --- a/src/main/java/com/example/solidconnection/auth/service/Subject.java +++ b/src/main/java/com/example/solidconnection/auth/domain/Subject.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.service; +package com.example.solidconnection.auth.domain; public record Subject( String value diff --git a/src/main/java/com/example/solidconnection/auth/domain/Token.java b/src/main/java/com/example/solidconnection/auth/domain/Token.java new file mode 100644 index 000000000..3613bffb0 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/domain/Token.java @@ -0,0 +1,6 @@ +package com.example.solidconnection.auth.domain; + +public interface Token { + + String token(); +} diff --git a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java deleted file mode 100644 index 560b0e139..000000000 --- a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.solidconnection.auth.domain; - -import lombok.Getter; - -@Getter -public enum TokenType { - - ACCESS("ACCESS:", 1000L * 60 * 60), // 1hour - REFRESH("REFRESH:", 1000L * 60 * 60 * 24 * 90), // 90days - BLACKLIST("BLACKLIST:", ACCESS.expireTime), - SIGN_UP("SIGN_UP:", 1000L * 60 * 10), // 10min - ; - - private final String prefix; - private final long expireTime; - - TokenType(String prefix, long expireTime) { - this.prefix = prefix; - this.expireTime = expireTime; - } - - public String addPrefix(String string) { - return prefix + string; - } -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java b/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java index 972470cca..434198c32 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java @@ -1,6 +1,6 @@ package com.example.solidconnection.auth.dto; -import com.example.solidconnection.auth.service.AccessToken; +import com.example.solidconnection.auth.domain.AccessToken; public record ReissueResponse( String accessToken diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java index b01fdd369..ac9d39290 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java @@ -1,7 +1,7 @@ package com.example.solidconnection.auth.dto; -import com.example.solidconnection.auth.service.AccessToken; -import com.example.solidconnection.auth.service.RefreshToken; +import com.example.solidconnection.auth.domain.AccessToken; +import com.example.solidconnection.auth.domain.RefreshToken; public record SignInResponse( String accessToken, diff --git a/src/main/java/com/example/solidconnection/auth/service/AccessToken.java b/src/main/java/com/example/solidconnection/auth/service/AccessToken.java deleted file mode 100644 index 3456a2171..000000000 --- a/src/main/java/com/example/solidconnection/auth/service/AccessToken.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.solidconnection.auth.service; - -import com.example.solidconnection.siteuser.domain.Role; - -public record AccessToken( - Subject subject, - Role role, - String token -) { - - public AccessToken(String subject, Role role, String token) { - this(new Subject(subject), role, token); - } -} diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index 01c162002..1c9478e80 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -3,6 +3,7 @@ import static com.example.solidconnection.common.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; +import com.example.solidconnection.auth.domain.AccessToken; import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.auth.token.TokenBlackListService; import com.example.solidconnection.common.exception.CustomException; @@ -26,11 +27,9 @@ public class AuthService { * - 엑세스 토큰을 블랙리스트에 추가한다. * - 리프레시 토큰을 삭제한다. * */ - public void signOut(String token) { - SiteUser siteUser = authTokenProvider.parseSiteUser(token); - AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser); - authTokenProvider.deleteRefreshTokenByAccessToken(accessToken); + public void signOut(String accessToken) { tokenBlackListService.addToBlacklist(accessToken); + authTokenProvider.deleteRefreshTokenByAccessToken(accessToken); } /* diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java index 8e55f77d4..ef188833b 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java @@ -2,7 +2,10 @@ import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.domain.AccessToken; +import com.example.solidconnection.auth.domain.RefreshToken; +import com.example.solidconnection.auth.domain.Subject; +import com.example.solidconnection.auth.token.config.TokenProperties; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.siteuser.domain.Role; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -10,7 +13,6 @@ import java.util.Map; import java.util.Objects; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @Component @@ -19,26 +21,30 @@ public class AuthTokenProvider { private static final String ROLE_CLAIM_KEY = "role"; - private final RedisTemplate redisTemplate; private final TokenProvider tokenProvider; + private final TokenStorage tokenStorage; private final SiteUserRepository siteUserRepository; + private final TokenProperties tokenProperties; public AccessToken generateAccessToken(SiteUser siteUser) { Subject subject = toSubject(siteUser); Role role = siteUser.getRole(); String token = tokenProvider.generateToken( - subject.value(), + subject, Map.of(ROLE_CLAIM_KEY, role.name()), - TokenType.ACCESS + tokenProperties.access().expireTime() ); - return new AccessToken(subject, role, token); + return new AccessToken(token); } public RefreshToken generateAndSaveRefreshToken(SiteUser siteUser) { Subject subject = toSubject(siteUser); - String token = tokenProvider.generateToken(subject.value(), TokenType.REFRESH); - tokenProvider.saveToken(token, TokenType.REFRESH); - return new RefreshToken(subject, token); + String token = tokenProvider.generateToken( + subject, + tokenProperties.refresh().expireTime() + ); + RefreshToken refreshToken = new RefreshToken(token); + return tokenStorage.saveToken(subject, refreshToken); } /* @@ -47,21 +53,20 @@ public RefreshToken generateAndSaveRefreshToken(SiteUser siteUser) { * - 조회된 리프레시 토큰과 요청된 토큰이 같은지 비교한다. * */ public boolean isValidRefreshToken(String requestedRefreshToken) { - String subject = tokenProvider.parseSubject(requestedRefreshToken); - String refreshTokenKey = TokenType.REFRESH.addPrefix(subject); - String foundRefreshToken = redisTemplate.opsForValue().get(refreshTokenKey); - return Objects.equals(requestedRefreshToken, foundRefreshToken); + Subject subject = tokenProvider.parseSubject(requestedRefreshToken); + return tokenStorage.findToken(subject, RefreshToken.class) + .map(foundRefreshToken -> Objects.equals(foundRefreshToken, requestedRefreshToken)) + .orElse(false); } - public void deleteRefreshTokenByAccessToken(AccessToken accessToken) { - String subject = accessToken.subject().value(); - String refreshTokenKey = TokenType.REFRESH.addPrefix(subject); - redisTemplate.delete(refreshTokenKey); + public void deleteRefreshTokenByAccessToken(String accessToken) { + Subject subject = tokenProvider.parseSubject(accessToken); + tokenStorage.deleteToken(subject, RefreshToken.class); } public SiteUser parseSiteUser(String token) { - String subject = tokenProvider.parseSubject(token); - long siteUserId = Long.parseLong(subject); + Subject subject = tokenProvider.parseSubject(token); + long siteUserId = Long.parseLong(subject.value()); return siteUserRepository.findById(siteUserId) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); } diff --git a/src/main/java/com/example/solidconnection/auth/service/RefreshToken.java b/src/main/java/com/example/solidconnection/auth/service/RefreshToken.java deleted file mode 100644 index 2aac3ad8c..000000000 --- a/src/main/java/com/example/solidconnection/auth/service/RefreshToken.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.solidconnection.auth.service; - -public record RefreshToken( - Subject subject, - String token -) { - - RefreshToken(String subject, String token) { - this(new Subject(subject), token); - } -} diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java deleted file mode 100644 index 05480b10d..000000000 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.solidconnection.auth.service; - -import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; -import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; - -import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.auth.token.config.JwtProperties; -import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.siteuser.domain.AuthType; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class SignUpTokenProvider { - - private static final String AUTH_TYPE_CLAIM_KEY = "authType"; - - private final JwtProperties jwtProperties; - private final RedisTemplate redisTemplate; - private final TokenProvider tokenProvider; - - public String generateAndSaveSignUpToken(String email, AuthType authType) { - Map authTypeClaim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, authType)); - Claims claims = Jwts.claims(authTypeClaim).setSubject(email); - Date now = new Date(); - Date expiredDate = new Date(now.getTime() + TokenType.SIGN_UP.getExpireTime()); - - String signUpToken = Jwts.builder() - .setClaims(claims) - .setIssuedAt(now) - .setExpiration(expiredDate) - .signWith(SignatureAlgorithm.HS512, jwtProperties.secret()) - .compact(); - return tokenProvider.saveToken(signUpToken, TokenType.SIGN_UP); - } - - public void deleteByEmail(String email) { - String key = TokenType.SIGN_UP.addPrefix(email); - redisTemplate.delete(key); - } - - public void validateSignUpToken(String token) { - validateFormatAndExpiration(token); - String email = parseEmail(token); - validateIssuedByServer(email); - } - - private void validateFormatAndExpiration(String token) { // 파싱되는지, AuthType이 포함되어있는지 검증 - try { - Claims claims = tokenProvider.parseClaims(token); - Objects.requireNonNull(claims.getSubject()); - String serializedAuthType = claims.get(AUTH_TYPE_CLAIM_KEY, String.class); - AuthType.valueOf(serializedAuthType); - } catch (Exception e) { - throw new CustomException(SIGN_UP_TOKEN_INVALID); - } - } - - private void validateIssuedByServer(String email) { - String key = TokenType.SIGN_UP.addPrefix(email); - if (redisTemplate.opsForValue().get(key) == null) { - throw new CustomException(SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER); - } - } - - public String parseEmail(String token) { - return tokenProvider.parseSubject(token); - } - - public AuthType parseAuthType(String token) { - Claims claims = tokenProvider.parseClaims(token); - String authTypeStr = claims.get(AUTH_TYPE_CLAIM_KEY, String.class); - return AuthType.valueOf(authTypeStr); - } -} diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java index 22120b084..32ce4eb8f 100644 --- a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java @@ -1,18 +1,16 @@ package com.example.solidconnection.auth.service; -import com.example.solidconnection.auth.domain.TokenType; -import io.jsonwebtoken.Claims; +import com.example.solidconnection.auth.domain.Subject; +import java.time.Duration; import java.util.Map; public interface TokenProvider { - String generateToken(String string, TokenType tokenType); + String generateToken(Subject subject, Duration expiration); - String generateToken(String string, Map claims, TokenType tokenType); + String generateToken(Subject subject, Map claims, Duration expiration); - String saveToken(String token, TokenType tokenType); + Subject parseSubject(String token); - String parseSubject(String token); - - Claims parseClaims(String token); + T parseClaims(String token, String claimName, Class claimType); } diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenStorage.java b/src/main/java/com/example/solidconnection/auth/service/TokenStorage.java new file mode 100644 index 000000000..19c3311f1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/TokenStorage.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.Subject; +import com.example.solidconnection.auth.domain.Token; +import java.util.Optional; + +public interface TokenStorage { + + T saveToken(Subject subject, T token); + + Optional findToken(Subject subject, Class tokenClass); + + void deleteToken(Subject subject, Class tokenClass); +} diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java index 9343bfa21..08ab0c0b7 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java @@ -1,13 +1,14 @@ package com.example.solidconnection.auth.service.oauth; +import com.example.solidconnection.auth.domain.SignUpToken; import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; import com.example.solidconnection.auth.dto.oauth.OAuthResponse; import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse; import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; import com.example.solidconnection.auth.dto.oauth.SignUpPrepareResponse; -import com.example.solidconnection.auth.service.SignInService; -import com.example.solidconnection.auth.service.SignUpTokenProvider; +import com.example.solidconnection.auth.service.signin.SignInService; +import com.example.solidconnection.auth.service.signup.SignUpTokenProvider; import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -50,7 +51,7 @@ private OAuthSignInResponse getSignInResponse(SiteUser siteUser) { } private SignUpPrepareResponse getSignUpPrepareResponse(OAuthUserInfoDto userInfoDto, AuthType authType) { - String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), authType); - return SignUpPrepareResponse.of(userInfoDto, signUpToken); + SignUpToken signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), authType); + return SignUpPrepareResponse.of(userInfoDto, signUpToken.token()); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java b/src/main/java/com/example/solidconnection/auth/service/signin/EmailSignInService.java similarity index 96% rename from src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java rename to src/main/java/com/example/solidconnection/auth/service/signin/EmailSignInService.java index 4dac56586..29f379a22 100644 --- a/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/signin/EmailSignInService.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.service; +package com.example.solidconnection.auth.service.signin; import static com.example.solidconnection.common.exception.ErrorCode.SIGN_IN_FAILED; diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/signin/SignInService.java similarity index 78% rename from src/main/java/com/example/solidconnection/auth/service/SignInService.java rename to src/main/java/com/example/solidconnection/auth/service/signin/SignInService.java index 16ec4c484..ee63a02c3 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/signin/SignInService.java @@ -1,6 +1,9 @@ -package com.example.solidconnection.auth.service; +package com.example.solidconnection.auth.service.signin; +import com.example.solidconnection.auth.domain.AccessToken; +import com.example.solidconnection.auth.domain.RefreshToken; import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/signup/EmailSignUpTokenProvider.java similarity index 93% rename from src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java rename to src/main/java/com/example/solidconnection/auth/service/signup/EmailSignUpTokenProvider.java index a3e2e5dc9..0f9d4281a 100644 --- a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/signup/EmailSignUpTokenProvider.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.service; +package com.example.solidconnection.auth.service.signup; import com.example.solidconnection.auth.dto.EmailSignUpTokenRequest; import com.example.solidconnection.common.exception.CustomException; @@ -27,6 +27,6 @@ public String issueEmailSignUpToken(EmailSignUpTokenRequest request) { } passwordTemporaryStorage.save(email, password); - return signUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.EMAIL); + return signUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.EMAIL).token(); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/PasswordTemporaryStorage.java b/src/main/java/com/example/solidconnection/auth/service/signup/PasswordTemporaryStorage.java similarity index 84% rename from src/main/java/com/example/solidconnection/auth/service/PasswordTemporaryStorage.java rename to src/main/java/com/example/solidconnection/auth/service/signup/PasswordTemporaryStorage.java index adcb8bf68..df70465b6 100644 --- a/src/main/java/com/example/solidconnection/auth/service/PasswordTemporaryStorage.java +++ b/src/main/java/com/example/solidconnection/auth/service/signup/PasswordTemporaryStorage.java @@ -1,8 +1,7 @@ -package com.example.solidconnection.auth.service; +package com.example.solidconnection.auth.service.signup; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.token.config.TokenProperties; import java.util.Optional; -import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.crypto.password.PasswordEncoder; @@ -16,14 +15,14 @@ public class PasswordTemporaryStorage { private final RedisTemplate redisTemplate; private final PasswordEncoder passwordEncoder; + private final TokenProperties tokenProperties; public void save(String email, String rawPassword) { String encodedPassword = passwordEncoder.encode(rawPassword); redisTemplate.opsForValue().set( convertToKey(email), encodedPassword, - TokenType.SIGN_UP.getExpireTime(), - TimeUnit.MILLISECONDS + tokenProperties.signUp().expireTime() ); } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/signup/SignUpService.java similarity index 97% rename from src/main/java/com/example/solidconnection/auth/service/SignUpService.java rename to src/main/java/com/example/solidconnection/auth/service/signup/SignUpService.java index d6feed9e1..86415d913 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/signup/SignUpService.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.service; +package com.example.solidconnection.auth.service.signup; import static com.example.solidconnection.common.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; @@ -6,6 +6,7 @@ import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; +import com.example.solidconnection.auth.service.signin.SignInService; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.location.country.service.InterestedCountryService; import com.example.solidconnection.location.region.service.InterestedRegionService; diff --git a/src/main/java/com/example/solidconnection/auth/service/signup/SignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/signup/SignUpTokenProvider.java new file mode 100644 index 000000000..f108365f3 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/signup/SignUpTokenProvider.java @@ -0,0 +1,72 @@ +package com.example.solidconnection.auth.service.signup; + +import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; + +import com.example.solidconnection.auth.domain.SignUpToken; +import com.example.solidconnection.auth.domain.Subject; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.service.TokenStorage; +import com.example.solidconnection.auth.token.config.TokenProperties; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SignUpTokenProvider { + + private static final String AUTH_TYPE_CLAIM_KEY = "authType"; + + private final TokenProvider tokenProvider; + private final TokenStorage tokenStorage; + private final TokenProperties tokenProperties; + + public SignUpToken generateAndSaveSignUpToken(String email, AuthType authType) { + Subject subject = new Subject(email); + String token = tokenProvider.generateToken( + subject, + Map.of(AUTH_TYPE_CLAIM_KEY, authType.toString()), + tokenProperties.signUp().expireTime() + ); + SignUpToken signUpToken = new SignUpToken(token); + return tokenStorage.saveToken(subject, signUpToken); + } + + public void deleteByEmail(String email) { + tokenStorage.deleteToken(new Subject(email), SignUpToken.class); + } + + public void validateSignUpToken(String token) { + validateFormatAndExpiration(token); + validateIssuedByServer(token); + } + + private void validateFormatAndExpiration(String token) { // subject와 claims가 파싱되는지, AuthType이 포함되어있는지 검증 + try { + tokenProvider.parseSubject(token); + String serializedAuthType = tokenProvider.parseClaims(token, AUTH_TYPE_CLAIM_KEY, String.class); + AuthType.valueOf(serializedAuthType); + } catch (Exception e) { + throw new CustomException(SIGN_UP_TOKEN_INVALID); + } + } + + private void validateIssuedByServer(String token) { + String email = parseEmail(token); + tokenStorage.findToken(new Subject(email), SignUpToken.class) + .filter(foundToken -> foundToken.equals(token)) + .orElseThrow(() -> new CustomException(SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER)); + } + + public String parseEmail(String token) { + return tokenProvider.parseSubject(token).value(); + } + + public AuthType parseAuthType(String token) { + String serializedAuthType = tokenProvider.parseClaims(token, AUTH_TYPE_CLAIM_KEY, String.class); + return AuthType.valueOf(serializedAuthType); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/token/JwtTokenProvider.java b/src/main/java/com/example/solidconnection/auth/token/JwtTokenProvider.java index d7c968ccf..262cfdcd3 100644 --- a/src/main/java/com/example/solidconnection/auth/token/JwtTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/token/JwtTokenProvider.java @@ -2,18 +2,17 @@ import static com.example.solidconnection.common.exception.ErrorCode.INVALID_TOKEN; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.domain.Subject; import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.auth.token.config.JwtProperties; import com.example.solidconnection.common.exception.CustomException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; +import java.time.Duration; import java.util.Date; import java.util.Map; -import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @Component @@ -21,23 +20,22 @@ public class JwtTokenProvider implements TokenProvider { private final JwtProperties jwtProperties; - private final RedisTemplate redisTemplate; @Override - public final String generateToken(String string, TokenType tokenType) { - return generateJwtTokenValue(string, Map.of(), tokenType.getExpireTime()); + public String generateToken(Subject subject, Duration expireTime) { + return generateJwtTokenValue(subject.value(), Map.of(), expireTime); } @Override - public String generateToken(String string, Map customClaims, TokenType tokenType) { - return generateJwtTokenValue(string, customClaims, tokenType.getExpireTime()); + public String generateToken(Subject subject, Map customClaims, Duration expireTime) { + return generateJwtTokenValue(subject.value(), customClaims, expireTime); } - private String generateJwtTokenValue(String subject, Map claims, long expireTime) { + private String generateJwtTokenValue(String subject, Map claims, Duration expireTime) { Claims jwtClaims = Jwts.claims().setSubject(subject); jwtClaims.putAll(claims); Date now = new Date(); - Date expiredDate = new Date(now.getTime() + expireTime); + Date expiredDate = new Date(now.getTime() + expireTime.toMillis()); return Jwts.builder() .setClaims(jwtClaims) .setIssuedAt(now) @@ -47,24 +45,20 @@ private String generateJwtTokenValue(String subject, Map claims, } @Override - public final String saveToken(String token, TokenType tokenType) { - String subject = parseSubject(token); - redisTemplate.opsForValue().set( - tokenType.addPrefix(subject), - token, - tokenType.getExpireTime(), - TimeUnit.MILLISECONDS - ); - return token; + public Subject parseSubject(String token) { + String subject = parseJwtClaims(token).getSubject(); + if (subject == null || subject.isBlank()) { + throw new CustomException(INVALID_TOKEN); + } + return new Subject(subject); } @Override - public String parseSubject(String token) { - return parseClaims(token).getSubject(); + public T parseClaims(String token, String claimName, Class claimType) { + return parseJwtClaims(token).get(claimName, claimType); } - @Override - public Claims parseClaims(String token) { + private Claims parseJwtClaims(String token) { try { return Jwts.parser() .setSigningKey(jwtProperties.secret()) diff --git a/src/main/java/com/example/solidconnection/auth/token/RedisTokenStorage.java b/src/main/java/com/example/solidconnection/auth/token/RedisTokenStorage.java new file mode 100644 index 000000000..291bf4ce1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/token/RedisTokenStorage.java @@ -0,0 +1,47 @@ +package com.example.solidconnection.auth.token; + +import com.example.solidconnection.auth.domain.Subject; +import com.example.solidconnection.auth.domain.Token; +import com.example.solidconnection.auth.service.TokenStorage; +import com.example.solidconnection.auth.token.config.TokenProperties; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RedisTokenStorage implements TokenStorage { + + private final RedisTemplate redisTemplate; + + @Override + public T saveToken(Subject subject, T token) { + redisTemplate.opsForValue().set( + createKey(subject, token.getClass()), + token.token(), + TokenProperties.getExpireTime(token.getClass()) + ); + return token; + } + + @Override + public Optional findToken(Subject subject, Class tokenClass) { + String key = createKey(subject, tokenClass); + String foundTokenValue = redisTemplate.opsForValue().get(key); + if (foundTokenValue == null || foundTokenValue.isBlank()) { + return Optional.empty(); + } + return Optional.of(foundTokenValue); + } + + @Override + public void deleteToken(Subject subject, Class tokenClass) { + String key = createKey(subject, tokenClass); + redisTemplate.delete(key); + } + + private String createKey(Subject subject, Class tokenClass) { + return TokenProperties.getStorageKeyPrefix(tokenClass) + ":" + subject.value(); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/token/TokenBlackListService.java b/src/main/java/com/example/solidconnection/auth/token/TokenBlackListService.java index 7f208710c..a2636f383 100644 --- a/src/main/java/com/example/solidconnection/auth/token/TokenBlackListService.java +++ b/src/main/java/com/example/solidconnection/auth/token/TokenBlackListService.java @@ -1,8 +1,6 @@ package com.example.solidconnection.auth.token; -import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; - -import com.example.solidconnection.auth.service.AccessToken; +import com.example.solidconnection.auth.token.config.TokenProperties; import com.example.solidconnection.security.filter.BlacklistChecker; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; @@ -14,6 +12,7 @@ public class TokenBlackListService implements BlacklistChecker { private static final String SIGN_OUT_VALUE = "signOut"; + private final TokenProperties tokenProperties; private final RedisTemplate redisTemplate; /* @@ -21,14 +20,22 @@ public class TokenBlackListService implements BlacklistChecker { * - key = BLACKLIST:{accessToken} * - value = {SIGN_OUT_VALUE} -> key 의 존재만 확인하므로, value 에는 무슨 값이 들어가도 상관없다. * */ - public void addToBlacklist(AccessToken accessToken) { - String blackListKey = BLACKLIST.addPrefix(accessToken.token()); - redisTemplate.opsForValue().set(blackListKey, SIGN_OUT_VALUE); + public void addToBlacklist(String accessToken) { + String blackListKey = createKey(accessToken); + redisTemplate.opsForValue().set( + blackListKey, + SIGN_OUT_VALUE, + tokenProperties.blackList().expireTime() + ); } @Override public boolean isTokenBlacklisted(String accessToken) { - String blackListTokenKey = BLACKLIST.addPrefix(accessToken); - return redisTemplate.hasKey(blackListTokenKey); + String blackListKey = createKey(accessToken); + return redisTemplate.hasKey(blackListKey); + } + + private String createKey(String accessToken) { + return tokenProperties.blackList().storageKeyPrefix() + ":" + accessToken; } } diff --git a/src/main/java/com/example/solidconnection/auth/token/config/TokenConfig.java b/src/main/java/com/example/solidconnection/auth/token/config/TokenConfig.java new file mode 100644 index 000000000..07becbb1d --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/token/config/TokenConfig.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.auth.token.config; + +import java.time.Duration; + +public record TokenConfig( + String storageKeyPrefix, + Duration expireTime +) { + +} diff --git a/src/main/java/com/example/solidconnection/auth/token/config/TokenProperties.java b/src/main/java/com/example/solidconnection/auth/token/config/TokenProperties.java new file mode 100644 index 000000000..cf8a6e9b4 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/token/config/TokenProperties.java @@ -0,0 +1,38 @@ +package com.example.solidconnection.auth.token.config; + +import com.example.solidconnection.auth.domain.AccessToken; +import com.example.solidconnection.auth.domain.RefreshToken; +import com.example.solidconnection.auth.domain.SignUpToken; +import com.example.solidconnection.auth.domain.Token; +import jakarta.annotation.PostConstruct; +import java.time.Duration; +import java.util.Map; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "token") +public record TokenProperties( + TokenConfig access, + TokenConfig refresh, + TokenConfig signUp, + TokenConfig blackList +) { + + private static Map, TokenConfig> tokenConfigs; + + @PostConstruct + public void init() { + tokenConfigs = Map.of( + AccessToken.class, access, + RefreshToken.class, refresh, + SignUpToken.class, signUp + ); + } + + public static Duration getExpireTime(Class tokenClass) { + return tokenConfigs.get(tokenClass).expireTime(); + } + + public static String getStorageKeyPrefix(Class tokenClass) { + return tokenConfigs.get(tokenClass).storageKeyPrefix(); + } +} diff --git a/src/main/java/com/example/solidconnection/security/authentication/TokenAuthenticationProvider.java b/src/main/java/com/example/solidconnection/security/authentication/TokenAuthenticationProvider.java index d0c105884..74783922a 100644 --- a/src/main/java/com/example/solidconnection/security/authentication/TokenAuthenticationProvider.java +++ b/src/main/java/com/example/solidconnection/security/authentication/TokenAuthenticationProvider.java @@ -21,7 +21,7 @@ public Authentication authenticate(Authentication auth) throws AuthenticationExc TokenAuthentication tokenAuth = (TokenAuthentication) auth; String token = tokenAuth.getToken(); - String username = tokenProvider.parseSubject(token); + String username = tokenProvider.parseSubject(token).value(); SiteUserDetails userDetails = (SiteUserDetails) siteUserDetailsService.loadUserByUsername(username); return new TokenAuthentication(token, userDetails); } diff --git a/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java index a5924b860..57b6ad97e 100644 --- a/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java +++ b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java @@ -6,7 +6,7 @@ import static org.mockito.BDDMockito.given; import com.example.solidconnection.auth.controller.config.RefreshTokenCookieProperties; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.token.config.TokenProperties; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; import com.example.solidconnection.support.TestContainerSpringBootTest; @@ -32,6 +32,9 @@ class RefreshTokenCookieManagerTest { @Autowired private RefreshTokenCookieManager cookieManager; + @Autowired + private TokenProperties tokenProperties; + @MockBean private RefreshTokenCookieProperties refreshTokenCookieProperties; @@ -59,7 +62,7 @@ void setUp() { () -> assertThat(header).contains("HttpOnly"), () -> assertThat(header).contains("Secure"), () -> assertThat(header).contains("Path=/"), - () -> assertThat(header).contains("Max-Age=" + TokenType.REFRESH.getExpireTime() / 1000), + () -> assertThat(header).contains("Max-Age=" + tokenProperties.refresh().expireTime().toSeconds()), () -> assertThat(header).contains("Domain=" + domain), () -> assertThat(header).contains("SameSite=" + SameSite.LAX.attributeValue()) ); diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java index caedec489..98454da0f 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java @@ -5,7 +5,9 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.domain.AccessToken; +import com.example.solidconnection.auth.domain.RefreshToken; +import com.example.solidconnection.auth.domain.Subject; import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.auth.token.TokenBlackListService; import com.example.solidconnection.common.exception.CustomException; @@ -19,7 +21,6 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; @DisplayName("인증 서비스 테스트") @TestContainerSpringBootTest @@ -35,7 +36,7 @@ class AuthServiceTest { private TokenBlackListService tokenBlackListService; @Autowired - private RedisTemplate redisTemplate; + private TokenStorage tokenStorage; @Autowired private SiteUserFixture siteUserFixture; @@ -43,13 +44,18 @@ class AuthServiceTest { @Autowired private SiteUserRepository siteUserRepository; + @Autowired + private TokenProvider tokenProvider; + private SiteUser siteUser; private AccessToken accessToken; + private Subject expectedSubject; @BeforeEach void setUp() { siteUser = siteUserFixture.사용자(); accessToken = authTokenProvider.generateAccessToken(siteUser); + expectedSubject = tokenProvider.parseSubject(accessToken.token()); } @Test @@ -58,9 +64,8 @@ void setUp() { authService.signOut(accessToken.token()); // then - String refreshTokenKey = TokenType.REFRESH.addPrefix(accessToken.subject().value()); assertAll( - () -> assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isNull(), + () -> assertThat(tokenStorage.findToken(expectedSubject, RefreshToken.class)).isEmpty(), () -> assertThat(tokenBlackListService.isTokenBlacklisted(accessToken.token())).isTrue() ); } @@ -72,11 +77,10 @@ void setUp() { // then LocalDate tomorrow = LocalDate.now().plusDays(1); - String refreshTokenKey = TokenType.REFRESH.addPrefix(accessToken.subject().value()); SiteUser actualSitUser = siteUserRepository.findById(siteUser.getId()).orElseThrow(); assertAll( () -> assertThat(actualSitUser.getQuitedAt()).isEqualTo(tomorrow), - () -> assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isNull(), + () -> assertThat(tokenStorage.findToken(expectedSubject, RefreshToken.class)).isEmpty(), () -> assertThat(tokenBlackListService.isTokenBlacklisted(accessToken.token())).isTrue() ); } diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java index 54dce4f68..54e5f236c 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java @@ -3,16 +3,19 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.domain.AccessToken; +import com.example.solidconnection.auth.domain.RefreshToken; +import com.example.solidconnection.auth.domain.Subject; +import com.example.solidconnection.siteuser.domain.Role; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; +import java.util.Optional; 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.data.redis.core.RedisTemplate; @TestContainerSpringBootTest @DisplayName("인증 토큰 제공자 테스트") @@ -22,18 +25,21 @@ class AuthTokenProviderTest { private AuthTokenProvider authTokenProvider; @Autowired - private RedisTemplate redisTemplate; + private TokenProvider tokenProvider; + + @Autowired + private TokenStorage tokenStorage; @Autowired private SiteUserFixture siteUserFixture; private SiteUser siteUser; - private String expectedSubject; + private Subject expectedSubject; @BeforeEach void setUp() { siteUser = siteUserFixture.사용자(); - expectedSubject = siteUser.getId().toString(); + expectedSubject = new Subject(siteUser.getId().toString()); } @Test @@ -42,10 +48,13 @@ void setUp() { AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser); // then + String accessTokenValue = accessToken.token(); + Subject actualSubject = tokenProvider.parseSubject(accessTokenValue); + Role actualRole = authTokenProvider.parseSiteUser(accessTokenValue).getRole(); assertAll( - () -> assertThat(accessToken.subject().value()).isEqualTo(expectedSubject), - () -> assertThat(accessToken.role()).isEqualTo(siteUser.getRole()), - () -> assertThat(accessToken.token()).isNotNull() + () -> assertThat(accessTokenValue).isNotNull(), + () -> assertThat(actualSubject).isEqualTo(expectedSubject), + () -> assertThat(actualRole).isEqualTo(siteUser.getRole()) ); } @@ -55,14 +64,14 @@ class 리프레시_토큰을_제공한다 { @Test void 리프레시_토큰을_생성하고_저장한다() { // when - RefreshToken actualRefreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); + RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); // then - String refreshTokenKey = TokenType.REFRESH.addPrefix(expectedSubject); - String expectedRefreshToken = redisTemplate.opsForValue().get(refreshTokenKey); + Subject actualSubject = tokenProvider.parseSubject(refreshToken.token()); + Optional savedRefreshToken = tokenStorage.findToken(expectedSubject, RefreshToken.class); assertAll( - () -> assertThat(actualRefreshToken.subject().value()).isEqualTo(expectedSubject), - () -> assertThat(actualRefreshToken.token()).isEqualTo(expectedRefreshToken) + () -> assertThat(savedRefreshToken).hasValue(refreshToken.token()), + () -> assertThat(actualSubject).isEqualTo(expectedSubject) ); } @@ -86,11 +95,10 @@ class 리프레시_토큰을_제공한다 { AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser); // when - authTokenProvider.deleteRefreshTokenByAccessToken(accessToken); + authTokenProvider.deleteRefreshTokenByAccessToken(accessToken.token()); // then - String refreshTokenKey = TokenType.REFRESH.addPrefix(expectedSubject); - assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isNull(); + assertThat(tokenStorage.findToken(expectedSubject, RefreshToken.class)).isEmpty(); } } diff --git a/src/test/java/com/example/solidconnection/auth/service/JwtTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/JwtTokenProviderTest.java index 62655df2a..506019478 100644 --- a/src/test/java/com/example/solidconnection/auth/service/JwtTokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/JwtTokenProviderTest.java @@ -4,7 +4,7 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.domain.Subject; import com.example.solidconnection.auth.token.JwtTokenProvider; import com.example.solidconnection.auth.token.config.JwtProperties; import com.example.solidconnection.common.exception.CustomException; @@ -13,6 +13,7 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; +import java.time.Duration; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -20,7 +21,6 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; @DisplayName("토큰 제공자 테스트") @TestContainerSpringBootTest @@ -32,68 +32,56 @@ class JwtTokenProviderTest { @Autowired private JwtProperties jwtProperties; - @Autowired - private RedisTemplate redisTemplate; + private final Subject expectedSubject = new Subject("subject123"); + private final Duration expectedExpireTime = Duration.ofMinutes(10); @Nested class 토큰을_생성한다 { @Test void subject_만_있는_토큰을_생성한다() { - // given - String actualSubject = "subject123"; - TokenType actualTokenType = TokenType.ACCESS; - // when - String token = tokenProvider.generateToken(actualSubject, actualTokenType); + String token = tokenProvider.generateToken(expectedSubject, expectedExpireTime); // then - subject와 만료 시간이 일치하는지 검증 - Claims claims = tokenProvider.parseClaims(token); - long expectedExpireTime = claims.getExpiration().getTime() - claims.getIssuedAt().getTime(); + Subject actualSubject = tokenProvider.parseSubject(token); + Duration actualExpireTime = getActualExpireTime(token); assertAll( - () -> assertThat(claims.getSubject()).isEqualTo(actualSubject), - () -> assertThat(expectedExpireTime).isEqualTo(actualTokenType.getExpireTime()) + () -> assertThat(actualSubject).isEqualTo(expectedSubject), + () -> assertThat(actualExpireTime).isEqualTo(expectedExpireTime) ); } @Test void subject_와_claims_가_있는_토큰을_생성한다() { // given - String actualSubject = "subject123"; - Map customClaims = Map.of("key1", "value1", "key2", "value2"); - TokenType actualTokenType = TokenType.ACCESS; + String key1 = "key1"; + String value1 = "value1"; + String key2 = "key2"; + String value2 = "value2"; + Map customClaims = Map.of(key1, value1, key2, value2); // when - String token = tokenProvider.generateToken(actualSubject, customClaims, actualTokenType); + String token = tokenProvider.generateToken(expectedSubject, customClaims, expectedExpireTime); // then - subject와 커스텀 클레임이 일치하는지 검증 - Claims claims = tokenProvider.parseClaims(token); - long expectedExpireTime = claims.getExpiration().getTime() - claims.getIssuedAt().getTime(); + Subject actualSubject = tokenProvider.parseSubject(token); + Duration actualExpireTime = getActualExpireTime(token); assertAll( - () -> assertThat(claims.getSubject()).isEqualTo(actualSubject), - () -> assertThat(claims).containsAllEntriesOf(customClaims), - () -> assertThat(expectedExpireTime).isEqualTo(actualTokenType.getExpireTime()) + () -> assertThat(actualSubject).isEqualTo(expectedSubject), + () -> assertThat(actualExpireTime).isEqualTo(expectedExpireTime), + () -> assertThat(tokenProvider.parseClaims(token, key1, String.class)).isEqualTo(value1), + () -> assertThat(tokenProvider.parseClaims(token, key2, String.class)).isEqualTo(value2) ); } - } - @Test - void 토큰을_저장한다() { - // given - String subject = "subject123"; - TokenType tokenType = TokenType.ACCESS; - String token = tokenProvider.generateToken(subject, tokenType); - - // when - String savedToken = tokenProvider.saveToken(token, tokenType); - - // then - key: "{TokenType.Prefix}:subject", value: {token} 로 저장되어있는지 검증, 반환하는 값이 value와 같은지 검증 - String key = tokenType.addPrefix(subject); - String value = redisTemplate.opsForValue().get(key); - assertAll( - () -> assertThat(value).isEqualTo(token), - () -> assertThat(savedToken).isEqualTo(value) - ); + private Duration getActualExpireTime(String token) { + Claims claims = Jwts.parser() + .setSigningKey(jwtProperties.secret()) + .parseClaimsJws(token) + .getBody(); + return Duration.ofMillis(claims.getExpiration().getTime() - claims.getIssuedAt().getTime()); + } } @Nested @@ -102,14 +90,13 @@ class 토큰으로부터_subject_를_추출한다 { @Test void 유효한_토큰의_subject_를_추출한다() { // given - String subject = "subject000"; - String token = createValidToken(subject); + String token = tokenProvider.generateToken(expectedSubject, expectedExpireTime); // when - String extractedSubject = tokenProvider.parseSubject(token); + Subject actualSubject = tokenProvider.parseSubject(token); // then - assertThat(extractedSubject).isEqualTo(subject); + assertThat(actualSubject).isEqualTo(expectedSubject); } @Test @@ -123,60 +110,76 @@ class 토큰으로부터_subject_를_추출한다 { .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); } + + @Test + void subject_가_없는_토큰의_subject_를_추출하면_예외가_발생한다() { + // given + Claims claims = Jwts.claims(new HashMap<>()); + String subjectNotExistingToken = createExpiredToken(claims); + String subjectBlankToken = tokenProvider.generateToken(new Subject(" "), expectedExpireTime); + + // when, then + assertAll( + () -> assertThatCode(() -> tokenProvider.parseSubject(subjectNotExistingToken)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()), + () -> assertThatCode(() -> tokenProvider.parseSubject(subjectBlankToken)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()) + ); + } } @Nested class 토큰으로부터_claim_을_추출한다 { + private final String claimKey = "key"; + private final String claimValue = "value"; + @Test void 유효한_토큰의_claim_을_추출한다() { // given - String subject = "subject"; - String claimKey = "key"; - String claimValue = "value"; - Claims expectedClaims = Jwts.claims(new HashMap<>(Map.of(claimKey, claimValue))).setSubject(subject); - String token = createValidToken(expectedClaims); + String token = tokenProvider.generateToken( + expectedSubject, + Map.of(claimKey, claimValue), + expectedExpireTime + ); // when - Claims actualClaims = tokenProvider.parseClaims(token); + String actualClaimValue = tokenProvider.parseClaims(token, claimKey, String.class); // then - assertAll( - () -> assertThat(actualClaims.getSubject()).isEqualTo(subject), - () -> assertThat(actualClaims.get(claimKey)).isEqualTo(claimValue) - ); + assertThat(actualClaimValue).isEqualTo(claimValue); } @Test void 유효하지_않은_토큰의_claim_을_추출하면_예외가_발생한다() { // given - String subject = "subject"; - Claims expectedClaims = Jwts.claims().setSubject(subject); + Claims expectedClaims = Jwts.claims(new HashMap<>(Map.of(claimKey, claimValue))); String token = createExpiredToken(expectedClaims); // when - assertThatCode(() -> tokenProvider.parseClaims(token)) + assertThatCode(() -> tokenProvider.parseClaims(token, claimKey, String.class)) .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); } - } - private String createValidToken(String subject) { - return Jwts.builder() - .setSubject(subject) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) - .compact(); - } + @Test + void 존재하지_않는_claim_을_추출하면_null을_반환한다() { + // given + String token = tokenProvider.generateToken( + expectedSubject, + Map.of(claimKey, claimValue), + expectedExpireTime + ); + String nonExistentClaimKey = "nonExistentKey"; - private String createValidToken(Claims claims) { - return Jwts.builder() - .setClaims(claims) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) - .compact(); + // when + String actualClaimValue = tokenProvider.parseClaims(token, nonExistentClaimKey, String.class); + + // then + assertThat(actualClaimValue).isNull(); + } } private String createExpiredToken(String subject) { diff --git a/src/test/java/com/example/solidconnection/auth/service/TokenBlackListServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/TokenBlackListServiceTest.java index 5267f88f3..74d6a9b7f 100644 --- a/src/test/java/com/example/solidconnection/auth/service/TokenBlackListServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/TokenBlackListServiceTest.java @@ -1,10 +1,9 @@ package com.example.solidconnection.auth.service; -import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; import static org.assertj.core.api.Assertions.assertThat; import com.example.solidconnection.auth.token.TokenBlackListService; -import com.example.solidconnection.siteuser.domain.Role; +import com.example.solidconnection.auth.token.config.TokenProperties; import com.example.solidconnection.support.TestContainerSpringBootTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -23,21 +22,23 @@ class TokenBlackListServiceTest { @Autowired private RedisTemplate redisTemplate; - private AccessToken accessToken; + @Autowired + private TokenProperties tokenProperties; + + private String accessToken; @BeforeEach void setUp() { - accessToken = new AccessToken("subject", Role.MENTEE, "token"); + accessToken = "accessToken"; } - @Test void 액세스_토큰을_블랙리스트에_추가한다() { // when tokenBlackListService.addToBlacklist(accessToken); // then - String blackListTokenKey = BLACKLIST.addPrefix(accessToken.token()); + String blackListTokenKey = tokenProperties.blackList().storageKeyPrefix() + ":" + accessToken; String foundBlackListToken = redisTemplate.opsForValue().get(blackListTokenKey); assertThat(foundBlackListToken).isNotNull(); } @@ -51,13 +52,13 @@ class 블랙리스트에_있는_토큰인지_확인한다 { tokenBlackListService.addToBlacklist(accessToken); // when, then - assertThat(tokenBlackListService.isTokenBlacklisted(accessToken.token())).isTrue(); + assertThat(tokenBlackListService.isTokenBlacklisted(accessToken)).isTrue(); } @Test void 블랙리스트에_토큰이_없는_경우() { // when, then - assertThat(tokenBlackListService.isTokenBlacklisted(accessToken.token())).isFalse(); + assertThat(tokenBlackListService.isTokenBlacklisted(accessToken)).isFalse(); } } } diff --git a/src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/signin/EmailSignInServiceTest.java similarity index 97% rename from src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java rename to src/test/java/com/example/solidconnection/auth/service/signin/EmailSignInServiceTest.java index 04b6780ad..46c6d565a 100644 --- a/src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/signin/EmailSignInServiceTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.service; +package com.example.solidconnection.auth.service.signin; import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; diff --git a/src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/signin/SignInServiceTest.java similarity index 65% rename from src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java rename to src/test/java/com/example/solidconnection/auth/service/signin/SignInServiceTest.java index da06aa3e4..957c5c3a1 100644 --- a/src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/signin/SignInServiceTest.java @@ -1,19 +1,22 @@ -package com.example.solidconnection.auth.service; +package com.example.solidconnection.auth.service.signin; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.domain.RefreshToken; +import com.example.solidconnection.auth.domain.Subject; import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.service.TokenStorage; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; import java.time.LocalDate; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; @DisplayName("로그인 서비스 테스트") @TestContainerSpringBootTest @@ -26,18 +29,18 @@ class SignInServiceTest { private TokenProvider tokenProvider; @Autowired - private RedisTemplate redisTemplate; + private TokenStorage tokenStorage; @Autowired private SiteUserFixture siteUserFixture; private SiteUser user; - private String subject; + private Subject subject; @BeforeEach void setUp() { user = siteUserFixture.사용자(); - subject = user.getId().toString(); + subject = new Subject(user.getId().toString()); } @Test @@ -46,13 +49,13 @@ void setUp() { SignInResponse signInResponse = signInService.signIn(user); // then - String accessTokenSubject = tokenProvider.parseSubject(signInResponse.accessToken()); - String refreshTokenSubject = tokenProvider.parseSubject(signInResponse.refreshToken()); - String savedRefreshToken = redisTemplate.opsForValue().get(TokenType.REFRESH.addPrefix(refreshTokenSubject)); + Subject accessTokenSubject = tokenProvider.parseSubject(signInResponse.accessToken()); + Subject refreshTokenSubject = tokenProvider.parseSubject(signInResponse.refreshToken()); + Optional savedRefreshToken = tokenStorage.findToken(subject, RefreshToken.class); assertAll( () -> assertThat(accessTokenSubject).isEqualTo(subject), () -> assertThat(refreshTokenSubject).isEqualTo(subject), - () -> assertThat(savedRefreshToken).isEqualTo(signInResponse.refreshToken())); + () -> assertThat(savedRefreshToken).hasValue(signInResponse.refreshToken())); } @Test diff --git a/src/test/java/com/example/solidconnection/auth/service/PasswordTemporaryStorageTest.java b/src/test/java/com/example/solidconnection/auth/service/signup/PasswordTemporaryStorageTest.java similarity index 96% rename from src/test/java/com/example/solidconnection/auth/service/PasswordTemporaryStorageTest.java rename to src/test/java/com/example/solidconnection/auth/service/signup/PasswordTemporaryStorageTest.java index ea3ed6355..2a56f94b3 100644 --- a/src/test/java/com/example/solidconnection/auth/service/PasswordTemporaryStorageTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/signup/PasswordTemporaryStorageTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.service; +package com.example.solidconnection.auth.service.signup; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/signup/SignUpTokenProviderTest.java similarity index 61% rename from src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java rename to src/test/java/com/example/solidconnection/auth/service/signup/SignUpTokenProviderTest.java index c75eac5f5..aff6a50d4 100644 --- a/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/signup/SignUpTokenProviderTest.java @@ -1,28 +1,33 @@ -package com.example.solidconnection.auth.service; +package com.example.solidconnection.auth.service.signup; import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; +import static java.util.Optional.empty; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.BDDMockito.given; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.domain.SignUpToken; +import com.example.solidconnection.auth.domain.Subject; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.service.TokenStorage; import com.example.solidconnection.auth.token.config.JwtProperties; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.support.TestContainerSpringBootTest; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; +import java.time.Duration; import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.Optional; 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.data.redis.core.RedisTemplate; +import org.springframework.boot.test.mock.mockito.SpyBean; @TestContainerSpringBootTest @DisplayName("회원가입 토큰 제공자 테스트") @@ -34,30 +39,30 @@ class SignUpTokenProviderTest { @Autowired private TokenProvider tokenProvider; - @Autowired - private RedisTemplate redisTemplate; + @SpyBean + private TokenStorage tokenStorage; @Autowired private JwtProperties jwtProperties; private final String authTypeClaimKey = "authType"; private final String email = "test@email.com"; + private final Subject subject = new Subject(email); private final AuthType authType = AuthType.KAKAO; @Test void 회원가입_토큰을_생성하고_저장한다() { // when - String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email, authType); + String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email, authType).token(); // then - Claims claims = tokenProvider.parseClaims(signUpToken); - String actualSubject = claims.getSubject(); - AuthType actualAuthType = AuthType.valueOf(claims.get(authTypeClaimKey, String.class)); - String signUpTokenKey = TokenType.SIGN_UP.addPrefix(email); + Subject actualSubject = tokenProvider.parseSubject(signUpToken); + String actualAuthType = tokenProvider.parseClaims(signUpToken, authTypeClaimKey, String.class); + Optional actualSavedToken = tokenStorage.findToken(actualSubject, SignUpToken.class); assertAll( - () -> assertThat(actualSubject).isEqualTo(email), - () -> assertThat(actualAuthType).isEqualTo(authType), - () -> assertThat(redisTemplate.opsForValue().get(signUpTokenKey)).isEqualTo(signUpToken) + () -> assertThat(actualSubject.value()).isEqualTo(email), + () -> assertThat(actualAuthType).isEqualTo(authType.toString()), + () -> assertThat(actualSavedToken).hasValue(signUpToken) ); } @@ -70,8 +75,7 @@ class SignUpTokenProviderTest { signUpTokenProvider.deleteByEmail(email); // then - String signUpTokenKey = TokenType.SIGN_UP.addPrefix(email); - assertThat(redisTemplate.opsForValue().get(signUpTokenKey)).isNull(); + assertThat(tokenStorage.findToken(subject, SignUpToken.class)).isEmpty(); } @Nested @@ -80,9 +84,7 @@ class 주어진_회원가입_토큰을_검증한다 { @Test void 검증_성공한다() { // given - Map claim = new HashMap<>(Map.of(authTypeClaimKey, authType)); - String validToken = createBaseJwtBuilder().setSubject(email).addClaims(claim).compact(); - redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), validToken); + String validToken = signUpTokenProvider.generateAndSaveSignUpToken(email, authType).token(); // when & then assertThatCode(() -> signUpTokenProvider.validateSignUpToken(validToken)).doesNotThrowAnyException(); @@ -114,8 +116,8 @@ class 주어진_회원가입_토큰을_검증한다 { void 정해진_형식에_맞지_않으면_예외가_발생한다_authType_클래스_불일치() { // given String wrongAuthType = "카카오"; - Map wrongClaim = new HashMap<>(Map.of(authTypeClaimKey, wrongAuthType)); - String wrongAuthTypeClaim = createBaseJwtBuilder().addClaims(wrongClaim).compact(); + Map wrongClaim = new HashMap<>(Map.of(authTypeClaimKey, wrongAuthType)); + String wrongAuthTypeClaim = tokenProvider.generateToken(subject, wrongClaim, Duration.ofMinutes(10)); // when & then assertThatCode(() -> signUpTokenProvider.validateSignUpToken(wrongAuthTypeClaim)) @@ -123,23 +125,11 @@ class 주어진_회원가입_토큰을_검증한다 { .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); } - @Test - void 정해진_형식에_맞지_않으면_예외가_발생한다_subject_누락() { - // given - Map claim = new HashMap<>(Map.of(authTypeClaimKey, authType)); - String noSubject = createBaseJwtBuilder().addClaims(claim).compact(); - - // when & then - assertThatCode(() -> signUpTokenProvider.validateSignUpToken(noSubject)) - .isInstanceOf(CustomException.class) - .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); - } - @Test void 우리_서버에_발급된_토큰이_아니면_예외가_발생한다() { // given - Map validClaim = new HashMap<>(Map.of(authTypeClaimKey, authType)); - String signUpToken = createBaseJwtBuilder().addClaims(validClaim).setSubject(email).compact(); + String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email, authType).token(); + given(tokenStorage.findToken(subject, SignUpToken.class)).willReturn(empty()); // when & then assertThatCode(() -> signUpTokenProvider.validateSignUpToken(signUpToken)) @@ -151,12 +141,10 @@ class 주어진_회원가입_토큰을_검증한다 { @Test void 회원가입_토큰에서_이메일을_추출한다() { // given - Map claim = Map.of(authTypeClaimKey, authType); - String validToken = createBaseJwtBuilder().setSubject(email).addClaims(claim).compact(); - redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), validToken); + String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email, authType).token(); // when - String extractedEmail = signUpTokenProvider.parseEmail(validToken); + String extractedEmail = signUpTokenProvider.parseEmail(signUpToken); // then assertThat(extractedEmail).isEqualTo(email); @@ -165,11 +153,10 @@ class 주어진_회원가입_토큰을_검증한다 { @Test void 회원가입_토큰에서_인증_타입을_추출한다() { // given - Map claim = Map.of(authTypeClaimKey, authType); - String validToken = createBaseJwtBuilder().setSubject(email).addClaims(claim).compact(); + String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email, authType).token(); // when - AuthType extractedAuthType = signUpTokenProvider.parseAuthType(validToken); + AuthType extractedAuthType = signUpTokenProvider.parseAuthType(signUpToken); // then assertThat(extractedAuthType).isEqualTo(authType); @@ -183,11 +170,4 @@ private String createExpiredToken() { .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) .compact(); } - - private JwtBuilder createBaseJwtBuilder() { - return Jwts.builder() - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()); - } } diff --git a/src/test/java/com/example/solidconnection/auth/token/RedisTokenStorageTest.java b/src/test/java/com/example/solidconnection/auth/token/RedisTokenStorageTest.java new file mode 100644 index 000000000..ff06979b2 --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/token/RedisTokenStorageTest.java @@ -0,0 +1,90 @@ +package com.example.solidconnection.auth.token; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.example.solidconnection.auth.domain.AccessToken; +import com.example.solidconnection.auth.domain.Subject; +import com.example.solidconnection.auth.domain.Token; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import java.time.Duration; +import java.util.Optional; +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; + +@TestContainerSpringBootTest +@DisplayName("Redis 토큰 저장소 테스트") +class RedisTokenStorageTest { + + @Autowired + private RedisTokenStorage redisTokenStorage; + + @Autowired + private TokenProvider tokenProvider; + + private Subject subject; + private Token expectedToken; + private Class tokenClass; + + @BeforeEach + void setUp() { + subject = new Subject("subject123"); + expectedToken = new AccessToken(tokenProvider.generateToken(subject, Duration.ofMinutes(10))); + tokenClass = expectedToken.getClass(); + } + + @Test + void 토큰을_저장한다() { + // when + Token savedToken = redisTokenStorage.saveToken(subject, expectedToken); + + // then + Optional foundToken = redisTokenStorage.findToken(subject, tokenClass); + assertAll( + () -> assertThat(foundToken).hasValue(expectedToken.token()), + () -> assertThat(savedToken).isEqualTo(expectedToken) + ); + } + + @Nested + class 토큰을_조회한다 { + + @Test + void 저장된_토큰이_있으면_Optional에_담아_반한다() { + // given + redisTokenStorage.saveToken(subject, expectedToken); + + // when + Optional actualToken = redisTokenStorage.findToken(subject, tokenClass); + + // then + assertThat(actualToken).hasValue(expectedToken.token()); + } + + @Test + void 저장된_토큰이_없으면_빈_Optional을_반환한다() { + // when + Optional foundToken = redisTokenStorage.findToken(subject, tokenClass); + + // then + assertThat(foundToken).isEmpty(); + } + } + + @Test + void 토큰을_삭제한다() { + // given + redisTokenStorage.saveToken(subject, expectedToken); + + // when + redisTokenStorage.deleteToken(subject, tokenClass); + + // then + Optional foundToken = redisTokenStorage.findToken(subject, tokenClass); + assertThat(foundToken).isEmpty(); + } +} diff --git a/src/test/java/com/example/solidconnection/security/filter/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/security/filter/SignOutCheckFilterTest.java index 3d78f1307..2667671cd 100644 --- a/src/test/java/com/example/solidconnection/security/filter/SignOutCheckFilterTest.java +++ b/src/test/java/com/example/solidconnection/security/filter/SignOutCheckFilterTest.java @@ -1,12 +1,12 @@ package com.example.solidconnection.security.filter; -import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; import static com.example.solidconnection.common.exception.ErrorCode.USER_ALREADY_SIGN_OUT; import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.spy; import com.example.solidconnection.auth.token.config.JwtProperties; +import com.example.solidconnection.auth.token.config.TokenProperties; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.support.TestContainerSpringBootTest; import io.jsonwebtoken.Jwts; @@ -37,6 +37,9 @@ class SignOutCheckFilterTest { @Autowired private JwtProperties jwtProperties; + @Autowired + private TokenProperties tokenProperties; + private HttpServletRequest request; private HttpServletResponse response; private FilterChain filterChain; @@ -58,7 +61,7 @@ void setUp() { // given String token = createToken(subject); request = createRequest(token); - String refreshTokenKey = BLACKLIST.addPrefix(token); + String refreshTokenKey = tokenProperties.blackList().storageKeyPrefix() + ":" + token; redisTemplate.opsForValue().set(refreshTokenKey, token); // when & then diff --git a/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java b/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java index b39c91ece..330b084dd 100644 --- a/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java @@ -4,7 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.example.solidconnection.auth.service.AccessToken; +import com.example.solidconnection.auth.domain.AccessToken; import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index ce5a848cb..9d255b46b 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -86,5 +86,16 @@ cors: news: default-thumbnail-url: "default-thumbnail-url" token: + access: + storage-key-prefix: "ACCESS" + expire-time: 10m refresh: cookie-domain: "test.domain.com" + storage-key-prefix: "REFRESH" + expire-time: 10m + sign-up: + storage-key-prefix: "SIGN_UP" + expire-time: 10m + black-list: + storage-key-prefix: "BLACKLIST" + expire-time: 10m