diff --git a/pom.xml b/pom.xml
index 1ab2d6a..91cc172 100644
--- a/pom.xml
+++ b/pom.xml
@@ -87,6 +87,25 @@
spring-security-test
test
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.7
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.7
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.7
+ runtime
+
+
+
junit
@@ -138,6 +157,7 @@
org.springframework.boot
spring-boot-starter-actuator
+
ch.qos.logback
@@ -149,11 +169,13 @@
wiremock-spring-boot
3.10.0
+
org.springframework.boot
spring-boot-starter-mail
+
io.github.cdimascio
diff --git a/src/main/java/org/pkwmtt/repository/UserRepository.java b/src/main/java/org/pkwmtt/repository/UserRepository.java
index 71ccd75..1e53f12 100644
--- a/src/main/java/org/pkwmtt/repository/UserRepository.java
+++ b/src/main/java/org/pkwmtt/repository/UserRepository.java
@@ -2,6 +2,8 @@
import org.pkwmtt.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
+import java.util.Optional;
public interface UserRepository extends JpaRepository {
+ Optional findByEmail(String email);
}
\ No newline at end of file
diff --git a/src/main/java/org/pkwmtt/security/auth/AuthController.java b/src/main/java/org/pkwmtt/security/auth/AuthController.java
new file mode 100644
index 0000000..f86dd7a
--- /dev/null
+++ b/src/main/java/org/pkwmtt/security/auth/AuthController.java
@@ -0,0 +1,24 @@
+package org.pkwmtt.security.auth;
+
+import lombok.RequiredArgsConstructor;
+import org.pkwmtt.security.auth.dto.UserRequestDTO;
+import org.pkwmtt.security.token.JwtServiceImpl;
+import org.pkwmtt.security.token.dto.UserDTO;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/pkwmtt/auth")
+@RequiredArgsConstructor
+public class AuthController {
+ private final AuthService authService;
+ private final JwtServiceImpl jwtServiceImpl;
+
+ @PostMapping("/authenticate")
+ public String authenticate(@RequestBody UserRequestDTO requestUser) {
+ UserDTO user = authService.authenticateUser(requestUser.getEmail());
+ return jwtServiceImpl.generateToken(user);
+ }
+}
diff --git a/src/main/java/org/pkwmtt/security/auth/AuthService.java b/src/main/java/org/pkwmtt/security/auth/AuthService.java
new file mode 100644
index 0000000..3fc3cde
--- /dev/null
+++ b/src/main/java/org/pkwmtt/security/auth/AuthService.java
@@ -0,0 +1,7 @@
+package org.pkwmtt.security.auth;
+
+import org.pkwmtt.security.token.dto.UserDTO;
+
+public interface AuthService {
+ UserDTO authenticateUser(String email);
+}
diff --git a/src/main/java/org/pkwmtt/security/auth/AuthServiceImpl.java b/src/main/java/org/pkwmtt/security/auth/AuthServiceImpl.java
new file mode 100644
index 0000000..a878b52
--- /dev/null
+++ b/src/main/java/org/pkwmtt/security/auth/AuthServiceImpl.java
@@ -0,0 +1,30 @@
+package org.pkwmtt.security.auth;
+
+import lombok.RequiredArgsConstructor;
+import org.pkwmtt.security.auth.dto.UserRequestDTO;
+import org.pkwmtt.security.token.dto.UserDTO;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class AuthServiceImpl implements AuthService {
+ private final AuthenticationManager authenticationManager;
+
+ @Override
+ public UserDTO authenticateUser(String email) {
+ Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
+ email,
+ null
+ ));
+
+ if (!authentication.isAuthenticated()) {
+ throw new UsernameNotFoundException("Invalid credentials");
+ }
+
+ return (UserDTO) authentication.getPrincipal();
+ }
+}
diff --git a/src/main/java/org/pkwmtt/security/auth/dto/UserRequestDTO.java b/src/main/java/org/pkwmtt/security/auth/dto/UserRequestDTO.java
new file mode 100644
index 0000000..f30ed0e
--- /dev/null
+++ b/src/main/java/org/pkwmtt/security/auth/dto/UserRequestDTO.java
@@ -0,0 +1,9 @@
+package org.pkwmtt.security.auth.dto;
+
+import lombok.Data;
+
+@Data
+public class UserRequestDTO {
+ private String otp_code;
+ private String email;
+}
diff --git a/src/main/java/org/pkwmtt/security/auth/provider/OTPAuthenticationProvider.java b/src/main/java/org/pkwmtt/security/auth/provider/OTPAuthenticationProvider.java
new file mode 100644
index 0000000..4baf894
--- /dev/null
+++ b/src/main/java/org/pkwmtt/security/auth/provider/OTPAuthenticationProvider.java
@@ -0,0 +1,61 @@
+package org.pkwmtt.security.auth.provider;
+import lombok.RequiredArgsConstructor;
+import org.pkwmtt.entity.User;
+import org.pkwmtt.repository.UserRepository;
+import org.pkwmtt.security.token.dto.UserDTO;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+@Component
+@RequiredArgsConstructor
+public class OTPAuthenticationProvider implements AuthenticationProvider {
+ private final UserRepository userRepository;
+
+ @Override
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+ String email = authentication.getName();
+
+ // Fetch user from DB
+ User user = userRepository.findByEmail(email)
+ .orElseThrow(() -> new BadCredentialsException("User not found"));
+
+ // Wrap role in a list to support multiple roles in the future
+ List authorities = Stream.of(user.getRole())
+ .map(role -> new SimpleGrantedAuthority("ROLE_" + role.name()))
+ .toList();
+
+ // Validate critical user fields
+ if(!isValidForAuthentication(user)){
+ throw new BadCredentialsException(
+ "Invalid User Credentials. Please contact the administrator."
+ );
+ }
+
+ UserDTO userMapped = new UserDTO(user);
+ return new UsernamePasswordAuthenticationToken(userMapped, null, authorities);
+ }
+
+ @Override
+ public boolean supports(Class> authentication) {
+ return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
+ }
+
+ /**
+ * Validates user data before authentication.
+ * Returns true if user has email, role, group, and is active.
+ */
+ private boolean isValidForAuthentication(User user) {
+ return user.getEmail() != null &&
+ user.getRole() != null &&
+ user.getGeneralGroup() != null &&
+ user.isActive();
+ }
+}
diff --git a/src/main/java/org/pkwmtt/security/config/SpringSecurity.java b/src/main/java/org/pkwmtt/security/config/SpringSecurity.java
index c4dc591..37aea49 100644
--- a/src/main/java/org/pkwmtt/security/config/SpringSecurity.java
+++ b/src/main/java/org/pkwmtt/security/config/SpringSecurity.java
@@ -1,21 +1,29 @@
package org.pkwmtt.security.config;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.pkwmtt.security.auth.provider.OTPAuthenticationProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
+import java.util.List;
+
import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
//@EnableWebSecurity
@Slf4j
@Configuration
+@RequiredArgsConstructor
public class SpringSecurity {
+ private final OTPAuthenticationProvider otpAuthenticationProvider;
+
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("Configuring Security Filter Chain...");
@@ -30,4 +38,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("Configuring Success...");
return http.build();
}
+
+ @Bean
+ public AuthenticationManager authenticationManager() {
+ return new ProviderManager(List.of(otpAuthenticationProvider));
+ }
}
diff --git a/src/main/java/org/pkwmtt/security/token/JwtService.java b/src/main/java/org/pkwmtt/security/token/JwtService.java
new file mode 100644
index 0000000..e248f86
--- /dev/null
+++ b/src/main/java/org/pkwmtt/security/token/JwtService.java
@@ -0,0 +1,9 @@
+package org.pkwmtt.security.token;
+
+import org.pkwmtt.security.token.dto.UserDTO;
+
+public interface JwtService {
+ String generateToken(UserDTO user);
+ Boolean validateToken(String token);
+ String getUserIdFromToken(String token);
+}
diff --git a/src/main/java/org/pkwmtt/security/token/JwtServiceImpl.java b/src/main/java/org/pkwmtt/security/token/JwtServiceImpl.java
new file mode 100644
index 0000000..f94079e
--- /dev/null
+++ b/src/main/java/org/pkwmtt/security/token/JwtServiceImpl.java
@@ -0,0 +1,81 @@
+package org.pkwmtt.security.token;
+
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import lombok.RequiredArgsConstructor;
+import org.pkwmtt.security.token.dto.UserDTO;
+import org.pkwmtt.security.token.utils.JwtUtils;
+import org.springframework.stereotype.Service;
+
+import javax.crypto.SecretKey;
+import java.util.Base64;
+import java.util.Date;
+
+@Service
+@RequiredArgsConstructor
+public class JwtServiceImpl implements JwtService {
+ private final JwtUtils jwtUtils;
+
+
+ /**
+ * Generates a JWT token for a given user.
+ * The token contains user's email, group, and role as claims,
+ * and is signed with a secret key.
+ *
+ * @param user - required user data to include in token claims
+ * @return signed JWT token as a String
+ */
+ @Override
+ public String generateToken(UserDTO user) {
+ return Jwts.builder()
+ .subject(user.getEmail())
+ .claim("group", user.getGroup())
+ .claim("role", user.getRole())
+ .issuedAt(new Date())
+ .expiration((new Date(System.currentTimeMillis() + jwtUtils.getExpirationMs())))
+ .signWith(decodeSecretKey())
+ .compact();
+ }
+
+
+ /**
+ * Decode a secret key for signing JWT.
+ * The key is decoded from Base64 stored in JwtUtils configuration.
+ *
+ * @return secret key for JWT signing
+ */
+ private SecretKey decodeSecretKey(){
+ byte[] decodedKey = Base64.getDecoder().decode(jwtUtils.getSecret());
+ return Keys.hmacShaKeyFor(decodedKey);
+ }
+
+ /**
+ * Validate a JWT token.
+ * Attempts to parse the token; if parsing fails, the token is considered invalid.
+ *
+ * @param token JWT token string to validate
+ * @return true if the token is valid, false otherwise
+ */
+ @Override
+ public Boolean validateToken(String token) {
+ try {
+ // TODO: add logic to validate the token
+ return true;
+ } catch (JwtException | IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Extracts the user identifier (email) from a JWT token.
+ *
+ * @param token JWT token to extract user from
+ * @return user email from token
+ */
+ @Override
+ public String getUserIdFromToken(String token) {
+ // TODO: implement token parsing to extract subject/email
+ return "";
+ }
+}
diff --git a/src/main/java/org/pkwmtt/security/token/dto/UserDTO.java b/src/main/java/org/pkwmtt/security/token/dto/UserDTO.java
new file mode 100644
index 0000000..b193edd
--- /dev/null
+++ b/src/main/java/org/pkwmtt/security/token/dto/UserDTO.java
@@ -0,0 +1,23 @@
+package org.pkwmtt.security.token.dto;
+
+import lombok.Data;
+import org.pkwmtt.entity.GeneralGroup;
+import org.pkwmtt.entity.User;
+import org.pkwmtt.enums.Role;
+
+import java.util.Optional;
+
+@Data
+public class UserDTO {
+ private String email;
+ private String group;
+ private Role role;
+
+ public UserDTO(User user){
+ this.email = user.getEmail();
+ this.role = user.getRole();
+ this.group = Optional.ofNullable(user.getGeneralGroup())
+ .map(GeneralGroup::getName)
+ .orElse(null);
+ }
+}
diff --git a/src/main/java/org/pkwmtt/security/token/utils/JwtUtils.java b/src/main/java/org/pkwmtt/security/token/utils/JwtUtils.java
new file mode 100644
index 0000000..21ee1bb
--- /dev/null
+++ b/src/main/java/org/pkwmtt/security/token/utils/JwtUtils.java
@@ -0,0 +1,20 @@
+package org.pkwmtt.security.token.utils;
+
+import lombok.Getter;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+
+@Getter
+@Component
+public class JwtUtils {
+ // Secret key used for signing JWTs. If the environment variable JWT_SECRET_KEY
+ // is not set, a default value "TEST_SECRET" is used. This allows the application
+ // to start without a real secret, e.g., for local development or tests.
+ private final String secret;
+ private final long expirationMs = 1000L * 60 * 60 * 24 * 30 * 6;
+
+ public JwtUtils(Environment environment) {
+ // Get the secret key from environment variables, or fallback to "TEST_SECRET"
+ this.secret = environment.getProperty("JWT_SECRET_KEY", "TEST_SECRET");
+ }
+}
diff --git a/src/test/java/org/pkwmtt/jwt/JwtSecretMakerTest.java b/src/test/java/org/pkwmtt/jwt/JwtSecretMakerTest.java
new file mode 100644
index 0000000..ed43d5d
--- /dev/null
+++ b/src/test/java/org/pkwmtt/jwt/JwtSecretMakerTest.java
@@ -0,0 +1,21 @@
+package org.pkwmtt.jwt;
+
+import io.jsonwebtoken.Jwts;
+import jakarta.xml.bind.DatatypeConverter;
+import org.junit.jupiter.api.Test;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+public class JwtSecretMakerTest {
+
+ @Test
+ public void generateSecretKey(){
+ SecretKey key = Jwts.SIG.HS512.key().build();
+ String encodedKey = DatatypeConverter.printHexBinary(key.getEncoded());
+ System.out.printf("\nKey = [%s]\n", encodedKey);
+ String base64Secret = Base64.getEncoder().encodeToString(encodedKey.getBytes(StandardCharsets.UTF_8));
+ System.out.println(base64Secret);
+ }
+}