From 493d7ad1762f87786012e697a8e936ae2f682751 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Fri, 2 Feb 2024 12:55:46 +0100 Subject: [PATCH 01/36] chore: removed .idea --- .gitignore | 3 ++- .idea/.gitignore | 3 --- .idea/misc.xml | 6 ------ .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ .idea/wiq_en2b.iml | 9 --------- 6 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/wiq_en2b.iml diff --git a/.gitignore b/.gitignore index 8bbe72a8..613d1130 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules coverage -docs/build \ No newline at end of file +docs/build +.idea \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d33521..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 07115cdf..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 5668832c..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/wiq_en2b.iml b/.idea/wiq_en2b.iml deleted file mode 100644 index d6ebd480..00000000 --- a/.idea/wiq_en2b.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file From 6a2d1243379e3ad5f4aa79ca3518d3a735537e88 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 20:14:49 +0100 Subject: [PATCH 02/36] feat: auth controller --- api/pom.xml | 6 +++++ .../lab/en2b/quizapi/auth/AuthController.java | 22 +++++++++++++++++++ .../lab/en2b/quizapi/auth/AuthService.java | 14 ++++++++++++ .../lab/en2b/quizapi/auth/dtos/LoginDto.java | 13 +++++++++++ 4 files changed, 55 insertions(+) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/AuthController.java create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/AuthService.java create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java diff --git a/api/pom.xml b/api/pom.xml index 70e9a137..a12a6ded 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -20,6 +20,12 @@ org.springframework.boot spring-boot-starter-data-jpa + 3.1.4 + + + jakarta.validation + jakarta.validation-api + 3.0.2 org.springframework.boot diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java new file mode 100644 index 00000000..d94b4c0f --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java @@ -0,0 +1,22 @@ +package lab.en2b.quizapi.auth; + +import jakarta.validation.Valid; +import lab.en2b.quizapi.auth.dtos.LoginDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +@RestController +@RequestMapping("/auth") +public class AuthController { + + @Autowired + private AuthService authService; + + @PostMapping("/login") + public ResponseEntity loginUser(@Valid @RequestBody LoginDto loginRequest){ + return authService.login(loginRequest); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java new file mode 100644 index 00000000..5f14fd4a --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -0,0 +1,14 @@ +package lab.en2b.quizapi.auth; + +import lab.en2b.quizapi.auth.dtos.LoginDto; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + public ResponseEntity login(LoginDto loginRequest) { + throw new UnsupportedOperationException(); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java new file mode 100644 index 00000000..1f0eecf0 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java @@ -0,0 +1,13 @@ +package lab.en2b.quizapi.auth.dtos; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class LoginDto { + private String email; + private String password; +} From f5b77914a599e42d9f2be6c56aa3025385bd7c78 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 20:17:09 +0100 Subject: [PATCH 03/36] feat: register endpoint --- .../lab/en2b/quizapi/auth/AuthController.java | 6 ++++++ .../lab/en2b/quizapi/auth/AuthService.java | 5 +++++ .../en2b/quizapi/auth/dtos/RegisterDto.java | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java index d94b4c0f..3ce5da36 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java @@ -2,6 +2,7 @@ import jakarta.validation.Valid; import lab.en2b.quizapi.auth.dtos.LoginDto; +import lab.en2b.quizapi.auth.dtos.RegisterDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -15,6 +16,11 @@ public class AuthController { @Autowired private AuthService authService; + @PostMapping("/register") + public ResponseEntity registerUser(@Valid @RequestBody RegisterDto registerRequest){ + return authService.register(registerRequest); + } + @PostMapping("/login") public ResponseEntity loginUser(@Valid @RequestBody LoginDto loginRequest){ return authService.login(loginRequest); diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 5f14fd4a..0d46fe38 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -1,6 +1,7 @@ package lab.en2b.quizapi.auth; import lab.en2b.quizapi.auth.dtos.LoginDto; +import lab.en2b.quizapi.auth.dtos.RegisterDto; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -11,4 +12,8 @@ public class AuthService { public ResponseEntity login(LoginDto loginRequest) { throw new UnsupportedOperationException(); } + + public ResponseEntity register(RegisterDto registerRequest) { + throw new UnsupportedOperationException(); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java new file mode 100644 index 00000000..389b8583 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java @@ -0,0 +1,18 @@ +package lab.en2b.quizapi.auth.dtos; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class RegisterDto { + @NonNull + private String email; + @NonNull + private String username; + @NonNull + private String password; +} From 2126f72ba3fe78367aef3e35550293ee52b56591 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 20:17:37 +0100 Subject: [PATCH 04/36] feat: non null checks in LoginDto --- api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java index 1f0eecf0..5e7a0ec5 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java @@ -3,11 +3,14 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.NonNull; @NoArgsConstructor @AllArgsConstructor @Data public class LoginDto { + @NonNull private String email; + @NonNull private String password; } From 51e37fee67f2283b3a0745dfbcc459fdf9332172 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 20:22:28 +0100 Subject: [PATCH 05/36] feat: refresh token endpoint --- api/pom.xml | 5 +++++ .../lab/en2b/quizapi/auth/AuthController.java | 6 ++++++ .../java/lab/en2b/quizapi/auth/AuthService.java | 5 +++++ .../en2b/quizapi/auth/dtos/RefreshTokenDto.java | 16 ++++++++++++++++ 4 files changed, 32 insertions(+) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenDto.java diff --git a/api/pom.xml b/api/pom.xml index a12a6ded..e813a6d1 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -52,6 +52,11 @@ spring-security-test test + + com.fasterxml.jackson.core + jackson-annotations + 2.15.2 + diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java index 3ce5da36..dd755545 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java @@ -2,6 +2,7 @@ import jakarta.validation.Valid; import lab.en2b.quizapi.auth.dtos.LoginDto; +import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -25,4 +26,9 @@ public ResponseEntity registerUser(@Valid @RequestBody RegisterDto registerRe public ResponseEntity loginUser(@Valid @RequestBody LoginDto loginRequest){ return authService.login(loginRequest); } + + @PostMapping("/refresh-token") + public ResponseEntity refreshToken(@Valid RefreshTokenDto refreshTokenRequest){ + return authService.refreshToken(refreshTokenRequest); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 0d46fe38..01067f0f 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -1,6 +1,7 @@ package lab.en2b.quizapi.auth; import lab.en2b.quizapi.auth.dtos.LoginDto; +import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -16,4 +17,8 @@ public ResponseEntity login(LoginDto loginRequest) { public ResponseEntity register(RegisterDto registerRequest) { throw new UnsupportedOperationException(); } + + public ResponseEntity refreshToken(RefreshTokenDto refreshTokenRequest) { + throw new UnsupportedOperationException(); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenDto.java new file mode 100644 index 00000000..f327ae8a --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenDto.java @@ -0,0 +1,16 @@ +package lab.en2b.quizapi.auth.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class RefreshTokenDto { + @NonNull + @JsonProperty("refresh_token") + private String refreshToken; +} From d9376bea81fe76fb1c04bbe31a4025b9af7af836 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 20:49:08 +0100 Subject: [PATCH 06/36] feat: mocked security config --- api/pom.xml | 20 ++++++ .../lab/en2b/quizapi/auth/UserService.java | 14 +++++ .../quizapi/auth/config/SecurityConfig.java | 61 +++++++++++++++++++ .../en2b/quizapi/auth/jwt/JwtAuthFilter.java | 46 ++++++++++++++ .../lab/en2b/quizapi/auth/jwt/JwtService.java | 15 +++++ 5 files changed, 156 insertions(+) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/UserService.java create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java diff --git a/api/pom.xml b/api/pom.xml index e813a6d1..bba945c2 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -57,6 +57,26 @@ jackson-annotations 2.15.2 + + io.jsonwebtoken + jjwt-api + 0.12.1 + + + io.jsonwebtoken + jjwt-impl + 0.12.1 + + + io.jsonwebtoken + jjwt-jackson + 0.12.1 + + + org.apache.tomcat.embed + tomcat-embed-core + 10.1.13 + diff --git a/api/src/main/java/lab/en2b/quizapi/auth/UserService.java b/api/src/main/java/lab/en2b/quizapi/auth/UserService.java new file mode 100644 index 00000000..883c04e5 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/UserService.java @@ -0,0 +1,14 @@ +package lab.en2b.quizapi.auth; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class UserService implements UserDetailsService { + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + throw new UnsupportedOperationException(); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java new file mode 100644 index 00000000..e77b3f6a --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -0,0 +1,61 @@ +package lab.en2b.quizapi.auth.config; + +import lab.en2b.quizapi.auth.UserService; +import lab.en2b.quizapi.auth.jwt.JwtAuthFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfig { + + @Autowired + private JwtAuthFilter authFilter; + @Autowired + public UserService userService; + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Bean + public AuthenticationManager authManager(HttpSecurity http) throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = + http.getSharedObject(AuthenticationManagerBuilder.class); + DaoAuthenticationProvider userPasswordProvider = new DaoAuthenticationProvider(); + userPasswordProvider.setUserDetailsService(userService); + userPasswordProvider.setPasswordEncoder(passwordEncoder()); + authenticationManagerBuilder.authenticationProvider(userPasswordProvider); + return authenticationManagerBuilder.build(); + } + /** + * Security filter used for filtering all petitions, applying cors and asking for authentication among other things + * @param http the http request to filter + * @return the filtered request + * @throws Exception if any problem happens when filtering + */ + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.authorizeHttpRequests(authorize -> + authorize.requestMatchers("/auth/login", "/auth/register","/auth/refresh-token").permitAll() + .anyRequest().authenticated()) + .cors(Customizer.withDefaults()) + //TODO: add exception handling + .sessionManagement(configuration -> configuration.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .build(); + } + + +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java new file mode 100644 index 00000000..d9b0369f --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -0,0 +1,46 @@ +package lab.en2b.quizapi.auth.jwt; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lab.en2b.quizapi.auth.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +@Component +public class JwtAuthFilter extends OncePerRequestFilter { + + @Autowired + private JwtService jwtService; + + @Autowired + private UserService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String authHeader = request.getHeader("Authorization"); + String token = null; + String username = null; + if (authHeader != null && authHeader.startsWith("Bearer ")) { + token = authHeader.substring(7); + username = jwtService.extractUsername(token); + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + if (jwtService.validateToken(token, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); + } +} + diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java new file mode 100644 index 00000000..a2c6c76b --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -0,0 +1,15 @@ +package lab.en2b.quizapi.auth.jwt; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +@Service +public class JwtService { + public String extractUsername(String token) { + throw new UnsupportedOperationException(); + } + + public boolean validateToken(String token, UserDetails userDetails) { + throw new UnsupportedOperationException(); + } +} From c1076831a76e60a22bfd56de8be850c4806a0ab1 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 21:01:59 +0100 Subject: [PATCH 07/36] feat: user and role tables --- .../quizapi/auth/config/SecurityConfig.java | 2 +- .../quizapi/auth/config/UserDetailsImpl.java | 62 ++++++++++++++++ .../en2b/quizapi/auth/jwt/JwtAuthFilter.java | 2 +- .../lab/en2b/quizapi/auth/jwt/JwtService.java | 4 +- .../main/java/lab/en2b/quizapi/user/User.java | 72 +++++++++++++++++++ .../lab/en2b/quizapi/user/UserRepository.java | 11 +++ .../quizapi/{auth => user}/UserService.java | 8 ++- .../java/lab/en2b/quizapi/user/role/Role.java | 29 ++++++++ 8 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java create mode 100644 api/src/main/java/lab/en2b/quizapi/user/User.java create mode 100644 api/src/main/java/lab/en2b/quizapi/user/UserRepository.java rename api/src/main/java/lab/en2b/quizapi/{auth => user}/UserService.java (60%) create mode 100644 api/src/main/java/lab/en2b/quizapi/user/role/Role.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index e77b3f6a..33c11b65 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -1,6 +1,6 @@ package lab.en2b.quizapi.auth.config; -import lab.en2b.quizapi.auth.UserService; +import lab.en2b.quizapi.user.UserService; import lab.en2b.quizapi.auth.jwt.JwtAuthFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java new file mode 100644 index 00000000..527a52e0 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java @@ -0,0 +1,62 @@ +package lab.en2b.quizapi.auth.config; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lab.en2b.quizapi.user.User; +import lab.en2b.quizapi.user.role.Role; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.io.Serial; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +@Getter +@AllArgsConstructor +public class UserDetailsImpl implements UserDetails { + @Serial + private static final long serialVersionUID = 1L; + private Long id; + private String username; + private String email; + @JsonIgnore + private String password; + private Collection authorities; + public static UserDetailsImpl build(User user) { + List authorities = new ArrayList<>(); + for(Role role : user.getRoles()){ + authorities.add(new SimpleGrantedAuthority(role.getName())); + } + return new UserDetailsImpl(user.getId(),user.getName() , user.getEmail(), user.getPassword(), authorities); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + @Override + public boolean isAccountNonLocked() { + return true; + } + @Override + public boolean isCredentialsNonExpired() { + return true; + } + @Override + public boolean isEnabled() { + return true; + } + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + UserDetailsImpl user = (UserDetailsImpl) o; + return Objects.equals(id, user.id); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index d9b0369f..5ca1d5ed 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -3,7 +3,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lab.en2b.quizapi.auth.UserService; +import lab.en2b.quizapi.user.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index a2c6c76b..3ec38b06 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -1,9 +1,9 @@ package lab.en2b.quizapi.auth.jwt; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; -@Service +@Component public class JwtService { public String extractUsername(String token) { throw new UnsupportedOperationException(); diff --git a/api/src/main/java/lab/en2b/quizapi/user/User.java b/api/src/main/java/lab/en2b/quizapi/user/User.java new file mode 100644 index 00000000..6b3bec08 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/user/User.java @@ -0,0 +1,72 @@ +package lab.en2b.quizapi.user; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lab.en2b.quizapi.user.role.Role; +import lombok.*; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.Set; + +@Entity +@Table( name = "users", + uniqueConstraints = { + @UniqueConstraint(columnNames = "email"), + @UniqueConstraint(columnNames = "name") + }) +@RequiredArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@AllArgsConstructor +@Builder +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Setter(AccessLevel.NONE) + private Long id; + + @NotBlank + @Size(max=255) + @NonNull + private String name; + + @NotBlank + @Size(max = 255) + @Email + @NonNull + private String email; + + @NotBlank + @Size(max = 255) + @NonNull + private String password; + + @Column(name = "refresh_token",unique = true) + @Size(max = 255) + private String refreshToken; + + @Column(name="refresh_expiration") + private Instant refreshExpiration; + + @NotNull + @ManyToMany + @JoinTable(name="users_roles", + joinColumns= + @JoinColumn(name="user_id", referencedColumnName="id"), + inverseJoinColumns= + @JoinColumn(name="role_id", referencedColumnName="id") + ) + @JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "permissions"}) + @JsonProperty("role") + private Set roles; +} diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java new file mode 100644 index 00000000..1e371aff --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java @@ -0,0 +1,11 @@ +package lab.en2b.quizapi.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java similarity index 60% rename from api/src/main/java/lab/en2b/quizapi/auth/UserService.java rename to api/src/main/java/lab/en2b/quizapi/user/UserService.java index 883c04e5..c387c7aa 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -1,14 +1,18 @@ -package lab.en2b.quizapi.auth; +package lab.en2b.quizapi.user; +import lab.en2b.quizapi.auth.config.UserDetailsImpl; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class UserService implements UserDetailsService { + private final UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - throw new UnsupportedOperationException(); + return UserDetailsImpl.build(userRepository.findByUsername(username).orElseThrow()); } } diff --git a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java b/api/src/main/java/lab/en2b/quizapi/user/role/Role.java new file mode 100644 index 00000000..a3c58cd0 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/user/role/Role.java @@ -0,0 +1,29 @@ +package lab.en2b.quizapi.user.role; + +import com.fasterxml.jackson.annotation.JsonView; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.*; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +@Entity +@Table(name = "roles") +@NoArgsConstructor +@RequiredArgsConstructor +@AllArgsConstructor +@Setter +@Getter +@Builder +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Setter(AccessLevel.NONE) + private Long id; + + @NonNull + @NotBlank + @Size(max=255) + private String name; +} From 575f170b8a915c3acc67ebcd06a63bdba19bd898 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 21:18:14 +0100 Subject: [PATCH 08/36] feat: jwt auth filter --- .../en2b/quizapi/auth/jwt/JwtAuthFilter.java | 16 ++-- .../lab/en2b/quizapi/auth/jwt/JwtService.java | 85 ++++++++++++++++++- .../lab/en2b/quizapi/user/UserRepository.java | 2 +- .../lab/en2b/quizapi/user/UserService.java | 2 +- .../java/lab/en2b/quizapi/user/role/Role.java | 3 - 5 files changed, 91 insertions(+), 17 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index 5ca1d5ed..8806cbc1 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -4,6 +4,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lab.en2b.quizapi.user.UserService; +import lombok.NonNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; @@ -23,24 +24,23 @@ public class JwtAuthFilter extends OncePerRequestFilter { private UserService userDetailsService; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); String token = null; String username = null; if (authHeader != null && authHeader.startsWith("Bearer ")) { token = authHeader.substring(7); - username = jwtService.extractUsername(token); + username = jwtService.getSubjectFromJwtToken(token); } - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null && jwtService.validateJwtToken(token)) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); - if (jwtService.validateToken(token, userDetails)) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authToken); - } + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); } filterChain.doFilter(request, response); } + } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index 3ec38b06..e37874e9 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -1,15 +1,92 @@ package lab.en2b.quizapi.auth.jwt; -import org.springframework.security.core.userdetails.UserDetails; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import lab.en2b.quizapi.auth.config.UserDetailsImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.function.Function; + @Component public class JwtService { - public String extractUsername(String token) { - throw new UnsupportedOperationException(); + + private static final Logger logger = LoggerFactory.getLogger(JwtService.class); + + @Value("${JWT_SECRET}") + private String jwtSecret; + + @Value("${JWT_EXPIRATION_MS}") + private int jwtExpirationMs; + + public String generateJwtTokenUserPassword(Authentication authentication) { + UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); + + return Jwts.builder() + .subject(userPrincipal.getEmail()) + .issuedAt(new Date()) + .claim("type","user-password") + .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) + .signWith(getSignInKey()) + .compact(); } - public boolean validateToken(String token, UserDetails userDetails) { + public String generateTokenFromUsername(String email) { + return Jwts.builder() + .subject(email) + .issuedAt(new Date()) + .claim("type","user-password") + .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) + .signWith(getSignInKey()) + .compact(); + } + public String extractUsername(String token) { throw new UnsupportedOperationException(); } + public boolean validateJwtToken(String authToken) { + try { + Jwts.parser().verifyWith(getSignInKey()).build().parseSignedClaims(authToken); + return true; + } catch (SignatureException e) { + logger.error("Invalid JWT signature: {}", e.getMessage()); + } catch (MalformedJwtException e) { + logger.error("Invalid JWT token: {}", e.getMessage()); + } catch (ExpiredJwtException e) { + logger.error("JWT token is expired: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + logger.error("JWT token is unsupported: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + logger.error("JWT claims string is empty: {}", e.getMessage()); + } + return false; + } + private Claims extractAllClaims(String token){ + return Jwts.parser() + .verifyWith(getSignInKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + public T extractClaim(String token, Function claimsResolver){ + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + public String getSubjectFromJwtToken(String token) { + if(validateJwtToken(token)){ + return extractClaim(token, Claims::getSubject); + }else{ + throw new IllegalArgumentException(); + } + } + private SecretKey getSignInKey(){ + byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); + return Keys.hmacShaKeyFor(keyBytes); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java index 1e371aff..c75e9dd1 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java @@ -7,5 +7,5 @@ @Repository public interface UserRepository extends JpaRepository { - Optional findByUsername(String username); + Optional findUserByName(String username); } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index c387c7aa..592afcd9 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -13,6 +13,6 @@ public class UserService implements UserDetailsService { private final UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - return UserDetailsImpl.build(userRepository.findByUsername(username).orElseThrow()); + return UserDetailsImpl.build(userRepository.findUserByName(username).orElseThrow()); } } diff --git a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java b/api/src/main/java/lab/en2b/quizapi/user/role/Role.java index a3c58cd0..9c635bd1 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java +++ b/api/src/main/java/lab/en2b/quizapi/user/role/Role.java @@ -1,12 +1,9 @@ package lab.en2b.quizapi.user.role; -import com.fasterxml.jackson.annotation.JsonView; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.*; -import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; @Entity @Table(name = "roles") From c65dcd769f2f48006d86ce7014d799dd71ff963e Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 21:24:07 +0100 Subject: [PATCH 09/36] refactor: jwt auth filter --- .../en2b/quizapi/auth/jwt/JwtAuthFilter.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index 8806cbc1..d1298a63 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -11,6 +11,7 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @@ -24,23 +25,31 @@ public class JwtAuthFilter extends OncePerRequestFilter { private UserService userDetailsService; @Override - protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { - String authHeader = request.getHeader("Authorization"); - String token = null; - String username = null; - if (authHeader != null && authHeader.startsWith("Bearer ")) { - token = authHeader.substring(7); - username = jwtService.getSubjectFromJwtToken(token); - } + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String token = parseJwt(request); + String username = jwtService.getSubjectFromJwtToken(token); - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null && jwtService.validateJwtToken(token)) { + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, + null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } filterChain.doFilter(request, response); } + private boolean isValidJwt(String token) { + return token != null && jwtService.validateJwtToken(token); + } + + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7); + } + return null; + } } From 4dc7fc7f1abc2c70022fb51eaf7f73fbe0606397 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 21:27:37 +0100 Subject: [PATCH 10/36] feat: added user to role --- api/src/main/java/lab/en2b/quizapi/user/role/Role.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java b/api/src/main/java/lab/en2b/quizapi/user/role/Role.java index 9c635bd1..f98b63b8 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java +++ b/api/src/main/java/lab/en2b/quizapi/user/role/Role.java @@ -1,10 +1,16 @@ package lab.en2b.quizapi.user.role; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import lab.en2b.quizapi.user.User; import lombok.*; +import java.util.Set; + @Entity @Table(name = "roles") @NoArgsConstructor @@ -23,4 +29,8 @@ public class Role { @NotBlank @Size(max=255) private String name; + + @ManyToMany(mappedBy ="roles") + @JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "roles"}) + private Set users; } From 627ed44faefe864f8a9eaeb57362197edb7b3d7c Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 21:32:58 +0100 Subject: [PATCH 11/36] feat: jwt expiration --- api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java | 3 +-- api/src/main/resources/application.properties | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index e37874e9..a45d9e14 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -20,6 +20,7 @@ public class JwtService { private static final Logger logger = LoggerFactory.getLogger(JwtService.class); + //MUST BE SET AS ENVIRONMENT VARIABLE @Value("${JWT_SECRET}") private String jwtSecret; @@ -32,7 +33,6 @@ public String generateJwtTokenUserPassword(Authentication authentication) { return Jwts.builder() .subject(userPrincipal.getEmail()) .issuedAt(new Date()) - .claim("type","user-password") .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) .signWith(getSignInKey()) .compact(); @@ -42,7 +42,6 @@ public String generateTokenFromUsername(String email) { return Jwts.builder() .subject(email) .issuedAt(new Date()) - .claim("type","user-password") .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) .signWith(getSignInKey()) .compact(); diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index 8b137891..4f6f5c6b 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -1 +1 @@ - +JWT_EXPIRATION_MS= 86400000 From e510b825e99a0c8a3ba7ac512b2ca510bd612ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 4 Feb 2024 11:01:02 +0100 Subject: [PATCH 12/36] chore: name added in README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f1dab6f8..6c2e3952 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ This is a base repo for the [Software Architecture course](http://arquisoft.gith | Jorge Joaquín Gancedo Fernández | UO282161 | | Sergio Quintana Fernández | UO288090 | | Diego Villanueva Berros | UO283615 | +| Gonzalo Suárez Losada | UO283928 | This repo is a basic application composed of several components. From d1d48783569fa38e1b78ed26ee970abaff8cae53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 4 Feb 2024 11:08:02 +0100 Subject: [PATCH 13/36] chore: name added in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c2e3952..dda9a07c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Arquisoft_wiq_en2b&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Arquisoft_wiq_en2b) This is a base repo for the [Software Architecture course](http://arquisoft.github.io/) in [2023/2024 edition](https://arquisoft.github.io/course2324.html). - + ## Contributors | Nombre | UO | |---------------------------------|----------| From 204096bd93fd5ee4f39d47cc7cb18274690f29da Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:33:18 +0100 Subject: [PATCH 14/36] feat: email as jwt subject --- .../java/lab/en2b/quizapi/auth/config/SecurityConfig.java | 2 ++ .../lab/en2b/quizapi/auth/config/UserDetailsImpl.java | 5 +---- .../java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java | 7 ++++--- api/src/main/java/lab/en2b/quizapi/user/User.java | 8 ++------ .../main/java/lab/en2b/quizapi/user/UserRepository.java | 2 +- api/src/main/java/lab/en2b/quizapi/user/UserService.java | 4 ++-- 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 33c11b65..082b356d 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -34,9 +34,11 @@ public PasswordEncoder passwordEncoder() { public AuthenticationManager authManager(HttpSecurity http) throws Exception { AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); + DaoAuthenticationProvider userPasswordProvider = new DaoAuthenticationProvider(); userPasswordProvider.setUserDetailsService(userService); userPasswordProvider.setPasswordEncoder(passwordEncoder()); + authenticationManagerBuilder.authenticationProvider(userPasswordProvider); return authenticationManagerBuilder.build(); } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java index 527a52e0..99a5a03c 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java @@ -9,7 +9,6 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; -import java.io.Serial; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -18,8 +17,6 @@ @Getter @AllArgsConstructor public class UserDetailsImpl implements UserDetails { - @Serial - private static final long serialVersionUID = 1L; private Long id; private String username; private String email; @@ -31,7 +28,7 @@ public static UserDetailsImpl build(User user) { for(Role role : user.getRoles()){ authorities.add(new SimpleGrantedAuthority(role.getName())); } - return new UserDetailsImpl(user.getId(),user.getName() , user.getEmail(), user.getPassword(), authorities); + return new UserDetailsImpl(user.getId(),user.getUsername() , user.getEmail(), user.getPassword(), authorities); } @Override diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index d1298a63..d50480ab 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -28,10 +28,11 @@ public class JwtAuthFilter extends OncePerRequestFilter { protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { String token = parseJwt(request); - String username = jwtService.getSubjectFromJwtToken(token); + String email = jwtService.getSubjectFromJwtToken(token); - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) { - UserDetails userDetails = userDetailsService.loadUserByUsername(username); + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) { + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + // this invokes UsernamePasswordAuthenticationToken, although it uses email as subject not username UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); diff --git a/api/src/main/java/lab/en2b/quizapi/user/User.java b/api/src/main/java/lab/en2b/quizapi/user/User.java index 6b3bec08..90e47230 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/User.java +++ b/api/src/main/java/lab/en2b/quizapi/user/User.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonView; import jakarta.persistence.*; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; @@ -10,18 +9,15 @@ import jakarta.validation.constraints.Size; import lab.en2b.quizapi.user.role.Role; import lombok.*; -import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; import java.time.Instant; -import java.time.LocalDateTime; import java.util.Set; @Entity @Table( name = "users", uniqueConstraints = { @UniqueConstraint(columnNames = "email"), - @UniqueConstraint(columnNames = "name") + @UniqueConstraint(columnNames = "username") }) @RequiredArgsConstructor @NoArgsConstructor @@ -38,7 +34,7 @@ public class User { @NotBlank @Size(max=255) @NonNull - private String name; + private String username; @NotBlank @Size(max = 255) diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java index c75e9dd1..2c0e6e04 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java @@ -7,5 +7,5 @@ @Repository public interface UserRepository extends JpaRepository { - Optional findUserByName(String username); + Optional findUserByEmail(String email); } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index 592afcd9..48b0b497 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -12,7 +12,7 @@ public class UserService implements UserDetailsService { private final UserRepository userRepository; @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - return UserDetailsImpl.build(userRepository.findUserByName(username).orElseThrow()); + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + return UserDetailsImpl.build(userRepository.findUserByEmail(email).orElseThrow()); } } From 72eb50a654670537b75469499aece387a3373844 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:36:56 +0100 Subject: [PATCH 15/36] chore: commented authManager --- .../java/lab/en2b/quizapi/auth/config/SecurityConfig.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 082b356d..3ecdb0a5 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -30,6 +30,13 @@ public class SecurityConfig { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + /** + * Builds the authorization manager taking into account password encoding + * @param http the http request to secure + * @return the newly created authentication manager + * @throws Exception if something goes wrong when creating the manager + */ @Bean public AuthenticationManager authManager(HttpSecurity http) throws Exception { AuthenticationManagerBuilder authenticationManagerBuilder = From 35c56557b1ab392cd4a0d740e39530847459404a Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:48:21 +0100 Subject: [PATCH 16/36] feat: dummy login implementation --- .../lab/en2b/quizapi/auth/AuthService.java | 35 +++++++++++++++++-- .../quizapi/auth/dtos/JwtResponseDto.java | 22 ++++++++++++ .../lab/en2b/quizapi/user/UserService.java | 4 +++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/dtos/JwtResponseDto.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 01067f0f..70660e79 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -1,17 +1,48 @@ package lab.en2b.quizapi.auth; +import lab.en2b.quizapi.auth.config.UserDetailsImpl; +import lab.en2b.quizapi.auth.dtos.JwtResponseDto; import lab.en2b.quizapi.auth.dtos.LoginDto; import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; +import lab.en2b.quizapi.auth.jwt.JwtService; +import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class AuthService { - public ResponseEntity login(LoginDto loginRequest) { - throw new UnsupportedOperationException(); + private final JwtService jwtService; + private final AuthenticationManager authenticationManager; + private final UserService userService; + @Transactional + public ResponseEntity login(LoginDto loginRequest){ + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword())); + SecurityContextHolder.getContext().setAuthentication(authentication); + String jwt = jwtService.generateJwtTokenUserPassword(authentication); + UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); + List roles = userDetails.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + String refreshToken = userService.createRefreshToken(userDetails.getId()); + return ResponseEntity.ok(new JwtResponseDto(jwt, + refreshToken, + userDetails.getId(), + userDetails.getUsername(), + userDetails.getEmail(), + roles)); } public ResponseEntity register(RegisterDto registerRequest) { diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/JwtResponseDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/JwtResponseDto.java new file mode 100644 index 00000000..d50c5f83 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/JwtResponseDto.java @@ -0,0 +1,22 @@ +package lab.en2b.quizapi.auth.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class JwtResponseDto { + private String token; + @JsonProperty("refresh_token") + private String refreshToken; + @JsonProperty("user_id") + private Long userId; + private String username; + private String email; + private List roles; +} diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index 48b0b497..b56e7ebc 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -15,4 +15,8 @@ public class UserService implements UserDetailsService { public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return UserDetailsImpl.build(userRepository.findUserByEmail(email).orElseThrow()); } + + public String createRefreshToken(Long id) { + throw new UnsupportedOperationException(); + } } From 4a93b0d86bf259ff1f4d9d4c915591ba2c3c7686 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:51:37 +0100 Subject: [PATCH 17/36] chore: cleaned code --- .../java/lab/en2b/quizapi/auth/AuthService.java | 14 ++++++-------- .../en2b/quizapi/auth/config/UserDetailsImpl.java | 7 +++++++ .../java/lab/en2b/quizapi/auth/jwt/JwtService.java | 3 +++ .../java/lab/en2b/quizapi/user/UserService.java | 4 ---- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 70660e79..fd658897 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -31,18 +31,16 @@ public ResponseEntity login(LoginDto loginRequest){ Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); - String jwt = jwtService.generateJwtTokenUserPassword(authentication); UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); - List roles = userDetails.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .collect(Collectors.toList()); - String refreshToken = userService.createRefreshToken(userDetails.getId()); - return ResponseEntity.ok(new JwtResponseDto(jwt, - refreshToken, + + return ResponseEntity.ok(new JwtResponseDto( + jwtService.generateJwtTokenUserPassword(authentication), + jwtService.createRefreshToken(userDetails.getId()), userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), - roles)); + userDetails.getStringRoles()) + ); } public ResponseEntity register(RegisterDto registerRequest) { diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java index 99a5a03c..776062d7 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java @@ -13,6 +13,7 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; @Getter @AllArgsConstructor @@ -56,4 +57,10 @@ public boolean equals(Object o) { UserDetailsImpl user = (UserDetailsImpl) o; return Objects.equals(id, user.id); } + + public List getStringRoles() { + return getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index a45d9e14..8381eab3 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -27,6 +27,9 @@ public class JwtService { @Value("${JWT_EXPIRATION_MS}") private int jwtExpirationMs; + public String createRefreshToken(Long id) { + throw new UnsupportedOperationException(); + } public String generateJwtTokenUserPassword(Authentication authentication) { UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index b56e7ebc..48b0b497 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -15,8 +15,4 @@ public class UserService implements UserDetailsService { public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return UserDetailsImpl.build(userRepository.findUserByEmail(email).orElseThrow()); } - - public String createRefreshToken(Long id) { - throw new UnsupportedOperationException(); - } } From 12e95d30de95af94f03fb419bc555058237a639b Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:54:20 +0100 Subject: [PATCH 18/36] chore: commented code --- api/src/main/java/lab/en2b/quizapi/auth/AuthService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index fd658897..ed92d039 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -25,7 +25,12 @@ public class AuthService { private final JwtService jwtService; private final AuthenticationManager authenticationManager; - private final UserService userService; + + /** + * Creates a session for a user. Throws an 401 unauthorized exception otherwise + * @param loginRequest the request containing the login info + * @return a response containing a fresh jwt token and a refresh token + */ @Transactional public ResponseEntity login(LoginDto loginRequest){ Authentication authentication = authenticationManager.authenticate( From 6563fb7f1ffb6efd34c490909bea364842a99601 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:57:05 +0100 Subject: [PATCH 19/36] chore: unused imports --- api/src/main/java/lab/en2b/quizapi/auth/AuthService.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index ed92d039..886b3f6f 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -6,20 +6,15 @@ import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; import lab.en2b.quizapi.auth.jwt.JwtService; -import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor public class AuthService { From ca736c42ce5a0776234e76858cbaa50574b9c89b Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 19:22:24 +0100 Subject: [PATCH 20/36] feat: register implementation --- .../lab/en2b/quizapi/auth/AuthService.java | 8 ++++++-- .../lab/en2b/quizapi/user/RoleRepository.java | 13 ++++++++++++ .../lab/en2b/quizapi/user/UserRepository.java | 6 +++++- .../lab/en2b/quizapi/user/UserService.java | 20 ++++++++++++++++++- 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 886b3f6f..5c418dc9 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -6,6 +6,7 @@ import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; import lab.en2b.quizapi.auth.jwt.JwtService; +import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; @@ -15,12 +16,14 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Set; + @Service @RequiredArgsConstructor public class AuthService { private final JwtService jwtService; private final AuthenticationManager authenticationManager; - + private final UserService userService; /** * Creates a session for a user. Throws an 401 unauthorized exception otherwise * @param loginRequest the request containing the login info @@ -44,7 +47,8 @@ public ResponseEntity login(LoginDto loginRequest){ } public ResponseEntity register(RegisterDto registerRequest) { - throw new UnsupportedOperationException(); + userService.createUser(registerRequest,Set.of("user")); + return ResponseEntity.ok("User registered successfully!"); } public ResponseEntity refreshToken(RefreshTokenDto refreshTokenRequest) { diff --git a/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java b/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java new file mode 100644 index 00000000..11401b1c --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java @@ -0,0 +1,13 @@ +package lab.en2b.quizapi.user; + + +import lab.en2b.quizapi.user.role.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(String roleName); +} diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java index 2c0e6e04..fa3eab46 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java @@ -7,5 +7,9 @@ @Repository public interface UserRepository extends JpaRepository { - Optional findUserByEmail(String email); + Optional findByEmail(String email); + + Boolean existsByUsername(String username); + + Boolean existsByEmail(String email); } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index 48b0b497..abbdb9b1 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -1,18 +1,36 @@ package lab.en2b.quizapi.user; import lab.en2b.quizapi.auth.config.UserDetailsImpl; +import lab.en2b.quizapi.auth.dtos.RegisterDto; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import java.util.Set; +import java.util.stream.Collectors; + @Service @RequiredArgsConstructor public class UserService implements UserDetailsService { private final UserRepository userRepository; + private final RoleRepository roleRepository; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - return UserDetailsImpl.build(userRepository.findUserByEmail(email).orElseThrow()); + return UserDetailsImpl.build(userRepository.findByEmail(email).orElseThrow()); + } + public void createUser(RegisterDto registerRequest, Set roleNames){ + if (userRepository.existsByEmail(registerRequest.getEmail()) || userRepository.existsByUsername(registerRequest.getUsername())) { + throw new IllegalArgumentException("Error: email is already in use!"); + } + + userRepository.save(User.builder() + .username(registerRequest.getUsername()) + .email(registerRequest.getEmail()) + .password(new BCryptPasswordEncoder().encode(registerRequest.getPassword())) + .roles(roleNames.stream().map( roleName -> roleRepository.findByName(roleName).orElseThrow()).collect(Collectors.toSet())) + .build()); } } From c4d0b28387b24764ef9d3b0e37dc8bdacfdd916c Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 19:24:07 +0100 Subject: [PATCH 21/36] chore: removed unused methods --- .../java/lab/en2b/quizapi/auth/jwt/JwtService.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index 8381eab3..9e528926 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -40,18 +40,6 @@ public String generateJwtTokenUserPassword(Authentication authentication) { .signWith(getSignInKey()) .compact(); } - - public String generateTokenFromUsername(String email) { - return Jwts.builder() - .subject(email) - .issuedAt(new Date()) - .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) - .signWith(getSignInKey()) - .compact(); - } - public String extractUsername(String token) { - throw new UnsupportedOperationException(); - } public boolean validateJwtToken(String authToken) { try { Jwts.parser().verifyWith(getSignInKey()).build().parseSignedClaims(authToken); From 2a0a6acaf6e4e32b088714a9e8e69f56113a743c Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:51:37 +0100 Subject: [PATCH 22/36] chore: cleaned code --- .../lab/en2b/quizapi/auth/AuthService.java | 26 +++++++++---------- .../quizapi/auth/config/UserDetailsImpl.java | 7 +++++ .../lab/en2b/quizapi/auth/jwt/JwtService.java | 3 +++ .../lab/en2b/quizapi/user/UserService.java | 4 --- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 70660e79..886b3f6f 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -6,43 +6,41 @@ import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; import lab.en2b.quizapi.auth.jwt.JwtService; -import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor public class AuthService { private final JwtService jwtService; private final AuthenticationManager authenticationManager; - private final UserService userService; + + /** + * Creates a session for a user. Throws an 401 unauthorized exception otherwise + * @param loginRequest the request containing the login info + * @return a response containing a fresh jwt token and a refresh token + */ @Transactional public ResponseEntity login(LoginDto loginRequest){ Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); - String jwt = jwtService.generateJwtTokenUserPassword(authentication); UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); - List roles = userDetails.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .collect(Collectors.toList()); - String refreshToken = userService.createRefreshToken(userDetails.getId()); - return ResponseEntity.ok(new JwtResponseDto(jwt, - refreshToken, + + return ResponseEntity.ok(new JwtResponseDto( + jwtService.generateJwtTokenUserPassword(authentication), + jwtService.createRefreshToken(userDetails.getId()), userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), - roles)); + userDetails.getStringRoles()) + ); } public ResponseEntity register(RegisterDto registerRequest) { diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java index 99a5a03c..776062d7 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java @@ -13,6 +13,7 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; @Getter @AllArgsConstructor @@ -56,4 +57,10 @@ public boolean equals(Object o) { UserDetailsImpl user = (UserDetailsImpl) o; return Objects.equals(id, user.id); } + + public List getStringRoles() { + return getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index a45d9e14..8381eab3 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -27,6 +27,9 @@ public class JwtService { @Value("${JWT_EXPIRATION_MS}") private int jwtExpirationMs; + public String createRefreshToken(Long id) { + throw new UnsupportedOperationException(); + } public String generateJwtTokenUserPassword(Authentication authentication) { UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index b56e7ebc..48b0b497 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -15,8 +15,4 @@ public class UserService implements UserDetailsService { public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return UserDetailsImpl.build(userRepository.findUserByEmail(email).orElseThrow()); } - - public String createRefreshToken(Long id) { - throw new UnsupportedOperationException(); - } } From c9ce864855a0dc50f1038b08318993cc5a3f1193 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 19:22:24 +0100 Subject: [PATCH 23/36] feat: register implementation --- .../lab/en2b/quizapi/auth/AuthService.java | 8 ++++++-- .../lab/en2b/quizapi/user/RoleRepository.java | 13 ++++++++++++ .../lab/en2b/quizapi/user/UserRepository.java | 6 +++++- .../lab/en2b/quizapi/user/UserService.java | 20 ++++++++++++++++++- 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 886b3f6f..5c418dc9 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -6,6 +6,7 @@ import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; import lab.en2b.quizapi.auth.jwt.JwtService; +import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; @@ -15,12 +16,14 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Set; + @Service @RequiredArgsConstructor public class AuthService { private final JwtService jwtService; private final AuthenticationManager authenticationManager; - + private final UserService userService; /** * Creates a session for a user. Throws an 401 unauthorized exception otherwise * @param loginRequest the request containing the login info @@ -44,7 +47,8 @@ public ResponseEntity login(LoginDto loginRequest){ } public ResponseEntity register(RegisterDto registerRequest) { - throw new UnsupportedOperationException(); + userService.createUser(registerRequest,Set.of("user")); + return ResponseEntity.ok("User registered successfully!"); } public ResponseEntity refreshToken(RefreshTokenDto refreshTokenRequest) { diff --git a/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java b/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java new file mode 100644 index 00000000..11401b1c --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java @@ -0,0 +1,13 @@ +package lab.en2b.quizapi.user; + + +import lab.en2b.quizapi.user.role.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(String roleName); +} diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java index 2c0e6e04..fa3eab46 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java @@ -7,5 +7,9 @@ @Repository public interface UserRepository extends JpaRepository { - Optional findUserByEmail(String email); + Optional findByEmail(String email); + + Boolean existsByUsername(String username); + + Boolean existsByEmail(String email); } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index 48b0b497..abbdb9b1 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -1,18 +1,36 @@ package lab.en2b.quizapi.user; import lab.en2b.quizapi.auth.config.UserDetailsImpl; +import lab.en2b.quizapi.auth.dtos.RegisterDto; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import java.util.Set; +import java.util.stream.Collectors; + @Service @RequiredArgsConstructor public class UserService implements UserDetailsService { private final UserRepository userRepository; + private final RoleRepository roleRepository; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - return UserDetailsImpl.build(userRepository.findUserByEmail(email).orElseThrow()); + return UserDetailsImpl.build(userRepository.findByEmail(email).orElseThrow()); + } + public void createUser(RegisterDto registerRequest, Set roleNames){ + if (userRepository.existsByEmail(registerRequest.getEmail()) || userRepository.existsByUsername(registerRequest.getUsername())) { + throw new IllegalArgumentException("Error: email is already in use!"); + } + + userRepository.save(User.builder() + .username(registerRequest.getUsername()) + .email(registerRequest.getEmail()) + .password(new BCryptPasswordEncoder().encode(registerRequest.getPassword())) + .roles(roleNames.stream().map( roleName -> roleRepository.findByName(roleName).orElseThrow()).collect(Collectors.toSet())) + .build()); } } From 6f569f593e524f6645a4a1f3305adeb78bf0091f Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 19:24:07 +0100 Subject: [PATCH 24/36] chore: removed unused methods --- .../java/lab/en2b/quizapi/auth/jwt/JwtService.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index 8381eab3..9e528926 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -40,18 +40,6 @@ public String generateJwtTokenUserPassword(Authentication authentication) { .signWith(getSignInKey()) .compact(); } - - public String generateTokenFromUsername(String email) { - return Jwts.builder() - .subject(email) - .issuedAt(new Date()) - .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) - .signWith(getSignInKey()) - .compact(); - } - public String extractUsername(String token) { - throw new UnsupportedOperationException(); - } public boolean validateJwtToken(String authToken) { try { Jwts.parser().verifyWith(getSignInKey()).build().parseSignedClaims(authToken); From 9f0097f31dcb820b1d3f826e33fd8abc88de170c Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 6 Feb 2024 11:59:14 +0100 Subject: [PATCH 25/36] fix!: cors filter --- .../quizapi/auth/config/SecurityConfig.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 3ecdb0a5..1838388a 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -16,6 +16,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; @Configuration @EnableWebSecurity @@ -30,7 +33,18 @@ public class SecurityConfig { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + // Configure CORS settings here + config.setAllowCredentials(true); + config.addAllowedOrigin("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } /** * Builds the authorization manager taking into account password encoding * @param http the http request to secure @@ -57,10 +71,9 @@ public AuthenticationManager authManager(HttpSecurity http) throws Exception { */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - return http.authorizeHttpRequests(authorize -> + return http.cors(Customizer.withDefaults()).authorizeHttpRequests(authorize -> authorize.requestMatchers("/auth/login", "/auth/register","/auth/refresh-token").permitAll() .anyRequest().authenticated()) - .cors(Customizer.withDefaults()) //TODO: add exception handling .sessionManagement(configuration -> configuration.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .build(); From 633222fcef21d2cd2114ab54a0790014e345d193 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 6 Feb 2024 12:54:35 +0100 Subject: [PATCH 26/36] chore: moved RoleRepository --- api/src/main/java/lab/en2b/quizapi/user/UserService.java | 1 + .../java/lab/en2b/quizapi/user/{ => role}/RoleRepository.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename api/src/main/java/lab/en2b/quizapi/user/{ => role}/RoleRepository.java (89%) diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index abbdb9b1..5991827a 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -2,6 +2,7 @@ import lab.en2b.quizapi.auth.config.UserDetailsImpl; import lab.en2b.quizapi.auth.dtos.RegisterDto; +import lab.en2b.quizapi.user.role.RoleRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; diff --git a/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java b/api/src/main/java/lab/en2b/quizapi/user/role/RoleRepository.java similarity index 89% rename from api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java rename to api/src/main/java/lab/en2b/quizapi/user/role/RoleRepository.java index 11401b1c..cfdc0db1 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/user/role/RoleRepository.java @@ -1,4 +1,4 @@ -package lab.en2b.quizapi.user; +package lab.en2b.quizapi.user.role; import lab.en2b.quizapi.user.role.Role; From 70f7215ff007e832d023ca1a7b751aac885f5cff Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 6 Feb 2024 17:49:28 +0100 Subject: [PATCH 27/36] feat: database settings -> environment variables --- api/src/main/resources/application.properties | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index 4f6f5c6b..c6bad700 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -1 +1,5 @@ -JWT_EXPIRATION_MS= 86400000 +JWT_EXPIRATION_MS=86400000 +spring.jpa.hibernate.ddl-auto=update +spring.datasource.url=${DATABASE_URL} +spring.datasource.username=${DATABASE_USER} +spring.datasource.password=${DATABASE_PASSWORD} From ac9d6528b441bf1c9578dde8417366e4d0148a84 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 6 Feb 2024 18:02:33 +0100 Subject: [PATCH 28/36] fix: added missing request body --- api/src/main/java/lab/en2b/quizapi/auth/AuthController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java index dd755545..1a311a0a 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java @@ -28,7 +28,7 @@ public ResponseEntity loginUser(@Valid @RequestBody LoginDto loginRequest){ } @PostMapping("/refresh-token") - public ResponseEntity refreshToken(@Valid RefreshTokenDto refreshTokenRequest){ + public ResponseEntity refreshToken(@Valid @RequestBody RefreshTokenDto refreshTokenRequest){ return authService.refreshToken(refreshTokenRequest); } } From a3164b2d562086938eeb8b66008c36423d2035cc Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 6 Feb 2024 18:44:22 +0100 Subject: [PATCH 29/36] fix: added missing dependencies --- api/pom.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api/pom.xml b/api/pom.xml index bba945c2..20ba80be 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -30,6 +30,7 @@ org.springframework.boot spring-boot-starter-security + 3.1.4 @@ -77,6 +78,14 @@ tomcat-embed-core 10.1.13 + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + From e99a2e15aeaf7fcc7094e938e11f3699eb5f9ccc Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 6 Feb 2024 23:09:11 +0100 Subject: [PATCH 30/36] refactor: JwtService -> JwtUtils --- .../main/java/lab/en2b/quizapi/auth/AuthService.java | 9 +++++---- .../lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java | 12 ++++++++---- .../auth/jwt/{JwtService.java => JwtUtils.java} | 4 ++-- 3 files changed, 15 insertions(+), 10 deletions(-) rename api/src/main/java/lab/en2b/quizapi/auth/jwt/{JwtService.java => JwtUtils.java} (98%) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 5c418dc9..84d9a744 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -5,7 +5,7 @@ import lab.en2b.quizapi.auth.dtos.LoginDto; import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; -import lab.en2b.quizapi.auth.jwt.JwtService; +import lab.en2b.quizapi.auth.jwt.JwtUtils; import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -19,9 +19,10 @@ import java.util.Set; @Service +@Transactional(readOnly = true) @RequiredArgsConstructor public class AuthService { - private final JwtService jwtService; + private final JwtUtils jwtUtils; private final AuthenticationManager authenticationManager; private final UserService userService; /** @@ -37,8 +38,8 @@ public ResponseEntity login(LoginDto loginRequest){ UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); return ResponseEntity.ok(new JwtResponseDto( - jwtService.generateJwtTokenUserPassword(authentication), - jwtService.createRefreshToken(userDetails.getId()), + jwtUtils.generateJwtTokenUserPassword(authentication), + jwtUtils.createRefreshToken(userDetails.getId()), userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index d50480ab..0014c0b8 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -19,7 +19,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { @Autowired - private JwtService jwtService; + private JwtUtils jwtUtils; @Autowired private UserService userDetailsService; @@ -28,9 +28,13 @@ public class JwtAuthFilter extends OncePerRequestFilter { protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { String token = parseJwt(request); - String email = jwtService.getSubjectFromJwtToken(token); + String email = null; + if(token != null){ + email = jwtUtils.getSubjectFromJwtToken(token); + } + System.out.println("entered"); - if (email != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) { + if ( email != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) { UserDetails userDetails = userDetailsService.loadUserByUsername(email); // this invokes UsernamePasswordAuthenticationToken, although it uses email as subject not username UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, @@ -42,7 +46,7 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull Ht } private boolean isValidJwt(String token) { - return token != null && jwtService.validateJwtToken(token); + return token != null && jwtUtils.validateJwtToken(token); } private String parseJwt(HttpServletRequest request) { diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java similarity index 98% rename from api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java rename to api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java index 9e528926..45f49f8c 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java @@ -16,9 +16,9 @@ import java.util.function.Function; @Component -public class JwtService { +public class JwtUtils { - private static final Logger logger = LoggerFactory.getLogger(JwtService.class); + private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); //MUST BE SET AS ENVIRONMENT VARIABLE @Value("${JWT_SECRET}") From 7820bf586be59c5796a6c30d391e380af7f5d3c9 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 6 Feb 2024 23:09:52 +0100 Subject: [PATCH 31/36] feat: updated pom dependencies --- api/pom.xml | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index 20ba80be..c16846b8 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -20,17 +20,22 @@ org.springframework.boot spring-boot-starter-data-jpa - 3.1.4 + 3.2.2 - jakarta.validation - jakarta.validation-api - 3.0.2 + org.springframework.boot + spring-boot-starter-web + 3.2.2 org.springframework.boot spring-boot-starter-security - 3.1.4 + 3.2.2 + + + jakarta.validation + jakarta.validation-api + 3.0.2 @@ -73,19 +78,6 @@ jjwt-jackson 0.12.1 - - org.apache.tomcat.embed - tomcat-embed-core - 10.1.13 - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-web - From 5a0c70c2297c52bce5b83e8144d3198c9d178ef8 Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 7 Feb 2024 00:05:34 +0100 Subject: [PATCH 32/36] feat: refresh token --- .../lab/en2b/quizapi/auth/AuthService.java | 15 ++--- .../quizapi/auth/config/SecurityConfig.java | 56 +++++++++---------- .../auth/dtos/RefreshTokenResponseDto.java | 20 +++++++ .../en2b/quizapi/auth/jwt/JwtAuthFilter.java | 1 - .../lab/en2b/quizapi/auth/jwt/JwtUtils.java | 22 +++++--- .../exceptions/TokenRefreshException.java | 7 +++ .../main/java/lab/en2b/quizapi/user/User.java | 8 +++ .../lab/en2b/quizapi/user/UserRepository.java | 2 + .../lab/en2b/quizapi/user/UserService.java | 18 ++++++ api/src/main/resources/application.properties | 1 + 10 files changed, 103 insertions(+), 47 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java create mode 100644 api/src/main/java/lab/en2b/quizapi/commons/exceptions/TokenRefreshException.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 84d9a744..c132fbe5 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -1,11 +1,11 @@ package lab.en2b.quizapi.auth; import lab.en2b.quizapi.auth.config.UserDetailsImpl; -import lab.en2b.quizapi.auth.dtos.JwtResponseDto; -import lab.en2b.quizapi.auth.dtos.LoginDto; -import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; -import lab.en2b.quizapi.auth.dtos.RegisterDto; +import lab.en2b.quizapi.auth.dtos.*; import lab.en2b.quizapi.auth.jwt.JwtUtils; +import lab.en2b.quizapi.commons.exceptions.TokenRefreshException; +import lab.en2b.quizapi.auth.dtos.RefreshTokenResponseDto; +import lab.en2b.quizapi.user.User; import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -19,7 +19,6 @@ import java.util.Set; @Service -@Transactional(readOnly = true) @RequiredArgsConstructor public class AuthService { private final JwtUtils jwtUtils; @@ -39,7 +38,7 @@ public ResponseEntity login(LoginDto loginRequest){ return ResponseEntity.ok(new JwtResponseDto( jwtUtils.generateJwtTokenUserPassword(authentication), - jwtUtils.createRefreshToken(userDetails.getId()), + userService.assignNewRefreshToken(userDetails.getId()), userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), @@ -53,6 +52,8 @@ public ResponseEntity register(RegisterDto registerRequest) { } public ResponseEntity refreshToken(RefreshTokenDto refreshTokenRequest) { - throw new UnsupportedOperationException(); + User user = userService.findByRefreshToken(refreshTokenRequest.getRefreshToken()).orElseThrow(() -> new TokenRefreshException( + "Refresh token is not in database!")); + return ResponseEntity.ok(new RefreshTokenResponseDto(jwtUtils.generateTokenFromEmail(user.getEmail()), user.obtainRefreshIfValid())); } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 1838388a..381a1cd6 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -1,17 +1,17 @@ package lab.en2b.quizapi.auth.config; import lab.en2b.quizapi.user.UserService; -import lab.en2b.quizapi.auth.jwt.JwtAuthFilter; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 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.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @@ -22,11 +22,8 @@ @Configuration @EnableWebSecurity -@EnableMethodSecurity +@RequiredArgsConstructor public class SecurityConfig { - - @Autowired - private JwtAuthFilter authFilter; @Autowired public UserService userService; @Bean @@ -45,24 +42,6 @@ public CorsFilter corsFilter() { source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } - /** - * Builds the authorization manager taking into account password encoding - * @param http the http request to secure - * @return the newly created authentication manager - * @throws Exception if something goes wrong when creating the manager - */ - @Bean - public AuthenticationManager authManager(HttpSecurity http) throws Exception { - AuthenticationManagerBuilder authenticationManagerBuilder = - http.getSharedObject(AuthenticationManagerBuilder.class); - - DaoAuthenticationProvider userPasswordProvider = new DaoAuthenticationProvider(); - userPasswordProvider.setUserDetailsService(userService); - userPasswordProvider.setPasswordEncoder(passwordEncoder()); - - authenticationManagerBuilder.authenticationProvider(userPasswordProvider); - return authenticationManagerBuilder.build(); - } /** * Security filter used for filtering all petitions, applying cors and asking for authentication among other things * @param http the http request to filter @@ -70,14 +49,29 @@ public AuthenticationManager authManager(HttpSecurity http) throws Exception { * @throws Exception if any problem happens when filtering */ @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - return http.cors(Customizer.withDefaults()).authorizeHttpRequests(authorize -> - authorize.requestMatchers("/auth/login", "/auth/register","/auth/refresh-token").permitAll() - .anyRequest().authenticated()) - //TODO: add exception handling + public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { + return http + .cors(Customizer.withDefaults()) .sessionManagement(configuration -> configuration.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers(HttpMethod.POST,"/auth/**").permitAll() + .anyRequest().authenticated()) + .csrf(AbstractHttpConfigurer::disable) + .authenticationManager(authenticationManager) .build(); + //TODO: add exception handling } - + /** + * Builds the authorization manager taking into account password encoding + * @param http the http request to secure + * @return the newly created authentication manager + * @throws Exception if something goes wrong when creating the manager + */ + @Bean + public AuthenticationManager authManager(HttpSecurity http) throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); + authenticationManagerBuilder.userDetailsService(userService).passwordEncoder(passwordEncoder()); + return authenticationManagerBuilder.build(); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java new file mode 100644 index 00000000..4af19ee2 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java @@ -0,0 +1,20 @@ +package lab.en2b.quizapi.auth.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RefreshTokenResponseDto { + + private String token; + @JsonProperty("refresh_token") + private String refreshToken; + + public RefreshTokenResponseDto(String accessToken, String refreshToken) { + this.token = accessToken; + this.refreshToken = refreshToken; + } + +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index 0014c0b8..f5ee7832 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -32,7 +32,6 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull Ht if(token != null){ email = jwtUtils.getSubjectFromJwtToken(token); } - System.out.println("entered"); if ( email != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) { UserDetails userDetails = userDetailsService.loadUserByUsername(email); diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java index 45f49f8c..bc782dfd 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java @@ -22,21 +22,18 @@ public class JwtUtils { //MUST BE SET AS ENVIRONMENT VARIABLE @Value("${JWT_SECRET}") - private String jwtSecret; - + private String JWT_SECRET; @Value("${JWT_EXPIRATION_MS}") - private int jwtExpirationMs; + private Long JWT_EXPIRATION_MS; + - public String createRefreshToken(Long id) { - throw new UnsupportedOperationException(); - } public String generateJwtTokenUserPassword(Authentication authentication) { UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); return Jwts.builder() .subject(userPrincipal.getEmail()) .issuedAt(new Date()) - .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) + .expiration(new Date((new Date()).getTime() + JWT_EXPIRATION_MS)) .signWith(getSignInKey()) .compact(); } @@ -76,7 +73,16 @@ public String getSubjectFromJwtToken(String token) { } } private SecretKey getSignInKey(){ - byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); + byte[] keyBytes = Decoders.BASE64.decode(JWT_SECRET); return Keys.hmacShaKeyFor(keyBytes); } + + public String generateTokenFromEmail(String email) { + return Jwts.builder() + .subject(email) + .issuedAt(new Date()) + .expiration(new Date((new Date()).getTime() + JWT_EXPIRATION_MS)) + .signWith(getSignInKey()) + .compact(); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/commons/exceptions/TokenRefreshException.java b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/TokenRefreshException.java new file mode 100644 index 00000000..3271e6cf --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/TokenRefreshException.java @@ -0,0 +1,7 @@ +package lab.en2b.quizapi.commons.exceptions; + +public class TokenRefreshException extends RuntimeException{ + public TokenRefreshException(String message) { + super(message); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/user/User.java b/api/src/main/java/lab/en2b/quizapi/user/User.java index 90e47230..b0abc6bc 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/User.java +++ b/api/src/main/java/lab/en2b/quizapi/user/User.java @@ -7,6 +7,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import lab.en2b.quizapi.commons.exceptions.TokenRefreshException; import lab.en2b.quizapi.user.role.Role; import lombok.*; @@ -65,4 +66,11 @@ public class User { @JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "permissions"}) @JsonProperty("role") private Set roles; + + public String obtainRefreshIfValid() { + if(getRefreshExpiration() == null || getRefreshExpiration().compareTo(Instant.now()) < 0){ + throw new TokenRefreshException( "Invalid refresh token. Please make a new login request"); + } + return getRefreshToken(); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java index fa3eab46..36a9fbe4 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java @@ -12,4 +12,6 @@ public interface UserRepository extends JpaRepository { Boolean existsByUsername(String username); Boolean existsByEmail(String email); + + Optional findByRefreshToken(String refreshToken); } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index 5991827a..55b82123 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -4,13 +4,17 @@ import lab.en2b.quizapi.auth.dtos.RegisterDto; import lab.en2b.quizapi.user.role.RoleRepository; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import java.time.Instant; +import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; @Service @@ -18,6 +22,8 @@ public class UserService implements UserDetailsService { private final UserRepository userRepository; private final RoleRepository roleRepository; + @Value("${REFRESH_TOKEN_DURATION_MS}") + private Long REFRESH_TOKEN_DURATION_MS; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return UserDetailsImpl.build(userRepository.findByEmail(email).orElseThrow()); @@ -34,4 +40,16 @@ public void createUser(RegisterDto registerRequest, Set roleNames){ .roles(roleNames.stream().map( roleName -> roleRepository.findByName(roleName).orElseThrow()).collect(Collectors.toSet())) .build()); } + + public Optional findByRefreshToken(String refreshToken) { + return userRepository.findByRefreshToken(refreshToken); + } + + public String assignNewRefreshToken(Long id) { + User user = userRepository.findById(id).orElseThrow(); + user.setRefreshToken(UUID.randomUUID().toString()); + user.setRefreshExpiration(Instant.now().plusMillis(REFRESH_TOKEN_DURATION_MS)); + userRepository.save(user); + return user.getRefreshToken(); + } } diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index c6bad700..8419350a 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -1,4 +1,5 @@ JWT_EXPIRATION_MS=86400000 +REFRESH_TOKEN_DURATION_MS=86400000 spring.jpa.hibernate.ddl-auto=update spring.datasource.url=${DATABASE_URL} spring.datasource.username=${DATABASE_USER} From c9091c3ea5f902ef0e3d65cd69f40e3ca23b451f Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 7 Feb 2024 00:13:06 +0100 Subject: [PATCH 33/36] feat: exception handling --- .../exceptions/CustomControllerAdvice.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java diff --git a/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java new file mode 100644 index 00000000..7032fbf4 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java @@ -0,0 +1,61 @@ +package lab.en2b.quizapi.commons.exceptions; + +import java.util.NoSuchElementException; + +import lombok.extern.log4j.Log4j2; +import org.springframework.core.annotation.Order; +import org.springframework.core.Ordered; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +@Log4j2 +@Order(Ordered.HIGHEST_PRECEDENCE) +public class CustomControllerAdvice extends ResponseEntityExceptionHandler { + @ExceptionHandler(NoSuchElementException.class) + public ResponseEntity handleNoSuchElementException(NoSuchElementException exception){ + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException exception){ + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity handleBadCredentialsException(BadCredentialsException exception){ + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDeniedException(AccessDeniedException exception){ + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(), HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(TokenRefreshException.class) + public ResponseEntity handleTokenRefreshException(TokenRefreshException exception) { + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.FORBIDDEN); + } + @ExceptionHandler(InternalAuthenticationServiceException.class) + public ResponseEntity handleInternalAuthenticationServiceException(InternalAuthenticationServiceException exception) { + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.FORBIDDEN); + } + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception exception){ + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.INTERNAL_SERVER_ERROR); + } + +} From 6b18c5cdf5da0b72f8dd6a16f825b96ff4a1554b Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 7 Feb 2024 00:15:14 +0100 Subject: [PATCH 34/36] refactor: moved user folder --- api/src/main/java/lab/en2b/quizapi/auth/AuthService.java | 4 ++-- .../java/lab/en2b/quizapi/auth/config/SecurityConfig.java | 2 +- .../java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java | 4 ++-- .../main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java | 2 +- .../main/java/lab/en2b/quizapi/{ => commons}/user/User.java | 4 ++-- .../lab/en2b/quizapi/{ => commons}/user/UserRepository.java | 2 +- .../lab/en2b/quizapi/{ => commons}/user/UserService.java | 4 ++-- .../java/lab/en2b/quizapi/{ => commons}/user/role/Role.java | 6 ++---- .../quizapi/{ => commons}/user/role/RoleRepository.java | 3 +-- 9 files changed, 14 insertions(+), 17 deletions(-) rename api/src/main/java/lab/en2b/quizapi/{ => commons}/user/User.java (95%) rename api/src/main/java/lab/en2b/quizapi/{ => commons}/user/UserRepository.java (91%) rename api/src/main/java/lab/en2b/quizapi/{ => commons}/user/UserService.java (96%) rename api/src/main/java/lab/en2b/quizapi/{ => commons}/user/role/Role.java (79%) rename api/src/main/java/lab/en2b/quizapi/{ => commons}/user/role/RoleRepository.java (78%) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index c132fbe5..82e39000 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -5,8 +5,8 @@ import lab.en2b.quizapi.auth.jwt.JwtUtils; import lab.en2b.quizapi.commons.exceptions.TokenRefreshException; import lab.en2b.quizapi.auth.dtos.RefreshTokenResponseDto; -import lab.en2b.quizapi.user.User; -import lab.en2b.quizapi.user.UserService; +import lab.en2b.quizapi.commons.user.User; +import lab.en2b.quizapi.commons.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 381a1cd6..85379e4c 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -1,6 +1,6 @@ package lab.en2b.quizapi.auth.config; -import lab.en2b.quizapi.user.UserService; +import lab.en2b.quizapi.commons.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java index 776062d7..7690b1d2 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java @@ -1,8 +1,8 @@ package lab.en2b.quizapi.auth.config; import com.fasterxml.jackson.annotation.JsonIgnore; -import lab.en2b.quizapi.user.User; -import lab.en2b.quizapi.user.role.Role; +import lab.en2b.quizapi.commons.user.User; +import lab.en2b.quizapi.commons.user.role.Role; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.security.core.GrantedAuthority; diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index f5ee7832..9f056081 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -3,7 +3,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lab.en2b.quizapi.user.UserService; +import lab.en2b.quizapi.commons.user.UserService; import lombok.NonNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; diff --git a/api/src/main/java/lab/en2b/quizapi/user/User.java b/api/src/main/java/lab/en2b/quizapi/commons/user/User.java similarity index 95% rename from api/src/main/java/lab/en2b/quizapi/user/User.java rename to api/src/main/java/lab/en2b/quizapi/commons/user/User.java index b0abc6bc..d6f041cb 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/User.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/User.java @@ -1,4 +1,4 @@ -package lab.en2b.quizapi.user; +package lab.en2b.quizapi.commons.user; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -8,7 +8,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lab.en2b.quizapi.commons.exceptions.TokenRefreshException; -import lab.en2b.quizapi.user.role.Role; +import lab.en2b.quizapi.commons.user.role.Role; import lombok.*; import java.time.Instant; diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/commons/user/UserRepository.java similarity index 91% rename from api/src/main/java/lab/en2b/quizapi/user/UserRepository.java rename to api/src/main/java/lab/en2b/quizapi/commons/user/UserRepository.java index 36a9fbe4..780f15cf 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/UserRepository.java @@ -1,4 +1,4 @@ -package lab.en2b.quizapi.user; +package lab.en2b.quizapi.commons.user; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java similarity index 96% rename from api/src/main/java/lab/en2b/quizapi/user/UserService.java rename to api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java index 55b82123..d6ed64d9 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java @@ -1,8 +1,8 @@ -package lab.en2b.quizapi.user; +package lab.en2b.quizapi.commons.user; import lab.en2b.quizapi.auth.config.UserDetailsImpl; import lab.en2b.quizapi.auth.dtos.RegisterDto; -import lab.en2b.quizapi.user.role.RoleRepository; +import lab.en2b.quizapi.commons.user.role.RoleRepository; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; diff --git a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java b/api/src/main/java/lab/en2b/quizapi/commons/user/role/Role.java similarity index 79% rename from api/src/main/java/lab/en2b/quizapi/user/role/Role.java rename to api/src/main/java/lab/en2b/quizapi/commons/user/role/Role.java index f98b63b8..8e828ff1 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/role/Role.java @@ -1,12 +1,10 @@ -package lab.en2b.quizapi.user.role; +package lab.en2b.quizapi.commons.user.role; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonView; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; -import lab.en2b.quizapi.user.User; +import lab.en2b.quizapi.commons.user.User; import lombok.*; import java.util.Set; diff --git a/api/src/main/java/lab/en2b/quizapi/user/role/RoleRepository.java b/api/src/main/java/lab/en2b/quizapi/commons/user/role/RoleRepository.java similarity index 78% rename from api/src/main/java/lab/en2b/quizapi/user/role/RoleRepository.java rename to api/src/main/java/lab/en2b/quizapi/commons/user/role/RoleRepository.java index cfdc0db1..84c7a7a2 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/role/RoleRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/role/RoleRepository.java @@ -1,7 +1,6 @@ -package lab.en2b.quizapi.user.role; +package lab.en2b.quizapi.commons.user.role; -import lab.en2b.quizapi.user.role.Role; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; From 8dd1e5c4b8fe43fe2b402047a614293faee4b951 Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 7 Feb 2024 00:25:11 +0100 Subject: [PATCH 35/36] feat: finished auth --- .../en2b/quizapi/auth/config/SecurityConfig.java | 7 +++++++ .../java/lab/en2b/quizapi/commons/user/User.java | 2 +- .../questions/question/QuestionController.java | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 85379e4c..321fb182 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -1,5 +1,6 @@ package lab.en2b.quizapi.auth.config; +import lab.en2b.quizapi.auth.jwt.JwtAuthFilter; import lab.en2b.quizapi.commons.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; @@ -16,6 +17,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @@ -27,6 +29,10 @@ public class SecurityConfig { @Autowired public UserService userService; @Bean + public JwtAuthFilter authenticationJwtTokenFilter() { + return new JwtAuthFilter(); + } + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @@ -58,6 +64,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, Authentication .anyRequest().authenticated()) .csrf(AbstractHttpConfigurer::disable) .authenticationManager(authenticationManager) + .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class) .build(); //TODO: add exception handling } diff --git a/api/src/main/java/lab/en2b/quizapi/commons/user/User.java b/api/src/main/java/lab/en2b/quizapi/commons/user/User.java index d6f041cb..56d64935 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/user/User.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/User.java @@ -56,7 +56,7 @@ public class User { private Instant refreshExpiration; @NotNull - @ManyToMany + @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name="users_roles", joinColumns= @JoinColumn(name="user_id", referencedColumnName="id"), diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java new file mode 100644 index 00000000..1c1e7127 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java @@ -0,0 +1,14 @@ +package lab.en2b.quizapi.questions.question; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/questions") +public class QuestionController { + @GetMapping("/dummy") + private String getDummyQuestion(){ + return "Who the hell is Steve Jobs?"; + } +} From c3947c766f99929bd5833054311fafcf854e0e91 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Wed, 7 Feb 2024 09:13:15 +0100 Subject: [PATCH 36/36] chore: used same logger as CustomControllerAdvice --- .../java/lab/en2b/quizapi/auth/jwt/JwtUtils.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java index bc782dfd..432b0813 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java @@ -5,8 +5,7 @@ import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; import lab.en2b.quizapi.auth.config.UserDetailsImpl; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -16,10 +15,9 @@ import java.util.function.Function; @Component +@Log4j2 public class JwtUtils { - private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); - //MUST BE SET AS ENVIRONMENT VARIABLE @Value("${JWT_SECRET}") private String JWT_SECRET; @@ -42,15 +40,15 @@ public boolean validateJwtToken(String authToken) { Jwts.parser().verifyWith(getSignInKey()).build().parseSignedClaims(authToken); return true; } catch (SignatureException e) { - logger.error("Invalid JWT signature: {}", e.getMessage()); + log.error("Invalid JWT signature: {}", e.getMessage()); } catch (MalformedJwtException e) { - logger.error("Invalid JWT token: {}", e.getMessage()); + log.error("Invalid JWT token: {}", e.getMessage()); } catch (ExpiredJwtException e) { - logger.error("JWT token is expired: {}", e.getMessage()); + log.error("JWT token is expired: {}", e.getMessage()); } catch (UnsupportedJwtException e) { - logger.error("JWT token is unsupported: {}", e.getMessage()); + log.error("JWT token is unsupported: {}", e.getMessage()); } catch (IllegalArgumentException e) { - logger.error("JWT claims string is empty: {}", e.getMessage()); + log.error("JWT claims string is empty: {}", e.getMessage()); } return false; }