package de.w665.biblenotes.service; import de.w665.biblenotes.db.repo.UserLoginRepository; import de.w665.biblenotes.db.repo.UserRepository; import de.w665.biblenotes.db.entity.User; import de.w665.biblenotes.db.entity.UserLogin; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import javax.crypto.SecretKey; import java.time.LocalDateTime; import java.util.Base64; import java.util.Date; import java.util.Optional; @Slf4j @Service public class AuthenticationService { private final UserRepository userRepository; private final UserLoginRepository userLoginRepository; @Value("${secureapi.jwt.secret}") private String secretString; @Value("${secureapi.jwt.expiration}") private long expirationTime; // in milliseconds private SecretKey secretKey; private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); public AuthenticationService(UserRepository userRepository, UserLoginRepository userLoginRepository) { this.userRepository = userRepository; this.userLoginRepository = userLoginRepository; } @PostConstruct public void init() { log.debug("Initializing secret key"); byte[] encodedKey = Base64.getEncoder().encode(secretString.getBytes()); // encode the secret key this.secretKey = Keys.hmacShaKeyFor(encodedKey); } public String authenticate(String username, String password, String remoteAddr, long... expirationTime/*FOR TESTING VALIDITY*/) { if(expirationTime.length > 0) { this.expirationTime = expirationTime[0]; } log.debug("Authenticating user: {}", username); Optional optionalUser = userRepository.findByUsername(username); if (optionalUser.isPresent() && passwordEncoder.matches(password, optionalUser.get().getPassword())) { User user = optionalUser.get(); UserLogin userLogin = new UserLogin(); userLogin.setUserId(user.getId()); userLogin.setLoginTime(LocalDateTime.now()); userLogin.setLoginIp(remoteAddr); userLoginRepository.save(userLogin); userRepository.updateLastLoginForUser(user.getUsername(), LocalDateTime.now()); return generateToken(user); } return null; } private String generateToken(User user) { long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); Date expiryDate = new Date(nowMillis + expirationTime); return Jwts.builder() .subject("BibleNotes Authentication Token") .issuedAt(now) .claim("role", user.getRole()) .claim("username", user.getUsername()) .claim("userId", user.getId()) .expiration(expiryDate) .signWith(secretKey) .compact(); } public boolean validateToken(String token) { try { Jwt jwt = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token); Claims claims = (Claims) jwt.getPayload(); return !claims.getExpiration().before(new Date()); // Checks if the token is expired too } catch (Exception e) { return false; } } public String extractSubject(String token) { return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getSubject(); } /** * Retrieves a typed claim from the JWT. * @param token the JWT from which to extract the claim * @param claimName the name of the claim to retrieve * @param claimType the Class object of the expected type of the claim value * @return the value of the specified claim as type T, or null if not found or in case of an error * Usage example: getClaimValue(token, "role", String.class) */ public T getClaimValue(String token, String claimName, Class claimType) { try { Jwt jwt = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token); Claims claims = (Claims) jwt.getPayload(); return claims.get(claimName, claimType); } catch (Exception e) { log.error("Error parsing claims from token: ", e); return null; } } public String encodePassword(String password) { return passwordEncoder.encode(password); } }