2020use Endroid \QrCode \Writer \PngWriter ;
2121use OTPHP \TOTP ;
2222use Security ;
23+ use Symfony \Component \Form \FormError ;
2324use Symfony \Component \HttpFoundation \RedirectResponse ;
2425use Symfony \Component \HttpFoundation \Request ;
2526use Symfony \Component \HttpFoundation \Response ;
2627use Symfony \Component \PasswordHasher \Hasher \UserPasswordHasherInterface ;
2728use Symfony \Component \Routing \Attribute \Route ;
29+ use Symfony \Component \Security \Core \Authentication \Token \Storage \TokenStorageInterface ;
30+ use Symfony \Component \Security \Core \Authentication \Token \UsernamePasswordToken ;
2831use Symfony \Component \Security \Core \User \UserInterface ;
32+ use Symfony \Component \Security \Csrf \CsrfToken ;
2933use Symfony \Component \Security \Csrf \CsrfTokenManagerInterface ;
3034use Symfony \Contracts \Translation \TranslatorInterface ;
3135
@@ -43,8 +47,12 @@ public function __construct(
4347 ) {}
4448
4549 #[Route('/edit ' , name: 'chamilo_core_account_edit ' , methods: ['GET ' , 'POST ' ])]
46- public function edit (Request $ request , UserRepository $ userRepository , IllustrationRepository $ illustrationRepo , SettingsManager $ settingsManager ): Response
47- {
50+ public function edit (
51+ Request $ request ,
52+ UserRepository $ userRepository ,
53+ IllustrationRepository $ illustrationRepo ,
54+ SettingsManager $ settingsManager
55+ ): Response {
4856 $ user = $ this ->userHelper ->getCurrent ();
4957
5058 if (!\is_object ($ user ) || !$ user instanceof UserInterface) {
@@ -69,6 +77,7 @@ public function edit(Request $request, UserRepository $userRepository, Illustrat
6977 $ password = $ form ['password ' ]->getData ();
7078 if ($ password ) {
7179 $ user ->setPlainPassword ($ password );
80+ $ user ->setPasswordUpdateAt (new \DateTimeImmutable ());
7281 }
7382 }
7483
@@ -96,32 +105,54 @@ public function changePassword(
96105 UserRepository $ userRepository ,
97106 CsrfTokenManagerInterface $ csrfTokenManager ,
98107 SettingsManager $ settingsManager ,
99- UserPasswordHasherInterface $ passwordHasher
108+ UserPasswordHasherInterface $ passwordHasher ,
109+ TokenStorageInterface $ tokenStorage ,
100110 ): Response {
101- /** @var User $user */
111+ /** @var ? User $user */
102112 $ user = $ this ->getUser ();
103113
104- // Ensure user is authenticated and has proper interface
105- if (!\is_object ($ user ) || !$ user instanceof UserInterface) {
106- throw $ this ->createAccessDeniedException ('This user does not have access to this section ' );
114+ if (!$ user || !$ user instanceof UserInterface) {
115+ $ userId = $ request ->query ->get ('userId ' );
116+ //error_log("User not logged in. Received userId from query: " . $userId);
117+
118+ if (!$ userId || !ctype_digit ($ userId )) {
119+ //error_log("Access denied: Missing or invalid userId.");
120+ throw $ this ->createAccessDeniedException ('This user does not have access to this section. ' );
121+ }
122+
123+ $ user = $ userRepository ->find ((int )$ userId );
124+
125+ if (!$ user || !$ user instanceof UserInterface) {
126+ //error_log("Access denied: User not found with ID $userId");
127+ throw $ this ->createAccessDeniedException ('User not found or invalid. ' );
128+ }
129+
130+ //error_log("Loaded user by ID: " . $user->getId());
107131 }
108132
109- // Build the form and inject user-related options
133+ $ isRotation = $ request ->query ->getBoolean ('rotate ' , false );
134+
110135 $ form = $ this ->createForm (ChangePasswordType::class, [
111136 'enable2FA ' => $ user ->getMfaEnabled (),
112137 ], [
113138 'user ' => $ user ,
114139 'portal_name ' => $ settingsManager ->getSetting ('platform.institution ' ),
115140 'password_hasher ' => $ passwordHasher ,
141+ 'enable_2fa_field ' => !$ isRotation ,
116142 ]);
117-
118143 $ form ->handleRequest ($ request );
144+
119145 $ session = $ request ->getSession ();
120146 $ qrCodeBase64 = null ;
121147 $ showQRCode = false ;
122148
123- // Generate TOTP secret and QR code for 2FA activation
124- if ($ form ->get ('enable2FA ' )->getData () && !$ user ->getMfaSecret ()) {
149+ // Build QR code preview if user opts to enable 2FA but hasn't saved yet
150+ if (
151+ $ form ->isSubmitted ()
152+ && $ form ->has ('enable2FA ' )
153+ && $ form ->get ('enable2FA ' )->getData ()
154+ && !$ user ->getMfaSecret ()
155+ ) {
125156 if (!$ session ->has ('temporary_mfa_secret ' )) {
126157 $ totp = TOTP ::create ();
127158 $ secret = $ totp ->getSecret ();
@@ -134,7 +165,6 @@ public function changePassword(
134165 $ portalName = $ settingsManager ->getSetting ('platform.institution ' );
135166 $ totp ->setLabel ($ portalName . ' - ' . $ user ->getEmail ());
136167
137- // Build QR code image
138168 $ qrCodeResult = Builder::create ()
139169 ->writer (new PngWriter ())
140170 ->data ($ totp ->getProvisioningUri ())
@@ -148,47 +178,78 @@ public function changePassword(
148178 $ showQRCode = true ;
149179 }
150180
151- // Handle form submission
152- if ($ form ->isSubmitted () && $ form ->isValid ()) {
153- $ newPassword = $ form ->get ('newPassword ' )->getData ();
154- $ enable2FA = $ form ->get ('enable2FA ' )->getData ();
155-
156- // Enable 2FA and store encrypted secret
157- if ($ enable2FA && !$ user ->getMfaSecret () && $ session ->has ('temporary_mfa_secret ' )) {
158- $ secret = $ session ->get ('temporary_mfa_secret ' );
159- $ encryptedSecret = $ this ->encryptTOTPSecret ($ secret , $ _ENV ['APP_SECRET ' ]);
160-
161- $ user ->setMfaSecret ($ encryptedSecret );
162- $ user ->setMfaEnabled (true );
163- $ user ->setMfaService ('TOTP ' );
164-
165- $ userRepository ->updateUser ($ user );
166- $ session ->remove ('temporary_mfa_secret ' );
167-
168- $ this ->addFlash ('success ' , '2FA activated successfully. ' );
169- return $ this ->redirectToRoute ('chamilo_core_account_home ' );
170- }
171-
172- // Disable 2FA if it was previously enabled
173- if (!$ enable2FA && $ user ->getMfaEnabled ()) {
174- $ user ->setMfaEnabled (false );
175- $ user ->setMfaSecret (null );
176-
177- $ userRepository ->updateUser ($ user );
178- $ this ->addFlash ('success ' , '2FA disabled successfully. ' );
179- return $ this ->redirectToRoute ('chamilo_core_account_home ' );
180- }
181-
182- // Update password if provided
183- if (!empty ($ newPassword )) {
184- $ user ->setPlainPassword ($ newPassword );
185- $ userRepository ->updateUser ($ user );
186- $ this ->addFlash ('success ' , 'Password updated successfully. ' );
187- return $ this ->redirectToRoute ('chamilo_core_account_home ' );
181+ if ($ form ->isSubmitted ()) {
182+ if ($ form ->isValid ()) {
183+ $ submittedToken = $ request ->request ->get ('_token ' );
184+ if (!$ csrfTokenManager ->isTokenValid (new CsrfToken ('change_password ' , $ submittedToken ))) {
185+ $ form ->addError (new FormError ($ this ->translator ->trans ('CSRF token is invalid. Please try again. ' )));
186+ } else {
187+ $ currentPassword = $ form ->get ('currentPassword ' )->getData ();
188+ $ newPassword = $ form ->get ('newPassword ' )->getData ();
189+ $ confirmPassword = $ form ->get ('confirmPassword ' )->getData ();
190+ $ enable2FA = !$ isRotation && $ form ->has ('enable2FA ' )
191+ ? $ form ->get ('enable2FA ' )->getData ()
192+ : false ;
193+
194+ if ($ enable2FA && !$ user ->getMfaSecret ()) {
195+ $ secret = $ session ->get ('temporary_mfa_secret ' );
196+ $ encryptedSecret = $ this ->encryptTOTPSecret ($ secret , $ _ENV ['APP_SECRET ' ]);
197+ $ user ->setMfaSecret ($ encryptedSecret );
198+ $ user ->setMfaEnabled (true );
199+ $ user ->setMfaService ('TOTP ' );
200+ $ userRepository ->updateUser ($ user );
201+
202+ $ session ->remove ('temporary_mfa_secret ' );
203+
204+ $ this ->addFlash ('success ' , '2FA activated successfully. ' );
205+
206+ return $ this ->redirectToRoute ('chamilo_core_account_home ' );
207+ }
208+
209+ if (!$ isRotation && !$ enable2FA && $ user ->getMfaEnabled ()) {
210+ $ user ->setMfaEnabled (false );
211+ $ user ->setMfaSecret (null );
212+ $ userRepository ->updateUser ($ user );
213+ $ this ->addFlash ('success ' , '2FA disabled successfully. ' );
214+ }
215+
216+ if ($ newPassword || $ confirmPassword || $ currentPassword ) {
217+ if (!$ userRepository ->isPasswordValid ($ user , $ currentPassword )) {
218+ $ form ->get ('currentPassword ' )->addError (new FormError (
219+ $ this ->translator ->trans ('The current password is incorrect ' )
220+ ));
221+ } elseif ($ newPassword !== $ confirmPassword ) {
222+ $ form ->get ('confirmPassword ' )->addError (new FormError (
223+ $ this ->translator ->trans ('Passwords do not match ' )
224+ ));
225+ } else {
226+ $ user ->setPlainPassword ($ newPassword );
227+ $ user ->setPasswordUpdateAt (new \DateTimeImmutable ());
228+ $ userRepository ->updateUser ($ user );
229+ $ this ->addFlash ('success ' , 'Password updated successfully. ' );
230+
231+ // Re-login if the user was not logged
232+ if (!$ this ->getUser ()) {
233+ $ token = new UsernamePasswordToken (
234+ $ user ,
235+ 'main ' ,
236+ $ user ->getRoles ()
237+ );
238+ $ tokenStorage ->setToken ($ token );
239+ $ request ->getSession ()->set ('_security_main ' , serialize ($ token ));
240+ }
241+
242+ return $ this ->redirectToRoute ('chamilo_core_account_home ' );
243+ }
244+ }
245+ }
246+ } else {
247+ error_log ("Form is NOT valid. " );
188248 }
249+ } else {
250+ error_log ("Form NOT submitted yet. " );
189251 }
190252
191- // Render form with optional QR code for 2FA
192253 return $ this ->render ('@ChamiloCore/Account/change_password.html.twig ' , [
193254 'form ' => $ form ->createView (),
194255 'qrCode ' => $ qrCodeBase64 ,
@@ -206,18 +267,7 @@ private function encryptTOTPSecret(string $secret, string $encryptionKey): strin
206267 $ iv = openssl_random_pseudo_bytes (openssl_cipher_iv_length ($ cipherMethod ));
207268 $ encryptedSecret = openssl_encrypt ($ secret , $ cipherMethod , $ encryptionKey , 0 , $ iv );
208269
209- return base64_encode ($ iv .':: ' .$ encryptedSecret );
210- }
211-
212- /**
213- * Validates the provided TOTP code for the given user.
214- */
215- private function isTOTPValid (User $ user , string $ totpCode ): bool
216- {
217- $ decryptedSecret = $ this ->decryptTOTPSecret ($ user ->getMfaSecret (), $ _ENV ['APP_SECRET ' ]);
218- $ totp = TOTP ::create ($ decryptedSecret );
219-
220- return $ totp ->verify ($ totpCode );
270+ return base64_encode ($ iv . ':: ' . $ encryptedSecret );
221271 }
222272
223273 /**
0 commit comments