diff --git a/build.gradle.kts b/build.gradle.kts index f435da0a..637b72cd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-oauth2-client") runtimeOnly("org.springframework.boot:spring-boot-devtools") // Thymeleaf implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE") @@ -48,7 +49,6 @@ dependencies { implementation("org.mapstruct:mapstruct:1.5.3.Final") // Annotation processors annotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final") - implementation("org.springframework.boot:spring-boot-starter-oauth2-client") // Testing testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") diff --git a/src/main/java/io/hexlet/typoreporter/config/SecurityConfig.java b/src/main/java/io/hexlet/typoreporter/config/SecurityConfig.java index 53ee6892..29ae4e1a 100644 --- a/src/main/java/io/hexlet/typoreporter/config/SecurityConfig.java +++ b/src/main/java/io/hexlet/typoreporter/config/SecurityConfig.java @@ -1,11 +1,9 @@ package io.hexlet.typoreporter.config; import io.hexlet.typoreporter.config.oauth2.OAuth2ConfigurationProperties; -import io.hexlet.typoreporter.handler.OAuth2SuccessHandler; import io.hexlet.typoreporter.handler.exception.ForbiddenDomainException; import io.hexlet.typoreporter.handler.exception.WorkspaceNotFoundException; import io.hexlet.typoreporter.security.service.AccountDetailService; -import io.hexlet.typoreporter.security.service.CustomOAuth2UserService; import io.hexlet.typoreporter.security.service.SecuredWorkspaceService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -28,12 +26,13 @@ import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.context.DelegatingSecurityContextRepository; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.annotation.RequestScope; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @@ -46,8 +45,6 @@ @Configuration @EnableMethodSecurity public class SecurityConfig { - @Autowired - private CustomOAuth2UserService oAuth2UserService; @Autowired private OAuth2ConfigurationProperties oAuth2ConfigurationProperties; @@ -111,11 +108,8 @@ public SecurityFilterChain filterChain(HttpSecurity http, ) .oauth2Login(config -> config .loginPage("/login") - .userInfoEndpoint() - .userService(oAuth2UserService) - .and() - .clientRegistrationRepository(getClientRegistrationRepository()) - .successHandler(getOAuth2SuccessHandler()) + .defaultSuccessUrl("/workspaces") + .failureUrl("/login") ) .csrf(csrf -> csrf .ignoringRequestMatchers( @@ -140,15 +134,18 @@ private ClientRegistration githubClientRegistration() { return CommonOAuth2Provider.GITHUB.getBuilder("github") .clientId(oAuth2ConfigurationProperties.getClientId()) .clientSecret(oAuth2ConfigurationProperties.getClientSecret()) + .redirectUri(oAuth2ConfigurationProperties.getRedirectUri()) .scope(oAuth2ConfigurationProperties.getScope()) .build(); } @Bean - public AuthenticationSuccessHandler getOAuth2SuccessHandler() { - return new OAuth2SuccessHandler(); + @RequestScope + public RestTemplate getRestTemplate() { + return new RestTemplate(); } + @Bean public AccessDeniedHandler accessDeniedHandler() { return new CustomAccessDeniedHandler(); @@ -179,10 +176,6 @@ protected void doFilterInternal(HttpServletRequest request, response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage()); } catch (WorkspaceNotFoundException e) { response.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage()); - } catch (IllegalArgumentException e) { - if (e.getMessage().contains("principalNull")) { - response.sendRedirect("/login"); - } } } }; diff --git a/src/main/java/io/hexlet/typoreporter/config/audit/AuditConfiguration.java b/src/main/java/io/hexlet/typoreporter/config/audit/AuditConfiguration.java index 9479c89b..6dcc4a75 100644 --- a/src/main/java/io/hexlet/typoreporter/config/audit/AuditConfiguration.java +++ b/src/main/java/io/hexlet/typoreporter/config/audit/AuditConfiguration.java @@ -4,18 +4,20 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import java.security.Principal; import java.util.Optional; -import static org.springframework.security.core.context.SecurityContextHolder.getContext; - @Configuration @EnableJpaAuditing public class AuditConfiguration { @Bean public AuditorAware auditorAware() { - return () -> Optional.ofNullable(getContext().getAuthentication()).map(Principal::getName); + return () -> Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .filter(Authentication::isAuthenticated) + .map(Principal::getName); } } diff --git a/src/main/java/io/hexlet/typoreporter/config/oauth2/OAuth2ConfigurationProperties.java b/src/main/java/io/hexlet/typoreporter/config/oauth2/OAuth2ConfigurationProperties.java index 42a9007d..10771d57 100644 --- a/src/main/java/io/hexlet/typoreporter/config/oauth2/OAuth2ConfigurationProperties.java +++ b/src/main/java/io/hexlet/typoreporter/config/oauth2/OAuth2ConfigurationProperties.java @@ -6,7 +6,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; -import java.util.List; +import java.util.HashSet; @Configuration @ConfigurationProperties(prefix = "spring.security.oauth2.client.registration.github") @@ -18,6 +18,8 @@ public class OAuth2ConfigurationProperties { @Value("clientSecret") private String clientSecret; @Value("scope") - private List scope; + private HashSet scope; + @Value("redirect-uri") + private String redirectUri; } diff --git a/src/main/java/io/hexlet/typoreporter/domain/account/CustomOAuth2User.java b/src/main/java/io/hexlet/typoreporter/domain/account/CustomOAuth2User.java index 0648e84c..bbb9b70c 100644 --- a/src/main/java/io/hexlet/typoreporter/domain/account/CustomOAuth2User.java +++ b/src/main/java/io/hexlet/typoreporter/domain/account/CustomOAuth2User.java @@ -1,5 +1,6 @@ package io.hexlet.typoreporter.domain.account; +import io.hexlet.typoreporter.service.dto.oauth2.PrivateEmail; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.user.OAuth2User; @@ -9,9 +10,11 @@ public class CustomOAuth2User implements OAuth2User { private final OAuth2User oAuth2User; + private final PrivateEmail privateEmail; - public CustomOAuth2User(OAuth2User oAuth2User) { + public CustomOAuth2User(OAuth2User oAuth2User, PrivateEmail email) { this.oAuth2User = oAuth2User; + this.privateEmail = email; } @Override @@ -26,15 +29,16 @@ public Collection getAuthorities() { @Override public String getName() { - return oAuth2User.getAttribute("email"); + return this.privateEmail.getEmail(); } public String getEmail() { - return oAuth2User.getAttribute("email"); + return this.privateEmail.getEmail(); } public String getLogin() { return oAuth2User.getAttribute("login"); } + //TODO: fix required sets first and last names after issue #286 will be done (empty names) public String getFirstName() { String[] fullName = Objects.requireNonNull(oAuth2User.getAttribute("name")).split(" "); return fullName[1]; diff --git a/src/main/java/io/hexlet/typoreporter/handler/OAuth2SuccessHandler.java b/src/main/java/io/hexlet/typoreporter/handler/OAuth2SuccessHandler.java deleted file mode 100644 index 27819e11..00000000 --- a/src/main/java/io/hexlet/typoreporter/handler/OAuth2SuccessHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.hexlet.typoreporter.handler; - -import io.hexlet.typoreporter.domain.account.CustomOAuth2User; -import io.hexlet.typoreporter.service.AccountService; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -import org.springframework.stereotype.Component; - -import java.io.IOException; - -@Component -public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { - @Autowired - private AccountService accountService; - - @Override - public void onAuthenticationSuccess(HttpServletRequest request, - HttpServletResponse response, - Authentication authentication) throws IOException, ServletException { - CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); - if (oAuth2User.getEmail() == null - || (oAuth2User.getFirstName() == null && oAuth2User.getLastName() == null)) { - //TODO: добавить ошибку на фронт, если email приватный - response.sendRedirect("/login"); - } - accountService.processOAuthPostLogin(oAuth2User); - response.sendRedirect("/workspaces"); - } -} diff --git a/src/main/java/io/hexlet/typoreporter/handler/exception/OAuth2Exception.java b/src/main/java/io/hexlet/typoreporter/handler/exception/OAuth2Exception.java new file mode 100644 index 00000000..ef6fd663 --- /dev/null +++ b/src/main/java/io/hexlet/typoreporter/handler/exception/OAuth2Exception.java @@ -0,0 +1,11 @@ +package io.hexlet.typoreporter.handler.exception; + +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ProblemDetail; +import org.springframework.web.ErrorResponseException; + +public class OAuth2Exception extends ErrorResponseException { + public OAuth2Exception(HttpStatusCode status, ProblemDetail body, Throwable cause) { + super(status, body, cause); + } +} diff --git a/src/main/java/io/hexlet/typoreporter/security/service/CustomOAuth2UserService.java b/src/main/java/io/hexlet/typoreporter/security/service/CustomOAuth2UserService.java index 3e5dd15b..f07b895b 100644 --- a/src/main/java/io/hexlet/typoreporter/security/service/CustomOAuth2UserService.java +++ b/src/main/java/io/hexlet/typoreporter/security/service/CustomOAuth2UserService.java @@ -1,6 +1,12 @@ package io.hexlet.typoreporter.security.service; import io.hexlet.typoreporter.domain.account.CustomOAuth2User; +import io.hexlet.typoreporter.service.AccountService; +import io.hexlet.typoreporter.service.GithubService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -8,10 +14,20 @@ import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class CustomOAuth2UserService extends DefaultOAuth2UserService { - @Override + private final GithubService githubService; + private final AccountService accountService; + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { OAuth2User user = super.loadUser(userRequest); - return new CustomOAuth2User(user); + var email = githubService.getPrivateEmail(userRequest.getAccessToken().getTokenValue()); + var customUser = new CustomOAuth2User(user, email); + Authentication authentication = new UsernamePasswordAuthenticationToken( + customUser, customUser.getPassword(), customUser.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + accountService.processOAuthPostLogin(customUser); + + return customUser; } } diff --git a/src/main/java/io/hexlet/typoreporter/service/AccountService.java b/src/main/java/io/hexlet/typoreporter/service/AccountService.java index 97071b98..51baa39b 100644 --- a/src/main/java/io/hexlet/typoreporter/service/AccountService.java +++ b/src/main/java/io/hexlet/typoreporter/service/AccountService.java @@ -143,13 +143,11 @@ public Account updatePassword(final UpdatePassword updatePassword, final String public void processOAuthPostLogin(CustomOAuth2User user) { var existUser = accountRepository.existsByEmail(user.getEmail()); if (!existUser) { - Account account = new Account(); - account.setEmail(user.getEmail()); + SignupAccount signupAccount = new SignupAccount( + user.getLogin(), user.getEmail(), + passwordEncoder.encode(user.getPassword()), user.getFirstName(), user.getLastName()); + Account account = accountMapper.toAccount(signupAccount); account.setAuthProvider(AuthProvider.GITHUB); - account.setUsername(user.getLogin()); - account.setPassword(passwordEncoder.encode(user.getPassword())); - account.setFirstName(user.getFirstName()); - account.setLastName(user.getLastName()); accountRepository.save(account); } } diff --git a/src/main/java/io/hexlet/typoreporter/service/GithubService.java b/src/main/java/io/hexlet/typoreporter/service/GithubService.java new file mode 100644 index 00000000..158e6af4 --- /dev/null +++ b/src/main/java/io/hexlet/typoreporter/service/GithubService.java @@ -0,0 +1,41 @@ +package io.hexlet.typoreporter.service; + +import io.hexlet.typoreporter.handler.exception.OAuth2Exception; +import io.hexlet.typoreporter.service.dto.oauth2.PrivateEmail; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Arrays; + +@Service +public class GithubService { + @Autowired + private RestTemplate restTemplate; + private static final String GITHUB_API_USER_PRIVATE_EMAILS = "https://api.github.com/user/emails"; + + public PrivateEmail getPrivateEmail(String accessToken) { + if (accessToken.isBlank()) { + throw new OAuth2Exception(HttpStatus.FORBIDDEN, ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN, + "Access token is not valid. Token is: " + accessToken), null); + } + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + HttpEntity requestEntity = new HttpEntity<>(headers); + var response = restTemplate.exchange( + GITHUB_API_USER_PRIVATE_EMAILS, HttpMethod.GET, requestEntity, PrivateEmail[].class); + if (response.getStatusCode() != HttpStatus.OK) { + throw new OAuth2Exception(HttpStatus.UNAUTHORIZED, ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, + "HTTP code response is not 200. Code: " + response.getStatusCode()), null); + } + return Arrays.stream(response.getBody()) + .filter(PrivateEmail::isPrimary) + .findFirst() + .orElseThrow(() -> new RuntimeException("no available email")); + } +} diff --git a/src/main/java/io/hexlet/typoreporter/service/dto/oauth2/PrivateEmail.java b/src/main/java/io/hexlet/typoreporter/service/dto/oauth2/PrivateEmail.java new file mode 100644 index 00000000..b7a1c61e --- /dev/null +++ b/src/main/java/io/hexlet/typoreporter/service/dto/oauth2/PrivateEmail.java @@ -0,0 +1,15 @@ +package io.hexlet.typoreporter.service.dto.oauth2; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class PrivateEmail { + private String email; + private boolean verified; + private boolean primary; + private String visibility; +} diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index eb0e55b0..96c2c614 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -20,6 +20,7 @@ spring: github: clientId: Ov23liMZqO6eA0FyjeM4 clientSecret: 3948c7b7ac39d2ee6611e611259c9422cdf00f96 + redirect-uri: "{baseUrl}/workspaces" scope: - user:email - read:user