Skip to content

Commit

Permalink
feat: #252 토큰 유효성 검증 api 추가 (#255)
Browse files Browse the repository at this point in the history
* feat: accessToken 검증 기능 추가

* feat: accessToken 검증 api 추가

* style: final 추가

* test: 변수 네이밍 변경

* refactor: 토큰 타입 상수 네이밍 변경

* refactor: try-catch 변수 네이밍 수정

* style: 메서드 순서 변경
  • Loading branch information
apptie authored Aug 10, 2023
1 parent 53671e1 commit c240dc9
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ private TokenDto convertTokenDto(final User persistUser) {
return new TokenDto(accessToken, refreshToken);
}

@Transactional(readOnly = true)
public TokenDto refreshToken(final String refreshToken) {
final PrivateClaims privateClaims = tokenDecoder.decode(TokenType.REFRESH, refreshToken)
.orElseThrow(
Expand All @@ -82,4 +81,9 @@ public TokenDto refreshToken(final String refreshToken) {

return new TokenDto(accessToken, refreshToken);
}

public boolean validateToken(final String accessToken) {
return tokenDecoder.decode(TokenType.ACCESS, accessToken)
.isPresent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
@RequiredArgsConstructor
public class JwtDecoder implements TokenDecoder {

private static final String TOKEN_TYPE = "Bearer ";
private static final String BEARER_TOKEN_PREFIX = "Bearer ";
private static final String CLAIM_NAME = "userId";
private static final int BEARER_END_INDEX = 7;

Expand All @@ -42,7 +42,7 @@ private void validateBearerToken(final String token) {
}

private void validateTokenType(final String tokenType) {
if (!TOKEN_TYPE.equals(tokenType)) {
if (!BEARER_TOKEN_PREFIX.equals(tokenType)) {
throw new InvalidTokenException("Bearer 타입이 아닙니다.");
}
}
Expand All @@ -58,16 +58,16 @@ private Optional<Claims> parse(final TokenType tokenType, final String token) {
.parseClaimsJws(findPureToken(token))
.getBody()
);
} catch (JwtException e) {
} catch (final JwtException ignored) {
return Optional.empty();
}
}

private PrivateClaims convert(final Claims claims) {
return new PrivateClaims(claims.get(CLAIM_NAME, Long.class));
private String findPureToken(final String token) {
return token.substring(BEARER_TOKEN_PREFIX.length());
}

private String findPureToken(final String token) {
return token.substring(TOKEN_TYPE.length());
private PrivateClaims convert(final Claims claims) {
return new PrivateClaims(claims.get(CLAIM_NAME, Long.class));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
import com.ddang.ddang.authentication.presentation.dto.request.AccessTokenRequest;
import com.ddang.ddang.authentication.presentation.dto.request.RefreshTokenRequest;
import com.ddang.ddang.authentication.presentation.dto.response.TokenResponse;
import com.ddang.ddang.authentication.presentation.dto.response.ValidatedTokenResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -23,7 +27,7 @@ public class AuthenticationController {

@PostMapping("/login/{oauth2Type}")
public ResponseEntity<Object> validate(
@PathVariable Oauth2Type oauth2Type,
@PathVariable final Oauth2Type oauth2Type,
@RequestBody final AccessTokenRequest request
) {
final TokenDto tokenDto = authenticationService.login(oauth2Type, request.accessToken());
Expand All @@ -37,4 +41,13 @@ public ResponseEntity<Object> refreshToken(@RequestBody final RefreshTokenReques

return ResponseEntity.ok(TokenResponse.from(tokenDto));
}

@GetMapping("/validate-token")
public ResponseEntity<ValidatedTokenResponse> validateToken(
@RequestHeader(HttpHeaders.AUTHORIZATION) final String accessToken
) {
final boolean validated = authenticationService.validateToken(accessToken);

return ResponseEntity.ok(new ValidatedTokenResponse(validated));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.ddang.ddang.authentication.presentation.dto.response;

public record ValidatedTokenResponse(boolean validated) {
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ddang.ddang.authentication.application;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
Expand Down Expand Up @@ -197,4 +198,41 @@ void setUp(
.isInstanceOf(InvalidTokenException.class)
.hasMessage("Bearer 타입이 아닙니다.");
}

@Test
void 유효한_accessToken을_검증하면_참을_반환한다() {
// given
final Map<String, Object> privateClaims = Map.of("userId", 1L);
final String accessToken = "Bearer " + tokenEncoder.encode(
LocalDateTime.now(),
TokenType.ACCESS,
privateClaims
);

// when
final boolean actual = authenticationService.validateToken(accessToken);

// then
assertThat(actual).isTrue();
}

@Test
void 만료된_accessToken을_검증하면_거짓을_반환한다() {
// given
final Instant instant = Instant.parse("2000-08-10T15:30:00Z");
final LocalDateTime expiredPublishTime = instant.atZone(ZoneId.of("UTC")).toLocalDateTime();

final Map<String, Object> privateClaims = Map.of("userId", 1L);
final String accessToken = "Bearer " + tokenEncoder.encode(
expiredPublishTime,
TokenType.ACCESS,
privateClaims
);

// when
final boolean actual = authenticationService.validateToken(accessToken);

// then
assertThat(actual).isFalse();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
Expand All @@ -29,6 +30,7 @@
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
Expand Down Expand Up @@ -164,4 +166,36 @@ void setUp() {
jsonPath("$.message").exists()
);
}

@Test
void 유효한_accessToken을_검증하면_참을_반환한다() throws Exception {
// given
given(authenticationService.validateToken(anyString())).willReturn(true);

// when & then
mockMvc.perform(get("/oauth2/validate-token")
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer accessToken")
)
.andExpectAll(
status().isOk(),
jsonPath("$.validated").value(true)
);
}

@Test
void 만료된_accessToken을_검증하면_거짓을_반환한다() throws Exception {
// given
given(authenticationService.validateToken(anyString())).willReturn(false);

// when & then
mockMvc.perform(get("/oauth2/validate-token")
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer invalidAccessToken")
)
.andExpectAll(
status().isOk(),
jsonPath("$.validated").value(false)
);
}
}

0 comments on commit c240dc9

Please sign in to comment.