diff --git a/build.gradle b/build.gradle index 555d570..32dba01 100644 --- a/build.gradle +++ b/build.gradle @@ -13,8 +13,8 @@ repositories { } dependencies { -// implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-webflux' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' diff --git a/src/main/java/nextstep/app/config/AuthConfig.java b/src/main/java/nextstep/app/config/AuthConfig.java index 61aebbc..5f42e8d 100644 --- a/src/main/java/nextstep/app/config/AuthConfig.java +++ b/src/main/java/nextstep/app/config/AuthConfig.java @@ -1,13 +1,17 @@ package nextstep.app.config; +import nextstep.app.infrastructure.GithubClient; import nextstep.security.access.AuthorizeRequestMatcherRegistry; import nextstep.security.access.matcher.AnyRequestMatcher; import nextstep.security.access.matcher.MvcRequestMatcher; import nextstep.security.authentication.AuthenticationManager; import nextstep.security.authentication.BasicAuthenticationFilter; +import nextstep.security.authentication.Oauth2AuthenticationProvider; +import nextstep.security.authentication.Oauth2LoginAuthenticationFilter; import nextstep.security.authentication.UsernamePasswordAuthenticationFilter; import nextstep.security.authentication.UsernamePasswordAuthenticationProvider; import nextstep.security.authorization.AuthorizationFilter; +import nextstep.security.authorization.Oauth2AuthorizationRequestRedirectFilter; import nextstep.security.authorization.SecurityContextHolderFilter; import nextstep.security.authorization.manager.RequestAuthorizationManager; import nextstep.security.config.DefaultSecurityFilterChain; @@ -26,16 +30,19 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.servlet.Filter; -import java.util.ArrayList; import java.util.List; @Configuration public class AuthConfig implements WebMvcConfigurer { + private static final String OAUTH2_REDIRECT_URL = "https://github.com/login/oauth/authorize?response_type=code&client_id=7fc956935c0618c560da&scope=read:user&redirect_uri=http://localhost:8080/oauth2/access"; + private final UserDetailsService userDetailsService; + private final GithubClient githubClient; - public AuthConfig(UserDetailsService userDetailsService) { + public AuthConfig(UserDetailsService userDetailsService, GithubClient githubClient) { this.userDetailsService = userDetailsService; + this.githubClient = githubClient; } @Bean @@ -44,18 +51,26 @@ public DelegatingFilterProxy securityFilterChainProxy() { } @Bean - public FilterChainProxy filterChainProxy() { - return new FilterChainProxy(List.of(securityFilterChainNew())); + public FilterChainProxy filterChainProxy(SecurityFilterChain securityFilterChain) { + return new FilterChainProxy(List.of(securityFilterChain)); } @Bean - public SecurityFilterChain securityFilterChainNew() { - List filters = new ArrayList<>(); - filters.add(new SecurityContextHolderFilter(securityContextRepository())); - filters.add(new UsernamePasswordAuthenticationFilter(authenticationManager(), securityContextRepository())); - filters.add(new BasicAuthenticationFilter(authenticationManager())); - filters.add(new ExceptionTranslateFilter(requestCache())); - filters.add(new AuthorizationFilter(authorizationManager())); + public SecurityFilterChain securityFilterChain() { + List filters = List.of( + new SecurityContextHolderFilter(securityContextRepository()), + new UsernamePasswordAuthenticationFilter(authenticationManager(), securityContextRepository()), + new BasicAuthenticationFilter(authenticationManager()), + new ExceptionTranslateFilter(requestCache()), + new Oauth2AuthorizationRequestRedirectFilter(new MvcRequestMatcher(HttpMethod.GET, "/oauth2/authorization/github"),OAUTH2_REDIRECT_URL), + new Oauth2LoginAuthenticationFilter( + new MvcRequestMatcher(HttpMethod.GET, "/oauth2/access"), + authenticationManager(), + securityContextRepository(), + "/members/authentication" + ), + new AuthorizationFilter(authorizationManager()) + ); return new DefaultSecurityFilterChain(AnyRequestMatcher.INSTANCE, filters); } @@ -79,9 +94,20 @@ public RequestAuthorizationManager authorizationManager() { return new RequestAuthorizationManager(requestMatcherRegistry); } + @Bean + public RequestAuthorizationManager oauth2authorizationManager() { + AuthorizeRequestMatcherRegistry requestMatcherRegistry = new AuthorizeRequestMatcherRegistry(); + requestMatcherRegistry + .matcher(new MvcRequestMatcher(HttpMethod.GET, "/oauth2/authorization/github")); + return new RequestAuthorizationManager(requestMatcherRegistry); + } + @Bean public AuthenticationManager authenticationManager() { - return new AuthenticationManager(new UsernamePasswordAuthenticationProvider(userDetailsService)); + return new AuthenticationManager( + new UsernamePasswordAuthenticationProvider(userDetailsService), + new Oauth2AuthenticationProvider(githubClient) + ); } } diff --git a/src/main/java/nextstep/app/config/WebClientConfig.java b/src/main/java/nextstep/app/config/WebClientConfig.java new file mode 100644 index 0000000..623f256 --- /dev/null +++ b/src/main/java/nextstep/app/config/WebClientConfig.java @@ -0,0 +1,18 @@ +package nextstep.app.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder() + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .build(); + } +} diff --git a/src/main/java/nextstep/app/infrastructure/GithubClient.java b/src/main/java/nextstep/app/infrastructure/GithubClient.java new file mode 100644 index 0000000..fc7c426 --- /dev/null +++ b/src/main/java/nextstep/app/infrastructure/GithubClient.java @@ -0,0 +1,47 @@ +package nextstep.app.infrastructure; + +import nextstep.app.infrastructure.dto.AccessRequest; +import nextstep.app.infrastructure.dto.AccessResponse; +import nextstep.app.infrastructure.dto.UserResponse; +import nextstep.security.oauth2user.BaseOauth2User; +import nextstep.security.oauth2user.OAuth2User; +import nextstep.security.oauth2user.Oauth2UserService; +import nextstep.security.exception.AuthenticationException; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.Set; + + +@Component +public class GithubClient implements Oauth2UserService { + + private final WebClient webClient; + + public GithubClient(WebClient webClient) { + this.webClient = webClient; + } + + @Override + public OAuth2User loadUser(String accessToken) throws AuthenticationException { + final AccessResponse accessResponse = webClient.post() + .uri("https://github.com/login/oauth/access_token") + .accept(MediaType.APPLICATION_JSON) + .body( + BodyInserters.fromValue(new AccessRequest(accessToken)) + ) + .retrieve() + .bodyToMono(AccessResponse.class) + .block(); + + final UserResponse userResponse = webClient.get() + .uri("https://api.github.com/user") + .header("Authorization", "Bearer " + accessResponse.getAccessToken()) + .retrieve() + .bodyToMono(UserResponse.class) + .block(); + return new BaseOauth2User(userResponse.getId(), Set.of()); + } +} diff --git a/src/main/java/nextstep/app/infrastructure/dto/AccessRequest.java b/src/main/java/nextstep/app/infrastructure/dto/AccessRequest.java new file mode 100644 index 0000000..f64b720 --- /dev/null +++ b/src/main/java/nextstep/app/infrastructure/dto/AccessRequest.java @@ -0,0 +1,37 @@ +package nextstep.app.infrastructure.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class AccessRequest { + @JsonProperty("client_id") + private String clientId = "7fc956935c0618c560da"; + @JsonProperty("client_secret") + private String clientSecret = "a3fd00e8f3146bf81e2c5b2ea328ccb8d330cd45"; + private String code; + @JsonProperty("redirect_uri") + private String redirectUri; + + public AccessRequest(String code) { + this.code = code; + } + + public String getClientId() { + return clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public String getCode() { + return code; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } +} diff --git a/src/main/java/nextstep/app/infrastructure/dto/AccessResponse.java b/src/main/java/nextstep/app/infrastructure/dto/AccessResponse.java new file mode 100644 index 0000000..cdf35ea --- /dev/null +++ b/src/main/java/nextstep/app/infrastructure/dto/AccessResponse.java @@ -0,0 +1,35 @@ +package nextstep.app.infrastructure.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class AccessResponse { + @JsonProperty("access_token") + private String accessToken; + private String scope; + @JsonProperty("token_type") + private String tokenType; + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public String getTokenType() { + return tokenType; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } +} diff --git a/src/main/java/nextstep/app/infrastructure/dto/UserResponse.java b/src/main/java/nextstep/app/infrastructure/dto/UserResponse.java new file mode 100644 index 0000000..89a902f --- /dev/null +++ b/src/main/java/nextstep/app/infrastructure/dto/UserResponse.java @@ -0,0 +1,13 @@ +package nextstep.app.infrastructure.dto; + +public class UserResponse { + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/src/main/java/nextstep/security/authentication/Oauth2Authentication.java b/src/main/java/nextstep/security/authentication/Oauth2Authentication.java new file mode 100644 index 0000000..c9e82c0 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/Oauth2Authentication.java @@ -0,0 +1,48 @@ +package nextstep.security.authentication; + +import java.util.Set; + +public class Oauth2Authentication implements Authentication { + + private final String id; + private final Set authorities; + private final boolean authenticated; + + public Oauth2Authentication( + String id, + Set authorities, + boolean authenticated + ) { + this.id = id; + this.authorities = authorities; + this.authenticated = authenticated; + } + + public static Oauth2Authentication ofRequest(String accessCode) { + return new Oauth2Authentication(accessCode, Set.of(), false); + } + + public static Authentication ofAuthenticated(String name, Set authorities) { + return new Oauth2Authentication(name, Set.of(), true); + } + + @Override + public Object getPrincipal() { + return id; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Set getAuthorities() { + return authorities; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } +} diff --git a/src/main/java/nextstep/security/authentication/Oauth2AuthenticationProvider.java b/src/main/java/nextstep/security/authentication/Oauth2AuthenticationProvider.java new file mode 100644 index 0000000..e4d546a --- /dev/null +++ b/src/main/java/nextstep/security/authentication/Oauth2AuthenticationProvider.java @@ -0,0 +1,24 @@ +package nextstep.security.authentication; + +import nextstep.security.oauth2user.OAuth2User; +import nextstep.security.oauth2user.Oauth2UserService; + +public class Oauth2AuthenticationProvider implements AuthenticationProvider { + + private final Oauth2UserService oauth2UserService; + + public Oauth2AuthenticationProvider(Oauth2UserService oauth2UserService) { + this.oauth2UserService = oauth2UserService; + } + + @Override + public Authentication authenticate(Authentication authentication) { + final OAuth2User oAuth2User = oauth2UserService.loadUser(authentication.getPrincipal().toString()); + return Oauth2Authentication.ofAuthenticated(oAuth2User.getName(), oAuth2User.getAuthorities()); + } + + @Override + public boolean supports(Class authentication) { + return Oauth2Authentication.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/nextstep/security/authentication/Oauth2LoginAuthenticationFilter.java b/src/main/java/nextstep/security/authentication/Oauth2LoginAuthenticationFilter.java new file mode 100644 index 0000000..e1ad446 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/Oauth2LoginAuthenticationFilter.java @@ -0,0 +1,56 @@ +package nextstep.security.authentication; + +import nextstep.security.access.matcher.RequestMatcher; +import nextstep.security.context.SecurityContext; +import nextstep.security.context.SecurityContextHolder; +import nextstep.security.context.SecurityContextRepository; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class Oauth2LoginAuthenticationFilter extends OncePerRequestFilter { + + private final RequestMatcher requestMatcher; + private final AuthenticationManager authenticationManager; + private final SecurityContextRepository securityContextRepository; + private final String redirectUrl; + + public Oauth2LoginAuthenticationFilter( + RequestMatcher requestMatcher, + AuthenticationManager authenticationManager, + SecurityContextRepository securityContextRepository, + String redirectUrl + ) { + this.requestMatcher = requestMatcher; + this.authenticationManager = authenticationManager; + this.securityContextRepository = securityContextRepository; + this.redirectUrl = redirectUrl; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + if (requestMatcher.matches(request)) { + final String accessToken = request.getParameter("code"); + + final SecurityContext context = SecurityContextHolder.getContext(); + + final Authentication authRequest = Oauth2Authentication.ofRequest(accessToken); + final Authentication authResult = authenticationManager.authenticate(authRequest); + + context.setAuthentication(authResult); + securityContextRepository.saveContext(context, request, response); + response.sendRedirect(redirectUrl); + return; + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/nextstep/security/authorization/Oauth2AuthorizationRequestRedirectFilter.java b/src/main/java/nextstep/security/authorization/Oauth2AuthorizationRequestRedirectFilter.java new file mode 100644 index 0000000..c314a1d --- /dev/null +++ b/src/main/java/nextstep/security/authorization/Oauth2AuthorizationRequestRedirectFilter.java @@ -0,0 +1,38 @@ +package nextstep.security.authorization; + +import nextstep.security.access.matcher.RequestMatcher; +import nextstep.security.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class Oauth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter { + + private final RequestMatcher requestMatcher; + private final String redirectUrl; + + public Oauth2AuthorizationRequestRedirectFilter(RequestMatcher requestMatcher, String redirectUrl) { + this.requestMatcher = requestMatcher; + this.redirectUrl = redirectUrl; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + if (requestMatcher.matches(request)) { + if (SecurityContextHolder.getContext().getAuthentication() == null) { + response.sendRedirect(redirectUrl); + return; + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/nextstep/security/oauth2user/BaseOauth2User.java b/src/main/java/nextstep/security/oauth2user/BaseOauth2User.java new file mode 100644 index 0000000..76ef598 --- /dev/null +++ b/src/main/java/nextstep/security/oauth2user/BaseOauth2User.java @@ -0,0 +1,27 @@ +package nextstep.security.oauth2user; + +import nextstep.security.oauth2user.OAuth2User; + +import java.util.Set; + +public class BaseOauth2User implements OAuth2User { + + private String name; + + private Set authorities; + + public BaseOauth2User(String name, Set authorities) { + this.name = name; + this.authorities = authorities; + } + + @Override + public String getName() { + return name; + } + + @Override + public Set getAuthorities() { + return authorities; + } +} diff --git a/src/main/java/nextstep/security/oauth2user/OAuth2User.java b/src/main/java/nextstep/security/oauth2user/OAuth2User.java new file mode 100644 index 0000000..a6453fc --- /dev/null +++ b/src/main/java/nextstep/security/oauth2user/OAuth2User.java @@ -0,0 +1,10 @@ +package nextstep.security.oauth2user; + +import java.util.Set; + +public interface OAuth2User { + + String getName(); + + Set getAuthorities(); +} diff --git a/src/main/java/nextstep/security/oauth2user/Oauth2UserService.java b/src/main/java/nextstep/security/oauth2user/Oauth2UserService.java new file mode 100644 index 0000000..4bc72ff --- /dev/null +++ b/src/main/java/nextstep/security/oauth2user/Oauth2UserService.java @@ -0,0 +1,7 @@ +package nextstep.security.oauth2user; + +import nextstep.security.exception.AuthenticationException; + +public interface Oauth2UserService { + OAuth2User loadUser(String accessToken) throws AuthenticationException; +}