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