Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.7</version>
<scope>runtime</scope>
</dependency>


<!--TESTING-->
<dependency>
<groupId>junit</groupId>
Expand Down Expand Up @@ -138,6 +157,7 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!--LOGBACK-->
<dependency>
<groupId>ch.qos.logback</groupId>
Expand All @@ -149,11 +169,13 @@
<artifactId>wiremock-spring-boot</artifactId>
<version>3.10.0</version>
</dependency>

<!--EMAIL-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

<!--.ENV-->
<dependency>
<groupId>io.github.cdimascio</groupId>
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/pkwmtt/repository/UserRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<User, Integer> {
Optional<User> findByEmail(String email);
}
24 changes: 24 additions & 0 deletions src/main/java/org/pkwmtt/security/auth/AuthController.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
7 changes: 7 additions & 0 deletions src/main/java/org/pkwmtt/security/auth/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.pkwmtt.security.auth;

import org.pkwmtt.security.token.dto.UserDTO;

public interface AuthService {
UserDTO authenticateUser(String email);
}
30 changes: 30 additions & 0 deletions src/main/java/org/pkwmtt/security/auth/AuthServiceImpl.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.pkwmtt.security.auth.dto;

import lombok.Data;

@Data
public class UserRequestDTO {
private String otp_code;
private String email;
}
Original file line number Diff line number Diff line change
@@ -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<SimpleGrantedAuthority> 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();
}
}
15 changes: 14 additions & 1 deletion src/main/java/org/pkwmtt/security/config/SpringSecurity.java
Original file line number Diff line number Diff line change
@@ -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...");
Expand All @@ -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));
}
}
9 changes: 9 additions & 0 deletions src/main/java/org/pkwmtt/security/token/JwtService.java
Original file line number Diff line number Diff line change
@@ -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);
}
81 changes: 81 additions & 0 deletions src/main/java/org/pkwmtt/security/token/JwtServiceImpl.java
Original file line number Diff line number Diff line change
@@ -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 "";
}
}
23 changes: 23 additions & 0 deletions src/main/java/org/pkwmtt/security/token/dto/UserDTO.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
20 changes: 20 additions & 0 deletions src/main/java/org/pkwmtt/security/token/utils/JwtUtils.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
21 changes: 21 additions & 0 deletions src/test/java/org/pkwmtt/jwt/JwtSecretMakerTest.java
Original file line number Diff line number Diff line change
@@ -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);
}
}