From 29c75bd51867ace6c43d3d364fec9f415386a341 Mon Sep 17 00:00:00 2001 From: henridb <henridb@stud.ntnu.no> Date: Wed, 17 Apr 2024 09:28:34 +0200 Subject: [PATCH] feat: add JWT token system --- .../properties/TokenProperties.java | 21 +++++ .../savingsapp/security/AuthIdentity.java | 22 +++++ .../security/AuthorizationFilter.java | 86 +++++++++++++++++++ .../savingsapp/security/SecurityConfig.java | 77 +++++++++++++++++ .../idi/stud/savingsapp/utils/TokenUtils.java | 35 ++++++++ 5 files changed, 241 insertions(+) create mode 100644 src/main/java/no/ntnu/idi/stud/savingsapp/properties/TokenProperties.java create mode 100644 src/main/java/no/ntnu/idi/stud/savingsapp/security/AuthIdentity.java create mode 100644 src/main/java/no/ntnu/idi/stud/savingsapp/security/AuthorizationFilter.java create mode 100644 src/main/java/no/ntnu/idi/stud/savingsapp/security/SecurityConfig.java create mode 100644 src/main/java/no/ntnu/idi/stud/savingsapp/utils/TokenUtils.java diff --git a/src/main/java/no/ntnu/idi/stud/savingsapp/properties/TokenProperties.java b/src/main/java/no/ntnu/idi/stud/savingsapp/properties/TokenProperties.java new file mode 100644 index 0000000..1f9405e --- /dev/null +++ b/src/main/java/no/ntnu/idi/stud/savingsapp/properties/TokenProperties.java @@ -0,0 +1,21 @@ +package no.ntnu.idi.stud.savingsapp.properties; + +import org.springframework.stereotype.Component; + +/** + * Configuration properties for token generation. + */ +@Component +public final class TokenProperties { + + /** + * The secret key used for token generation. + */ + public static final String SECRET = "topsecretkey"; + + /** + * The duration of the token validity in minutes. + */ + public static final int DURATION = 30; + +} diff --git a/src/main/java/no/ntnu/idi/stud/savingsapp/security/AuthIdentity.java b/src/main/java/no/ntnu/idi/stud/savingsapp/security/AuthIdentity.java new file mode 100644 index 0000000..526337d --- /dev/null +++ b/src/main/java/no/ntnu/idi/stud/savingsapp/security/AuthIdentity.java @@ -0,0 +1,22 @@ +package no.ntnu.idi.stud.savingsapp.security; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Represents the identity of an authenticated user. + */ +@Data +@AllArgsConstructor +public class AuthIdentity { + + /** + * The ID of the authenticated user. + */ + private long id; + + /** + * The role of the authenticated user. + */ + private String role; +} diff --git a/src/main/java/no/ntnu/idi/stud/savingsapp/security/AuthorizationFilter.java b/src/main/java/no/ntnu/idi/stud/savingsapp/security/AuthorizationFilter.java new file mode 100644 index 0000000..4346b2f --- /dev/null +++ b/src/main/java/no/ntnu/idi/stud/savingsapp/security/AuthorizationFilter.java @@ -0,0 +1,86 @@ +package no.ntnu.idi.stud.savingsapp.security; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import no.ntnu.idi.stud.savingsapp.model.Role; +import no.ntnu.idi.stud.savingsapp.properties.TokenProperties; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Collections; + +/** + * Filter responsible for JSON Web Token (JWT) authorization. + * It extracts the JWT from the request header, validates it, and sets + * the authentication context. + */ +public class AuthorizationFilter extends OncePerRequestFilter { + + private static final Logger LOGGER = LogManager.getLogger(AuthorizationFilter.class); + + /** + * Filters incoming requests and processes JWT authorization. + * + * @param request The HTTP servlet request. + * @param response The HTTP servlet response. + * @param filterChain The filter chain for the request. + * @throws ServletException If an error occurs during servlet processing. + * @throws IOException If an I/O error occurs during request processing. + */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + final String header = request.getHeader(HttpHeaders.AUTHORIZATION); + if (header == null || !header.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + String token = header.substring(7); + final DecodedJWT decodedJWT = validateToken(token); + if (decodedJWT == null) { + filterChain.doFilter(request, response); + return; + } + long userId = Long.parseLong(decodedJWT.getSubject()); + String userRole = decodedJWT.getClaim("user_role").asString(); + String role = userRole.equals(Role.ADMIN.name()) ? "ADMIN" : "USER"; + + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + new AuthIdentity(userId, role), null, + Collections.singletonList(new SimpleGrantedAuthority(role))); + SecurityContextHolder.getContext().setAuthentication(authentication); + + filterChain.doFilter(request, response); + } + + /** + * Validates the JWT token. + * + * @param token The JWT token to validate. + * @return The decoded JWT if valid, null otherwise. + */ + public DecodedJWT validateToken(final String token) { + try { + final Algorithm hmac512 = Algorithm.HMAC512(TokenProperties.SECRET); + final JWTVerifier verifier = JWT.require(hmac512).build(); + return verifier.verify(token); + } catch (final JWTVerificationException verificationEx) { + LOGGER.warn("token is invalid: {}", verificationEx.getMessage()); + return null; + } + } +} diff --git a/src/main/java/no/ntnu/idi/stud/savingsapp/security/SecurityConfig.java b/src/main/java/no/ntnu/idi/stud/savingsapp/security/SecurityConfig.java new file mode 100644 index 0000000..df4425f --- /dev/null +++ b/src/main/java/no/ntnu/idi/stud/savingsapp/security/SecurityConfig.java @@ -0,0 +1,77 @@ +package no.ntnu.idi.stud.savingsapp.security; + +import jakarta.servlet.DispatcherType; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; + +/** + * Configuration class responsible for defining security configurations for the application. + */ +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + /** + * Provides a bean for password encoder. + * + * @return A PasswordEncoder instance. + */ + @Bean + public PasswordEncoder encoder() { + return new BCryptPasswordEncoder(); + } + + /** + * Configures the security filter chain. + * + * @param http The HttpSecurity object to configure. + * @return A SecurityFilterChain instance. + * @throws Exception If an error occurs during configuration. + */ + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http.cors() + .and() + .csrf() + .disable() + .authorizeHttpRequests(auth -> { + auth.dispatcherTypeMatchers(DispatcherType.ERROR).permitAll() + .requestMatchers("/swagger/**", "/api/auth/**").permitAll().anyRequest().authenticated(); + }) + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + .addFilterBefore(new AuthorizationFilter(), UsernamePasswordAuthenticationFilter.class) + .build(); + } + + /** + * Provides a bean for configuring CORS (Cross-Origin Resource Sharing). + * + * @return A CorsConfigurationSource instance. + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + config.setAllowedOrigins(List.of("http://localhost:5173")); + config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "Cache-Control")); + config.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + return source; + } +} diff --git a/src/main/java/no/ntnu/idi/stud/savingsapp/utils/TokenUtils.java b/src/main/java/no/ntnu/idi/stud/savingsapp/utils/TokenUtils.java new file mode 100644 index 0000000..8f63af5 --- /dev/null +++ b/src/main/java/no/ntnu/idi/stud/savingsapp/utils/TokenUtils.java @@ -0,0 +1,35 @@ +package no.ntnu.idi.stud.savingsapp.utils; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import no.ntnu.idi.stud.savingsapp.model.User; +import no.ntnu.idi.stud.savingsapp.properties.TokenProperties; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.Instant; + +/** + * Utility class for working with JWT (JSON Web Token) generation and validation. + */ +@Component +public final class TokenUtils { + + /** + * Generates a JWT (JSON Web Token) for the given user. + * + * @param user The user for whom the token is generated. + * @return The generated JWT token as a string. + */ + public static String generateToken(final User user) { + final Instant now = Instant.now(); + final Algorithm hmac512 = Algorithm.HMAC512(TokenProperties.SECRET);; + return JWT.create() + .withSubject(String.valueOf(user.getId())) + .withClaim("user_role", user.getRole().name()) + .withIssuer("sparesti") + .withIssuedAt(now) + .withExpiresAt(now.plusMillis(Duration.ofMinutes(TokenProperties.DURATION).toMillis())) + .sign(hmac512); + } +} -- GitLab