Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,10 @@ public ResponseEntity<Void> quit(

@PostMapping("/reissue")
public ResponseEntity<ReissueResponse> reissueToken(
@AuthorizedUser long siteUserId,
@Valid @RequestBody ReissueRequest reissueRequest
) {
ReissueResponse reissueResponse = authService.reissue(reissueRequest);
ReissueResponse reissueResponse = authService.reissue(siteUserId, reissueRequest);
return ResponseEntity.ok(reissueResponse);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
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, String token) {
this(new Subject(subject), token);
public AccessToken(String subject, Role role, String token) {
this(new Subject(subject), role, token);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ public class AuthService {
* - 리프레시 토큰을 삭제한다.
* */
public void signOut(String token) {
AccessToken accessToken = authTokenProvider.toAccessToken(token);
Subject subject = authTokenProvider.parseSubject(token);
long siteUserId = Long.parseLong(subject.value());
SiteUser siteUser = siteUserRepository.findById(siteUserId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));

AccessToken accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole());
authTokenProvider.deleteRefreshTokenByAccessToken(accessToken);
tokenBlackListService.addToBlacklist(accessToken);
}
Expand All @@ -53,15 +58,17 @@ public void quit(long siteUserId, String token) {
* - 유효한 리프레시토큰이면, 액세스 토큰을 재발급한다.
* - 그렇지 않으면 예외를 발생시킨다.
* */
public ReissueResponse reissue(ReissueRequest reissueRequest) {
public ReissueResponse reissue(long siteUserId, ReissueRequest reissueRequest) {
// 리프레시 토큰 확인
String requestedRefreshToken = reissueRequest.refreshToken();
if (!authTokenProvider.isValidRefreshToken(requestedRefreshToken)) {
throw new CustomException(REFRESH_TOKEN_EXPIRED);
}
// 액세스 토큰 재발급
SiteUser siteUser = siteUserRepository.findById(siteUserId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
Subject subject = authTokenProvider.parseSubject(requestedRefreshToken);
AccessToken newAccessToken = authTokenProvider.generateAccessToken(subject);
AccessToken newAccessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole());
return ReissueResponse.from(newAccessToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.example.solidconnection.auth.service;

import com.example.solidconnection.auth.domain.TokenType;
import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.siteuser.domain.SiteUser;
import java.util.Map;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
Expand All @@ -11,12 +13,16 @@
@RequiredArgsConstructor
public class AuthTokenProvider {

private static final String ROLE_CLAIM_KEY = "role";

private final RedisTemplate<String, String> redisTemplate;
private final TokenProvider tokenProvider;

public AccessToken generateAccessToken(Subject subject) {
String token = tokenProvider.generateToken(subject.value(), TokenType.ACCESS);
return new AccessToken(subject, token);
public AccessToken generateAccessToken(Subject subject, Role role) {
String token = tokenProvider.generateToken(
subject.value(), Map.of(ROLE_CLAIM_KEY, role.name()), TokenType.ACCESS
);
return new AccessToken(subject, role, token);
}

public RefreshToken generateAndSaveRefreshToken(Subject subject) {
Expand Down Expand Up @@ -51,8 +57,4 @@ public Subject parseSubject(String token) {
public Subject toSubject(SiteUser siteUser) {
return new Subject(siteUser.getId().toString());
}

public AccessToken toAccessToken(String token) {
return new AccessToken(parseSubject(token), token);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class SignInService {
public SignInResponse signIn(SiteUser siteUser) {
resetQuitedAt(siteUser);
Subject subject = authTokenProvider.toSubject(siteUser);
AccessToken accessToken = authTokenProvider.generateAccessToken(subject);
AccessToken accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole());
RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(subject);
return SignInResponse.of(accessToken, refreshToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import com.example.solidconnection.auth.domain.TokenType;
import io.jsonwebtoken.Claims;
import java.util.Map;

public interface TokenProvider {

String generateToken(String string, TokenType tokenType);

String generateToken(String string, Map<String, String> claims, TokenType tokenType);

String saveToken(String token, TokenType tokenType);

String parseSubject(String token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
Expand All @@ -24,11 +25,21 @@ public class JwtTokenProvider implements TokenProvider {

@Override
public final String generateToken(String string, TokenType tokenType) {
Claims claims = Jwts.claims().setSubject(string);
return generateJwtTokenValue(string, Map.of(), tokenType.getExpireTime());
}

@Override
public String generateToken(String string, Map<String, String> customClaims, TokenType tokenType) {
return generateJwtTokenValue(string, customClaims, tokenType.getExpireTime());
}

private String generateJwtTokenValue(String subject, Map<String, String> claims, long expireTime) {
Claims jwtClaims = Jwts.claims().setSubject(subject);
jwtClaims.putAll(claims);
Date now = new Date();
Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime());
Date expiredDate = new Date(now.getTime() + expireTime);
return Jwts.builder()
.setClaims(claims)
.setClaims(jwtClaims)
.setIssuedAt(now)
.setExpiration(expiredDate)
.signWith(SignatureAlgorithm.HS512, jwtProperties.secret())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import com.example.solidconnection.support.TestContainerSpringBootTest;
import java.time.LocalDate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -43,12 +44,19 @@ class AuthServiceTest {
@Autowired
private SiteUserRepository siteUserRepository;

private SiteUser siteUser;
private Subject subject;
private AccessToken accessToken;

@BeforeEach
void setUp() {
siteUser = siteUserFixture.사용자();
subject = authTokenProvider.toSubject(siteUser);
accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole());
}

@Test
void 로그아웃한다() {
// given
Subject subject = new Subject("subject");
AccessToken accessToken = authTokenProvider.generateAccessToken(subject);

// when
authService.signOut(accessToken.token());

Expand All @@ -62,18 +70,13 @@ class AuthServiceTest {

@Test
void 탈퇴한다() {
// given
SiteUser user = siteUserFixture.사용자();
Subject subject = authTokenProvider.toSubject(user);
AccessToken accessToken = authTokenProvider.generateAccessToken(subject);

// when
authService.quit(user.getId(), accessToken.token());
authService.quit(siteUser.getId(), accessToken.token());

// then
LocalDate tomorrow = LocalDate.now().plusDays(1);
String refreshTokenKey = TokenType.REFRESH.addPrefix(subject.value());
SiteUser actualSitUser = siteUserRepository.findById(user.getId()).orElseThrow();
SiteUser actualSitUser = siteUserRepository.findById(siteUser.getId()).orElseThrow();
assertAll(
() -> assertThat(actualSitUser.getQuitedAt()).isEqualTo(tomorrow),
() -> assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isNull(),
Expand All @@ -91,7 +94,7 @@ class 토큰을_재발급한다 {
ReissueRequest reissueRequest = new ReissueRequest(refreshToken.token());

// when
ReissueResponse reissuedAccessToken = authService.reissue(reissueRequest);
ReissueResponse reissuedAccessToken = authService.reissue(siteUser.getId(), reissueRequest);

// then - 요청의 리프레시 토큰과 재발급한 액세스 토큰의 subject 가 동일해야 한다.
Subject expectedSubject = authTokenProvider.parseSubject(refreshToken.token());
Expand All @@ -102,11 +105,11 @@ class 토큰을_재발급한다 {
@Test
void 요청의_리프레시_토큰이_저장되어있지_않다면_예외가_발생한다() {
// given
String invalidRefreshToken = authTokenProvider.generateAccessToken(new Subject("subject")).token();
String invalidRefreshToken = accessToken.token();
ReissueRequest reissueRequest = new ReissueRequest(invalidRefreshToken);

// when, then
assertThatCode(() -> authService.reissue(reissueRequest))
assertThatCode(() -> authService.reissue(siteUser.getId(), reissueRequest))
.isInstanceOf(CustomException.class)
.hasMessage(REFRESH_TOKEN_EXPIRED.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertAll;

import com.example.solidconnection.auth.domain.TokenType;
import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.support.TestContainerSpringBootTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
Expand Down Expand Up @@ -32,11 +33,16 @@ void setUp() {
@Test
void 액세스_토큰을_생성한다() {
// when
AccessToken accessToken = authTokenProvider.generateAccessToken(subject);
Role expectedRole = Role.MENTEE;
AccessToken accessToken = authTokenProvider.generateAccessToken(subject, expectedRole);

// then
String actualSubject = authTokenProvider.parseSubject(accessToken.token()).value();
assertThat(actualSubject).isEqualTo(subject.value());
assertAll(
() -> assertThat(actualSubject).isEqualTo(subject.value()),
() -> assertThat(accessToken.role()).isEqualTo(expectedRole),
() -> assertThat(accessToken.token()).isNotNull()
);
}

@Nested
Expand All @@ -61,7 +67,7 @@ class 리프레시_토큰을_제공한다 {
void 유효한_리프레시_토큰인지_확인한다() {
// given
RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(subject);
AccessToken fakeRefreshToken = authTokenProvider.generateAccessToken(subject);
AccessToken fakeRefreshToken = authTokenProvider.generateAccessToken(subject, Role.MENTEE);

// when, then
assertAll(
Expand All @@ -74,7 +80,7 @@ class 리프레시_토큰을_제공한다 {
void 액세스_토큰에_해당하는_리프레시_토큰을_삭제한다() {
// given
authTokenProvider.generateAndSaveRefreshToken(subject);
AccessToken accessToken = authTokenProvider.generateAccessToken(subject);
AccessToken accessToken = authTokenProvider.generateAccessToken(subject, Role.MENTEE);

// when
authTokenProvider.deleteRefreshTokenByAccessToken(accessToken);
Expand All @@ -88,7 +94,7 @@ class 리프레시_토큰을_제공한다 {
@Test
void 토큰으로부터_Subject_를_추출한다() {
// given
String accessToken = authTokenProvider.generateAccessToken(subject).token();
String accessToken = authTokenProvider.generateAccessToken(subject, Role.MENTEE).token();

// when
Subject actualSubject = authTokenProvider.parseSubject(accessToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,46 @@ class JwtTokenProviderTest {
@Autowired
private RedisTemplate<String, String> redisTemplate;

@Test
void 토큰을_생성한다() {
// given
String actualSubject = "subject123";
TokenType actualTokenType = TokenType.ACCESS;
@Nested
class 토큰을_생성한다 {

// when
String token = tokenProvider.generateToken(actualSubject, actualTokenType);
@Test
void subject_만_있는_토큰을_생성한다() {
// given
String actualSubject = "subject123";
TokenType actualTokenType = TokenType.ACCESS;

// then - subject와 만료 시간이 일치하는지 검증
Claims claims = tokenProvider.parseClaims(token);
long expectedExpireTime = claims.getExpiration().getTime() - claims.getIssuedAt().getTime();
assertAll(
() -> assertThat(claims.getSubject()).isEqualTo(actualSubject),
() -> assertThat(expectedExpireTime).isEqualTo(actualTokenType.getExpireTime())
);
// when
String token = tokenProvider.generateToken(actualSubject, actualTokenType);

// then - subject와 만료 시간이 일치하는지 검증
Claims claims = tokenProvider.parseClaims(token);
long expectedExpireTime = claims.getExpiration().getTime() - claims.getIssuedAt().getTime();
assertAll(
() -> assertThat(claims.getSubject()).isEqualTo(actualSubject),
() -> assertThat(expectedExpireTime).isEqualTo(actualTokenType.getExpireTime())
);
}

@Test
void subject_와_claims_가_있는_토큰을_생성한다() {
// given
String actualSubject = "subject123";
Map<String, String> customClaims = Map.of("key1", "value1", "key2", "value2");
TokenType actualTokenType = TokenType.ACCESS;

// when
String token = tokenProvider.generateToken(actualSubject, customClaims, actualTokenType);

// then - subject와 커스텀 클레임이 일치하는지 검증
Claims claims = tokenProvider.parseClaims(token);
long expectedExpireTime = claims.getExpiration().getTime() - claims.getIssuedAt().getTime();
assertAll(
() -> assertThat(claims.getSubject()).isEqualTo(actualSubject),
() -> assertThat(claims).containsAllEntriesOf(customClaims),
() -> assertThat(expectedExpireTime).isEqualTo(actualTokenType.getExpireTime())
);
}
}

@Test
Expand Down
Loading
Loading