diff --git a/backend-submodule b/backend-submodule index f0d09e17..50c926ca 160000 --- a/backend-submodule +++ b/backend-submodule @@ -1 +1 @@ -Subproject commit f0d09e1762db4da9943ef2c856dff6da3a0bb6f4 +Subproject commit 50c926cae9148cb23709e9554abe766063aecc48 diff --git a/src/main/kotlin/com/petqua/application/auth/AuthDtos.kt b/src/main/kotlin/com/petqua/application/auth/AuthDtos.kt index a749bda7..4aca6776 100644 --- a/src/main/kotlin/com/petqua/application/auth/AuthDtos.kt +++ b/src/main/kotlin/com/petqua/application/auth/AuthDtos.kt @@ -1,6 +1,6 @@ package com.petqua.application.auth -data class AuthResponse( +data class AuthTokenInfo( val accessToken: String, val refreshToken: String, -) \ No newline at end of file +) diff --git a/src/main/kotlin/com/petqua/application/auth/OauthService.kt b/src/main/kotlin/com/petqua/application/auth/AuthService.kt similarity index 64% rename from src/main/kotlin/com/petqua/application/auth/OauthService.kt rename to src/main/kotlin/com/petqua/application/auth/AuthService.kt index c5d7de78..f443fe6b 100644 --- a/src/main/kotlin/com/petqua/application/auth/OauthService.kt +++ b/src/main/kotlin/com/petqua/application/auth/AuthService.kt @@ -1,5 +1,10 @@ package com.petqua.application.auth +import com.petqua.common.domain.findByIdOrThrow +import com.petqua.exception.auth.AuthException +import com.petqua.exception.auth.AuthExceptionType.EXPIRED_REFRESH_TOKEN +import com.petqua.exception.auth.AuthExceptionType.INVALID_REFRESH_TOKEN +import com.petqua.exception.auth.AuthExceptionType.NOT_RENEWABLE_ACCESS_TOKEN import com.petqua.domain.auth.Authority.MEMBER import com.petqua.domain.auth.oauth.OauthClientProvider import com.petqua.domain.auth.oauth.OauthServerType @@ -17,7 +22,7 @@ import java.util.Date @Transactional @Service -class OauthService( +class AuthService( private val oauthClientProvider: OauthClientProvider, private val memberRepository: MemberRepository, private val authTokenProvider: AuthTokenProvider, @@ -29,18 +34,42 @@ class OauthService( return oauthClient.getAuthCodeRequestUrl() } - fun login(oauthServerType: OauthServerType, code: String): AuthResponse { + fun login(oauthServerType: OauthServerType, code: String): AuthTokenInfo { val oauthClient = oauthClientProvider.getOauthClient(oauthServerType) val oauthUserInfo = oauthClient.requestOauthUserInfo(oauthClient.requestToken(code)) val member = getMemberByOauthInfo(oauthUserInfo.oauthId, oauthServerType) ?: createMember(oauthUserInfo, oauthServerType) val authToken = createAuthToken(member) - return AuthResponse( + return AuthTokenInfo( accessToken = authToken.accessToken, refreshToken = authToken.refreshToken, ) } + fun extendLogin(accessToken: String, refreshToken: String): AuthTokenInfo { + validateTokenExpiredStatusForExtendLogin(accessToken, refreshToken) + val savedRefreshToken = refreshTokenRepository.findByToken(refreshToken) + ?: throw AuthException(INVALID_REFRESH_TOKEN) + if (savedRefreshToken.token != refreshToken) { + throw AuthException(INVALID_REFRESH_TOKEN) + } + val member = memberRepository.findByIdOrThrow(savedRefreshToken.memberId) + val authToken = createAuthToken(member) + return AuthTokenInfo( + accessToken = authToken.accessToken, + refreshToken = authToken.refreshToken, + ) + } + + private fun validateTokenExpiredStatusForExtendLogin(accessToken: String, refreshToken: String) { + if (!authTokenProvider.isExpiredAccessToken(accessToken)) { + throw AuthException(NOT_RENEWABLE_ACCESS_TOKEN) + } + if (authTokenProvider.isExpiredRefreshToken(refreshToken)) { + throw AuthException(EXPIRED_REFRESH_TOKEN) + } + } + private fun getMemberByOauthInfo(oauthId: String, oauthServerType: OauthServerType): Member? { return memberRepository.findByOauthIdAndOauthServerNumber(oauthId, oauthServerType.number) } diff --git a/src/main/kotlin/com/petqua/domain/auth/Authority.kt b/src/main/kotlin/com/petqua/domain/auth/Authority.kt index e9e24871..425ef340 100644 --- a/src/main/kotlin/com/petqua/domain/auth/Authority.kt +++ b/src/main/kotlin/com/petqua/domain/auth/Authority.kt @@ -1,7 +1,7 @@ package com.petqua.domain.auth -import com.petqua.common.exception.auth.AuthException -import com.petqua.common.exception.auth.AuthExceptionType +import com.petqua.exception.auth.AuthException +import com.petqua.exception.auth.AuthExceptionType import java.util.Locale enum class Authority { diff --git a/src/main/kotlin/com/petqua/domain/auth/oauth/OauthClientProvider.kt b/src/main/kotlin/com/petqua/domain/auth/oauth/OauthClientProvider.kt index 428a6beb..ee61ad19 100644 --- a/src/main/kotlin/com/petqua/domain/auth/oauth/OauthClientProvider.kt +++ b/src/main/kotlin/com/petqua/domain/auth/oauth/OauthClientProvider.kt @@ -1,7 +1,7 @@ package com.petqua.domain.auth.oauth -import com.petqua.common.exception.auth.OauthClientException -import com.petqua.common.exception.auth.OauthClientExceptionType.UNSUPPORTED_OAUTH_SERVER_TYPE +import com.petqua.exception.auth.OauthClientException +import com.petqua.exception.auth.OauthClientExceptionType.UNSUPPORTED_OAUTH_SERVER_TYPE import org.springframework.stereotype.Component @Component diff --git a/src/main/kotlin/com/petqua/domain/auth/oauth/OauthServerType.kt b/src/main/kotlin/com/petqua/domain/auth/oauth/OauthServerType.kt index 29c26cee..e7f34346 100644 --- a/src/main/kotlin/com/petqua/domain/auth/oauth/OauthServerType.kt +++ b/src/main/kotlin/com/petqua/domain/auth/oauth/OauthServerType.kt @@ -1,7 +1,7 @@ package com.petqua.domain.auth.oauth -import com.petqua.common.exception.auth.OauthClientException -import com.petqua.common.exception.auth.OauthClientExceptionType.UNSUPPORTED_OAUTH_SERVER_TYPE +import com.petqua.exception.auth.OauthClientException +import com.petqua.exception.auth.OauthClientExceptionType.UNSUPPORTED_OAUTH_SERVER_TYPE import java.util.Locale.ENGLISH enum class OauthServerType( diff --git a/src/main/kotlin/com/petqua/domain/auth/token/AccessTokenClaims.kt b/src/main/kotlin/com/petqua/domain/auth/token/AccessTokenClaims.kt index e745a29d..384c51b1 100644 --- a/src/main/kotlin/com/petqua/domain/auth/token/AccessTokenClaims.kt +++ b/src/main/kotlin/com/petqua/domain/auth/token/AccessTokenClaims.kt @@ -1,7 +1,7 @@ package com.petqua.domain.auth.token -import com.petqua.common.exception.auth.AuthException -import com.petqua.common.exception.auth.AuthExceptionType.INVALID_ACCESS_TOKEN +import com.petqua.exception.auth.AuthException +import com.petqua.exception.auth.AuthExceptionType.INVALID_ACCESS_TOKEN import com.petqua.domain.auth.Authority private const val MEMBER_ID = "memberId" diff --git a/src/main/kotlin/com/petqua/domain/auth/token/AuthTokenProvider.kt b/src/main/kotlin/com/petqua/domain/auth/token/AuthTokenProvider.kt index 4d5d7352..d6b0bdb6 100644 --- a/src/main/kotlin/com/petqua/domain/auth/token/AuthTokenProvider.kt +++ b/src/main/kotlin/com/petqua/domain/auth/token/AuthTokenProvider.kt @@ -1,9 +1,9 @@ package com.petqua.domain.auth.token -import com.petqua.common.exception.auth.AuthException -import com.petqua.common.exception.auth.AuthExceptionType.EXPIRED_TOKEN -import com.petqua.common.exception.auth.AuthExceptionType.INVALID_ACCESS_TOKEN -import com.petqua.common.exception.auth.AuthExceptionType.INVALID_TOKEN +import com.petqua.exception.auth.AuthException +import com.petqua.exception.auth.AuthExceptionType.EXPIRED_ACCESS_TOKEN +import com.petqua.exception.auth.AuthExceptionType.INVALID_ACCESS_TOKEN +import com.petqua.exception.auth.AuthExceptionType.INVALID_REFRESH_TOKEN import com.petqua.domain.member.Member import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.JwtException @@ -39,15 +39,31 @@ class AuthTokenProvider( return jwtProvider.isValidToken(token) } - fun getAccessTokenClaims(token: String): AccessTokenClaims { + fun getAccessTokenClaimsOrThrow(token: String): AccessTokenClaims { try { return AccessTokenClaims.from(jwtProvider.getPayload(token)) } catch (e: ExpiredJwtException) { - throw AuthException(EXPIRED_TOKEN) + throw AuthException(EXPIRED_ACCESS_TOKEN) } catch (e: JwtException) { - throw AuthException(INVALID_TOKEN) + throw AuthException(INVALID_ACCESS_TOKEN) } catch (e: NullPointerException) { throw AuthException(INVALID_ACCESS_TOKEN) } } + + fun isExpiredAccessToken(token: String): Boolean { + try { + return jwtProvider.isExpiredToken(token) + } catch (e: JwtException) { + throw AuthException(INVALID_ACCESS_TOKEN) + } + } + + fun isExpiredRefreshToken(token: String): Boolean { + try { + return jwtProvider.isExpiredToken(token) + } catch (e: JwtException) { + throw AuthException(INVALID_REFRESH_TOKEN) + } + } } diff --git a/src/main/kotlin/com/petqua/domain/auth/token/JwtProvider.kt b/src/main/kotlin/com/petqua/domain/auth/token/JwtProvider.kt index 450779cb..4458de24 100644 --- a/src/main/kotlin/com/petqua/domain/auth/token/JwtProvider.kt +++ b/src/main/kotlin/com/petqua/domain/auth/token/JwtProvider.kt @@ -1,6 +1,7 @@ package com.petqua.domain.auth.token import io.jsonwebtoken.Claims +import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.Header.JWT_TYPE import io.jsonwebtoken.Header.TYPE import io.jsonwebtoken.Jws @@ -55,6 +56,15 @@ class JwtProvider( return true } + fun isExpiredToken(token: String): Boolean { + try { + parseToken(token) + } catch (e: ExpiredJwtException) { + return true + } + return false + } + fun getPayload(token: String): Map { val tokenClaims = parseToken(token) return tokenClaims.body.entries.associate {(key, value) -> diff --git a/src/main/kotlin/com/petqua/domain/auth/token/RefreshTokenRepository.kt b/src/main/kotlin/com/petqua/domain/auth/token/RefreshTokenRepository.kt index c7753c00..d46ff314 100644 --- a/src/main/kotlin/com/petqua/domain/auth/token/RefreshTokenRepository.kt +++ b/src/main/kotlin/com/petqua/domain/auth/token/RefreshTokenRepository.kt @@ -6,7 +6,7 @@ interface RefreshTokenRepository : CrudRepository { fun existsByToken(token: String): Boolean - fun findByMemberId(memberId: Long): RefreshToken? + fun findByToken(token: String): RefreshToken? fun deleteByMemberId(memberId: Long) } diff --git a/src/main/kotlin/com/petqua/common/exception/auth/AuthException.kt b/src/main/kotlin/com/petqua/exception/auth/AuthException.kt similarity index 87% rename from src/main/kotlin/com/petqua/common/exception/auth/AuthException.kt rename to src/main/kotlin/com/petqua/exception/auth/AuthException.kt index afc0917b..d01283d1 100644 --- a/src/main/kotlin/com/petqua/common/exception/auth/AuthException.kt +++ b/src/main/kotlin/com/petqua/exception/auth/AuthException.kt @@ -1,4 +1,4 @@ -package com.petqua.common.exception.auth +package com.petqua.exception.auth import com.petqua.common.exception.BaseException import com.petqua.common.exception.BaseExceptionType diff --git a/src/main/kotlin/com/petqua/common/exception/auth/AuthExceptionType.kt b/src/main/kotlin/com/petqua/exception/auth/AuthExceptionType.kt similarity index 72% rename from src/main/kotlin/com/petqua/common/exception/auth/AuthExceptionType.kt rename to src/main/kotlin/com/petqua/exception/auth/AuthExceptionType.kt index 61991b00..5ccc896a 100644 --- a/src/main/kotlin/com/petqua/common/exception/auth/AuthExceptionType.kt +++ b/src/main/kotlin/com/petqua/exception/auth/AuthExceptionType.kt @@ -1,4 +1,4 @@ -package com.petqua.common.exception.auth +package com.petqua.exception.auth import com.petqua.common.exception.BaseExceptionType import org.springframework.http.HttpStatus @@ -9,14 +9,14 @@ enum class AuthExceptionType( private val errorMessage: String, ) : BaseExceptionType { - INVALID_TOKEN(BAD_REQUEST, "유효하지 않은 토큰입니다."), - EXPIRED_TOKEN(BAD_REQUEST, "만료된 토큰입니다."), + EXPIRED_ACCESS_TOKEN(BAD_REQUEST, "만료된 AccessToken입니다."), + EXPIRED_REFRESH_TOKEN(BAD_REQUEST, "만료된 RefreshToken입니다."), + + NOT_RENEWABLE_ACCESS_TOKEN(BAD_REQUEST, "유효한 AccessToken은 갱신할 수 없습니다."), INVALID_ACCESS_TOKEN(BAD_REQUEST, "올바른 형태의 AccessToken이 아닙니다."), INVALID_REFRESH_TOKEN(BAD_REQUEST, "올바른 형태의 RefreshToken이 아닙니다."), UNSUPPORTED_AUTHORITY(BAD_REQUEST, "해당하는 권한이 존재하지 않습니다."), - - INVALID_REQUEST(BAD_REQUEST, "올바르지 않은 요청입니다."), ; override fun httpStatus(): HttpStatus { diff --git a/src/main/kotlin/com/petqua/common/exception/auth/OauthClientException.kt b/src/main/kotlin/com/petqua/exception/auth/OauthClientException.kt similarity index 88% rename from src/main/kotlin/com/petqua/common/exception/auth/OauthClientException.kt rename to src/main/kotlin/com/petqua/exception/auth/OauthClientException.kt index f64b82f1..934fb3d3 100644 --- a/src/main/kotlin/com/petqua/common/exception/auth/OauthClientException.kt +++ b/src/main/kotlin/com/petqua/exception/auth/OauthClientException.kt @@ -1,4 +1,4 @@ -package com.petqua.common.exception.auth +package com.petqua.exception.auth import com.petqua.common.exception.BaseException import com.petqua.common.exception.BaseExceptionType diff --git a/src/main/kotlin/com/petqua/common/exception/auth/OauthClientExceptionType.kt b/src/main/kotlin/com/petqua/exception/auth/OauthClientExceptionType.kt similarity index 93% rename from src/main/kotlin/com/petqua/common/exception/auth/OauthClientExceptionType.kt rename to src/main/kotlin/com/petqua/exception/auth/OauthClientExceptionType.kt index a607f578..1dacdc1b 100644 --- a/src/main/kotlin/com/petqua/common/exception/auth/OauthClientExceptionType.kt +++ b/src/main/kotlin/com/petqua/exception/auth/OauthClientExceptionType.kt @@ -1,4 +1,4 @@ -package com.petqua.common.exception.auth +package com.petqua.exception.auth import com.petqua.common.exception.BaseExceptionType import org.springframework.http.HttpStatus diff --git a/src/main/kotlin/com/petqua/presentation/auth/AuthController.kt b/src/main/kotlin/com/petqua/presentation/auth/AuthController.kt new file mode 100644 index 00000000..511d5bb4 --- /dev/null +++ b/src/main/kotlin/com/petqua/presentation/auth/AuthController.kt @@ -0,0 +1,74 @@ +package com.petqua.presentation.auth + +import com.petqua.application.auth.AuthService +import com.petqua.application.auth.AuthTokenInfo +import com.petqua.domain.auth.oauth.OauthServerType +import org.springframework.http.HttpHeaders.AUTHORIZATION +import org.springframework.http.HttpHeaders.SET_COOKIE +import org.springframework.http.HttpStatus.FOUND +import org.springframework.http.ResponseCookie +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.CookieValue +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RequestMapping("/auth") +@RestController +class AuthController( + private val authService: AuthService +) { + + @GetMapping("/{oauthServerType}") + fun redirectToAuthCodeRequestUrl( + @PathVariable oauthServerType: OauthServerType, + ): ResponseEntity { + val redirectUri = authService.getAuthCodeRequestUrl(oauthServerType) + return ResponseEntity.status(FOUND) + .location(redirectUri) + .build() + } + + @GetMapping("/login/{oauthServerType}") + fun login( + @PathVariable oauthServerType: OauthServerType, + @RequestParam("code") code: String, + ): ResponseEntity { + val authTokenInfo = authService.login(oauthServerType, code) + val refreshTokenCookie = createRefreshTokenCookie(authTokenInfo) + val authResponse = AuthResponse( + accessToken = authTokenInfo.accessToken + ) + return ResponseEntity + .ok() + .header(SET_COOKIE, refreshTokenCookie.toString()) + .body(authResponse) + } + + @GetMapping("/token") + fun extendLogin( + @RequestHeader(AUTHORIZATION) accessToken: String, + @CookieValue("refresh-token") refreshToken: String, + ): ResponseEntity { + val authTokenInfo = authService.extendLogin(accessToken, refreshToken) + val refreshTokenCookie = createRefreshTokenCookie(authTokenInfo) + val authResponse = AuthResponse( + accessToken = authTokenInfo.accessToken + ) + return ResponseEntity + .ok() + .header(SET_COOKIE, refreshTokenCookie.toString()) + .body(authResponse) + } + + private fun createRefreshTokenCookie(authTokenInfo: AuthTokenInfo): ResponseCookie { + return ResponseCookie.from("refresh-token", authTokenInfo.refreshToken) + .sameSite("None") + .secure(true) + .httpOnly(true) + .build() + } +} diff --git a/src/main/kotlin/com/petqua/presentation/auth/AuthDtos.kt b/src/main/kotlin/com/petqua/presentation/auth/AuthDtos.kt new file mode 100644 index 00000000..e5769e38 --- /dev/null +++ b/src/main/kotlin/com/petqua/presentation/auth/AuthDtos.kt @@ -0,0 +1,5 @@ +package com.petqua.presentation.auth + +data class AuthResponse( + val accessToken: String, +) diff --git a/src/main/kotlin/com/petqua/presentation/auth/LoginArgumentResolver.kt b/src/main/kotlin/com/petqua/presentation/auth/LoginArgumentResolver.kt index 6e927c24..fba71621 100644 --- a/src/main/kotlin/com/petqua/presentation/auth/LoginArgumentResolver.kt +++ b/src/main/kotlin/com/petqua/presentation/auth/LoginArgumentResolver.kt @@ -1,26 +1,19 @@ package com.petqua.presentation.auth -import com.petqua.common.exception.auth.AuthException -import com.petqua.common.exception.auth.AuthExceptionType import com.petqua.domain.auth.Auth import com.petqua.domain.auth.LoginMember import com.petqua.domain.auth.token.AuthTokenProvider -import com.petqua.domain.auth.token.RefreshTokenRepository -import jakarta.servlet.http.HttpServletRequest import org.springframework.core.MethodParameter -import org.springframework.http.HttpHeaders +import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.stereotype.Component import org.springframework.web.bind.support.WebDataBinderFactory import org.springframework.web.context.request.NativeWebRequest import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.ModelAndViewContainer -private const val REFRESH_TOKEN_COOKIE = "refresh-token" - @Component class LoginArgumentResolver( private val authTokenProvider: AuthTokenProvider, - private val refreshTokenRepository: RefreshTokenRepository ) : HandlerMethodArgumentResolver { override fun supportsParameter(parameter: MethodParameter): Boolean { @@ -34,20 +27,8 @@ class LoginArgumentResolver( webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory? ): LoginMember { - val request = webRequest.getNativeRequest(HttpServletRequest::class.java) - ?: throw AuthException(AuthExceptionType.INVALID_REQUEST) - val refreshToken = request.cookies?.find { it.name == REFRESH_TOKEN_COOKIE }?.value - val accessToken = webRequest.getHeader(HttpHeaders.AUTHORIZATION) as String - val accessTokenClaims = authTokenProvider.getAccessTokenClaims(accessToken) - if (refreshToken == null) { - return LoginMember.from(accessTokenClaims) - } - - val savedRefreshToken = refreshTokenRepository.findByMemberId(accessTokenClaims.memberId) - ?: throw AuthException(AuthExceptionType.INVALID_REFRESH_TOKEN) - if (savedRefreshToken.token == refreshToken) { - return LoginMember.from(accessTokenClaims) - } - throw AuthException(AuthExceptionType.INVALID_REFRESH_TOKEN) + val accessToken = webRequest.getHeader(AUTHORIZATION) as String + val accessTokenClaims = authTokenProvider.getAccessTokenClaimsOrThrow(accessToken) + return LoginMember.from(accessTokenClaims) } } diff --git a/src/main/kotlin/com/petqua/presentation/auth/OauthController.kt b/src/main/kotlin/com/petqua/presentation/auth/OauthController.kt deleted file mode 100644 index 29fb2635..00000000 --- a/src/main/kotlin/com/petqua/presentation/auth/OauthController.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.petqua.presentation.auth - -import com.petqua.application.auth.AuthResponse -import com.petqua.application.auth.OauthService -import com.petqua.domain.auth.oauth.OauthServerType -import org.springframework.http.HttpHeaders.SET_COOKIE -import org.springframework.http.HttpStatus.FOUND -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.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController - -@RequestMapping("/oauth") -@RestController -class OauthController( - private val oauthService: OauthService -) { - - @GetMapping("/{oauthServerType}") - fun redirectToAuthCodeRequestUrl( - @PathVariable oauthServerType: OauthServerType, - ): ResponseEntity { - val redirectUri = oauthService.getAuthCodeRequestUrl(oauthServerType) - return ResponseEntity.status(FOUND) - .location(redirectUri) - .build() - } - - @GetMapping("/login/{oauthServerType}") - fun login( - @PathVariable oauthServerType: OauthServerType, - @RequestParam("code") code: String, - ): ResponseEntity { - val oauthResponse = oauthService.login(oauthServerType, code) - return ResponseEntity - .ok() - .header(SET_COOKIE, oauthResponse.refreshToken) - .body(oauthResponse) - } -} diff --git a/src/test/kotlin/com/petqua/application/auth/AuthServiceTest.kt b/src/test/kotlin/com/petqua/application/auth/AuthServiceTest.kt new file mode 100644 index 00000000..e702a44e --- /dev/null +++ b/src/test/kotlin/com/petqua/application/auth/AuthServiceTest.kt @@ -0,0 +1,146 @@ +package com.petqua.application.auth + +import com.petqua.exception.auth.AuthException +import com.petqua.exception.auth.AuthExceptionType +import com.petqua.domain.auth.oauth.OauthServerType.KAKAO +import com.petqua.domain.auth.token.AuthTokenProvider +import com.petqua.domain.auth.token.RefreshToken +import com.petqua.domain.auth.token.RefreshTokenRepository +import com.petqua.domain.member.MemberRepository +import com.petqua.test.DataCleaner +import com.petqua.test.config.OauthTestConfig +import com.petqua.test.fixture.member +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE +import org.springframework.context.annotation.Import +import java.lang.System.currentTimeMillis +import java.util.Date + + +@SpringBootTest(webEnvironment = NONE) +@Import(OauthTestConfig::class) +class AuthServiceTest( + private var authService: AuthService, + private val refreshTokenRepository: RefreshTokenRepository, + private val authTokenProvider: AuthTokenProvider, + private val memberRepository: MemberRepository, + private val dataCleaner: DataCleaner, +) : BehaviorSpec({ + + Given("카카오 소셜 로그인을") { + + When("요청하면") { + val authTokenInfo = authService.login(KAKAO, "accessCode") + + Then("멤버의 인증 토큰을 발급한다") { + assertSoftly(authTokenInfo) { + authTokenProvider.isValidToken(accessToken) shouldBe true + authTokenProvider.isValidToken(refreshToken) shouldBe true + } + } + + Then("발급한 refreshToken을 저장한다") { + refreshTokenRepository.existsByToken(authTokenInfo.refreshToken) shouldBe true + } + } + } + + Given("로그인 연장을") { + val member = memberRepository.save(member()) + val expiredAccessToken = authTokenProvider.createAuthToken(member, Date(0)).accessToken + val refreshToken = authTokenProvider.createAuthToken(member, Date()).refreshToken + refreshTokenRepository.save( + RefreshToken( + memberId = member.id, + token = refreshToken + ) + ) + + When("요청하면") { + val authTokenInfo = authService.extendLogin(expiredAccessToken, refreshToken) + + Then("멤버의 인증 토큰을 발급한다") { + authTokenProvider.isValidToken(authTokenInfo.accessToken) shouldBe true + authTokenProvider.isValidToken(authTokenInfo.refreshToken) shouldBe true + } + + Then("발급한 refreshToken을 저장한다") { + refreshTokenRepository.existsByToken(authTokenInfo.refreshToken) shouldBe true + } + } + } + + Given("로그인 연장 요청시") { + val member = memberRepository.save(member()) + + When("AccessToken이 만료되지 않은 경우") { + val authToken = authTokenProvider.createAuthToken(member, Date()) + refreshTokenRepository.save( + RefreshToken( + memberId = member.id, + token = authToken.refreshToken + ) + ) + + Then("예외가 발생한다") { + shouldThrow { + authService.extendLogin(authToken.accessToken, authToken.refreshToken) + }.exceptionType() shouldBe AuthExceptionType.NOT_RENEWABLE_ACCESS_TOKEN + } + } + + When("RefreshToken이 만료된 경우") { + val expiredAuthToken = authTokenProvider.createAuthToken(member, Date(0)) + refreshTokenRepository.save( + RefreshToken( + memberId = member.id, + token = expiredAuthToken.refreshToken + ) + ) + + Then("예외가 발생한다") { + shouldThrow { + authService.extendLogin(expiredAuthToken.accessToken, expiredAuthToken.refreshToken) + }.exceptionType() shouldBe AuthExceptionType.EXPIRED_REFRESH_TOKEN + } + } + + When("RefreshToken이 저장되어있지 않은 경우") { + val expiredAccessToken = authTokenProvider.createAuthToken(member, Date(0)).accessToken + val unsavedRefreshToken = authTokenProvider.createAuthToken(member, Date()).refreshToken + + Then("예외가 발생한다") { + shouldThrow { + authService.extendLogin(expiredAccessToken, unsavedRefreshToken) + }.exceptionType() shouldBe AuthExceptionType.INVALID_REFRESH_TOKEN + } + } + + When("RefreshToken이 저장된 토큰값과 다른 경우") { + val expiredAccessToken = authTokenProvider.createAuthToken(member, Date(0)).accessToken + val oneMinuteAgoMillSec = currentTimeMillis() - 60 * 1000 + val unsavedRefreshToken = authTokenProvider.createAuthToken(member, Date(oneMinuteAgoMillSec)).refreshToken + val refreshToken = authTokenProvider.createAuthToken(member, Date()).refreshToken + refreshTokenRepository.save( + RefreshToken( + memberId = member.id, + token = refreshToken + ) + ) + + Then("예외가 발생한다") { + shouldThrow { + authService.extendLogin(expiredAccessToken, unsavedRefreshToken) + }.exceptionType() shouldBe AuthExceptionType.INVALID_REFRESH_TOKEN + } + } + } + + afterContainer { + dataCleaner.clean() + } +}) diff --git a/src/test/kotlin/com/petqua/application/auth/OauthServiceTest.kt b/src/test/kotlin/com/petqua/application/auth/OauthServiceTest.kt deleted file mode 100644 index 7639ede7..00000000 --- a/src/test/kotlin/com/petqua/application/auth/OauthServiceTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.petqua.application.auth - -import com.petqua.domain.auth.oauth.OauthServerType.KAKAO -import com.petqua.domain.auth.token.JwtProvider -import com.petqua.domain.auth.token.RefreshTokenRepository -import com.petqua.test.config.OauthTestConfig -import io.kotest.assertions.assertSoftly -import io.kotest.core.spec.style.BehaviorSpec -import io.kotest.matchers.shouldBe -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE -import org.springframework.context.annotation.Import - -@SpringBootTest(webEnvironment = NONE) -@Import(OauthTestConfig::class) -class OauthServiceTest( - private var oauthService: OauthService, - private val refreshTokenRepository: RefreshTokenRepository, - private val jwtProvider: JwtProvider, -) : BehaviorSpec({ - - Given("소셜 로그인 테스트") { - - When("OauthServerType과 accessCode를 가지고 로그인을 하면") { - val oauthResponse = oauthService.login(KAKAO, "accessCode") - - Then("멤버의 인증 토큰을 발급한다") { - assertSoftly(oauthResponse) { - jwtProvider.isValidToken(accessToken) shouldBe true - jwtProvider.isValidToken(refreshToken) shouldBe true - } - } - - Then("발급한 refreshToken을 저장한다") { - refreshTokenRepository.existsByToken(oauthResponse.refreshToken) shouldBe true - } - } - } -}) diff --git a/src/test/kotlin/com/petqua/domain/auth/AuthTokenProviderTest.kt b/src/test/kotlin/com/petqua/domain/auth/AuthTokenProviderTest.kt index e7257a13..82a54ded 100644 --- a/src/test/kotlin/com/petqua/domain/auth/AuthTokenProviderTest.kt +++ b/src/test/kotlin/com/petqua/domain/auth/AuthTokenProviderTest.kt @@ -33,7 +33,7 @@ class AuthTokenProviderTest( val authToken = authTokenProvider.createAuthToken(member, issuedDate) val accessTokenExpirationTime = parseExpirationTime(jwtProvider.parseToken(authToken.accessToken)) val refreshTokenExpirationTime = parseExpirationTime(jwtProvider.parseToken(authToken.refreshToken)) - val accessTokenClaims = authTokenProvider.getAccessTokenClaims(authToken.accessToken) + val accessTokenClaims = authTokenProvider.getAccessTokenClaimsOrThrow(authToken.accessToken) Then("JWT타입인 accessToken과 refreshToken이 발급된다") { accessTokenClaims.memberId shouldBe member.id diff --git a/src/test/kotlin/com/petqua/presentation/auth/AuthControllerTest.kt b/src/test/kotlin/com/petqua/presentation/auth/AuthControllerTest.kt new file mode 100644 index 00000000..7b88d4ae --- /dev/null +++ b/src/test/kotlin/com/petqua/presentation/auth/AuthControllerTest.kt @@ -0,0 +1,86 @@ +package com.petqua.presentation.auth + +import com.petqua.domain.auth.token.AuthTokenProvider +import com.petqua.domain.auth.token.RefreshToken +import com.petqua.domain.auth.token.RefreshTokenRepository +import com.petqua.domain.member.MemberRepository +import com.petqua.test.ApiTestConfig +import com.petqua.test.config.OauthTestConfig +import com.petqua.test.fixture.member +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.restassured.module.kotlin.extensions.Extract +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import org.springframework.context.annotation.Import +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus.OK +import java.util.Date + +@Import(OauthTestConfig::class) +class AuthControllerTest( + private val memberRepository: MemberRepository, + private val refreshTokenRepository: RefreshTokenRepository, + private val authTokenProvider: AuthTokenProvider, +) : ApiTestConfig() { + + init { + Given("소셜 로그인을 할 때") { + + When("카카오 로그인을 시도하면") { + val response = Given { + log().all() + } When { + queryParam("code", "accessCode") + get("/auth/login/kakao") + } Then { + log().all() + } Extract { + response() + } + + Then("인증토큰이 반환된다.") { + val authResponse = response.`as`(AuthResponse::class.java) + + response.statusCode shouldBe OK.value() + authResponse.accessToken.shouldNotBeNull() + } + } + } + + Given("로그인 연장을") { + val member = memberRepository.save(member()) + val expiredAccessToken = authTokenProvider.createAuthToken(member, Date(0)).accessToken + val refreshToken = authTokenProvider.createAuthToken(member, Date()).refreshToken + refreshTokenRepository.save( + RefreshToken( + memberId = member.id, + token = refreshToken + ) + ) + + When("요청하면") { + val response = Given { + log().all() + .header(HttpHeaders.AUTHORIZATION, expiredAccessToken) + .cookie("refresh-token", refreshToken) + } When { + get("/auth/token") + } Then { + log().all() + } Extract { + response() + } + + Then("인증토큰이 반환된다.") { + val authResponse = response.`as`(AuthResponse::class.java) + + response.statusCode shouldBe OK.value() + authResponse.accessToken.shouldNotBeNull() + } + } + } + } +} diff --git a/src/test/kotlin/com/petqua/presentation/auth/OauthControllerTest.kt b/src/test/kotlin/com/petqua/presentation/auth/OauthControllerTest.kt deleted file mode 100644 index d9dad722..00000000 --- a/src/test/kotlin/com/petqua/presentation/auth/OauthControllerTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.petqua.presentation.auth - -import com.petqua.application.auth.AuthResponse -import com.petqua.test.ApiTestConfig -import com.petqua.test.config.OauthTestConfig -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.restassured.module.kotlin.extensions.Extract -import io.restassured.module.kotlin.extensions.Given -import io.restassured.module.kotlin.extensions.Then -import io.restassured.module.kotlin.extensions.When -import org.springframework.context.annotation.Import -import org.springframework.http.HttpStatus.OK - -@Import(OauthTestConfig::class) -class OauthControllerTest : ApiTestConfig() { - - init { - Given("소셜 로그인을 할 때") { - - When("카카오 로그인을 시도하면") { - val response = Given { - log().all() - } When { - queryParam("code", "accessCode") - get("/oauth/login/kakao") - } Then { - log().all() - } Extract { - response() - } - - Then("인증토큰이 반환된다.") { - val authResponse = response.`as`(AuthResponse::class.java) - - response.statusCode shouldBe OK.value() - authResponse.accessToken shouldNotBe null - authResponse.refreshToken shouldNotBe null - } - } - } - } -} diff --git a/src/test/kotlin/com/petqua/test/ApiTestConfig.kt b/src/test/kotlin/com/petqua/test/ApiTestConfig.kt index 069be5c5..7d132dd8 100644 --- a/src/test/kotlin/com/petqua/test/ApiTestConfig.kt +++ b/src/test/kotlin/com/petqua/test/ApiTestConfig.kt @@ -1,6 +1,6 @@ package com.petqua.test -import com.petqua.application.auth.AuthResponse +import com.petqua.presentation.auth.AuthResponse import com.petqua.test.config.OauthTestConfig import io.kotest.core.spec.style.BehaviorSpec import io.restassured.RestAssured @@ -45,7 +45,7 @@ abstract class ApiTestConfig : BehaviorSpec() { log().all() .queryParam("code", "code") } When { - get("/oauth/login/{oauthServerType}", "kakao") + get("/auth/login/{oauthServerType}", "kakao") } Then { log().all() } Extract {