Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package smartpot.com.api.Exception;

public class EncryptionException extends RuntimeException {
public EncryptionException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,17 @@
import org.springframework.web.filter.OncePerRequestFilter;
import smartpot.com.api.Security.Service.JwtService;
import smartpot.com.api.Users.Model.DTO.UserDTO;
import smartpot.com.api.Users.Service.SUser;

import java.io.IOException;

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

// TODO: implement role for jwt

private final JwtService jwtService;
private final SUser serviceUser;

public JwtAuthFilter(JwtService jwtService, SUser serviceUser) {
public JwtAuthFilter(JwtService jwtService) {
this.jwtService = jwtService;
this.serviceUser = serviceUser;
}

@Override
Expand All @@ -35,16 +31,14 @@ protected void doFilterInternal(
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {

String authHeader = request.getHeader("Authorization");
try {
UserDTO user = jwtService.validateAuthHeader(authHeader);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
user, user.getPassword(), null /* user.getAuthorities() */);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
} catch (Exception ignored) {
}
} catch (Exception ignored) {}
filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ public ResponseEntity<?> forgotPassword(@RequestBody UserDTO reqUser) {
)
}
)
public ResponseEntity<?> resetPassword(@RequestBody UserDTO reqUser, @RequestHeader("Authorization") String resetToken) {
public ResponseEntity<?> resetPassword(@RequestBody UserDTO reqUser, @RequestHeader("Authorization") String token, @RequestHeader("Reset-Token") String resetToken) {
try {
return new ResponseEntity<>(new TokenResponse(jwtService.resetPassword(reqUser)), HttpStatus.OK);
return new ResponseEntity<>(new TokenResponse(jwtService.resetPassword(reqUser, jwtService.validateAuthHeader(token).getEmail(), resetToken)), HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(new ErrorResponse("Error al restablecer contraseña [" + e.getMessage() + "]", HttpStatus.BAD_REQUEST.value()), HttpStatus.BAD_REQUEST);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package smartpot.com.api.Security.Model.DTO;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@NoArgsConstructor
public class ResetTokenDTO {
private String token;
private String operation;
private Date expiration;

public ResetTokenDTO(String token, String operation, Date expiration) {
this.token = token;
this.operation = operation;
this.expiration = expiration;
}

static public String convertToJson(ResetTokenDTO resetToken) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(resetToken);
}

public static ResetTokenDTO convertToDTO(String json) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(json, ResetTokenDTO.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package smartpot.com.api.Security.Service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import smartpot.com.api.Exception.EncryptionException;

import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;

@Service
public class AESEncryptionService implements EncryptionServiceI {

SecureRandom random = new SecureRandom();
@Value("${application.security.aes.key}")
private String aesKey;

public AESEncryptionService() {}

private SecretKey getSecretKey() {
byte[] decoded = Base64.getDecoder().decode(aesKey);
if (decoded.length != 32) {
throw new IllegalArgumentException("La clave debe tener 256 bits (32 bytes)");
}
return new SecretKeySpec(decoded, "AES");
}

@Override
public String encrypt(String data) throws EncryptionException {
try {
// get salt
byte[] salt = new byte[8];
random.nextBytes(salt);
String saltedData = Base64.getEncoder().encodeToString(salt) + ":" + data;

byte[] iv = new byte[12];
random.nextBytes(iv);
SecretKey key = getSecretKey(); // get encryption key
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));

byte[] encrypted = cipher.doFinal(saltedData.getBytes());

byte[] output = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, output, 0, iv.length);
System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
return Base64.getUrlEncoder().encodeToString(output);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | // if you need more specific errors, catch each one separately
InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new EncryptionException("Error while encrypting data");
}

}

@Override
public String decrypt(String encryptedData) throws EncryptionException {
try {
byte[] decoded = Base64.getUrlDecoder().decode(encryptedData);

byte[] iv = new byte[12];
byte[] cipherText = new byte[decoded.length - 12];
System.arraycopy(decoded, 0, iv, 0, 12);
System.arraycopy(decoded, 12, cipherText, 0, cipherText.length);

SecretKey key = getSecretKey();
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
String result = new String(cipher.doFinal(cipherText));

// remove salt
return result.split(":", 2)[1];
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | // if you need more specific errors, catch each one separately
InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new EncryptionException("Error while decrypting data");
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package smartpot.com.api.Security.Service;

import smartpot.com.api.Exception.EncryptionException;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public interface EncryptionServiceI {
String encrypt(String plainText) throws EncryptionException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException;
String decrypt(String cipherText) throws EncryptionException;
}
89 changes: 60 additions & 29 deletions src/main/java/smartpot/com/api/Security/Service/JwtService.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,24 @@
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import smartpot.com.api.Exception.EncryptionException;
import smartpot.com.api.Exception.InvalidTokenException;
import smartpot.com.api.Mail.Model.DTO.EmailDTO;
import smartpot.com.api.Mail.Service.EmailService;
import smartpot.com.api.Mail.Validator.EmailValidatorI;
import smartpot.com.api.Security.Model.DTO.ResetTokenDTO;
import smartpot.com.api.Users.Model.DTO.UserDTO;
import smartpot.com.api.Users.Service.SUserI;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.*;

@Service
public class JwtService implements JwtServiceI {

private final SUserI serviceUser;
private final EmailService emailService;
private final EmailValidatorI emailValidator;
private final EncryptionServiceI encryptionService;

@Value("${application.security.jwt.secret-key}")
private String secretKey;
Expand All @@ -41,29 +40,27 @@ public class JwtService implements JwtServiceI {
* @param serviceUser servicio que maneja las operaciones de base de datos.
*/
@Autowired
public JwtService(SUserI serviceUser, EmailService emailService, EmailValidatorI emailValidator) {
public JwtService(SUserI serviceUser, EmailService emailService, EmailValidatorI emailValidator, EncryptionServiceI encryptionService) {
this.serviceUser = serviceUser;
this.emailService = emailService;
this.emailValidator = emailValidator;
this.encryptionService = encryptionService;
}

@Override
public String Login(UserDTO reqUser) throws Exception {
return Optional.of(serviceUser.getUserByEmail(reqUser.getEmail()))
.filter(userDTO -> new BCryptPasswordEncoder().matches(reqUser.getPassword(), userDTO.getPassword()))
.map(validUser -> generateToken(validUser.getId(), validUser.getEmail()))
.map(validUser -> {
try {
return generateToken(validUser.getId(), validUser.getEmail());
} catch (Exception e) {
throw new ValidationException(e);
}
})
.orElseThrow(() -> new Exception("Credenciales Invalidas"));

}

@Override
public String Register(UserDTO reqUser) throws Exception {
return Optional.ofNullable(serviceUser.CreateUser(reqUser))
.map(user -> generateToken(reqUser.getId(), user.getEmail()))
.orElseThrow(() -> new Exception("User already registered."));
}

private String generateToken(String id, String email) {
private String generateToken(String id, String email) throws Exception {
// TODO: Refine token (email != subject)
Map<String, Object> claims = new HashMap<>();
claims.put("id", id);
Expand All @@ -75,11 +72,13 @@ private String generateToken(String id, String email) {

@Override
public UserDTO validateAuthHeader(String authHeader) throws Exception {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new Exception("El encabezado de autorización es inválido. Se esperaba 'Bearer <token>'.");
if (authHeader == null || !authHeader.startsWith("SmartPot-OAuth ")) {
throw new Exception("El encabezado de autorización es inválido. Se esperaba 'SmartPot-OAuth <token>'.");
}

String token = authHeader.split(" ")[1];
token = encryptionService.decrypt(token);

String email = extractEmail(token);
UserDetails user = serviceUser.loadUserByUsername(email);
if (email == null) {
Expand All @@ -93,27 +92,53 @@ public UserDTO validateAuthHeader(String authHeader) throws Exception {
UserDTO finalUser = serviceUser.getUserByEmail(email);
finalUser.setPassword("");
return finalUser;

}

@Override
public String resetPassword(UserDTO reqUser) throws Exception {
return Optional.of(serviceUser.getUserByEmail(reqUser.getEmail()))
public String resetPassword(UserDTO user, String email, String resetToken) throws Exception {
return Optional.of(serviceUser.getUserByEmail(email))
.map(validUser -> {
try {
return serviceUser.UpdateUser(validUser.getId(), validUser);
String decrypted = encryptionService.decrypt(resetToken);
ResetTokenDTO resetTokenDTO = ResetTokenDTO.convertToDTO(decrypted);

if (!validateResetToken(resetTokenDTO)) {
throw new ValidationException("Provided reset token is not valid");
}

return serviceUser.UpdateUserPassword(validUser, user.getPassword());
} catch (Exception e) {
throw new ValidationException(e);
}
})
.map(validUser -> {
try {
return generateToken(validUser.getId(), validUser.getEmail());
} catch (Exception e) {
throw new ValidationException(e);
}
})
.map(validUser -> generateToken(validUser.getId(), validUser.getEmail()))
.orElseThrow(() -> new Exception("Credenciales Invalidas"));
}

@Override
public Boolean forgotPassword(String email) throws Exception {
return Optional.of(serviceUser.getUserByEmail(email))
.map(validUser -> generateToken(validUser.getId(), validUser.getEmail()))
.map(validUser -> {
try {
return generateToken(validUser.getId(), validUser.getEmail());
} catch (Exception e) {
throw new ValidationException(e);
}
})
.map(token -> new ResetTokenDTO(token, "reset", new Date(System.currentTimeMillis() + expiration) ))
.map(token -> {
try {
return encryptionService.encrypt(ResetTokenDTO.convertToJson(token));
} catch (Exception e) {
throw new EncryptionException("Conversion to json or encryption failed: " + e);
}
})
.map(token -> new EmailDTO(null, email, "Token para recuperar contraseña: " + token, "Recuperar contraseña", "", null, "true"))
.map(emailService::sendSimpleMail)
.map(ValidDTO -> {
Expand All @@ -136,19 +161,26 @@ private Boolean validateToken(String token, UserDetails userDetails) {
}
String username = extractUsername(token);
return userDetails.getUsername().equals(username) && !expirationDate.before(new Date());
}


private Boolean validateResetToken(ResetTokenDTO resetTokenDTO) {
String token = encryptionService.decrypt(resetTokenDTO.getToken());
if (!validateToken(token, serviceUser.loadUserByUsername(extractEmail(token)))) {
return false;
}
return !resetTokenDTO.getExpiration().before(new Date());
}

private String createToken(Map<String, Object> claims, String username) {
return Jwts.builder()
private String createToken(Map<String, Object> claims, String username) throws Exception {
String token = Jwts.builder()
.claims(claims)
.subject(username)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignKey())
//.signWith(getSignKey(), SignatureAlgorithm.HS256)
.compact();
return encryptionService.encrypt(token);
}

private SecretKey getSignKey() {
Expand All @@ -175,5 +207,4 @@ private String extractUsername(String token) {
private String extractEmail(String token) {
return extractAllClaims(token).get("email", String.class);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public interface JwtServiceI {

UserDTO validateAuthHeader(String token) throws Exception;

String resetPassword(UserDTO reqUser) throws Exception;
String resetPassword(UserDTO reqUser, String email, String resetToken) throws Exception;

Boolean forgotPassword(String email) throws Exception;
}
Loading
Loading