Merge pull request #2 from HideyoshiNakazone/devel

[v0.0.2] Adds Google and Github OAuth2 Authentication
This commit is contained in:
2022-11-10 01:20:26 -03:00
committed by GitHub
25 changed files with 514 additions and 149 deletions

10
pom.xml
View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.1</version>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hideyoshi</groupId>
@@ -24,7 +24,7 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.7.3</version>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -34,6 +34,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
@@ -46,7 +50,7 @@
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.0.0</version>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>

View File

@@ -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);

View File

@@ -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()) {

View File

@@ -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();

View File

@@ -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<String,TokenDTO> 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);
}
}

View File

@@ -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<String> notProtectedPaths = Arrays.asList(
"/user/login"
);
return notProtectedPaths.contains(path);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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.");
}
}

View File

@@ -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<OAuth2AuthorizationRequest> {
@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)
);
}
}

View File

@@ -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;
}

View File

@@ -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<String, TokenDTO> tokens = this.generateTokens(userSaved, algorithm, request);
HashMap<String, TokenDTO> 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);
}
}

View File

@@ -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<Void> 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<Void> 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<UserDTO> 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<Void> deleteUser(@PathVariable("id") Long id) {
this.userService.deleteUser(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}

View File

@@ -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.");
}
}

View File

@@ -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

View File

@@ -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<Role> 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<? extends GrantedAuthority> 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();
}
}

View File

@@ -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);
}

View File

@@ -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<ValidPassword, String> {
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();
}
}
}

View File

@@ -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 {};

View File

@@ -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}

View File

@@ -9,3 +9,13 @@ databaseChangeLog:
path: sqls/db-table-model-client.sql
relativeToChangelogFile: true
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

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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();
}
}