Skip to content

Commit 22aa446

Browse files
Merge pull request #39 from SmartPotTech/feature/HU-03-change-password
Feature/hu 03 change password
2 parents 27708ac + d5f6c10 commit 22aa446

12 files changed

Lines changed: 282 additions & 49 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ MAIL_PROPERTIES_SMTP_STARTTLS_ENABLE=<TRUE/FALSE> # Enable STARTTLS for secure
4949
SECURITY_JWT_SECRET_KEY=<JWT_SECRET_KEY> # Secret key for signing JWT tokens
5050
SECURITY_JWT_EXPIRATION=<JWT_EXPIRATION> # JWT expiration time (in ms)
5151
SECURITY_PUBLIC_ROUTES=<PUBLIC_ROUTES> # Public routes that do not require authentication (e.g., /auth/login)
52+
SECURITY_AES_KEY=<AES_KEY> # AES encryption key for sensitive data
5253

5354
# Rate Limiting Config
5455
# Settings for API rate limiting
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package smartpot.com.api.Exception;
2+
3+
public class EncryptionException extends RuntimeException {
4+
public EncryptionException(String message) {
5+
super(message);
6+
}
7+
}

src/main/java/smartpot/com/api/Security/Config/Filters/JwtAuthFilter.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,17 @@
1212
import org.springframework.web.filter.OncePerRequestFilter;
1313
import smartpot.com.api.Security.Service.JwtService;
1414
import smartpot.com.api.Users.Model.DTO.UserDTO;
15-
import smartpot.com.api.Users.Service.SUser;
1615

1716
import java.io.IOException;
1817

1918
@Component
2019
public class JwtAuthFilter extends OncePerRequestFilter {
2120

2221
// TODO: implement role for jwt
23-
2422
private final JwtService jwtService;
25-
private final SUser serviceUser;
2623

27-
public JwtAuthFilter(JwtService jwtService, SUser serviceUser) {
24+
public JwtAuthFilter(JwtService jwtService) {
2825
this.jwtService = jwtService;
29-
this.serviceUser = serviceUser;
3026
}
3127

3228
@Override
@@ -35,16 +31,14 @@ protected void doFilterInternal(
3531
@NonNull HttpServletResponse response,
3632
@NonNull FilterChain filterChain
3733
) throws ServletException, IOException {
38-
3934
String authHeader = request.getHeader("Authorization");
4035
try {
4136
UserDTO user = jwtService.validateAuthHeader(authHeader);
4237
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
4338
user, user.getPassword(), null /* user.getAuthorities() */);
4439
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
4540
SecurityContextHolder.getContext().setAuthentication(authToken);
46-
} catch (Exception ignored) {
47-
}
41+
} catch (Exception ignored) {}
4842
filterChain.doFilter(request, response);
4943
}
5044
}

