From 9d93f04817ea08a9b6717306220cd72838ad067e Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Thu, 10 Nov 2022 01:03:32 -0300 Subject: [PATCH] [v0.0.2] Adds Google and Github OAuth2 Authentication Adds to the API the feature of OAuth2 Authentication via two providers: Google and Github. For that the tests were updated. --- pom.xml | 10 +- .../base/config/CorsConfig.java | 12 ++- .../base/config/DefaultUserConfig.java | 4 +- .../base/security/SecurityConfig.java | 78 ++++++++++++---- .../filter/CustomAuthenticationFilter.java | 17 ++-- .../filter/CustomAuthorizationFilter.java | 21 +++-- .../security/oauth/mapper/GithubOAuthMap.java | 23 +++++ .../security/oauth/mapper/GoogleOAuthMap.java | 24 +++++ .../base/security/oauth/mapper/OAuthMap.java | 12 +++ .../security/oauth/mapper/OAuthMapEnum.java | 37 ++++++++ .../oauth/repo/OAuthRequestRepository.java | 70 ++++++++++++++ .../base/security/service/AuthService.java | 10 ++ .../security/service/AuthServiceImpl.java | 92 ++++++++++++++++++- .../base/user/api/UserController.java | 48 ++++------ .../base/user/entity/Provider.java | 12 +++ .../base/user/entity/User.java | 10 +- .../base/user/model/UserDTO.java | 54 ++++------- .../base/user/service/UserServiceImpl.java | 16 ++-- .../validator/password/PasswordValidator.java | 17 +++- .../validator/password/ValidPassword.java | 13 ++- src/main/resources/application.yml | 22 ++++- .../changelog/client/db.changelog-client.yml | 12 ++- .../client/sqls/adds-user-provider.sql | 14 +++ .../base/user/repo/UserRepositoryTest.java | 16 ++-- .../user/service/UserServiceImplTest.java | 19 ++-- 25 files changed, 514 insertions(+), 149 deletions(-) create mode 100644 src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/GithubOAuthMap.java create mode 100644 src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/GoogleOAuthMap.java create mode 100644 src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/OAuthMap.java create mode 100644 src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/OAuthMapEnum.java create mode 100644 src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/repo/OAuthRequestRepository.java create mode 100644 src/main/resources/db/changelog/client/sqls/adds-user-provider.sql diff --git a/pom.xml b/pom.xml index 680343c..6285b0f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 2.7.1 + 2.7.5 com.hideyoshi @@ -24,7 +24,7 @@ org.springframework.boot spring-boot-starter-validation - 2.7.3 + 2.7.5 org.springframework.boot @@ -34,6 +34,10 @@ org.springframework.boot spring-boot-starter-security + + org.springframework.boot + spring-boot-starter-oauth2-client + org.springframework.session spring-session-core @@ -46,7 +50,7 @@ com.auth0 java-jwt - 4.0.0 + 4.2.1 org.postgresql diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/config/CorsConfig.java b/src/main/java/com/hideyoshi/backendportfolio/base/config/CorsConfig.java index 04d9d68..636afd7 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/base/config/CorsConfig.java +++ b/src/main/java/com/hideyoshi/backendportfolio/base/config/CorsConfig.java @@ -1,5 +1,6 @@ package com.hideyoshi.backendportfolio.base.config; +import antlr.actions.python.CodeLexer; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -8,12 +9,13 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; +import java.util.Collections; import java.util.List; @Configuration public class CorsConfig { - @Value("${com.hideyoshi.frontEndPath}") + @Value("${com.hideyoshi.frontendPath}") private String FRONTEND_PATH; @Value("${com.hideyoshi.frontendConnectionType}") @@ -32,10 +34,12 @@ public class CorsConfig { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of(connectionProtocol + FRONTEND_PATH)); - configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); - configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token")); - configuration.setAllowCredentials(true); + configuration.setAllowedMethods(Collections.singletonList("*")); + configuration.setAllowedHeaders(Collections.singletonList("*")); +// configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); +// configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token")); configuration.setExposedHeaders(List.of("x-auth-token")); + configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/config/DefaultUserConfig.java b/src/main/java/com/hideyoshi/backendportfolio/base/config/DefaultUserConfig.java index 2b30817..6c1b2a7 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/base/config/DefaultUserConfig.java +++ b/src/main/java/com/hideyoshi/backendportfolio/base/config/DefaultUserConfig.java @@ -1,5 +1,6 @@ package com.hideyoshi.backendportfolio.base.config; +import com.hideyoshi.backendportfolio.base.user.entity.Provider; import com.hideyoshi.backendportfolio.base.user.entity.Role; import com.hideyoshi.backendportfolio.base.user.model.UserDTO; import com.hideyoshi.backendportfolio.base.user.repo.UserRepository; @@ -30,10 +31,11 @@ public class DefaultUserConfig { CommandLineRunner run(UserService userService, UserRepository userRepo) { return args -> { UserDTO defaultUser = UserDTO.builder() - .fullname(ADMIN_NAME) + .name(ADMIN_NAME) .email(ADMIN_EMAIL) .username(ADMIN_USERNAME) .password(ADMIN_PASSWORD) + .provider(Provider.LOCAL) .roles(new ArrayList<>()) .build(); if (!userRepo.findByUsername(defaultUser.getUsername()).isPresent()) { diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/SecurityConfig.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/SecurityConfig.java index f02c3de..42dc350 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/base/security/SecurityConfig.java +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/SecurityConfig.java @@ -3,29 +3,31 @@ package com.hideyoshi.backendportfolio.base.security; import com.hideyoshi.backendportfolio.base.config.RestAuthenticationEntryPointConfig; import com.hideyoshi.backendportfolio.base.security.filter.CustomAuthenticationFilter; import com.hideyoshi.backendportfolio.base.security.filter.CustomAuthorizationFilter; +import com.hideyoshi.backendportfolio.base.security.oauth.repo.OAuthRequestRepository; import com.hideyoshi.backendportfolio.base.security.service.AuthService; +import com.hideyoshi.backendportfolio.util.exception.AuthenticationInvalidException; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; +import lombok.extern.log4j.Log4j2; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.servlet.HandlerExceptionResolver; -import java.util.Arrays; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +@Log4j2 @Configuration @EnableWebSecurity @RequiredArgsConstructor @@ -37,6 +39,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { private final BCryptPasswordEncoder passwordEncoder; + private final OAuthRequestRepository oAuthRequestRepository; + private final RestAuthenticationEntryPointConfig restAuthenticationEntryPointConfig; @Override @@ -48,22 +52,64 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { + http.cors().and().csrf().disable(); + + this.addSecurityToHttp(http); + this.addOAuthSecurityToHttp(http); + } + + private void addSecurityToHttp(HttpSecurity http) throws Exception { + CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(this.authenticationManager(), this.authService, this.restAuthenticationEntryPointConfig); customAuthenticationFilter.setFilterProcessesUrl("/user/login"); - http.cors().and().csrf().disable() - .authorizeRequests().antMatchers("/session/**").permitAll() - .and().authorizeRequests().antMatchers("/user/signup").permitAll() - .and().authorizeRequests().antMatchers("/user/login/refresh").permitAll() - .and().authorizeRequests().antMatchers("/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN") - .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) - .and().addFilter(customAuthenticationFilter) - .addFilterBefore(new CustomAuthorizationFilter(this.authService), UsernamePasswordAuthenticationFilter.class); + http.authorizeRequests() + .antMatchers("/session/**").permitAll() + .and().authorizeRequests().antMatchers("/user/signup").permitAll() + .and().authorizeRequests().antMatchers("/user/oauth/**").permitAll() + .and().authorizeRequests().antMatchers("/user/login/**").permitAll() + .and().authorizeRequests().antMatchers("/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN") + + .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + .and().addFilter(customAuthenticationFilter) + + .addFilterBefore(new CustomAuthorizationFilter(this.authService), UsernamePasswordAuthenticationFilter.class); } + private void addOAuthSecurityToHttp(HttpSecurity http) throws Exception { + + http.oauth2Login() + .authorizationEndpoint() + .authorizationRequestRepository(this.oAuthRequestRepository) + .and().successHandler(this::successHandler) + .and().exceptionHandling() + .authenticationEntryPoint(this::authenticationEntryPoint); + } + + private void successHandler(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication ) throws IOException { + + OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal(); + + this.authService.loginOAuthUser( + request, + response, + oauthUser + ); + + } + + private void authenticationEntryPoint(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authentication ) { + throw new AuthenticationInvalidException(authentication.getMessage()); + } + + @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthenticationFilter.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthenticationFilter.java index a265181..2beac74 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthenticationFilter.java +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthenticationFilter.java @@ -7,6 +7,7 @@ import com.hideyoshi.backendportfolio.base.security.service.AuthService; import com.hideyoshi.backendportfolio.base.user.model.TokenDTO; import com.hideyoshi.backendportfolio.base.user.model.UserDTO; import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -57,18 +58,12 @@ public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFi @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException { - UserDTO user = (UserDTO) authentication.getPrincipal(); - Algorithm algorithm = Algorithm.HMAC256("secret".getBytes()); + this.authService.loginUser( + request, + response, + (UserDTO) authentication.getPrincipal() + ); - HashMap tokens = this.authService.generateTokens(user, algorithm, request); - - HttpSession httpSession = request.getSession(); - UserDTO authenticatedUser = user.toResponse(tokens.get("accessToken"), tokens.get("refreshToken")); - httpSession.setAttribute("user", authenticatedUser); - - response.setContentType(APPLICATION_JSON_VALUE); - new ObjectMapper() - .writeValue(response.getOutputStream(), authenticatedUser); } } diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthorizationFilter.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthorizationFilter.java index 012d5e8..1bb8d00 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthorizationFilter.java +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/filter/CustomAuthorizationFilter.java @@ -2,24 +2,16 @@ package com.hideyoshi.backendportfolio.base.security.filter; import com.fasterxml.jackson.databind.ObjectMapper; import com.hideyoshi.backendportfolio.base.security.service.AuthService; -import com.hideyoshi.backendportfolio.util.exception.BadRequestException; -import lombok.extern.log4j.Log4j2; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.web.servlet.HandlerExceptionResolver; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; +import java.util.*; import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.http.HttpStatus.FORBIDDEN; @@ -38,7 +30,7 @@ public class CustomAuthorizationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - if (request.getServletPath().equals("/user/login")) { + if (this.isPathNotProtected(request.getServletPath())) { filterChain.doFilter(request, response); } else { String authorizationHeader = request.getHeader(AUTHORIZATION); @@ -69,4 +61,13 @@ public class CustomAuthorizationFilter extends OncePerRequestFilter { } } + private Boolean isPathNotProtected(String path) { + + List notProtectedPaths = Arrays.asList( + "/user/login" + ); + + return notProtectedPaths.contains(path); + } + } diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/GithubOAuthMap.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/GithubOAuthMap.java new file mode 100644 index 0000000..7f27aa4 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/GithubOAuthMap.java @@ -0,0 +1,23 @@ +package com.hideyoshi.backendportfolio.base.security.oauth.mapper; + +import com.hideyoshi.backendportfolio.base.user.entity.Provider; +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.core.user.OAuth2User; + +@AllArgsConstructor +public class GithubOAuthMap implements OAuthMap { + + private OAuth2User oAuth2User; + + @Override + public String getPrincipal() { + return oAuth2User.getAttribute("login"); + } + + @Override + public Provider getProvider() { + return Provider.GITHUB; + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/GoogleOAuthMap.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/GoogleOAuthMap.java new file mode 100644 index 0000000..9aa4ffe --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/GoogleOAuthMap.java @@ -0,0 +1,24 @@ +package com.hideyoshi.backendportfolio.base.security.oauth.mapper; + +import com.hideyoshi.backendportfolio.base.user.entity.Provider; +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.core.user.OAuth2User; + +@AllArgsConstructor +public class GoogleOAuthMap implements OAuthMap { + + private OAuth2User oauthUser; + + @Override + public String getPrincipal() { + return this.oauthUser.getAttribute("given_name"); + } + + @Override + public Provider getProvider() { + return Provider.GOOGLE; + } + + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/OAuthMap.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/OAuthMap.java new file mode 100644 index 0000000..ff607c3 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/OAuthMap.java @@ -0,0 +1,12 @@ +package com.hideyoshi.backendportfolio.base.security.oauth.mapper; + +import com.hideyoshi.backendportfolio.base.user.entity.Provider; +import org.springframework.security.oauth2.core.user.OAuth2User; + +public interface OAuthMap { + + String getPrincipal(); + + Provider getProvider(); + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/OAuthMapEnum.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/OAuthMapEnum.java new file mode 100644 index 0000000..aad4182 --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/mapper/OAuthMapEnum.java @@ -0,0 +1,37 @@ +package com.hideyoshi.backendportfolio.base.security.oauth.mapper; + +import com.hideyoshi.backendportfolio.base.user.entity.Provider; + +public enum OAuthMapEnum { + + GOOGLE(GoogleOAuthMap.class, Provider.GOOGLE), + + GITHUB(GithubOAuthMap.class, Provider.GITHUB); + + private Class oAuthMap; + + private Provider provider; + + private OAuthMapEnum(Class oAuthMap, Provider provider) { + this.oAuthMap = oAuthMap; + this.provider = provider; + } + + public Class getMap() { + return oAuthMap; + } + + public Provider getProvider() { + return provider; + } + + public static OAuthMapEnum byValue(String name) { + for (OAuthMapEnum e : values()) { + if (e.getProvider().getName().equals(name)) { + return e; + } + } + throw new IllegalArgumentException("Argument not valid."); + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/repo/OAuthRequestRepository.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/repo/OAuthRequestRepository.java new file mode 100644 index 0000000..6df7e6f --- /dev/null +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/oauth/repo/OAuthRequestRepository.java @@ -0,0 +1,70 @@ +package com.hideyoshi.backendportfolio.base.security.oauth.repo; + +import lombok.extern.log4j.Log4j2; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.stereotype.Repository; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; + +@Log4j2 +@Repository +public class OAuthRequestRepository implements AuthorizationRequestRepository { + + @Override + public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { + + String state = request.getParameter("state"); + log.info(state); + + if (Objects.nonNull(state)) { + return removeAuthorizationRequest(request); + } + return null; + } + + @Override + public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) { + + String state = authorizationRequest.getState(); + log.info(state); + + request.getSession().setAttribute( + String.format("state_%s", state), + authorizationRequest + ); + + } + + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) { + + String state = request.getParameter("state"); + + OAuth2AuthorizationRequest authorizationRequest = null; + if (Objects.nonNull(state)) { + authorizationRequest = this.getAuthorizationRequestFromSession(request, state); + } + + if (Objects.nonNull(authorizationRequest)) { + removeAuthorizationRequestFromSession(request, state); + return authorizationRequest; + } + return null; + } + + private OAuth2AuthorizationRequest getAuthorizationRequestFromSession(HttpServletRequest request, String state) { + return (OAuth2AuthorizationRequest) request.getSession().getAttribute( + String.format("state_%s", state) + ); + } + + private void removeAuthorizationRequestFromSession(HttpServletRequest request, String state) { + request.getSession().removeAttribute( + String.format("state_%s", state) + ); + } + +} diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthService.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthService.java index 5194340..5a8561c 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthService.java +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthService.java @@ -4,10 +4,12 @@ import com.auth0.jwt.algorithms.Algorithm; import com.hideyoshi.backendportfolio.base.user.model.TokenDTO; import com.hideyoshi.backendportfolio.base.user.model.UserDTO; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; +import java.io.IOException; import java.util.HashMap; public interface AuthService { @@ -24,4 +26,12 @@ public interface AuthService { UserDTO signupUser(@Valid UserDTO user, HttpServletRequest request); + UserDTO generateUserWithTokens(UserDTO user, HttpServletRequest request); + + UserDTO processOAuthPostLogin(@Valid UserDTO user, HttpServletRequest request); + + void loginUser(HttpServletRequest request, HttpServletResponse response, @Valid UserDTO user) throws IOException; + + void loginOAuthUser(HttpServletRequest request, HttpServletResponse response, OAuth2User user) throws IOException; + } diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthServiceImpl.java b/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthServiceImpl.java index 7ce5f27..1d0bcd6 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthServiceImpl.java +++ b/src/main/java/com/hideyoshi/backendportfolio/base/security/service/AuthServiceImpl.java @@ -4,6 +4,11 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hideyoshi.backendportfolio.base.security.oauth.mapper.OAuthMap; +import com.hideyoshi.backendportfolio.base.security.oauth.mapper.OAuthMapEnum; +import com.hideyoshi.backendportfolio.base.user.entity.Provider; +import com.hideyoshi.backendportfolio.base.user.entity.Role; import com.hideyoshi.backendportfolio.base.user.model.TokenDTO; import com.hideyoshi.backendportfolio.base.user.model.UserDTO; import com.hideyoshi.backendportfolio.base.user.service.UserService; @@ -16,6 +21,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import org.springframework.web.servlet.HandlerExceptionResolver; @@ -23,10 +29,12 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.validation.Valid; +import java.io.IOException; import java.util.*; import java.util.stream.Collectors; import static java.util.Arrays.stream; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @Log4j2 @Service @@ -66,6 +74,7 @@ public class AuthServiceImpl implements AuthService { .sign(algorithm); return new TokenDTO(accessToken, expirationDate); + } @Override @@ -160,16 +169,93 @@ public class AuthServiceImpl implements AuthService { @Override public UserDTO signupUser(@Valid UserDTO user, HttpServletRequest request) { + user.setProvider(Provider.LOCAL); + + return this.generateUserWithTokens( + this.userService.saveUser(user), + request + ); + + } + + @Override + public UserDTO generateUserWithTokens(UserDTO user, HttpServletRequest request) { + Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET.getBytes()); - UserDTO userSaved = this.userService.saveUser(user); - HashMap tokens = this.generateTokens(userSaved, algorithm, request); + HashMap tokens = this.generateTokens(user, algorithm, request); HttpSession httpSession = request.getSession(); - UserDTO userAuthenticated = userSaved.toResponse(tokens.get("accessToken"), tokens.get("refreshToken")); + UserDTO userAuthenticated = user.toResponse(tokens.get("accessToken"), tokens.get("refreshToken")); + httpSession.setAttribute("user", userAuthenticated); return userAuthenticated; } + @Override + public UserDTO processOAuthPostLogin(@Valid UserDTO user, HttpServletRequest request) { + + if (Objects.nonNull(user.getId())) { + this.userService.alterUser(user.getId(), user); + } else { + this.userService.saveUser(user); + } + + return this.generateUserWithTokens(user, request); + + } + + @Override + public void loginUser(HttpServletRequest request, HttpServletResponse response, @Valid UserDTO user) throws IOException { + + UserDTO authenticatedUser = this.generateUserWithTokens( + user, + request + ); + + response.setContentType(APPLICATION_JSON_VALUE); + new ObjectMapper() + .writeValue(response.getOutputStream(), authenticatedUser); + } + + public void loginOAuthUser(HttpServletRequest request, + HttpServletResponse response, + OAuth2User oauthUser) throws IOException { + + String[] url = request.getRequestURL().toString().split("/"); + String clientId = url[url.length-1]; + + OAuthMap oauthMap = null; + try { + oauthMap = (OAuthMap) OAuthMapEnum.byValue(clientId).getMap() + .getDeclaredConstructor(OAuth2User.class).newInstance(oauthUser); + } catch (Exception e) { + throw new BadRequestException("No Such Provider"); + } + + UserDTO user = null; + try { + user = this.userService.getUser(oauthMap.getPrincipal()); + } catch (BadRequestException e) { + user = UserDTO.builder() + .name(oauthUser.getAttribute("name")) + .username(oauthMap.getPrincipal()) + .email(oauthUser.getAttribute("email")) + .roles(Arrays.asList(Role.USER)) + .provider(oauthMap.getProvider()) + .build(); + } + + UserDTO authenticatedUser = this.processOAuthPostLogin( + user, + request + ); + + response.setContentType(APPLICATION_JSON_VALUE); + new ObjectMapper() + .writeValue(response.getOutputStream(), authenticatedUser); + + } + } diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/api/UserController.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/api/UserController.java index 9abb532..f94bb5e 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/base/user/api/UserController.java +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/api/UserController.java @@ -10,15 +10,19 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Controller; -import org.springframework.validation.BindingResult; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; +import java.io.IOException; import java.net.URI; +import java.security.Provider; import java.util.List; @Log4j2 @@ -49,34 +53,6 @@ public class UserController { return ResponseEntity.created(uri).body(this.authService.signupUser(user, request)); } - @PostMapping("/delete/{id}") - @UserResourceGuard(accessType = UserResourceGuardEnum.SAME_USER) - public ResponseEntity deleteUser(@PathVariable("id") Long id) { - this.userService.deleteUser(id); - return new ResponseEntity<>(HttpStatus.NO_CONTENT); - } -// -// @PostMapping("/alter/{id}") -// @UserResourceGuard(accessType = UserResourceGuardEnum.SAME_USER) -// public ResponseEntity alterUser(@PathVariable("id") Long id, @RequestBody @Valid UserDTO user) { -// this.userService.alterUser(id, user); -// return new ResponseEntity<>(HttpStatus.NO_CONTENT); -// } -// -// @PostMapping("/alter/{id}/role/add") -// @UserResourceGuard(accessType = UserResourceGuardEnum.SAME_USER) -// public ResponseEntity addRoleToUser(@PathVariable("id") Long id, @RequestBody RoleToUserDTO filter) { -// userService.addRoleToUser(id, filter.getRoleName()); -// return ResponseEntity.ok().build(); -// } -// -// @PostMapping("/alter/{id}/role/delete") -// @UserResourceGuard(accessType = UserResourceGuardEnum.SAME_USER) -// public ResponseEntity deleteRoleToUser(@PathVariable("id") Long id, @RequestBody RoleToUserDTO filter) { -// userService.removeRoleFromUser(id, filter.getRoleName()); -// return ResponseEntity.ok().build(); -// } - @PostMapping("/login/refresh") @UserResourceGuard(accessType = UserResourceGuardEnum.OPEN) public ResponseEntity refreshAccessToken( @@ -86,4 +62,18 @@ public class UserController { return ResponseEntity.ok(this.authService.refreshAccessToken(refreshToken.getToken(), request, response)); } + @GetMapping("/login/callback") + @UserResourceGuard(accessType = UserResourceGuardEnum.OPEN) + public void oauthCallback(HttpServletResponse response) throws IOException { + log.info("Teste"); + response.sendRedirect("http://localhost:4200"); + } + + @PostMapping("/delete/{id}") + @UserResourceGuard(accessType = UserResourceGuardEnum.SAME_USER) + public ResponseEntity deleteUser(@PathVariable("id") Long id) { + this.userService.deleteUser(id); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + } diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/Provider.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/Provider.java index df2cea1..25db4a3 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/Provider.java +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/Provider.java @@ -4,7 +4,10 @@ public enum Provider { GOOGLE("google"), + GITHUB("github"), + LOCAL("local"); + private String name; Provider(String name) { @@ -15,4 +18,13 @@ public enum Provider { return name; } + public static Provider byValue(String name) { + for (Provider p : values()) { + if (p.getName().equals(name)) { + return p; + } + } + throw new IllegalArgumentException("Argument not valid."); + } + } diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/User.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/User.java index 4470b66..a01d52f 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/User.java +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/entity/User.java @@ -25,10 +25,10 @@ public class User { private Long id; @Column( - name = "full_name", + name = "name", nullable = false ) - private String fullname; + private String name; @Column( name = "email", @@ -52,6 +52,12 @@ public class User { ) private String password; + @Column( + name = "provider", + nullable = false + ) + private String provider; + @Column( name = "roles", nullable = false diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/model/UserDTO.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/model/UserDTO.java index 51370eb..53d7ffc 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/base/user/model/UserDTO.java +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/model/UserDTO.java @@ -36,7 +36,7 @@ public class UserDTO implements UserDetails { private Long id; @NotEmpty - private String fullname; + private String name; @NotEmpty @ValidEmail @@ -59,104 +59,82 @@ public class UserDTO implements UserDetails { private Provider provider; - public UserDTO( - String fullname, - String email, - String username, - String password - ) { - this.fullname = fullname; - this.email = email; - this.username = username; - this.password = password; - this.roles = List.of(Role.USER); - } - - public UserDTO( - String fullname, - String email, - String username, - String password, - List roles - ) { - this.fullname = fullname; - this.email = email; - this.username = username; - this.password = password; - this.roles = roles; - } - public UserDTO(User entity) { this.id = entity.getId(); - this.fullname = entity.getFullname(); + this.name = entity.getName(); this.email = entity.getEmail(); this.username = entity.getUsername(); this.password = entity.getPassword(); + this.provider = Provider.byValue(entity.getProvider()); this.roles = entity.getRoles(); } public User toEntity() { return new User( this.id, - this.fullname, + this.name, this.email, this.username, this.password, + this.provider.getName(), Objects.nonNull(this.roles) ? this.roles.stream() .map(role -> role.getDescription()) .collect(Collectors.joining("&")) : Role.USER.getDescription() ); } - @JsonIgnore @Override + @JsonIgnore public Collection getAuthorities() { - return this.roles.stream() + return this.getRoles().stream() .map(role -> new SimpleGrantedAuthority(role.getDescription())) .collect(Collectors.toList()); } - @JsonIgnore @Override + @JsonIgnore public boolean isAccountNonExpired() { return true; } - @JsonIgnore @Override + @JsonIgnore public boolean isAccountNonLocked() { return true; } - @JsonIgnore @Override + @JsonIgnore public boolean isCredentialsNonExpired() { return true; } - @JsonIgnore @Override + @JsonIgnore public boolean isEnabled() { return true; } public UserDTO toResponse() { return UserDTO.builder() - .fullname(this.fullname) + .name(this.name) .email(this.email) .username(this.username) + .provider(this.provider) .build(); } public UserDTO toResponse(TokenDTO accessToken, TokenDTO refreshToken) { return UserDTO.builder() .id(this.id) - .fullname(this.fullname) + .name(this.name) .email(this.email) .username(this.username) + .provider(this.provider) .roles(this.roles) .accessToken(accessToken) .refreshToken(refreshToken) .build(); } + } diff --git a/src/main/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImpl.java b/src/main/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImpl.java index 3af3254..9573a1c 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImpl.java +++ b/src/main/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImpl.java @@ -34,9 +34,13 @@ public class UserServiceImpl implements UserService { throw new BadRequestException(String.format("User %s already exists. Try another UserName.", userOnDB.getUsername())); }); - log.info(String.format("Saving to the database user of name: %s", user.getFullname())); + log.info(String.format("Saving to the database user of name: %s", user.getName())); - user.setPassword(passwordEncoder.encode(user.getPassword())); + if (Objects.nonNull(user.getPassword())) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + } else { + user.setPassword(""); + } UserDTO userSaved = new UserDTO(userRepo.save(user.toEntity())); if (!userSaved.getRoles().contains(Role.USER)) { @@ -81,10 +85,10 @@ public class UserServiceImpl implements UserService { log.info(String.format("Adding to user %s the role %s", userOnDB.getUsername(), newAuthority.getDescription())); - if (roles.add(newAuthority)) { - userOnDB.setRoles(roles); - this.alterUser(userOnDB.getId(), userOnDB); - } + roles.add(newAuthority); + userOnDB.setRoles(roles); + + this.alterUser(userOnDB.getId(), userOnDB); } diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/PasswordValidator.java b/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/PasswordValidator.java index e63d3c1..4def643 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/PasswordValidator.java +++ b/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/PasswordValidator.java @@ -1,5 +1,6 @@ package com.hideyoshi.backendportfolio.util.validator.password; +import com.hideyoshi.backendportfolio.base.user.entity.Provider; import lombok.RequiredArgsConstructor; import javax.validation.ConstraintValidator; @@ -9,17 +10,25 @@ import java.util.regex.Pattern; @RequiredArgsConstructor public class PasswordValidator implements ConstraintValidator { + Provider provider; + private final String PASSWORD_PATTERN = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$"; @Override - public void initialize(ValidPassword constraintAnnotation) {} + public void initialize(ValidPassword constraintAnnotation) { + this.provider = constraintAnnotation.provider(); + } @Override public boolean isValid(String password, ConstraintValidatorContext context) { - return Pattern.compile(PASSWORD_PATTERN) - .matcher(password) - .matches(); + if (this.provider.equals(Provider.GOOGLE)) { + return true; + } else { + return Pattern.compile(PASSWORD_PATTERN) + .matcher(password) + .matches(); + } } } diff --git a/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/ValidPassword.java b/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/ValidPassword.java index 35cc646..32e773c 100644 --- a/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/ValidPassword.java +++ b/src/main/java/com/hideyoshi/backendportfolio/util/validator/password/ValidPassword.java @@ -1,5 +1,7 @@ package com.hideyoshi.backendportfolio.util.validator.password; +import com.hideyoshi.backendportfolio.base.user.entity.Provider; + import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; @@ -9,13 +11,16 @@ import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; -@Target({TYPE, FIELD, ANNOTATION_TYPE}) -@Retention(RUNTIME) -@Constraint(validatedBy = PasswordValidator.class) @Documented +@Retention(RUNTIME) +@Target({TYPE, FIELD, ANNOTATION_TYPE}) +@Constraint(validatedBy = PasswordValidator.class) public @interface ValidPassword { - String message() default "Invalid password"; + String message() default + "The password must have at least: a special character, a number, a uppercase and a lowercase "; + + Provider provider() default Provider.LOCAL; Class[] groups() default {}; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4af4807..d0849fc 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,6 @@ com: hideyoshi: - frontEndPath: ${FRONTEND_PATH} + frontendPath: ${FRONTEND_PATH} frontendConnectionType: ${FRONTEND_CONNECTION_TYPE} tokenSecret: ${TOKEN_SECRET} accessTokenDuration: ${ACCESS_TOKEN_DURATION} @@ -17,6 +17,26 @@ server: spring: + security: + oauth2: + client: + registration: + + google: + clientId: ${GOOGLE_CLIENT_ID} + clientSecret: ${GOOGLE_CLIENT_SECRET} + redirectUri: ${GOOGLE_REDIRECT_URL} + scope: + - email + - profile + + github: + clientId: ${GITHUB_CLIENT_ID} + clientSecret: ${GITHUB_CLIENT_SECRET} + redirectUri: ${GITHUB_REDIRECT_URL} + scope: + - user + datasource: url: jdbc:${DATABASE_URL} username: ${DATABASE_USERNAME} diff --git a/src/main/resources/db/changelog/client/db.changelog-client.yml b/src/main/resources/db/changelog/client/db.changelog-client.yml index 3ba2038..db79302 100644 --- a/src/main/resources/db/changelog/client/db.changelog-client.yml +++ b/src/main/resources/db/changelog/client/db.changelog-client.yml @@ -8,4 +8,14 @@ databaseChangeLog: encoding: utf8 path: sqls/db-table-model-client.sql relativeToChangelogFile: true - dbms: postgresql \ No newline at end of file + dbms: postgresql + + - changeSet: + id: adds-user-provider + author: vitor.h.n.batista@gmail.com + changes: + - sqlFile: + encoding: utf8 + path: sqls/adds-user-provider.sql + relativeToChangelogFile: true + dbms: postgresql diff --git a/src/main/resources/db/changelog/client/sqls/adds-user-provider.sql b/src/main/resources/db/changelog/client/sqls/adds-user-provider.sql new file mode 100644 index 0000000..4c2dd2b --- /dev/null +++ b/src/main/resources/db/changelog/client/sqls/adds-user-provider.sql @@ -0,0 +1,14 @@ +alter table if exists auth."user" + rename column "full_name" to "name"; + +ALTER TABLE IF EXISTS auth.user + ADD COLUMN IF NOT EXISTS provider VARCHAR + CHECK ( provider IN ('google', 'github', 'local') ) DEFAULT 'local' NOT NULL; + +ALTER TABLE auth."user" + DROP CONSTRAINT IF EXISTS client_email_unique; + +ALTER TABLE auth."user" + DROP CONSTRAINT IF EXISTS user_email_provider_unique; +ALTER TABLE auth."user" + ADD CONSTRAINT user_email_provider_unique UNIQUE (email, provider); \ No newline at end of file diff --git a/src/test/java/com/hideyoshi/backendportfolio/base/user/repo/UserRepositoryTest.java b/src/test/java/com/hideyoshi/backendportfolio/base/user/repo/UserRepositoryTest.java index 0e526c4..0ff60f2 100644 --- a/src/test/java/com/hideyoshi/backendportfolio/base/user/repo/UserRepositoryTest.java +++ b/src/test/java/com/hideyoshi/backendportfolio/base/user/repo/UserRepositoryTest.java @@ -1,5 +1,6 @@ package com.hideyoshi.backendportfolio.base.user.repo; +import com.hideyoshi.backendportfolio.base.user.entity.Provider; import com.hideyoshi.backendportfolio.base.user.entity.Role; import com.hideyoshi.backendportfolio.base.user.entity.User; import com.hideyoshi.backendportfolio.base.user.model.UserDTO; @@ -58,13 +59,14 @@ class UserRepositoryTest { } private User createEntity() { - return new UserDTO( - "Clark Kent", - "superman@gmail.com", - "Superman", - "password", - List.of(Role.USER) - ).toEntity(); + return UserDTO.builder() + .name("Clark Kent") + .email("superman@gmail.com") + .username("Superman") + .password("password") + .provider(Provider.LOCAL) + .roles(List.of(Role.USER)) + .build().toEntity(); } } \ No newline at end of file diff --git a/src/test/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImplTest.java b/src/test/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImplTest.java index 033a427..5d18242 100644 --- a/src/test/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImplTest.java +++ b/src/test/java/com/hideyoshi/backendportfolio/base/user/service/UserServiceImplTest.java @@ -1,6 +1,7 @@ package com.hideyoshi.backendportfolio.base.user.service; import com.hideyoshi.backendportfolio.base.security.service.AuthService; +import com.hideyoshi.backendportfolio.base.user.entity.Provider; import com.hideyoshi.backendportfolio.base.user.entity.Role; import com.hideyoshi.backendportfolio.base.user.entity.User; import com.hideyoshi.backendportfolio.base.user.model.UserDTO; @@ -349,15 +350,15 @@ class UserServiceImplTest { } private UserDTO createUser() { - UserDTO userCreated = new UserDTO( - "Clark Kent", - "superman@gmail.com", - "Superman", - "password", - List.of(Role.USER) - ); - userCreated.setId(1L); - return userCreated; + return UserDTO.builder() + .id(1L) + .name("Clark Kent") + .email("superman@gmail.com") + .username("Superman") + .password("password") + .provider(Provider.LOCAL) + .roles(List.of(Role.USER)) + .build(); } } \ No newline at end of file