From d6a43546cbc5b9b20aba4b74a154fa8b7f1c9043 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Wed, 12 Feb 2025 13:49:19 +0900 Subject: [PATCH 01/25] =?UTF-8?q?feat:=20application.properties=20?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=ED=95=B4=20OAuth2Client=20=EB=A5=BC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=ED=95=98=EB=8A=94=20Repository=20=EB=A5=BC?= =?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 --- src/main/java/nextstep/app/OAuth2Config.java | 16 +++++ .../registration/ClientRegistration.java | 23 +++++++ .../registration/ClientRegistrationDao.java | 18 +++++ .../ClientRegistrationRepository.java | 5 ++ .../registration/OAuth2ClientProperties.java | 66 +++++++++++++++++++ .../resources/application-test.properties | 18 +++++ src/main/resources/application.properties | 20 ++++++ 7 files changed, 166 insertions(+) create mode 100644 src/main/java/nextstep/app/OAuth2Config.java create mode 100644 src/main/java/nextstep/oauth2/registration/ClientRegistration.java create mode 100644 src/main/java/nextstep/oauth2/registration/ClientRegistrationDao.java create mode 100644 src/main/java/nextstep/oauth2/registration/ClientRegistrationRepository.java create mode 100644 src/main/java/nextstep/oauth2/registration/OAuth2ClientProperties.java create mode 100644 src/main/resources/application-test.properties diff --git a/src/main/java/nextstep/app/OAuth2Config.java b/src/main/java/nextstep/app/OAuth2Config.java new file mode 100644 index 0000000..fd22d38 --- /dev/null +++ b/src/main/java/nextstep/app/OAuth2Config.java @@ -0,0 +1,16 @@ +package nextstep.app; + +import nextstep.oauth2.registration.ClientRegistrationRepository; +import nextstep.oauth2.registration.OAuth2ClientProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@EnableConfigurationProperties(OAuth2ClientProperties.class) +@Configuration +public class OAuth2Config { + private final ClientRegistrationRepository registrationRepository; + + public OAuth2Config(OAuth2ClientProperties oauth2ClientProperties) { + this.registrationRepository = oauth2ClientProperties.createClientRegistrationDao(); + } +} diff --git a/src/main/java/nextstep/oauth2/registration/ClientRegistration.java b/src/main/java/nextstep/oauth2/registration/ClientRegistration.java new file mode 100644 index 0000000..e5592ab --- /dev/null +++ b/src/main/java/nextstep/oauth2/registration/ClientRegistration.java @@ -0,0 +1,23 @@ +package nextstep.oauth2.registration; + +import java.util.Set; + +public record ClientRegistration( + String registrationId, + String clientId, + String clientSecret, + String redirectUri, + Set scopes, + 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/oauth2/registration/ClientRegistrationDao.java b/src/main/java/nextstep/oauth2/registration/ClientRegistrationDao.java new file mode 100644 index 0000000..44606e8 --- /dev/null +++ b/src/main/java/nextstep/oauth2/registration/ClientRegistrationDao.java @@ -0,0 +1,18 @@ +package nextstep.oauth2.registration; + +import org.springframework.stereotype.Repository; + +import java.util.Map; + +@Repository +public final class ClientRegistrationDao implements ClientRegistrationRepository { + private final Map registrations; + + public ClientRegistrationDao(Map registrations) { + this.registrations = registrations; + } + + public ClientRegistration findByRegistrationId(String registrationId) { + return this.registrations.get(registrationId); + } +} diff --git a/src/main/java/nextstep/oauth2/registration/ClientRegistrationRepository.java b/src/main/java/nextstep/oauth2/registration/ClientRegistrationRepository.java new file mode 100644 index 0000000..f153edc --- /dev/null +++ b/src/main/java/nextstep/oauth2/registration/ClientRegistrationRepository.java @@ -0,0 +1,5 @@ +package nextstep.oauth2.registration; + +public interface ClientRegistrationRepository { + ClientRegistration findByRegistrationId(String registrationId); +} diff --git a/src/main/java/nextstep/oauth2/registration/OAuth2ClientProperties.java b/src/main/java/nextstep/oauth2/registration/OAuth2ClientProperties.java new file mode 100644 index 0000000..29a6f99 --- /dev/null +++ b/src/main/java/nextstep/oauth2/registration/OAuth2ClientProperties.java @@ -0,0 +1,66 @@ +package nextstep.oauth2.registration; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@ConfigurationProperties(prefix = "spring.security.oauth2.client") +public record OAuth2ClientProperties( + Map provider, + Map registration +) { + public ClientRegistrationDao createClientRegistrationDao() { + return new ClientRegistrationDao(getClientRegistration()); + } + + private Map getClientRegistration() { + return this.registration.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, this::getClientRegistration + )); + } + + private ClientRegistration getClientRegistration( + Map.Entry entry + ) { + final String registrationId = entry.getKey(); + final Registration registration = entry.getValue(); + final Provider provider = this.provider.get(registrationId); + return new ClientRegistration( + registrationId, + registration.clientId(), + registration.clientSecret(), + registration.redirectUri(), + registration.scope(), + provider.getProviderDetails() + ); + } + + public record Registration( + String provider, + String clientId, + String clientSecret, + String redirectUri, + Set scope + ) {} + + public record Provider( + String authorizationUri, + String tokenUri, + String userInfoUri, + String userNameAttributeName + ) { + public ClientRegistration.ProviderDetails getProviderDetails() { + return new ClientRegistration.ProviderDetails( + authorizationUri, tokenUri, getUserInfo() + ); + } + + public ClientRegistration.UserInfoEndpoint getUserInfo() { + return new ClientRegistration.UserInfoEndpoint( + userInfoUri, userNameAttributeName + ); + } + } +} diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties new file mode 100644 index 0000000..77bbbc0 --- /dev/null +++ b/src/main/resources/application-test.properties @@ -0,0 +1,18 @@ +spring.security.oauth2.client.registration.github.provider=github +spring.security.oauth2.client.registration.github.client-id=Ov23liTBhugSIcf8VX1v +spring.security.oauth2.client.registration.github.client-secret=your_client_secret +spring.security.oauth2.client.registration.github.redirect-uri=http://localhost:8080/login/oauth2/code/github +spring.security.oauth2.client.registration.github.scope=read:user +spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize +spring.security.oauth2.client.provider.github.token-uri=http://localhost:8089/login/oauth/access_token +spring.security.oauth2.client.provider.github.user-info-uri=http://localhost:8089/user +spring.security.oauth2.client.provider.github.user-name-attribute-name=email +spring.security.oauth2.client.registration.google.provider=google +spring.security.oauth2.client.registration.google.client-id=88951150265-q7aq5urrim4rrtqo62hc96o6r5v7r34c.apps.googleusercontent.com +spring.security.oauth2.client.registration.google.client-secret=your_client_secret +spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/google +spring.security.oauth2.client.registration.google.scope=https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email +spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/auth +spring.security.oauth2.client.provider.google.token-uri=http://localhost:8089/o/oauth2/token +spring.security.oauth2.client.provider.google.user-info-uri=http://localhost:8089/oauth2/v1/userinfo +spring.security.oauth2.client.provider.google.user-name-attribute-name=email diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e69de29..24b6fa2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -0,0 +1,20 @@ +spring.security.oauth2.client.registration.github.provider=github +spring.security.oauth2.client.registration.github.client-id=Ov23liTBhugSIcf8VX1v +#TODO: client_secret +spring.security.oauth2.client.registration.github.client-secret=your_client_secret +spring.security.oauth2.client.registration.github.redirect-uri=http://localhost:8080/login/oauth2/code/github +spring.security.oauth2.client.registration.github.scope=read:user +spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize +spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token +spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user +spring.security.oauth2.client.provider.github.user-name-attribute-name=email +spring.security.oauth2.client.registration.google.provider=google +spring.security.oauth2.client.registration.google.client-id=88951150265-q7aq5urrim4rrtqo62hc96o6r5v7r34c.apps.googleusercontent.com +#TODO: client_secret +spring.security.oauth2.client.registration.google.client-secret=your_client_secret +spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/google +spring.security.oauth2.client.registration.google.scope=https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email +spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/auth +spring.security.oauth2.client.provider.google.token-uri=https://accounts.google.com/o/oauth2/token +spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v1/userinfo +spring.security.oauth2.client.provider.google.user-name-attribute-name=email From dbbc4972daa03e18691078e351c47e5f898ebdbf Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Sat, 15 Feb 2025 21:31:54 +0900 Subject: [PATCH 02/25] =?UTF-8?q?feat:=20endpoint=20=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EB=90=98=EB=8A=94=20dto=EB=A5=BC=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth2/authentication/OAuth2AccessToken.java | 5 +++++ .../endpoint/dto/OAuth2AccessTokenResponse.java | 7 +++++++ .../dto/OAuth2AuthorizationCodeGrantRequest.java | 8 ++++++++ .../endpoint/dto/OAuth2AuthorizationExchange.java | 6 ++++++ .../endpoint/dto/OAuth2AuthorizationRequest.java | 12 ++++++++++++ .../endpoint/dto/OAuth2AuthorizationResponse.java | 7 +++++++ 6 files changed, 45 insertions(+) create mode 100644 src/main/java/nextstep/oauth2/authentication/OAuth2AccessToken.java create mode 100644 src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AccessTokenResponse.java create mode 100644 src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationCodeGrantRequest.java create mode 100644 src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationExchange.java create mode 100644 src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationRequest.java create mode 100644 src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationResponse.java diff --git a/src/main/java/nextstep/oauth2/authentication/OAuth2AccessToken.java b/src/main/java/nextstep/oauth2/authentication/OAuth2AccessToken.java new file mode 100644 index 0000000..4a78efb --- /dev/null +++ b/src/main/java/nextstep/oauth2/authentication/OAuth2AccessToken.java @@ -0,0 +1,5 @@ +package nextstep.oauth2.authentication; + +public record OAuth2AccessToken( + String tokenValue +) {} diff --git a/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AccessTokenResponse.java b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AccessTokenResponse.java new file mode 100644 index 0000000..4caa065 --- /dev/null +++ b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AccessTokenResponse.java @@ -0,0 +1,7 @@ +package nextstep.oauth2.endpoint.dto; + +import nextstep.oauth2.authentication.OAuth2AccessToken; + +public record OAuth2AccessTokenResponse( + OAuth2AccessToken accessToken +) {} diff --git a/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationCodeGrantRequest.java b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationCodeGrantRequest.java new file mode 100644 index 0000000..48f181b --- /dev/null +++ b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationCodeGrantRequest.java @@ -0,0 +1,8 @@ +package nextstep.oauth2.endpoint.dto; + +import nextstep.oauth2.registration.ClientRegistration; + +public record OAuth2AuthorizationCodeGrantRequest( + ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange +) {} diff --git a/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationExchange.java b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationExchange.java new file mode 100644 index 0000000..1157381 --- /dev/null +++ b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationExchange.java @@ -0,0 +1,6 @@ +package nextstep.oauth2.endpoint.dto; + +public record OAuth2AuthorizationExchange( + OAuth2AuthorizationRequest authorizationRequest, + OAuth2AuthorizationResponse authorizationResponse +) {} diff --git a/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationRequest.java b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationRequest.java new file mode 100644 index 0000000..30a327e --- /dev/null +++ b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationRequest.java @@ -0,0 +1,12 @@ +package nextstep.oauth2.endpoint.dto; + +import java.util.Set; + +public record OAuth2AuthorizationRequest( + String authorizationUri, + String clientId, + String redirectUri, + Set scopes, + String state, + String authorizationRequestUri +) {} diff --git a/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationResponse.java b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationResponse.java new file mode 100644 index 0000000..ea5ee68 --- /dev/null +++ b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationResponse.java @@ -0,0 +1,7 @@ +package nextstep.oauth2.endpoint.dto; + +public record OAuth2AuthorizationResponse( + String code, + String redirectUri, + String state +) {} From d65d82cf59de2408236ff56139d85f5c7ee497cc Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Sat, 15 Feb 2025 22:08:53 +0900 Subject: [PATCH 03/25] =?UTF-8?q?feat:=20OAuth2AccessTokenResponse=20?= =?UTF-8?q?=EB=A5=BC=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20OAuth2Access?= =?UTF-8?q?TokenResponseClient=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OAuth2AccessTokenResponseClient.java | 21 +++++++++ .../OAuth2AuthorizationCodeGrantRequest.java | 45 ++++++++++++++++++- .../oauth2/web/OAuth2ParameterNames.java | 16 +++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/main/java/nextstep/oauth2/endpoint/OAuth2AccessTokenResponseClient.java create mode 100644 src/main/java/nextstep/oauth2/web/OAuth2ParameterNames.java diff --git a/src/main/java/nextstep/oauth2/endpoint/OAuth2AccessTokenResponseClient.java b/src/main/java/nextstep/oauth2/endpoint/OAuth2AccessTokenResponseClient.java new file mode 100644 index 0000000..b39c5a3 --- /dev/null +++ b/src/main/java/nextstep/oauth2/endpoint/OAuth2AccessTokenResponseClient.java @@ -0,0 +1,21 @@ +package nextstep.oauth2.endpoint; + +import nextstep.oauth2.authentication.OAuth2AccessToken; +import nextstep.oauth2.endpoint.dto.OAuth2AccessTokenResponse; +import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +import static nextstep.oauth2.web.OAuth2ParameterNames.ACCESS_TOKEN; + +public class OAuth2AccessTokenResponseClient { + private final RestTemplate rest = new RestTemplate(); + + public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest grantRequest) { + final String accessToken = (String) rest.exchange( + grantRequest.requestEntity(), Map.class + ).getBody().get(ACCESS_TOKEN); + return new OAuth2AccessTokenResponse(new OAuth2AccessToken(accessToken)); + } +} diff --git a/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationCodeGrantRequest.java b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationCodeGrantRequest.java index 48f181b..ff9755e 100644 --- a/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationCodeGrantRequest.java +++ b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationCodeGrantRequest.java @@ -1,8 +1,51 @@ package nextstep.oauth2.endpoint.dto; import nextstep.oauth2.registration.ClientRegistration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.RequestEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; + +import static nextstep.oauth2.web.OAuth2ParameterNames.CLIENT_ID; +import static nextstep.oauth2.web.OAuth2ParameterNames.CLIENT_SECRET; +import static nextstep.oauth2.web.OAuth2ParameterNames.CODE; +import static nextstep.oauth2.web.OAuth2ParameterNames.GRANT_TYPE; +import static nextstep.oauth2.web.OAuth2ParameterNames.REDIRECT_URI; +import static nextstep.oauth2.web.OAuth2ParameterNames.STATE; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.HttpMethod.POST; public record OAuth2AuthorizationCodeGrantRequest( ClientRegistration clientRegistration, OAuth2AuthorizationExchange authorizationExchange -) {} +) { + public RequestEntity> requestEntity() { + return new RequestEntity<>(requestBody(), headers(), POST, tokenUri()); + } + + private MultiValueMap requestBody() { + final MultiValueMap body = new LinkedMultiValueMap<>(); + body.add(GRANT_TYPE, "authorization_code"); + body.add(CODE, authorizationExchange.authorizationResponse().code()); + body.add(STATE, authorizationExchange.authorizationRequest().state()); + body.add(REDIRECT_URI, authorizationExchange.authorizationRequest().redirectUri()); + body.add(CLIENT_ID, clientRegistration.clientId()); + body.add(CLIENT_SECRET, clientRegistration.clientSecret()); + return body; + } + + private HttpHeaders headers() { + final HttpHeaders headers = new HttpHeaders(); + headers.set(CONTENT_TYPE, "application/x-www-form-urlencoded"); + return headers; + } + + private URI tokenUri() { + return UriComponentsBuilder.fromUriString( + clientRegistration.providerDetails().tokenUri() + ).build().toUri(); + } +} diff --git a/src/main/java/nextstep/oauth2/web/OAuth2ParameterNames.java b/src/main/java/nextstep/oauth2/web/OAuth2ParameterNames.java new file mode 100644 index 0000000..6fa82f5 --- /dev/null +++ b/src/main/java/nextstep/oauth2/web/OAuth2ParameterNames.java @@ -0,0 +1,16 @@ +package nextstep.oauth2.web; + +public final class OAuth2ParameterNames { + public static final String CODE = "code"; + public static final String STATE = "state"; + public static final String ERROR = "error"; + public static final String SCOPE = "scope"; + public static final String ACCESS_TOKEN = "access_token"; + public static final String GRANT_TYPE = "grant_type"; + public static final String CLIENT_ID = "client_id"; + public static final String CLIENT_SECRET = "client_secret"; + public static final String REDIRECT_URI = "redirect_uri"; + public static final String RESPONSE_TYPE = "response_type"; + + private OAuth2ParameterNames() {} +} From 68604ac85caf77e8a488eeb4f62762df45bc1fa3 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Sat, 15 Feb 2025 23:04:22 +0900 Subject: [PATCH 04/25] =?UTF-8?q?feat:=20OAuth2User=20=EC=99=80=20OAuth2Us?= =?UTF-8?q?erService=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authentication/OAuth2AccessToken.java | 4 +- .../OAuth2AuthenticationException.java | 7 +++ .../OAuth2AuthorizationException.java | 7 +++ .../registration/ClientRegistration.java | 8 +++- .../oauth2/userinfo/DefaultOAuth2User.java | 10 ++++ .../userinfo/DefaultOAuth2UserService.java | 26 ++++++++++ .../nextstep/oauth2/userinfo/OAuth2User.java | 12 +++++ .../oauth2/userinfo/OAuth2UserRequest.java | 47 +++++++++++++++++++ .../oauth2/userinfo/OAuth2UserService.java | 5 ++ 9 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 src/main/java/nextstep/oauth2/exception/OAuth2AuthenticationException.java create mode 100644 src/main/java/nextstep/oauth2/exception/OAuth2AuthorizationException.java create mode 100644 src/main/java/nextstep/oauth2/userinfo/DefaultOAuth2User.java create mode 100644 src/main/java/nextstep/oauth2/userinfo/DefaultOAuth2UserService.java create mode 100644 src/main/java/nextstep/oauth2/userinfo/OAuth2User.java create mode 100644 src/main/java/nextstep/oauth2/userinfo/OAuth2UserRequest.java create mode 100644 src/main/java/nextstep/oauth2/userinfo/OAuth2UserService.java diff --git a/src/main/java/nextstep/oauth2/authentication/OAuth2AccessToken.java b/src/main/java/nextstep/oauth2/authentication/OAuth2AccessToken.java index 4a78efb..d2c1e4c 100644 --- a/src/main/java/nextstep/oauth2/authentication/OAuth2AccessToken.java +++ b/src/main/java/nextstep/oauth2/authentication/OAuth2AccessToken.java @@ -1,5 +1,3 @@ package nextstep.oauth2.authentication; -public record OAuth2AccessToken( - String tokenValue -) {} +public record OAuth2AccessToken(String token) {} diff --git a/src/main/java/nextstep/oauth2/exception/OAuth2AuthenticationException.java b/src/main/java/nextstep/oauth2/exception/OAuth2AuthenticationException.java new file mode 100644 index 0000000..b76e89e --- /dev/null +++ b/src/main/java/nextstep/oauth2/exception/OAuth2AuthenticationException.java @@ -0,0 +1,7 @@ +package nextstep.oauth2.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.UNAUTHORIZED) +public class OAuth2AuthenticationException extends RuntimeException {} diff --git a/src/main/java/nextstep/oauth2/exception/OAuth2AuthorizationException.java b/src/main/java/nextstep/oauth2/exception/OAuth2AuthorizationException.java new file mode 100644 index 0000000..b1b2e2c --- /dev/null +++ b/src/main/java/nextstep/oauth2/exception/OAuth2AuthorizationException.java @@ -0,0 +1,7 @@ +package nextstep.oauth2.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.UNAUTHORIZED) +public class OAuth2AuthorizationException extends RuntimeException {} diff --git a/src/main/java/nextstep/oauth2/registration/ClientRegistration.java b/src/main/java/nextstep/oauth2/registration/ClientRegistration.java index e5592ab..09c2f59 100644 --- a/src/main/java/nextstep/oauth2/registration/ClientRegistration.java +++ b/src/main/java/nextstep/oauth2/registration/ClientRegistration.java @@ -1,5 +1,7 @@ package nextstep.oauth2.registration; +import org.springframework.util.StringUtils; + import java.util.Set; public record ClientRegistration( @@ -19,5 +21,9 @@ public record ProviderDetails( public record UserInfoEndpoint( String uri, String userNameAttributeName - ) {} + ) { + public boolean hasText() { + return StringUtils.hasText(uri) && StringUtils.hasText(userNameAttributeName); + } + } } diff --git a/src/main/java/nextstep/oauth2/userinfo/DefaultOAuth2User.java b/src/main/java/nextstep/oauth2/userinfo/DefaultOAuth2User.java new file mode 100644 index 0000000..e275d84 --- /dev/null +++ b/src/main/java/nextstep/oauth2/userinfo/DefaultOAuth2User.java @@ -0,0 +1,10 @@ +package nextstep.oauth2.userinfo; + +import java.util.Map; +import java.util.Set; + +public record DefaultOAuth2User( + Set authorities, + Map attributes, + String userNameAttributeName +) implements OAuth2User {} diff --git a/src/main/java/nextstep/oauth2/userinfo/DefaultOAuth2UserService.java b/src/main/java/nextstep/oauth2/userinfo/DefaultOAuth2UserService.java new file mode 100644 index 0000000..d04fb5c --- /dev/null +++ b/src/main/java/nextstep/oauth2/userinfo/DefaultOAuth2UserService.java @@ -0,0 +1,26 @@ +package nextstep.oauth2.userinfo; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +import static java.util.Collections.emptySet; + +public class DefaultOAuth2UserService implements OAuth2UserService { + private final RestTemplate rest = new RestTemplate(); + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) { + final ResponseEntity> response = rest.exchange( + userRequest.requestEntity(), + new ParameterizedTypeReference<>() {} + ); + return new DefaultOAuth2User( + emptySet(), + userRequest.customAttributes(response.getBody()), + userRequest.userNameAttributeName() + ); + } +} diff --git a/src/main/java/nextstep/oauth2/userinfo/OAuth2User.java b/src/main/java/nextstep/oauth2/userinfo/OAuth2User.java new file mode 100644 index 0000000..9bea96a --- /dev/null +++ b/src/main/java/nextstep/oauth2/userinfo/OAuth2User.java @@ -0,0 +1,12 @@ +package nextstep.oauth2.userinfo; + +import java.util.Map; +import java.util.Set; + +public interface OAuth2User { + Set authorities(); + + Map attributes(); + + String userNameAttributeName(); +} diff --git a/src/main/java/nextstep/oauth2/userinfo/OAuth2UserRequest.java b/src/main/java/nextstep/oauth2/userinfo/OAuth2UserRequest.java new file mode 100644 index 0000000..f87a496 --- /dev/null +++ b/src/main/java/nextstep/oauth2/userinfo/OAuth2UserRequest.java @@ -0,0 +1,47 @@ +package nextstep.oauth2.userinfo; + +import nextstep.oauth2.authentication.OAuth2AccessToken; +import nextstep.oauth2.exception.OAuth2AuthenticationException; +import nextstep.oauth2.registration.ClientRegistration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.RequestEntity; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import static org.springframework.http.HttpMethod.GET; + +public record OAuth2UserRequest( + ClientRegistration clientRegistration, + OAuth2AccessToken accessToken +) { + public RequestEntity requestEntity() { + final HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken.token()); + headers.add(HttpHeaders.ACCEPT, "application/json"); + + final URI uri = UriComponentsBuilder.fromUriString( + clientRegistration.providerDetails().userInfoEndpoint().uri() + ).build().toUri(); + + return new RequestEntity<>(headers, GET, uri); + } + + public Map customAttributes(Map body) { + final Map customAttributes = new HashMap<>(body); + + // TODO: Custom Attribute 를 추가하거나 변형하는 로직 작성 + + return customAttributes; + } + + public String userNameAttributeName() { + if (!clientRegistration.providerDetails().userInfoEndpoint().hasText()) { + throw new OAuth2AuthenticationException(); + } + return clientRegistration.providerDetails() + .userInfoEndpoint().userNameAttributeName(); + } +} diff --git a/src/main/java/nextstep/oauth2/userinfo/OAuth2UserService.java b/src/main/java/nextstep/oauth2/userinfo/OAuth2UserService.java new file mode 100644 index 0000000..4006eb9 --- /dev/null +++ b/src/main/java/nextstep/oauth2/userinfo/OAuth2UserService.java @@ -0,0 +1,5 @@ +package nextstep.oauth2.userinfo; + +public interface OAuth2UserService { + OAuth2User loadUser(OAuth2UserRequest userRequest); +} From f3581d5908a398888e1a1babc075d59e50f88c30 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Sun, 16 Feb 2025 01:53:21 +0900 Subject: [PATCH 05/25] =?UTF-8?q?feat:=20AuthenticationToken=20=EA=B3=BC?= =?UTF-8?q?=20AuthenticationProvider=20=EB=A5=BC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...thorizationCodeAuthenticationProvider.java | 43 ++++++++ .../OAuth2LoginAuthenticationProvider.java | 51 ++++++++++ .../token/OAuth2AuthenticationToken.java | 36 +++++++ ...2AuthorizationCodeAuthenticationToken.java | 67 +++++++++++++ .../token/OAuth2LoginAuthenticationToken.java | 99 +++++++++++++++++++ .../dto/OAuth2AuthorizationExchange.java | 8 +- 6 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 src/main/java/nextstep/oauth2/authentication/provider/OAuth2AuthorizationCodeAuthenticationProvider.java create mode 100644 src/main/java/nextstep/oauth2/authentication/provider/OAuth2LoginAuthenticationProvider.java create mode 100644 src/main/java/nextstep/oauth2/authentication/token/OAuth2AuthenticationToken.java create mode 100644 src/main/java/nextstep/oauth2/authentication/token/OAuth2AuthorizationCodeAuthenticationToken.java create mode 100644 src/main/java/nextstep/oauth2/authentication/token/OAuth2LoginAuthenticationToken.java diff --git a/src/main/java/nextstep/oauth2/authentication/provider/OAuth2AuthorizationCodeAuthenticationProvider.java b/src/main/java/nextstep/oauth2/authentication/provider/OAuth2AuthorizationCodeAuthenticationProvider.java new file mode 100644 index 0000000..48b5fa6 --- /dev/null +++ b/src/main/java/nextstep/oauth2/authentication/provider/OAuth2AuthorizationCodeAuthenticationProvider.java @@ -0,0 +1,43 @@ +package nextstep.oauth2.authentication.provider; + +import nextstep.oauth2.authentication.OAuth2AccessToken; +import nextstep.oauth2.authentication.token.OAuth2AuthorizationCodeAuthenticationToken; +import nextstep.oauth2.endpoint.OAuth2AccessTokenResponseClient; +import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationCodeGrantRequest; +import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationExchange; +import nextstep.oauth2.exception.OAuth2AuthenticationException; +import nextstep.oauth2.registration.ClientRegistration; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authentication.AuthenticationProvider; + +public class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider { + private final OAuth2AccessTokenResponseClient client = new OAuth2AccessTokenResponseClient(); + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + final OAuth2AuthorizationCodeAuthenticationToken codeToken = (OAuth2AuthorizationCodeAuthenticationToken) authentication; + if (!codeToken.isValid()) { + throw new OAuth2AuthenticationException(); + } + final ClientRegistration registration = codeToken.getClientRegistration(); + final OAuth2AuthorizationExchange exchange = codeToken.getAuthorizationExchange(); + return new OAuth2AuthorizationCodeAuthenticationToken( + registration, exchange, getAccessToken(registration, exchange) + ); + } + + @Override + public boolean supports(Class authentication) { + return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication); + } + + private OAuth2AccessToken getAccessToken( + ClientRegistration registration, + OAuth2AuthorizationExchange exchange + ) { + return client.getTokenResponse( + new OAuth2AuthorizationCodeGrantRequest(registration, exchange) + ).accessToken(); + } +} diff --git a/src/main/java/nextstep/oauth2/authentication/provider/OAuth2LoginAuthenticationProvider.java b/src/main/java/nextstep/oauth2/authentication/provider/OAuth2LoginAuthenticationProvider.java new file mode 100644 index 0000000..17c5ad7 --- /dev/null +++ b/src/main/java/nextstep/oauth2/authentication/provider/OAuth2LoginAuthenticationProvider.java @@ -0,0 +1,51 @@ +package nextstep.oauth2.authentication.provider; + +import nextstep.oauth2.authentication.OAuth2AccessToken; +import nextstep.oauth2.authentication.token.OAuth2AuthorizationCodeAuthenticationToken; +import nextstep.oauth2.authentication.token.OAuth2LoginAuthenticationToken; +import nextstep.oauth2.userinfo.OAuth2User; +import nextstep.oauth2.userinfo.OAuth2UserRequest; +import nextstep.oauth2.userinfo.OAuth2UserService; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authentication.AuthenticationProvider; + +public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider { + private final OAuth2AuthorizationCodeAuthenticationProvider provider = new OAuth2AuthorizationCodeAuthenticationProvider(); + private final OAuth2UserService userService; + + public OAuth2LoginAuthenticationProvider(OAuth2UserService userService) { + this.userService = userService; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + final OAuth2LoginAuthenticationToken loginToken = (OAuth2LoginAuthenticationToken) authentication; + final OAuth2AccessToken accessToken = getAccessToken(loginToken); + final OAuth2User oauth2User = loadUser(loginToken, accessToken); + return new OAuth2LoginAuthenticationToken( + loginToken.getClientRegistration(), loginToken.getAuthorizationExchange(), + oauth2User, oauth2User.authorities(), accessToken + ); + } + + @Override + public boolean supports(Class authentication) { + return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication); + } + + private OAuth2AccessToken getAccessToken(OAuth2LoginAuthenticationToken loginToken) { + return ((OAuth2AuthorizationCodeAuthenticationToken) provider.authenticate( + new OAuth2AuthorizationCodeAuthenticationToken( + loginToken.getClientRegistration(), + loginToken.getAuthorizationExchange() + ) + )).getAccessToken(); + } + + private OAuth2User loadUser(OAuth2LoginAuthenticationToken loginToken, OAuth2AccessToken accessToken) { + return userService.loadUser( + new OAuth2UserRequest(loginToken.getClientRegistration(), accessToken) + ); + } +} diff --git a/src/main/java/nextstep/oauth2/authentication/token/OAuth2AuthenticationToken.java b/src/main/java/nextstep/oauth2/authentication/token/OAuth2AuthenticationToken.java new file mode 100644 index 0000000..ccc9ea6 --- /dev/null +++ b/src/main/java/nextstep/oauth2/authentication/token/OAuth2AuthenticationToken.java @@ -0,0 +1,36 @@ +package nextstep.oauth2.authentication.token; + +import nextstep.oauth2.userinfo.OAuth2User; +import nextstep.security.authentication.Authentication; + +import java.util.Set; + +public final class OAuth2AuthenticationToken implements Authentication { + private final OAuth2User principal; + private final Set authorities; + + public OAuth2AuthenticationToken(OAuth2User principal, Set authorities) { + this.principal = principal; + this.authorities = authorities; + } + + @Override + public Set getAuthorities() { + return authorities; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public boolean isAuthenticated() { + return true; + } +} diff --git a/src/main/java/nextstep/oauth2/authentication/token/OAuth2AuthorizationCodeAuthenticationToken.java b/src/main/java/nextstep/oauth2/authentication/token/OAuth2AuthorizationCodeAuthenticationToken.java new file mode 100644 index 0000000..82145e7 --- /dev/null +++ b/src/main/java/nextstep/oauth2/authentication/token/OAuth2AuthorizationCodeAuthenticationToken.java @@ -0,0 +1,67 @@ +package nextstep.oauth2.authentication.token; + +import nextstep.oauth2.authentication.OAuth2AccessToken; +import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationExchange; +import nextstep.oauth2.registration.ClientRegistration; +import nextstep.security.authentication.Authentication; + +import java.util.Set; + +public final class OAuth2AuthorizationCodeAuthenticationToken implements Authentication { + private final ClientRegistration clientRegistration; + private final OAuth2AuthorizationExchange authorizationExchange; + private final OAuth2AccessToken accessToken; + + public OAuth2AuthorizationCodeAuthenticationToken( + ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange, + OAuth2AccessToken accessToken + ) { + this.clientRegistration = clientRegistration; + this.authorizationExchange = authorizationExchange; + this.accessToken = accessToken; + } + + public OAuth2AuthorizationCodeAuthenticationToken( + ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange + ) { + this(clientRegistration, authorizationExchange, null); + } + + public ClientRegistration getClientRegistration() { + return clientRegistration; + } + + public OAuth2AuthorizationExchange getAuthorizationExchange() { + return authorizationExchange; + } + + public OAuth2AccessToken getAccessToken() { + return accessToken; + } + + public boolean isValid() { + return authorizationExchange.isSameState(); + } + + @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/oauth2/authentication/token/OAuth2LoginAuthenticationToken.java b/src/main/java/nextstep/oauth2/authentication/token/OAuth2LoginAuthenticationToken.java new file mode 100644 index 0000000..ec33c28 --- /dev/null +++ b/src/main/java/nextstep/oauth2/authentication/token/OAuth2LoginAuthenticationToken.java @@ -0,0 +1,99 @@ +package nextstep.oauth2.authentication.token; + +import nextstep.oauth2.authentication.OAuth2AccessToken; +import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationExchange; +import nextstep.oauth2.registration.ClientRegistration; +import nextstep.oauth2.userinfo.OAuth2User; +import nextstep.security.authentication.Authentication; + +import java.util.Collections; +import java.util.Set; + +public final class OAuth2LoginAuthenticationToken implements Authentication { + private final ClientRegistration clientRegistration; + private final OAuth2AuthorizationExchange authorizationExchange; + private final Set authorities; + private final boolean authenticated; + private final OAuth2User principal; + private final OAuth2AccessToken accessToken; + + private OAuth2LoginAuthenticationToken( + ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange, + Set authorities, + boolean authenticated, + OAuth2User principal, + OAuth2AccessToken accessToken + ) { + this.clientRegistration = clientRegistration; + this.authorizationExchange = authorizationExchange; + this.authorities = authorities; + this.authenticated = authenticated; + this.principal = principal; + this.accessToken = accessToken; + } + + public OAuth2LoginAuthenticationToken( + ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange, + OAuth2User principal, + Set authorities, + OAuth2AccessToken accessToken + ) { + this( + clientRegistration, + authorizationExchange, + authorities, + true, + principal, + accessToken + ); + } + + public OAuth2LoginAuthenticationToken( + ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange + ) { + this( + clientRegistration, + authorizationExchange, + Collections.emptySet(), + false, + null, + null + ); + } + + public ClientRegistration getClientRegistration() { + return clientRegistration; + } + + public OAuth2AuthorizationExchange getAuthorizationExchange() { + return authorizationExchange; + } + + public OAuth2AccessToken getAccessToken() { + return accessToken; + } + + + @Override + public Set getAuthorities() { + return authorities; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } +} diff --git a/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationExchange.java b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationExchange.java index 1157381..555069e 100644 --- a/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationExchange.java +++ b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationExchange.java @@ -3,4 +3,10 @@ public record OAuth2AuthorizationExchange( OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationResponse authorizationResponse -) {} +) { + public boolean isSameState() { + return authorizationRequest.state().equals( + authorizationResponse.state() + ); + } +} From b494728186ea126f57e09bf9d138819ed9dc5b16 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Sun, 16 Feb 2025 02:16:00 +0900 Subject: [PATCH 06/25] =?UTF-8?q?feat:=20ProfileUser=20=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnsupportedRegistrationIdException.java | 11 +++++++++ .../oauth2/profile/GithubProfileUser.java | 17 ++++++++++++++ .../oauth2/profile/GoogleProfileUser.java | 17 ++++++++++++++ .../oauth2/profile/OAuth2ProfileUser.java | 23 +++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 src/main/java/nextstep/oauth2/exception/UnsupportedRegistrationIdException.java create mode 100644 src/main/java/nextstep/oauth2/profile/GithubProfileUser.java create mode 100644 src/main/java/nextstep/oauth2/profile/GoogleProfileUser.java create mode 100644 src/main/java/nextstep/oauth2/profile/OAuth2ProfileUser.java diff --git a/src/main/java/nextstep/oauth2/exception/UnsupportedRegistrationIdException.java b/src/main/java/nextstep/oauth2/exception/UnsupportedRegistrationIdException.java new file mode 100644 index 0000000..9233c3e --- /dev/null +++ b/src/main/java/nextstep/oauth2/exception/UnsupportedRegistrationIdException.java @@ -0,0 +1,11 @@ +package nextstep.oauth2.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.UNAUTHORIZED) +public class UnsupportedRegistrationIdException extends RuntimeException { + public UnsupportedRegistrationIdException() { + super("Unsupported registration ID"); + } +} diff --git a/src/main/java/nextstep/oauth2/profile/GithubProfileUser.java b/src/main/java/nextstep/oauth2/profile/GithubProfileUser.java new file mode 100644 index 0000000..5adee76 --- /dev/null +++ b/src/main/java/nextstep/oauth2/profile/GithubProfileUser.java @@ -0,0 +1,17 @@ +package nextstep.oauth2.profile; + +import java.util.Map; + +public record GithubProfileUser( + String name, + String imageUrl, + String email +) implements OAuth2ProfileUser { + public static GithubProfileUser of(Map attributes) { + return new GithubProfileUser( + attributes.get("name").toString(), + attributes.get("avatar_url").toString(), + attributes.get("email").toString() + ); + } +} diff --git a/src/main/java/nextstep/oauth2/profile/GoogleProfileUser.java b/src/main/java/nextstep/oauth2/profile/GoogleProfileUser.java new file mode 100644 index 0000000..d1259d3 --- /dev/null +++ b/src/main/java/nextstep/oauth2/profile/GoogleProfileUser.java @@ -0,0 +1,17 @@ +package nextstep.oauth2.profile; + +import java.util.Map; + +public record GoogleProfileUser( + String name, + String imageUrl, + String email +) implements OAuth2ProfileUser { + public static GithubProfileUser of(Map attributes) { + return new GithubProfileUser( + attributes.get("name").toString(), + attributes.get("picture").toString(), + attributes.get("email").toString() + ); + } +} diff --git a/src/main/java/nextstep/oauth2/profile/OAuth2ProfileUser.java b/src/main/java/nextstep/oauth2/profile/OAuth2ProfileUser.java new file mode 100644 index 0000000..05010ac --- /dev/null +++ b/src/main/java/nextstep/oauth2/profile/OAuth2ProfileUser.java @@ -0,0 +1,23 @@ +package nextstep.oauth2.profile; + +import nextstep.oauth2.exception.UnsupportedRegistrationIdException; + +import java.util.Map; + +public interface OAuth2ProfileUser { + static OAuth2ProfileUser of(String registrationId, Map attributes) { + if ("google".equals(registrationId)) { + return GoogleProfileUser.of(attributes); + } + if ("github".equals(registrationId)) { + return GithubProfileUser.of(attributes); + } + throw new UnsupportedRegistrationIdException(); + } + + String name(); + + String imageUrl(); + + String email(); +} From 6e84278370b6dec46a6b82d1eb779831b2770f9b Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Sun, 16 Feb 2025 02:34:16 +0900 Subject: [PATCH 07/25] =?UTF-8?q?feat:=20State=20=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=208=EC=9E=90=EB=A6=AC=20=EB=9E=9C=EB=8D=A4=20?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=EC=97=B4=EC=9D=84=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=98=EB=8A=94=20Generator=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/oauth2/web/StateGenerator.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/nextstep/oauth2/web/StateGenerator.java diff --git a/src/main/java/nextstep/oauth2/web/StateGenerator.java b/src/main/java/nextstep/oauth2/web/StateGenerator.java new file mode 100644 index 0000000..357dd6e --- /dev/null +++ b/src/main/java/nextstep/oauth2/web/StateGenerator.java @@ -0,0 +1,19 @@ +package nextstep.oauth2.web; + +import java.security.SecureRandom; + +public final class StateGenerator { + private static final SecureRandom random = new SecureRandom(); + + private StateGenerator() {} + + public static String generateKey() { + final int length = 8; + final String alphabet = "abcdefghijklmnopqrstuvwxyz"; + final StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + sb.append(alphabet.charAt(random.nextInt(alphabet.length()))); + } + return sb.toString(); + } +} From cde753e7ac45b113af0200408789a109b593f007 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Sun, 16 Feb 2025 08:48:49 +0900 Subject: [PATCH 08/25] =?UTF-8?q?feat:=20OAuth2AuthorizedClientRepository?= =?UTF-8?q?=20=EB=A5=BC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth2/web/OAuth2AuthorizedClient.java | 10 +++++ .../oauth2/web/OAuth2AuthorizedClientDao.java | 45 +++++++++++++++++++ .../web/OAuth2AuthorizedClientRepository.java | 27 +++++++++++ 3 files changed, 82 insertions(+) create mode 100644 src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClient.java create mode 100644 src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientDao.java create mode 100644 src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientRepository.java diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClient.java b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClient.java new file mode 100644 index 0000000..56f0465 --- /dev/null +++ b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClient.java @@ -0,0 +1,10 @@ +package nextstep.oauth2.web; + +import nextstep.oauth2.authentication.OAuth2AccessToken; +import nextstep.oauth2.registration.ClientRegistration; + +public record OAuth2AuthorizedClient( + ClientRegistration clientRegistration, + String principalName, + OAuth2AccessToken accessToken +) {} diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientDao.java b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientDao.java new file mode 100644 index 0000000..49fdb47 --- /dev/null +++ b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientDao.java @@ -0,0 +1,45 @@ +package nextstep.oauth2.web; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import nextstep.security.authentication.Authentication; + +import java.util.HashMap; +import java.util.Map; + +public class OAuth2AuthorizedClientDao implements OAuth2AuthorizedClientRepository { + private static final String ATTRIBUTE_NAME = "AUTHORIZED_CLIENTS"; + + @Override + public OAuth2AuthorizedClient loadAuthorizedClient(String clientRegistrationId, Authentication principal, HttpServletRequest request) { + return getAuthorizedClients(request).get(clientRegistrationId); + } + + @Override + public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, HttpServletRequest request, HttpServletResponse response) { + final Map authorizedClients = getAuthorizedClients(request); + authorizedClients.put( + authorizedClient.clientRegistration().registrationId(), + authorizedClient + ); + request.getSession().setAttribute(ATTRIBUTE_NAME, authorizedClients); + } + + @Override + public void removeAuthorizedClient(String clientRegistrationId, Authentication principal, HttpServletRequest request, HttpServletResponse response) { + getAuthorizedClients(request).remove(clientRegistrationId); + } + + private Map getAuthorizedClients(HttpServletRequest request) { + final HttpSession session = request.getSession(false); + return session == null ? + new HashMap<>() + : getAuthorizedClients(session); + } + + @SuppressWarnings("unchecked") + private Map getAuthorizedClients(HttpSession session) { + return (Map) session.getAttribute(ATTRIBUTE_NAME); + } +} diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientRepository.java b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientRepository.java new file mode 100644 index 0000000..189deac --- /dev/null +++ b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientRepository.java @@ -0,0 +1,27 @@ +package nextstep.oauth2.web; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.authentication.Authentication; + +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 + ); +} From c547968590b6f509b76766c039a92f45134d0400 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Sun, 16 Feb 2025 09:21:27 +0900 Subject: [PATCH 09/25] =?UTF-8?q?feat:=20OAuth2AuthorizationRequestReposit?= =?UTF-8?q?ory=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/UnmatchedStateException.java | 11 ++++ .../oauth2/keygen/DefaultStateGenerator.java | 31 ++++++++++ .../oauth2/keygen/StateGenerator.java | 5 ++ .../web/OAuth2AuthorizationRequestDao.java | 57 +++++++++++++++++++ .../OAuth2AuthorizationRequestRepository.java | 20 +++++++ .../nextstep/oauth2/web/StateGenerator.java | 19 ------- 6 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 src/main/java/nextstep/oauth2/exception/UnmatchedStateException.java create mode 100644 src/main/java/nextstep/oauth2/keygen/DefaultStateGenerator.java create mode 100644 src/main/java/nextstep/oauth2/keygen/StateGenerator.java create mode 100644 src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestDao.java create mode 100644 src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRepository.java delete mode 100644 src/main/java/nextstep/oauth2/web/StateGenerator.java diff --git a/src/main/java/nextstep/oauth2/exception/UnmatchedStateException.java b/src/main/java/nextstep/oauth2/exception/UnmatchedStateException.java new file mode 100644 index 0000000..4f3a2e8 --- /dev/null +++ b/src/main/java/nextstep/oauth2/exception/UnmatchedStateException.java @@ -0,0 +1,11 @@ +package nextstep.oauth2.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.UNAUTHORIZED) +public class UnmatchedStateException extends RuntimeException { + public UnmatchedStateException() { + super("OAuth2 state of httpRequest and session are mismatched"); + } +} diff --git a/src/main/java/nextstep/oauth2/keygen/DefaultStateGenerator.java b/src/main/java/nextstep/oauth2/keygen/DefaultStateGenerator.java new file mode 100644 index 0000000..20488f5 --- /dev/null +++ b/src/main/java/nextstep/oauth2/keygen/DefaultStateGenerator.java @@ -0,0 +1,31 @@ +package nextstep.oauth2.keygen; + +import java.security.SecureRandom; + + +public final class DefaultStateGenerator implements StateGenerator { + private static final SecureRandom RANDOM = new SecureRandom(); + private static final int LENGTH = 8; + private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyz"; + + private DefaultStateGenerator() {} + + public static DefaultStateGenerator getInstance() { + return SingletonHolder.INSTANCE; + } + + @Override + public String generateKey() { + final StringBuilder sb = new StringBuilder(LENGTH); + for (int i = 0; i < LENGTH; i++) { + sb.append(ALPHABET.charAt( + RANDOM.nextInt(ALPHABET.length()) + )); + } + return sb.toString(); + } + + public static class SingletonHolder { + private static final DefaultStateGenerator INSTANCE = new DefaultStateGenerator(); + } +} diff --git a/src/main/java/nextstep/oauth2/keygen/StateGenerator.java b/src/main/java/nextstep/oauth2/keygen/StateGenerator.java new file mode 100644 index 0000000..39b086b --- /dev/null +++ b/src/main/java/nextstep/oauth2/keygen/StateGenerator.java @@ -0,0 +1,5 @@ +package nextstep.oauth2.keygen; + +public interface StateGenerator { + String generateKey(); +} diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestDao.java b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestDao.java new file mode 100644 index 0000000..403aac5 --- /dev/null +++ b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestDao.java @@ -0,0 +1,57 @@ +package nextstep.oauth2.web; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationRequest; +import nextstep.oauth2.exception.UnmatchedStateException; + +public class OAuth2AuthorizationRequestDao implements OAuth2AuthorizationRequestRepository { + private final String ATTRIBUTE_NAME = "AUTHORIZATION_REQUEST"; + + @Override + public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { + validateState(request); + return getAuthorizationRequest(request); + } + + @Override + public void saveAuthorizationRequest( + OAuth2AuthorizationRequest authorizationRequest, + HttpServletRequest request, + HttpServletResponse response + ) { + if (authorizationRequest == null) { + request.getSession().removeAttribute(ATTRIBUTE_NAME); + return; + } + request.getSession().setAttribute(ATTRIBUTE_NAME, authorizationRequest); + } + + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest + (HttpServletRequest request, + HttpServletResponse response + ) { + final OAuth2AuthorizationRequest authorizationRequest = loadAuthorizationRequest(request); + request.getSession().removeAttribute(ATTRIBUTE_NAME); + return authorizationRequest; + } + + private void validateState(HttpServletRequest request) { + final String httpState = request.getParameter(OAuth2ParameterNames.STATE); + final OAuth2AuthorizationRequest authorizationRequest = getAuthorizationRequest(request); + if ( + httpState != null && authorizationRequest != null + && !httpState.equals(authorizationRequest.state()) + ) { + throw new UnmatchedStateException(); + } + } + + private OAuth2AuthorizationRequest getAuthorizationRequest(HttpServletRequest request) { + final HttpSession session = request.getSession(false); + return session == null ? null + : (OAuth2AuthorizationRequest) session.getAttribute(this.ATTRIBUTE_NAME); + } +} diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRepository.java b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRepository.java new file mode 100644 index 0000000..a46e63e --- /dev/null +++ b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRepository.java @@ -0,0 +1,20 @@ +package nextstep.oauth2.web; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationRequest; + +public interface OAuth2AuthorizationRequestRepository { + 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/oauth2/web/StateGenerator.java b/src/main/java/nextstep/oauth2/web/StateGenerator.java deleted file mode 100644 index 357dd6e..0000000 --- a/src/main/java/nextstep/oauth2/web/StateGenerator.java +++ /dev/null @@ -1,19 +0,0 @@ -package nextstep.oauth2.web; - -import java.security.SecureRandom; - -public final class StateGenerator { - private static final SecureRandom random = new SecureRandom(); - - private StateGenerator() {} - - public static String generateKey() { - final int length = 8; - final String alphabet = "abcdefghijklmnopqrstuvwxyz"; - final StringBuilder sb = new StringBuilder(length); - for (int i = 0; i < length; i++) { - sb.append(alphabet.charAt(random.nextInt(alphabet.length()))); - } - return sb.toString(); - } -} From a518b2de497dcfe07b8e5731683ab5898e70d10d Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Sun, 16 Feb 2025 09:38:46 +0900 Subject: [PATCH 10/25] =?UTF-8?q?feat:=20OAuth2AuthorizationRequestResolve?= =?UTF-8?q?r=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OAuth2AuthenticationException.java | 10 ++- .../OAuth2AuthorizationException.java | 10 ++- .../exception/UnmatchedStateException.java | 2 +- .../UnsupportedRegistrationIdException.java | 4 +- .../oauth2/profile/OAuth2ProfileUser.java | 2 +- .../registration/ClientRegistrationDao.java | 7 +- .../OAuth2AuthorizationRequestDao.java | 3 +- .../OAuth2AuthorizationRequestRepository.java | 2 +- .../OAuth2AuthorizationRequestResolver.java | 70 +++++++++++++++++++ .../OAuth2AuthorizedClient.java | 2 +- .../OAuth2AuthorizedClientDao.java | 2 +- .../OAuth2AuthorizedClientRepository.java | 2 +- 12 files changed, 104 insertions(+), 12 deletions(-) rename src/main/java/nextstep/oauth2/web/{ => authorizationrequest}/OAuth2AuthorizationRequestDao.java (95%) rename src/main/java/nextstep/oauth2/web/{ => authorizationrequest}/OAuth2AuthorizationRequestRepository.java (92%) create mode 100644 src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestResolver.java rename src/main/java/nextstep/oauth2/web/{ => authorizedclient}/OAuth2AuthorizedClient.java (85%) rename src/main/java/nextstep/oauth2/web/{ => authorizedclient}/OAuth2AuthorizedClientDao.java (97%) rename src/main/java/nextstep/oauth2/web/{ => authorizedclient}/OAuth2AuthorizedClientRepository.java (94%) diff --git a/src/main/java/nextstep/oauth2/exception/OAuth2AuthenticationException.java b/src/main/java/nextstep/oauth2/exception/OAuth2AuthenticationException.java index b76e89e..95bfcbb 100644 --- a/src/main/java/nextstep/oauth2/exception/OAuth2AuthenticationException.java +++ b/src/main/java/nextstep/oauth2/exception/OAuth2AuthenticationException.java @@ -4,4 +4,12 @@ import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(HttpStatus.UNAUTHORIZED) -public class OAuth2AuthenticationException extends RuntimeException {} +public class OAuth2AuthenticationException extends RuntimeException { + public OAuth2AuthenticationException(String message) { + super(message); + } + + public OAuth2AuthenticationException() { + this("OAuth2 인증에 실패했습니다."); + } +} diff --git a/src/main/java/nextstep/oauth2/exception/OAuth2AuthorizationException.java b/src/main/java/nextstep/oauth2/exception/OAuth2AuthorizationException.java index b1b2e2c..f305923 100644 --- a/src/main/java/nextstep/oauth2/exception/OAuth2AuthorizationException.java +++ b/src/main/java/nextstep/oauth2/exception/OAuth2AuthorizationException.java @@ -4,4 +4,12 @@ import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(HttpStatus.UNAUTHORIZED) -public class OAuth2AuthorizationException extends RuntimeException {} +public class OAuth2AuthorizationException extends RuntimeException { + public OAuth2AuthorizationException(String message) { + super(message); + } + + public OAuth2AuthorizationException() { + super("OAuth2 인가에 실패했습니다."); + } +} diff --git a/src/main/java/nextstep/oauth2/exception/UnmatchedStateException.java b/src/main/java/nextstep/oauth2/exception/UnmatchedStateException.java index 4f3a2e8..02d6da3 100644 --- a/src/main/java/nextstep/oauth2/exception/UnmatchedStateException.java +++ b/src/main/java/nextstep/oauth2/exception/UnmatchedStateException.java @@ -6,6 +6,6 @@ @ResponseStatus(HttpStatus.UNAUTHORIZED) public class UnmatchedStateException extends RuntimeException { public UnmatchedStateException() { - super("OAuth2 state of httpRequest and session are mismatched"); + super("HttpRequest 와 세션의 OAuth2 state 가 불일치 합니다."); } } diff --git a/src/main/java/nextstep/oauth2/exception/UnsupportedRegistrationIdException.java b/src/main/java/nextstep/oauth2/exception/UnsupportedRegistrationIdException.java index 9233c3e..2765ef6 100644 --- a/src/main/java/nextstep/oauth2/exception/UnsupportedRegistrationIdException.java +++ b/src/main/java/nextstep/oauth2/exception/UnsupportedRegistrationIdException.java @@ -5,7 +5,7 @@ @ResponseStatus(HttpStatus.UNAUTHORIZED) public class UnsupportedRegistrationIdException extends RuntimeException { - public UnsupportedRegistrationIdException() { - super("Unsupported registration ID"); + public UnsupportedRegistrationIdException(String registrationId) { + super("지원하지 않는 registration ID 입니다; " + registrationId); } } diff --git a/src/main/java/nextstep/oauth2/profile/OAuth2ProfileUser.java b/src/main/java/nextstep/oauth2/profile/OAuth2ProfileUser.java index 05010ac..1472981 100644 --- a/src/main/java/nextstep/oauth2/profile/OAuth2ProfileUser.java +++ b/src/main/java/nextstep/oauth2/profile/OAuth2ProfileUser.java @@ -12,7 +12,7 @@ static OAuth2ProfileUser of(String registrationId, Map attribute if ("github".equals(registrationId)) { return GithubProfileUser.of(attributes); } - throw new UnsupportedRegistrationIdException(); + throw new UnsupportedRegistrationIdException(registrationId); } String name(); diff --git a/src/main/java/nextstep/oauth2/registration/ClientRegistrationDao.java b/src/main/java/nextstep/oauth2/registration/ClientRegistrationDao.java index 44606e8..e3c82be 100644 --- a/src/main/java/nextstep/oauth2/registration/ClientRegistrationDao.java +++ b/src/main/java/nextstep/oauth2/registration/ClientRegistrationDao.java @@ -1,5 +1,6 @@ package nextstep.oauth2.registration; +import nextstep.oauth2.exception.UnsupportedRegistrationIdException; import org.springframework.stereotype.Repository; import java.util.Map; @@ -13,6 +14,10 @@ public ClientRegistrationDao(Map registrations) { } public ClientRegistration findByRegistrationId(String registrationId) { - return this.registrations.get(registrationId); + final ClientRegistration registration = registrations.get(registrationId); + if (registration == null) { + throw new UnsupportedRegistrationIdException(registrationId); + } + return registration; } } diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestDao.java b/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestDao.java similarity index 95% rename from src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestDao.java rename to src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestDao.java index 403aac5..1574122 100644 --- a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestDao.java +++ b/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestDao.java @@ -1,10 +1,11 @@ -package nextstep.oauth2.web; +package nextstep.oauth2.web.authorizationrequest; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationRequest; import nextstep.oauth2.exception.UnmatchedStateException; +import nextstep.oauth2.web.OAuth2ParameterNames; public class OAuth2AuthorizationRequestDao implements OAuth2AuthorizationRequestRepository { private final String ATTRIBUTE_NAME = "AUTHORIZATION_REQUEST"; diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRepository.java b/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestRepository.java similarity index 92% rename from src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRepository.java rename to src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestRepository.java index a46e63e..43543b1 100644 --- a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRepository.java +++ b/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestRepository.java @@ -1,4 +1,4 @@ -package nextstep.oauth2.web; +package nextstep.oauth2.web.authorizationrequest; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestResolver.java b/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestResolver.java new file mode 100644 index 0000000..2b9b727 --- /dev/null +++ b/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestResolver.java @@ -0,0 +1,70 @@ +package nextstep.oauth2.web.authorizationrequest; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationRequest; +import nextstep.oauth2.keygen.DefaultStateGenerator; +import nextstep.oauth2.keygen.StateGenerator; +import nextstep.oauth2.registration.ClientRegistration; +import nextstep.oauth2.registration.ClientRegistrationRepository; +import org.springframework.web.util.UriComponentsBuilder; + +import static nextstep.oauth2.web.OAuth2ParameterNames.CLIENT_ID; +import static nextstep.oauth2.web.OAuth2ParameterNames.CODE; +import static nextstep.oauth2.web.OAuth2ParameterNames.REDIRECT_URI; +import static nextstep.oauth2.web.OAuth2ParameterNames.RESPONSE_TYPE; +import static nextstep.oauth2.web.OAuth2ParameterNames.SCOPE; +import static nextstep.oauth2.web.OAuth2ParameterNames.STATE; + +public class OAuth2AuthorizationRequestResolver { + private static final StateGenerator STATE_GENERATOR = DefaultStateGenerator.getInstance(); + + private final String authorizationRequestBaseUri; + private final ClientRegistrationRepository clientRegistrationRepository; + + public OAuth2AuthorizationRequestResolver(String authorizationRequestBaseUri, ClientRegistrationRepository clientRegistrationRepository) { + this.authorizationRequestBaseUri = authorizationRequestBaseUri; + this.clientRegistrationRepository = clientRegistrationRepository; + } + + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + final String registrationId = resolveRegistrationId(request); + return registrationId == null ? null : resolve(registrationId); + } + + private String resolveRegistrationId(HttpServletRequest request) { + final String uri = request.getRequestURI(); + return uri.startsWith(authorizationRequestBaseUri) + ? uri.substring(authorizationRequestBaseUri.length()) + : null; + } + + private OAuth2AuthorizationRequest resolve(String registrationId) { + final ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(registrationId); + final String state = STATE_GENERATOR.generateKey(); + return new OAuth2AuthorizationRequest( + clientRegistration.providerDetails().authorizationUri(), + clientRegistration.clientId(), + clientRegistration.redirectUri(), + clientRegistration.scopes(), + state, + authorizationRequestUri(clientRegistration, state) + ); + } + + private String authorizationRequestUri( + ClientRegistration clientRegistration, + String state + ) { + return clientRegistration.providerDetails().authorizationUri() + + "?" + + UriComponentsBuilder.newInstance() + .queryParam(CLIENT_ID, clientRegistration.clientId()) + .queryParam(RESPONSE_TYPE, CODE) + .queryParam(SCOPE, clientRegistration.scopes()) + .queryParam(REDIRECT_URI, clientRegistration.redirectUri()) + .queryParam(STATE, state) + .build() + .toUri() + .getQuery(); + } +} diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClient.java b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClient.java similarity index 85% rename from src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClient.java rename to src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClient.java index 56f0465..aeeee86 100644 --- a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClient.java +++ b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClient.java @@ -1,4 +1,4 @@ -package nextstep.oauth2.web; +package nextstep.oauth2.web.authorizedclient; import nextstep.oauth2.authentication.OAuth2AccessToken; import nextstep.oauth2.registration.ClientRegistration; diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientDao.java b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java similarity index 97% rename from src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientDao.java rename to src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java index 49fdb47..1ff6693 100644 --- a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientDao.java +++ b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java @@ -1,4 +1,4 @@ -package nextstep.oauth2.web; +package nextstep.oauth2.web.authorizedclient; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientRepository.java b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientRepository.java similarity index 94% rename from src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientRepository.java rename to src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientRepository.java index 189deac..4682938 100644 --- a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizedClientRepository.java +++ b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientRepository.java @@ -1,4 +1,4 @@ -package nextstep.oauth2.web; +package nextstep.oauth2.web.authorizedclient; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; From f6203fb61e9fd9662eb3cd499b014c9ef716140f Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Sun, 16 Feb 2025 09:54:42 +0900 Subject: [PATCH 11/25] =?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 --- ...th2AuthorizationRequestRedirectFilter.java | 49 +++++++++++++++++++ .../OAuth2AuthorizationRequestDao.java | 10 ++++ .../OAuth2AuthorizationRequestResolver.java | 9 ++-- 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java new file mode 100644 index 0000000..820cf3a --- /dev/null +++ b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java @@ -0,0 +1,49 @@ +package nextstep.oauth2.web; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationRequest; +import nextstep.oauth2.registration.ClientRegistrationRepository; +import nextstep.oauth2.web.authorizationrequest.OAuth2AuthorizationRequestDao; +import nextstep.oauth2.web.authorizationrequest.OAuth2AuthorizationRequestRepository; +import nextstep.oauth2.web.authorizationrequest.OAuth2AuthorizationRequestResolver; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter { + private static final String BASE_URI = "/oauth2/authorization/"; + + private final OAuth2AuthorizationRequestResolver authorizationRequestResolver; + private final OAuth2AuthorizationRequestRepository OAuth2AuthorizationRequestRepository; + + private OAuth2AuthorizationRequestRedirectFilter( + OAuth2AuthorizationRequestResolver authorizationRequestResolver, + OAuth2AuthorizationRequestRepository oAuth2AuthorizationRequestRepository + ) { + this.authorizationRequestResolver = authorizationRequestResolver; + this.OAuth2AuthorizationRequestRepository = oAuth2AuthorizationRequestRepository; + } + + public static OAuth2AuthorizationRequestRedirectFilter of( + ClientRegistrationRepository clientRegistrationRepository + ) { + return new OAuth2AuthorizationRequestRedirectFilter( + new OAuth2AuthorizationRequestResolver(clientRegistrationRepository, BASE_URI), + OAuth2AuthorizationRequestDao.getInstance() + ); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + final OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request); + if (authorizationRequest == null) { + filterChain.doFilter(request, response); + return; + } + OAuth2AuthorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); + response.sendRedirect(authorizationRequest.authorizationRequestUri()); + } +} diff --git a/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestDao.java b/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestDao.java index 1574122..4d863a4 100644 --- a/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestDao.java +++ b/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestDao.java @@ -10,6 +10,12 @@ public class OAuth2AuthorizationRequestDao implements OAuth2AuthorizationRequestRepository { private final String ATTRIBUTE_NAME = "AUTHORIZATION_REQUEST"; + private OAuth2AuthorizationRequestDao() {} + + public static OAuth2AuthorizationRequestDao getInstance() { + return SingletonHolder.INSTANCE; + } + @Override public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { validateState(request); @@ -55,4 +61,8 @@ private OAuth2AuthorizationRequest getAuthorizationRequest(HttpServletRequest re return session == null ? null : (OAuth2AuthorizationRequest) session.getAttribute(this.ATTRIBUTE_NAME); } + + private static class SingletonHolder { + private static final OAuth2AuthorizationRequestDao INSTANCE = new OAuth2AuthorizationRequestDao(); + } } diff --git a/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestResolver.java b/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestResolver.java index 2b9b727..00429ad 100644 --- a/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestResolver.java +++ b/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestResolver.java @@ -18,12 +18,15 @@ public class OAuth2AuthorizationRequestResolver { private static final StateGenerator STATE_GENERATOR = DefaultStateGenerator.getInstance(); - private final String authorizationRequestBaseUri; private final ClientRegistrationRepository clientRegistrationRepository; + private final String authorizationRequestBaseUri; - public OAuth2AuthorizationRequestResolver(String authorizationRequestBaseUri, ClientRegistrationRepository clientRegistrationRepository) { - this.authorizationRequestBaseUri = authorizationRequestBaseUri; + public OAuth2AuthorizationRequestResolver( + ClientRegistrationRepository clientRegistrationRepository, + String authorizationRequestBaseUri + ) { this.clientRegistrationRepository = clientRegistrationRepository; + this.authorizationRequestBaseUri = authorizationRequestBaseUri; } public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { From c31f07083aa6e9ea82d29b6f98d5cafd7b9e7114 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Sun, 16 Feb 2025 10:12:02 +0900 Subject: [PATCH 12/25] =?UTF-8?q?refactor:=20SecurityContextRepository=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=EB=A5=BC=20?= =?UTF-8?q?=EB=94=B0=EB=A1=9C=20=EC=84=A0=EC=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HttpSessionSecurityContextRepository.java | 14 +++++++++++++- .../context/SecurityContextHolderFilter.java | 2 +- .../context/SecurityContextRepository.java | 14 ++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/main/java/nextstep/security/context/SecurityContextRepository.java diff --git a/src/main/java/nextstep/security/context/HttpSessionSecurityContextRepository.java b/src/main/java/nextstep/security/context/HttpSessionSecurityContextRepository.java index 9689990..113c8c6 100644 --- a/src/main/java/nextstep/security/context/HttpSessionSecurityContextRepository.java +++ b/src/main/java/nextstep/security/context/HttpSessionSecurityContextRepository.java @@ -4,9 +4,16 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; -public class HttpSessionSecurityContextRepository { +public class HttpSessionSecurityContextRepository implements SecurityContextRepository { public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + private HttpSessionSecurityContextRepository() {} + + public static HttpSessionSecurityContextRepository getInstance() { + return SingletonHolder.INSTANCE; + } + + @Override public SecurityContext loadContext(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { @@ -16,8 +23,13 @@ public SecurityContext loadContext(HttpServletRequest request) { return (SecurityContext) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); } + @Override public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { HttpSession session = request.getSession(); session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); } + + private static class SingletonHolder { + private static final HttpSessionSecurityContextRepository INSTANCE = new HttpSessionSecurityContextRepository(); + } } diff --git a/src/main/java/nextstep/security/context/SecurityContextHolderFilter.java b/src/main/java/nextstep/security/context/SecurityContextHolderFilter.java index c1fd4f2..34edc40 100644 --- a/src/main/java/nextstep/security/context/SecurityContextHolderFilter.java +++ b/src/main/java/nextstep/security/context/SecurityContextHolderFilter.java @@ -10,7 +10,7 @@ import java.io.IOException; public class SecurityContextHolderFilter extends GenericFilterBean { - private final HttpSessionSecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository(); + private final SecurityContextRepository securityContextRepository = HttpSessionSecurityContextRepository.getInstance(); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { diff --git a/src/main/java/nextstep/security/context/SecurityContextRepository.java b/src/main/java/nextstep/security/context/SecurityContextRepository.java new file mode 100644 index 0000000..a56e94c --- /dev/null +++ b/src/main/java/nextstep/security/context/SecurityContextRepository.java @@ -0,0 +1,14 @@ +package nextstep.security.context; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public interface SecurityContextRepository { + SecurityContext loadContext(HttpServletRequest request); + + void saveContext( + SecurityContext context, + HttpServletRequest request, + HttpServletResponse response + ); +} From a0b1a60927add26076594569f5561e5fad277b70 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Sun, 16 Feb 2025 10:55:08 +0900 Subject: [PATCH 13/25] =?UTF-8?q?feat:=20AbstractAuthenticationProcessingF?= =?UTF-8?q?ilter=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../token/OAuth2LoginAuthenticationToken.java | 2 +- ...bstractAuthenticationProcessingFilter.java | 81 +++++++++++++++++++ .../handler/AuthenticationFailureHandler.java | 17 ++++ .../handler/AuthenticationSuccessHandler.java | 17 ++++ .../DefaultAuthenticationFailureHandler.java | 31 +++++++ .../DefaultAuthenticationSuccessHandler.java | 28 +++++++ 6 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 src/main/java/nextstep/security/authentication/AbstractAuthenticationProcessingFilter.java create mode 100644 src/main/java/nextstep/security/authentication/handler/AuthenticationFailureHandler.java create mode 100644 src/main/java/nextstep/security/authentication/handler/AuthenticationSuccessHandler.java create mode 100644 src/main/java/nextstep/security/authentication/handler/DefaultAuthenticationFailureHandler.java create mode 100644 src/main/java/nextstep/security/authentication/handler/DefaultAuthenticationSuccessHandler.java diff --git a/src/main/java/nextstep/oauth2/authentication/token/OAuth2LoginAuthenticationToken.java b/src/main/java/nextstep/oauth2/authentication/token/OAuth2LoginAuthenticationToken.java index ec33c28..1f416ce 100644 --- a/src/main/java/nextstep/oauth2/authentication/token/OAuth2LoginAuthenticationToken.java +++ b/src/main/java/nextstep/oauth2/authentication/token/OAuth2LoginAuthenticationToken.java @@ -88,7 +88,7 @@ public Object getCredentials() { } @Override - public Object getPrincipal() { + public OAuth2User getPrincipal() { return principal; } diff --git a/src/main/java/nextstep/security/authentication/AbstractAuthenticationProcessingFilter.java b/src/main/java/nextstep/security/authentication/AbstractAuthenticationProcessingFilter.java new file mode 100644 index 0000000..fd4d290 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AbstractAuthenticationProcessingFilter.java @@ -0,0 +1,81 @@ +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 nextstep.security.access.RequestMatcher; +import nextstep.security.authentication.handler.AuthenticationFailureHandler; +import nextstep.security.authentication.handler.AuthenticationSuccessHandler; +import nextstep.security.authentication.handler.DefaultAuthenticationFailureHandler; +import nextstep.security.authentication.handler.DefaultAuthenticationSuccessHandler; +import nextstep.security.context.HttpSessionSecurityContextRepository; +import nextstep.security.context.SecurityContext; +import nextstep.security.context.SecurityContextHolder; +import nextstep.security.context.SecurityContextRepository; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; + +public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean { + private static final SecurityContextRepository securityContextRepository = HttpSessionSecurityContextRepository.getInstance(); + private static final AuthenticationSuccessHandler successHandler = DefaultAuthenticationSuccessHandler.getInstance(); + private static final AuthenticationFailureHandler failureHandler = DefaultAuthenticationFailureHandler.getInstance(); + + private final RequestMatcher requiresAuthenticationRequestMatcher; + private final AuthenticationManager authenticationManager; + + protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher, AuthenticationManager authenticationManager) { + this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher; + this.authenticationManager = authenticationManager; + } + + protected AbstractAuthenticationProcessingFilter(String filterProcessesUrl, AuthenticationManager authenticationManager) { + this(request -> request.getRequestURI().startsWith(filterProcessesUrl), authenticationManager); + } + + protected AuthenticationManager getAuthenticationManager() { + return authenticationManager; + } + + protected abstract Authentication attemptAuthentication( + HttpServletRequest request, + HttpServletResponse response + ) throws AuthenticationException, IOException, ServletException; + + @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 ServletException, IOException { + if (!requiresAuthenticationRequestMatcher.matches(request)) { + chain.doFilter(request, response); + return; + } + try { + final Authentication authentication = attemptAuthentication(request, response); + if (authentication == null) { + return; + } + final SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authentication); + SecurityContextHolder.setContext(context); + securityContextRepository.saveContext(context, request, response); + successHandler.onAuthenticationSuccess(request, response, authentication); + } catch (AuthenticationException e) { + SecurityContextHolder.clearContext(); + failureHandler.onAuthenticationFailure(request, response, e); + } + } +} diff --git a/src/main/java/nextstep/security/authentication/handler/AuthenticationFailureHandler.java b/src/main/java/nextstep/security/authentication/handler/AuthenticationFailureHandler.java new file mode 100644 index 0000000..30081a2 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/handler/AuthenticationFailureHandler.java @@ -0,0 +1,17 @@ +package nextstep.security.authentication.handler; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.authentication.AuthenticationException; + +import java.io.IOException; + +@FunctionalInterface +public interface AuthenticationFailureHandler { + void onAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception + ) throws IOException, ServletException; +} diff --git a/src/main/java/nextstep/security/authentication/handler/AuthenticationSuccessHandler.java b/src/main/java/nextstep/security/authentication/handler/AuthenticationSuccessHandler.java new file mode 100644 index 0000000..a67a687 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/handler/AuthenticationSuccessHandler.java @@ -0,0 +1,17 @@ +package nextstep.security.authentication.handler; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.authentication.Authentication; + +import java.io.IOException; + +@FunctionalInterface +public interface AuthenticationSuccessHandler { + void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException, ServletException; +} diff --git a/src/main/java/nextstep/security/authentication/handler/DefaultAuthenticationFailureHandler.java b/src/main/java/nextstep/security/authentication/handler/DefaultAuthenticationFailureHandler.java new file mode 100644 index 0000000..95bf20f --- /dev/null +++ b/src/main/java/nextstep/security/authentication/handler/DefaultAuthenticationFailureHandler.java @@ -0,0 +1,31 @@ +package nextstep.security.authentication.handler; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.authentication.AuthenticationException; +import org.springframework.http.HttpStatus; + +import java.io.IOException; + +public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler { + private static final HttpStatus status = HttpStatus.UNAUTHORIZED; + + private DefaultAuthenticationFailureHandler() {} + + public static DefaultAuthenticationFailureHandler getInstance() { + return SingletonHolder.INSTANCE; + } + + @Override + public void onAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception + ) throws IOException { + response.sendError(status.value(), status.getReasonPhrase()); + } + + private static class SingletonHolder { + private static final DefaultAuthenticationFailureHandler INSTANCE = new DefaultAuthenticationFailureHandler(); + } +} diff --git a/src/main/java/nextstep/security/authentication/handler/DefaultAuthenticationSuccessHandler.java b/src/main/java/nextstep/security/authentication/handler/DefaultAuthenticationSuccessHandler.java new file mode 100644 index 0000000..f7dbd27 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/handler/DefaultAuthenticationSuccessHandler.java @@ -0,0 +1,28 @@ +package nextstep.security.authentication.handler; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.authentication.Authentication; + +import java.io.IOException; + +public class DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + private DefaultAuthenticationSuccessHandler() {} + + public static DefaultAuthenticationSuccessHandler getInstance() { + return SingletonHolder.INSTANCE; + } + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException { + response.sendRedirect("/"); + } + + private static class SingletonHolder { + private static final DefaultAuthenticationSuccessHandler INSTANCE = new DefaultAuthenticationSuccessHandler(); + } +} From 3654c2540de072ee688c275825666ea9ecd3d636 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Sun, 16 Feb 2025 11:33:05 +0900 Subject: [PATCH 14/25] =?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 --- .../OAuth2AuthorizationException.java | 15 --- .../web/OAuth2LoginAuthenticationFilter.java | 117 ++++++++++++++++++ 2 files changed, 117 insertions(+), 15 deletions(-) delete mode 100644 src/main/java/nextstep/oauth2/exception/OAuth2AuthorizationException.java create mode 100644 src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java diff --git a/src/main/java/nextstep/oauth2/exception/OAuth2AuthorizationException.java b/src/main/java/nextstep/oauth2/exception/OAuth2AuthorizationException.java deleted file mode 100644 index f305923..0000000 --- a/src/main/java/nextstep/oauth2/exception/OAuth2AuthorizationException.java +++ /dev/null @@ -1,15 +0,0 @@ -package nextstep.oauth2.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.UNAUTHORIZED) -public class OAuth2AuthorizationException extends RuntimeException { - public OAuth2AuthorizationException(String message) { - super(message); - } - - public OAuth2AuthorizationException() { - super("OAuth2 인가에 실패했습니다."); - } -} diff --git a/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java b/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java new file mode 100644 index 0000000..5294f86 --- /dev/null +++ b/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java @@ -0,0 +1,117 @@ +package nextstep.oauth2.web; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.oauth2.authentication.token.OAuth2AuthenticationToken; +import nextstep.oauth2.authentication.token.OAuth2LoginAuthenticationToken; +import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationExchange; +import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationRequest; +import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationResponse; +import nextstep.oauth2.exception.OAuth2AuthenticationException; +import nextstep.oauth2.registration.ClientRegistration; +import nextstep.oauth2.registration.ClientRegistrationRepository; +import nextstep.oauth2.web.authorizationrequest.OAuth2AuthorizationRequestDao; +import nextstep.oauth2.web.authorizationrequest.OAuth2AuthorizationRequestRepository; +import nextstep.oauth2.web.authorizedclient.OAuth2AuthorizedClient; +import nextstep.oauth2.web.authorizedclient.OAuth2AuthorizedClientRepository; +import nextstep.security.authentication.AbstractAuthenticationProcessingFilter; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationManager; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + private static final String BASE_URI = "/login/oauth2/code/"; + private static final OAuth2AuthorizationRequestRepository requestRepository = OAuth2AuthorizationRequestDao.getInstance(); + + private final ClientRegistrationRepository registrationRepository; + private final OAuth2AuthorizedClientRepository clientRepository; + + public OAuth2LoginAuthenticationFilter( + ClientRegistrationRepository registrationRepository, + OAuth2AuthorizedClientRepository clientRepository, + AuthenticationManager authenticationManager + ) { + super(BASE_URI, authenticationManager); + this.registrationRepository = registrationRepository; + this.clientRepository = clientRepository; + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { + final OAuth2LoginAuthenticationToken loginToken = getLoginToken(request); + final OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + loginToken.getClientRegistration(), + loginToken.getPrincipal().toString(), + loginToken.getAccessToken() + ); + final OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken( + loginToken.getPrincipal(), + loginToken.getAuthorities() + ); + clientRepository.saveAuthorizedClient(authorizedClient, authentication, request, response); + return authentication; + } + + public OAuth2LoginAuthenticationToken getLoginToken( + HttpServletRequest request + ) { + final MultiValueMap params = getParams(request); + final ClientRegistration clientRegistration = getClientRegistration(request); + final OAuth2AuthorizationRequest authorizationRequest = getAuthorizationRequest(request); + final OAuth2AuthorizationResponse authorizationResponse = new OAuth2AuthorizationResponse( + clientRegistration.redirectUri(), + params.getFirst(OAuth2ParameterNames.CODE), + params.getFirst(OAuth2ParameterNames.STATE) + ); + return (OAuth2LoginAuthenticationToken) getAuthenticationManager() + .authenticate(new OAuth2LoginAuthenticationToken( + clientRegistration, + new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse) + )); + } + + public MultiValueMap getParams(HttpServletRequest request) { + final MultiValueMap params = request.getParameterMap().entrySet() + .stream().collect(Collectors.toMap( + Map.Entry::getKey, + entry -> List.of(entry.getValue()), + (oldValue, newValue) -> oldValue, + LinkedMultiValueMap::new + )); + final boolean isAuthorizationResponseSuccess = StringUtils.hasText(params.getFirst(OAuth2ParameterNames.CODE)) + && StringUtils.hasText(params.getFirst(OAuth2ParameterNames.STATE)); + final boolean isAuthorizationResponseError = StringUtils.hasText(params.getFirst(OAuth2ParameterNames.ERROR)) + && StringUtils.hasText(params.getFirst(OAuth2ParameterNames.STATE)); + final boolean isAuthorizationResponse = isAuthorizationResponseSuccess || isAuthorizationResponseError; + if (!isAuthorizationResponse) { + throw new OAuth2AuthenticationException(); + } + return params; + } + + public ClientRegistration getClientRegistration(HttpServletRequest request) { + final String requestUri = request.getRequestURI(); + final String registrationId = requestUri.startsWith(BASE_URI) + ? requestUri.substring(BASE_URI.length()) + : null; + final ClientRegistration clientRegistration = registrationRepository.findByRegistrationId(registrationId); + if (clientRegistration == null) { + throw new OAuth2AuthenticationException(); + } + return clientRegistration; + } + + public OAuth2AuthorizationRequest getAuthorizationRequest(HttpServletRequest request) { + final OAuth2AuthorizationRequest authorizationRequest = requestRepository.loadAuthorizationRequest(request); + if (authorizationRequest == null) { + throw new OAuth2AuthenticationException(); + } + return authorizationRequest; + } +} From 34ad28277f69e225ad84e040ccbb0459d49a28a1 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Wed, 19 Feb 2025 21:14:08 +0900 Subject: [PATCH 15/25] =?UTF-8?q?feat:=20AuthenticationFilter=20=EC=97=90?= =?UTF-8?q?=20AuthenticationManager=20=EB=A5=BC=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20Config=20=EB=A5=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/app/OAuth2Config.java | 84 ++++++++++++++++++- .../java/nextstep/app/SecurityConfig.java | 51 +++++++---- .../java/nextstep/app/ui/LoginController.java | 11 +-- .../userinfo/DefaultOAuth2UserService.java | 26 ------ .../nextstep/oauth2/userinfo/OAuth2User.java | 8 ++ ...th2AuthorizationRequestRedirectFilter.java | 4 +- .../OAuth2AuthorizedClientDao.java | 10 +++ .../OAuth2AuthorizedClientRepository.java | 4 + .../BasicAuthenticationFilter.java | 8 +- .../UsernamePasswordAuthenticationFilter.java | 84 +++++-------------- 10 files changed, 169 insertions(+), 121 deletions(-) delete mode 100644 src/main/java/nextstep/oauth2/userinfo/DefaultOAuth2UserService.java diff --git a/src/main/java/nextstep/app/OAuth2Config.java b/src/main/java/nextstep/app/OAuth2Config.java index fd22d38..6c14d20 100644 --- a/src/main/java/nextstep/app/OAuth2Config.java +++ b/src/main/java/nextstep/app/OAuth2Config.java @@ -1,16 +1,94 @@ package nextstep.app; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.oauth2.authentication.provider.OAuth2LoginAuthenticationProvider; +import nextstep.oauth2.profile.OAuth2ProfileUser; import nextstep.oauth2.registration.ClientRegistrationRepository; import nextstep.oauth2.registration.OAuth2ClientProperties; +import nextstep.oauth2.userinfo.OAuth2User; +import nextstep.oauth2.userinfo.OAuth2UserRequest; +import nextstep.oauth2.userinfo.OAuth2UserService; +import nextstep.oauth2.web.authorizedclient.OAuth2AuthorizedClientRepository; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.DaoAuthenticationProvider; +import nextstep.security.authentication.ProviderManager; +import nextstep.security.userdetails.UserDetailsService; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.Map; +import java.util.Set; @EnableConfigurationProperties(OAuth2ClientProperties.class) @Configuration public class OAuth2Config { - private final ClientRegistrationRepository registrationRepository; + private final RestTemplate rest = new RestTemplate(); + + @Bean + public ClientRegistrationRepository registrationRepository( + OAuth2ClientProperties oauth2ClientProperties + ) { + return oauth2ClientProperties.createClientRegistrationDao(); + } + + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository() { + return OAuth2AuthorizedClientRepository.getInstance(); + } + + @Bean + public AuthenticationManager authenticationManager( + UserDetailsService userDetailsService, + OAuth2UserService oAuth2UserService + ) { + return new ProviderManager(List.of( + new DaoAuthenticationProvider(userDetailsService), + new OAuth2LoginAuthenticationProvider(oAuth2UserService) + )); + } + + @Bean + public OAuth2UserService oauth2UserService( + MemberRepository memberRepository + ) { + return new OAuth2UserService() { + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) { + final String userNameAttributeName = userRequest.clientRegistration() + .providerDetails().userInfoEndpoint().userNameAttributeName(); + final Map attributes = exchangeAttributes(userRequest); + final OAuth2ProfileUser profileUser = OAuth2ProfileUser.of( + userRequest.clientRegistration().registrationId(), + attributes + ); + final Set authorities = memberRepository.findByEmail( + attributes.get(userNameAttributeName).toString() + ).orElseGet( + () -> memberRepository.save(new Member( + profileUser.email(), "", profileUser.name(), + profileUser.imageUrl(), Set.of("USER") + )) + ).getRoles(); + return OAuth2User.of(authorities, attributes, userNameAttributeName); + } - public OAuth2Config(OAuth2ClientProperties oauth2ClientProperties) { - this.registrationRepository = oauth2ClientProperties.createClientRegistrationDao(); + private Map exchangeAttributes(OAuth2UserRequest userRequest) { + final HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + userRequest.accessToken().token()); + return rest.exchange( + userRequest.clientRegistration().providerDetails().userInfoEndpoint().uri(), + HttpMethod.GET, + new HttpEntity<>(headers), + Map.class + ).getBody(); + } + }; } } diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index a1e4d6c..445db7f 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -2,15 +2,25 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import nextstep.oauth2.registration.ClientRegistrationRepository; +import nextstep.oauth2.web.OAuth2AuthorizationRequestRedirectFilter; +import nextstep.oauth2.web.OAuth2LoginAuthenticationFilter; +import nextstep.oauth2.web.authorizedclient.OAuth2AuthorizedClientRepository; import nextstep.security.access.AnyRequestMatcher; import nextstep.security.access.MvcRequestMatcher; import nextstep.security.access.RequestMatcherEntry; 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.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; @@ -31,15 +41,11 @@ @Configuration public class SecurityConfig { - private final MemberRepository memberRepository; - - public SecurityConfig(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } - @Bean - public DelegatingFilterProxy delegatingFilterProxy() { - return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain()))); + public DelegatingFilterProxy delegatingFilterProxy( + SecurityFilterChain securityFilterChain + ) { + return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain))); } @Bean @@ -53,13 +59,20 @@ public SecuredMethodInterceptor securedMethodInterceptor() { } @Bean - public SecurityFilterChain securityFilterChain() { + public SecurityFilterChain securityFilterChain( + AuthenticationManager authenticationManager, + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository, + RequestAuthorizationManager requestAuthorizationManager + ) { return new DefaultSecurityFilterChain( List.of( new SecurityContextHolderFilter(), - new UsernamePasswordAuthenticationFilter(userDetailsService()), - new BasicAuthenticationFilter(userDetailsService()), - new AuthorizationFilter(requestAuthorizationManager()) + new UsernamePasswordAuthenticationFilter(authenticationManager), + new BasicAuthenticationFilter(authenticationManager), + new OAuth2AuthorizationRequestRedirectFilter(clientRegistrationRepository), + new OAuth2LoginAuthenticationFilter(clientRegistrationRepository, authorizedClientRepository, authenticationManager), + new AuthorizationFilter(requestAuthorizationManager) ) ); } @@ -72,17 +85,21 @@ public RoleHierarchy roleHierarchy() { } @Bean - public RequestAuthorizationManager requestAuthorizationManager() { + public RequestAuthorizationManager requestAuthorizationManager( + RoleHierarchy roleHierarchy + ) { 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, "/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); } @Bean - public UserDetailsService userDetailsService() { + public UserDetailsService userDetailsService( + MemberRepository memberRepository + ) { return username -> { Member member = memberRepository.findByEmail(username) .orElseThrow(() -> new AuthenticationException("존재하지 않는 사용자입니다.")); diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 25d905b..f5f1e00 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -4,6 +4,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import nextstep.oauth2.userinfo.OAuth2User; import nextstep.security.authentication.Authentication; import nextstep.security.context.SecurityContextHolder; import org.springframework.stereotype.Controller; @@ -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()).userNameAttributeName(); + return (String) ((OAuth2User) authentication.getPrincipal()).attributes().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/oauth2/userinfo/DefaultOAuth2UserService.java b/src/main/java/nextstep/oauth2/userinfo/DefaultOAuth2UserService.java deleted file mode 100644 index d04fb5c..0000000 --- a/src/main/java/nextstep/oauth2/userinfo/DefaultOAuth2UserService.java +++ /dev/null @@ -1,26 +0,0 @@ -package nextstep.oauth2.userinfo; - -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.ResponseEntity; -import org.springframework.web.client.RestTemplate; - -import java.util.Map; - -import static java.util.Collections.emptySet; - -public class DefaultOAuth2UserService implements OAuth2UserService { - private final RestTemplate rest = new RestTemplate(); - - @Override - public OAuth2User loadUser(OAuth2UserRequest userRequest) { - final ResponseEntity> response = rest.exchange( - userRequest.requestEntity(), - new ParameterizedTypeReference<>() {} - ); - return new DefaultOAuth2User( - emptySet(), - userRequest.customAttributes(response.getBody()), - userRequest.userNameAttributeName() - ); - } -} diff --git a/src/main/java/nextstep/oauth2/userinfo/OAuth2User.java b/src/main/java/nextstep/oauth2/userinfo/OAuth2User.java index 9bea96a..0a3b4db 100644 --- a/src/main/java/nextstep/oauth2/userinfo/OAuth2User.java +++ b/src/main/java/nextstep/oauth2/userinfo/OAuth2User.java @@ -4,6 +4,14 @@ import java.util.Set; public interface OAuth2User { + static DefaultOAuth2User of( + Set authorities, + Map attributes, + String userNameAttributeName + ) { + return new DefaultOAuth2User(authorities, attributes, userNameAttributeName); + } + Set authorities(); Map attributes(); diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java index 820cf3a..c828f30 100644 --- a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java +++ b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java @@ -27,10 +27,10 @@ private OAuth2AuthorizationRequestRedirectFilter( this.OAuth2AuthorizationRequestRepository = oAuth2AuthorizationRequestRepository; } - public static OAuth2AuthorizationRequestRedirectFilter of( + public OAuth2AuthorizationRequestRedirectFilter( ClientRegistrationRepository clientRegistrationRepository ) { - return new OAuth2AuthorizationRequestRedirectFilter( + this( new OAuth2AuthorizationRequestResolver(clientRegistrationRepository, BASE_URI), OAuth2AuthorizationRequestDao.getInstance() ); diff --git a/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java index 1ff6693..3c6edda 100644 --- a/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java +++ b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java @@ -11,6 +11,12 @@ public class OAuth2AuthorizedClientDao implements OAuth2AuthorizedClientRepository { private static final String ATTRIBUTE_NAME = "AUTHORIZED_CLIENTS"; + private OAuth2AuthorizedClientDao() {} + + public static OAuth2AuthorizedClientDao getInstance() { + return SingletonHolder.INSTANCE; + } + @Override public OAuth2AuthorizedClient loadAuthorizedClient(String clientRegistrationId, Authentication principal, HttpServletRequest request) { return getAuthorizedClients(request).get(clientRegistrationId); @@ -42,4 +48,8 @@ private Map getAuthorizedClients(HttpServletRequ private Map getAuthorizedClients(HttpSession session) { return (Map) session.getAttribute(ATTRIBUTE_NAME); } + + private static class SingletonHolder { + private static final OAuth2AuthorizedClientDao INSTANCE = new OAuth2AuthorizedClientDao(); + } } diff --git a/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientRepository.java b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientRepository.java index 4682938..fe31679 100644 --- a/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientRepository.java +++ b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientRepository.java @@ -5,6 +5,10 @@ import nextstep.security.authentication.Authentication; public interface OAuth2AuthorizedClientRepository { + static OAuth2AuthorizedClientRepository getInstance() { + return OAuth2AuthorizedClientDao.getInstance(); + } + OAuth2AuthorizedClient loadAuthorizedClient( String clientRegistrationId, Authentication principal, diff --git a/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java index 9e60daf..442f7b2 100644 --- a/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java +++ b/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java @@ -6,7 +6,6 @@ import jakarta.servlet.http.HttpServletResponse; import nextstep.security.context.SecurityContext; import nextstep.security.context.SecurityContextHolder; -import nextstep.security.userdetails.UserDetailsService; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.util.StringUtils; @@ -15,17 +14,14 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; -import java.util.List; public class BasicAuthenticationFilter extends OncePerRequestFilter { public static final String AUTHENTICATION_SCHEME_BASIC = "Basic"; private final AuthenticationManager authenticationManager; - public BasicAuthenticationFilter(UserDetailsService userDetailsService) { - this.authenticationManager = new ProviderManager( - List.of(new DaoAuthenticationProvider(userDetailsService)) - ); + public BasicAuthenticationFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; } @Override diff --git a/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationFilter.java b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationFilter.java index 3c7d065..65e95c9 100644 --- a/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationFilter.java +++ b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationFilter.java @@ -1,84 +1,44 @@ 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 nextstep.security.access.MvcRequestMatcher; import nextstep.security.context.HttpSessionSecurityContextRepository; -import nextstep.security.context.SecurityContext; -import nextstep.security.context.SecurityContextHolder; -import nextstep.security.userdetails.UserDetailsService; import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.web.filter.GenericFilterBean; -import java.io.IOException; -import java.util.List; - -public class UsernamePasswordAuthenticationFilter extends GenericFilterBean { - private static final MvcRequestMatcher DEFAULT_MVC_REQUEST_MATCHER = new MvcRequestMatcher(HttpMethod.POST, "/login"); +public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; + private static final MvcRequestMatcher DEFAULT_MVC_REQUEST_MATCHER = new MvcRequestMatcher(HttpMethod.POST, "/login"); + private static final HttpSessionSecurityContextRepository securityContextRepository = HttpSessionSecurityContextRepository.getInstance(); - private final AuthenticationManager authenticationManager; - private final HttpSessionSecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository(); - - public UsernamePasswordAuthenticationFilter(UserDetailsService userDetailsService) { - this.authenticationManager = new ProviderManager( - List.of(new DaoAuthenticationProvider(userDetailsService)) - ); + public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) { + super(DEFAULT_MVC_REQUEST_MATCHER, authenticationManager); } @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); - } - - public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - if (!requiresAuthentication(request)) { - chain.doFilter(request, response); - return; - } - - try { - Authentication authenticationResult = attemptAuthentication(request); - if (authenticationResult == null) { - return; - } - - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(authenticationResult); - SecurityContextHolder.setContext(context); - this.securityContextRepository.saveContext(context, request, response); - - } catch (AuthenticationException e) { - SecurityContextHolder.clearContext(); - response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); - } - } - - private boolean requiresAuthentication(HttpServletRequest request) { - return DEFAULT_MVC_REQUEST_MATCHER.matches(request); - } - - private Authentication attemptAuthentication(HttpServletRequest request) { - String username = obtainUsername(request); - username = (username != null) ? username.trim() : ""; - String password = obtainPassword(request); - password = (password != null) ? password : ""; - - UsernamePasswordAuthenticationToken unauthenticated = UsernamePasswordAuthenticationToken.unauthenticated(username, password); - return this.authenticationManager.authenticate(unauthenticated); + protected Authentication attemptAuthentication( + HttpServletRequest request, + HttpServletResponse response + ) throws AuthenticationException { + return getAuthenticationManager().authenticate( + UsernamePasswordAuthenticationToken.unauthenticated( + obtainUsername(request), obtainPassword(request) + ) + ); } private String obtainUsername(HttpServletRequest request) { - return request.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY); + return trim(request.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY)); } private String obtainPassword(HttpServletRequest request) { - return request.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY); + return trim(request.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY)); + } + + private String trim(String str) { + return str == null || str.isBlank() + ? "" + : str.trim(); } } From fba333b71dcc13fd47641f5b474ba39c19658424 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Wed, 19 Feb 2025 21:27:29 +0900 Subject: [PATCH 16/25] =?UTF-8?q?test:=20FormLoginTest=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=B1=EA=B3=B5?= =?UTF-8?q?=EC=8B=9C=20OK=20=EA=B0=80=20=EC=95=84=EB=8B=88=EB=9D=BC=20302?= =?UTF-8?q?=20Redirect=20=EA=B0=80=20=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/nextstep/app/FormLoginTest.java | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/src/test/java/nextstep/app/FormLoginTest.java b/src/test/java/nextstep/app/FormLoginTest.java index 6301c1a..eb00f69 100644 --- a/src/test/java/nextstep/app/FormLoginTest.java +++ b/src/test/java/nextstep/app/FormLoginTest.java @@ -40,22 +40,24 @@ void setUp() { @DisplayName("로그인 성공") @Test void login_success() throws Exception { - ResultActions loginResponse = mockMvc.perform(post("/login") - .param("username", TEST_USER_MEMBER.getEmail()) - .param("password", TEST_USER_MEMBER.getPassword()) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions loginResponse = mockMvc.perform( + post("/login") + .param("username", TEST_USER_MEMBER.getEmail()) + .param("password", TEST_USER_MEMBER.getPassword()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); - loginResponse.andExpect(status().isOk()); + loginResponse.andExpect(status().is3xxRedirection()); } @DisplayName("로그인 실패 - 사용자 없음") @Test void login_fail_with_no_user() throws Exception { - ResultActions response = mockMvc.perform(post("/login") - .param("username", "none") - .param("password", "none") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions response = mockMvc.perform( + post("/login") + .param("username", "none") + .param("password", "none") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); response.andExpect(status().isUnauthorized()); @@ -64,10 +66,11 @@ void login_fail_with_no_user() throws Exception { @DisplayName("로그인 실패 - 비밀번호 불일치") @Test void login_fail_with_invalid_password() throws Exception { - ResultActions response = mockMvc.perform(post("/login") - .param("username", TEST_USER_MEMBER.getEmail()) - .param("password", "invalid") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions response = mockMvc.perform( + post("/login") + .param("username", TEST_USER_MEMBER.getEmail()) + .param("password", "invalid") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); response.andExpect(status().isUnauthorized()); @@ -78,18 +81,20 @@ void login_fail_with_invalid_password() throws Exception { void admin_login_after_members() throws Exception { MockHttpSession session = new MockHttpSession(); - ResultActions loginResponse = mockMvc.perform(post("/login") - .param("username", TEST_ADMIN_MEMBER.getEmail()) - .param("password", TEST_ADMIN_MEMBER.getPassword()) - .session(session) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions loginResponse = mockMvc.perform( + post("/login") + .param("username", TEST_ADMIN_MEMBER.getEmail()) + .param("password", TEST_ADMIN_MEMBER.getPassword()) + .session(session) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); - loginResponse.andExpect(status().isOk()); + loginResponse.andExpect(status().is3xxRedirection()); - ResultActions membersResponse = mockMvc.perform(get("/members") - .session(session) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions membersResponse = mockMvc.perform( + get("/members") + .session(session) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); membersResponse.andExpect(status().isOk()); @@ -100,18 +105,20 @@ void admin_login_after_members() throws Exception { void user_login_after_members() throws Exception { MockHttpSession session = new MockHttpSession(); - ResultActions loginResponse = mockMvc.perform(post("/login") - .param("username", TEST_USER_MEMBER.getEmail()) - .param("password", TEST_USER_MEMBER.getPassword()) - .session(session) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions loginResponse = mockMvc.perform( + post("/login") + .param("username", TEST_USER_MEMBER.getEmail()) + .param("password", TEST_USER_MEMBER.getPassword()) + .session(session) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); - loginResponse.andExpect(status().isOk()); + loginResponse.andExpect(status().is3xxRedirection()); - ResultActions membersResponse = mockMvc.perform(get("/members") - .session(session) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions membersResponse = mockMvc.perform( + get("/members") + .session(session) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); membersResponse.andExpect(status().isForbidden()); From 556d4c5376810d27a8d694e77335c4afbdfe1a9e Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Wed, 19 Feb 2025 21:32:35 +0900 Subject: [PATCH 17/25] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=8B=9C=20=EA=B3=B5=ED=86=B5=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20Member=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20Fixture=20=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 --- src/test/java/nextstep/app/BasicAuthTest.java | 51 ++++++++++--------- src/test/java/nextstep/app/Fixture.java | 18 +++++++ src/test/java/nextstep/app/FormLoginTest.java | 12 ++--- src/test/java/nextstep/app/SecuredTest.java | 25 +++++---- 4 files changed, 61 insertions(+), 45 deletions(-) create mode 100644 src/test/java/nextstep/app/Fixture.java diff --git a/src/test/java/nextstep/app/BasicAuthTest.java b/src/test/java/nextstep/app/BasicAuthTest.java index 2dea9bb..5dc6596 100644 --- a/src/test/java/nextstep/app/BasicAuthTest.java +++ b/src/test/java/nextstep/app/BasicAuthTest.java @@ -1,6 +1,5 @@ package nextstep.app; -import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -14,17 +13,16 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import java.util.Base64; -import java.util.Set; +import static nextstep.app.Fixture.TEST_ADMIN_MEMBER; +import static nextstep.app.Fixture.TEST_USER_MEMBER; +import static nextstep.app.Fixture.setUpMembers; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc class BasicAuthTest { - private final Member TEST_ADMIN_MEMBER = new Member("a@a.com", "password", "a", "", Set.of("ADMIN")); - private final Member TEST_USER_MEMBER = new Member("b@b.com", "password", "b", "", Set.of()); - @Autowired private MockMvc mockMvc; @@ -33,8 +31,7 @@ class BasicAuthTest { @BeforeEach void setUp() { - memberRepository.save(TEST_ADMIN_MEMBER); - memberRepository.save(TEST_USER_MEMBER); + setUpMembers(memberRepository); } @DisplayName("ADMIN 권한을 가진 사용자가 요청할 경우 모든 회원 정보를 조회할 수 있다.") @@ -42,9 +39,10 @@ void setUp() { void request_success_with_admin_user() throws Exception { String token = Base64.getEncoder().encodeToString((TEST_ADMIN_MEMBER.getEmail() + ":" + TEST_ADMIN_MEMBER.getPassword()).getBytes()); - ResultActions response = mockMvc.perform(get("/members") - .header("Authorization", "Basic " + token) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions response = mockMvc.perform( + get("/members") + .header("Authorization", "Basic " + token) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); response.andExpect(status().isOk()) @@ -56,9 +54,10 @@ void request_success_with_admin_user() throws Exception { void request_fail_with_general_user() throws Exception { String token = Base64.getEncoder().encodeToString((TEST_USER_MEMBER.getEmail() + ":" + TEST_USER_MEMBER.getPassword()).getBytes()); - ResultActions response = mockMvc.perform(get("/members") - .header("Authorization", "Basic " + token) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions response = mockMvc.perform( + get("/members") + .header("Authorization", "Basic " + token) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); response.andExpect(status().isForbidden()); @@ -69,9 +68,10 @@ void request_fail_with_general_user() throws Exception { void request_fail_with_no_user() throws Exception { String token = Base64.getEncoder().encodeToString(("none" + ":" + "none").getBytes()); - ResultActions response = mockMvc.perform(get("/members") - .header("Authorization", "Basic " + token) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions response = mockMvc.perform( + get("/members") + .header("Authorization", "Basic " + token) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); response.andExpect(status().isUnauthorized()); @@ -82,9 +82,10 @@ void request_fail_with_no_user() throws Exception { void request_fail_invalid_password() throws Exception { String token = Base64.getEncoder().encodeToString((TEST_ADMIN_MEMBER.getEmail() + ":" + "invalid").getBytes()); - ResultActions response = mockMvc.perform(get("/members") - .header("Authorization", "Basic " + token) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions response = mockMvc.perform( + get("/members") + .header("Authorization", "Basic " + token) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); response.andExpect(status().isUnauthorized()); @@ -95,9 +96,10 @@ void request_fail_invalid_password() throws Exception { void request_success_members_me() throws Exception { String token = Base64.getEncoder().encodeToString((TEST_ADMIN_MEMBER.getEmail() + ":" + TEST_ADMIN_MEMBER.getPassword()).getBytes()); - ResultActions response = mockMvc.perform(get("/members/me") - .header("Authorization", "Basic " + token) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions response = mockMvc.perform( + get("/members/me") + .header("Authorization", "Basic " + token) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); response.andExpect(status().isOk()) @@ -107,8 +109,9 @@ void request_success_members_me() throws Exception { @DisplayName("인증되지 않은 사용자는 자신의 정보를 조회할 수 없다.") @Test void request_fail_members_me() throws Exception { - ResultActions response = mockMvc.perform(get("/members/me") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions response = mockMvc.perform( + get("/members/me") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); response.andExpect(status().isUnauthorized()); diff --git a/src/test/java/nextstep/app/Fixture.java b/src/test/java/nextstep/app/Fixture.java new file mode 100644 index 0000000..e42c129 --- /dev/null +++ b/src/test/java/nextstep/app/Fixture.java @@ -0,0 +1,18 @@ +package nextstep.app; + +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; + +import java.util.Set; + +public final class Fixture { + public static final Member TEST_ADMIN_MEMBER = new Member("a@a.com", "password", "a", "", Set.of("ADMIN")); + public static final Member TEST_USER_MEMBER = new Member("b@b.com", "password", "b", "", Set.of()); + + private Fixture() {} + + public static void setUpMembers(MemberRepository memberRepository) { + memberRepository.save(TEST_ADMIN_MEMBER); + memberRepository.save(TEST_USER_MEMBER); + } +} diff --git a/src/test/java/nextstep/app/FormLoginTest.java b/src/test/java/nextstep/app/FormLoginTest.java index eb00f69..c9b291d 100644 --- a/src/test/java/nextstep/app/FormLoginTest.java +++ b/src/test/java/nextstep/app/FormLoginTest.java @@ -1,6 +1,5 @@ package nextstep.app; -import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -13,8 +12,9 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import java.util.Set; - +import static nextstep.app.Fixture.TEST_ADMIN_MEMBER; +import static nextstep.app.Fixture.TEST_USER_MEMBER; +import static nextstep.app.Fixture.setUpMembers; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -22,9 +22,6 @@ @SpringBootTest @AutoConfigureMockMvc class FormLoginTest { - private final Member TEST_ADMIN_MEMBER = new Member("a@a.com", "password", "a", "", Set.of("ADMIN")); - private final Member TEST_USER_MEMBER = new Member("b@b.com", "password", "b", "", Set.of()); - @Autowired private MockMvc mockMvc; @@ -33,8 +30,7 @@ class FormLoginTest { @BeforeEach void setUp() { - memberRepository.save(TEST_ADMIN_MEMBER); - memberRepository.save(TEST_USER_MEMBER); + setUpMembers(memberRepository); } @DisplayName("로그인 성공") diff --git a/src/test/java/nextstep/app/SecuredTest.java b/src/test/java/nextstep/app/SecuredTest.java index 9e672e5..1184622 100644 --- a/src/test/java/nextstep/app/SecuredTest.java +++ b/src/test/java/nextstep/app/SecuredTest.java @@ -1,6 +1,5 @@ package nextstep.app; -import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -14,8 +13,10 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import java.util.Base64; -import java.util.Set; +import static nextstep.app.Fixture.TEST_ADMIN_MEMBER; +import static nextstep.app.Fixture.TEST_USER_MEMBER; +import static nextstep.app.Fixture.setUpMembers; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -23,9 +24,6 @@ @SpringBootTest @AutoConfigureMockMvc class SecuredTest { - private final Member TEST_ADMIN_MEMBER = new Member("a@a.com", "password", "a", "", Set.of("ADMIN")); - private final Member TEST_USER_MEMBER = new Member("b@b.com", "password", "b", "", Set.of()); - @Autowired private MockMvc mockMvc; @@ -34,8 +32,7 @@ class SecuredTest { @BeforeEach void setUp() { - memberRepository.save(TEST_ADMIN_MEMBER); - memberRepository.save(TEST_USER_MEMBER); + setUpMembers(memberRepository); } @DisplayName("ADMIN 권한을 가진 사용자가 요청할 경우 모든 회원 정보를 조회할 수 있다.") @@ -43,9 +40,10 @@ void setUp() { void request_search_success_with_admin_user() throws Exception { String token = Base64.getEncoder().encodeToString((TEST_ADMIN_MEMBER.getEmail() + ":" + TEST_ADMIN_MEMBER.getPassword()).getBytes()); - ResultActions response = mockMvc.perform(get("/search") - .header("Authorization", "Basic " + token) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions response = mockMvc.perform( + get("/search") + .header("Authorization", "Basic " + token) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ).andDo(print()); response.andExpect(status().isOk()) @@ -57,9 +55,10 @@ void request_search_success_with_admin_user() throws Exception { void request_search_fail_with_general_user() throws Exception { String token = Base64.getEncoder().encodeToString((TEST_USER_MEMBER.getEmail() + ":" + TEST_USER_MEMBER.getPassword()).getBytes()); - ResultActions response = mockMvc.perform(get("/search") - .header("Authorization", "Basic " + token) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ResultActions response = mockMvc.perform( + get("/search") + .header("Authorization", "Basic " + token) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ).andDo(print()); response.andExpect(status().isForbidden()); From c0715a9f9979303ead51ae6341f4b30aba55bf9b Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Wed, 19 Feb 2025 22:11:34 +0900 Subject: [PATCH 18/25] =?UTF-8?q?test:=20OAuth2=20LoginRedirect=20?= =?UTF-8?q?=EC=97=90=20=EA=B4=80=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/oauth2/LoginRedirectFilterTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/test/java/nextstep/app/oauth2/LoginRedirectFilterTest.java diff --git a/src/test/java/nextstep/app/oauth2/LoginRedirectFilterTest.java b/src/test/java/nextstep/app/oauth2/LoginRedirectFilterTest.java new file mode 100644 index 0000000..38b0550 --- /dev/null +++ b/src/test/java/nextstep/app/oauth2/LoginRedirectFilterTest.java @@ -0,0 +1,31 @@ +package nextstep.app.oauth2; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc +class LoginRedirectFilterTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void github() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/oauth2/authorization/github")) + .andExpect(MockMvcResultMatchers.status().is3xxRedirection()); + } + + @Test + void google() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/oauth2/authorization/google")) + .andExpect(MockMvcResultMatchers.status().is3xxRedirection()); + } +} From b1f894f23ca3939659e603c149f1ef0bde86db69 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Thu, 20 Feb 2025 00:08:23 +0900 Subject: [PATCH 19/25] =?UTF-8?q?test:=20GitHub=20=EC=99=80=20Google=20?= =?UTF-8?q?=EC=9D=84=20Stub=20=EC=9D=84=20=ED=99=9C=EC=9A=A9=ED=95=B4=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=ED=95=98=EB=8A=94=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/app/domain/MemberRepository.java | 2 + .../InmemoryMemberRepository.java | 5 + .../OAuth2AuthorizedClientDao.java | 9 +- src/test/java/nextstep/app/Fixture.java | 1 + .../GithubAuthenticationFilterTest.java | 120 ++++++++++++++++++ .../GoogleAuthenticationFilterTest.java | 120 ++++++++++++++++++ .../app/oauth2/LoginRedirectFilterTest.java | 3 + 7 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 src/test/java/nextstep/app/oauth2/GithubAuthenticationFilterTest.java create mode 100644 src/test/java/nextstep/app/oauth2/GoogleAuthenticationFilterTest.java diff --git a/src/main/java/nextstep/app/domain/MemberRepository.java b/src/main/java/nextstep/app/domain/MemberRepository.java index d2aacaf..8903663 100644 --- a/src/main/java/nextstep/app/domain/MemberRepository.java +++ b/src/main/java/nextstep/app/domain/MemberRepository.java @@ -9,4 +9,6 @@ public interface MemberRepository { List findAll(); Member save(Member member); + + void clear(); } diff --git a/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java b/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java index bd6d10f..4b82a51 100644 --- a/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java +++ b/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java @@ -29,4 +29,9 @@ public Member save(Member member) { members.put(member.getEmail(), member); return member; } + + @Override + public void clear() { + members.clear(); + } } diff --git a/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java index 3c6edda..554a567 100644 --- a/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java +++ b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java @@ -39,14 +39,17 @@ public void removeAuthorizedClient(String clientRegistrationId, Authentication p private Map getAuthorizedClients(HttpServletRequest request) { final HttpSession session = request.getSession(false); - return session == null ? - new HashMap<>() + return session == null + ? new HashMap<>() : getAuthorizedClients(session); } @SuppressWarnings("unchecked") private Map getAuthorizedClients(HttpSession session) { - return (Map) session.getAttribute(ATTRIBUTE_NAME); + var authorizedClients = session.getAttribute(ATTRIBUTE_NAME); + return authorizedClients == null + ? new HashMap<>() + : (Map) authorizedClients; } private static class SingletonHolder { diff --git a/src/test/java/nextstep/app/Fixture.java b/src/test/java/nextstep/app/Fixture.java index e42c129..2846ec7 100644 --- a/src/test/java/nextstep/app/Fixture.java +++ b/src/test/java/nextstep/app/Fixture.java @@ -12,6 +12,7 @@ public final class Fixture { private Fixture() {} public static void setUpMembers(MemberRepository memberRepository) { + memberRepository.clear(); memberRepository.save(TEST_ADMIN_MEMBER); memberRepository.save(TEST_USER_MEMBER); } diff --git a/src/test/java/nextstep/app/oauth2/GithubAuthenticationFilterTest.java b/src/test/java/nextstep/app/oauth2/GithubAuthenticationFilterTest.java new file mode 100644 index 0000000..8044f72 --- /dev/null +++ b/src/test/java/nextstep/app/oauth2/GithubAuthenticationFilterTest.java @@ -0,0 +1,120 @@ +package nextstep.app.oauth2; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +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; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static nextstep.oauth2.web.OAuth2ParameterNames.ACCESS_TOKEN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureWireMock(port = 8089) +class GithubAuthenticationFilterTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private MemberRepository memberRepository; + + static Stream userProvider() { + return UserStub.list().stream(); + } + + @DisplayName("GitHub 인증 기능 테스트") + @ParameterizedTest + @MethodSource("userProvider") + void authenticationFilterWithState(UserStub user) throws Exception { + stub(user); + + final MockHttpSession session = new MockHttpSession(); + final String state = mockMvc.perform(MockMvcRequestBuilders.get("/oauth2/authorization/github").session(session)) + .andExpect(MockMvcResultMatchers.status().is3xxRedirection()) + .andReturn().getResponse().getHeader(HttpHeaders.LOCATION).split("&state=")[1]; + + mockMvc.perform(MockMvcRequestBuilders.get( + "/login/oauth2/code/github?code=" + user.code + "&state=" + state + ).session(session)).andDo(print()) + .andExpect(MockMvcResultMatchers.status().is3xxRedirection()) + .andExpect(MockMvcResultMatchers.redirectedUrl("/")); + + final Member savedMember = memberRepository.findByEmail(user.email).get(); + assertThat(savedMember).isNotNull(); + assertThat(savedMember.getEmail()).isEqualTo(user.email); + assertThat(savedMember.getName()).isEqualTo(user.name); + } + + private void stub(UserStub user) throws JsonProcessingException { + stubForAccessToken(user); + stubForUser(user); + } + + private void stubForAccessToken(UserStub user) throws JsonProcessingException { + stubFor(post( + urlEqualTo("/login/oauth/access_token") + ).willReturn( + aResponse().withHeader( + HttpHeaders.CONTENT_TYPE, "application/json" + ).withBody(new ObjectMapper().writeValueAsString(Map.of( + ACCESS_TOKEN, user.accessToken, + "token_type", "bearer" + ))) + )); + } + + private void stubForUser(UserStub user) throws JsonProcessingException { + stubFor(get(urlEqualTo("/user")).withHeader( + "Authorization", equalTo("Bearer " + user.accessToken) + ).willReturn( + aResponse().withHeader( + HttpHeaders.CONTENT_TYPE, "application/json" + ).withBody(new ObjectMapper().writeValueAsString(Map.of( + "email", user.email, + "name", user.name, + "avatar_url", user.avatarUrl + ))) + )); + } + + private record UserStub( + String code, + String accessToken, + String email, + String name, + String avatarUrl + ) { + private static final UserStub USER_B = new UserStub("b", "b_access_token", "b@b.com", "b", "b_avatar_url"); + private static final UserStub USER_C = new UserStub("c", "c_access_token", "c@c.com", "c", "c_avatar_url"); + + private static List list() { + return List.of(USER_B, USER_C); + } + } +} diff --git a/src/test/java/nextstep/app/oauth2/GoogleAuthenticationFilterTest.java b/src/test/java/nextstep/app/oauth2/GoogleAuthenticationFilterTest.java new file mode 100644 index 0000000..1047eae --- /dev/null +++ b/src/test/java/nextstep/app/oauth2/GoogleAuthenticationFilterTest.java @@ -0,0 +1,120 @@ +package nextstep.app.oauth2; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +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; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static nextstep.oauth2.web.OAuth2ParameterNames.ACCESS_TOKEN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureWireMock(port = 8089) +class GoogleAuthenticationFilterTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private MemberRepository memberRepository; + + static Stream userProvider() { + return UserStub.list().stream(); + } + + @DisplayName("Google 인증 기능 테스트") + @ParameterizedTest + @MethodSource("userProvider") + void authenticationFilterWithState(UserStub user) throws Exception { + stub(user); + + final MockHttpSession session = new MockHttpSession(); + final String state = mockMvc.perform(MockMvcRequestBuilders.get("/oauth2/authorization/google").session(session)) + .andExpect(MockMvcResultMatchers.status().is3xxRedirection()) + .andReturn().getResponse().getHeader(HttpHeaders.LOCATION).split("&state=")[1]; + + mockMvc.perform(MockMvcRequestBuilders.get( + "/login/oauth2/code/google?code=" + user.code + "&state=" + state + ).session(session)).andDo(print()) + .andExpect(MockMvcResultMatchers.status().is3xxRedirection()) + .andExpect(MockMvcResultMatchers.redirectedUrl("/")); + + final Member savedMember = memberRepository.findByEmail(user.email).get(); + assertThat(savedMember).isNotNull(); + assertThat(savedMember.getEmail()).isEqualTo(user.email); + assertThat(savedMember.getName()).isEqualTo(user.name); + } + + private void stub(UserStub user) throws JsonProcessingException { + stubForAccessToken(user); + stubForUser(user); + } + + private void stubForAccessToken(UserStub user) throws JsonProcessingException { + stubFor(post( + urlEqualTo("/o/oauth2/token") + ).willReturn( + aResponse().withHeader( + HttpHeaders.CONTENT_TYPE, "application/json" + ).withBody(new ObjectMapper().writeValueAsString(Map.of( + ACCESS_TOKEN, user.accessToken, + "token_type", "bearer" + ))) + )); + } + + private void stubForUser(UserStub user) throws JsonProcessingException { + stubFor(get(urlEqualTo("/oauth2/v1/userinfo")).withHeader( + "Authorization", equalTo("Bearer " + user.accessToken) + ).willReturn( + aResponse().withHeader( + HttpHeaders.CONTENT_TYPE, "application/json" + ).withBody(new ObjectMapper().writeValueAsString(Map.of( + "email", user.email, + "name", user.name, + "picture", user.picture + ))) + )); + } + + private record UserStub( + String code, + String accessToken, + String email, + String name, + String picture + ) { + private static final UserStub USER_B = new UserStub("b", "b_access_token", "b@b.com", "b", "b_avatar_url"); + private static final UserStub USER_C = new UserStub("c", "c_access_token", "c@c.com", "c", "c_avatar_url"); + + private static List list() { + return List.of(USER_B, USER_C); + } + } +} diff --git a/src/test/java/nextstep/app/oauth2/LoginRedirectFilterTest.java b/src/test/java/nextstep/app/oauth2/LoginRedirectFilterTest.java index 38b0550..20140ef 100644 --- a/src/test/java/nextstep/app/oauth2/LoginRedirectFilterTest.java +++ b/src/test/java/nextstep/app/oauth2/LoginRedirectFilterTest.java @@ -1,5 +1,6 @@ package nextstep.app.oauth2; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -17,12 +18,14 @@ class LoginRedirectFilterTest { @Autowired private MockMvc mockMvc; + @DisplayName("GitHub OAuth2 로그인 리다이렉트에 관한 테스트") @Test void github() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/oauth2/authorization/github")) .andExpect(MockMvcResultMatchers.status().is3xxRedirection()); } + @DisplayName("Google OAuth2 로그인 리다이렉트에 관한 테스트") @Test void google() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/oauth2/authorization/google")) From bae948c342d1bd1423193f08078fa53c26eb8fcf Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Thu, 20 Feb 2025 02:34:45 +0900 Subject: [PATCH 20/25] =?UTF-8?q?test:=20LoginToken=20=EC=9D=84=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=EB=B0=9B=EC=9D=84=20=EC=8B=9C,=20user=20code?= =?UTF-8?q?=20=EA=B0=80=20=EC=9D=BC=EC=B9=98=ED=95=98=EB=8A=94=EC=A7=80=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=ED=95=98=EB=8A=94=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth2/endpoint/dto/OAuth2AuthorizationResponse.java | 2 +- .../nextstep/app/oauth2/GithubAuthenticationFilterTest.java | 3 +++ .../nextstep/app/oauth2/GoogleAuthenticationFilterTest.java | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationResponse.java b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationResponse.java index ea5ee68..d4d3b53 100644 --- a/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationResponse.java +++ b/src/main/java/nextstep/oauth2/endpoint/dto/OAuth2AuthorizationResponse.java @@ -1,7 +1,7 @@ package nextstep.oauth2.endpoint.dto; public record OAuth2AuthorizationResponse( - String code, String redirectUri, + String code, String state ) {} diff --git a/src/test/java/nextstep/app/oauth2/GithubAuthenticationFilterTest.java b/src/test/java/nextstep/app/oauth2/GithubAuthenticationFilterTest.java index 8044f72..8a106a2 100644 --- a/src/test/java/nextstep/app/oauth2/GithubAuthenticationFilterTest.java +++ b/src/test/java/nextstep/app/oauth2/GithubAuthenticationFilterTest.java @@ -23,6 +23,7 @@ import java.util.stream.Stream; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.containing; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.post; @@ -79,6 +80,8 @@ private void stub(UserStub user) throws JsonProcessingException { private void stubForAccessToken(UserStub user) throws JsonProcessingException { stubFor(post( urlEqualTo("/login/oauth/access_token") + ).withRequestBody( + containing("code=" + user.code) ).willReturn( aResponse().withHeader( HttpHeaders.CONTENT_TYPE, "application/json" diff --git a/src/test/java/nextstep/app/oauth2/GoogleAuthenticationFilterTest.java b/src/test/java/nextstep/app/oauth2/GoogleAuthenticationFilterTest.java index 1047eae..59af50c 100644 --- a/src/test/java/nextstep/app/oauth2/GoogleAuthenticationFilterTest.java +++ b/src/test/java/nextstep/app/oauth2/GoogleAuthenticationFilterTest.java @@ -23,6 +23,7 @@ import java.util.stream.Stream; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.containing; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.post; @@ -79,6 +80,8 @@ private void stub(UserStub user) throws JsonProcessingException { private void stubForAccessToken(UserStub user) throws JsonProcessingException { stubFor(post( urlEqualTo("/o/oauth2/token") + ).withRequestBody( + containing("code=" + user.code) ).willReturn( aResponse().withHeader( HttpHeaders.CONTENT_TYPE, "application/json" From b6765369589d7423e66bfbb9e193b9a9cfe44691 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Thu, 20 Feb 2025 21:29:55 +0900 Subject: [PATCH 21/25] =?UTF-8?q?refactor:=20Dao=20=EB=8C=80=EC=8B=A0?= =?UTF-8?q?=EC=97=90,=20Repository=20=EC=9D=98=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=EB=B0=A9=EB=B2=95=EC=9D=84=20=EC=9C=A0=EC=B6=94=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - https://github.com/spring-projects/spring-security/blob/main/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.java - https://github.com/spring-projects/spring-security/blob/main/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java - https://github.com/spring-projects/spring-security/blob/main/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/InMemoryClientRegistrationRepository.java --- ...Dao.java => InMemoryClientRegistrationRepository.java} | 4 ++-- .../oauth2/registration/OAuth2ClientProperties.java | 4 ++-- .../web/OAuth2AuthorizationRequestRedirectFilter.java | 4 ++-- .../oauth2/web/OAuth2LoginAuthenticationFilter.java | 4 ++-- ... HttpSessionOAuth2AuthorizationRequestRepository.java} | 8 ++++---- ...a => HttpSessionOAuth2AuthorizedClientRepository.java} | 8 ++++---- .../OAuth2AuthorizedClientRepository.java | 2 +- 7 files changed, 17 insertions(+), 17 deletions(-) rename src/main/java/nextstep/oauth2/registration/{ClientRegistrationDao.java => InMemoryClientRegistrationRepository.java} (76%) rename src/main/java/nextstep/oauth2/web/authorizationrequest/{OAuth2AuthorizationRequestDao.java => HttpSessionOAuth2AuthorizationRequestRepository.java} (85%) rename src/main/java/nextstep/oauth2/web/authorizedclient/{OAuth2AuthorizedClientDao.java => HttpSessionOAuth2AuthorizedClientRepository.java} (84%) diff --git a/src/main/java/nextstep/oauth2/registration/ClientRegistrationDao.java b/src/main/java/nextstep/oauth2/registration/InMemoryClientRegistrationRepository.java similarity index 76% rename from src/main/java/nextstep/oauth2/registration/ClientRegistrationDao.java rename to src/main/java/nextstep/oauth2/registration/InMemoryClientRegistrationRepository.java index e3c82be..fc0a2de 100644 --- a/src/main/java/nextstep/oauth2/registration/ClientRegistrationDao.java +++ b/src/main/java/nextstep/oauth2/registration/InMemoryClientRegistrationRepository.java @@ -6,10 +6,10 @@ import java.util.Map; @Repository -public final class ClientRegistrationDao implements ClientRegistrationRepository { +public final class InMemoryClientRegistrationRepository implements ClientRegistrationRepository { private final Map registrations; - public ClientRegistrationDao(Map registrations) { + public InMemoryClientRegistrationRepository(Map registrations) { this.registrations = registrations; } diff --git a/src/main/java/nextstep/oauth2/registration/OAuth2ClientProperties.java b/src/main/java/nextstep/oauth2/registration/OAuth2ClientProperties.java index 29a6f99..0e5a673 100644 --- a/src/main/java/nextstep/oauth2/registration/OAuth2ClientProperties.java +++ b/src/main/java/nextstep/oauth2/registration/OAuth2ClientProperties.java @@ -11,8 +11,8 @@ public record OAuth2ClientProperties( Map provider, Map registration ) { - public ClientRegistrationDao createClientRegistrationDao() { - return new ClientRegistrationDao(getClientRegistration()); + public InMemoryClientRegistrationRepository createClientRegistrationDao() { + return new InMemoryClientRegistrationRepository(getClientRegistration()); } private Map getClientRegistration() { diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java index c828f30..ade7e19 100644 --- a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java +++ b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java @@ -6,7 +6,7 @@ import jakarta.servlet.http.HttpServletResponse; import nextstep.oauth2.endpoint.dto.OAuth2AuthorizationRequest; import nextstep.oauth2.registration.ClientRegistrationRepository; -import nextstep.oauth2.web.authorizationrequest.OAuth2AuthorizationRequestDao; +import nextstep.oauth2.web.authorizationrequest.HttpSessionOAuth2AuthorizationRequestRepository; import nextstep.oauth2.web.authorizationrequest.OAuth2AuthorizationRequestRepository; import nextstep.oauth2.web.authorizationrequest.OAuth2AuthorizationRequestResolver; import org.springframework.web.filter.OncePerRequestFilter; @@ -32,7 +32,7 @@ public OAuth2AuthorizationRequestRedirectFilter( ) { this( new OAuth2AuthorizationRequestResolver(clientRegistrationRepository, BASE_URI), - OAuth2AuthorizationRequestDao.getInstance() + HttpSessionOAuth2AuthorizationRequestRepository.getInstance() ); } diff --git a/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java b/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java index 5294f86..6e5b8be 100644 --- a/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java +++ b/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java @@ -10,7 +10,7 @@ import nextstep.oauth2.exception.OAuth2AuthenticationException; import nextstep.oauth2.registration.ClientRegistration; import nextstep.oauth2.registration.ClientRegistrationRepository; -import nextstep.oauth2.web.authorizationrequest.OAuth2AuthorizationRequestDao; +import nextstep.oauth2.web.authorizationrequest.HttpSessionOAuth2AuthorizationRequestRepository; import nextstep.oauth2.web.authorizationrequest.OAuth2AuthorizationRequestRepository; import nextstep.oauth2.web.authorizedclient.OAuth2AuthorizedClient; import nextstep.oauth2.web.authorizedclient.OAuth2AuthorizedClientRepository; @@ -27,7 +27,7 @@ public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private static final String BASE_URI = "/login/oauth2/code/"; - private static final OAuth2AuthorizationRequestRepository requestRepository = OAuth2AuthorizationRequestDao.getInstance(); + private static final OAuth2AuthorizationRequestRepository requestRepository = HttpSessionOAuth2AuthorizationRequestRepository.getInstance(); private final ClientRegistrationRepository registrationRepository; private final OAuth2AuthorizedClientRepository clientRepository; diff --git a/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestDao.java b/src/main/java/nextstep/oauth2/web/authorizationrequest/HttpSessionOAuth2AuthorizationRequestRepository.java similarity index 85% rename from src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestDao.java rename to src/main/java/nextstep/oauth2/web/authorizationrequest/HttpSessionOAuth2AuthorizationRequestRepository.java index 4d863a4..b080d2b 100644 --- a/src/main/java/nextstep/oauth2/web/authorizationrequest/OAuth2AuthorizationRequestDao.java +++ b/src/main/java/nextstep/oauth2/web/authorizationrequest/HttpSessionOAuth2AuthorizationRequestRepository.java @@ -7,12 +7,12 @@ import nextstep.oauth2.exception.UnmatchedStateException; import nextstep.oauth2.web.OAuth2ParameterNames; -public class OAuth2AuthorizationRequestDao implements OAuth2AuthorizationRequestRepository { +public class HttpSessionOAuth2AuthorizationRequestRepository implements OAuth2AuthorizationRequestRepository { private final String ATTRIBUTE_NAME = "AUTHORIZATION_REQUEST"; - private OAuth2AuthorizationRequestDao() {} + private HttpSessionOAuth2AuthorizationRequestRepository() {} - public static OAuth2AuthorizationRequestDao getInstance() { + public static HttpSessionOAuth2AuthorizationRequestRepository getInstance() { return SingletonHolder.INSTANCE; } @@ -63,6 +63,6 @@ private OAuth2AuthorizationRequest getAuthorizationRequest(HttpServletRequest re } private static class SingletonHolder { - private static final OAuth2AuthorizationRequestDao INSTANCE = new OAuth2AuthorizationRequestDao(); + private static final HttpSessionOAuth2AuthorizationRequestRepository INSTANCE = new HttpSessionOAuth2AuthorizationRequestRepository(); } } diff --git a/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java b/src/main/java/nextstep/oauth2/web/authorizedclient/HttpSessionOAuth2AuthorizedClientRepository.java similarity index 84% rename from src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java rename to src/main/java/nextstep/oauth2/web/authorizedclient/HttpSessionOAuth2AuthorizedClientRepository.java index 554a567..a9c06c3 100644 --- a/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientDao.java +++ b/src/main/java/nextstep/oauth2/web/authorizedclient/HttpSessionOAuth2AuthorizedClientRepository.java @@ -8,12 +8,12 @@ import java.util.HashMap; import java.util.Map; -public class OAuth2AuthorizedClientDao implements OAuth2AuthorizedClientRepository { +public class HttpSessionOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository { private static final String ATTRIBUTE_NAME = "AUTHORIZED_CLIENTS"; - private OAuth2AuthorizedClientDao() {} + private HttpSessionOAuth2AuthorizedClientRepository() {} - public static OAuth2AuthorizedClientDao getInstance() { + public static HttpSessionOAuth2AuthorizedClientRepository getInstance() { return SingletonHolder.INSTANCE; } @@ -53,6 +53,6 @@ private Map getAuthorizedClients(HttpSession ses } private static class SingletonHolder { - private static final OAuth2AuthorizedClientDao INSTANCE = new OAuth2AuthorizedClientDao(); + private static final HttpSessionOAuth2AuthorizedClientRepository INSTANCE = new HttpSessionOAuth2AuthorizedClientRepository(); } } diff --git a/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientRepository.java b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientRepository.java index fe31679..69f12bf 100644 --- a/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientRepository.java +++ b/src/main/java/nextstep/oauth2/web/authorizedclient/OAuth2AuthorizedClientRepository.java @@ -6,7 +6,7 @@ public interface OAuth2AuthorizedClientRepository { static OAuth2AuthorizedClientRepository getInstance() { - return OAuth2AuthorizedClientDao.getInstance(); + return HttpSessionOAuth2AuthorizedClientRepository.getInstance(); } OAuth2AuthorizedClient loadAuthorizedClient( From 94b3a941a655b058f4499b646b89ae91b2894f75 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Thu, 20 Feb 2025 21:34:10 +0900 Subject: [PATCH 22/25] =?UTF-8?q?fix:=20=EB=A9=A4=EB=B2=84=EC=9D=98=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=EC=9D=84=20=EB=8C=80=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EB=A1=9C=20=EC=8B=9C=EC=9E=91=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=ED=95=9C=20=EC=98=A4=ED=83=80=EB=A5=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/OAuth2AuthorizationRequestRedirectFilter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java index ade7e19..9442836 100644 --- a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java +++ b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java @@ -17,14 +17,14 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt private static final String BASE_URI = "/oauth2/authorization/"; private final OAuth2AuthorizationRequestResolver authorizationRequestResolver; - private final OAuth2AuthorizationRequestRepository OAuth2AuthorizationRequestRepository; + private final OAuth2AuthorizationRequestRepository authorizationRequestRepository; private OAuth2AuthorizationRequestRedirectFilter( OAuth2AuthorizationRequestResolver authorizationRequestResolver, OAuth2AuthorizationRequestRepository oAuth2AuthorizationRequestRepository ) { this.authorizationRequestResolver = authorizationRequestResolver; - this.OAuth2AuthorizationRequestRepository = oAuth2AuthorizationRequestRepository; + this.authorizationRequestRepository = oAuth2AuthorizationRequestRepository; } public OAuth2AuthorizationRequestRedirectFilter( @@ -43,7 +43,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse filterChain.doFilter(request, response); return; } - OAuth2AuthorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); + authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); response.sendRedirect(authorizationRequest.authorizationRequestUri()); } } From 3db0484ca0f6f52aeda72b81e3422af5366c6b59 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Thu, 20 Feb 2025 21:54:52 +0900 Subject: [PATCH 23/25] =?UTF-8?q?fix:=20attemptAuthentication=20=EC=8B=9C?= =?UTF-8?q?=20removeAuthorizationRequest=20=EB=A5=BC=20=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth2/web/OAuth2LoginAuthenticationFilter.java | 12 +++++++----- ...pSessionOAuth2AuthorizationRequestRepository.java | 7 +++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java b/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java index 6e5b8be..2a66749 100644 --- a/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java +++ b/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java @@ -44,7 +44,8 @@ public OAuth2LoginAuthenticationFilter( @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { - final OAuth2LoginAuthenticationToken loginToken = getLoginToken(request); + final OAuth2AuthorizationRequest authorizationRequest = popAuthorizationRequest(request, response); + final OAuth2LoginAuthenticationToken loginToken = getLoginToken(request, authorizationRequest); final OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( loginToken.getClientRegistration(), loginToken.getPrincipal().toString(), @@ -59,11 +60,10 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ } public OAuth2LoginAuthenticationToken getLoginToken( - HttpServletRequest request + HttpServletRequest request, OAuth2AuthorizationRequest authorizationRequest ) { final MultiValueMap params = getParams(request); final ClientRegistration clientRegistration = getClientRegistration(request); - final OAuth2AuthorizationRequest authorizationRequest = getAuthorizationRequest(request); final OAuth2AuthorizationResponse authorizationResponse = new OAuth2AuthorizationResponse( clientRegistration.redirectUri(), params.getFirst(OAuth2ParameterNames.CODE), @@ -107,8 +107,10 @@ public ClientRegistration getClientRegistration(HttpServletRequest request) { return clientRegistration; } - public OAuth2AuthorizationRequest getAuthorizationRequest(HttpServletRequest request) { - final OAuth2AuthorizationRequest authorizationRequest = requestRepository.loadAuthorizationRequest(request); + public OAuth2AuthorizationRequest popAuthorizationRequest( + HttpServletRequest request, HttpServletResponse response + ) { + final OAuth2AuthorizationRequest authorizationRequest = requestRepository.removeAuthorizationRequest(request, response); if (authorizationRequest == null) { throw new OAuth2AuthenticationException(); } diff --git a/src/main/java/nextstep/oauth2/web/authorizationrequest/HttpSessionOAuth2AuthorizationRequestRepository.java b/src/main/java/nextstep/oauth2/web/authorizationrequest/HttpSessionOAuth2AuthorizationRequestRepository.java index b080d2b..45f315f 100644 --- a/src/main/java/nextstep/oauth2/web/authorizationrequest/HttpSessionOAuth2AuthorizationRequestRepository.java +++ b/src/main/java/nextstep/oauth2/web/authorizationrequest/HttpSessionOAuth2AuthorizationRequestRepository.java @@ -36,10 +36,9 @@ public void saveAuthorizationRequest( } @Override - public OAuth2AuthorizationRequest removeAuthorizationRequest - (HttpServletRequest request, - HttpServletResponse response - ) { + public OAuth2AuthorizationRequest removeAuthorizationRequest( + HttpServletRequest request, HttpServletResponse response + ) { final OAuth2AuthorizationRequest authorizationRequest = loadAuthorizationRequest(request); request.getSession().removeAttribute(ATTRIBUTE_NAME); return authorizationRequest; From 5d68d0dc640af2e1fd53ebca1cc7b8a5ed58d3ec Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Thu, 20 Feb 2025 22:13:58 +0900 Subject: [PATCH 24/25] =?UTF-8?q?refactor:=20OAuth2UserService=20=EC=9D=98?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=EC=9D=84=20DefaultOAuth2UserService=20?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=ED=95=B4=20=EA=B5=AC=ED=98=84=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=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/OAuth2Config.java | 51 ---------------- .../app/service/DefaultOAuth2UserService.java | 58 +++++++++++++++++++ 2 files changed, 58 insertions(+), 51 deletions(-) create mode 100644 src/main/java/nextstep/app/service/DefaultOAuth2UserService.java diff --git a/src/main/java/nextstep/app/OAuth2Config.java b/src/main/java/nextstep/app/OAuth2Config.java index 6c14d20..d8e6205 100644 --- a/src/main/java/nextstep/app/OAuth2Config.java +++ b/src/main/java/nextstep/app/OAuth2Config.java @@ -1,13 +1,8 @@ package nextstep.app; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; import nextstep.oauth2.authentication.provider.OAuth2LoginAuthenticationProvider; -import nextstep.oauth2.profile.OAuth2ProfileUser; import nextstep.oauth2.registration.ClientRegistrationRepository; import nextstep.oauth2.registration.OAuth2ClientProperties; -import nextstep.oauth2.userinfo.OAuth2User; -import nextstep.oauth2.userinfo.OAuth2UserRequest; import nextstep.oauth2.userinfo.OAuth2UserService; import nextstep.oauth2.web.authorizedclient.OAuth2AuthorizedClientRepository; import nextstep.security.authentication.AuthenticationManager; @@ -17,20 +12,12 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.web.client.RestTemplate; import java.util.List; -import java.util.Map; -import java.util.Set; @EnableConfigurationProperties(OAuth2ClientProperties.class) @Configuration public class OAuth2Config { - private final RestTemplate rest = new RestTemplate(); - @Bean public ClientRegistrationRepository registrationRepository( OAuth2ClientProperties oauth2ClientProperties @@ -53,42 +40,4 @@ public AuthenticationManager authenticationManager( new OAuth2LoginAuthenticationProvider(oAuth2UserService) )); } - - @Bean - public OAuth2UserService oauth2UserService( - MemberRepository memberRepository - ) { - return new OAuth2UserService() { - @Override - public OAuth2User loadUser(OAuth2UserRequest userRequest) { - final String userNameAttributeName = userRequest.clientRegistration() - .providerDetails().userInfoEndpoint().userNameAttributeName(); - final Map attributes = exchangeAttributes(userRequest); - final OAuth2ProfileUser profileUser = OAuth2ProfileUser.of( - userRequest.clientRegistration().registrationId(), - attributes - ); - final Set authorities = memberRepository.findByEmail( - attributes.get(userNameAttributeName).toString() - ).orElseGet( - () -> memberRepository.save(new Member( - profileUser.email(), "", profileUser.name(), - profileUser.imageUrl(), Set.of("USER") - )) - ).getRoles(); - return OAuth2User.of(authorities, attributes, userNameAttributeName); - } - - private Map exchangeAttributes(OAuth2UserRequest userRequest) { - final HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + userRequest.accessToken().token()); - return rest.exchange( - userRequest.clientRegistration().providerDetails().userInfoEndpoint().uri(), - HttpMethod.GET, - new HttpEntity<>(headers), - Map.class - ).getBody(); - } - }; - } } diff --git a/src/main/java/nextstep/app/service/DefaultOAuth2UserService.java b/src/main/java/nextstep/app/service/DefaultOAuth2UserService.java new file mode 100644 index 0000000..9e4bcad --- /dev/null +++ b/src/main/java/nextstep/app/service/DefaultOAuth2UserService.java @@ -0,0 +1,58 @@ +package nextstep.app.service; + +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.oauth2.profile.OAuth2ProfileUser; +import nextstep.oauth2.userinfo.OAuth2User; +import nextstep.oauth2.userinfo.OAuth2UserRequest; +import nextstep.oauth2.userinfo.OAuth2UserService; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; +import java.util.Set; + +@Service +public class DefaultOAuth2UserService implements OAuth2UserService { + private static final RestTemplate rest = new RestTemplate(); + + private final MemberRepository memberRepository; + + public DefaultOAuth2UserService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) { + final String userNameAttributeName = userRequest.clientRegistration() + .providerDetails().userInfoEndpoint().userNameAttributeName(); + final Map attributes = exchangeAttributes(userRequest); + final OAuth2ProfileUser profileUser = OAuth2ProfileUser.of( + userRequest.clientRegistration().registrationId(), + attributes + ); + final Set authorities = memberRepository.findByEmail( + attributes.get(userNameAttributeName).toString() + ).orElseGet( + () -> memberRepository.save(new Member( + profileUser.email(), "", profileUser.name(), + profileUser.imageUrl(), Set.of("USER") + )) + ).getRoles(); + return OAuth2User.of(authorities, attributes, userNameAttributeName); + } + + private Map exchangeAttributes(OAuth2UserRequest userRequest) { + final HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + userRequest.accessToken().token()); + return rest.exchange( + userRequest.clientRegistration().providerDetails().userInfoEndpoint().uri(), + HttpMethod.GET, + new HttpEntity<>(headers), + Map.class + ).getBody(); + } +} From 9a3a8f2b24afff7fad693f7b4be40c978e825371 Mon Sep 17 00:00:00 2001 From: "Pyro(JeongWan Gho)" Date: Thu, 20 Feb 2025 23:05:16 +0900 Subject: [PATCH 25/25] =?UTF-8?q?feat:=20OAuth2AuthorizationCodeAuthentica?= =?UTF-8?q?tionToken=20=EC=9D=98=20principal=20=EA=B3=BC=20credentials=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/spring-projects/spring-security/blob/7fc5d50adfe5546ca725f3259e4d65ee6083f7cd/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java#L112 --- .../token/OAuth2AuthorizationCodeAuthenticationToken.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/nextstep/oauth2/authentication/token/OAuth2AuthorizationCodeAuthenticationToken.java b/src/main/java/nextstep/oauth2/authentication/token/OAuth2AuthorizationCodeAuthenticationToken.java index 82145e7..9e01e80 100644 --- a/src/main/java/nextstep/oauth2/authentication/token/OAuth2AuthorizationCodeAuthenticationToken.java +++ b/src/main/java/nextstep/oauth2/authentication/token/OAuth2AuthorizationCodeAuthenticationToken.java @@ -52,12 +52,13 @@ public Set getAuthorities() { @Override public Object getCredentials() { - return null; + return (accessToken != null) ? accessToken.token() + : authorizationExchange.authorizationResponse().code(); } @Override public Object getPrincipal() { - return null; + return clientRegistration.clientId(); } @Override