diff --git a/src/main/java/dev/gfxv/blps/security/JwtAuthenticationFilter.java b/src/main/java/dev/gfxv/blps/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..23668a1 --- /dev/null +++ b/src/main/java/dev/gfxv/blps/security/JwtAuthenticationFilter.java @@ -0,0 +1,72 @@ +package dev.gfxv.blps.security; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.experimental.FieldDefaults; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +@Component +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + JwtUtils jwtUtils; + + @Autowired + public JwtAuthenticationFilter(JwtUtils jwtUtils) { + this.jwtUtils = jwtUtils; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + try { + String jwt = parseJwt(request); + if (jwt != null && jwtUtils.validateJwtToken(jwt)) { + String username = jwtUtils.getUsernameFromJwtToken(jwt); + List roles = jwtUtils.getRolesFromJwtToken(jwt); + + List authorities = roles.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + username, null, authorities); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + // TODO: add error logging + } + + filterChain.doFilter(request, response); + } + + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7); + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/dev/gfxv/blps/security/JwtUtils.java b/src/main/java/dev/gfxv/blps/security/JwtUtils.java new file mode 100644 index 0000000..087e0a8 --- /dev/null +++ b/src/main/java/dev/gfxv/blps/security/JwtUtils.java @@ -0,0 +1,72 @@ +package dev.gfxv.blps.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + + +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class JwtUtils { + + @Value("${jwt.secret}") + private String jwtSecret; + + @Value("${jwt.expiration}") + private long jwtExpiration; + + public String generateJwtToken(Authentication authentication) { + UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); + + List roles = userPrincipal.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + + return Jwts.builder() + .setSubject(userPrincipal.getUsername()) + .claim("roles", roles) + .setIssuedAt(new Date()) + .setExpiration(new Date((new Date()).getTime() + jwtExpiration)) + .signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes()), SignatureAlgorithm.HS512) + .compact(); + } + + public String getUsernameFromJwtToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes())) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + } + + public List getRolesFromJwtToken(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes())) + .build() + .parseClaimsJws(token) + .getBody(); + return claims.get("roles", List.class); + } + + public boolean validateJwtToken(String authToken) { + try { + Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes())) + .build() + .parseClaimsJws(authToken); + return true; + } catch (Exception e) { + // TODO: add error logging + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/gfxv/blps/security/UserDetailsImpl.java b/src/main/java/dev/gfxv/blps/security/UserDetailsImpl.java new file mode 100644 index 0000000..7b4b3c7 --- /dev/null +++ b/src/main/java/dev/gfxv/blps/security/UserDetailsImpl.java @@ -0,0 +1,81 @@ +package dev.gfxv.blps.security; + +import dev.gfxv.blps.entity.User; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.FieldDefaults; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class UserDetailsImpl implements UserDetails { + Long id; + String username; + String email; + String password; + Collection authorities; + + public UserDetailsImpl(Long id, String username, String email, String password, + Collection authorities) { + this.id = id; + this.username = username; + this.email = email; + this.password = password; + this.authorities = authorities; + } + + public static UserDetailsImpl build(User user) { + List authorities = user.getRoles().stream() + .map(role -> new SimpleGrantedAuthority(role.getName())) + .collect(Collectors.toList()); + + return new UserDetailsImpl( + user.getId(), + user.getUsername(), + user.getEmail(), + user.getPassword(), + authorities); + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + +} diff --git a/src/main/java/dev/gfxv/blps/service/UserDetailsServiceImpl.java b/src/main/java/dev/gfxv/blps/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..58ea391 --- /dev/null +++ b/src/main/java/dev/gfxv/blps/service/UserDetailsServiceImpl.java @@ -0,0 +1,32 @@ +package dev.gfxv.blps.service; + +import dev.gfxv.blps.entity.User; +import dev.gfxv.blps.repository.UserRepository; +import dev.gfxv.blps.security.UserDetailsImpl; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.experimental.FieldDefaults; +import org.springframework.beans.factory.annotation.Autowired; +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 +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class UserDetailsServiceImpl implements UserDetailsService { + + UserRepository userRepository; + + @Autowired + public UserDetailsServiceImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username)); + return UserDetailsImpl.build(user); + } +}