diff --git a/server/src/main/java/com/mixel/docusphere/config/SecurityConfig.java b/server/src/main/java/com/mixel/docusphere/config/SecurityConfig.java index 567d282..b3947b0 100644 --- a/server/src/main/java/com/mixel/docusphere/config/SecurityConfig.java +++ b/server/src/main/java/com/mixel/docusphere/config/SecurityConfig.java @@ -1,22 +1,66 @@ package com.mixel.docusphere.config; +import com.mixel.docusphere.config.jwt.JwtAuthenticationEntryPoint; +import com.mixel.docusphere.config.jwt.JwtRequestFilter; +import com.mixel.docusphere.service.CustomUserDetailsService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 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; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig { + private final CustomUserDetailsService customUserDetailsService; + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + private final JwtRequestFilter jwtRequestFilter; + private final PasswordEncoder passwordEncoder; + + public SecurityConfig(CustomUserDetailsService customUserDetailsService, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, @Lazy JwtRequestFilter jwtRequestFilter, PasswordEncoder passwordEncoder) { + this.customUserDetailsService = customUserDetailsService; + this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint; + this.jwtRequestFilter = jwtRequestFilter; + this.passwordEncoder = passwordEncoder; + } + + @Autowired + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder); + } + + // TODO: use PasswordService instead of BCryptPasswordEncoder + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) // Disable CSRF protection + http.csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() // Allow all requests without authentication - ); + .requestMatchers("/authenticate", "/register").permitAll() + .anyRequest().authenticated() + ) + .exceptionHandling(exception -> exception.authenticationEntryPoint(jwtAuthenticationEntryPoint)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/server/src/main/java/com/mixel/docusphere/config/jwt/JwtAuthenticationEntryPoint.java b/server/src/main/java/com/mixel/docusphere/config/jwt/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..c10fa84 --- /dev/null +++ b/server/src/main/java/com/mixel/docusphere/config/jwt/JwtAuthenticationEntryPoint.java @@ -0,0 +1,20 @@ +package com.mixel.docusphere.config.jwt; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); + } + +} diff --git a/server/src/main/java/com/mixel/docusphere/config/jwt/JwtConfig.java b/server/src/main/java/com/mixel/docusphere/config/jwt/JwtConfig.java new file mode 100644 index 0000000..ae0aabf --- /dev/null +++ b/server/src/main/java/com/mixel/docusphere/config/jwt/JwtConfig.java @@ -0,0 +1,21 @@ +package com.mixel.docusphere.config.jwt; + +import io.github.cdimascio.dotenv.Dotenv; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JwtConfig { + + private final Dotenv dotenv = Dotenv.configure().load(); + + @Bean + public String jwtSecret() { + return dotenv.get("JWT_SECRET_KEY"); + } + + @Bean + public long jwtExpirationTime() { + return Long.parseLong(dotenv.get("JWT_EXPIRATION_TIME", "3600000")); // Default 1 hour + } +} diff --git a/server/src/main/java/com/mixel/docusphere/config/jwt/JwtRequestFilter.java b/server/src/main/java/com/mixel/docusphere/config/jwt/JwtRequestFilter.java new file mode 100644 index 0000000..68dafa0 --- /dev/null +++ b/server/src/main/java/com/mixel/docusphere/config/jwt/JwtRequestFilter.java @@ -0,0 +1,69 @@ +package com.mixel.docusphere.config.jwt; + +import com.mixel.docusphere.service.CustomUserDetailsService; +import com.mixel.docusphere.service.JwtService; +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +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.WebAuthenticationDetails; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtRequestFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + + private final CustomUserDetailsService userDetailsService; + + @Autowired + public JwtRequestFilter(JwtService jwtService, @Lazy CustomUserDetailsService userDetailsService) { + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + final String requestTokenHeader = request.getHeader("Authorization"); + + String username = null; + String jwtToken = null; + + if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { + jwtToken = requestTokenHeader.substring(7); + try { + username = jwtService.getUsernameFromToken(jwtToken); + } catch (IllegalArgumentException e) { + System.out.println("Unable to get JWT Token"); + } catch (ExpiredJwtException e) { + System.out.println("JWT Token has expired"); + } + } else { + logger.warn("JWT Token does not begin with Bearer String"); + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + + if (jwtService.validateToken(jwtToken, userDetails.getUsername())) { + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetails(request)); + SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); + } + } + chain.doFilter(request, response); + } +} diff --git a/server/src/main/java/com/mixel/docusphere/controller/AuthController.java b/server/src/main/java/com/mixel/docusphere/controller/AuthController.java new file mode 100644 index 0000000..299dffd --- /dev/null +++ b/server/src/main/java/com/mixel/docusphere/controller/AuthController.java @@ -0,0 +1,4 @@ +package com.mixel.docusphere.controller; + +public class AuthController { +} diff --git a/server/src/main/java/com/mixel/docusphere/dto/response/JwtResponseDTO.java b/server/src/main/java/com/mixel/docusphere/dto/response/JwtResponseDTO.java new file mode 100644 index 0000000..1471313 --- /dev/null +++ b/server/src/main/java/com/mixel/docusphere/dto/response/JwtResponseDTO.java @@ -0,0 +1,14 @@ +package com.mixel.docusphere.dto.response; + +public class JwtResponseDTO { + + private final String jwtToken; + + public JwtResponseDTO(String jwtToken){ + this.jwtToken = jwtToken; + } + + public String getJwtToken(){ + return jwtToken; + } +} diff --git a/server/src/main/java/com/mixel/docusphere/repository/UserRepository.java b/server/src/main/java/com/mixel/docusphere/repository/UserRepository.java index 456987e..4531429 100644 --- a/server/src/main/java/com/mixel/docusphere/repository/UserRepository.java +++ b/server/src/main/java/com/mixel/docusphere/repository/UserRepository.java @@ -2,7 +2,10 @@ package com.mixel.docusphere.repository; import com.mixel.docusphere.entity.User; import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; import java.util.UUID; public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); } \ No newline at end of file diff --git a/server/src/main/java/com/mixel/docusphere/service/CustomUserDetailsService.java b/server/src/main/java/com/mixel/docusphere/service/CustomUserDetailsService.java new file mode 100644 index 0000000..2a3fe97 --- /dev/null +++ b/server/src/main/java/com/mixel/docusphere/service/CustomUserDetailsService.java @@ -0,0 +1,26 @@ +package com.mixel.docusphere.service; + +import com.mixel.docusphere.repository.UserRepository; +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; + +import java.util.ArrayList; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + public CustomUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + com.mixel.docusphere.entity.User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username)); + return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPasswordHash(), new ArrayList<>()); + } +} diff --git a/server/src/main/java/com/mixel/docusphere/service/JwtService.java b/server/src/main/java/com/mixel/docusphere/service/JwtService.java new file mode 100644 index 0000000..55b8469 --- /dev/null +++ b/server/src/main/java/com/mixel/docusphere/service/JwtService.java @@ -0,0 +1,84 @@ +package com.mixel.docusphere.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Service +public class JwtService { + + private final String jwtSecret; + private final long jwtExpiration; + + @Autowired + public JwtService(String jwtSecret, long jwtExpirationTime) { + this.jwtSecret = jwtSecret; + this.jwtExpiration = jwtExpirationTime; + } + + private Key getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); + return Keys.hmacShaKeyFor(keyBytes); + } + + public String getUserIdFromToken(String token) { + return getClaimFromToken(token, Claims::getId); + } + + public String getUsernameFromToken(String token) { + return getClaimFromToken(token, Claims::getSubject); + } + + public Date getExpirationDateFromToken(String token) { + return getClaimFromToken(token, Claims::getExpiration); + } + + public T getClaimFromToken(String token, Function claimsResolver) { + final Claims claims = getAllClaimsFromToken(token); + return claimsResolver.apply(claims); + } + + private Boolean isTokenExpired(String token) { + final Date expiration = getExpirationDateFromToken(token); + return expiration.before(new Date()); + } + + public String generateToken(String username) { + Map claims = new HashMap<>(); + return doGenerateToken(claims, username); + } + + public Boolean validateToken(String token, String username) { + final String usernameFromToken = getUsernameFromToken(token); + return (username.equals(usernameFromToken) && !isTokenExpired(token)); + } + + private String doGenerateToken(Map claims, String subject) { + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + private Claims getAllClaimsFromToken(String token) { + return Jwts + .parser() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } +} diff --git a/server/src/main/java/com/mixel/docusphere/service/UserService.java b/server/src/main/java/com/mixel/docusphere/service/UserService.java index 8c51ebc..65dd8e4 100644 --- a/server/src/main/java/com/mixel/docusphere/service/UserService.java +++ b/server/src/main/java/com/mixel/docusphere/service/UserService.java @@ -5,7 +5,6 @@ import com.mixel.docusphere.dto.response.UserRespondDTO; import com.mixel.docusphere.entity.User; import com.mixel.docusphere.mapper.UserMapper; import com.mixel.docusphere.repository.UserRepository; -import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; import org.springframework.stereotype.Service; import java.util.List; @@ -21,7 +20,6 @@ public class UserService { private final PasswordService passwordService; // Constructor - public UserService(UserRepository userRepository, PasswordService passwordService) { this.userRepository = userRepository; this.passwordService = passwordService; diff --git a/server/src/main/java/com/mixel/docusphere/util/JwtUtil.java b/server/src/main/java/com/mixel/docusphere/util/JwtUtil.java new file mode 100644 index 0000000..f303834 --- /dev/null +++ b/server/src/main/java/com/mixel/docusphere/util/JwtUtil.java @@ -0,0 +1,5 @@ +package com.mixel.docusphere.util; + + +public class JwtUtil { +}