From 9728b008859da27283c5a78559e66c121994ec28 Mon Sep 17 00:00:00 2001 From: Seokjin Jeon Date: Thu, 23 May 2024 00:28:25 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20refactor:=20JWT=EC=97=90=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=81=B4=EB=A0=88?= =?UTF-8?q?=EC=9E=84=EC=9D=84=20=EB=8B=B4=EA=B8=B0=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=ED=8E=B8=20?= =?UTF-8?q?(#982)=20(#985)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: MemberAuthCommandService oAuth2Login 메서드명 변경 - oAuth2Login -> login * refactor: Authentication을 사용한 인증/인가 리팩터링 * refactor: AuthenticationArgumentResolver 파라미터에 어노테이션 제거 * feat: 토큰 Subject에 식별자 추가 - 하위 호환성을 지키기 위해 기존 방식은 유지 --- .../AdminAuthenticationArgumentResolver.java | 40 +++++++++ .../AnnotationAuthorizationInterceptor.java | 52 ++++++++++++ .../com/festago/auth/AuthInterceptor.java | 82 ------------------- .../com/festago/auth/AuthenticateContext.java | 18 ++-- .../auth/FixedAuthorizationInterceptor.java | 49 +++++++++++ .../MemberAuthenticationArgumentResolver.java | 37 +++++++++ .../festago/auth/RoleArgumentResolver.java | 12 ++- .../auth/annotation/Authorization.java | 14 ++++ .../festago/auth/annotation/MemberAuth.java | 2 + .../auth/application/AuthTokenExtractor.java | 8 -- .../auth/application/AuthTokenProvider.java | 9 -- .../command/AdminAuthCommandService.java | 11 ++- .../command/MemberAuthCommandService.java | 2 +- .../command/MemberAuthFacadeService.java | 13 ++- .../com/festago/auth/config/AuthConfig.java | 24 ------ .../com/festago/auth/config/LoginConfig.java | 71 +++++++++++----- .../com/festago/auth/domain/AuthPayload.java | 29 ------- .../domain/AuthenticationTokenExtractor.java | 12 +++ .../authentication/AdminAuthentication.java | 26 ++++++ .../AnonymousAuthentication.java | 25 ++++++ .../domain/authentication/Authentication.java | 14 ++++ .../authentication/MemberAuthentication.java | 26 ++++++ .../AdminAuthenticationClaimsExtractor.java | 23 ++++++ .../AdminAuthenticationTokenExtractor.java | 21 +++++ .../AdminAuthenticationTokenProvider.java | 26 ++++++ .../AuthenticationClaimsExtractor.java | 13 +++ ...CompositeAuthenticationTokenExtractor.java | 30 +++++++ .../CompositeHttpRequestTokenExtractor.java | 24 ++++++ .../infrastructure/JwtAuthTokenExtractor.java | 59 ------------- .../infrastructure/JwtAuthTokenProvider.java | 44 ---------- .../auth/infrastructure/JwtTokenParser.java | 47 +++++++++++ .../MemberAuthenticationClaimsExtractor.java | 23 ++++++ .../MemberAuthenticationTokenExtractor.java | 21 +++++ .../MemberAuthenticationTokenProvider.java | 26 ++++++ .../infrastructure/TokenProviderTemplate.java | 43 ++++++++++ .../v1/AdminAuthV1Controller.java | 6 +- .../v1/MemberAuthV1Controller.java | 10 +-- .../command/AdminAuthCommandServiceTest.java | 10 +-- .../command/MemberAuthCommandServiceTest.java | 6 +- ...dminAuthenticationClaimsExtractorTest.java | 52 ++++++++++++ ...ositeAuthenticationTokenExtractorTest.java | 56 +++++++++++++ ...ompositeHttpRequestTokenExtractorTest.java | 52 ++++++++++++ ...actorTest.java => JwtTokenParserTest.java} | 71 +++++++--------- ...mberAuthenticationClaimsExtractorTest.java | 52 ++++++++++++ ...st.java => TokenProviderTemplateTest.java} | 27 ++++-- .../auth/RoleArgumentResolverTest.java | 82 ------------------- .../MockAuthTestExecutionListener.java | 15 +++- .../support/MockAuthTokenExtractor.java | 19 ----- .../MockAuthenticationTokenExtractor.java | 19 +++++ .../com/festago/support/TestAuthConfig.java | 6 +- 50 files changed, 982 insertions(+), 477 deletions(-) create mode 100644 backend/src/main/java/com/festago/auth/AdminAuthenticationArgumentResolver.java create mode 100644 backend/src/main/java/com/festago/auth/AnnotationAuthorizationInterceptor.java delete mode 100644 backend/src/main/java/com/festago/auth/AuthInterceptor.java create mode 100644 backend/src/main/java/com/festago/auth/FixedAuthorizationInterceptor.java create mode 100644 backend/src/main/java/com/festago/auth/MemberAuthenticationArgumentResolver.java create mode 100644 backend/src/main/java/com/festago/auth/annotation/Authorization.java delete mode 100644 backend/src/main/java/com/festago/auth/application/AuthTokenExtractor.java delete mode 100644 backend/src/main/java/com/festago/auth/application/AuthTokenProvider.java delete mode 100644 backend/src/main/java/com/festago/auth/domain/AuthPayload.java create mode 100644 backend/src/main/java/com/festago/auth/domain/AuthenticationTokenExtractor.java create mode 100644 backend/src/main/java/com/festago/auth/domain/authentication/AdminAuthentication.java create mode 100644 backend/src/main/java/com/festago/auth/domain/authentication/AnonymousAuthentication.java create mode 100644 backend/src/main/java/com/festago/auth/domain/authentication/Authentication.java create mode 100644 backend/src/main/java/com/festago/auth/domain/authentication/MemberAuthentication.java create mode 100644 backend/src/main/java/com/festago/auth/infrastructure/AdminAuthenticationClaimsExtractor.java create mode 100644 backend/src/main/java/com/festago/auth/infrastructure/AdminAuthenticationTokenExtractor.java create mode 100644 backend/src/main/java/com/festago/auth/infrastructure/AdminAuthenticationTokenProvider.java create mode 100644 backend/src/main/java/com/festago/auth/infrastructure/AuthenticationClaimsExtractor.java create mode 100644 backend/src/main/java/com/festago/auth/infrastructure/CompositeAuthenticationTokenExtractor.java create mode 100644 backend/src/main/java/com/festago/auth/infrastructure/CompositeHttpRequestTokenExtractor.java delete mode 100644 backend/src/main/java/com/festago/auth/infrastructure/JwtAuthTokenExtractor.java delete mode 100644 backend/src/main/java/com/festago/auth/infrastructure/JwtAuthTokenProvider.java create mode 100644 backend/src/main/java/com/festago/auth/infrastructure/JwtTokenParser.java create mode 100644 backend/src/main/java/com/festago/auth/infrastructure/MemberAuthenticationClaimsExtractor.java create mode 100644 backend/src/main/java/com/festago/auth/infrastructure/MemberAuthenticationTokenExtractor.java create mode 100644 backend/src/main/java/com/festago/auth/infrastructure/MemberAuthenticationTokenProvider.java create mode 100644 backend/src/main/java/com/festago/auth/infrastructure/TokenProviderTemplate.java create mode 100644 backend/src/test/java/com/festago/auth/infrastructure/AdminAuthenticationClaimsExtractorTest.java create mode 100644 backend/src/test/java/com/festago/auth/infrastructure/CompositeAuthenticationTokenExtractorTest.java create mode 100644 backend/src/test/java/com/festago/auth/infrastructure/CompositeHttpRequestTokenExtractorTest.java rename backend/src/test/java/com/festago/auth/infrastructure/{JwtAuthTokenExtractorTest.java => JwtTokenParserTest.java} (52%) create mode 100644 backend/src/test/java/com/festago/auth/infrastructure/MemberAuthenticationClaimsExtractorTest.java rename backend/src/test/java/com/festago/auth/infrastructure/{JwtAuthTokenProviderTest.java => TokenProviderTemplateTest.java} (50%) delete mode 100644 backend/src/test/java/com/festago/presentation/auth/RoleArgumentResolverTest.java delete mode 100644 backend/src/test/java/com/festago/support/MockAuthTokenExtractor.java create mode 100644 backend/src/test/java/com/festago/support/MockAuthenticationTokenExtractor.java diff --git a/backend/src/main/java/com/festago/auth/AdminAuthenticationArgumentResolver.java b/backend/src/main/java/com/festago/auth/AdminAuthenticationArgumentResolver.java new file mode 100644 index 000000000..c62ede6a2 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/AdminAuthenticationArgumentResolver.java @@ -0,0 +1,40 @@ +package com.festago.auth; + +import com.festago.auth.domain.authentication.AdminAuthentication; +import com.festago.auth.domain.authentication.Authentication; +import com.festago.common.exception.UnexpectedException; +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; +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; + +public class AdminAuthenticationArgumentResolver implements HandlerMethodArgumentResolver { + + private final AuthenticateContext authenticateContext; + + public AdminAuthenticationArgumentResolver(AuthenticateContext authenticateContext) { + Assert.notNull(authenticateContext, "The authenticateContext must not be null"); + this.authenticateContext = authenticateContext; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(AdminAuthentication.class); + } + + @Override + public AdminAuthentication resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) { + Authentication authentication = authenticateContext.getAuthentication(); + if (authentication instanceof AdminAuthentication adminAuthentication) { + return adminAuthentication; + } + throw new UnexpectedException("인가된 권한이 인자의 권한과 맞지 않습니다."); + } +} diff --git a/backend/src/main/java/com/festago/auth/AnnotationAuthorizationInterceptor.java b/backend/src/main/java/com/festago/auth/AnnotationAuthorizationInterceptor.java new file mode 100644 index 000000000..02eaefc0d --- /dev/null +++ b/backend/src/main/java/com/festago/auth/AnnotationAuthorizationInterceptor.java @@ -0,0 +1,52 @@ +package com.festago.auth; + +import com.festago.auth.annotation.Authorization; +import com.festago.auth.application.HttpRequestTokenExtractor; +import com.festago.auth.domain.AuthenticationTokenExtractor; +import com.festago.auth.domain.authentication.Authentication; +import com.festago.common.exception.ErrorCode; +import com.festago.common.exception.ForbiddenException; +import com.festago.common.exception.UnauthorizedException; +import com.festago.common.exception.UnexpectedException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.Assert; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +public class AnnotationAuthorizationInterceptor implements HandlerInterceptor { + + private final HttpRequestTokenExtractor httpRequestTokenExtractor; + private final AuthenticationTokenExtractor authenticationTokenExtractor; + private final AuthenticateContext authenticateContext; + + public AnnotationAuthorizationInterceptor( + HttpRequestTokenExtractor httpRequestTokenExtractor, + AuthenticationTokenExtractor authenticationTokenExtractor, + AuthenticateContext authenticateContext) + { + Assert.notNull(httpRequestTokenExtractor, "The httpRequestTokenExtractor must not be null"); + Assert.notNull(authenticationTokenExtractor, "The authenticationTokenExtractor must not be null"); + Assert.notNull(authenticateContext, "The authenticateContext must not be null"); + this.httpRequestTokenExtractor = httpRequestTokenExtractor; + this.authenticationTokenExtractor = authenticationTokenExtractor; + this.authenticateContext = authenticateContext; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + Authorization authorization = handlerMethod.getMethodAnnotation(Authorization.class); + if (authorization == null) { + throw new UnexpectedException("HandlerMethod에 Authorization 어노테이션이 없습니다."); + } + String token = httpRequestTokenExtractor.extract(request) + .orElseThrow(() -> new UnauthorizedException(ErrorCode.NEED_AUTH_TOKEN)); + Authentication authentication = authenticationTokenExtractor.extract(token); + if (authentication.getRole() != authorization.role()) { + throw new ForbiddenException(ErrorCode.NOT_ENOUGH_PERMISSION); + } + authenticateContext.setAuthentication(authentication); + return true; + } +} diff --git a/backend/src/main/java/com/festago/auth/AuthInterceptor.java b/backend/src/main/java/com/festago/auth/AuthInterceptor.java deleted file mode 100644 index 855bb1bab..000000000 --- a/backend/src/main/java/com/festago/auth/AuthInterceptor.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.festago.auth; - -import com.festago.auth.application.AuthTokenExtractor; -import com.festago.auth.application.HttpRequestTokenExtractor; -import com.festago.auth.domain.AuthPayload; -import com.festago.auth.domain.Role; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.ForbiddenException; -import com.festago.common.exception.UnauthorizedException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.util.Assert; -import org.springframework.web.servlet.HandlerInterceptor; - -public class AuthInterceptor implements HandlerInterceptor { - - private final AuthTokenExtractor authTokenExtractor; - private final HttpRequestTokenExtractor httpRequestTokenExtractor; - private final AuthenticateContext authenticateContext; - private final Role role; - - private AuthInterceptor(AuthTokenExtractor authTokenExtractor, HttpRequestTokenExtractor httpRequestTokenExtractor, - AuthenticateContext authenticateContext, Role role) { - Assert.notNull(authTokenExtractor, "The authExtractor must not be null"); - Assert.notNull(httpRequestTokenExtractor, "The tokenExtractor must not be null"); - Assert.notNull(authenticateContext, "The authenticateContext must not be null"); - Assert.notNull(role, "The role must not be null"); - this.authTokenExtractor = authTokenExtractor; - this.httpRequestTokenExtractor = httpRequestTokenExtractor; - this.authenticateContext = authenticateContext; - this.role = role; - } - - public static AuthInterceptorBuilder builder() { - return new AuthInterceptorBuilder(); - } - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - String token = httpRequestTokenExtractor.extract(request) - .orElseThrow(() -> new UnauthorizedException(ErrorCode.NEED_AUTH_TOKEN)); - AuthPayload payload = authTokenExtractor.extract(token); - if (payload.getRole() != this.role) { - throw new ForbiddenException(ErrorCode.NOT_ENOUGH_PERMISSION); - } - authenticateContext.setAuthenticate(payload.getMemberId(), payload.getRole()); - return true; - } - - public static class AuthInterceptorBuilder { - - private AuthTokenExtractor authTokenExtractor; - private HttpRequestTokenExtractor httpRequestTokenExtractor; - private AuthenticateContext authenticateContext; - private Role role; - - public AuthInterceptorBuilder authExtractor(AuthTokenExtractor authTokenExtractor) { - this.authTokenExtractor = authTokenExtractor; - return this; - } - - public AuthInterceptorBuilder authenticateContext(AuthenticateContext authenticateContext) { - this.authenticateContext = authenticateContext; - return this; - } - - public AuthInterceptorBuilder tokenExtractor(HttpRequestTokenExtractor httpRequestTokenExtractor) { - this.httpRequestTokenExtractor = httpRequestTokenExtractor; - return this; - } - - public AuthInterceptorBuilder role(Role role) { - this.role = role; - return this; - } - - public AuthInterceptor build() { - return new AuthInterceptor(authTokenExtractor, httpRequestTokenExtractor, authenticateContext, role); - } - } -} diff --git a/backend/src/main/java/com/festago/auth/AuthenticateContext.java b/backend/src/main/java/com/festago/auth/AuthenticateContext.java index 7af198861..ba36ddb17 100644 --- a/backend/src/main/java/com/festago/auth/AuthenticateContext.java +++ b/backend/src/main/java/com/festago/auth/AuthenticateContext.java @@ -1,6 +1,8 @@ package com.festago.auth; import com.festago.auth.domain.Role; +import com.festago.auth.domain.authentication.AnonymousAuthentication; +import com.festago.auth.domain.authentication.Authentication; import org.springframework.stereotype.Component; import org.springframework.web.context.annotation.RequestScope; @@ -8,19 +10,21 @@ @RequestScope public class AuthenticateContext { - private Long id; - private Role role = Role.ANONYMOUS; + private Authentication authentication = AnonymousAuthentication.getInstance(); - public void setAuthenticate(Long id, Role role) { - this.id = id; - this.role = role; + public void setAuthentication(Authentication authentication) { + this.authentication = authentication; } public Long getId() { - return id; + return authentication.getId(); } public Role getRole() { - return role; + return authentication.getRole(); + } + + public Authentication getAuthentication() { + return authentication; } } diff --git a/backend/src/main/java/com/festago/auth/FixedAuthorizationInterceptor.java b/backend/src/main/java/com/festago/auth/FixedAuthorizationInterceptor.java new file mode 100644 index 000000000..5689679fb --- /dev/null +++ b/backend/src/main/java/com/festago/auth/FixedAuthorizationInterceptor.java @@ -0,0 +1,49 @@ +package com.festago.auth; + +import com.festago.auth.application.HttpRequestTokenExtractor; +import com.festago.auth.domain.AuthenticationTokenExtractor; +import com.festago.auth.domain.Role; +import com.festago.auth.domain.authentication.Authentication; +import com.festago.common.exception.ErrorCode; +import com.festago.common.exception.ForbiddenException; +import com.festago.common.exception.UnauthorizedException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.Assert; +import org.springframework.web.servlet.HandlerInterceptor; + +public class FixedAuthorizationInterceptor implements HandlerInterceptor { + + private final HttpRequestTokenExtractor httpRequestTokenExtractor; + private final AuthenticationTokenExtractor authenticationTokenExtractor; + private final AuthenticateContext authenticateContext; + private final Role role; + + public FixedAuthorizationInterceptor( + HttpRequestTokenExtractor httpRequestTokenExtractor, + AuthenticationTokenExtractor authenticationTokenExtractor, + AuthenticateContext authenticateContext, + Role role + ) { + Assert.notNull(httpRequestTokenExtractor, "The httpRequestTokenExtractor must not be null"); + Assert.notNull(authenticationTokenExtractor, "The authenticationTokenExtractor must not be null"); + Assert.notNull(authenticateContext, "The authenticateContext must not be null"); + Assert.notNull(role, "The role must not be null"); + this.httpRequestTokenExtractor = httpRequestTokenExtractor; + this.authenticationTokenExtractor = authenticationTokenExtractor; + this.authenticateContext = authenticateContext; + this.role = role; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String token = httpRequestTokenExtractor.extract(request) + .orElseThrow(() -> new UnauthorizedException(ErrorCode.NEED_AUTH_TOKEN)); + Authentication authentication = authenticationTokenExtractor.extract(token); + if (authentication.getRole() != role) { + throw new ForbiddenException(ErrorCode.NOT_ENOUGH_PERMISSION); + } + authenticateContext.setAuthentication(authentication); + return true; + } +} diff --git a/backend/src/main/java/com/festago/auth/MemberAuthenticationArgumentResolver.java b/backend/src/main/java/com/festago/auth/MemberAuthenticationArgumentResolver.java new file mode 100644 index 000000000..12b219713 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/MemberAuthenticationArgumentResolver.java @@ -0,0 +1,37 @@ +package com.festago.auth; + +import com.festago.auth.domain.authentication.Authentication; +import com.festago.auth.domain.authentication.MemberAuthentication; +import com.festago.common.exception.UnexpectedException; +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; +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; + +public class MemberAuthenticationArgumentResolver implements HandlerMethodArgumentResolver { + + private final AuthenticateContext authenticateContext; + + public MemberAuthenticationArgumentResolver(AuthenticateContext authenticateContext) { + Assert.notNull(authenticateContext, "The authenticateContext must not be null"); + this.authenticateContext = authenticateContext; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(MemberAuthentication.class); + } + + @Override + public MemberAuthentication resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory + ) { + Authentication authentication = authenticateContext.getAuthentication(); + if (authentication instanceof MemberAuthentication memberAuthentication) { + return memberAuthentication; + } + throw new UnexpectedException("인가된 권한이 인자의 권한과 맞지 않습니다."); + } +} diff --git a/backend/src/main/java/com/festago/auth/RoleArgumentResolver.java b/backend/src/main/java/com/festago/auth/RoleArgumentResolver.java index 611588c72..451288621 100644 --- a/backend/src/main/java/com/festago/auth/RoleArgumentResolver.java +++ b/backend/src/main/java/com/festago/auth/RoleArgumentResolver.java @@ -1,8 +1,7 @@ package com.festago.auth; import com.festago.auth.domain.Role; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.ForbiddenException; +import com.festago.common.exception.UnexpectedException; import org.springframework.core.MethodParameter; import org.springframework.util.Assert; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -10,6 +9,10 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +/** + * @deprecated 기존 Long으로 식별자를 받는 Controller가 많기에, 해당 클래스 삭제하지 않고 유지 + */ +@Deprecated public class RoleArgumentResolver implements HandlerMethodArgumentResolver { private final Role role; @@ -30,9 +33,10 @@ public boolean supportsParameter(MethodParameter parameter) { @Override public Long resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + NativeWebRequest webRequest, WebDataBinderFactory binderFactory + ) { if (authenticateContext.getRole() != this.role) { - throw new ForbiddenException(ErrorCode.NOT_ENOUGH_PERMISSION); + throw new UnexpectedException("인가된 권한이 인자의 권한과 맞지 않습니다."); } return authenticateContext.getId(); } diff --git a/backend/src/main/java/com/festago/auth/annotation/Authorization.java b/backend/src/main/java/com/festago/auth/annotation/Authorization.java new file mode 100644 index 000000000..368831572 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/annotation/Authorization.java @@ -0,0 +1,14 @@ +package com.festago.auth.annotation; + +import com.festago.auth.domain.Role; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Authorization { + + Role role(); +} diff --git a/backend/src/main/java/com/festago/auth/annotation/MemberAuth.java b/backend/src/main/java/com/festago/auth/annotation/MemberAuth.java index a120042df..71184b3f2 100644 --- a/backend/src/main/java/com/festago/auth/annotation/MemberAuth.java +++ b/backend/src/main/java/com/festago/auth/annotation/MemberAuth.java @@ -1,5 +1,6 @@ package com.festago.auth.annotation; +import com.festago.auth.domain.Role; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -9,6 +10,7 @@ @SecurityRequirement(name = "bearerAuth") @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) +@Authorization(role = Role.MEMBER) public @interface MemberAuth { } diff --git a/backend/src/main/java/com/festago/auth/application/AuthTokenExtractor.java b/backend/src/main/java/com/festago/auth/application/AuthTokenExtractor.java deleted file mode 100644 index 0898a534a..000000000 --- a/backend/src/main/java/com/festago/auth/application/AuthTokenExtractor.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.festago.auth.application; - -import com.festago.auth.domain.AuthPayload; - -public interface AuthTokenExtractor { - - AuthPayload extract(String token); -} diff --git a/backend/src/main/java/com/festago/auth/application/AuthTokenProvider.java b/backend/src/main/java/com/festago/auth/application/AuthTokenProvider.java deleted file mode 100644 index 243e38b1d..000000000 --- a/backend/src/main/java/com/festago/auth/application/AuthTokenProvider.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.festago.auth.application; - -import com.festago.auth.domain.AuthPayload; -import com.festago.auth.dto.v1.TokenResponse; - -public interface AuthTokenProvider { - - TokenResponse provide(AuthPayload authPayload); -} diff --git a/backend/src/main/java/com/festago/auth/application/command/AdminAuthCommandService.java b/backend/src/main/java/com/festago/auth/application/command/AdminAuthCommandService.java index 2df05fdeb..66b67d4f5 100644 --- a/backend/src/main/java/com/festago/auth/application/command/AdminAuthCommandService.java +++ b/backend/src/main/java/com/festago/auth/application/command/AdminAuthCommandService.java @@ -2,13 +2,12 @@ import com.festago.admin.domain.Admin; import com.festago.admin.repository.AdminRepository; -import com.festago.auth.application.AuthTokenProvider; -import com.festago.auth.domain.AuthPayload; import com.festago.auth.domain.AuthType; -import com.festago.auth.domain.Role; +import com.festago.auth.domain.authentication.AdminAuthentication; import com.festago.auth.dto.command.AdminLoginCommand; import com.festago.auth.dto.command.AdminLoginResult; import com.festago.auth.dto.command.AdminSignupCommand; +import com.festago.auth.infrastructure.AdminAuthenticationTokenProvider; import com.festago.common.exception.BadRequestException; import com.festago.common.exception.ErrorCode; import com.festago.common.exception.ForbiddenException; @@ -23,15 +22,15 @@ @RequiredArgsConstructor public class AdminAuthCommandService { - private final AuthTokenProvider authTokenProvider; + private final AdminAuthenticationTokenProvider adminAuthenticationTokenProvider; private final AdminRepository adminRepository; private final PasswordEncoder passwordEncoder; @Transactional(readOnly = true) public AdminLoginResult login(AdminLoginCommand command) { Admin admin = findAdminWithAuthenticate(command); - AuthPayload authPayload = new AuthPayload(admin.getId(), Role.ADMIN); - String accessToken = authTokenProvider.provide(authPayload).token(); + AdminAuthentication adminAuthentication = new AdminAuthentication(admin.getId()); + String accessToken = adminAuthenticationTokenProvider.provide(adminAuthentication).token(); return new AdminLoginResult( admin.getUsername(), getAuthType(admin), diff --git a/backend/src/main/java/com/festago/auth/application/command/MemberAuthCommandService.java b/backend/src/main/java/com/festago/auth/application/command/MemberAuthCommandService.java index 4b2b73a9a..80279b72e 100644 --- a/backend/src/main/java/com/festago/auth/application/command/MemberAuthCommandService.java +++ b/backend/src/main/java/com/festago/auth/application/command/MemberAuthCommandService.java @@ -32,7 +32,7 @@ public class MemberAuthCommandService { private final UserInfoMemberMapper userInfoMemberMapper; private final Clock clock; - public LoginResult oAuth2Login(UserInfo userInfo) { + public LoginResult login(UserInfo userInfo) { Member member = memberRepository.findBySocialIdAndSocialType(userInfo.socialId(), userInfo.socialType()) .orElseGet(() -> signUp(userInfo)); RefreshToken refreshToken = saveRefreshToken(member.getId()); diff --git a/backend/src/main/java/com/festago/auth/application/command/MemberAuthFacadeService.java b/backend/src/main/java/com/festago/auth/application/command/MemberAuthFacadeService.java index 32a1001fe..3980691f3 100644 --- a/backend/src/main/java/com/festago/auth/application/command/MemberAuthFacadeService.java +++ b/backend/src/main/java/com/festago/auth/application/command/MemberAuthFacadeService.java @@ -1,19 +1,18 @@ package com.festago.auth.application.command; -import com.festago.auth.application.AuthTokenProvider; import com.festago.auth.application.OAuth2Client; import com.festago.auth.application.OAuth2Clients; -import com.festago.auth.domain.AuthPayload; import com.festago.auth.domain.OpenIdClient; import com.festago.auth.domain.OpenIdClients; -import com.festago.auth.domain.Role; import com.festago.auth.domain.SocialType; import com.festago.auth.domain.UserInfo; +import com.festago.auth.domain.authentication.MemberAuthentication; import com.festago.auth.dto.v1.LoginResult; import com.festago.auth.dto.v1.LoginV1Response; import com.festago.auth.dto.v1.TokenRefreshResult; import com.festago.auth.dto.v1.TokenRefreshV1Response; import com.festago.auth.dto.v1.TokenResponse; +import com.festago.auth.infrastructure.MemberAuthenticationTokenProvider; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -25,7 +24,7 @@ public class MemberAuthFacadeService { private final OAuth2Clients oAuth2Clients; private final OpenIdClients openIdClients; private final MemberAuthCommandService memberAuthCommandService; - private final AuthTokenProvider authTokenProvider; + private final MemberAuthenticationTokenProvider authTokenProvider; public LoginV1Response oAuth2Login(SocialType socialType, String code) { OAuth2Client oAuth2Client = oAuth2Clients.getClient(socialType); @@ -35,9 +34,9 @@ public LoginV1Response oAuth2Login(SocialType socialType, String code) { } private LoginV1Response login(UserInfo userInfo) { - LoginResult loginResult = memberAuthCommandService.oAuth2Login(userInfo); + LoginResult loginResult = memberAuthCommandService.login(userInfo); - TokenResponse accessToken = authTokenProvider.provide(new AuthPayload(loginResult.memberId(), Role.MEMBER)); + TokenResponse accessToken = authTokenProvider.provide(new MemberAuthentication(loginResult.memberId())); return new LoginV1Response( loginResult.nickname(), loginResult.profileImageUrl(), @@ -62,7 +61,7 @@ public void logout(Long memberId, UUID refreshTokenId) { public TokenRefreshV1Response refresh(UUID refreshTokenId) { TokenRefreshResult tokenRefreshResult = memberAuthCommandService.refresh(refreshTokenId); Long memberId = tokenRefreshResult.memberId(); - TokenResponse accessToken = authTokenProvider.provide(new AuthPayload(memberId, Role.MEMBER)); + TokenResponse accessToken = authTokenProvider.provide(new MemberAuthentication(memberId)); return new TokenRefreshV1Response( accessToken, new TokenResponse( diff --git a/backend/src/main/java/com/festago/auth/config/AuthConfig.java b/backend/src/main/java/com/festago/auth/config/AuthConfig.java index cc1df0f98..d7252d980 100644 --- a/backend/src/main/java/com/festago/auth/config/AuthConfig.java +++ b/backend/src/main/java/com/festago/auth/config/AuthConfig.java @@ -1,16 +1,10 @@ package com.festago.auth.config; -import com.festago.auth.application.AuthTokenExtractor; -import com.festago.auth.application.AuthTokenProvider; import com.festago.auth.application.OAuth2Client; import com.festago.auth.application.OAuth2Clients; import com.festago.auth.domain.OpenIdClient; import com.festago.auth.domain.OpenIdClients; -import com.festago.auth.infrastructure.JwtAuthTokenExtractor; -import com.festago.auth.infrastructure.JwtAuthTokenProvider; -import java.time.Clock; import java.util.List; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.factory.PasswordEncoderFactories; @@ -19,14 +13,6 @@ @Configuration public class AuthConfig { - private static final long EXPIRATION_MINUTES = 360; - - private final String secretKey; - - public AuthConfig(@Value("${festago.auth-secret-key}") String secretKey) { - this.secretKey = secretKey; - } - @Bean public OAuth2Clients oAuth2Clients(List oAuth2Clients) { return OAuth2Clients.builder() @@ -41,16 +27,6 @@ public OpenIdClients openIdClients(List openIdClients) { .build(); } - @Bean - public AuthTokenProvider authProvider(Clock clock) { - return new JwtAuthTokenProvider(secretKey, EXPIRATION_MINUTES, clock); - } - - @Bean - public AuthTokenExtractor authExtractor(Clock clock) { - return new JwtAuthTokenExtractor(secretKey, clock); - } - @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); diff --git a/backend/src/main/java/com/festago/auth/config/LoginConfig.java b/backend/src/main/java/com/festago/auth/config/LoginConfig.java index a8d9fc72e..25729873a 100644 --- a/backend/src/main/java/com/festago/auth/config/LoginConfig.java +++ b/backend/src/main/java/com/festago/auth/config/LoginConfig.java @@ -1,11 +1,15 @@ package com.festago.auth.config; -import com.festago.auth.AuthInterceptor; +import com.festago.auth.AdminAuthenticationArgumentResolver; +import com.festago.auth.AnnotationAuthorizationInterceptor; import com.festago.auth.AuthenticateContext; +import com.festago.auth.FixedAuthorizationInterceptor; +import com.festago.auth.MemberAuthenticationArgumentResolver; import com.festago.auth.RoleArgumentResolver; -import com.festago.auth.annotation.MemberAuth; -import com.festago.auth.application.AuthTokenExtractor; +import com.festago.auth.annotation.Authorization; +import com.festago.auth.domain.AuthenticationTokenExtractor; import com.festago.auth.domain.Role; +import com.festago.auth.infrastructure.CompositeHttpRequestTokenExtractor; import com.festago.auth.infrastructure.CookieHttpRequestTokenExtractor; import com.festago.auth.infrastructure.HeaderHttpRequestTokenExtractor; import com.festago.common.interceptor.AnnotationDelegateInterceptor; @@ -23,53 +27,76 @@ @RequiredArgsConstructor public class LoginConfig implements WebMvcConfigurer { - private final AuthTokenExtractor authTokenExtractor; + private final AuthenticationTokenExtractor memberAuthenticationTokenExtractor; + private final AuthenticationTokenExtractor adminAuthenticationTokenExtractor; + private final AuthenticationTokenExtractor compositeAuthenticationTokenExtractor; private final AuthenticateContext authenticateContext; @Override public void addArgumentResolvers(List resolvers) { resolvers.add(new RoleArgumentResolver(Role.MEMBER, authenticateContext)); resolvers.add(new RoleArgumentResolver(Role.ADMIN, authenticateContext)); + resolvers.add(new MemberAuthenticationArgumentResolver(authenticateContext)); + resolvers.add(new AdminAuthenticationArgumentResolver(authenticateContext)); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(HttpMethodDelegateInterceptor.builder() .allowMethod(HttpMethod.GET, HttpMethod.POST, HttpMethod.DELETE, HttpMethod.PUT, HttpMethod.PATCH) - .interceptor(adminAuthInterceptor()) + .interceptor(adminFixedAuthorizationInterceptor()) .build()) .addPathPatterns("/admin/**") .excludePathPatterns("/admin/api/v1/auth/login", "/admin/api/v1/auth/initialize"); registry.addInterceptor(HttpMethodDelegateInterceptor.builder() .allowMethod(HttpMethod.GET, HttpMethod.POST, HttpMethod.DELETE, HttpMethod.PUT, HttpMethod.PATCH) - .interceptor(memberAuthInterceptor()) + .interceptor(memberFixedAuthorizationInterceptor()) .build()) .addPathPatterns("/member-tickets/**", "/members/**", "/auth/**", "/students/**", "/member-fcm/**") .excludePathPatterns("/auth/oauth2"); registry.addInterceptor(AnnotationDelegateInterceptor.builder() - .annotation(MemberAuth.class) - .interceptor(memberAuthInterceptor()) + .annotation(Authorization.class) + .interceptor(annotationAuthorizationInterceptor()) .build()) .addPathPatterns("/api/**"); } @Bean - public AuthInterceptor adminAuthInterceptor() { - return AuthInterceptor.builder() - .authExtractor(authTokenExtractor) - .tokenExtractor(new CookieHttpRequestTokenExtractor()) - .authenticateContext(authenticateContext) - .role(Role.ADMIN) - .build(); + public FixedAuthorizationInterceptor adminFixedAuthorizationInterceptor() { + return new FixedAuthorizationInterceptor( + compositeHttpRequestTokenExtractor(), + adminAuthenticationTokenExtractor, + authenticateContext, + Role.ADMIN + ); } @Bean - public AuthInterceptor memberAuthInterceptor() { - return AuthInterceptor.builder() - .authExtractor(authTokenExtractor) - .tokenExtractor(new HeaderHttpRequestTokenExtractor()) - .authenticateContext(authenticateContext) - .role(Role.MEMBER) - .build(); + public CompositeHttpRequestTokenExtractor compositeHttpRequestTokenExtractor() { + return new CompositeHttpRequestTokenExtractor( + List.of( + new HeaderHttpRequestTokenExtractor(), + new CookieHttpRequestTokenExtractor() + ) + ); + } + + @Bean + public FixedAuthorizationInterceptor memberFixedAuthorizationInterceptor() { + return new FixedAuthorizationInterceptor( + compositeHttpRequestTokenExtractor(), + memberAuthenticationTokenExtractor, + authenticateContext, + Role.MEMBER + ); + } + + @Bean + public AnnotationAuthorizationInterceptor annotationAuthorizationInterceptor() { + return new AnnotationAuthorizationInterceptor( + compositeHttpRequestTokenExtractor(), + compositeAuthenticationTokenExtractor, + authenticateContext + ); } } diff --git a/backend/src/main/java/com/festago/auth/domain/AuthPayload.java b/backend/src/main/java/com/festago/auth/domain/AuthPayload.java deleted file mode 100644 index 615ddbeba..000000000 --- a/backend/src/main/java/com/festago/auth/domain/AuthPayload.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.festago.auth.domain; - -import com.festago.common.exception.UnexpectedException; - -public class AuthPayload { - - private final Long memberId; - private final Role role; - - public AuthPayload(Long memberId, Role role) { - validate(role); - this.memberId = memberId; - this.role = role; - } - - private void validate(Role role) { - if (role == null) { - throw new UnexpectedException("role은 null이 될 수 없습니다."); - } - } - - public Long getMemberId() { - return memberId; - } - - public Role getRole() { - return role; - } -} diff --git a/backend/src/main/java/com/festago/auth/domain/AuthenticationTokenExtractor.java b/backend/src/main/java/com/festago/auth/domain/AuthenticationTokenExtractor.java new file mode 100644 index 000000000..7751ae5dc --- /dev/null +++ b/backend/src/main/java/com/festago/auth/domain/AuthenticationTokenExtractor.java @@ -0,0 +1,12 @@ +package com.festago.auth.domain; + +import com.festago.auth.domain.authentication.Authentication; + +/** + * 문자열 형식의 토큰을 받아 Authentication을 반환하는 인터페이스
구현체에서 반환하는 Authentication는 반드시 null이 아니여야 한다.
null을 반환하는 대신 + * AnonymousAuthentication.getInstance() 반환할 것! + */ +public interface AuthenticationTokenExtractor { + + Authentication extract(String token); +} diff --git a/backend/src/main/java/com/festago/auth/domain/authentication/AdminAuthentication.java b/backend/src/main/java/com/festago/auth/domain/authentication/AdminAuthentication.java new file mode 100644 index 000000000..f928a3e3c --- /dev/null +++ b/backend/src/main/java/com/festago/auth/domain/authentication/AdminAuthentication.java @@ -0,0 +1,26 @@ +package com.festago.auth.domain.authentication; + +import com.festago.auth.domain.Role; +import com.festago.common.exception.UnexpectedException; + +public class AdminAuthentication implements Authentication { + + private final Long id; + + public AdminAuthentication(Long id) { + if (id == null) { + throw new UnexpectedException("id는 null이 될 수 없습니다."); + } + this.id = id; + } + + @Override + public Long getId() { + return id; + } + + @Override + public Role getRole() { + return Role.ADMIN; + } +} diff --git a/backend/src/main/java/com/festago/auth/domain/authentication/AnonymousAuthentication.java b/backend/src/main/java/com/festago/auth/domain/authentication/AnonymousAuthentication.java new file mode 100644 index 000000000..bbc9eecec --- /dev/null +++ b/backend/src/main/java/com/festago/auth/domain/authentication/AnonymousAuthentication.java @@ -0,0 +1,25 @@ +package com.festago.auth.domain.authentication; + +import com.festago.auth.domain.Role; + +public class AnonymousAuthentication implements Authentication { + + private static final AnonymousAuthentication INSTANCE = new AnonymousAuthentication(); + + private AnonymousAuthentication() { + } + + public static Authentication getInstance() { + return INSTANCE; + } + + @Override + public Long getId() { + return null; + } + + @Override + public Role getRole() { + return Role.ANONYMOUS; + } +} diff --git a/backend/src/main/java/com/festago/auth/domain/authentication/Authentication.java b/backend/src/main/java/com/festago/auth/domain/authentication/Authentication.java new file mode 100644 index 000000000..b571257ef --- /dev/null +++ b/backend/src/main/java/com/festago/auth/domain/authentication/Authentication.java @@ -0,0 +1,14 @@ +package com.festago.auth.domain.authentication; + +import com.festago.auth.domain.Role; + +/** + * 인증 정보를 담은 인터페이스
+ * 구현체는 반드시 getId(), getRole()에 null을 반환하지 않도록 해야한다. + */ +public interface Authentication { + + Long getId(); + + Role getRole(); +} diff --git a/backend/src/main/java/com/festago/auth/domain/authentication/MemberAuthentication.java b/backend/src/main/java/com/festago/auth/domain/authentication/MemberAuthentication.java new file mode 100644 index 000000000..fc27f667a --- /dev/null +++ b/backend/src/main/java/com/festago/auth/domain/authentication/MemberAuthentication.java @@ -0,0 +1,26 @@ +package com.festago.auth.domain.authentication; + +import com.festago.auth.domain.Role; +import com.festago.common.exception.UnexpectedException; + +public class MemberAuthentication implements Authentication { + + private final Long id; + + public MemberAuthentication(Long id) { + if (id == null) { + throw new UnexpectedException("id는 null이 될 수 없습니다."); + } + this.id = id; + } + + @Override + public Long getId() { + return id; + } + + @Override + public Role getRole() { + return Role.MEMBER; + } +} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/AdminAuthenticationClaimsExtractor.java b/backend/src/main/java/com/festago/auth/infrastructure/AdminAuthenticationClaimsExtractor.java new file mode 100644 index 000000000..7a246ae0c --- /dev/null +++ b/backend/src/main/java/com/festago/auth/infrastructure/AdminAuthenticationClaimsExtractor.java @@ -0,0 +1,23 @@ +package com.festago.auth.infrastructure; + +import com.festago.auth.domain.Role; +import com.festago.auth.domain.authentication.AdminAuthentication; +import com.festago.auth.domain.authentication.AnonymousAuthentication; +import com.festago.auth.domain.authentication.Authentication; +import io.jsonwebtoken.Claims; +import org.springframework.stereotype.Component; + +@Component +public class AdminAuthenticationClaimsExtractor implements AuthenticationClaimsExtractor { + + private static final String ADMIN_ID_KEY = "adminId"; + + @Override + public Authentication extract(Claims claims) { + if (!claims.getAudience().contains(Role.ADMIN.name())) { + return AnonymousAuthentication.getInstance(); + } + Long adminId = claims.get(ADMIN_ID_KEY, Long.class); + return new AdminAuthentication(adminId); + } +} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/AdminAuthenticationTokenExtractor.java b/backend/src/main/java/com/festago/auth/infrastructure/AdminAuthenticationTokenExtractor.java new file mode 100644 index 000000000..f816f78ca --- /dev/null +++ b/backend/src/main/java/com/festago/auth/infrastructure/AdminAuthenticationTokenExtractor.java @@ -0,0 +1,21 @@ +package com.festago.auth.infrastructure; + +import com.festago.auth.domain.AuthenticationTokenExtractor; +import com.festago.auth.domain.authentication.Authentication; +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AdminAuthenticationTokenExtractor implements AuthenticationTokenExtractor { + + private final JwtTokenParser jwtTokenParser; + private final AdminAuthenticationClaimsExtractor adminAuthenticationClaimsExtractor; + + @Override + public Authentication extract(String token) { + Claims claims = jwtTokenParser.getClaims(token); + return adminAuthenticationClaimsExtractor.extract(claims); + } +} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/AdminAuthenticationTokenProvider.java b/backend/src/main/java/com/festago/auth/infrastructure/AdminAuthenticationTokenProvider.java new file mode 100644 index 000000000..fe572fb13 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/infrastructure/AdminAuthenticationTokenProvider.java @@ -0,0 +1,26 @@ +package com.festago.auth.infrastructure; + +import com.festago.auth.domain.Role; +import com.festago.auth.domain.authentication.AdminAuthentication; +import com.festago.auth.dto.v1.TokenResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AdminAuthenticationTokenProvider { + + private static final String ADMIN_ID_KEY = "adminId"; + private static final long EXPIRATION_MINUTES = 60L * 24L; + + private final TokenProviderTemplate tokenProviderTemplate; + + public TokenResponse provide(AdminAuthentication adminAuthentication) { + return tokenProviderTemplate.provide(EXPIRATION_MINUTES, + jwtBuilder -> jwtBuilder + .subject(adminAuthentication.getId().toString()) + .claim(ADMIN_ID_KEY, adminAuthentication.getId()) + .audience().add(Role.ADMIN.name()).and() + ); + } +} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/AuthenticationClaimsExtractor.java b/backend/src/main/java/com/festago/auth/infrastructure/AuthenticationClaimsExtractor.java new file mode 100644 index 000000000..7cceb81d0 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/infrastructure/AuthenticationClaimsExtractor.java @@ -0,0 +1,13 @@ +package com.festago.auth.infrastructure; + +import com.festago.auth.domain.authentication.Authentication; +import io.jsonwebtoken.Claims; + +/** + * AuthenticationTokenExtractor의 필드로 사용되기 위해 설계되었음
JWT Claims에서 값을 추출하여 Authentication을 반환하는 인터페이스
구현체에서 + * 반환하는 Authentication는 반드시 null이 아니여야 한다.
null을 반환하는 대신 AnonymousAuthentication.getInstance() 반환할 것! + */ +public interface AuthenticationClaimsExtractor { + + Authentication extract(Claims claims); +} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/CompositeAuthenticationTokenExtractor.java b/backend/src/main/java/com/festago/auth/infrastructure/CompositeAuthenticationTokenExtractor.java new file mode 100644 index 000000000..96b7f02c1 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/infrastructure/CompositeAuthenticationTokenExtractor.java @@ -0,0 +1,30 @@ +package com.festago.auth.infrastructure; + +import com.festago.auth.domain.AuthenticationTokenExtractor; +import com.festago.auth.domain.Role; +import com.festago.auth.domain.authentication.AnonymousAuthentication; +import com.festago.auth.domain.authentication.Authentication; +import io.jsonwebtoken.Claims; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CompositeAuthenticationTokenExtractor implements AuthenticationTokenExtractor { + + private final JwtTokenParser jwtTokenParser; + private final List authenticationClaimsExtractors; + + @Override + public Authentication extract(String token) { + Claims claims = jwtTokenParser.getClaims(token); + for (AuthenticationClaimsExtractor claimsExtractor : authenticationClaimsExtractors) { + Authentication authentication = claimsExtractor.extract(claims); + if (authentication.getRole() != Role.ANONYMOUS) { + return authentication; + } + } + return AnonymousAuthentication.getInstance(); + } +} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/CompositeHttpRequestTokenExtractor.java b/backend/src/main/java/com/festago/auth/infrastructure/CompositeHttpRequestTokenExtractor.java new file mode 100644 index 000000000..ebb806424 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/infrastructure/CompositeHttpRequestTokenExtractor.java @@ -0,0 +1,24 @@ +package com.festago.auth.infrastructure; + +import com.festago.auth.application.HttpRequestTokenExtractor; +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class CompositeHttpRequestTokenExtractor implements HttpRequestTokenExtractor { + + private final List httpRequestTokenExtractors; + + @Override + public Optional extract(HttpServletRequest request) { + for (HttpRequestTokenExtractor httpRequestTokenExtractor : httpRequestTokenExtractors) { + Optional token = httpRequestTokenExtractor.extract(request); + if (token.isPresent()) { + return token; + } + } + return Optional.empty(); + } +} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthTokenExtractor.java b/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthTokenExtractor.java deleted file mode 100644 index 46c563bbc..000000000 --- a/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthTokenExtractor.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.festago.auth.infrastructure; - -import com.festago.auth.application.AuthTokenExtractor; -import com.festago.auth.domain.AuthPayload; -import com.festago.auth.domain.Role; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.UnauthorizedException; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.UnsupportedJwtException; -import io.jsonwebtoken.security.Keys; -import io.jsonwebtoken.security.SignatureException; -import java.nio.charset.StandardCharsets; -import java.time.Clock; -import java.util.Date; -import javax.crypto.SecretKey; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class JwtAuthTokenExtractor implements AuthTokenExtractor { - - private static final String MEMBER_ID_KEY = "memberId"; - private static final String ROLE_ID_KEY = "role"; - - private final JwtParser jwtParser; - - public JwtAuthTokenExtractor(String secretKey, Clock clock) { - SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); - this.jwtParser = Jwts.parser() - .clock(() -> Date.from(clock.instant())) - .verifyWith(key) - .build(); - } - - @Override - public AuthPayload extract(String token) { - Claims claims = getClaims(token); - Long memberId = claims.get(MEMBER_ID_KEY, Long.class); - String role = claims.get(ROLE_ID_KEY, String.class); - return new AuthPayload(memberId, Role.from(role)); - } - - private Claims getClaims(String code) { - try { - return jwtParser.parseSignedClaims(code) - .getPayload(); - } catch (ExpiredJwtException e) { - throw new UnauthorizedException(ErrorCode.EXPIRED_AUTH_TOKEN); - } catch (SignatureException | IllegalArgumentException | MalformedJwtException | UnsupportedJwtException e) { - throw new UnauthorizedException(ErrorCode.INVALID_AUTH_TOKEN); - } catch (Exception e) { - log.error("JWT 토큰 파싱 중에 문제가 발생했습니다."); - throw e; - } - } -} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthTokenProvider.java b/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthTokenProvider.java deleted file mode 100644 index 472ee2222..000000000 --- a/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthTokenProvider.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.festago.auth.infrastructure; - -import com.festago.auth.application.AuthTokenProvider; -import com.festago.auth.domain.AuthPayload; -import com.festago.auth.dto.v1.TokenResponse; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; -import java.nio.charset.StandardCharsets; -import java.time.Clock; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.Date; -import javax.crypto.SecretKey; - -public class JwtAuthTokenProvider implements AuthTokenProvider { - - private static final String MEMBER_ID_KEY = "memberId"; - private static final String ROLE_ID_KEY = "role"; - - private final SecretKey key; - private final long expirationMinutes; - private final Clock clock; - - public JwtAuthTokenProvider(String secretKey, long expirationMinutes, Clock clock) { - this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); - this.expirationMinutes = expirationMinutes; - this.clock = clock; - } - - @Override - public TokenResponse provide(AuthPayload authPayload) { - Instant now = clock.instant(); - Instant expiredAt = now.plus(expirationMinutes, ChronoUnit.MINUTES); - String accessToken = Jwts.builder() - .claim(MEMBER_ID_KEY, authPayload.getMemberId()) - .claim(ROLE_ID_KEY, authPayload.getRole()) - .issuedAt(Date.from(now)) - .expiration(Date.from(expiredAt)) - .signWith(key) - .compact(); - return new TokenResponse(accessToken, LocalDateTime.ofInstant(expiredAt, clock.getZone())); - } -} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/JwtTokenParser.java b/backend/src/main/java/com/festago/auth/infrastructure/JwtTokenParser.java new file mode 100644 index 000000000..f5751116a --- /dev/null +++ b/backend/src/main/java/com/festago/auth/infrastructure/JwtTokenParser.java @@ -0,0 +1,47 @@ +package com.festago.auth.infrastructure; + +import com.festago.common.exception.ErrorCode; +import com.festago.common.exception.UnauthorizedException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.util.Date; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class JwtTokenParser { + + private final JwtParser jwtParser; + + public JwtTokenParser( + @Value("${festago.auth-secret-key}") String secretKey, + Clock clock + ) { + this.jwtParser = Jwts.parser() + .clock(() -> Date.from(clock.instant())) + .verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8))) + .build(); + } + + public Claims getClaims(String token) { + try { + return jwtParser.parseSignedClaims(token) + .getPayload(); + } catch (ExpiredJwtException e) { + throw new UnauthorizedException(ErrorCode.EXPIRED_AUTH_TOKEN); + } catch (JwtException | IllegalArgumentException e) { + throw new UnauthorizedException(ErrorCode.INVALID_AUTH_TOKEN); + } catch (Exception e) { + log.error("JWT 토큰 파싱 중에 문제가 발생했습니다."); + throw e; + } + } +} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/MemberAuthenticationClaimsExtractor.java b/backend/src/main/java/com/festago/auth/infrastructure/MemberAuthenticationClaimsExtractor.java new file mode 100644 index 000000000..1c2653ca3 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/infrastructure/MemberAuthenticationClaimsExtractor.java @@ -0,0 +1,23 @@ +package com.festago.auth.infrastructure; + +import com.festago.auth.domain.Role; +import com.festago.auth.domain.authentication.AnonymousAuthentication; +import com.festago.auth.domain.authentication.Authentication; +import com.festago.auth.domain.authentication.MemberAuthentication; +import io.jsonwebtoken.Claims; +import org.springframework.stereotype.Component; + +@Component +public class MemberAuthenticationClaimsExtractor implements AuthenticationClaimsExtractor { + + private static final String MEMBER_ID_KEY = "memberId"; + + @Override + public Authentication extract(Claims claims) { + if (!claims.getAudience().contains(Role.MEMBER.name())) { + return AnonymousAuthentication.getInstance(); + } + Long memberId = claims.get(MEMBER_ID_KEY, Long.class); + return new MemberAuthentication(memberId); + } +} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/MemberAuthenticationTokenExtractor.java b/backend/src/main/java/com/festago/auth/infrastructure/MemberAuthenticationTokenExtractor.java new file mode 100644 index 000000000..dc83680c1 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/infrastructure/MemberAuthenticationTokenExtractor.java @@ -0,0 +1,21 @@ +package com.festago.auth.infrastructure; + +import com.festago.auth.domain.AuthenticationTokenExtractor; +import com.festago.auth.domain.authentication.Authentication; +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MemberAuthenticationTokenExtractor implements AuthenticationTokenExtractor { + + private final JwtTokenParser jwtTokenParser; + private final MemberAuthenticationClaimsExtractor memberAuthenticationClaimsExtractor; + + @Override + public Authentication extract(String token) { + Claims claims = jwtTokenParser.getClaims(token); + return memberAuthenticationClaimsExtractor.extract(claims); + } +} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/MemberAuthenticationTokenProvider.java b/backend/src/main/java/com/festago/auth/infrastructure/MemberAuthenticationTokenProvider.java new file mode 100644 index 000000000..830acc15e --- /dev/null +++ b/backend/src/main/java/com/festago/auth/infrastructure/MemberAuthenticationTokenProvider.java @@ -0,0 +1,26 @@ +package com.festago.auth.infrastructure; + +import com.festago.auth.domain.Role; +import com.festago.auth.domain.authentication.MemberAuthentication; +import com.festago.auth.dto.v1.TokenResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MemberAuthenticationTokenProvider { + + private static final String MEMBER_ID_KEY = "memberId"; + private static final long EXPIRATION_MINUTES = 60L * 6L; + + private final TokenProviderTemplate tokenProviderTemplate; + + public TokenResponse provide(MemberAuthentication memberAuthentication) { + return tokenProviderTemplate.provide(EXPIRATION_MINUTES, + jwtBuilder -> jwtBuilder + .subject(memberAuthentication.getId().toString()) + .claim(MEMBER_ID_KEY, memberAuthentication.getId()) + .audience().add(Role.MEMBER.name()).and() + ); + } +} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/TokenProviderTemplate.java b/backend/src/main/java/com/festago/auth/infrastructure/TokenProviderTemplate.java new file mode 100644 index 000000000..a6824bdcc --- /dev/null +++ b/backend/src/main/java/com/festago/auth/infrastructure/TokenProviderTemplate.java @@ -0,0 +1,43 @@ +package com.festago.auth.infrastructure; + +import com.festago.auth.dto.v1.TokenResponse; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.function.UnaryOperator; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class TokenProviderTemplate { + + private final SecretKey secretKey; + private final Clock clock; + + public TokenProviderTemplate( + @Value("${festago.auth-secret-key}") String secretKey, + Clock clock + ) { + this.secretKey = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + this.clock = clock; + } + + public TokenResponse provide(long expirationMinutes, UnaryOperator template) { + Instant now = clock.instant(); + Instant expiredAt = now.plus(expirationMinutes, ChronoUnit.MINUTES); + JwtBuilder builder = Jwts.builder() + .expiration(Date.from(expiredAt)) + .issuedAt(Date.from(now)) + .signWith(secretKey); + template.apply(builder); + String accessToken = builder.compact(); + return new TokenResponse(accessToken, LocalDateTime.ofInstant(expiredAt, clock.getZone())); + } +} diff --git a/backend/src/main/java/com/festago/auth/presentation/v1/AdminAuthV1Controller.java b/backend/src/main/java/com/festago/auth/presentation/v1/AdminAuthV1Controller.java index 7323444e4..8be72a99b 100644 --- a/backend/src/main/java/com/festago/auth/presentation/v1/AdminAuthV1Controller.java +++ b/backend/src/main/java/com/festago/auth/presentation/v1/AdminAuthV1Controller.java @@ -1,7 +1,7 @@ package com.festago.auth.presentation.v1; -import com.festago.auth.annotation.Admin; import com.festago.auth.application.command.AdminAuthCommandService; +import com.festago.auth.domain.authentication.AdminAuthentication; import com.festago.auth.dto.AdminLoginV1Request; import com.festago.auth.dto.AdminLoginV1Response; import com.festago.auth.dto.AdminSignupV1Request; @@ -66,9 +66,9 @@ private String createLogoutCookie() { @PostMapping("/signup") public ResponseEntity signupAdminAccount( @RequestBody @Valid AdminSignupV1Request request, - @Admin Long adminId + AdminAuthentication adminAuthentication ) { - adminAuthCommandService.signup(adminId, request.toCommand()); + adminAuthCommandService.signup(adminAuthentication.getId(), request.toCommand()); return ResponseEntity.ok().build(); } diff --git a/backend/src/main/java/com/festago/auth/presentation/v1/MemberAuthV1Controller.java b/backend/src/main/java/com/festago/auth/presentation/v1/MemberAuthV1Controller.java index c5df4c655..d389a3f98 100644 --- a/backend/src/main/java/com/festago/auth/presentation/v1/MemberAuthV1Controller.java +++ b/backend/src/main/java/com/festago/auth/presentation/v1/MemberAuthV1Controller.java @@ -1,9 +1,9 @@ package com.festago.auth.presentation.v1; -import com.festago.auth.annotation.Member; import com.festago.auth.annotation.MemberAuth; import com.festago.auth.application.command.MemberAuthFacadeService; import com.festago.auth.domain.SocialType; +import com.festago.auth.domain.authentication.MemberAuthentication; import com.festago.auth.dto.v1.LoginV1Response; import com.festago.auth.dto.v1.LogoutV1Request; import com.festago.auth.dto.v1.OAuth2LoginV1Request; @@ -66,10 +66,10 @@ public ResponseEntity openIdLogin( @PostMapping("/logout") @Operation(description = "로그인 된 사용자를 로그아웃 처리한다.", summary = "로그아웃") public ResponseEntity logout( - @Member Long memberId, + MemberAuthentication memberAuthentication, @RequestBody @Valid LogoutV1Request request ) { - memberAuthFacadeService.logout(memberId, UUID.fromString(request.refreshToken())); + memberAuthFacadeService.logout(memberAuthentication.getId(), UUID.fromString(request.refreshToken())); return ResponseEntity.ok().build(); } @@ -85,8 +85,8 @@ public ResponseEntity refresh( @MemberAuth @DeleteMapping @Operation(description = "사용자를 탈퇴 처리한다.", summary = "회원 탈퇴") - public ResponseEntity deleteAccount(@Member Long memberId) { - memberAuthFacadeService.deleteAccount(memberId); + public ResponseEntity deleteAccount(MemberAuthentication memberAuthentication) { + memberAuthFacadeService.deleteAccount(memberAuthentication.getId()); return ResponseEntity.ok().build(); } } diff --git a/backend/src/test/java/com/festago/auth/application/command/AdminAuthCommandServiceTest.java b/backend/src/test/java/com/festago/auth/application/command/AdminAuthCommandServiceTest.java index 0fc53dfd4..d4be91a28 100644 --- a/backend/src/test/java/com/festago/auth/application/command/AdminAuthCommandServiceTest.java +++ b/backend/src/test/java/com/festago/auth/application/command/AdminAuthCommandServiceTest.java @@ -12,10 +12,10 @@ import com.festago.admin.domain.Admin; import com.festago.admin.repository.AdminRepository; import com.festago.admin.repository.MemoryAdminRepository; -import com.festago.auth.application.AuthTokenProvider; import com.festago.auth.dto.command.AdminLoginCommand; import com.festago.auth.dto.command.AdminSignupCommand; import com.festago.auth.dto.v1.TokenResponse; +import com.festago.auth.infrastructure.AdminAuthenticationTokenProvider; import com.festago.common.exception.BadRequestException; import com.festago.common.exception.ErrorCode; import com.festago.common.exception.ForbiddenException; @@ -35,16 +35,16 @@ class AdminAuthCommandServiceTest { AdminRepository adminRepository; - AuthTokenProvider authTokenProvider; + AdminAuthenticationTokenProvider adminAuthenticationTokenProvider; AdminAuthCommandService adminAuthCommandService; @BeforeEach void setUp() { adminRepository = new MemoryAdminRepository(); - authTokenProvider = mock(AuthTokenProvider.class); + adminAuthenticationTokenProvider = mock(AdminAuthenticationTokenProvider.class); adminAuthCommandService = new AdminAuthCommandService( - authTokenProvider, + adminAuthenticationTokenProvider, adminRepository, PasswordEncoderFactories.createDelegatingPasswordEncoder() ); @@ -96,7 +96,7 @@ class 로그인 { .username("admin") .password("password") .build(); - given(authTokenProvider.provide(any())) + given(adminAuthenticationTokenProvider.provide(any())) .willReturn(new TokenResponse("token", LocalDateTime.now().plusWeeks(1))); // when diff --git a/backend/src/test/java/com/festago/auth/application/command/MemberAuthCommandServiceTest.java b/backend/src/test/java/com/festago/auth/application/command/MemberAuthCommandServiceTest.java index bac29d949..8a6dd0c00 100644 --- a/backend/src/test/java/com/festago/auth/application/command/MemberAuthCommandServiceTest.java +++ b/backend/src/test/java/com/festago/auth/application/command/MemberAuthCommandServiceTest.java @@ -59,12 +59,12 @@ void setUp() { } @Nested - class oauth2Login { + class login { @Test void 신규_회원으로_로그인하면_회원과_리프래쉬_토큰이_저장된다() { // when - var actual = memberAuthCommandService.oAuth2Login(getUserInfo("1")); + var actual = memberAuthCommandService.login(getUserInfo("1")); // then assertThat(memberRepository.findById(actual.memberId())).isPresent(); @@ -79,7 +79,7 @@ class oauth2Login { RefreshTokenFixture.builder().memberId(member.getId()).build()); // when - var actual = memberAuthCommandService.oAuth2Login(getUserInfo(member.getSocialId())); + var actual = memberAuthCommandService.login(getUserInfo(member.getSocialId())); // then assertThat(refreshTokenRepository.findById(originToken.getId())).isPresent(); diff --git a/backend/src/test/java/com/festago/auth/infrastructure/AdminAuthenticationClaimsExtractorTest.java b/backend/src/test/java/com/festago/auth/infrastructure/AdminAuthenticationClaimsExtractorTest.java new file mode 100644 index 000000000..b13765bd1 --- /dev/null +++ b/backend/src/test/java/com/festago/auth/infrastructure/AdminAuthenticationClaimsExtractorTest.java @@ -0,0 +1,52 @@ +package com.festago.auth.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.festago.auth.domain.Role; +import com.festago.auth.domain.authentication.Authentication; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AdminAuthenticationClaimsExtractorTest { + + AdminAuthenticationClaimsExtractor adminAuthenticationClaimsExtractor = new AdminAuthenticationClaimsExtractor(); + + @ParameterizedTest + @EnumSource(names = "ADMIN", mode = EnumSource.Mode.EXCLUDE) + void Claims의_audience가_ADMIN이_아니면_반환되는_Authentication의_권한은_ANONYMOUS이다(Role role) { + // given + Claims claims = Jwts.claims() + .audience().add(role.name()).and() + .build(); + + // when + Authentication authentication = adminAuthenticationClaimsExtractor.extract(claims); + + // then + assertThat(authentication.getRole()) + .isEqualTo(Role.ANONYMOUS); + } + + @Test + void Claims의_audience가_ADMIN이면_Authentication의_권한은_ADMIN이다() { + // given + Claims claims = Jwts.claims() + .audience().add(Role.ADMIN.name()).and() + .add("adminId", 1) + .build(); + + // when + Authentication authentication = adminAuthenticationClaimsExtractor.extract(claims); + + // then + assertThat(authentication.getRole()) + .isEqualTo(Role.ADMIN); + } +} diff --git a/backend/src/test/java/com/festago/auth/infrastructure/CompositeAuthenticationTokenExtractorTest.java b/backend/src/test/java/com/festago/auth/infrastructure/CompositeAuthenticationTokenExtractorTest.java new file mode 100644 index 000000000..a1c77dc00 --- /dev/null +++ b/backend/src/test/java/com/festago/auth/infrastructure/CompositeAuthenticationTokenExtractorTest.java @@ -0,0 +1,56 @@ +package com.festago.auth.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.mock; + +import com.festago.auth.domain.Role; +import com.festago.auth.domain.authentication.AdminAuthentication; +import com.festago.auth.domain.authentication.AnonymousAuthentication; +import com.festago.auth.domain.authentication.Authentication; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class CompositeAuthenticationTokenExtractorTest { + + JwtTokenParser jwtTokenParser; + + @BeforeEach + void setUp() { + jwtTokenParser = mock(); + } + + @Test + void AuthenticationClaimsExtractors_모두_AnonymousAuthentication을_반환하면_권한이_Anonymous인_Authentication을_반환한다() { + // given + CompositeAuthenticationTokenExtractor compositeAuthenticationTokenExtractor = new CompositeAuthenticationTokenExtractor( + jwtTokenParser, + List.of(claims -> AnonymousAuthentication.getInstance(), claims -> AnonymousAuthentication.getInstance()) + ); + + // when + Authentication actual = compositeAuthenticationTokenExtractor.extract("token"); + + // then + assertThat(actual.getRole()).isEqualTo(Role.ANONYMOUS); + } + + @Test + void HttpRequestTokenExtractors_중_하나라도_AnonymousAuthentication이_아닌_값을_반환하면_해당_값을_반환한다() { + // given + CompositeAuthenticationTokenExtractor compositeAuthenticationTokenExtractor = new CompositeAuthenticationTokenExtractor( + jwtTokenParser, + List.of(claims -> AnonymousAuthentication.getInstance(), claims -> new AdminAuthentication(1L)) + ); + + // when + Authentication actual = compositeAuthenticationTokenExtractor.extract("token"); + + // then + assertThat(actual.getRole()).isEqualTo(Role.ADMIN); + } +} diff --git a/backend/src/test/java/com/festago/auth/infrastructure/CompositeHttpRequestTokenExtractorTest.java b/backend/src/test/java/com/festago/auth/infrastructure/CompositeHttpRequestTokenExtractorTest.java new file mode 100644 index 000000000..f85cf700c --- /dev/null +++ b/backend/src/test/java/com/festago/auth/infrastructure/CompositeHttpRequestTokenExtractorTest.java @@ -0,0 +1,52 @@ +package com.festago.auth.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.mock; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class CompositeHttpRequestTokenExtractorTest { + + HttpServletRequest request; + + @BeforeEach + void setUp() { + request = mock(HttpServletRequest.class); + } + + @Test + void HttpRequestTokenExtractors_모두_빈_옵셔널을_반환하면_빈_옵셔널을_반환한다() { + // given + CompositeHttpRequestTokenExtractor compositeHttpRequestTokenExtractor = new CompositeHttpRequestTokenExtractor( + List.of(req -> Optional.empty(), req -> Optional.empty()) + ); + + // when + Optional actual = compositeHttpRequestTokenExtractor.extract(request); + + // then + assertThat(actual).isEmpty(); + } + + @Test + void HttpRequestTokenExtractors_중_하나라도_빈_옵셔널이_아닌_값을_반환하면_해당_값을_반환한다() { + // given + CompositeHttpRequestTokenExtractor compositeHttpRequestTokenExtractor = new CompositeHttpRequestTokenExtractor( + List.of(req -> Optional.empty(), req -> Optional.of("present")) + ); + + // when + Optional actual = compositeHttpRequestTokenExtractor.extract(request); + + // then + assertThat(actual).contains("present"); + } +} diff --git a/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthTokenExtractorTest.java b/backend/src/test/java/com/festago/auth/infrastructure/JwtTokenParserTest.java similarity index 52% rename from backend/src/test/java/com/festago/auth/infrastructure/JwtAuthTokenExtractorTest.java rename to backend/src/test/java/com/festago/auth/infrastructure/JwtTokenParserTest.java index 62b6385c6..21bbafea8 100644 --- a/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthTokenExtractorTest.java +++ b/backend/src/test/java/com/festago/auth/infrastructure/JwtTokenParserTest.java @@ -2,33 +2,40 @@ import static com.festago.common.exception.ErrorCode.EXPIRED_AUTH_TOKEN; import static com.festago.common.exception.ErrorCode.INVALID_AUTH_TOKEN; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.festago.auth.domain.AuthPayload; import com.festago.auth.domain.Role; import com.festago.common.exception.UnauthorizedException; -import com.festago.common.exception.UnexpectedException; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import java.nio.charset.StandardCharsets; import java.security.Key; import java.time.Clock; import java.util.Date; -import org.assertj.core.api.SoftAssertions; +import javax.crypto.SecretKey; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -@DisplayNameGeneration(ReplaceUnderscores.class) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class JwtAuthTokenExtractorTest { +class JwtTokenParserTest { - private static final String MEMBER_ID_KEY = "memberId"; - private static final String ROLE_ID_KEY = "role"; - private static final String SECRET_KEY = "1231231231231231223131231231231231231212312312"; - private static final Key KEY = Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8)); + private static final String KEY = "1231231231231231223131231231231231231212312312"; + private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(KEY.getBytes(StandardCharsets.UTF_8)); - JwtAuthTokenExtractor jwtAuthExtractor = new JwtAuthTokenExtractor(SECRET_KEY, Clock.systemDefaultZone()); + JwtTokenParser jwtTokenParser; + + @BeforeEach + void setUp() { + jwtTokenParser = new JwtTokenParser( + KEY, + Clock.systemDefaultZone() + ); + } @Test void JWT_토큰의_형식이_아니면_예외() { @@ -36,7 +43,7 @@ class JwtAuthTokenExtractorTest { String token = "Hello World"; // when & then - assertThatThrownBy(() -> jwtAuthExtractor.extract(token)) + assertThatThrownBy(() -> jwtTokenParser.getClaims(token)) .isInstanceOf(UnauthorizedException.class) .hasMessage(INVALID_AUTH_TOKEN.getMessage()); } @@ -45,13 +52,13 @@ class JwtAuthTokenExtractorTest { void 기간이_만료된_토큰이면_예외() { //given String token = Jwts.builder() - .claim(MEMBER_ID_KEY, 1L) + .audience().add(Role.MEMBER.name()).and() .expiration(new Date(new Date().getTime() - 1000)) - .signWith(KEY) + .signWith(SECRET_KEY) .compact(); // when & then - assertThatThrownBy(() -> jwtAuthExtractor.extract(token)) + assertThatThrownBy(() -> jwtTokenParser.getClaims(token)) .isInstanceOf(UnauthorizedException.class) .hasMessage(EXPIRED_AUTH_TOKEN.getMessage()); } @@ -62,36 +69,21 @@ class JwtAuthTokenExtractorTest { Key otherKey = Keys.hmacShaKeyFor(("a" + SECRET_KEY).getBytes(StandardCharsets.UTF_8)); String token = Jwts.builder() - .claim(MEMBER_ID_KEY, 1L) + .audience().add(Role.MEMBER.name()).and() .expiration(new Date(new Date().getTime() + 10000)) .signWith(otherKey) .compact(); // when & then - assertThatThrownBy(() -> jwtAuthExtractor.extract(token)) + assertThatThrownBy(() -> jwtTokenParser.getClaims(token)) .isInstanceOf(UnauthorizedException.class) .hasMessage(INVALID_AUTH_TOKEN.getMessage()); } - @Test - void role_필드가_없으면_예외() { - // given - String token = Jwts.builder() - .claim(MEMBER_ID_KEY, 1) - .expiration(new Date(new Date().getTime() + 10000)) - .signWith(KEY) - .compact(); - - // when & then - assertThatThrownBy(() -> jwtAuthExtractor.extract(token)) - .isInstanceOf(UnexpectedException.class) - .hasMessage("해당하는 Role이 없습니다."); - } - @Test void token이_null이면_예외() { // when & then - assertThatThrownBy(() -> jwtAuthExtractor.extract(null)) + assertThatThrownBy(() -> jwtTokenParser.getClaims(null)) .isInstanceOf(UnauthorizedException.class) .hasMessage(INVALID_AUTH_TOKEN.getMessage()); } @@ -99,21 +91,16 @@ class JwtAuthTokenExtractorTest { @Test void 토큰_추출_성공() { // given - Long memberId = 1L; String token = Jwts.builder() - .claim(MEMBER_ID_KEY, memberId) - .claim(ROLE_ID_KEY, Role.MEMBER) + .audience().add(Role.MEMBER.name()).and() .expiration(new Date(new Date().getTime() + 10000)) - .signWith(KEY) + .signWith(SECRET_KEY) .compact(); // when - AuthPayload payload = jwtAuthExtractor.extract(token); + Claims claims = jwtTokenParser.getClaims(token); // then - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(payload.getMemberId()).isEqualTo(memberId); - softly.assertThat(payload.getRole()).isEqualTo(Role.MEMBER); - }); + assertThat(claims.getAudience()).containsOnly(Role.MEMBER.name()); } } diff --git a/backend/src/test/java/com/festago/auth/infrastructure/MemberAuthenticationClaimsExtractorTest.java b/backend/src/test/java/com/festago/auth/infrastructure/MemberAuthenticationClaimsExtractorTest.java new file mode 100644 index 000000000..26e066f69 --- /dev/null +++ b/backend/src/test/java/com/festago/auth/infrastructure/MemberAuthenticationClaimsExtractorTest.java @@ -0,0 +1,52 @@ +package com.festago.auth.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.festago.auth.domain.Role; +import com.festago.auth.domain.authentication.Authentication; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class MemberAuthenticationClaimsExtractorTest { + + MemberAuthenticationClaimsExtractor memberAuthenticationClaimsExtractor = new MemberAuthenticationClaimsExtractor(); + + @ParameterizedTest + @EnumSource(names = "MEMBER", mode = EnumSource.Mode.EXCLUDE) + void Claims의_audience가_MEMBER가_아니면_반환되는_Authentication의_권한은_ANONYMOUS이다(Role role) { + // given + Claims claims = Jwts.claims() + .audience().add(role.name()).and() + .build(); + + // when + Authentication authentication = memberAuthenticationClaimsExtractor.extract(claims); + + // then + assertThat(authentication.getRole()) + .isEqualTo(Role.ANONYMOUS); + } + + @Test + void Claims의_audience가_MEMBER이면_Authentication의_권한은_MEMBER이다() { + // given + Claims claims = Jwts.claims() + .audience().add(Role.MEMBER.name()).and() + .add("memberId", 1) + .build(); + + // when + Authentication authentication = memberAuthenticationClaimsExtractor.extract(claims); + + // then + assertThat(authentication.getRole()) + .isEqualTo(Role.MEMBER); + } +} diff --git a/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthTokenProviderTest.java b/backend/src/test/java/com/festago/auth/infrastructure/TokenProviderTemplateTest.java similarity index 50% rename from backend/src/test/java/com/festago/auth/infrastructure/JwtAuthTokenProviderTest.java rename to backend/src/test/java/com/festago/auth/infrastructure/TokenProviderTemplateTest.java index 261e0db2c..93d8dfff5 100644 --- a/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthTokenProviderTest.java +++ b/backend/src/test/java/com/festago/auth/infrastructure/TokenProviderTemplateTest.java @@ -2,33 +2,42 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.festago.auth.domain.AuthPayload; -import com.festago.auth.domain.Role; import com.festago.auth.dto.v1.TokenResponse; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; import java.time.Clock; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -@DisplayNameGeneration(ReplaceUnderscores.class) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class JwtAuthTokenProviderTest { +class TokenProviderTemplateTest { private static final String SECRET_KEY = "1231231231231231223131231231231231231212312312"; - JwtAuthTokenProvider jwtAuthProvider = new JwtAuthTokenProvider(SECRET_KEY, 360, Clock.systemDefaultZone()); + + TokenProviderTemplate tokenProviderTemplate; + + @BeforeEach + void setUp() { + tokenProviderTemplate = new TokenProviderTemplate( + SECRET_KEY, + Clock.systemDefaultZone() + ); + } @Test void 토큰_생성_성공() { // given - AuthPayload authPayload = new AuthPayload(1L, Role.MEMBER); JwtParser parser = Jwts.parser() - .setSigningKey(SECRET_KEY.getBytes()) + .verifyWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8))) .build(); // when - TokenResponse response = jwtAuthProvider.provide(authPayload); + TokenResponse response = tokenProviderTemplate.provide(60, jwtBuilder -> jwtBuilder); // then assertThat(parser.isSigned(response.token())) diff --git a/backend/src/test/java/com/festago/presentation/auth/RoleArgumentResolverTest.java b/backend/src/test/java/com/festago/presentation/auth/RoleArgumentResolverTest.java deleted file mode 100644 index 37152a4ba..000000000 --- a/backend/src/test/java/com/festago/presentation/auth/RoleArgumentResolverTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.festago.presentation.auth; - -import static com.festago.common.exception.ErrorCode.NOT_ENOUGH_PERMISSION; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.festago.auth.AuthenticateContext; -import com.festago.auth.RoleArgumentResolver; -import com.festago.auth.domain.Role; -import com.festago.common.exception.ForbiddenException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -@DisplayNameGeneration(ReplaceUnderscores.class) -@SuppressWarnings("NonAsciiCharacters") -class RoleArgumentResolverTest { - - AuthenticateContext authenticateContext; - - RoleArgumentResolver roleArgumentResolver; - - @BeforeEach - void setUp() { - authenticateContext = new AuthenticateContext(); - } - - @ParameterizedTest - @ValueSource(strings = {"ADMIN", "ANONYMOUS"}) - void Role이_Member일때_Member가_아니면_예외(Role role) { - // given - roleArgumentResolver = new RoleArgumentResolver(Role.MEMBER, authenticateContext); - authenticateContext.setAuthenticate(1L, role); - - // when & then - assertThatThrownBy(() -> roleArgumentResolver.resolveArgument(null, null, null, null)) - .isInstanceOf(ForbiddenException.class) - .hasMessage(NOT_ENOUGH_PERMISSION.getMessage()); - } - - @Test - void Role이_Member일때_Member이면_성공() throws Exception { - // given - roleArgumentResolver = new RoleArgumentResolver(Role.MEMBER, authenticateContext); - authenticateContext.setAuthenticate(1L, Role.MEMBER); - - // when - Long memberId = roleArgumentResolver.resolveArgument(null, null, null, null); - - // then - assertThat(memberId).isEqualTo(authenticateContext.getId()); - } - - @ParameterizedTest - @ValueSource(strings = {"MEMBER", "ANONYMOUS"}) - void Role이_Admin일때_Admin이_아니면_예외(Role role) { - // given - roleArgumentResolver = new RoleArgumentResolver(Role.ADMIN, authenticateContext); - authenticateContext.setAuthenticate(1L, role); - - // when & then - assertThatThrownBy(() -> roleArgumentResolver.resolveArgument(null, null, null, null)) - .isInstanceOf(ForbiddenException.class) - .hasMessage(NOT_ENOUGH_PERMISSION.getMessage()); - } - - @Test - void Role이_Admin일때_Admin이면_성공() throws Exception { - // given - roleArgumentResolver = new RoleArgumentResolver(Role.ADMIN, authenticateContext); - authenticateContext.setAuthenticate(1L, Role.ADMIN); - - // when - Long memberId = roleArgumentResolver.resolveArgument(null, null, null, null); - - // then - assertThat(memberId).isEqualTo(authenticateContext.getId()); - } -} diff --git a/backend/src/test/java/com/festago/support/MockAuthTestExecutionListener.java b/backend/src/test/java/com/festago/support/MockAuthTestExecutionListener.java index d202a5803..9fe5beb09 100644 --- a/backend/src/test/java/com/festago/support/MockAuthTestExecutionListener.java +++ b/backend/src/test/java/com/festago/support/MockAuthTestExecutionListener.java @@ -1,7 +1,10 @@ package com.festago.support; import com.festago.auth.AuthenticateContext; -import com.festago.auth.domain.Role; +import com.festago.auth.domain.authentication.AdminAuthentication; +import com.festago.auth.domain.authentication.AnonymousAuthentication; +import com.festago.auth.domain.authentication.MemberAuthentication; +import com.festago.auth.domain.authentication.Authentication; import java.lang.reflect.Method; import org.springframework.context.ApplicationContext; import org.springframework.test.context.TestContext; @@ -16,7 +19,13 @@ public void beforeTestMethod(TestContext testContext) throws Exception { WithMockAuth withMockAuth = testMethod.getDeclaredAnnotation(WithMockAuth.class); ApplicationContext applicationContext = testContext.getApplicationContext(); AuthenticateContext authenticateContext = applicationContext.getBean(AuthenticateContext.class); - authenticateContext.setAuthenticate(withMockAuth.id(), withMockAuth.role()); + long id = withMockAuth.id(); + Authentication authentication = switch (withMockAuth.role()) { + case ANONYMOUS -> AnonymousAuthentication.getInstance(); + case MEMBER -> new MemberAuthentication(id); + case ADMIN -> new AdminAuthentication(id); + }; + authenticateContext.setAuthentication(authentication); } } @@ -24,6 +33,6 @@ public void beforeTestMethod(TestContext testContext) throws Exception { public void afterTestMethod(TestContext testContext) throws Exception { ApplicationContext applicationContext = testContext.getApplicationContext(); AuthenticateContext authenticateContext = applicationContext.getBean(AuthenticateContext.class); - authenticateContext.setAuthenticate(null, Role.ANONYMOUS); + authenticateContext.setAuthentication(AnonymousAuthentication.getInstance()); } } diff --git a/backend/src/test/java/com/festago/support/MockAuthTokenExtractor.java b/backend/src/test/java/com/festago/support/MockAuthTokenExtractor.java deleted file mode 100644 index e6c8d7e4b..000000000 --- a/backend/src/test/java/com/festago/support/MockAuthTokenExtractor.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.festago.support; - -import com.festago.auth.AuthenticateContext; -import com.festago.auth.application.AuthTokenExtractor; -import com.festago.auth.domain.AuthPayload; - -public class MockAuthTokenExtractor implements AuthTokenExtractor { - - private final AuthenticateContext authenticateContext; - - public MockAuthTokenExtractor(AuthenticateContext authenticateContext) { - this.authenticateContext = authenticateContext; - } - - @Override - public AuthPayload extract(String token) { - return new AuthPayload(authenticateContext.getId(), authenticateContext.getRole()); - } -} diff --git a/backend/src/test/java/com/festago/support/MockAuthenticationTokenExtractor.java b/backend/src/test/java/com/festago/support/MockAuthenticationTokenExtractor.java new file mode 100644 index 000000000..4005fd9a9 --- /dev/null +++ b/backend/src/test/java/com/festago/support/MockAuthenticationTokenExtractor.java @@ -0,0 +1,19 @@ +package com.festago.support; + +import com.festago.auth.AuthenticateContext; +import com.festago.auth.domain.AuthenticationTokenExtractor; +import com.festago.auth.domain.authentication.Authentication; + +public class MockAuthenticationTokenExtractor implements AuthenticationTokenExtractor { + + private final AuthenticateContext authenticateContext; + + public MockAuthenticationTokenExtractor(AuthenticateContext authenticateContext) { + this.authenticateContext = authenticateContext; + } + + @Override + public Authentication extract(String token) { + return authenticateContext.getAuthentication(); + } +} diff --git a/backend/src/test/java/com/festago/support/TestAuthConfig.java b/backend/src/test/java/com/festago/support/TestAuthConfig.java index 3ddf07189..4c6a103cb 100644 --- a/backend/src/test/java/com/festago/support/TestAuthConfig.java +++ b/backend/src/test/java/com/festago/support/TestAuthConfig.java @@ -1,8 +1,6 @@ package com.festago.support; import com.festago.auth.AuthenticateContext; -import com.festago.auth.application.AuthTokenExtractor; -import org.mockito.Mockito; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; @@ -15,7 +13,7 @@ public AuthenticateContext authenticateContext() { } @Bean - public AuthTokenExtractor authExtractor(AuthenticateContext authenticateContext) { - return Mockito.spy(new MockAuthTokenExtractor(authenticateContext)); + public MockAuthenticationTokenExtractor mockAuthenticationTokenExtractor(AuthenticateContext authenticateContext) { + return new MockAuthenticationTokenExtractor(authenticateContext); } }