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
@@ -0,0 +1,26 @@
package clap.server.adapter.inbound.web.auth;

import clap.server.adapter.inbound.web.dto.auth.ReissueTokenResponse;
import clap.server.application.port.inbound.auth.ReissueTokenUsecase;
import clap.server.common.annotation.architecture.WebAdapter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;

@Tag(name = "토큰 재발급")
@WebAdapter
@RequiredArgsConstructor
@RequestMapping("/api/auths")
public class ReissueTokenController {
private final ReissueTokenUsecase reissueTokenUsecase;

@Operation(summary = "토큰 재발급 API")
@PostMapping("/reissuance")
public ResponseEntity<ReissueTokenResponse> login(@RequestHeader String refreshToken) {
return ResponseEntity.ok(reissueTokenUsecase.reissueToken(refreshToken));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package clap.server.adapter.inbound.web.dto.auth;

public record ReissueTokenResponse(
String accessToken,
String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package clap.server.adapter.outbound.infrastructure.redis.refresh;

import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
Expand All @@ -8,6 +9,7 @@

@Getter
@RedisHash("refreshToken")
@Builder
@ToString(of = {"memberId", "token", "ttl"})
@EqualsAndHashCode(of = {"memberId", "token"})
public class RefreshTokenEntity {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package clap.server.adapter.outbound.infrastructure.redis.refresh;


import clap.server.adapter.outbound.persistense.mapper.MemberPersistenceMapper;
import clap.server.domain.model.auth.RefreshToken;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring", uses = {MemberPersistenceMapper.class})
@Mapper(componentModel = "spring")
public interface RefreshTokenMapper {
@InheritInverseConfiguration
RefreshToken toDomain(final RefreshTokenEntity entity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import clap.server.adapter.inbound.web.dto.auth.LoginResponse;
import clap.server.adapter.inbound.web.dto.auth.MemberInfoResponse;
import clap.server.adapter.inbound.web.dto.auth.ReissueTokenResponse;
import clap.server.domain.model.auth.CustomJwts;
import clap.server.domain.model.member.Member;

public class AuthResponseMapper {
Expand All @@ -26,4 +28,11 @@ public static MemberInfoResponse toMemberInfoResponse(Member member) {
member.getStatus()
);
}

public static ReissueTokenResponse toReissueTokenResponse(final CustomJwts jwtTokens) {
return new ReissueTokenResponse(
jwtTokens.accessToken(),
jwtTokens.refreshToken()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package clap.server.application.port.inbound.auth;

import clap.server.adapter.inbound.web.dto.auth.ReissueTokenResponse;

public interface ReissueTokenUsecase {
ReissueTokenResponse reissueToken(String oldRefreshToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus;
import clap.server.application.mapper.response.AuthResponseMapper;
import clap.server.application.port.inbound.auth.AuthUsecase;
import clap.server.application.port.outbound.auth.CommandRefreshTokenPort;
import clap.server.application.port.outbound.member.LoadMemberPort;
import clap.server.common.annotation.architecture.ApplicationService;
import clap.server.domain.model.auth.CustomJwts;
Expand All @@ -18,6 +19,7 @@
@RequiredArgsConstructor
class AuthService implements AuthUsecase {
private final LoadMemberPort loadMemberPort;
private final CommandRefreshTokenPort commandRefreshTokenPort;
private final IssueTokenService issueTokenService;
private final PasswordEncoder passwordEncoder;

Expand All @@ -29,22 +31,25 @@ public LoginResponse login(String nickname, String password) {
validatePassword(password, member.getPassword());

if (member.getStatus().equals(MemberStatus.APPROVAL_REQUEST)) {
String temporaryToken = issueTokenService.createTemporaryToken(member);
String temporaryToken = issueTokenService.issueTemporaryToken(member.getMemberId());
return AuthResponseMapper.toLoginResponse(
temporaryToken, null, member
);
} else {
CustomJwts jwtTokens = issueTokenService.createToken(member);
CustomJwts jwtTokens = issueTokenService.issueTokens(member);
commandRefreshTokenPort.save(
issueTokenService.issueRefreshToken(member.getMemberId())
);
return AuthResponseMapper.toLoginResponse(
jwtTokens.accessToken(), jwtTokens.refreshToken(), member
);
}
}


private void validatePassword(String inputPassword, String encodedPassword) {
if (!passwordEncoder.matches(inputPassword, encodedPassword)) {
throw new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND);
}
}

}
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
package clap.server.application.service.auth;

import clap.server.adapter.outbound.jwt.JwtClaims;
import clap.server.adapter.outbound.jwt.access.AccessTokenClaim;
import clap.server.adapter.outbound.jwt.access.temporary.TemporaryTokenClaim;
import clap.server.adapter.outbound.jwt.refresh.RefreshTokenClaim;
import clap.server.application.port.outbound.auth.CommandRefreshTokenPort;
import clap.server.adapter.outbound.jwt.refresh.RefreshTokenClaimKeys;
import clap.server.application.port.outbound.auth.JwtProvider;
import clap.server.application.port.outbound.auth.LoadRefreshTokenPort;
import clap.server.domain.model.auth.CustomJwts;
import clap.server.domain.model.auth.RefreshToken;
import clap.server.domain.model.member.Member;
import clap.server.exception.AuthException;
import clap.server.exception.code.AuthErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.function.Function;

@RequiredArgsConstructor
@Component
Expand All @@ -25,56 +24,49 @@ class IssueTokenService {
private final JwtProvider accessTokenProvider;
private final JwtProvider refreshTokenProvider;
private final JwtProvider temporaryTokenProvider;
private final LoadRefreshTokenPort loadRefreshTokenPort;
private final CommandRefreshTokenPort commandRefreshTokenPort;

public CustomJwts createToken(Member member) {
public CustomJwts issueTokens(Member member) {
String accessToken = accessTokenProvider.createToken(AccessTokenClaim.of(member.getMemberId()));
String refreshToken = refreshTokenProvider.createToken(RefreshTokenClaim.of(member.getMemberId()));

commandRefreshTokenPort.save(
RefreshToken.of(
member.getMemberId(), refreshToken,
toSeconds(refreshTokenProvider.getExpiredDate(refreshToken))
)
);

return CustomJwts.of(accessToken, refreshToken);
}

public String createTemporaryToken(Member member) {
return temporaryTokenProvider.createToken(TemporaryTokenClaim.of(member.getMemberId()));
public String issueAccessToken(Long memberId) {
return accessTokenProvider.createToken(AccessTokenClaim.of(memberId));
}

private long toSeconds(LocalDateTime expiredDate) {
return Duration.between(LocalDateTime.now(), expiredDate).getSeconds();
}

public RefreshToken refresh(
Long memberId,
String oldRefreshToken,
String newRefreshToken
) throws IllegalArgumentException, IllegalStateException {
RefreshToken refreshToken = loadRefreshTokenPort.findByMemberId(memberId).orElseThrow(
()-> new AuthException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND)
public RefreshToken issueRefreshToken(Long memberId) {
String refreshToken = refreshTokenProvider.createToken(RefreshTokenClaim.of(memberId));
return RefreshToken.of(
memberId, refreshToken,
toSeconds(refreshTokenProvider.getExpiredDate(refreshToken))
);
validateToken(oldRefreshToken, refreshToken);
}

public String issueTemporaryToken(Long memberId) {
return temporaryTokenProvider.createToken(TemporaryTokenClaim.of(memberId));
}

refreshToken.rotation(newRefreshToken);
commandRefreshTokenPort.save(refreshToken);

return refreshToken;
public Long resolveRefreshToken(String refreshToken) {
JwtClaims claims = refreshTokenProvider.parseJwtClaimsFromToken(refreshToken);

return getClaimValue(claims,
RefreshTokenClaimKeys.USER_ID.getValue(),
Long::parseLong);
}

private void validateToken(String oldRefreshToken, RefreshToken refreshToken) {
if (isTakenAway(oldRefreshToken, refreshToken.getToken())) {
commandRefreshTokenPort.delete(refreshToken);
throw new AuthException(AuthErrorCode.REFRESH_TOKEN_MISMATCHED);
}
private long toSeconds(LocalDateTime expiredDate) {
return Duration.between(LocalDateTime.now(), expiredDate).getSeconds();
}

private boolean isTakenAway(String requestRefreshToken, String expectedRefreshToken) {
return !requestRefreshToken.equals(expectedRefreshToken);
private static <T> T getClaimValue(JwtClaims jwtClaims, String key, Function<String, T> converter) {
Object value = jwtClaims.getClaims().get(key);
if (value != null) {
return converter.apply(value.toString());
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package clap.server.application.service.auth;

import clap.server.adapter.inbound.web.dto.auth.ReissueTokenResponse;
import clap.server.application.port.inbound.auth.ReissueTokenUsecase;
import clap.server.application.port.outbound.auth.CommandRefreshTokenPort;
import clap.server.application.port.outbound.auth.LoadRefreshTokenPort;
import clap.server.common.annotation.architecture.ApplicationService;
import clap.server.domain.model.auth.CustomJwts;
import clap.server.domain.model.auth.RefreshToken;
import clap.server.exception.AuthException;
import clap.server.exception.code.AuthErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;

import static clap.server.application.mapper.response.AuthResponseMapper.toReissueTokenResponse;

@ApplicationService
@RequiredArgsConstructor
class ReissueTokenService implements ReissueTokenUsecase {
private final IssueTokenService issueTokenService;
private final LoadRefreshTokenPort loadRefreshTokenPort;
private final CommandRefreshTokenPort commandRefreshTokenPort;

@Transactional
public ReissueTokenResponse reissueToken(String oldRefreshToken) {
Long memberId = issueTokenService.resolveRefreshToken(oldRefreshToken);
RefreshToken newRefreshToken;
try {
newRefreshToken = refresh(memberId, oldRefreshToken,
issueTokenService.issueRefreshToken(memberId).getToken());
} catch (IllegalArgumentException e) {
throw new AuthException(AuthErrorCode.EXPIRED_TOKEN);
} catch (IllegalStateException e) {
throw new AuthException(AuthErrorCode.TAKEN_AWAY_TOKEN);
}

String newAccessToken = issueTokenService.issueAccessToken(memberId);
CustomJwts tokens = CustomJwts.of(newAccessToken, newRefreshToken.getToken());
return toReissueTokenResponse(tokens);
}

private RefreshToken refresh(
Long memberId,
String oldRefreshToken,
String newRefreshToken
) throws IllegalArgumentException, IllegalStateException {
RefreshToken refreshToken = loadRefreshTokenPort.findByMemberId(memberId).orElseThrow(
() -> new AuthException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND)
);
validateToken(oldRefreshToken, refreshToken);

refreshToken.rotation(newRefreshToken);
commandRefreshTokenPort.save(refreshToken);
return refreshToken;
}

private void validateToken(String oldRefreshToken, RefreshToken refreshToken) {
if (isTakenAway(oldRefreshToken, refreshToken.getToken())) {
commandRefreshTokenPort.delete(refreshToken);
throw new AuthException(AuthErrorCode.REFRESH_TOKEN_MISMATCHED);
}
}

private boolean isTakenAway(String requestRefreshToken, String expectedRefreshToken) {
return !requestRefreshToken.equals(expectedRefreshToken);
}

}
Loading