diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index ffcc4d65e..05dd77482 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -12,6 +12,12 @@ === Github 로그인 operation::auth/login[snippets='http-request,request-parameters,http-response,response-fields'] +=== 리프래시 토큰 +operation::auth/refresh[snippets='http-request,http-response'] + +=== 로그아웃 +operation::auth/logout[snippets='http-request,http-response'] + [[Member]] == 회원 diff --git a/backend/src/docs/asciidoc/index.html b/backend/src/docs/asciidoc/index.html index 193bfdad2..870bb1bbc 100644 --- a/backend/src/docs/asciidoc/index.html +++ b/backend/src/docs/asciidoc/index.html @@ -475,7 +475,7 @@

Github 로

HTTP request

-
POST /api/login/token?code=authorization-code HTTP/1.1
+
POST /api/auth/login?code=authorization-code HTTP/1.1
 Content-Type: application/json
 Accept: application/json
 Host: localhost:8080
@@ -890,4 +890,4 @@

- \ No newline at end of file + diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java index 821ca7459..ca6abeb60 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthRequestMatchConfig.java @@ -1,11 +1,15 @@ package com.woowacourse.moamoa.auth.config; +import static org.springframework.http.HttpMethod.DELETE; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.http.HttpMethod.PUT; + import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcher; import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcherBuilder; import lombok.AllArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; @Configuration @AllArgsConstructor @@ -14,10 +18,10 @@ public class AuthRequestMatchConfig { @Bean public AuthenticationRequestMatcher authenticationRequestMatcher() { return new AuthenticationRequestMatcherBuilder() - .addUpAuthenticationPath(HttpMethod.POST, "/api/studies", "/api/studies/\\d+/reviews", "/api/studies/\\d+/reviews/\\d+") - .addUpAuthenticationPath(HttpMethod.GET, "/api/my/studies", "/api/members/me", "/api/members/me/role") - .addUpAuthenticationPath(HttpMethod.PUT, "/api/studies/\\d+/reviews/\\d+") - .addUpAuthenticationPath(HttpMethod.DELETE, "/api/studies/\\d+/reviews/\\d+") + .addUpAuthenticationPath(POST, "/api/studies", "/api/studies/\\d+/reviews", "/api/studies/\\d+/reviews/\\d+") + .addUpAuthenticationPath(GET, "/api/my/studies", "/api/members/me", "/api/members/me/role") + .addUpAuthenticationPath(PUT, "/api/studies/\\d+/reviews/\\d+") + .addUpAuthenticationPath(DELETE, "/api/studies/\\d+/reviews/\\d+") .build(); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticationExtractor.java b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticationExtractor.java index 5bc64399a..ebfda47b1 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticationExtractor.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticationExtractor.java @@ -7,8 +7,8 @@ public class AuthenticationExtractor { - private static String BEARER_TYPE = "Bearer"; - private static String ACCESS_TOKEN_TYPE = AuthenticationExtractor.class.getSimpleName() + ".ACCESS_TOKEN_TYPE"; + private static final String BEARER_TYPE = "Bearer"; + private static final String ACCESS_TOKEN_TYPE = AuthenticationExtractor.class.getSimpleName() + ".ACCESS_TOKEN_TYPE"; public static String extract(HttpServletRequest request) { Enumeration headers = request.getHeaders(AUTHORIZATION); diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java index 9596a74bd..ef362e67e 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthController.java @@ -1,9 +1,15 @@ package com.woowacourse.moamoa.auth.controller; +import com.woowacourse.moamoa.auth.config.AuthenticationPrincipal; import com.woowacourse.moamoa.auth.service.AuthService; -import com.woowacourse.moamoa.auth.service.response.TokenResponse; +import com.woowacourse.moamoa.auth.service.response.AccessTokenResponse; +import com.woowacourse.moamoa.auth.service.response.TokensResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -12,10 +18,48 @@ @RequiredArgsConstructor public class AuthController { + private static final String REFRESH_TOKEN = "refreshToken"; + private static final int REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60; + private final AuthService authService; - @PostMapping("/api/login/token") - public ResponseEntity login(@RequestParam final String code) { - return ResponseEntity.ok().body(authService.createToken(code)); + @PostMapping("/api/auth/login") + public ResponseEntity login(@RequestParam final String code) { + final TokensResponse tokenResponse = authService.createToken(code); + + final AccessTokenResponse response = new AccessTokenResponse(tokenResponse.getAccessToken(), authService.getExpireTime()); + final ResponseCookie cookie = putTokenInCookie(tokenResponse); + + return ResponseEntity.ok().header("Set-Cookie", cookie.toString()).body(response); + } + + @GetMapping("/api/auth/refresh") + public ResponseEntity refreshToken(@AuthenticationPrincipal Long githubId, @CookieValue String refreshToken) { + return ResponseEntity.ok().body(authService.refreshToken(githubId, refreshToken)); + } + + @DeleteMapping("/api/auth/logout") + public ResponseEntity logout(@AuthenticationPrincipal Long githubId) { + authService.logout(githubId); + + return ResponseEntity.noContent().header("Set-Cookie", removeCookie().toString()).build(); + } + + private ResponseCookie putTokenInCookie(final TokensResponse tokenResponse) { + return ResponseCookie.from(REFRESH_TOKEN, tokenResponse.getRefreshToken()) + .maxAge(REFRESH_TOKEN_EXPIRATION) + .path("/") + .secure(true) + .httpOnly(true) + .build(); + } + + private ResponseCookie removeCookie() { + return ResponseCookie.from(REFRESH_TOKEN, null) + .maxAge(0) + .path("/") + .secure(true) + .httpOnly(true) + .build(); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolver.java b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolver.java index bcac02054..1e4472da9 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolver.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolver.java @@ -28,8 +28,8 @@ public boolean supportsParameter(final MethodParameter parameter) { public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer, final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) { final HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - final String token = AuthenticationExtractor.extract(request); + if (token == null) { throw new UnauthorizedException("인증 타입이 올바르지 않습니다."); } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/domain/Token.java b/backend/src/main/java/com/woowacourse/moamoa/auth/domain/Token.java new file mode 100644 index 000000000..33760f67a --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/domain/Token.java @@ -0,0 +1,36 @@ +package com.woowacourse.moamoa.auth.domain; + +import static javax.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = PROTECTED) +@AllArgsConstructor +public class Token { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Column(nullable = false) + private Long githubId; + + private String refreshToken; + + public Token(final Long githubId, final String refreshToken) { + this(null, githubId, refreshToken); + } + + public void updateRefreshToken(final String refreshToken) { + this.refreshToken = refreshToken; + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/domain/repository/TokenRepository.java b/backend/src/main/java/com/woowacourse/moamoa/auth/domain/repository/TokenRepository.java new file mode 100644 index 000000000..c5c3fc487 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/domain/repository/TokenRepository.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.auth.domain.repository; + +import com.woowacourse.moamoa.auth.domain.Token; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TokenRepository extends JpaRepository { + + Optional findByGithubId(Long githubId); +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/exception/RefreshTokenExpirationException.java b/backend/src/main/java/com/woowacourse/moamoa/auth/exception/RefreshTokenExpirationException.java new file mode 100644 index 000000000..146bdb374 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/exception/RefreshTokenExpirationException.java @@ -0,0 +1,9 @@ +package com.woowacourse.moamoa.auth.exception; + +import com.woowacourse.moamoa.common.exception.UnauthorizedException; + +public class RefreshTokenExpirationException extends UnauthorizedException { + public RefreshTokenExpirationException() { + super("만료된 리프래시 토큰입니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenExpirationException.java b/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenExpirationException.java new file mode 100644 index 000000000..3524d8e76 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenExpirationException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.auth.exception; + +import com.woowacourse.moamoa.common.exception.UnauthorizedException; + +public class TokenExpirationException extends UnauthorizedException { + + public TokenExpirationException() { + super("만료된 토큰입니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenNotFoundException.java b/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenNotFoundException.java new file mode 100644 index 000000000..f947fff24 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenNotFoundException.java @@ -0,0 +1,10 @@ +package com.woowacourse.moamoa.auth.exception; + +import com.woowacourse.moamoa.common.exception.UnauthorizedException; + +public class TokenNotFoundException extends UnauthorizedException { + + public TokenNotFoundException() { + super("토큰이 존재하지 않습니다."); + } +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java index 0c305f46b..64772bdf3 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/JwtTokenProvider.java @@ -1,5 +1,7 @@ package com.woowacourse.moamoa.auth.infrastructure; +import com.woowacourse.moamoa.auth.exception.RefreshTokenExpirationException; +import com.woowacourse.moamoa.auth.service.response.TokensResponse; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtException; @@ -15,6 +17,8 @@ @Component public class JwtTokenProvider implements TokenProvider { + private static final long REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60 * 1000; // 7일 + private final SecretKey key; private final long validityInMilliseconds; @@ -27,16 +31,23 @@ public JwtTokenProvider( } @Override - public String createToken(final Long payload) { + public TokensResponse createToken(final Long payload) { final Date now = new Date(); - final Date validity = new Date(now.getTime() + validityInMilliseconds); - return Jwts.builder() + String accessToken = Jwts.builder() .setSubject(payload.toString()) .setIssuedAt(now) - .setExpiration(validity) + .setExpiration(new Date(now.getTime() + validityInMilliseconds)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + String refreshToken = Jwts.builder() + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION)) .signWith(key, SignatureAlgorithm.HS256) .compact(); + + return new TokensResponse(accessToken, refreshToken); } @Override @@ -57,11 +68,47 @@ public boolean validateToken(final String token) { .build() .parseClaimsJws(token); - return !claims.getBody() - .getExpiration() - .before(new Date()); + Date tokenExpirationDate = claims.getBody().getExpiration(); + validateTokenExpiration(tokenExpirationDate); + + return true; } catch (JwtException | IllegalArgumentException e) { return false; } } + + @Override + public String recreationAccessToken(final Long githubId, final String refreshToken) { + Jws claims = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(refreshToken); + + Date tokenExpirationDate = claims.getBody().getExpiration(); + validateTokenExpiration(tokenExpirationDate); + + return createAccessToken(githubId); + } + + private void validateTokenExpiration(Date tokenExpirationDate) { + if (tokenExpirationDate.before(new Date())) { + throw new RefreshTokenExpirationException(); + } + } + + private String createAccessToken(final Long githubId) { + final Date now = new Date(); + + return Jwts.builder() + .setSubject(Long.toString(githubId)) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + validityInMilliseconds)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + @Override + public long getValidityInMilliseconds() { + return validityInMilliseconds; + } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/TokenProvider.java b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/TokenProvider.java index 014343e4f..daed6cd84 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/TokenProvider.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/TokenProvider.java @@ -1,10 +1,16 @@ package com.woowacourse.moamoa.auth.infrastructure; +import com.woowacourse.moamoa.auth.service.response.TokensResponse; + public interface TokenProvider { - String createToken(final Long payload); + TokensResponse createToken(final Long payload); String getPayload(final String token); boolean validateToken(final String token); + + String recreationAccessToken(final Long githubId, final String refreshToken); + + long getValidityInMilliseconds(); } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/service/AuthService.java b/backend/src/main/java/com/woowacourse/moamoa/auth/service/AuthService.java index 442e57568..22082543e 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/service/AuthService.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/service/AuthService.java @@ -1,10 +1,16 @@ package com.woowacourse.moamoa.auth.service; +import com.woowacourse.moamoa.auth.domain.Token; +import com.woowacourse.moamoa.auth.domain.repository.TokenRepository; +import com.woowacourse.moamoa.auth.exception.TokenNotFoundException; import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; import com.woowacourse.moamoa.auth.service.oauthclient.OAuthClient; import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; -import com.woowacourse.moamoa.auth.service.response.TokenResponse; +import com.woowacourse.moamoa.auth.service.response.AccessTokenResponse; +import com.woowacourse.moamoa.auth.service.response.TokensResponse; +import com.woowacourse.moamoa.common.exception.UnauthorizedException; import com.woowacourse.moamoa.member.service.MemberService; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,15 +23,51 @@ public class AuthService { private final MemberService memberService; private final TokenProvider tokenProvider; private final OAuthClient oAuthClient; + private final TokenRepository tokenRepository; @Transactional - public TokenResponse createToken(final String code) { + public TokensResponse createToken(final String code) { final String accessToken = oAuthClient.getAccessToken(code); final GithubProfileResponse githubProfileResponse = oAuthClient.getProfile(accessToken); - memberService.saveOrUpdate(githubProfileResponse.toMember()); - final String jwtToken = tokenProvider.createToken(githubProfileResponse.getGitgubId()); - return new TokenResponse(jwtToken); + final Long githubId = githubProfileResponse.getGithubId(); + final Optional token = tokenRepository.findByGithubId(githubId); + + final TokensResponse tokenResponse = tokenProvider.createToken(githubProfileResponse.getGithubId()); + + if (token.isPresent()) { + token.get().updateRefreshToken(tokenResponse.getRefreshToken()); + return tokenResponse; + } + + tokenRepository.save(new Token(githubProfileResponse.getGithubId(), tokenResponse.getRefreshToken())); + + return tokenResponse; + } + + public AccessTokenResponse refreshToken(final Long githubId, final String refreshToken) { + final Token token = tokenRepository.findByGithubId(githubId) + .orElseThrow(TokenNotFoundException::new); + + if (!token.getRefreshToken().equals(refreshToken)) { + throw new UnauthorizedException("유효하지 않은 토큰입니다."); + } + + String accessToken = tokenProvider.recreationAccessToken(githubId, refreshToken); + + return new AccessTokenResponse(accessToken, tokenProvider.getValidityInMilliseconds()); + } + + @Transactional + public void logout(final Long githubId) { + final Token token = tokenRepository.findByGithubId(githubId) + .orElseThrow(TokenNotFoundException::new); + + tokenRepository.delete(token); + } + + public long getExpireTime() { + return tokenProvider.getValidityInMilliseconds(); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/service/oauthclient/response/GithubProfileResponse.java b/backend/src/main/java/com/woowacourse/moamoa/auth/service/oauthclient/response/GithubProfileResponse.java index 6a7364c14..330c76e56 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/service/oauthclient/response/GithubProfileResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/service/oauthclient/response/GithubProfileResponse.java @@ -2,15 +2,17 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.woowacourse.moamoa.member.domain.Member; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor +@AllArgsConstructor public class GithubProfileResponse { @JsonProperty("id") - private Long gitgubId; + private Long githubId; @JsonProperty("login") private String username; @@ -21,15 +23,7 @@ public class GithubProfileResponse { @JsonProperty("html_url") private String profileUrl; - public GithubProfileResponse(final Long gitgubId, final String username, final String imageUrl, - final String profileUrl) { - this.gitgubId = gitgubId; - this.username = username; - this.imageUrl = imageUrl; - this.profileUrl = profileUrl; - } - public Member toMember() { - return new Member(gitgubId, username, imageUrl, profileUrl); + return new Member(githubId, username, imageUrl, profileUrl); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/service/request/AccessTokenRequest.java b/backend/src/main/java/com/woowacourse/moamoa/auth/service/request/AccessTokenRequest.java index 6e0d26fc2..80ce902b5 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/service/request/AccessTokenRequest.java +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/service/request/AccessTokenRequest.java @@ -16,14 +16,6 @@ public class AccessTokenRequest { private final String code; - public String getClientId() { - return clientId; - } - - public String getClientSecret() { - return clientSecret; - } - public String getCode() { return code; } diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/AccessTokenResponse.java b/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/AccessTokenResponse.java new file mode 100644 index 000000000..f89683d1b --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/AccessTokenResponse.java @@ -0,0 +1,12 @@ +package com.woowacourse.moamoa.auth.service.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class AccessTokenResponse { + + private final String accessToken; + private final long expiredTime; +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokenResponse.java b/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokenResponse.java deleted file mode 100644 index 47bbbb7c4..000000000 --- a/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokenResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.woowacourse.moamoa.auth.service.response; - -public class TokenResponse { - - private final String token; - - public TokenResponse(final String token) { - this.token = token; - } - - public String getToken() { - return token; - } -} diff --git a/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokensResponse.java b/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokensResponse.java new file mode 100644 index 000000000..fad30b049 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokensResponse.java @@ -0,0 +1,12 @@ +package com.woowacourse.moamoa.auth.service.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class TokensResponse { + + private final String accessToken; + private final String refreshToken; +} diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java b/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java index 0c888fbcb..98ede3ea6 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java +++ b/backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java @@ -1,7 +1,10 @@ package com.woowacourse.moamoa.common.advice; import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; +import com.woowacourse.moamoa.auth.exception.RefreshTokenExpirationException; import com.woowacourse.moamoa.common.advice.response.ErrorResponse; import com.woowacourse.moamoa.common.exception.BadRequestException; import com.woowacourse.moamoa.common.exception.InvalidFormatException; @@ -9,17 +12,14 @@ import com.woowacourse.moamoa.common.exception.UnauthorizedException; import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException; import com.woowacourse.moamoa.study.service.exception.FailureParticipationException; - -import org.springframework.http.HttpStatus; +import io.jsonwebtoken.JwtException; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import io.jsonwebtoken.JwtException; -import lombok.extern.slf4j.Slf4j; - @RestControllerAdvice @Slf4j public class CommonControllerAdvice { @@ -39,25 +39,31 @@ public ResponseEntity handleBadRequest() { FailureParticipationException.class }) public ResponseEntity handleBadRequest(final Exception e) { - log.error("HandleBadRequest : {}", e.getMessage()); + log.debug("HandleBadRequest : {}", e.getMessage()); return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage())); } @ExceptionHandler({UnauthorizedException.class, JwtException.class}) public ResponseEntity handleUnauthorized(final Exception e) { - log.error("UnauthorizedException : {}", e.getMessage()); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + log.debug("UnauthorizedException : {}", e.getMessage()); + return ResponseEntity.status(UNAUTHORIZED).build(); + } + + @ExceptionHandler(RefreshTokenExpirationException.class) + public ResponseEntity handle(RefreshTokenExpirationException e) { + log.debug("RefreshTokenExpirationException : {}", e.getMessage()); + return ResponseEntity.status(UNAUTHORIZED).body(new ErrorResponse(e.getMessage(), 4001)); } @ExceptionHandler(NotFoundException.class) public ResponseEntity handleNotFound(final Exception e) { - log.error("NotFoundException : {}", e.getMessage()); - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(e.getMessage())); + log.debug("NotFoundException : {}", e.getMessage()); + return ResponseEntity.status(NOT_FOUND).body(new ErrorResponse(e.getMessage())); } @ExceptionHandler(RuntimeException.class) public ResponseEntity handleInternalServerError(RuntimeException e) { - log.error("RuntimeException : {}", e.getMessage()); + log.debug("RuntimeException : {}", e.getMessage()); return ResponseEntity.status(INTERNAL_SERVER_ERROR).body(new ErrorResponse("요청을 처리할 수 없습니다.")); } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/advice/response/ErrorResponse.java b/backend/src/main/java/com/woowacourse/moamoa/common/advice/response/ErrorResponse.java index 0ab20d12c..3f9eb0ba0 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/common/advice/response/ErrorResponse.java +++ b/backend/src/main/java/com/woowacourse/moamoa/common/advice/response/ErrorResponse.java @@ -3,12 +3,22 @@ public class ErrorResponse { private final String message; + private int code; public ErrorResponse(final String message) { this.message = message; } + public ErrorResponse(final String message, final int code) { + this.message = message; + this.code = code; + } + public String getMessage() { return message; } + + public int getCode() { + return code; + } } diff --git a/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java b/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java index e4da1c683..12a44b543 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java +++ b/backend/src/main/java/com/woowacourse/moamoa/common/config/WebConfig.java @@ -24,8 +24,10 @@ public void addArgumentResolvers(final List resol @Override public void addCorsMappings(final CorsRegistry registry) { registry.addMapping("/api/**") + .allowedOrigins("https://dev.moamoa.space", "https://moamoa.space") .allowedMethods(ALLOW_METHODS) - .exposedHeaders(HttpHeaders.LOCATION); + .exposedHeaders(HttpHeaders.LOCATION) + .allowCredentials(true); } @Bean diff --git a/backend/src/main/java/com/woowacourse/moamoa/member/controller/MemberController.java b/backend/src/main/java/com/woowacourse/moamoa/member/controller/MemberController.java index e02848a9f..2432bda40 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/member/controller/MemberController.java +++ b/backend/src/main/java/com/woowacourse/moamoa/member/controller/MemberController.java @@ -3,18 +3,16 @@ import com.woowacourse.moamoa.auth.config.AuthenticationPrincipal; import com.woowacourse.moamoa.member.service.response.MemberResponse; import com.woowacourse.moamoa.member.service.MemberService; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController +@RequiredArgsConstructor public class MemberController { - private MemberService memberService; - - public MemberController(final MemberService memberService) { - this.memberService = memberService; - } + private final MemberService memberService; @RequestMapping("/api/members/me") public ResponseEntity getCurrentMember( diff --git a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java index 418d9f090..e0fdd177b 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/AcceptanceTest.java @@ -19,7 +19,6 @@ import io.restassured.specification.RequestSpecification; import java.util.Map; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.beans.factory.annotation.Autowired; @@ -67,7 +66,7 @@ public class AcceptanceTest { @Value("${oauth2.github.client-secret}") private String clientSecret; - private MockRestServiceServer mockServer; + protected MockRestServiceServer mockServer; @BeforeEach protected void setRestDocumentation(RestDocumentationContextProvider restDocumentation) { @@ -107,17 +106,6 @@ void tearDown() { jdbcTemplate.update("ALTER TABLE study AUTO_INCREMENT = 1"); } - private void mockingGithubServer(String authorizationCode, GithubProfileResponse response) { - try { - mockingGithubServerForGetAccessToken(authorizationCode, Map.of("access_token", "access-token", - "token_type", "bearer", - "scope", "")); - mockingGithubServerForGetProfile("access-token", HttpStatus.OK, response); - } catch (Exception e) { - Assertions.fail(e.getMessage()); - } - } - protected void mockingGithubServerForGetAccessToken(final String authorizationCode, final Map accessTokenResponse) throws JsonProcessingException { diff --git a/backend/src/test/java/com/woowacourse/acceptance/steps/LoginSteps.java b/backend/src/test/java/com/woowacourse/acceptance/steps/LoginSteps.java index eedebb1d4..c690b881f 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/steps/LoginSteps.java +++ b/backend/src/test/java/com/woowacourse/acceptance/steps/LoginSteps.java @@ -42,25 +42,30 @@ public class LoginSteps extends Steps { } private String getIssuedBearerToken() { - if (tokenCache.containsKey(githubProfile.getGitgubId())) { - return tokenCache.get(githubProfile.getGitgubId()); + if (tokenCache.containsKey(githubProfile.getGithubId())) { + return tokenCache.get(githubProfile.getGithubId()); } + final String bearerToken = requestBearerToken(); - tokenCache.put(githubProfile.getGitgubId(), bearerToken); + tokenCache.put(githubProfile.getGithubId(), bearerToken); + return bearerToken; } private String requestBearerToken() { final String authorizationCode = "Authorization Code"; mockingGithubServer(authorizationCode, githubProfile); + final String token = RestAssured.given().log().all() .param("code", authorizationCode) .when() - .post("/api/login/token") + .post("/api/auth/login") .then().log().all() .statusCode(HttpStatus.OK.value()) - .extract().jsonPath().getString("token"); + .extract().jsonPath().getString("accessToken"); + mockServer.reset(); + return "Bearer " + token; } } diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java index 6f5bc2a67..b41780ed6 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/auth/AuthAcceptanceTest.java @@ -1,8 +1,14 @@ package com.woowacourse.acceptance.test.auth; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; @@ -11,16 +17,22 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.woowacourse.acceptance.AcceptanceTest; +import com.woowacourse.moamoa.auth.domain.Token; +import com.woowacourse.moamoa.auth.domain.repository.TokenRepository; import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; import io.restassured.RestAssured; import java.util.Map; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.restdocs.payload.JsonFieldType; public class AuthAcceptanceTest extends AcceptanceTest { + @Autowired + private TokenRepository tokenRepository; + @DisplayName("Authorization code를 받아서 token을 발급한다.") @Test void getJwtToken() throws JsonProcessingException { @@ -35,13 +47,55 @@ void getJwtToken() throws JsonProcessingException { RestAssured.given(spec).log().all() .filter(document("auth/login", requestParameters(parameterWithName("code").description("Authorization code")), - responseFields(fieldWithPath("token").type(JsonFieldType.STRING).description("사용자 토큰")))) + responseFields( + fieldWithPath("accessToken").type(STRING).description("사용자 토큰"), + fieldWithPath("expiredTime").type(NUMBER).description("유효시간") + ))) .queryParam("code", authorizationCode) .when() - .post("/api/login/token") + .post("/api/auth/login") .then().log().all() .statusCode(HttpStatus.OK.value()) - .body("token", notNullValue()); + .body("accessToken", notNullValue()) + .body("expiredTime", notNullValue()); + } + + @DisplayName("RefreshToken 으로 AccessToken 을 재발급한다.") + @Test + void refreshToken() { + final String token = getBearerTokenBySignInOrUp(new GithubProfileResponse(4L, "verus", "https://image", "github.com")); + final Token foundToken = tokenRepository.findByGithubId(4L).get(); + + RestAssured.given(spec).log().all() + .filter(document("auth/refresh", + requestHeaders(headerWithName("Authorization").description("Bearer Token")))) + .cookie("refreshToken", foundToken.getRefreshToken()) + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .header(AUTHORIZATION, token) + .when() + .get("/api/auth/refresh") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .body("accessToken", notNullValue()); + } + + @DisplayName("로그아웃시에 쿠키를 제거해준다.") + @Test + public void logout() { + final String token = getBearerTokenBySignInOrUp(new GithubProfileResponse(4L, "verus", "https://image", "github.com")); + final Token foundToken = tokenRepository.findByGithubId(4L).get(); + + RestAssured.given(spec).log().all() + .filter(document("auth/logout", + requestHeaders(headerWithName("Authorization").description("Bearer Token")))) + .cookie("refreshToken", foundToken.getRefreshToken()) + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .header(AUTHORIZATION, token) + .when() + .delete("/api/auth/logout") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()) + .cookie("refreshToken", is("")); } @Test @@ -56,7 +110,7 @@ void get401ByInvalidAuthorizationCode() throws JsonProcessingException { RestAssured.given().log().all() .param("code", invalidAuthorizationCode) .when() - .post("/api/login/token") + .post("/api/auth/login") .then().log().all() .statusCode(HttpStatus.UNAUTHORIZED.value()); } @@ -76,7 +130,7 @@ void get401ByInvalidAccessToken() throws JsonProcessingException { RestAssured.given().log().all() .param("code", authorizationCode) .when() - .post("/api/login/token") + .post("/api/auth/login") .then().log().all() .statusCode(HttpStatus.UNAUTHORIZED.value()); } @@ -85,4 +139,30 @@ private void mockingGithubServerForGetProfile(final String accessToken, final Ht throws JsonProcessingException { mockingGithubServerForGetProfile(accessToken, status, null); } + + private String getBearerTokenBySignInOrUp(GithubProfileResponse response) { + final String authorizationCode = "Authorization Code"; + mockingGithubServer(authorizationCode, response); + final String token = RestAssured.given().log().all() + .param("code", authorizationCode) + .when() + .post("/api/auth/login") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().jsonPath().getString("accessToken"); + mockServer.reset(); + return "Bearer " + token; + } + + private void mockingGithubServer(String authorizationCode, GithubProfileResponse response) { + try { + mockingGithubServerForGetAccessToken(authorizationCode, Map.of( + "access_token", "access-token", + "token_type", "bearer", + "scope", "")); + mockingGithubServerForGetProfile("access-token", HttpStatus.OK, response); + } catch (Exception e) { + Assertions.fail(e.getMessage()); + } + } } diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java index 86185c487..76cac2cf9 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java @@ -4,7 +4,6 @@ import io.restassured.RestAssured; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.test.context.jdbc.Sql; public class CorsAcceptanceTest extends AcceptanceTest { @@ -12,7 +11,6 @@ public class CorsAcceptanceTest extends AcceptanceTest { @Test void corsTest() { RestAssured.given() - .header("Origin", "https://xxx.com") .header("Access-Control-Request-Method", "GET") .when() .options("/api/studies") diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java index 700ed9feb..74b2aeca9 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/member/MemberAcceptanceTest.java @@ -1,5 +1,6 @@ package com.woowacourse.acceptance.test.member; +import static org.apache.http.HttpHeaders.AUTHORIZATION; import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_깃허브_ID; import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이름; import static com.woowacourse.acceptance.fixture.MemberFixtures.베루스_이미지_URL; @@ -13,7 +14,6 @@ import com.woowacourse.acceptance.AcceptanceTest; import com.woowacourse.moamoa.member.service.response.MemberResponse; import io.restassured.RestAssured; -import org.apache.http.HttpHeaders; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; @@ -24,7 +24,7 @@ void getCurrentMember() { final String token = 베루스가().로그인한다(); final MemberResponse memberResponse = RestAssured.given(spec).log().all() - .header(HttpHeaders.AUTHORIZATION, token) + .header(AUTHORIZATION, token) .filter(document("members/me", requestHeaders(headerWithName("Authorization").description("Bearer Token")))) .when().log().all() diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java index 47e22f18b..59cfd4788 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthControllerTest.java @@ -7,8 +7,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.woowacourse.moamoa.WebMVCTest; -import com.woowacourse.moamoa.auth.service.response.TokenResponse; - +import com.woowacourse.moamoa.auth.service.response.TokensResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -17,11 +16,12 @@ public class AuthControllerTest extends WebMVCTest { @DisplayName("Authorization 요청과 응답 형식을 확인한다.") @Test void getJwtToken() throws Exception { - given(authService.createToken("Authorization code")).willReturn(new TokenResponse("jwt token")); + given(authService.createToken("Authorization code")) + .willReturn(new TokensResponse("jwt token", "refreshtoken")); - mockMvc.perform(post("/api/login/token?code=Authorization code")) + mockMvc.perform(post("/api/auth/login?code=Authorization code")) .andExpect(status().isOk()) - .andExpect(jsonPath("token").value("jwt token")) + .andExpect(jsonPath("$.accessToken").value("jwt token")) .andDo(print()); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java index 8f2141a96..1bb654cd9 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationArgumentResolverTest.java @@ -42,7 +42,7 @@ void validAuthorizationTypeIsBearer() { @DisplayName("Authorization 인증 타입이 Bearer인 경우 payload를 반환한다.") @Test void getToken() { - String wrongBearerToken = "Bearer " + tokenProvider.createToken(1L); + String wrongBearerToken = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); given(nativeWebRequest.getNativeRequest(HttpServletRequest.class)) .willReturn(httpServletRequest); diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java index 6c68edcc9..fa4fb8ab5 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/controller/AuthenticationInterceptorTest.java @@ -29,7 +29,7 @@ void isPreflightRequest() { @DisplayName("유효한 토큰을 검증한다.") @Test void validateValidToken() { - final String token = tokenProvider.createToken(1L); + final String token = tokenProvider.createToken(1L).getAccessToken(); String bearerToken = "Bearer " + token; given(httpServletRequest.getMethod()) diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java index a0eb0cb71..4fed7e132 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/infrastructure/TokenProviderTest.java @@ -47,7 +47,7 @@ void isExpiredToken() { @DisplayName("유효한 토큰을 검증한다.") @Test void validateTokenByValidToken() { - String token = tokenProvider.createToken(1L); + String token = tokenProvider.createToken(1L).getAccessToken(); assertThat(tokenProvider.validateToken(token)).isTrue(); } @@ -55,7 +55,7 @@ void validateTokenByValidToken() { @DisplayName("유효하지 않은 토큰을 검증한다.") @Test void validateTokenByInvalidToken() { - String token = tokenProvider.createToken(1L); + String token = tokenProvider.createToken(1L).getAccessToken(); String invalidToken = token + "dummy"; @@ -65,7 +65,7 @@ void validateTokenByInvalidToken() { @DisplayName("JwtToken payload 검증한다.") @Test void validatePayload() { - String token = tokenProvider.createToken(1L); + String token = tokenProvider.createToken(1L).getAccessToken(); assertThat(tokenProvider.getPayload(token)).isEqualTo("1"); } @@ -73,7 +73,7 @@ void validatePayload() { @DisplayName("JwtToken 형식을 검증한다.") @Test void validateJwtTokenFormat() { - String token = tokenProvider.createToken(1L); + String token = tokenProvider.createToken(1L).getAccessToken(); final String[] parts = token.split("\\."); diff --git a/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java b/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java index 6ca8156df..4c2c0de24 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/auth/service/AuthServiceTest.java @@ -1,28 +1,113 @@ package com.woowacourse.moamoa.auth.service; -import static org.mockito.Mockito.when; -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; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.woowacourse.moamoa.WebMVCTest; -import com.woowacourse.moamoa.auth.service.response.TokenResponse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import com.woowacourse.moamoa.auth.domain.Token; +import com.woowacourse.moamoa.auth.domain.repository.TokenRepository; +import com.woowacourse.moamoa.auth.infrastructure.TokenProvider; +import com.woowacourse.moamoa.auth.service.oauthclient.OAuthClient; +import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse; +import com.woowacourse.moamoa.auth.service.response.AccessTokenResponse; +import com.woowacourse.moamoa.auth.service.response.TokensResponse; +import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.exception.UnauthorizedException; +import com.woowacourse.moamoa.member.domain.repository.MemberRepository; +import com.woowacourse.moamoa.member.query.MemberDao; +import com.woowacourse.moamoa.member.service.MemberService; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +@RepositoryTest +class AuthServiceTest { + + private AuthService authService; + + private OAuthClient oAuthClient; + + private TokenProvider tokenProvider; + + private MemberService memberService; + + @Autowired + private TokenRepository tokenRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberDao memberDao; + + @BeforeEach + void setUp() { + memberService = new MemberService(memberRepository, memberDao); + tokenProvider = Mockito.mock(TokenProvider.class); + oAuthClient = Mockito.mock(OAuthClient.class); + authService = new AuthService(memberService, tokenProvider, oAuthClient, tokenRepository); + + Mockito.when(oAuthClient.getAccessToken("authorization-code")).thenReturn("access-token"); + Mockito.when(oAuthClient.getProfile("access-token")) + .thenReturn(new GithubProfileResponse(1L, "dwoo", "imageUrl", "profileUrl")); + Mockito.when(tokenProvider.createToken(1L)) + .thenReturn(new TokensResponse("accessToken", "refreshToken")); + Mockito.when(tokenProvider.recreationAccessToken(1L, "refreshToken")) + .thenReturn("recreationAccessToken"); + } + + @DisplayName("RefreshToken 을 저장한다.") + @Test + public void saveRefreshToken() { + authService.createToken("authorization-code"); + final Token token = tokenRepository.findByGithubId(1L).get(); + + assertThat(token.getRefreshToken()).isEqualTo("refreshToken"); + } + + @DisplayName("RefreshToken 을 이용하여 AccessToken 을 업데이트한다.") + @Test + public void updateRefreshToken() { + authService.createToken("authorization-code"); + final Token token = tokenRepository.findByGithubId(1L).get(); + final String refreshToken = token.getRefreshToken(); + + final AccessTokenResponse accessTokenResponse = authService.refreshToken(1L, refreshToken); -class AuthServiceTest extends WebMVCTest { + assertThat(refreshToken).isNotBlank(); + assertThat(accessTokenResponse.getAccessToken()).isEqualTo("recreationAccessToken"); + } + + @DisplayName("DB에 저장되어 있지 않은 refresh token으로 access token을 발급받을 수 없다.") + @Test + public void validateRefreshToken() { + assertThatThrownBy(() -> authService.refreshToken(1L, "InvalidRefreshToken")) + .isInstanceOf(UnauthorizedException.class); + } - @DisplayName("Authorization code를 받아서 token을 발급한다.") + @DisplayName("refresh token을 통해 access token을 발급받을 수 있다.") @Test - void getTokenByAuthorizationCode() throws Exception { - when(authService.createToken("authorization-code")).thenReturn(new TokenResponse("this is jwt-token")); - - mockMvc.perform(post("/api/login/token") - .param("code", "authorization-code")) - .andExpect(status().isOk()) - .andExpect(jsonPath("token").value("this is jwt-token")) - .andDo(print()); + public void recreationAccessToken() { + authService.createToken("authorization-code"); + final Token token = tokenRepository.findByGithubId(1L).get(); + + assertDoesNotThrow(() -> authService.refreshToken(1L, token.getRefreshToken())); + } + + @DisplayName("로그아웃을 하면 Token 을 제거한다.") + @Test + public void logout() { + authService.createToken("authorization-code"); + final Token token = tokenRepository.findByGithubId(1L).get(); + + authService.logout(token.getGithubId()); + + final Optional foundToken = tokenRepository.findByGithubId(token.getGithubId()); + + assertThat(token).isNotNull(); + assertThat(foundToken.isEmpty()).isTrue(); } } diff --git a/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java index 881d6876a..eac8b078c 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/member/webmvc/MemberWebMvcTest.java @@ -31,7 +31,7 @@ void unauthorizedToken(String invalidToken) throws Exception { @DisplayName("사용자가 없는 경우 400 에러 반환") @Test void notFound() throws Exception { - final String token = tokenProvider.createToken(1L); + final String token = tokenProvider.createToken(1L).getAccessToken(); given(memberService.getByGithubId(any())).willThrow(MemberNotFoundException.class); mockMvc.perform(get("/api/members/me") diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java index 753be2067..8296decd0 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/controller/SearchingReviewControllerTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.woowacourse.moamoa.common.RepositoryTest; +import com.woowacourse.moamoa.common.utils.DateTimeSystem; import com.woowacourse.moamoa.member.domain.Member; import com.woowacourse.moamoa.member.domain.repository.MemberRepository; import com.woowacourse.moamoa.member.query.data.MemberData; @@ -17,21 +18,17 @@ import com.woowacourse.moamoa.review.service.response.WriterResponse; import com.woowacourse.moamoa.study.domain.Study; import com.woowacourse.moamoa.study.domain.repository.StudyRepository; -import com.woowacourse.moamoa.common.utils.DateTimeSystem; import com.woowacourse.moamoa.study.service.StudyService; import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest; - import java.time.LocalDate; import java.util.List; import javax.persistence.EntityManager; - 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.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.jdbc.core.JdbcTemplate; @RepositoryTest class SearchingReviewControllerTest { diff --git a/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/BadRequestReviewWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/BadRequestReviewWebMvcTest.java index f62241fad..f9c6d2e0e 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/BadRequestReviewWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/review/webmvc/BadRequestReviewWebMvcTest.java @@ -18,7 +18,7 @@ class BadRequestReviewWebMvcTest extends WebMVCTest { @DisplayName("필수 데이터인 후기 내용이 공백인 경우 400을 반환한다.") @Test void requestByBlankContent() throws Exception { - final String token = "Bearer " + tokenProvider.createToken(1L); + final String token = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); final String content = objectMapper.writeValueAsString(new WriteReviewRequest("")); mockMvc.perform(post("/api/studies/1/reviews") @@ -32,7 +32,7 @@ void requestByBlankContent() throws Exception { @DisplayName("필수 데이터인 후기 내용이 null 값인 경우 400을 반환한다.") @Test void requestByEmptyContent() throws Exception { - final String token = "Bearer " + tokenProvider.createToken(1L); + final String token = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); mockMvc.perform(post("/api/studies/1/reviews") .header(HttpHeaders.AUTHORIZATION, token) diff --git a/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java b/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java index a6d5b946c..c3e25b2b2 100644 --- a/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java +++ b/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/BadRequestMyMemberRoleWebMvcTest.java @@ -16,7 +16,7 @@ public class BadRequestMyMemberRoleWebMvcTest extends WebMVCTest { @DisplayName("study Id가 없을 경우 400 에러가 발생한다. ") @Test void getMyStudiesWithoutStudyId() throws Exception { - final String token = "Bearer " + tokenProvider.createToken(1L); + final String token = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); mockMvc.perform(get("/api/members/me/role") .header(HttpHeaders.AUTHORIZATION, token) @@ -28,7 +28,7 @@ void getMyStudiesWithoutStudyId() throws Exception { @DisplayName("study Id가 String일 경우 400 에러가 발생한다.") @Test void getMyStudiesWithoutStudyId1() throws Exception { - final String token = "Bearer " + tokenProvider.createToken(1L); + final String token = "Bearer " + tokenProvider.createToken(1L).getAccessToken(); mockMvc.perform(get("/api/members/me/role") .param("study-id", "one") diff --git a/backend/src/test/resources/schema.sql b/backend/src/test/resources/schema.sql index a08adda05..6d6ff5c7f 100644 --- a/backend/src/test/resources/schema.sql +++ b/backend/src/test/resources/schema.sql @@ -5,6 +5,7 @@ DROP TABLE IF EXISTS category; DROP TABLE IF EXISTS review; DROP TABLE IF EXISTS study; DROP TABLE IF EXISTS member; +DROP TABLE IF EXISTS token; CREATE TABLE member ( @@ -79,3 +80,10 @@ CREATE TABLE study_member FOREIGN KEY (study_id) REFERENCES study (id), FOREIGN KEY (member_id) REFERENCES member (id) ); + +CREATE TABLE token +( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + github_id BIGINT NOT NULL UNIQUE, + refresh_token MEDIUMTEXT +);