src/main/java/smartpot/com/api/Security/Controller/AuthController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ public ResponseEntity<?> forgotPassword(@RequestBody UserDTO reqUser) {
123123
)
124124
}
125125
)
126-
public ResponseEntity<?> resetPassword(@RequestBody UserDTO reqUser, @RequestHeader("Authorization") String resetToken) {
126+
public ResponseEntity<?> resetPassword(@RequestBody UserDTO reqUser, @RequestHeader("Authorization") String token, @RequestHeader("Reset-Token") String resetToken) {
127127
try {
128-
return new ResponseEntity<>(new TokenResponse(jwtService.resetPassword(reqUser)), HttpStatus.OK);
128+
return new ResponseEntity<>(new TokenResponse(jwtService.resetPassword(reqUser, jwtService.validateAuthHeader(token).getEmail(), resetToken)), HttpStatus.OK);
129129
} catch (Exception e) {
130130
return new ResponseEntity<>(new ErrorResponse("Error al restablecer contraseña [" + e.getMessage() + "]", HttpStatus.BAD_REQUEST.value()), HttpStatus.BAD_REQUEST);
131131
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package smartpot.com.api.Security.Model.DTO;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.util.Date;
9+
10+
@Data
11+
@NoArgsConstructor
12+
public class ResetTokenDTO {
13+
private String token;
14+
private String operation;
15+
private Date expiration;
16+
17+
public ResetTokenDTO(String token, String operation, Date expiration) {
18+
this.token = token;
19+
this.operation = operation;
20+
this.expiration = expiration;
21+
}
22+
23+
static public String convertToJson(ResetTokenDTO resetToken) throws JsonProcessingException {
24+
ObjectMapper objectMapper = new ObjectMapper();
25+
return objectMapper.writeValueAsString(resetToken);
26+
}
27+
28+
public static ResetTokenDTO convertToDTO(String json) throws JsonProcessingException {
29+
ObjectMapper objectMapper = new ObjectMapper();
30+
return objectMapper.readValue(json, ResetTokenDTO.class);
31+
}
32+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package smartpot.com.api.Security.Service;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.stereotype.Service;
5+
import smartpot.com.api.Exception.EncryptionException;
6+
7+
import javax.crypto.*;
8+
import javax.crypto.spec.GCMParameterSpec;
9+
import javax.crypto.spec.SecretKeySpec;
10+
import java.security.InvalidAlgorithmParameterException;
11+
import java.security.InvalidKeyException;
12+
import java.security.NoSuchAlgorithmException;
13+
import java.security.SecureRandom;
14+
import java.util.Base64;
15+
16+
@Service
17+
public class AESEncryptionService implements EncryptionServiceI {
18+
19+
SecureRandom random = new SecureRandom();
20+
@Value("${application.security.aes.key}")
21+
private String aesKey;
22+
23+
public AESEncryptionService() {}
24+
25+
private SecretKey getSecretKey() {
26+
byte[] decoded = Base64.getDecoder().decode(aesKey);
27+
if (decoded.length != 32) {
28+
throw new IllegalArgumentException("La clave debe tener 256 bits (32 bytes)");
29+
}
30+
return new SecretKeySpec(decoded, "AES");
31+
}
32+
33+
@Override
34+
public String encrypt(String data) throws EncryptionException {
35+
try {
36+
// get salt
37+
byte[] salt = new byte[8];
38+
random.nextBytes(salt);
39+
String saltedData = Base64.getEncoder().encodeToString(salt) + ":" + data;
40+
41+
byte[] iv = new byte[12];
42+
random.nextBytes(iv);
43+
SecretKey key = getSecretKey(); // get encryption key
44+
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
45+
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
46+
47+
byte[] encrypted = cipher.doFinal(saltedData.getBytes());
48+
49+
byte[] output = new byte[iv.length + encrypted.length];
50+
System.arraycopy(iv, 0, output, 0, iv.length);
51+
System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
52+
return Base64.getUrlEncoder().encodeToString(output);
53+
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | // if you need more specific errors, catch each one separately
54+
InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
55+
throw new EncryptionException("Error while encrypting data");
56+
}
57+
58+
}
59+
60+
@Override
61+
public String decrypt(String encryptedData) throws EncryptionException {
62+
try {
63+
byte[] decoded = Base64.getUrlDecoder().decode(encryptedData);
64+
65+
byte[] iv = new byte[12];
66+
byte[] cipherText = new byte[decoded.length - 12];
67+
System.arraycopy(decoded, 0, iv, 0, 12);
68+
System.arraycopy(decoded, 12, cipherText, 0, cipherText.length);
69+
70+
SecretKey key = getSecretKey();
71+
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
72+
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
73+
String result = new String(cipher.doFinal(cipherText));
74+
75+
// remove salt
76+
return result.split(":", 2)[1];
77+
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | // if you need more specific errors, catch each one separately
78+
InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
79+
throw new EncryptionException("Error while decrypting data");
80+
}
81+
82+
}
83+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package smartpot.com.api.Security.Service;
2+
3+
import smartpot.com.api.Exception.EncryptionException;
4+
5+
import javax.crypto.BadPaddingException;
6+
import javax.crypto.IllegalBlockSizeException;
7+
import javax.crypto.NoSuchPaddingException;
8+
import java.security.InvalidAlgorithmParameterException;
9+
import java.security.InvalidKeyException;
10+
import java.security.NoSuchAlgorithmException;
11+
12+
public interface EncryptionServiceI {
13+
String encrypt(String plainText) throws EncryptionException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException;
14+
String decrypt(String cipherText) throws EncryptionException;
15+
}

src/main/java/smartpot/com/api/Security/Service/JwtService.java

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,24 @@
1010
import org.springframework.security.core.userdetails.UserDetails;
1111
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
1212
import org.springframework.stereotype.Service;
13+
import smartpot.com.api.Exception.EncryptionException;
1314
import smartpot.com.api.Exception.InvalidTokenException;
1415
import smartpot.com.api.Mail.Model.DTO.EmailDTO;
1516
import smartpot.com.api.Mail.Service.EmailService;
1617
import smartpot.com.api.Mail.Validator.EmailValidatorI;
18+
import smartpot.com.api.Security.Model.DTO.ResetTokenDTO;
1719
import smartpot.com.api.Users.Model.DTO.UserDTO;
1820
import smartpot.com.api.Users.Service.SUserI;
19-
2021
import javax.crypto.SecretKey;
21-
import java.util.Date;
22-
import java.util.HashMap;
23-
import java.util.Map;
24-
import java.util.Optional;
22+
import java.util.*;
2523

2624
@Service
2725
public class JwtService implements JwtServiceI {
2826

2927
private final SUserI serviceUser;
3028
private final EmailService emailService;
3129
private final EmailValidatorI emailValidator;
30+
private final EncryptionServiceI encryptionService;
3231

3332
@Value("${application.security.jwt.secret-key}")
3433
private String secretKey;
@@ -41,29 +40,38 @@ public class JwtService implements JwtServiceI {
4140
* @param serviceUser servicio que maneja las operaciones de base de datos.
4241
*/
4342
@Autowired
44-
public JwtService(SUserI serviceUser, EmailService emailService, EmailValidatorI emailValidator) {
43+
public JwtService(SUserI serviceUser, EmailService emailService, EmailValidatorI emailValidator, EncryptionServiceI encryptionService) {
4544
this.serviceUser = serviceUser;
4645
this.emailService = emailService;
4746
this.emailValidator = emailValidator;
47+
this.encryptionService = encryptionService;
4848
}
4949

5050
@Override
5151
public String Login(UserDTO reqUser) throws Exception {
5252
return Optional.of(serviceUser.getUserByEmail(reqUser.getEmail()))
5353
.filter(userDTO -> new BCryptPasswordEncoder().matches(reqUser.getPassword(), userDTO.getPassword()))
54-
.map(validUser -> generateToken(validUser.getId(), validUser.getEmail()))
54+
.map(validUser -> {
55+
try {
56+
return generateToken(validUser.getId(), validUser.getEmail());
57+
} catch (Exception e) {
58+
throw new ValidationException(e);
59+
}
60+
})
5561
.orElseThrow(() -> new Exception("Credenciales Invalidas"));
56-
5762
}
5863

64+
/**
65+
* @param reqUser
66+
* @return
67+
* @throws Exception
68+
*/
5969
@Override
6070
public String Register(UserDTO reqUser) throws Exception {
61-
return Optional.ofNullable(serviceUser.CreateUser(reqUser))
62-
.map(user -> generateToken(reqUser.getId(), user.getEmail()))
63-
.orElseThrow(() -> new Exception("User already registered."));
71+
return "";
6472
}
6573

66-
private String generateToken(String id, String email) {
74+
private String generateToken(String id, String email) throws Exception {
6775
// TODO: Refine token (email != subject)
6876
Map<String, Object> claims = new HashMap<>();
6977
claims.put("id", id);
@@ -75,11 +83,13 @@ private String generateToken(String id, String email) {
7583

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

8290
String token = authHeader.split(" ")[1];
91+
token = encryptionService.decrypt(token);
92+
8393
String email = extractEmail(token);
8494
UserDetails user = serviceUser.loadUserByUsername(email);
8595
if (email == null) {
@@ -93,27 +103,53 @@ public UserDTO validateAuthHeader(String authHeader) throws Exception {
93103
UserDTO finalUser = serviceUser.getUserByEmail(email);
94104
finalUser.setPassword("");
95105
return finalUser;
96-
97106
}
98107

99108
@Override
100-
public String resetPassword(UserDTO reqUser) throws Exception {
101-
return Optional.of(serviceUser.getUserByEmail(reqUser.getEmail()))
109+
public String resetPassword(UserDTO user, String email, String resetToken) throws Exception {
110+
return Optional.of(serviceUser.getUserByEmail(email))
102111
.map(validUser -> {
103112
try {
104-
return serviceUser.UpdateUser(validUser.getId(), validUser);
113+
String decrypted = encryptionService.decrypt(resetToken);
114+
ResetTokenDTO resetTokenDTO = ResetTokenDTO.convertToDTO(decrypted);
115+
116+
if (!validateResetToken(resetTokenDTO)) {
117+
throw new ValidationException("Provided reset token is not valid");
118+
}
119+
120+
return serviceUser.UpdateUserPassword(validUser, user.getPassword());
121+
} catch (Exception e) {
122+
throw new ValidationException(e);
123+
}
124+
})
125+
.map(validUser -> {
126+
try {
127+
return generateToken(validUser.getId(), validUser.getEmail());
105128
} catch (Exception e) {
106129
throw new ValidationException(e);
107130
}
108131
})
109-
.map(validUser -> generateToken(validUser.getId(), validUser.getEmail()))
110132
.orElseThrow(() -> new Exception("Credenciales Invalidas"));
111133
}
112134

113135
@Override
114136
public Boolean forgotPassword(String email) throws Exception {
115137
return Optional.of(serviceUser.getUserByEmail(email))
116-
.map(validUser -> generateToken(validUser.getId(), validUser.getEmail()))
138+
.map(validUser -> {
139+
try {
140+
return generateToken(validUser.getId(), validUser.getEmail());
141+
} catch (Exception e) {
142+
throw new ValidationException(e);
143+
}
144+
})
145+
.map(token -> new ResetTokenDTO(token, "reset", new Date(System.currentTimeMillis() + expiration) ))
146+
.map(token -> {
147+
try {
148+
return encryptionService.encrypt(ResetTokenDTO.convertToJson(token));
149+
} catch (Exception e) {
150+
throw new EncryptionException("Conversion to json or encryption failed: " + e);
151+
}
152+
})
117153
.map(token -> new EmailDTO(null, email, "Token para recuperar contraseña: " + token, "Recuperar contraseña", "", null, "true"))
118154
.map(emailService::sendSimpleMail)
119155
.map(ValidDTO -> {
@@ -136,19 +172,26 @@ private Boolean validateToken(String token, UserDetails userDetails) {
136172
}
137173
String username = extractUsername(token);
138174
return userDetails.getUsername().equals(username) && !expirationDate.before(new Date());
175+
}
139176

140-
177+
private Boolean validateResetToken(ResetTokenDTO resetTokenDTO) {
178+
String token = encryptionService.decrypt(resetTokenDTO.getToken());
179+
if (!validateToken(token, serviceUser.loadUserByUsername(extractEmail(token)))) {
180+
return false;
181+
}
182+
return !resetTokenDTO.getExpiration().before(new Date());
141183
}
142184

143-
private String createToken(Map<String, Object> claims, String username) {
144-
return Jwts.builder()
185+
private String createToken(Map<String, Object> claims, String username) throws Exception {
186+
String token = Jwts.builder()
145187
.claims(claims)
146188
.subject(username)
147189
.issuedAt(new Date(System.currentTimeMillis()))
148190
.expiration(new Date(System.currentTimeMillis() + expiration))
149191
.signWith(getSignKey())
150192
//.signWith(getSignKey(), SignatureAlgorithm.HS256)
151193
.compact();
194+
return encryptionService.encrypt(token);
152195
}
153196

154197
private SecretKey getSignKey() {
@@ -175,5 +218,4 @@ private String extractUsername(String token) {
175218
private String extractEmail(String token) {
176219
return extractAllClaims(token).get("email", String.class);
177220
}
178-
179221
}

0 commit comments

Comments
 (0)