From c64369ad1c76f6c9615ec27fc2a808444aa4485f Mon Sep 17 00:00:00 2001 From: Hanif Date: Wed, 3 Jan 2024 09:48:24 +0700 Subject: [PATCH] Add Security & Repository Package (JWT, Repository, Enums) --- .../modules/auth/WebSecurityConfig.java | 68 +++++++++++++++ .../modules/auth/jwt/AuthEntryPointJwt.java | 38 ++++++++ .../modules/auth/jwt/AuthTokenFilter.java | 55 ++++++++++++ .../modules/auth/jwt/JwtUtils.java | 87 +++++++++++++++++++ .../auth/repository/RoleRepository.java | 15 ++++ .../auth/repository/UserRepository.java | 18 ++++ 6 files changed, 281 insertions(+) create mode 100644 src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/WebSecurityConfig.java create mode 100644 src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/jwt/AuthEntryPointJwt.java create mode 100644 src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/jwt/AuthTokenFilter.java create mode 100644 src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/jwt/JwtUtils.java create mode 100644 src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/repository/RoleRepository.java create mode 100644 src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/repository/UserRepository.java diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/WebSecurityConfig.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/WebSecurityConfig.java new file mode 100644 index 0000000..2d8205f --- /dev/null +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/WebSecurityConfig.java @@ -0,0 +1,68 @@ +package com.hakimfauzi23.boilerplatespringsecurity.modules.auth; + +import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.AuthEntryPointJwt; +import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.AuthTokenFilter; +import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service.UserDetailsServiceImpl; +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.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +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 +@EnableMethodSecurity +public class WebSecurityConfig { + + @Autowired + UserDetailsServiceImpl userDetailsService; + + @Autowired + private AuthEntryPointJwt unauthorizedHandler; + + @Bean + public AuthTokenFilter authenticationJwtTokenFilter() { + return new AuthTokenFilter(); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + httpSecurity.csrf(csrf -> csrf.disable()) + .exceptionHandling(ex -> ex.authenticationEntryPoint(unauthorizedHandler)) + .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> + auth.requestMatchers("/api/auth/**").permitAll() + .requestMatchers("api/test/**").permitAll() + .anyRequest().authenticated()); + + httpSecurity.authenticationProvider(authenticationProvider()); + httpSecurity.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + return httpSecurity.build(); + } +} diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/jwt/AuthEntryPointJwt.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/jwt/AuthEntryPointJwt.java new file mode 100644 index 0000000..bbca619 --- /dev/null +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/jwt/AuthEntryPointJwt.java @@ -0,0 +1,38 @@ +package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Component +public class AuthEntryPointJwt implements AuthenticationEntryPoint { + + private static final Logger LOGGER = LoggerFactory.getLogger(AuthEntryPointJwt.class); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + final Map body = new HashMap<>(); + body.put("status", HttpServletResponse.SC_UNAUTHORIZED); + body.put("error", "Unauthorized"); + body.put("message", authException.getMessage()); + body.put("path", request.getServletPath()); + + final ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getOutputStream(), body); + } +} diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/jwt/AuthTokenFilter.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/jwt/AuthTokenFilter.java new file mode 100644 index 0000000..3c0b3ae --- /dev/null +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/jwt/AuthTokenFilter.java @@ -0,0 +1,55 @@ +package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt; + +import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service.UserDetailsServiceImpl; +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.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.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class AuthTokenFilter extends OncePerRequestFilter { + + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private UserDetailsServiceImpl userDetailsService; + + @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); + + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, + null, + userDetails.getAuthorities()); + + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + logger.error("Cannot set user authentication: {}", e); + } + + filterChain.doFilter(request, response); + } + + private String parseJwt(HttpServletRequest request) { + String jwt = jwtUtils.getJwtFromCookies(request); + return jwt; + } +} diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/jwt/JwtUtils.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/jwt/JwtUtils.java new file mode 100644 index 0000000..99c29dc --- /dev/null +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/jwt/JwtUtils.java @@ -0,0 +1,87 @@ +package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt; + +import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service.UserDetailsImpl; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; +import org.springframework.web.util.WebUtils; + +import java.security.Key; +import java.util.Date; + +@Component +public class JwtUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(JwtUtils.class); + + @Value("${spring.app.jwtSecret}") + private String jwtSecret; + + @Value("${spring.app.jwtExpirationMs}") + private int jwtExpirationMs; + + @Value("${spring.app.jwtCookieName}") + private String jwtCookie; + + public String getJwtFromCookies(HttpServletRequest request) { + Cookie cookie = WebUtils.getCookie(request, jwtCookie); + if (cookie != null) { + return cookie.getValue(); + } else { + return null; + } + } + + public ResponseCookie generateJwtCookie(UserDetailsImpl userDetails) { + String jwt = generateTokenFromUsername(userDetails.getUsername()); + ResponseCookie cookie = ResponseCookie.from(jwtCookie, jwt).path("/api").maxAge(24 * 60 * 60).httpOnly(true).build(); + return cookie; + } + + public ResponseCookie getCleanJwtCookie() { + ResponseCookie cookie = ResponseCookie.from(jwtCookie, null).path("/api").build(); + return cookie; + } + + public String getUserNameFromJwtToken(String token) { + return Jwts.parserBuilder().setSigningKey(key()).build() + .parseClaimsJws(token).getBody().getSubject(); + } + + private Key key() { + return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret)); + } + + public boolean validateJwtToken(String authToken) { + try { + Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken); + return true; + } 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; + } + + public String generateTokenFromUsername(String username) { + return Jwts.builder() + .setSubject(username) + .setIssuedAt(new Date()) + .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)) + .signWith(key(), SignatureAlgorithm.HS256) + .compact(); + } +} diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/repository/RoleRepository.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/repository/RoleRepository.java new file mode 100644 index 0000000..c1c499a --- /dev/null +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/repository/RoleRepository.java @@ -0,0 +1,15 @@ +package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository; + +import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.ERole; +import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.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(ERole name); + +} diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/repository/UserRepository.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/repository/UserRepository.java new file mode 100644 index 0000000..367efd9 --- /dev/null +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/modules/auth/repository/UserRepository.java @@ -0,0 +1,18 @@ +package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository; + +import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.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); + + Boolean existsByUsername(String username); + + Boolean existsByEmail(String email); + +}