From 57f68fd8da28e311b2db0f814a8643dceca041e1 Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Sat, 22 Feb 2025 20:12:34 +0900 Subject: [PATCH 01/18] =?UTF-8?q?feat:=20OAuth2AuthorizationRequestRedirec?= =?UTF-8?q?tFilter=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OAuth2AuthorizationRequestResolver 구현 - AuthorizationRequestRepository 구현 - OAuth2AuthorizationRequest 구현 --- .../AuthorizationRequestRepository.java | 13 ++++ ...ultOAuth2AuthorizationRequestResolver.java | 64 +++++++++++++++++++ ...nOAuth2AuthorizationRequestRepository.java | 50 +++++++++++++++ ...th2AuthorizationRequestRedirectFilter.java | 49 ++++++++++++++ .../OAuth2AuthorizationRequestResolver.java | 8 +++ .../registration/ClientRegistration.java | 26 ++++++++ .../ClientRegistrationRepository.java | 6 ++ .../core/OAuth2AuthorizationRequest.java | 13 ++++ .../oauth2/core/OAuth2ParameterNames.java | 12 ++++ 9 files changed, 241 insertions(+) create mode 100644 src/main/java/nextstep/security/oauth2/client/AuthorizationRequestRepository.java create mode 100644 src/main/java/nextstep/security/oauth2/client/DefaultOAuth2AuthorizationRequestResolver.java create mode 100644 src/main/java/nextstep/security/oauth2/client/HttpSessionOAuth2AuthorizationRequestRepository.java create mode 100644 src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java create mode 100644 src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestResolver.java create mode 100644 src/main/java/nextstep/security/oauth2/client/registration/ClientRegistration.java create mode 100644 src/main/java/nextstep/security/oauth2/client/registration/ClientRegistrationRepository.java create mode 100644 src/main/java/nextstep/security/oauth2/core/OAuth2AuthorizationRequest.java create mode 100644 src/main/java/nextstep/security/oauth2/core/OAuth2ParameterNames.java diff --git a/src/main/java/nextstep/security/oauth2/client/AuthorizationRequestRepository.java b/src/main/java/nextstep/security/oauth2/client/AuthorizationRequestRepository.java new file mode 100644 index 0000000..7482b06 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/AuthorizationRequestRepository.java @@ -0,0 +1,13 @@ +package nextstep.security.oauth2.client; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.oauth2.core.OAuth2AuthorizationRequest; + +public interface AuthorizationRequestRepository { + OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request); + + void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response); + + OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response); +} diff --git a/src/main/java/nextstep/security/oauth2/client/DefaultOAuth2AuthorizationRequestResolver.java b/src/main/java/nextstep/security/oauth2/client/DefaultOAuth2AuthorizationRequestResolver.java new file mode 100644 index 0000000..daae642 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/DefaultOAuth2AuthorizationRequestResolver.java @@ -0,0 +1,64 @@ +package nextstep.security.oauth2.client; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Base64; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.oauth2.client.registration.ClientRegistration; +import nextstep.security.oauth2.client.registration.ClientRegistrationRepository; +import nextstep.security.oauth2.core.OAuth2AuthorizationRequest; +import org.springframework.web.util.UriComponentsBuilder; + +public class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + private final ClientRegistrationRepository clientRegistrationRepository; + private final String authorizationRequestBaseUri; + + public DefaultOAuth2AuthorizationRequestResolver( + ClientRegistrationRepository clientRegistrationRepository, + String authorizationRequestBaseUri) { + this.clientRegistrationRepository = clientRegistrationRepository; + this.authorizationRequestBaseUri = authorizationRequestBaseUri; + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + String registrationId = resolveRegistrationId(request); + if (registrationId == null) { + return null; + } + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); + + if (clientRegistration == null) { + throw new AuthenticationException("Invalid Client Registration with Id: " + registrationId); + } + + String paramsQuery = UriComponentsBuilder.newInstance() + .queryParam("client_id", clientRegistration.clientId()) + .queryParam("response_type", "code") + .queryParam("scope", clientRegistration.scope()) + .queryParam("redirect_uri", clientRegistration.redirectUri()) + .build() + .toUri() + .getQuery(); + + String authorizationRequestUri = + clientRegistration.providerDetails().authorizationUri() + "?" + paramsQuery; + + return new OAuth2AuthorizationRequest( + clientRegistration.providerDetails().authorizationUri(), + clientRegistration.clientId(), + clientRegistration.redirectUri(), + clientRegistration.scope(), + authorizationRequestUri + ); + } + + private String resolveRegistrationId(HttpServletRequest request) { + String uri = request.getRequestURI(); + + if (uri.startsWith(authorizationRequestBaseUri)) { + return uri.substring(authorizationRequestBaseUri.length()); + } + + return null; + } +} diff --git a/src/main/java/nextstep/security/oauth2/client/HttpSessionOAuth2AuthorizationRequestRepository.java b/src/main/java/nextstep/security/oauth2/client/HttpSessionOAuth2AuthorizationRequestRepository.java new file mode 100644 index 0000000..36ed1d6 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/HttpSessionOAuth2AuthorizationRequestRepository.java @@ -0,0 +1,50 @@ +package nextstep.security.oauth2.client; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import nextstep.security.oauth2.core.OAuth2AuthorizationRequest; +import org.springframework.util.Assert; + +public class HttpSessionOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository { + + private static final String DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME = + HttpSessionOAuth2AuthorizationRequestRepository.class + .getName() + ".AUTHORIZATION_REQUEST"; + + private final String sessionAttributeName = DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME; + + @Override + public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { + Assert.notNull(request, "request cannot be null"); + return getAuthorizationRequest(request); + } + + @Override + public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, + HttpServletResponse response) { + Assert.notNull(request, "request cannot be null"); + Assert.notNull(response, "response cannot be null"); + if (authorizationRequest == null) { + removeAuthorizationRequest(request, response); + return; + } + request.getSession().setAttribute(this.sessionAttributeName, authorizationRequest); + } + + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, + HttpServletResponse response) { + Assert.notNull(response, "response cannot be null"); + OAuth2AuthorizationRequest authorizationRequest = loadAuthorizationRequest(request); + if (authorizationRequest != null) { + request.getSession().removeAttribute(this.sessionAttributeName); + } + return authorizationRequest; + } + + private OAuth2AuthorizationRequest getAuthorizationRequest(HttpServletRequest request) { + HttpSession session = request.getSession(false); + return (session != null) ? (OAuth2AuthorizationRequest) session.getAttribute(this.sessionAttributeName) : null; + } +} diff --git a/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java new file mode 100644 index 0000000..e4acc86 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java @@ -0,0 +1,49 @@ +package nextstep.security.oauth2.client; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import nextstep.security.oauth2.client.registration.ClientRegistrationRepository; +import nextstep.security.oauth2.core.OAuth2AuthenticationException; +import nextstep.security.oauth2.core.OAuth2AuthorizationRequest; +import org.springframework.util.Assert; +import org.springframework.web.filter.OncePerRequestFilter; + +public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter { + public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization/"; + private final OAuth2AuthorizationRequestResolver authorizationRequestResolver; + private final AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); + + public OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository) { + this(clientRegistrationRepository, DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); + } + + public OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository, + String authorizationRequestBaseUri) { + this.authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, + authorizationRequestBaseUri); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request); + if (authorizationRequest != null) { + sendRedirectForAuthorization(request, response, authorizationRequest); + return; + } + } catch (Exception e) { + throw new OAuth2AuthenticationException(); + } + filterChain.doFilter(request, response); + } + + private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response, + OAuth2AuthorizationRequest authorizationRequest) throws IOException { + this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); + response.sendRedirect(authorizationRequest.authorizationRequestUri()); + } +} diff --git a/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestResolver.java b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestResolver.java new file mode 100644 index 0000000..a88b5a8 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestResolver.java @@ -0,0 +1,8 @@ +package nextstep.security.oauth2.client; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.oauth2.core.OAuth2AuthorizationRequest; + +public interface OAuth2AuthorizationRequestResolver { + OAuth2AuthorizationRequest resolve(HttpServletRequest request); +} diff --git a/src/main/java/nextstep/security/oauth2/client/registration/ClientRegistration.java b/src/main/java/nextstep/security/oauth2/client/registration/ClientRegistration.java new file mode 100644 index 0000000..e67c716 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/registration/ClientRegistration.java @@ -0,0 +1,26 @@ +package nextstep.security.oauth2.client.registration; + +import java.util.Set; + +public record ClientRegistration( + String registrationId, + String clientId, + String clientSecret, + String redirectUri, + Set scope, + String authorizationGrantType, + String clientName, + ProviderDetails providerDetails +) { + public record ProviderDetails( + String authorizationUri, + String tokenUri, + UserInfoEndpoint userInfoEndpoint + ) { + } + + public record UserInfoEndpoint(String uri, + String userNameAttributeName + ) { + } +} diff --git a/src/main/java/nextstep/security/oauth2/client/registration/ClientRegistrationRepository.java b/src/main/java/nextstep/security/oauth2/client/registration/ClientRegistrationRepository.java new file mode 100644 index 0000000..a11f75f --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/registration/ClientRegistrationRepository.java @@ -0,0 +1,6 @@ +package nextstep.security.oauth2.client.registration; + +public interface ClientRegistrationRepository { + + ClientRegistration findByRegistrationId(String registrationId); +} diff --git a/src/main/java/nextstep/security/oauth2/core/OAuth2AuthorizationRequest.java b/src/main/java/nextstep/security/oauth2/core/OAuth2AuthorizationRequest.java new file mode 100644 index 0000000..44ab550 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/core/OAuth2AuthorizationRequest.java @@ -0,0 +1,13 @@ +package nextstep.security.oauth2.core; + +import java.util.Set; + +public record OAuth2AuthorizationRequest( + String authorizationUri, + String clientId, + String redirectUri, + Set scope, + String authorizationRequestUri +) { + +} diff --git a/src/main/java/nextstep/security/oauth2/core/OAuth2ParameterNames.java b/src/main/java/nextstep/security/oauth2/core/OAuth2ParameterNames.java new file mode 100644 index 0000000..8c613aa --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/core/OAuth2ParameterNames.java @@ -0,0 +1,12 @@ +package nextstep.security.oauth2.core; + +public class OAuth2ParameterNames { + public static final String CLIENT_ID = "client_id"; + public static final String REDIRECT_URI = "redirect_uri"; + public static final String RESPONSE_TYPE = "response_type"; + public static final String SCOPE = "scope"; + public static final String CODE = "code"; + public static final String GRANT_TYPE = "grant_type"; + public static final String AUTHORIZATION_CODE = "authorization_code"; + public static final String ACCESS_TOKEN = "access_token"; +} From e41136ac1b2c4b72fa25cc871087a102e3b91a79 Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Sat, 22 Feb 2025 23:25:08 +0900 Subject: [PATCH 02/18] =?UTF-8?q?feat:=20ClientRegistrationRepository=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 빈 등록 --- .../java/nextstep/app/SecurityConfig.java | 58 ++++++++++++++++--- .../OAuth2ClientProperties.java | 3 +- .../ClientRegistrationRepository.java | 21 +++++++ src/main/resources/application.yml | 2 + src/test/resources/application.yml | 2 + 5 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 251b2e7..220f56f 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -1,5 +1,7 @@ package nextstep.app; +import java.util.Map; +import java.util.stream.Collectors; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import nextstep.app.infrastructure.InmemoryMemberRepository; @@ -13,6 +15,8 @@ import nextstep.security.authentication.OAuth2AccessTokenClient; import nextstep.security.authentication.OAuth2AuthenticationFilter; import nextstep.security.authentication.OAuth2ClientProperties; +import nextstep.security.authentication.OAuth2ClientProperties.Provider; +import nextstep.security.authentication.OAuth2ClientProperties.Registration; import nextstep.security.authentication.OAuth2LoginRedirectFilter; import nextstep.security.authentication.OAuth2UserInfoClient; import nextstep.security.authentication.UsernamePasswordAuthenticationFilter; @@ -22,6 +26,10 @@ import nextstep.security.config.FilterChainProxy; import nextstep.security.config.SecurityFilterChain; import nextstep.security.context.SecurityContextHolderFilter; +import nextstep.security.oauth2.client.OAuth2AuthorizationRequestRedirectFilter; +import nextstep.security.oauth2.client.registration.ClientRegistration; +import nextstep.security.oauth2.client.registration.ClientRegistrationRepository; +import nextstep.security.oauth2.client.registration.ClientRegistrationRepository.InMemoryClientRegistrationRepository; import nextstep.security.userdetails.UserDetails; import nextstep.security.userdetails.UserDetailsService; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -39,8 +47,8 @@ @EnableConfigurationProperties(OAuth2ClientProperties.class) public class SecurityConfig { - private final MemberRepository memberRepository; - private final OAuth2ClientProperties oAuth2ClientProperties; + private final MemberRepository memberRepository; + private final OAuth2ClientProperties oAuth2ClientProperties; public SecurityConfig(MemberRepository memberRepository, OAuth2ClientProperties oAuth2ClientProperties) { @@ -73,6 +81,12 @@ public FilterChainProxy filterChainProxy(List securityFilte return new FilterChainProxy(securityFilterChains); } + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + Map registrations = getClientRegistrations(oAuth2ClientProperties); + return new InMemoryClientRegistrationRepository(registrations); + } + @Bean public SecuredMethodInterceptor securedMethodInterceptor() { return new SecuredMethodInterceptor(); @@ -85,8 +99,7 @@ public SecurityFilterChain securityFilterChain() { new SecurityContextHolderFilter(), new UsernamePasswordAuthenticationFilter(userDetailsService()), new BasicAuthenticationFilter(userDetailsService()), - new OAuth2LoginRedirectFilter(oAuth2ClientProperties), - new OAuth2AuthenticationFilter(oAuth2ClientProperties, memberRepository, oAuth2AccessTokenClient(), oAuth2UserInfoClient()), + new OAuth2AuthorizationRequestRedirectFilter(clientRegistrationRepository()), new AuthorizationFilter(requestAuthorizationManager()) ) ); @@ -102,9 +115,12 @@ public RoleHierarchy roleHierarchy() { @Bean public RequestAuthorizationManager requestAuthorizationManager() { List> mappings = new ArrayList<>(); - mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members"), new AuthorityAuthorizationManager(roleHierarchy(), "ADMIN"))); - mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members/me"), new AuthorityAuthorizationManager(roleHierarchy(), "USER"))); - mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/search"), new PermitAllAuthorizationManager())); + mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members"), + new AuthorityAuthorizationManager(roleHierarchy(), "ADMIN"))); + mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members/me"), + new AuthorityAuthorizationManager(roleHierarchy(), "USER"))); + mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/search"), + new PermitAllAuthorizationManager())); mappings.add(new RequestMatcherEntry<>(AnyRequestMatcher.INSTANCE, new PermitAllAuthorizationManager())); return new RequestAuthorizationManager(mappings); } @@ -133,4 +149,32 @@ public Set getAuthorities() { }; }; } + + private Map getClientRegistrations(OAuth2ClientProperties oAuth2ClientProperties) { + return oAuth2ClientProperties.getRegistrations().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> toClientRegistration(entry.getKey(), entry.getValue()))); + } + + private ClientRegistration toClientRegistration(String registrationId, + Registration registration) { + Provider provider = oAuth2ClientProperties.getProviders().get(registration.provider()); + return new ClientRegistration( + registrationId, + registration.clientId(), + registration.clientSecret(), + registration.redirectUri(), + Set.copyOf(registration.scope()), + registration.authorizationGrantType(), + provider.name(), + new ClientRegistration.ProviderDetails( + provider.authorizationUri(), + provider.tokenUri(), + new ClientRegistration.UserInfoEndpoint( + provider.userInfoUri(), + provider.userNameAttributeName() + ) + ) + ); + } } diff --git a/src/main/java/nextstep/security/authentication/OAuth2ClientProperties.java b/src/main/java/nextstep/security/authentication/OAuth2ClientProperties.java index 9496e7a..752f460 100644 --- a/src/main/java/nextstep/security/authentication/OAuth2ClientProperties.java +++ b/src/main/java/nextstep/security/authentication/OAuth2ClientProperties.java @@ -41,7 +41,8 @@ public record Provider( String authorizationUri, String tokenUri, String userInfoUri, - String accessTokenUri + String accessTokenUri, + String userNameAttributeName ) { } } diff --git a/src/main/java/nextstep/security/oauth2/client/registration/ClientRegistrationRepository.java b/src/main/java/nextstep/security/oauth2/client/registration/ClientRegistrationRepository.java index a11f75f..45c94c7 100644 --- a/src/main/java/nextstep/security/oauth2/client/registration/ClientRegistrationRepository.java +++ b/src/main/java/nextstep/security/oauth2/client/registration/ClientRegistrationRepository.java @@ -1,6 +1,27 @@ package nextstep.security.oauth2.client.registration; +import java.util.Map; + public interface ClientRegistrationRepository { ClientRegistration findByRegistrationId(String registrationId); + + class InMemoryClientRegistrationRepository implements ClientRegistrationRepository { + private final Map registrations; + + public InMemoryClientRegistrationRepository(Map registrations) { + this.registrations = registrations; + } + + @Override + public ClientRegistration findByRegistrationId(String registrationId) { + if (registrationId == null || registrationId.isBlank()) { + throw new IllegalArgumentException("Registration ID must not be null or empty"); + } + return registrations.get(registrationId); + } + + } } + + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 78593fa..bf0f942 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,8 +26,10 @@ spring: token-uri: "https://github.com/login/oauth/access_token" access-token-uri: "http://localhost:8089/login/oauth/access_token" user-info-uri: "https://api.github.com/user" + user-name-attribute-name: "email" google: authorization-uri: "https://accounts.google.com/o/oauth2/v2/auth?" token-uri: "https://oauth2.googleapis.com/token" access-token-uri: "http://localhost:8089/login/oauth/access_token" user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo" + user-name-attribute-name: "email" diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 7061def..480dbd6 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -27,9 +27,11 @@ spring: token-uri: "https://github.com/login/oauth/access_token" access-token-uri: "http://localhost:8089/login/oauth/access_token" user-info-uri: "http://localhost:8089/user" + user-name-attribute-name: "email" google: name: "google" authorization-uri: "https://accounts.google.com/o/oauth2/v2/auth?" token-uri: "https://oauth2.googleapis.com/token" access-token-uri: "http://localhost:8089/oauth2.googleapis.com/token" user-info-uri: "http://localhost:8089/oauth2/v1/userinfo" + user-name-attribute-name: "email" From 2584da7ec5fb1018689993e12fcc6901d19a5dc0 Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Sun, 23 Feb 2025 00:38:57 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20OAuth2UserService=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/ui/LoginController.java | 11 +- .../authentication/GithubProfileUser.java | 30 +++++ .../authentication/GoogleProfileUser.java | 30 +++++ .../authentication/OAuth2ProfileUser.java | 7 ++ .../OAuth2ProfileUserFactory.java | 13 ++ .../userinfo/DefaultOAuth2UserService.java | 116 ++++++++++++++++++ .../client/userinfo/OAuth2UserRequest.java | 7 ++ .../client/userinfo/OAuth2UserService.java | 7 ++ .../oauth2/core/OAuth2AccessToken.java | 5 + .../core/OAuth2AuthenticationException.java | 7 ++ .../oauth2/core/user/DefaultOAuth2User.java | 35 ++++++ .../security/oauth2/core/user/OAuth2User.java | 12 ++ 12 files changed, 275 insertions(+), 5 deletions(-) create mode 100644 src/main/java/nextstep/security/oauth2/client/authentication/GithubProfileUser.java create mode 100644 src/main/java/nextstep/security/oauth2/client/authentication/GoogleProfileUser.java create mode 100644 src/main/java/nextstep/security/oauth2/client/authentication/OAuth2ProfileUser.java create mode 100644 src/main/java/nextstep/security/oauth2/client/authentication/OAuth2ProfileUserFactory.java create mode 100644 src/main/java/nextstep/security/oauth2/client/userinfo/DefaultOAuth2UserService.java create mode 100644 src/main/java/nextstep/security/oauth2/client/userinfo/OAuth2UserRequest.java create mode 100644 src/main/java/nextstep/security/oauth2/client/userinfo/OAuth2UserService.java create mode 100644 src/main/java/nextstep/security/oauth2/core/OAuth2AccessToken.java create mode 100644 src/main/java/nextstep/security/oauth2/core/OAuth2AuthenticationException.java create mode 100644 src/main/java/nextstep/security/oauth2/core/user/DefaultOAuth2User.java create mode 100644 src/main/java/nextstep/security/oauth2/core/user/OAuth2User.java diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 25d905b..9fda8ac 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpSession; import nextstep.security.authentication.Authentication; import nextstep.security.context.SecurityContextHolder; +import nextstep.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -32,10 +33,10 @@ private String extractUsername(Authentication authentication) { if (authentication.getPrincipal() instanceof String) { return (String) authentication.getPrincipal(); } -// if (authentication.getPrincipal() instanceof OAuth2User) { -// String userNameAttributeName = ((OAuth2User) authentication.getPrincipal()).getUserNameAttributeName(); -// return (String) ((OAuth2User) authentication.getPrincipal()).getAttributes().get(userNameAttributeName); -// } + if (authentication.getPrincipal() instanceof OAuth2User) { + String userNameAttributeName = ((OAuth2User) authentication.getPrincipal()).getUserNameAttributeName(); + return (String) ((OAuth2User) authentication.getPrincipal()).getAttributes().get(userNameAttributeName); + } return ""; } @@ -54,4 +55,4 @@ public String logout(HttpServletRequest request, HttpServletResponse response) { return "redirect:/"; } -} \ No newline at end of file +} diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/GithubProfileUser.java b/src/main/java/nextstep/security/oauth2/client/authentication/GithubProfileUser.java new file mode 100644 index 0000000..3577d02 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/authentication/GithubProfileUser.java @@ -0,0 +1,30 @@ +package nextstep.security.oauth2.client.authentication; + +import java.util.Map; + +public class GithubProfileUser implements OAuth2ProfileUser { + private final String name; + private final String imageUrl; + private final String email; + + public GithubProfileUser(Map attributes) { + this.name = (String) attributes.get("name"); + this.imageUrl = (String) attributes.get("avatar_url"); + this.email = (String) attributes.get("email"); // 이메일이 null일 수도 있음 + } + + @Override + public String getName() { + return name; + } + + @Override + public String getImageUrl() { + return imageUrl; + } + + @Override + public String getEmail() { + return email; + } +} diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/GoogleProfileUser.java b/src/main/java/nextstep/security/oauth2/client/authentication/GoogleProfileUser.java new file mode 100644 index 0000000..27d3a9d --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/authentication/GoogleProfileUser.java @@ -0,0 +1,30 @@ +package nextstep.security.oauth2.client.authentication; + +import java.util.Map; + +public class GoogleProfileUser implements OAuth2ProfileUser { + private final String name; + private final String imageUrl; + private final String email; + + public GoogleProfileUser(Map attributes) { + this.name = (String) attributes.get("name"); + this.imageUrl = (String) attributes.get("picture"); + this.email = (String) attributes.get("email"); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getImageUrl() { + return imageUrl; + } + + @Override + public String getEmail() { + return email; + } +} diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2ProfileUser.java b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2ProfileUser.java new file mode 100644 index 0000000..4a669bd --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2ProfileUser.java @@ -0,0 +1,7 @@ +package nextstep.security.oauth2.client.authentication; + +public interface OAuth2ProfileUser { + String getName(); + String getImageUrl(); + String getEmail(); +} diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2ProfileUserFactory.java b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2ProfileUserFactory.java new file mode 100644 index 0000000..391abaf --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2ProfileUserFactory.java @@ -0,0 +1,13 @@ +package nextstep.security.oauth2.client.authentication; + +import java.util.Map; + +public class OAuth2ProfileUserFactory { + public static OAuth2ProfileUser create(String registrationId, Map attributes) { + return switch (registrationId) { + case "github" -> new GithubProfileUser(attributes); + case "google" -> new GoogleProfileUser(attributes); + default -> throw new IllegalArgumentException("Unsupported OAuth2 provider: " + registrationId); + }; + } +} diff --git a/src/main/java/nextstep/security/oauth2/client/userinfo/DefaultOAuth2UserService.java b/src/main/java/nextstep/security/oauth2/client/userinfo/DefaultOAuth2UserService.java new file mode 100644 index 0000000..b6d456a --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/userinfo/DefaultOAuth2UserService.java @@ -0,0 +1,116 @@ +package nextstep.security.oauth2.client.userinfo; + +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.security.oauth2.client.authentication.OAuth2ProfileUser; +import nextstep.security.oauth2.client.authentication.OAuth2ProfileUserFactory; +import nextstep.security.oauth2.client.registration.ClientRegistration; +import nextstep.security.oauth2.core.OAuth2AuthenticationException; +import nextstep.security.oauth2.core.user.DefaultOAuth2User; +import nextstep.security.oauth2.core.user.OAuth2User; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +public class DefaultOAuth2UserService implements OAuth2UserService { + private final MemberRepository memberRepository; + + private static final ParameterizedTypeReference> PARAMETERIZED_RESPONSE_TYPE = new ParameterizedTypeReference<>() { + }; + private final RestOperations restOperations; + + public DefaultOAuth2UserService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + this.restOperations = new RestTemplate(); + } + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + Assert.notNull(userRequest, "userRequest cannot be null"); + String userNameAttributeName = getUserNameAttributeName(userRequest); + RequestEntity request = convertRequestEntity(userRequest); + ResponseEntity> response = getResponse(request); + Map attributes = convertAttributes(userRequest, response.getBody()); + String email = attributes.get("email").toString(); + Member member = memberRepository.findByEmail(email) + .orElseGet(() -> memberRepository.save( + new Member( + email, + "", + attributes.get("name").toString(), + attributes.get("imageUrl").toString(), + Set.of("USER") + ) + )); + return new DefaultOAuth2User(member, attributes, userNameAttributeName); + } + + private String getUserNameAttributeName(OAuth2UserRequest userRequest) { + if (!StringUtils + .hasText(userRequest.clientRegistration().providerDetails().userInfoEndpoint().uri())) { + throw new OAuth2AuthenticationException(); + } + String userNameAttributeName = userRequest.clientRegistration() + .providerDetails() + .userInfoEndpoint() + .userNameAttributeName(); + + if (!StringUtils.hasText(userNameAttributeName)) { + throw new OAuth2AuthenticationException(); + } + + return userNameAttributeName; + } + + private RequestEntity convertRequestEntity(OAuth2UserRequest userRequest) { + ClientRegistration clientRegistration = userRequest.clientRegistration(); + + HttpHeaders headers = new HttpHeaders(); + + URI uri = UriComponentsBuilder + .fromUriString(clientRegistration.providerDetails().userInfoEndpoint().uri()) + .build() + .toUri(); + + headers.setBearerAuth(userRequest.accessToken().tokenValue()); + + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + + return new RequestEntity<>(headers, HttpMethod.GET, uri); + } + + private Map convertAttributes(OAuth2UserRequest userRequest, Map body) { + String clientRegistrationId = userRequest.clientRegistration().registrationId(); + + OAuth2ProfileUser profileUser = OAuth2ProfileUserFactory.create(clientRegistrationId, body); + + Map convertedAttributes = new HashMap<>(); + convertedAttributes.put("name", profileUser.getName()); + convertedAttributes.put("imageUrl", profileUser.getImageUrl()); + convertedAttributes.put("email", profileUser.getEmail()); + + return convertedAttributes; + } + + private ResponseEntity> getResponse(RequestEntity request) { + try { + return this.restOperations.exchange(request, PARAMETERIZED_RESPONSE_TYPE); + } catch (Exception e) { + throw new OAuth2AuthenticationException(); + } + } +} + diff --git a/src/main/java/nextstep/security/oauth2/client/userinfo/OAuth2UserRequest.java b/src/main/java/nextstep/security/oauth2/client/userinfo/OAuth2UserRequest.java new file mode 100644 index 0000000..23637df --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/userinfo/OAuth2UserRequest.java @@ -0,0 +1,7 @@ +package nextstep.security.oauth2.client.userinfo; + +import nextstep.security.oauth2.client.registration.ClientRegistration; +import nextstep.security.oauth2.core.OAuth2AccessToken; + +public record OAuth2UserRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken) { +} diff --git a/src/main/java/nextstep/security/oauth2/client/userinfo/OAuth2UserService.java b/src/main/java/nextstep/security/oauth2/client/userinfo/OAuth2UserService.java new file mode 100644 index 0000000..deec0b9 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/userinfo/OAuth2UserService.java @@ -0,0 +1,7 @@ +package nextstep.security.oauth2.client.userinfo; + +import nextstep.security.oauth2.core.user.OAuth2User; + +public interface OAuth2UserService { + OAuth2User loadUser(OAuth2UserRequest userRequest); +} diff --git a/src/main/java/nextstep/security/oauth2/core/OAuth2AccessToken.java b/src/main/java/nextstep/security/oauth2/core/OAuth2AccessToken.java new file mode 100644 index 0000000..38a2292 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/core/OAuth2AccessToken.java @@ -0,0 +1,5 @@ +package nextstep.security.oauth2.core; + +public record OAuth2AccessToken(String tokenValue) { + +} diff --git a/src/main/java/nextstep/security/oauth2/core/OAuth2AuthenticationException.java b/src/main/java/nextstep/security/oauth2/core/OAuth2AuthenticationException.java new file mode 100644 index 0000000..7d76930 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/core/OAuth2AuthenticationException.java @@ -0,0 +1,7 @@ +package nextstep.security.oauth2.core; + +import nextstep.security.authentication.AuthenticationException; + +public class OAuth2AuthenticationException extends AuthenticationException { + +} diff --git a/src/main/java/nextstep/security/oauth2/core/user/DefaultOAuth2User.java b/src/main/java/nextstep/security/oauth2/core/user/DefaultOAuth2User.java new file mode 100644 index 0000000..c09b5f5 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/core/user/DefaultOAuth2User.java @@ -0,0 +1,35 @@ +package nextstep.security.oauth2.core.user; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import nextstep.app.domain.Member; + +public class DefaultOAuth2User implements OAuth2User { + + private final Member member; + private final Map attributes; + private final String userNameAttributeName; + + // 생성자 + public DefaultOAuth2User(Member member, Map attributes, String userNameAttributeName) { + this.member = member; + this.attributes = attributes; + this.userNameAttributeName = userNameAttributeName; + } + + @Override + public String getUserNameAttributeName() { + return userNameAttributeName; + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Set getAuthorities() { + return member.getRoles(); + } +} diff --git a/src/main/java/nextstep/security/oauth2/core/user/OAuth2User.java b/src/main/java/nextstep/security/oauth2/core/user/OAuth2User.java new file mode 100644 index 0000000..0bbc701 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/core/user/OAuth2User.java @@ -0,0 +1,12 @@ +package nextstep.security.oauth2.core.user; + +import java.util.Map; +import java.util.Set; + +public interface OAuth2User { + Set getAuthorities(); + + Map getAttributes(); + + String getUserNameAttributeName(); +} From b394ce609919c8e29182693a485c7a030a0475bb Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Mon, 24 Feb 2025 04:13:05 +0900 Subject: [PATCH 04/18] =?UTF-8?q?chore:=20yml=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 2 +- src/test/resources/application.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bf0f942..672bea8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -28,7 +28,7 @@ spring: user-info-uri: "https://api.github.com/user" user-name-attribute-name: "email" google: - authorization-uri: "https://accounts.google.com/o/oauth2/v2/auth?" + authorization-uri: "https://accounts.google.com/o/oauth2/v2/auth" token-uri: "https://oauth2.googleapis.com/token" access-token-uri: "http://localhost:8089/login/oauth/access_token" user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo" diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 480dbd6..845b118 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -24,14 +24,14 @@ spring: github: name: "github" authorization-uri: "https://github.com/login/oauth/authorize?" - token-uri: "https://github.com/login/oauth/access_token" + token-uri: "http://localhost:8089/login/oauth/access_token" access-token-uri: "http://localhost:8089/login/oauth/access_token" user-info-uri: "http://localhost:8089/user" user-name-attribute-name: "email" google: name: "google" - authorization-uri: "https://accounts.google.com/o/oauth2/v2/auth?" - token-uri: "https://oauth2.googleapis.com/token" + authorization-uri: "https://accounts.google.com/o/oauth2/v2/auth" + token-uri: "http://localhost:8089/oauth2.googleapis.com/token" access-token-uri: "http://localhost:8089/oauth2.googleapis.com/token" user-info-uri: "http://localhost:8089/oauth2/v1/userinfo" user-name-attribute-name: "email" From 67da26b38b6c80088ca3ba32de60739ad6329d00 Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Mon, 24 Feb 2025 04:14:36 +0900 Subject: [PATCH 05/18] =?UTF-8?q?feat:=20OAuth2LoginAuthenticationFilter?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OAuth2AccessTokenResponseClient.java | 74 ++++++++ .../OAuth2AuthorizationCodeGrantRequest.java | 8 + .../oauth2/client/OAuth2AuthorizedClient.java | 8 + .../client/OAuth2AuthorizedClientService.java | 10 + .../OAuth2AccessTokenResponse.java | 19 ++ .../OAuth2AuthenticationToken.java | 46 +++++ ...thorizationCodeAuthenticationProvider.java | 37 ++++ ...2AuthorizationCodeAuthenticationToken.java | 63 +++++++ .../OAuth2LoginAuthenticationProvider.java | 50 +++++ .../OAuth2LoginAuthenticationToken.java | 81 ++++++++ .../web/AuthenticationSuccessHandler.java | 12 ++ ...ssionOAuth2AuthorizedClientRepository.java | 66 +++++++ .../web/OAuth2AuthorizationResponseUtils.java | 44 +++++ .../web/OAuth2AuthorizedClientRepository.java | 17 ++ .../web/OAuth2LoginAuthenticationFilter.java | 177 ++++++++++++++++++ .../core/OAuth2AuthorizationExchange.java | 13 ++ .../core/OAuth2AuthorizationResponse.java | 17 ++ 17 files changed, 742 insertions(+) create mode 100644 src/main/java/nextstep/security/oauth2/OAuth2AccessTokenResponseClient.java create mode 100644 src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationCodeGrantRequest.java create mode 100644 src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizedClient.java create mode 100644 src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizedClientService.java create mode 100644 src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AccessTokenResponse.java create mode 100644 src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthenticationToken.java create mode 100644 src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java create mode 100644 src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java create mode 100644 src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java create mode 100644 src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java create mode 100644 src/main/java/nextstep/security/oauth2/client/web/AuthenticationSuccessHandler.java create mode 100644 src/main/java/nextstep/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java create mode 100644 src/main/java/nextstep/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java create mode 100644 src/main/java/nextstep/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java create mode 100644 src/main/java/nextstep/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java create mode 100644 src/main/java/nextstep/security/oauth2/core/OAuth2AuthorizationExchange.java create mode 100644 src/main/java/nextstep/security/oauth2/core/OAuth2AuthorizationResponse.java diff --git a/src/main/java/nextstep/security/oauth2/OAuth2AccessTokenResponseClient.java b/src/main/java/nextstep/security/oauth2/OAuth2AccessTokenResponseClient.java new file mode 100644 index 0000000..a28378c --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/OAuth2AccessTokenResponseClient.java @@ -0,0 +1,74 @@ +package nextstep.security.oauth2; + + +import java.net.URI; +import java.util.Map; +import nextstep.security.oauth2.client.OAuth2AuthorizationCodeGrantRequest; +import nextstep.security.oauth2.client.authentication.OAuth2AccessTokenResponse; +import nextstep.security.oauth2.core.OAuth2AccessToken; +import nextstep.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +public class OAuth2AccessTokenResponseClient { + + private final RestTemplate restOperations = new RestTemplate(); + + private static MultiValueMap getStringStringMultiValueMap( + OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest, String clientId) { + String clientSecret = authorizationCodeGrantRequest.clientRegistration().clientSecret(); + String redirectUri = authorizationCodeGrantRequest.authorizationExchange().authorizationRequest() + .redirectUri(); + String authorizationCode = authorizationCodeGrantRequest.authorizationExchange().authorizationResponse().code(); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", "authorization_code"); + body.add("code", authorizationCode); + body.add("redirect_uri", redirectUri); + body.add("client_id", clientId); + body.add("client_secret", clientSecret); + return body; + } + + public OAuth2AccessTokenResponse getTokenResponse( + OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) { + + RequestEntity request = convertRequestEntity(authorizationCodeGrantRequest); + OAuth2AccessTokenResponse tokenResponse = getResponse(request); + Assert.notNull(tokenResponse, + "The authorization server responded to this Authorization Code grant request with an empty body; as such, it cannot be materialized into an OAuth2AccessTokenResponse instance. Please check the HTTP response code in your server logs for more details."); + return tokenResponse; + } + + private RequestEntity convertRequestEntity(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) { + String tokenUri = authorizationCodeGrantRequest.clientRegistration().providerDetails().tokenUri(); + String clientId = authorizationCodeGrantRequest.clientRegistration().clientId(); + MultiValueMap body = getStringStringMultiValueMap( + authorizationCodeGrantRequest, clientId); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Content-Type", "application/x-www-form-urlencoded"); + + URI uri = UriComponentsBuilder.fromUriString(tokenUri).build().toUri(); + + return new RequestEntity<>(body, headers, HttpMethod.POST, uri); + } + + private OAuth2AccessTokenResponse getResponse(RequestEntity request) { + try { + ResponseEntity results = this.restOperations.exchange(request, Map.class); + String accessToken = (String) results.getBody().get("access_token"); + return new OAuth2AccessTokenResponse(new OAuth2AccessToken(accessToken)); + } catch (Exception e) { + throw new OAuth2AuthenticationException(); + } + } +} + diff --git a/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationCodeGrantRequest.java b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationCodeGrantRequest.java new file mode 100644 index 0000000..9372f1b --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationCodeGrantRequest.java @@ -0,0 +1,8 @@ +package nextstep.security.oauth2.client; + +import nextstep.security.oauth2.client.registration.ClientRegistration; +import nextstep.security.oauth2.core.OAuth2AuthorizationExchange; + +public record OAuth2AuthorizationCodeGrantRequest(ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange) { +} diff --git a/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizedClient.java b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizedClient.java new file mode 100644 index 0000000..0dbe3cb --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizedClient.java @@ -0,0 +1,8 @@ +package nextstep.security.oauth2.client; + +import nextstep.security.oauth2.client.registration.ClientRegistration; +import nextstep.security.oauth2.core.OAuth2AccessToken; + +public record OAuth2AuthorizedClient(ClientRegistration clientRegistration, String principalName, + OAuth2AccessToken accessToken) { +} diff --git a/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizedClientService.java b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizedClientService.java new file mode 100644 index 0000000..0704e5f --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizedClientService.java @@ -0,0 +1,10 @@ +package nextstep.security.oauth2.client; + +public interface OAuth2AuthorizedClientService { + + OAuth2AuthorizedClient loadAuthorizedClient(String clientRegistrationId, String principalName); + + void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, String principalName); + + void removeAuthorizedClient(String clientRegistrationId, String principalName); +} diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AccessTokenResponse.java b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AccessTokenResponse.java new file mode 100644 index 0000000..601290a --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AccessTokenResponse.java @@ -0,0 +1,19 @@ +package nextstep.security.oauth2.client.authentication; + +import nextstep.security.oauth2.core.OAuth2AccessToken; + +public class OAuth2AccessTokenResponse { + private OAuth2AccessToken accessToken; + + public OAuth2AccessTokenResponse() { + } + + public OAuth2AccessTokenResponse(OAuth2AccessToken accessToken) { + this.accessToken = accessToken; + } + + public OAuth2AccessToken getAccessToken() { + return accessToken; + } +} + diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthenticationToken.java b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthenticationToken.java new file mode 100644 index 0000000..a99013a --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthenticationToken.java @@ -0,0 +1,46 @@ +package nextstep.security.oauth2.client.authentication; + +import java.util.Set; +import nextstep.security.authentication.Authentication; +import nextstep.security.oauth2.core.user.OAuth2User; +import org.springframework.util.Assert; + +public class OAuth2AuthenticationToken implements Authentication { + private final OAuth2User principal; + + private final Set authorities; + + private final boolean authenticated; + + public OAuth2AuthenticationToken(OAuth2User principal, Set authorities) { + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + this.authorities = authorities; + this.authenticated = true; + } + + @Override + public Set getAuthorities() { + return authorities; + } + + @Override + public Object getCredentials() { + return ""; + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } + + public String getName() { + return principal.toString(); + } + +} diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java new file mode 100644 index 0000000..66f50ef --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java @@ -0,0 +1,37 @@ +package nextstep.security.oauth2.client.authentication; + +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authentication.AuthenticationProvider; +import nextstep.security.oauth2.OAuth2AccessTokenResponseClient; +import nextstep.security.oauth2.client.OAuth2AuthorizationCodeGrantRequest; +import nextstep.security.oauth2.core.OAuth2AuthorizationRequest; +import nextstep.security.oauth2.core.OAuth2AuthorizationResponse; +import org.springframework.util.Assert; + +public class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider { + private final OAuth2AccessTokenResponseClient accessTokenResponseClient; + + public OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AccessTokenResponseClient accessTokenResponseClient) { + Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null"); + this.accessTokenResponseClient = accessTokenResponseClient; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = (OAuth2AuthorizationCodeAuthenticationToken) authentication; + + OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient.getTokenResponse( + new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(), + authorizationCodeAuthentication.getAuthorizationExchange())); + + return new OAuth2AuthorizationCodeAuthenticationToken( + authorizationCodeAuthentication.getClientRegistration(), + authorizationCodeAuthentication.getAuthorizationExchange(), accessTokenResponse.getAccessToken()); + } + + @Override + public boolean supports(Class authentication) { + return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java new file mode 100644 index 0000000..9547ba2 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java @@ -0,0 +1,63 @@ +package nextstep.security.oauth2.client.authentication; + + +import java.util.Set; +import nextstep.security.authentication.Authentication; +import nextstep.security.oauth2.client.registration.ClientRegistration; +import nextstep.security.oauth2.core.OAuth2AccessToken; +import nextstep.security.oauth2.core.OAuth2AuthorizationExchange; + +public class OAuth2AuthorizationCodeAuthenticationToken implements Authentication { + private ClientRegistration clientRegistration; + + private OAuth2AuthorizationExchange authorizationExchange; + + private OAuth2AccessToken accessToken; + + public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange) { + this.clientRegistration = clientRegistration; + this.authorizationExchange = authorizationExchange; + } + + public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange, + OAuth2AccessToken accessToken) { + this.clientRegistration = clientRegistration; + this.authorizationExchange = authorizationExchange; + this.accessToken = accessToken; + } + + public ClientRegistration getClientRegistration() { + return clientRegistration; + } + + public OAuth2AuthorizationExchange getAuthorizationExchange() { + return authorizationExchange; + } + + public OAuth2AccessToken getAccessToken() { + return accessToken; + } + + @Override + public Set getAuthorities() { + return Set.of(); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + + @Override + public boolean isAuthenticated() { + return false; + } +} + diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java new file mode 100644 index 0000000..7ba36ea --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java @@ -0,0 +1,50 @@ +package nextstep.security.oauth2.client.authentication; + +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authentication.AuthenticationProvider; +import nextstep.security.oauth2.OAuth2AccessTokenResponseClient; +import nextstep.security.oauth2.client.userinfo.OAuth2UserRequest; +import nextstep.security.oauth2.client.userinfo.OAuth2UserService; +import nextstep.security.oauth2.core.OAuth2AccessToken; +import nextstep.security.oauth2.core.user.OAuth2User; + +public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider { + + private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider; + + private final OAuth2UserService userService; + + public OAuth2LoginAuthenticationProvider(OAuth2UserService userService) { + this.authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider( + new OAuth2AccessTokenResponseClient()); + this.userService = userService; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken) authentication; + + OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken + = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider + .authenticate( + new OAuth2AuthorizationCodeAuthenticationToken( + loginAuthenticationToken.getClientRegistration(), + loginAuthenticationToken.getAuthorizationExchange() + ) + ); + + OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken(); + OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest( + loginAuthenticationToken.getClientRegistration(), accessToken)); + + return new OAuth2LoginAuthenticationToken( + loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(), + oauth2User, oauth2User.getAuthorities(), accessToken); + } + + @Override + public boolean supports(Class authentication) { + return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java new file mode 100644 index 0000000..d0763fb --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java @@ -0,0 +1,81 @@ +package nextstep.security.oauth2.client.authentication; + +import java.util.Set; +import nextstep.security.authentication.Authentication; +import nextstep.security.oauth2.client.registration.ClientRegistration; +import nextstep.security.oauth2.core.OAuth2AccessToken; +import nextstep.security.oauth2.core.OAuth2AuthorizationExchange; +import nextstep.security.oauth2.core.user.OAuth2User; +import org.springframework.util.Assert; + +public class OAuth2LoginAuthenticationToken implements Authentication { + + private final ClientRegistration clientRegistration; + private final OAuth2AuthorizationExchange authorizationExchange; + private OAuth2User principal; + private OAuth2AccessToken accessToken; + + private Set authorities; + + private boolean authenticated; + + public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange) { + Assert.notNull(clientRegistration, "clientRegistration cannot be null"); + Assert.notNull(authorizationExchange, "authorizationExchange cannot be null"); + this.clientRegistration = clientRegistration; + this.authorizationExchange = authorizationExchange; + this.setAuthenticated(false); + } + + public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange, OAuth2User principal, + Set authorities, OAuth2AccessToken accessToken) { + Assert.notNull(clientRegistration, "clientRegistration cannot be null"); + Assert.notNull(authorizationExchange, "authorizationExchange cannot be null"); + Assert.notNull(principal, "principal cannot be null"); + Assert.notNull(accessToken, "accessToken cannot be null"); + this.clientRegistration = clientRegistration; + this.authorizationExchange = authorizationExchange; + this.principal = principal; + this.accessToken = accessToken; + this.setAuthenticated(true); + } + + + @Override + public OAuth2User getPrincipal() { + return this.principal; + } + + @Override + public boolean isAuthenticated() { + return this.authenticated; + } + + public void setAuthenticated(boolean authenticated) { + this.authenticated = authenticated; + } + + @Override + public Object getCredentials() { + return ""; + } + + @Override + public Set getAuthorities() { + return this.authorities; + } + + public ClientRegistration getClientRegistration() { + return this.clientRegistration; + } + + public OAuth2AuthorizationExchange getAuthorizationExchange() { + return this.authorizationExchange; + } + + public OAuth2AccessToken getAccessToken() { + return this.accessToken; + } +} diff --git a/src/main/java/nextstep/security/oauth2/client/web/AuthenticationSuccessHandler.java b/src/main/java/nextstep/security/oauth2/client/web/AuthenticationSuccessHandler.java new file mode 100644 index 0000000..f7ba492 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/web/AuthenticationSuccessHandler.java @@ -0,0 +1,12 @@ +package nextstep.security.oauth2.client.web; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import nextstep.security.authentication.Authentication; + +public interface AuthenticationSuccessHandler { + void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException; +} diff --git a/src/main/java/nextstep/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java b/src/main/java/nextstep/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java new file mode 100644 index 0000000..fc6bc68 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java @@ -0,0 +1,66 @@ +package nextstep.security.oauth2.client.web; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import java.util.HashMap; +import java.util.Map; +import nextstep.security.authentication.Authentication; +import nextstep.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.util.Assert; + +public class HttpSessionOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository { + + private static final String DEFAULT_AUTHORIZED_CLIENTS_ATTR_NAME = HttpSessionOAuth2AuthorizedClientRepository.class + .getName() + ".AUTHORIZED_CLIENTS"; + + private final String sessionAttributeName = DEFAULT_AUTHORIZED_CLIENTS_ATTR_NAME; + + @SuppressWarnings("unchecked") + @Override + public OAuth2AuthorizedClient loadAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request) { + Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); + Assert.notNull(request, "request cannot be null"); + return this.getAuthorizedClients(request).get(clientRegistrationId); + } + + @Override + public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, + HttpServletRequest request, HttpServletResponse response) { + Assert.notNull(authorizedClient, "authorizedClient cannot be null"); + Assert.notNull(request, "request cannot be null"); + Assert.notNull(response, "response cannot be null"); + Map authorizedClients = this.getAuthorizedClients(request); + authorizedClients.put(authorizedClient.clientRegistration().registrationId(), authorizedClient); + request.getSession().setAttribute(this.sessionAttributeName, authorizedClients); + } + + @Override + public void removeAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request, HttpServletResponse response) { + Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); + Assert.notNull(request, "request cannot be null"); + Map authorizedClients = this.getAuthorizedClients(request); + if (!authorizedClients.isEmpty()) { + if (authorizedClients.remove(clientRegistrationId) != null) { + if (!authorizedClients.isEmpty()) { + request.getSession().setAttribute(this.sessionAttributeName, authorizedClients); + } else { + request.getSession().removeAttribute(this.sessionAttributeName); + } + } + } + } + + @SuppressWarnings("unchecked") + private Map getAuthorizedClients(HttpServletRequest request) { + HttpSession session = request.getSession(false); + Map authorizedClients = (session != null) + ? (Map) session.getAttribute(this.sessionAttributeName) : null; + if (authorizedClients == null) { + authorizedClients = new HashMap<>(); + } + return authorizedClients; + } +} diff --git a/src/main/java/nextstep/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java b/src/main/java/nextstep/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java new file mode 100644 index 0000000..778018f --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java @@ -0,0 +1,44 @@ +package nextstep.security.oauth2.client.web; + +import java.util.Map; +import nextstep.security.oauth2.core.OAuth2AuthorizationResponse; +import nextstep.security.oauth2.core.OAuth2ParameterNames; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +public class OAuth2AuthorizationResponseUtils { + private OAuth2AuthorizationResponseUtils() { + } + + static MultiValueMap toMultiMap(Map map) { + MultiValueMap params = new LinkedMultiValueMap<>(map.size()); + map.forEach((key, values) -> { + for (String value : values) { + params.add(key, value); + } + }); + return params; + } + + static boolean isAuthorizationResponse(MultiValueMap request) { + return isAuthorizationResponseSuccess(request) || isAuthorizationResponseError(request); + } + + static boolean isAuthorizationResponseSuccess(MultiValueMap request) { + return StringUtils.hasText(request.getFirst(OAuth2ParameterNames.CODE)); + } + + static boolean isAuthorizationResponseError(MultiValueMap request) { + return StringUtils.hasText(request.getFirst(OAuth2ParameterNames.ERROR)); + } + + public static OAuth2AuthorizationResponse convert(MultiValueMap request, String redirectUri) { + String code = request.getFirst(OAuth2ParameterNames.CODE); + String errorCode = request.getFirst(OAuth2ParameterNames.ERROR); + if (StringUtils.hasText(code)) { + return OAuth2AuthorizationResponse.success(code, redirectUri); + } + return OAuth2AuthorizationResponse.error(errorCode, redirectUri); + } +} diff --git a/src/main/java/nextstep/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java b/src/main/java/nextstep/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java new file mode 100644 index 0000000..6be0bfa --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java @@ -0,0 +1,17 @@ +package nextstep.security.oauth2.client.web; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.authentication.Authentication; +import nextstep.security.oauth2.client.OAuth2AuthorizedClient; + +public interface OAuth2AuthorizedClientRepository { + OAuth2AuthorizedClient loadAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request); + + void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, + HttpServletRequest request, HttpServletResponse response); + + void removeAuthorizedClient(String clientRegistrationId, Authentication principal, HttpServletRequest request, + HttpServletResponse response); +} diff --git a/src/main/java/nextstep/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java b/src/main/java/nextstep/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java new file mode 100644 index 0000000..c06ead8 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java @@ -0,0 +1,177 @@ +package nextstep.security.oauth2.client.web; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import nextstep.security.access.RequestMatcher; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.context.HttpSessionSecurityContextRepository; +import nextstep.security.context.SecurityContext; +import nextstep.security.context.SecurityContextHolder; +import nextstep.security.oauth2.client.AuthorizationRequestRepository; +import nextstep.security.oauth2.client.HttpSessionOAuth2AuthorizationRequestRepository; +import nextstep.security.oauth2.client.OAuth2AuthorizedClient; +import nextstep.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import nextstep.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; +import nextstep.security.oauth2.client.registration.ClientRegistration; +import nextstep.security.oauth2.client.registration.ClientRegistrationRepository; +import nextstep.security.oauth2.core.OAuth2AuthenticationException; +import nextstep.security.oauth2.core.OAuth2AuthorizationExchange; +import nextstep.security.oauth2.core.OAuth2AuthorizationRequest; +import nextstep.security.oauth2.core.OAuth2AuthorizationResponse; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.log.LogMessage; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; +import org.springframework.web.filter.GenericFilterBean; + +public class OAuth2LoginAuthenticationFilter extends GenericFilterBean { + public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/oauth2/code/"; + + private final AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); + + private final Converter authenticationResultConverter = this::createAuthenticationResult; + + private final HttpSessionSecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository(); + + private final ClientRegistrationRepository clientRegistrationRepository; + + private final RequestMatcher requiresAuthenticationRequestMatcher; + + private final AuthenticationManager authenticationManager; + + private final OAuth2AuthorizedClientRepository authorizedClientRepository; + + private static final AuthenticationSuccessHandler successHandler = (request, response, authentication) -> response.sendRedirect( + "/"); + + public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository, + AuthenticationManager authenticationManager) { + this.clientRegistrationRepository = clientRegistrationRepository; + this.requiresAuthenticationRequestMatcher = request -> { + String uri = request.getRequestURI(); + return uri.startsWith(DEFAULT_FILTER_PROCESSES_URI); + }; + this.authorizedClientRepository = authorizedClientRepository; + this.authenticationManager = authenticationManager; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); + } + + private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (!requiresAuthentication(request)) { + chain.doFilter(request, response); + return; + } + try { + Authentication authenticationResult = attemptAuthentication(request, response); + if (authenticationResult == null) { + return; + } + successfulAuthentication(request, response, authenticationResult); + } catch (AuthenticationException ex) { + SecurityContextHolder.clearContext(); + } + } + + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException, IOException, ServletException { + // request에서 parameter를 가져오기 + MultiValueMap params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap()); + if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) { + throw new OAuth2AuthenticationException(); + } + + // session에서 authorizationRequest를 가져오기 + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest( + request, response); + if (authorizationRequest == null) { + throw new OAuth2AuthenticationException(); + } + + // registrationId를 가져오고 clientRegistration을 가져오기 + String registrationId = getRegistrationId(request); + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); + if (clientRegistration == null) { + throw new OAuth2AuthenticationException(); + } + + // code를 포함한 authorization response를 객체로 가져오기 + OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, + clientRegistration.redirectUri()); + + // access token을 가져오기 위한 request 객체 만들기 + OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration, + new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse)); + + // OAuth2LoginAuthenticationToken 만들기 + OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) getAuthenticationManager() + .authenticate(authenticationRequest); + + // provider 인증 후 authenticated된 OAuth2AuthenticationToken 객체 가져오기 + OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter.convert( + authenticationResult); + Assert.notNull(oauth2Authentication, "authentication result cannot be null"); + + // authorizedClientRepository 에 저장할 OAuth2AuthorizedClient을 만들고 저장 + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + authenticationResult.getClientRegistration(), oauth2Authentication.getName(), + authenticationResult.getAccessToken()); + + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response); + + return oauth2Authentication; + } + + private boolean requiresAuthentication(HttpServletRequest request) { + if (this.requiresAuthenticationRequestMatcher.matches(request)) { + return true; + } + if (this.logger.isTraceEnabled()) { + this.logger + .trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher)); + } + return false; + } + + private AuthenticationManager getAuthenticationManager() { + return this.authenticationManager; + } + + private String getRegistrationId(HttpServletRequest request) { + String uri = request.getRequestURI(); + + if (uri.startsWith(DEFAULT_FILTER_PROCESSES_URI)) { + return uri.substring(DEFAULT_FILTER_PROCESSES_URI.length()); + } + + return null; + } + + private OAuth2AuthenticationToken createAuthenticationResult(OAuth2LoginAuthenticationToken authenticationResult) { + return new OAuth2AuthenticationToken(authenticationResult.getPrincipal(), + authenticationResult.getAuthorities()); + } + + private void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, + Authentication authResult) throws ServletException, IOException { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authResult); + SecurityContextHolder.setContext(context); + securityContextRepository.saveContext(context, request, response); + + successHandler.onAuthenticationSuccess(request, response, authResult); + } +} diff --git a/src/main/java/nextstep/security/oauth2/core/OAuth2AuthorizationExchange.java b/src/main/java/nextstep/security/oauth2/core/OAuth2AuthorizationExchange.java new file mode 100644 index 0000000..3312593 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/core/OAuth2AuthorizationExchange.java @@ -0,0 +1,13 @@ +package nextstep.security.oauth2.core; + +import org.springframework.util.Assert; + +public record OAuth2AuthorizationExchange( + OAuth2AuthorizationRequest authorizationRequest, + OAuth2AuthorizationResponse authorizationResponse) { + + public OAuth2AuthorizationExchange { + Assert.notNull(authorizationRequest, "authorizationRequest cannot be null"); + Assert.notNull(authorizationResponse, "authorizationResponse cannot be null"); + } +} diff --git a/src/main/java/nextstep/security/oauth2/core/OAuth2AuthorizationResponse.java b/src/main/java/nextstep/security/oauth2/core/OAuth2AuthorizationResponse.java new file mode 100644 index 0000000..ca5ab7c --- /dev/null +++ b/src/main/java/nextstep/security/oauth2/core/OAuth2AuthorizationResponse.java @@ -0,0 +1,17 @@ +package nextstep.security.oauth2.core; + +import org.springframework.util.Assert; + +public record OAuth2AuthorizationResponse( + String code, + String redirectUri +) { + public static OAuth2AuthorizationResponse success(String code, String redirectUri) { + return new OAuth2AuthorizationResponse(code, redirectUri); + } + + public static OAuth2AuthorizationResponse error(String errorCode, String redirectUri) { + Assert.hasText(errorCode, "errorCode cannot be empty"); + return new OAuth2AuthorizationResponse(errorCode, redirectUri); + } +} From 13c80cb341d20b399af6dad689bcd535ca14781a Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Mon, 24 Feb 2025 04:14:54 +0900 Subject: [PATCH 06/18] =?UTF-8?q?feat:=20OAuth2ParameterNames=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/security/oauth2/core/OAuth2ParameterNames.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/nextstep/security/oauth2/core/OAuth2ParameterNames.java b/src/main/java/nextstep/security/oauth2/core/OAuth2ParameterNames.java index 8c613aa..071d028 100644 --- a/src/main/java/nextstep/security/oauth2/core/OAuth2ParameterNames.java +++ b/src/main/java/nextstep/security/oauth2/core/OAuth2ParameterNames.java @@ -6,6 +6,9 @@ public class OAuth2ParameterNames { public static final String RESPONSE_TYPE = "response_type"; public static final String SCOPE = "scope"; public static final String CODE = "code"; + public static final String ERROR = "error"; + public static final String ERROR_DESCRIPTION = "error_description"; + public static final String ERROR_URI = "error_uri"; public static final String GRANT_TYPE = "grant_type"; public static final String AUTHORIZATION_CODE = "authorization_code"; public static final String ACCESS_TOKEN = "access_token"; From eeed97c8160f0a299bc46a20e3321f83f3cc2e94 Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Mon, 24 Feb 2025 04:15:20 +0900 Subject: [PATCH 07/18] =?UTF-8?q?refactor:=20OAuth2LoginAuthenticationFilt?= =?UTF-8?q?er=20=EB=B9=88=EC=9C=BC=EB=A1=9C=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/SecurityConfig.java | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 220f56f..6143e65 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -1,6 +1,9 @@ package nextstep.app; +import java.util.ArrayList; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; @@ -11,25 +14,36 @@ import nextstep.security.access.hierarchicalroles.RoleHierarchy; import nextstep.security.access.hierarchicalroles.RoleHierarchyImpl; import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authentication.AuthenticationManager; import nextstep.security.authentication.BasicAuthenticationFilter; +import nextstep.security.authentication.DaoAuthenticationProvider; import nextstep.security.authentication.OAuth2AccessTokenClient; -import nextstep.security.authentication.OAuth2AuthenticationFilter; import nextstep.security.authentication.OAuth2ClientProperties; import nextstep.security.authentication.OAuth2ClientProperties.Provider; import nextstep.security.authentication.OAuth2ClientProperties.Registration; -import nextstep.security.authentication.OAuth2LoginRedirectFilter; import nextstep.security.authentication.OAuth2UserInfoClient; +import nextstep.security.authentication.ProviderManager; import nextstep.security.authentication.UsernamePasswordAuthenticationFilter; -import nextstep.security.authorization.*; +import nextstep.security.authorization.AuthorityAuthorizationManager; +import nextstep.security.authorization.AuthorizationFilter; +import nextstep.security.authorization.AuthorizationManager; +import nextstep.security.authorization.PermitAllAuthorizationManager; +import nextstep.security.authorization.RequestAuthorizationManager; +import nextstep.security.authorization.SecuredMethodInterceptor; import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.DelegatingFilterProxy; import nextstep.security.config.FilterChainProxy; import nextstep.security.config.SecurityFilterChain; import nextstep.security.context.SecurityContextHolderFilter; import nextstep.security.oauth2.client.OAuth2AuthorizationRequestRedirectFilter; +import nextstep.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider; import nextstep.security.oauth2.client.registration.ClientRegistration; import nextstep.security.oauth2.client.registration.ClientRegistrationRepository; import nextstep.security.oauth2.client.registration.ClientRegistrationRepository.InMemoryClientRegistrationRepository; +import nextstep.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import nextstep.security.oauth2.client.userinfo.OAuth2UserService; +import nextstep.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository; +import nextstep.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import nextstep.security.userdetails.UserDetails; import nextstep.security.userdetails.UserDetailsService; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -38,21 +52,19 @@ import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.http.HttpMethod; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - @Configuration @EnableAspectJAutoProxy @EnableConfigurationProperties(OAuth2ClientProperties.class) public class SecurityConfig { private final MemberRepository memberRepository; + private final OAuth2UserService oAuth2UserService; private final OAuth2ClientProperties oAuth2ClientProperties; public SecurityConfig(MemberRepository memberRepository, OAuth2ClientProperties oAuth2ClientProperties) { this.memberRepository = memberRepository; + this.oAuth2UserService = new DefaultOAuth2UserService(memberRepository); this.oAuth2ClientProperties = oAuth2ClientProperties; } @@ -92,6 +104,13 @@ public SecuredMethodInterceptor securedMethodInterceptor() { return new SecuredMethodInterceptor(); } + @Bean + public AuthenticationManager authenticationManager() { + return new ProviderManager(List.of( + new DaoAuthenticationProvider(userDetailsService()), + new OAuth2LoginAuthenticationProvider(oAuth2UserService))); + } + @Bean public SecurityFilterChain securityFilterChain() { return new DefaultSecurityFilterChain( @@ -100,6 +119,8 @@ public SecurityFilterChain securityFilterChain() { new UsernamePasswordAuthenticationFilter(userDetailsService()), new BasicAuthenticationFilter(userDetailsService()), new OAuth2AuthorizationRequestRedirectFilter(clientRegistrationRepository()), + new OAuth2LoginAuthenticationFilter(clientRegistrationRepository(), + new HttpSessionOAuth2AuthorizedClientRepository(), authenticationManager()), new AuthorizationFilter(requestAuthorizationManager()) ) ); From 706e8c979139d405017aa4e017517acaef618527 Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Mon, 24 Feb 2025 20:29:10 +0900 Subject: [PATCH 08/18] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/GithubAuthenticationFilterTest.java | 35 ++++++++++--------- .../app/GoogleAuthenticationFilterTest.java | 34 ++++++++++-------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/src/test/java/nextstep/app/GithubAuthenticationFilterTest.java b/src/test/java/nextstep/app/GithubAuthenticationFilterTest.java index 1746661..cdf09c3 100644 --- a/src/test/java/nextstep/app/GithubAuthenticationFilterTest.java +++ b/src/test/java/nextstep/app/GithubAuthenticationFilterTest.java @@ -2,9 +2,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpSession; -import nextstep.security.context.HttpSessionSecurityContextRepository; -import nextstep.security.context.SecurityContext; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -12,6 +11,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpSession; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -23,15 +23,18 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static org.assertj.core.api.Assertions.assertThat; +@ActiveProfiles("test") @SpringBootTest @AutoConfigureMockMvc -@ActiveProfiles("test") @AutoConfigureWireMock(port = 8089) class GithubAuthenticationFilterTest { @Autowired private MockMvc mockMvc; + @Autowired + private MemberRepository memberRepository; + @BeforeEach void setupMockServer() throws Exception { stubForAccessToken(); @@ -40,20 +43,21 @@ void setupMockServer() throws Exception { @Test void redirectAndRequestGithubAccessToken() throws Exception { + MockHttpSession session = new MockHttpSession(); + + mockMvc.perform(MockMvcRequestBuilders.get("/oauth2/authorization/github").session(session)) + .andExpect(MockMvcResultMatchers.status().is3xxRedirection()); + String requestUri = "/login/oauth2/code/github?code=mock_code"; - mockMvc.perform(MockMvcRequestBuilders.get(requestUri)) + mockMvc.perform(MockMvcRequestBuilders.get(requestUri).session(session)) .andExpect(MockMvcResultMatchers.status().is3xxRedirection()) - .andExpect(MockMvcResultMatchers.redirectedUrl("/")) - .andExpect(request -> { - HttpSession session = request.getRequest().getSession(); - assert session != null; - SecurityContext context = (SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); - assertThat(context).isNotNull(); - assertThat(context.getAuthentication()).isNotNull(); - assertThat(context.getAuthentication().isAuthenticated()).isTrue(); - assertThat(context.getAuthentication().getPrincipal()).isEqualTo("a@a.com"); - }); + .andExpect(MockMvcResultMatchers.redirectedUrl("/")); + + Member savedMember = memberRepository.findByEmail("a@a.com").get(); + assertThat(savedMember).isNotNull(); + assertThat(savedMember.getEmail()).isEqualTo("a@a.com"); + assertThat(savedMember.getName()).isEqualTo("a"); } private static void stubForAccessToken() throws JsonProcessingException { @@ -81,4 +85,3 @@ private static void stubForUser() throws JsonProcessingException { .withBody(profileJsonResponse))); } } - diff --git a/src/test/java/nextstep/app/GoogleAuthenticationFilterTest.java b/src/test/java/nextstep/app/GoogleAuthenticationFilterTest.java index b559e57..b76568b 100644 --- a/src/test/java/nextstep/app/GoogleAuthenticationFilterTest.java +++ b/src/test/java/nextstep/app/GoogleAuthenticationFilterTest.java @@ -6,14 +6,14 @@ import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpSession; import java.util.HashMap; import java.util.Map; -import nextstep.security.context.HttpSessionSecurityContextRepository; -import nextstep.security.context.SecurityContext; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -21,6 +21,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpSession; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -35,6 +36,9 @@ class GoogleAuthenticationFilterTest { @Autowired private MockMvc mockMvc; + @Autowired + private MemberRepository memberRepository; + @BeforeEach void setupMockServer() throws Exception { stubForAccessToken(); @@ -43,20 +47,22 @@ void setupMockServer() throws Exception { @Test void redirectAndRequestGoogleAccessToken() throws Exception { + MockHttpSession session = new MockHttpSession(); + + mockMvc.perform(MockMvcRequestBuilders.get("/oauth2/authorization/google").session(session)) + .andDo(print()) + .andExpect(MockMvcResultMatchers.status().is3xxRedirection()); + String requestUri = "/login/oauth2/code/google?code=mock_code"; - mockMvc.perform(MockMvcRequestBuilders.get(requestUri)) + mockMvc.perform(MockMvcRequestBuilders.get(requestUri).session(session)) .andExpect(MockMvcResultMatchers.status().is3xxRedirection()) - .andExpect(MockMvcResultMatchers.redirectedUrl("/")) - .andExpect(request -> { - HttpSession session = request.getRequest().getSession(); - assert session != null; - SecurityContext context = (SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); - assertThat(context).isNotNull(); - assertThat(context.getAuthentication()).isNotNull(); - assertThat(context.getAuthentication().isAuthenticated()).isTrue(); - assertThat(context.getAuthentication().getPrincipal()).isEqualTo("a@a.com"); - }); + .andExpect(MockMvcResultMatchers.redirectedUrl("/")); + + Member savedMember = memberRepository.findByEmail("a@a.com").get(); + assertThat(savedMember).isNotNull(); + assertThat(savedMember.getEmail()).isEqualTo("a@a.com"); + assertThat(savedMember.getName()).isEqualTo("a"); } private static void stubForAccessToken() throws JsonProcessingException { From 9ff198dae64e939aba5f6e2509b6701408498075 Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Mon, 24 Feb 2025 21:06:15 +0900 Subject: [PATCH 09/18] =?UTF-8?q?remove:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/SecurityConfig.java | 12 --- .../authentication/GitHubUserInfo.java | 36 ------- .../authentication/GoogleUserInfo.java | 35 ------- .../OAuth2AccessTokenClient.java | 40 -------- .../OAuth2AuthenticationFilter.java | 95 ------------------- .../OAuth2LoginRedirectFilter.java | 71 -------------- .../authentication/OAuth2UserInfo.java | 11 --- .../authentication/OAuth2UserInfoClient.java | 27 ------ .../authentication/OAuth2UserInfoFactory.java | 24 ----- 9 files changed, 351 deletions(-) delete mode 100644 src/main/java/nextstep/security/authentication/GitHubUserInfo.java delete mode 100644 src/main/java/nextstep/security/authentication/GoogleUserInfo.java delete mode 100644 src/main/java/nextstep/security/authentication/OAuth2AccessTokenClient.java delete mode 100644 src/main/java/nextstep/security/authentication/OAuth2AuthenticationFilter.java delete mode 100644 src/main/java/nextstep/security/authentication/OAuth2LoginRedirectFilter.java delete mode 100644 src/main/java/nextstep/security/authentication/OAuth2UserInfo.java delete mode 100644 src/main/java/nextstep/security/authentication/OAuth2UserInfoClient.java delete mode 100644 src/main/java/nextstep/security/authentication/OAuth2UserInfoFactory.java diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 6143e65..8f8484e 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -17,11 +17,9 @@ import nextstep.security.authentication.AuthenticationManager; import nextstep.security.authentication.BasicAuthenticationFilter; import nextstep.security.authentication.DaoAuthenticationProvider; -import nextstep.security.authentication.OAuth2AccessTokenClient; import nextstep.security.authentication.OAuth2ClientProperties; import nextstep.security.authentication.OAuth2ClientProperties.Provider; import nextstep.security.authentication.OAuth2ClientProperties.Registration; -import nextstep.security.authentication.OAuth2UserInfoClient; import nextstep.security.authentication.ProviderManager; import nextstep.security.authentication.UsernamePasswordAuthenticationFilter; import nextstep.security.authorization.AuthorityAuthorizationManager; @@ -68,16 +66,6 @@ public SecurityConfig(MemberRepository memberRepository, this.oAuth2ClientProperties = oAuth2ClientProperties; } - @Bean - public OAuth2AccessTokenClient oAuth2AccessTokenClient() { - return new OAuth2AccessTokenClient(); - } - - @Bean - public OAuth2UserInfoClient oAuth2UserInfoClient() { - return new OAuth2UserInfoClient(); - } - @Bean public MemberRepository memberRepository() { return new InmemoryMemberRepository(); diff --git a/src/main/java/nextstep/security/authentication/GitHubUserInfo.java b/src/main/java/nextstep/security/authentication/GitHubUserInfo.java deleted file mode 100644 index 7f00cc7..0000000 --- a/src/main/java/nextstep/security/authentication/GitHubUserInfo.java +++ /dev/null @@ -1,36 +0,0 @@ -package nextstep.security.authentication; - -public class GitHubUserInfo implements OAuth2UserInfo { - private final String id; - private final String name; - private final String email; - private final String avatar_url; - - public GitHubUserInfo(String id, String name, String email, String avatarUrl) { - this.id = id; - this.name = name; - this.email = email; - this.avatar_url = avatarUrl; - } - - // Getter, Setter - @Override - public String getId() { - return id; - } - - @Override - public String getName() { - return name; - } - - @Override - public String getEmail() { - return email; - } - - @Override - public String getPictureUrl() { - return avatar_url; - } -} diff --git a/src/main/java/nextstep/security/authentication/GoogleUserInfo.java b/src/main/java/nextstep/security/authentication/GoogleUserInfo.java deleted file mode 100644 index 8fd95dc..0000000 --- a/src/main/java/nextstep/security/authentication/GoogleUserInfo.java +++ /dev/null @@ -1,35 +0,0 @@ -package nextstep.security.authentication; - -public class GoogleUserInfo implements OAuth2UserInfo { - private final String id; - private final String email; - private final String name; - private final String picture; - - public GoogleUserInfo(String id, String name, String email, String picture) { - this.id = id; - this.email = email; - this.name = name; - this.picture = picture; - } - - @Override - public String getId() { - return id; - } - - @Override - public String getName() { - return name; - } - - @Override - public String getEmail() { - return email; - } - - @Override - public String getPictureUrl() { - return picture; - } -} diff --git a/src/main/java/nextstep/security/authentication/OAuth2AccessTokenClient.java b/src/main/java/nextstep/security/authentication/OAuth2AccessTokenClient.java deleted file mode 100644 index ddf6bf2..0000000 --- a/src/main/java/nextstep/security/authentication/OAuth2AccessTokenClient.java +++ /dev/null @@ -1,40 +0,0 @@ -package nextstep.security.authentication; - -import static nextstep.security.authentication.OAuth2ClientProperties.*; - -import java.util.HashMap; -import java.util.Map; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.client.RestTemplate; - -public class OAuth2AccessTokenClient { - - private final RestTemplate restTemplate = new RestTemplate(); - - public String getAccessToken(String code, Registration registration, Provider provider) { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - Map params = new HashMap<>(); - params.put("code", code); - params.put("client_id", registration.clientId()); - params.put("client_secret", registration.clientSecret()); - params.put("redirect_uri", registration.redirectUri()); - params.put("grant_type", registration.authorizationGrantType()); - - HttpEntity> request = new HttpEntity<>(params, headers); - - ResponseEntity response = restTemplate.exchange( - provider.accessTokenUri(), - HttpMethod.POST, - request, - Map.class - ); - - return response.getBody().get("access_token").toString(); - } -} diff --git a/src/main/java/nextstep/security/authentication/OAuth2AuthenticationFilter.java b/src/main/java/nextstep/security/authentication/OAuth2AuthenticationFilter.java deleted file mode 100644 index 4e503d2..0000000 --- a/src/main/java/nextstep/security/authentication/OAuth2AuthenticationFilter.java +++ /dev/null @@ -1,95 +0,0 @@ -package nextstep.security.authentication; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Set; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; -import nextstep.security.authentication.OAuth2ClientProperties.Provider; -import nextstep.security.authentication.OAuth2ClientProperties.Registration; -import nextstep.security.context.HttpSessionSecurityContextRepository; -import nextstep.security.context.SecurityContextHolder; -import org.springframework.http.HttpMethod; -import org.springframework.web.filter.GenericFilterBean; - -public class OAuth2AuthenticationFilter extends GenericFilterBean { - - private static final String REQUEST_URI = "/login/oauth2/code"; - - private final OAuth2ClientProperties oAuth2ClientProperties; - private final OAuth2AccessTokenClient oAuth2AccessTokenClient; - private final MemberRepository memberRepository; - private final OAuth2UserInfoClient oAuth2UserInfoClient; - - public OAuth2AuthenticationFilter(OAuth2ClientProperties oAuth2ClientProperties, - MemberRepository memberRepository, - OAuth2AccessTokenClient oAuth2AccessTokenClient, - OAuth2UserInfoClient oAuth2UserInfoClient) { - this.oAuth2AccessTokenClient = oAuth2AccessTokenClient; - this.oAuth2UserInfoClient = oAuth2UserInfoClient; - this.oAuth2ClientProperties = oAuth2ClientProperties; - this.memberRepository = memberRepository; - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, - FilterChain filterChain) throws IOException, ServletException { - HttpServletRequest httpRequest = (HttpServletRequest) request; - HttpServletResponse httpResponse = (HttpServletResponse) response; - - String requestURI = httpRequest.getRequestURI(); - String method = httpRequest.getMethod(); - - String registrationSource = null; - if (requestURI.startsWith(REQUEST_URI)) { - registrationSource = getRegistrationId(requestURI); - } - - if (registrationSource != null && method.equals(HttpMethod.GET.name())) { - Registration registration = oAuth2ClientProperties.getRegistrations().get(registrationSource); - Provider provider = oAuth2ClientProperties.getProviders().get(registrationSource); - - String code = httpRequest.getParameter("code"); - - String accessToken = oAuth2AccessTokenClient.getAccessToken(code, registration, provider); - - OAuth2UserInfo userInfo = oAuth2UserInfoClient.getUserInfo(accessToken, provider); - - String email = userInfo.getEmail(); - Member member = memberRepository.findByEmail(email) - .orElse(new Member( - email, "", userInfo.getName(), userInfo.getPictureUrl(), Set.of("USER") - )); - - UsernamePasswordAuthenticationToken authentication = - UsernamePasswordAuthenticationToken.authenticated( - member.getEmail(), null, member.getRoles()); - - SecurityContextHolder.getContext().setAuthentication(authentication); - - httpRequest.getSession(true).setAttribute( - HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, - SecurityContextHolder.getContext() - ); - - httpResponse.sendRedirect("/"); - return; - } - - filterChain.doFilter(request, response); - } - - private String getRegistrationId(String requestURI) { - for (String registrationId : oAuth2ClientProperties.getRegistrations().keySet()) { - if (requestURI.contains(registrationId)) { - return registrationId; - } - } - return null; - } -} diff --git a/src/main/java/nextstep/security/authentication/OAuth2LoginRedirectFilter.java b/src/main/java/nextstep/security/authentication/OAuth2LoginRedirectFilter.java deleted file mode 100644 index 1369a2c..0000000 --- a/src/main/java/nextstep/security/authentication/OAuth2LoginRedirectFilter.java +++ /dev/null @@ -1,71 +0,0 @@ -package nextstep.security.authentication; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Map; -import nextstep.security.authentication.OAuth2ClientProperties.Provider; -import nextstep.security.authentication.OAuth2ClientProperties.Registration; -import org.springframework.http.HttpMethod; -import org.springframework.web.filter.GenericFilterBean; -import org.springframework.web.util.UriComponentsBuilder; - -public class OAuth2LoginRedirectFilter extends GenericFilterBean { - - private static final String REQUEST_URI = "/oauth2/authorization"; - - private final Map registrations; - private final Map providers; - - public OAuth2LoginRedirectFilter(OAuth2ClientProperties properties) { - this.registrations = properties.getRegistrations(); - this.providers = properties.getProviders(); - } - - @Override - public void doFilter(ServletRequest request, - ServletResponse response, - FilterChain chain) throws IOException, ServletException { - HttpServletRequest httpServletRequest = (HttpServletRequest) request; - HttpServletResponse httpServletResponse = (HttpServletResponse) response; - - String requestURI = httpServletRequest.getRequestURI(); - String method = httpServletRequest.getMethod(); - - String registrationSource = null; - if (requestURI.startsWith(REQUEST_URI)) { - registrationSource = getRegistrationId(requestURI); - } - - if (registrationSource != null && method.equals(HttpMethod.GET.name())) { - Registration registration = registrations.get(registrationSource); - Provider provider = providers.get(registrationSource); - - String redirectUri = UriComponentsBuilder.fromHttpUrl(provider.authorizationUri()) - .queryParam("client_id", registration.clientId()) - .queryParam("scope", String.join(" ", registration.scope())) - .queryParam("response_type", "code") - .queryParam("redirect_uri", registration.redirectUri()) - .build() - .toUriString(); - - httpServletResponse.sendRedirect(redirectUri); - return; - } - - chain.doFilter(request, response); - } - - private String getRegistrationId(String requestURI) { - for (String registrationId : registrations.keySet()) { - if (requestURI.contains(registrationId)) { - return registrationId; - } - } - return null; - } -} diff --git a/src/main/java/nextstep/security/authentication/OAuth2UserInfo.java b/src/main/java/nextstep/security/authentication/OAuth2UserInfo.java deleted file mode 100644 index d5648f7..0000000 --- a/src/main/java/nextstep/security/authentication/OAuth2UserInfo.java +++ /dev/null @@ -1,11 +0,0 @@ -package nextstep.security.authentication; - -public interface OAuth2UserInfo { - String getId(); - - String getName(); - - String getEmail(); - - String getPictureUrl(); -} diff --git a/src/main/java/nextstep/security/authentication/OAuth2UserInfoClient.java b/src/main/java/nextstep/security/authentication/OAuth2UserInfoClient.java deleted file mode 100644 index 1f21e85..0000000 --- a/src/main/java/nextstep/security/authentication/OAuth2UserInfoClient.java +++ /dev/null @@ -1,27 +0,0 @@ -package nextstep.security.authentication; - -import java.util.Map; -import nextstep.security.authentication.OAuth2ClientProperties.Provider; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.web.client.RestTemplate; - -public class OAuth2UserInfoClient { - - private final RestTemplate restTemplate = new RestTemplate(); - - public OAuth2UserInfo getUserInfo(String accessToken, Provider provider) { - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add("Authorization", "Bearer " + accessToken); - - HttpEntity request = new HttpEntity<>(httpHeaders); - ResponseEntity response = restTemplate.exchange(provider.userInfoUri(), HttpMethod.GET, - request, Map.class); - - Map attributes = response.getBody(); - - return OAuth2UserInfoFactory.getOAuth2UserInfo(provider.name(), attributes); - } -} diff --git a/src/main/java/nextstep/security/authentication/OAuth2UserInfoFactory.java b/src/main/java/nextstep/security/authentication/OAuth2UserInfoFactory.java deleted file mode 100644 index 060be53..0000000 --- a/src/main/java/nextstep/security/authentication/OAuth2UserInfoFactory.java +++ /dev/null @@ -1,24 +0,0 @@ -package nextstep.security.authentication; - -import java.util.Map; - -public class OAuth2UserInfoFactory { - public static OAuth2UserInfo getOAuth2UserInfo(String providerName, Map attributes) { - if ("google".equalsIgnoreCase(providerName)) { - return new GoogleUserInfo( - (String) attributes.get("id"), - (String) attributes.get("name"), - (String) attributes.get("email"), - (String) attributes.get("picture") - ); - } else if ("github".equalsIgnoreCase(providerName)) { - return new GitHubUserInfo( - (String) attributes.get("id"), - (String) attributes.get("name"), - (String) attributes.get("email"), - (String) attributes.get("avatar_url") - ); - } - throw new IllegalArgumentException("Unsupported provider: " + providerName); - } -} From aded1bc27eaad749056b2146a2041e71f9d24e76 Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Tue, 25 Feb 2025 22:41:29 +0900 Subject: [PATCH 10/18] =?UTF-8?q?refactor:=20Resolver=EC=9D=98=20authoriza?= =?UTF-8?q?tionRequestUri=EB=A5=BC=20=ED=95=98=EB=82=98=EB=A1=9C=20?= =?UTF-8?q?=ED=95=A9=EC=B9=98=EA=B3=A0,=20scope=EB=A5=BC=20=EA=B3=B5?= =?UTF-8?q?=EB=B0=B1=EC=9C=BC=EB=A1=9C=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DefaultOAuth2AuthorizationRequestResolver.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/nextstep/security/oauth2/client/DefaultOAuth2AuthorizationRequestResolver.java b/src/main/java/nextstep/security/oauth2/client/DefaultOAuth2AuthorizationRequestResolver.java index daae642..b810a40 100644 --- a/src/main/java/nextstep/security/oauth2/client/DefaultOAuth2AuthorizationRequestResolver.java +++ b/src/main/java/nextstep/security/oauth2/client/DefaultOAuth2AuthorizationRequestResolver.java @@ -31,17 +31,14 @@ public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { throw new AuthenticationException("Invalid Client Registration with Id: " + registrationId); } - String paramsQuery = UriComponentsBuilder.newInstance() + String authorizationRequestUri = UriComponentsBuilder + .fromHttpUrl(clientRegistration.providerDetails().authorizationUri()) .queryParam("client_id", clientRegistration.clientId()) .queryParam("response_type", "code") - .queryParam("scope", clientRegistration.scope()) + .queryParam("scope", String.join(" ", clientRegistration.scope())) .queryParam("redirect_uri", clientRegistration.redirectUri()) .build() - .toUri() - .getQuery(); - - String authorizationRequestUri = - clientRegistration.providerDetails().authorizationUri() + "?" + paramsQuery; + .toUriString(); return new OAuth2AuthorizationRequest( clientRegistration.providerDetails().authorizationUri(), From 5bbe21e12ecb12d652cbf829719c682d803971aa Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Tue, 25 Feb 2025 22:58:54 +0900 Subject: [PATCH 11/18] =?UTF-8?q?refactor:=20=ED=95=84=EB=93=9C=EB=A5=BC?= =?UTF-8?q?=20DI=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/app/SecurityConfig.java | 9 ++++++++- .../client/OAuth2AuthorizationRequestRedirectFilter.java | 9 ++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 8f8484e..858f5ea 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -33,6 +33,8 @@ import nextstep.security.config.FilterChainProxy; import nextstep.security.config.SecurityFilterChain; import nextstep.security.context.SecurityContextHolderFilter; +import nextstep.security.oauth2.client.AuthorizationRequestRepository; +import nextstep.security.oauth2.client.HttpSessionOAuth2AuthorizationRequestRepository; import nextstep.security.oauth2.client.OAuth2AuthorizationRequestRedirectFilter; import nextstep.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider; import nextstep.security.oauth2.client.registration.ClientRegistration; @@ -99,6 +101,11 @@ public AuthenticationManager authenticationManager() { new OAuth2LoginAuthenticationProvider(oAuth2UserService))); } + @Bean + public AuthorizationRequestRepository authorizationRequestRepository() { + return new HttpSessionOAuth2AuthorizationRequestRepository(); + } + @Bean public SecurityFilterChain securityFilterChain() { return new DefaultSecurityFilterChain( @@ -106,7 +113,7 @@ public SecurityFilterChain securityFilterChain() { new SecurityContextHolderFilter(), new UsernamePasswordAuthenticationFilter(userDetailsService()), new BasicAuthenticationFilter(userDetailsService()), - new OAuth2AuthorizationRequestRedirectFilter(clientRegistrationRepository()), + new OAuth2AuthorizationRequestRedirectFilter(clientRegistrationRepository(), authorizationRequestRepository()), new OAuth2LoginAuthenticationFilter(clientRegistrationRepository(), new HttpSessionOAuth2AuthorizedClientRepository(), authenticationManager()), new AuthorizationFilter(requestAuthorizationManager()) diff --git a/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java index e4acc86..a08a07c 100644 --- a/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java +++ b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java @@ -14,14 +14,17 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter { public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization/"; private final OAuth2AuthorizationRequestResolver authorizationRequestResolver; - private final AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); + private final AuthorizationRequestRepository authorizationRequestRepository; - public OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository) { - this(clientRegistrationRepository, DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); + public OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository, + AuthorizationRequestRepository authorizationRequestRepository) { + this(clientRegistrationRepository, authorizationRequestRepository, DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); } public OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository, + AuthorizationRequestRepository authorizationRequestRepository, String authorizationRequestBaseUri) { + this.authorizationRequestRepository = authorizationRequestRepository; this.authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, authorizationRequestBaseUri); } From 21780ba6a001d597bb702f4777476437753373db Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Tue, 25 Feb 2025 22:59:50 +0900 Subject: [PATCH 12/18] =?UTF-8?q?remove:=20=EC=82=AC=EC=9A=A9=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth2/client/DefaultOAuth2AuthorizationRequestResolver.java | 1 - .../oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/nextstep/security/oauth2/client/DefaultOAuth2AuthorizationRequestResolver.java b/src/main/java/nextstep/security/oauth2/client/DefaultOAuth2AuthorizationRequestResolver.java index b810a40..550550a 100644 --- a/src/main/java/nextstep/security/oauth2/client/DefaultOAuth2AuthorizationRequestResolver.java +++ b/src/main/java/nextstep/security/oauth2/client/DefaultOAuth2AuthorizationRequestResolver.java @@ -1,7 +1,6 @@ package nextstep.security.oauth2.client; import jakarta.servlet.http.HttpServletRequest; -import java.util.Base64; import nextstep.security.authentication.AuthenticationException; import nextstep.security.oauth2.client.registration.ClientRegistration; import nextstep.security.oauth2.client.registration.ClientRegistrationRepository; diff --git a/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java index a08a07c..eaab3ab 100644 --- a/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java +++ b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java @@ -8,7 +8,6 @@ import nextstep.security.oauth2.client.registration.ClientRegistrationRepository; import nextstep.security.oauth2.core.OAuth2AuthenticationException; import nextstep.security.oauth2.core.OAuth2AuthorizationRequest; -import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter { From 601f3a6bf1647ba8da4f9941e56fea5e12c93b97 Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Tue, 25 Feb 2025 23:09:08 +0900 Subject: [PATCH 13/18] =?UTF-8?q?refactor:=20catch=20=EB=B8=94=EB=A1=9D?= =?UTF-8?q?=EC=97=90=20=EC=98=88=EC=99=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/OAuth2AuthorizationRequestRedirectFilter.java | 2 +- .../oauth2/core/OAuth2AuthenticationException.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java index eaab3ab..b8c5a4b 100644 --- a/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java +++ b/src/main/java/nextstep/security/oauth2/client/OAuth2AuthorizationRequestRedirectFilter.java @@ -38,7 +38,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; } } catch (Exception e) { - throw new OAuth2AuthenticationException(); + throw new OAuth2AuthenticationException("OAuth2 인증 요청을 처리하는데 실패 했습니다: " + e.getMessage()); } filterChain.doFilter(request, response); } diff --git a/src/main/java/nextstep/security/oauth2/core/OAuth2AuthenticationException.java b/src/main/java/nextstep/security/oauth2/core/OAuth2AuthenticationException.java index 7d76930..aca51d7 100644 --- a/src/main/java/nextstep/security/oauth2/core/OAuth2AuthenticationException.java +++ b/src/main/java/nextstep/security/oauth2/core/OAuth2AuthenticationException.java @@ -3,5 +3,10 @@ import nextstep.security.authentication.AuthenticationException; public class OAuth2AuthenticationException extends AuthenticationException { - + + public OAuth2AuthenticationException() {} + + public OAuth2AuthenticationException(String message) { + super(message); + } } From dc6e13a442d770e78c2e3e88af946a03b2822c51 Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Tue, 25 Feb 2025 23:10:11 +0900 Subject: [PATCH 14/18] =?UTF-8?q?refactor:=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20@Nullable=20=EC=95=A0=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EC=9D=84=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth2/client/authentication/GithubProfileUser.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/GithubProfileUser.java b/src/main/java/nextstep/security/oauth2/client/authentication/GithubProfileUser.java index 3577d02..bb8feb2 100644 --- a/src/main/java/nextstep/security/oauth2/client/authentication/GithubProfileUser.java +++ b/src/main/java/nextstep/security/oauth2/client/authentication/GithubProfileUser.java @@ -1,16 +1,18 @@ package nextstep.security.oauth2.client.authentication; +import jakarta.annotation.Nullable; import java.util.Map; public class GithubProfileUser implements OAuth2ProfileUser { private final String name; private final String imageUrl; + @Nullable private final String email; public GithubProfileUser(Map attributes) { this.name = (String) attributes.get("name"); this.imageUrl = (String) attributes.get("avatar_url"); - this.email = (String) attributes.get("email"); // 이메일이 null일 수도 있음 + this.email = (String) attributes.get("email"); } @Override @@ -24,6 +26,7 @@ public String getImageUrl() { } @Override + @Nullable public String getEmail() { return email; } From 54cc2f57249a108b95c82babeb9418d4ad4fbf4a Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Wed, 26 Feb 2025 00:06:32 +0900 Subject: [PATCH 15/18] =?UTF-8?q?refactor:=20OAuth2LoginAuthenticationToke?= =?UTF-8?q?n=EC=97=90=EC=84=9C=20=EA=B6=8C=ED=95=9C=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OAuth2LoginAuthenticationProvider.java | 2 +- .../authentication/OAuth2LoginAuthenticationToken.java | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java index 7ba36ea..d9021cd 100644 --- a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java +++ b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java @@ -40,7 +40,7 @@ public Authentication authenticate(Authentication authentication) throws Authent return new OAuth2LoginAuthenticationToken( loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(), - oauth2User, oauth2User.getAuthorities(), accessToken); + oauth2User, accessToken); } @Override diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java index d0763fb..ee5f19f 100644 --- a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java +++ b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java @@ -1,5 +1,6 @@ package nextstep.security.oauth2.client.authentication; +import java.util.Collections; import java.util.Set; import nextstep.security.authentication.Authentication; import nextstep.security.oauth2.client.registration.ClientRegistration; @@ -9,14 +10,10 @@ import org.springframework.util.Assert; public class OAuth2LoginAuthenticationToken implements Authentication { - private final ClientRegistration clientRegistration; private final OAuth2AuthorizationExchange authorizationExchange; private OAuth2User principal; private OAuth2AccessToken accessToken; - - private Set authorities; - private boolean authenticated; public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration, @@ -30,7 +27,7 @@ public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration, public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration, OAuth2AuthorizationExchange authorizationExchange, OAuth2User principal, - Set authorities, OAuth2AccessToken accessToken) { + OAuth2AccessToken accessToken) { Assert.notNull(clientRegistration, "clientRegistration cannot be null"); Assert.notNull(authorizationExchange, "authorizationExchange cannot be null"); Assert.notNull(principal, "principal cannot be null"); @@ -64,7 +61,7 @@ public Object getCredentials() { @Override public Set getAuthorities() { - return this.authorities; + return Collections.emptySet(); } public ClientRegistration getClientRegistration() { From 5a9fe0699e312997e5371963d7a9571b0e5f9e1e Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Wed, 26 Feb 2025 00:25:32 +0900 Subject: [PATCH 16/18] =?UTF-8?q?refactor:=20=EC=83=81=EC=88=98=20enum?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/OAuth2AuthorizationResponseUtils.java | 8 ++-- .../oauth2/core/OAuth2ParameterNames.java | 39 +++++++++++++------ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/main/java/nextstep/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java b/src/main/java/nextstep/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java index 778018f..08aec88 100644 --- a/src/main/java/nextstep/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java +++ b/src/main/java/nextstep/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java @@ -26,16 +26,16 @@ static boolean isAuthorizationResponse(MultiValueMap request) { } static boolean isAuthorizationResponseSuccess(MultiValueMap request) { - return StringUtils.hasText(request.getFirst(OAuth2ParameterNames.CODE)); + return StringUtils.hasText(request.getFirst(OAuth2ParameterNames.CODE.getValue())); } static boolean isAuthorizationResponseError(MultiValueMap request) { - return StringUtils.hasText(request.getFirst(OAuth2ParameterNames.ERROR)); + return StringUtils.hasText(request.getFirst(OAuth2ParameterNames.ERROR.getValue())); } public static OAuth2AuthorizationResponse convert(MultiValueMap request, String redirectUri) { - String code = request.getFirst(OAuth2ParameterNames.CODE); - String errorCode = request.getFirst(OAuth2ParameterNames.ERROR); + String code = request.getFirst(OAuth2ParameterNames.CODE.getValue()); + String errorCode = request.getFirst(OAuth2ParameterNames.ERROR.getValue()); if (StringUtils.hasText(code)) { return OAuth2AuthorizationResponse.success(code, redirectUri); } diff --git a/src/main/java/nextstep/security/oauth2/core/OAuth2ParameterNames.java b/src/main/java/nextstep/security/oauth2/core/OAuth2ParameterNames.java index 071d028..9ee314b 100644 --- a/src/main/java/nextstep/security/oauth2/core/OAuth2ParameterNames.java +++ b/src/main/java/nextstep/security/oauth2/core/OAuth2ParameterNames.java @@ -1,15 +1,30 @@ package nextstep.security.oauth2.core; -public class OAuth2ParameterNames { - public static final String CLIENT_ID = "client_id"; - public static final String REDIRECT_URI = "redirect_uri"; - public static final String RESPONSE_TYPE = "response_type"; - public static final String SCOPE = "scope"; - public static final String CODE = "code"; - public static final String ERROR = "error"; - public static final String ERROR_DESCRIPTION = "error_description"; - public static final String ERROR_URI = "error_uri"; - public static final String GRANT_TYPE = "grant_type"; - public static final String AUTHORIZATION_CODE = "authorization_code"; - public static final String ACCESS_TOKEN = "access_token"; +public enum OAuth2ParameterNames { + CLIENT_ID("client_id"), + REDIRECT_URI("redirect_uri"), + RESPONSE_TYPE("response_type"), + SCOPE("scope"), + CODE("code"), + ERROR("error"), + ERROR_DESCRIPTION("error_description"), + ERROR_URI("error_uri"), + GRANT_TYPE("grant_type"), + AUTHORIZATION_CODE("authorization_code"), + ACCESS_TOKEN("access_token"); + + private final String value; + + OAuth2ParameterNames(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } } From a1aaf2a261beec19d7adaaa24d717f4a6d7beb0a Mon Sep 17 00:00:00 2001 From: parkSeryu Date: Wed, 26 Feb 2025 00:27:14 +0900 Subject: [PATCH 17/18] =?UTF-8?q?refactor:=20=EB=86=93=EC=B9=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=84=B4=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OAuth2AuthorizationCodeAuthenticationToken.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java index 9547ba2..71d78c1 100644 --- a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java +++ b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java @@ -47,12 +47,13 @@ public Set getAuthorities() { @Override public Object getCredentials() { - return null; + return (this.accessToken != null) ? this.accessToken.tokenValue() + : this.authorizationExchange.authorizationResponse().code(); } @Override public Object getPrincipal() { - return null; + return this.clientRegistration.clientId(); } @Override From e6ddaeb0ccd151f1d64ed9741b0e42b1d4889841 Mon Sep 17 00:00:00 2001 From: parkseryu Date: Wed, 26 Feb 2025 10:29:11 +0900 Subject: [PATCH 18/18] =?UTF-8?q?refactor:=20OAuth2LoginAuthenticationFilt?= =?UTF-8?q?er=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OAuth2LoginAuthenticationProvider.java | 4 ++ .../web/OAuth2LoginAuthenticationFilter.java | 52 +++++++++---------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java index d9021cd..4c0e18f 100644 --- a/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java +++ b/src/main/java/nextstep/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java @@ -25,6 +25,7 @@ public OAuth2LoginAuthenticationProvider(OAuth2UserService userService) { public Authentication authenticate(Authentication authentication) throws AuthenticationException { OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken) authentication; + // 1. 코드 교환 OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider .authenticate( @@ -34,7 +35,10 @@ public Authentication authenticate(Authentication authentication) throws Authent ) ); + // 2. Access Token 요청 OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken(); + + // 3. User 정보 가져오기 OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest( loginAuthenticationToken.getClientRegistration(), accessToken)); diff --git a/src/main/java/nextstep/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java b/src/main/java/nextstep/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java index c06ead8..7f10d50 100644 --- a/src/main/java/nextstep/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java +++ b/src/main/java/nextstep/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java @@ -27,7 +27,6 @@ import nextstep.security.oauth2.core.OAuth2AuthorizationResponse; import org.springframework.core.convert.converter.Converter; import org.springframework.core.log.LogMessage; -import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.web.filter.GenericFilterBean; @@ -88,51 +87,36 @@ private void doFilter(HttpServletRequest request, HttpServletResponse response, public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { - // request에서 parameter를 가져오기 + // 1. 요청 파라미터에서 필요한 정보를 추출 MultiValueMap params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap()); if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) { - throw new OAuth2AuthenticationException(); + throw new OAuth2AuthenticationException("Invalid OAuth2 Authorization Response"); } - // session에서 authorizationRequest를 가져오기 + // 2. 세션에서 OAuth2AuthorizationRequest 로드 OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest( request, response); if (authorizationRequest == null) { - throw new OAuth2AuthenticationException(); + throw new OAuth2AuthenticationException("Authorization Request not found in session"); } - // registrationId를 가져오고 clientRegistration을 가져오기 + // 3. registrationId와 ClientRegistration 조회 String registrationId = getRegistrationId(request); ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); if (clientRegistration == null) { - throw new OAuth2AuthenticationException(); + throw new OAuth2AuthenticationException("Client Registration not found"); } - // code를 포함한 authorization response를 객체로 가져오기 + // 4. 인증에 필요한 OAuth2AuthorizationResponse 생성 OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, clientRegistration.redirectUri()); - // access token을 가져오기 위한 request 객체 만들기 - OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration, + // 5. Authentication 생성 (실제 인증은 Provider가 수행) + Authentication authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse)); - // OAuth2LoginAuthenticationToken 만들기 - OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) getAuthenticationManager() - .authenticate(authenticationRequest); - - // provider 인증 후 authenticated된 OAuth2AuthenticationToken 객체 가져오기 - OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter.convert( - authenticationResult); - Assert.notNull(oauth2Authentication, "authentication result cannot be null"); - - // authorizedClientRepository 에 저장할 OAuth2AuthorizedClient을 만들고 저장 - OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( - authenticationResult.getClientRegistration(), oauth2Authentication.getName(), - authenticationResult.getAccessToken()); - - this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response); - - return oauth2Authentication; + // 6. AuthenticationManager에 요청 전달 + return getAuthenticationManager().authenticate(authenticationRequest); } private boolean requiresAuthentication(HttpServletRequest request) { @@ -172,6 +156,20 @@ private void successfulAuthentication(HttpServletRequest request, HttpServletRes SecurityContextHolder.setContext(context); securityContextRepository.saveContext(context, request, response); + if (authResult instanceof OAuth2LoginAuthenticationToken authentication) { + OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter.convert( + authentication); + + // authorizedClientRepository 에 저장할 OAuth2AuthorizedClient을 만들고 저장 + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + authentication.getClientRegistration(), + oauth2Authentication.getName(), + authentication.getAccessToken()); + + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, + response); + } + successHandler.onAuthenticationSuccess(request, response, authResult); } }