Skip to content

[고정완] OAuth 2.0 리뷰 요청드립니다. #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d6a4354
feat: application.properties 를 통해 OAuth2Client 를 등록하는 Repository 를 구현
ghojeong Feb 12, 2025
dbbc497
feat: endpoint 에서 사용되는 dto를 정의
ghojeong Feb 15, 2025
d65d82c
feat: OAuth2AccessTokenResponse 를 가져오는 OAuth2AccessTokenResponseClien…
ghojeong Feb 15, 2025
68604ac
feat: OAuth2User 와 OAuth2UserService 구현
ghojeong Feb 15, 2025
f3581d5
feat: AuthenticationToken 과 AuthenticationProvider 를 구현
ghojeong Feb 15, 2025
b494728
feat: ProfileUser 를 정의함
ghojeong Feb 15, 2025
6e84278
feat: State 를 위해 8자리 랜덤 문자열을 생성하는 Generator 구현
ghojeong Feb 15, 2025
cde753e
feat: OAuth2AuthorizedClientRepository 를 구현
ghojeong Feb 15, 2025
c547968
feat: OAuth2AuthorizationRequestRepository 구현
ghojeong Feb 16, 2025
a518b2d
feat: OAuth2AuthorizationRequestResolver 구현
ghojeong Feb 16, 2025
f6203fb
feat: OAuth2AuthorizationRequestRedirectFilter 구현
ghojeong Feb 16, 2025
c31f070
refactor: SecurityContextRepository 인터페이스를 따로 선언
ghojeong Feb 16, 2025
a0b1a60
feat: AbstractAuthenticationProcessingFilter 구현
ghojeong Feb 16, 2025
3654c25
feat: OAuth2LoginAuthenticationFilter 구현
ghojeong Feb 16, 2025
34ad282
feat: AuthenticationFilter 에 AuthenticationManager 를 활용하도록 Config 를 수정
ghojeong Feb 19, 2025
fba333b
test: FormLoginTest 에서 로그인 성공시 OK 가 아니라 302 Redirect 가 되도록 테스트를 수정
ghojeong Feb 19, 2025
556d4c5
fix: 테스트 시 공통적으로 사용하는 Member 정보를 Fixture 로 분리
ghojeong Feb 19, 2025
c0715a9
test: OAuth2 LoginRedirect 에 관한 테스트 작성
ghojeong Feb 19, 2025
b1f894f
test: GitHub 와 Google 을 Stub 을 활용해 인증하는 테스트 작성
ghojeong Feb 19, 2025
bae948c
test: LoginToken 을 발급받을 시, user code 가 일치하는지 확인하는 테스트 추가
ghojeong Feb 19, 2025
b676536
refactor: Dao 대신에, Repository 의 구현방법을 유추할 수 있도록 네이밍
ghojeong Feb 20, 2025
94b3a94
fix: 멤버의 네이밍을 대문자로 시작하게 한 오타를 수정
ghojeong Feb 20, 2025
3db0484
fix: attemptAuthentication 시 removeAuthorizationRequest 를 하도록 로직 수정
ghojeong Feb 20, 2025
5d68d0d
refactor: OAuth2UserService 의 설정을 DefaultOAuth2UserService 를 통해 구현하는 …
ghojeong Feb 20, 2025
9a3a8f2
feat: OAuth2AuthorizationCodeAuthenticationToken 의 principal 과 creden…
ghojeong Feb 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/main/java/nextstep/app/OAuth2Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package nextstep.app;

import nextstep.oauth2.authentication.provider.OAuth2LoginAuthenticationProvider;
import nextstep.oauth2.registration.ClientRegistrationRepository;
import nextstep.oauth2.registration.OAuth2ClientProperties;
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 java.util.List;

@EnableConfigurationProperties(OAuth2ClientProperties.class)
@Configuration
public class OAuth2Config {
@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)
));
}
}
51 changes: 34 additions & 17 deletions src/main/java/nextstep/app/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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)
)
);
}
Expand All @@ -72,17 +85,21 @@ public RoleHierarchy roleHierarchy() {
}

@Bean
public RequestAuthorizationManager requestAuthorizationManager() {
public RequestAuthorizationManager requestAuthorizationManager(
RoleHierarchy roleHierarchy
) {
List<RequestMatcherEntry<AuthorizationManager>> 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("존재하지 않는 사용자입니다."));
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/nextstep/app/domain/MemberRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public interface MemberRepository {
List<Member> findAll();

Member save(Member member);

void clear();
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public Member save(Member member) {
members.put(member.getEmail(), member);
return member;
}

@Override
public void clear() {
members.clear();
}
}
58 changes: 58 additions & 0 deletions src/main/java/nextstep/app/service/DefaultOAuth2UserService.java
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영 잘 해주셨습니다!
DefaultOAuth2UserService에서 제공해주는 것 외에 커스텀이 필요한 부분은 어떤 부분인지 확인해보고 구현을 해보면
추후 스프링 시큐리티를 사용할 때 조금 더 색다르게 다가올 것 같네요 :)
어떤 부분이 있을 지 고민만 해보셔요~

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<String, Object> attributes = exchangeAttributes(userRequest);
final OAuth2ProfileUser profileUser = OAuth2ProfileUser.of(
userRequest.clientRegistration().registrationId(),
attributes
);
final Set<String> 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();
}
}
11 changes: 6 additions & 5 deletions src/main/java/nextstep/app/ui/LoginController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 "";
}

Expand All @@ -54,4 +55,4 @@ public String logout(HttpServletRequest request, HttpServletResponse response) {

return "redirect:/";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package nextstep.oauth2.authentication;

public record OAuth2AccessToken(String token) {}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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)
);
}
}
Original file line number Diff line number Diff line change
@@ -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<String> authorities;

public OAuth2AuthenticationToken(OAuth2User principal, Set<String> authorities) {
this.principal = principal;
this.authorities = authorities;
}

@Override
public Set<String> getAuthorities() {
return authorities;
}

@Override
public Object getCredentials() {
return null;
}

@Override
public Object getPrincipal() {
return principal;
}

@Override
public boolean isAuthenticated() {
return true;
}
}
Loading