@@ -92,7 +92,7 @@ public function pageRegister($can_register = false){
9292 }
9393
9494 /**
95- * Render the "lost password" page.
95+ * Render the "forgot password" page.
9696 *
9797 * This creates a simple form to allow users who forgot their password to have a time-limited password reset link emailed to them.
9898 * By default, this is a "public page" (does not require authentication).
@@ -107,24 +107,33 @@ public function pageForgotPassword(){
107107 'validators ' => $ this ->_app ->jsValidator ->rules ()
108108 ]);
109109 }
110-
110+
111111 /**
112- * Render the "reset password" page.
112+ * Render the "set password" page.
113113 *
114- * This is the actual page that is linked to in the "forgot password" email.
115- * By default, this is a "public page" (does not require authentication).
114+ * If $flag_new_user is set to true, this renders the page where new users who have had accounts created
115+ * for them by another user, can set their password. If set to false, this renders the new password page for password reset requests.
116+ * By default, this is a "public page" (does not require authentication).
116117 * Request type: GET
117- */
118- public function pageResetPassword (){
118+ * @param bool $flag_new_user Set to true if this is for a new user who doesn't yet have a password.
119+ */
120+ public function pageSetPassword ($ flag_new_user = false ){
121+ // Look up the user for the secret token
122+ $ token = $ this ->_app ->request ->get ()['secret_token ' ];
119123
120- $ schema = new \Fortress \RequestSchema ($ this ->_app ->config ('schema.path ' ) . "/forms/reset-password.json " );
121- $ this ->_app ->jsValidator ->setSchema ($ schema );
124+ $ schema = new \Fortress \RequestSchema ($ this ->_app ->config ('schema.path ' ) . "/forms/set-password.json " );
125+ $ this ->_app ->jsValidator ->setSchema ($ schema );
126+
127+ if ($ flag_new_user )
128+ $ template = 'account/create-password.twig ' ;
129+ else
130+ $ template = 'account/reset-password.twig ' ;
122131
123- $ this ->_app ->render (' account/reset-password.twig ' , [
124- 'secret_token ' => $ this -> _app -> request -> get ()[ ' secret_token ' ] ,
132+ $ this ->_app ->render ($ template , [
133+ 'secret_token ' => $ token ,
125134 'validators ' => $ this ->_app ->jsValidator ->rules ()
126135 ]);
127- }
136+ }
128137
129138 /**
130139 * Render the "resend account activation link" page.
@@ -241,6 +250,12 @@ public function login(){
241250 }
242251 }
243252
253+ // Check that the user has a password set (so, rule out newly created accounts without a password)
254+ if (!$ user ->password ) {
255+ $ ms ->addMessageTranslated ("danger " , "ACCOUNT_USER_OR_PASS_INVALID " );
256+ $ this ->_app ->halt (403 );
257+ }
258+
244259 // Check that the user's account is enabled
245260 if ($ user ->flag_enabled == 0 ){
246261 $ ms ->addMessageTranslated ("danger " , "ACCOUNT_DISABLED " );
@@ -293,21 +308,7 @@ public function login(){
293308 * Request type: POST
294309 */
295310 public function logout ($ complete = false ){
296- if ($ complete ){
297- $ storage = new \Birke \Rememberme \Storage \PDO ($ this ->_app ->remember_me_table );
298- $ storage ->setConnection (\Illuminate \Database \Capsule \Manager::connection ()->getPdo ());
299- $ storage ->cleanAllTriplets ($ this ->_app ->user ->id );
300- }
301- // Change cookie path
302- $ cookie = $ this ->_app ->remember_me ->getCookie ();
303- $ cookie ->setPath ("/ " );
304- $ this ->_app ->remember_me ->setCookie ($ cookie );
305-
306- if ($ this ->_app ->remember_me ->clearCookie ())
307- error_log ("Cleared cookie " );
308-
309- session_regenerate_id (true );
310- session_destroy ();
311+ $ this ->_app ->user ->logout ($ complete );
311312 $ this ->_app ->redirect ($ this ->_app ->site ->uri ['public ' ]);
312313 }
313314
@@ -435,12 +436,18 @@ public function register(){
435436 foreach ($ defaultGroups as $ group_id => $ group )
436437 $ user ->addGroup ($ group_id );
437438
439+ // Store new user to database
440+ $ user ->save ();
441+
438442 // Create sign-up event
439443 $ sign_up_event = $ user ->newEventSignUp ();
444+ $ sign_up_event ->save ();
440445
441446 if ($ this ->_app ->site ->require_activation ) {
442- // Create verification request
447+ // Create verification request event
443448 $ verification_request = $ user ->newEventVerificationRequest ();
449+ $ user ->save (); // Re-save is needed to update user's secret token
450+ $ verification_request ->save ();
444451
445452 // Create and send verification email
446453 $ twig = $ this ->_app ->view ()->getEnvironment ();
@@ -460,15 +467,10 @@ public function register(){
460467 }
461468
462469 $ ms ->addMessageTranslated ("success " , "ACCOUNT_REGISTRATION_COMPLETE_TYPE2 " );
463- $ verification_request ->save ();
464470 } else
465471 // No activation required
466472 $ ms ->addMessageTranslated ("success " , "ACCOUNT_REGISTRATION_COMPLETE_TYPE1 " );
467-
468- $ sign_up_event ->save ();
469- // Store new user to database
470- $ user ->store ();
471- }
473+ }
472474
473475 /**
474476 * Processes an new account activation request.
@@ -516,12 +518,14 @@ public function activate(){
516518 * Processes a request to email a forgotten password reset link to the user.
517519 *
518520 * Processes the request from the form on the "forgot password" page, checking that:
519- * 1. The provided username exists ;
520- * 2. The provided email address matches the username;
521- * 3. The user doesn't already have an outstanding password reset request;
522- * 4. The submitted data is valid .
521+ * 1. The provided email address belongs to a registered account ;
522+ * 2. The submitted data is valid.
523+ * Note that we have removed the requirement that a password reset request not already be in progress.
524+ * This is because we need to allow users to re-request a reset, even if they lose the first reset email .
523525 * This route is "public access".
524- * Request type: POST
526+ * Request type: POST
527+ * @todo rate-limit forgotten password requests, to prevent password-reset spamming
528+ * @todo require additional user information
525529 */
526530 public function forgotPassword (){
527531 $ data = $ this ->_app ->request ->post ();
@@ -540,26 +544,17 @@ public function forgotPassword(){
540544 $ this ->_app ->halt (400 );
541545 }
542546
543- // Check that the username exists
544- if (!UserLoader::exists ($ data ['user_name ' ], 'user_name ' )) {
545- $ ms ->addMessageTranslated ("danger " , "ACCOUNT_INVALID_USERNAME " );
546- $ this ->_app ->halt (400 );
547- }
547+ // Load the user, by the specified email address
548+ $ user = User::where ('email ' , $ data ['email ' ])->first ();
548549
549- // Load the user, by username
550- $ user = UserLoader::fetch ($ data ['user_name ' ], 'user_name ' );
551-
552- // Check that the specified email is correct
553- if (strtolower ($ user ->email ) != strtolower ($ data ['email ' ])){
554- $ ms ->addMessageTranslated ("danger " , "ACCOUNT_USER_OR_EMAIL_INVALID " );
555- $ this ->_app ->halt (400 );
550+ // Check that the email exists.
551+ // On failure, we should still pretend like we succeeded, to prevent account enumeration
552+ if (!$ user ) {
553+ $ ms ->addMessageTranslated ("success " , "FORGOTPASS_REQUEST_SUCCESS " );
554+ $ this ->_app ->halt (200 );
556555 }
557556
558- // Check if the user has any outstanding lost password requests
559- if ($ user ->flag_password_reset == 1 ) {
560- $ ms ->addMessageTranslated ("danger " , "FORGOTPASS_REQUEST_EXISTS " );
561- $ this ->_app ->halt (403 );
562- }
557+ // TODO: rate-limit the number of password reset requests for a given user
563558
564559 // Generate a new password reset request. This will also generate a new secret token for the user.
565560 $ event = $ user ->newEventPasswordReset ();
@@ -588,22 +583,21 @@ public function forgotPassword(){
588583 }
589584
590585 /**
591- * Processes a request to reset a user's password.
586+ * Processes a request to reset a user's password, or set the password for a new user .
592587 *
593- * Processes the request from the password reset form, which should have the reset token embedded in it, checking that:
594- * 1. The provided activation token is associated with an existing user account;
595- * 2. The provided username matches the activation token;
596- * 3. The user has a lost password request in progress;
597- * 4. The token has not expired;
598- * 5. The submitted data (new password) is valid.
588+ * Processes the request from the password create/reset form, which should have the secret token embedded in it, checking that:
589+ * 1. The provided secret token is associated with an existing user account;
590+ * 2. The user has a lost password request in progress;
591+ * 3. The token has not expired;
592+ * 4. The submitted data (new password) is valid.
599593 * This route is "public access".
600594 * Request type: POST
601595 */
602- public function resetPassword ( ){
596+ public function setPassword ( $ flag_new_user = false ){
603597 $ data = $ this ->_app ->request ->post ();
604598
605599 // Load the request schema
606- $ requestSchema = new \Fortress \RequestSchema ($ this ->_app ->config ('schema.path ' ) . "/forms/reset -password.json " );
600+ $ requestSchema = new \Fortress \RequestSchema ($ this ->_app ->config ('schema.path ' ) . "/forms/set -password.json " );
607601
608602 // Get the alert message stream
609603 $ ms = $ this ->_app ->alerts ;
@@ -616,20 +610,15 @@ public function resetPassword(){
616610 $ this ->_app ->halt (400 );
617611 }
618612
619- // Fetch the user, by looking up the submitted activation token
620- $ user = UserLoader:: fetch ( $ data [ 'secret_token ' ], 'secret_token ' );
613+ // Fetch the user, by looking up the submitted secret token
614+ $ user = User:: where ( 'secret_token ' , $ data [ 'secret_token ' ])-> first ( );
621615
616+ // If no user exists for this token, just say the token is invalid.
622617 if (!$ user ){
623618 $ ms ->addMessageTranslated ("danger " , "FORGOTPASS_INVALID_TOKEN " );
624619 $ this ->_app ->halt (400 );
625620 }
626621
627- // Check that the username matches the submitted username
628- if (strtolower ($ user ->user_name ) != strtolower ($ data ['user_name ' ])){
629- $ ms ->addMessageTranslated ("danger " , "ACCOUNT_INVALID_USERNAME " );
630- $ this ->_app ->halt (400 );
631- }
632-
633622 // Get the most recent password reset request time
634623 $ last_password_reset_time = $ user ->lastEventTime ('password_reset_request ' );
635624
@@ -639,14 +628,19 @@ public function resetPassword(){
639628 $ this ->_app ->halt (400 );
640629 }
641630
642- // Check the time to see if the token is still valid based on the timeout value. If not valid make the user restart the password request
631+ // Check the time to see if the token is still valid based on the timeout value. If not valid, make the user restart the password request
643632 $ current_time = new \DateTime ("now " );
644633 $ last_password_reset_datetime = new \DateTime ($ last_password_reset_time );
645634 $ current_token_life = $ current_time ->getTimestamp () - $ last_password_reset_datetime ->getTimestamp ();
646635
647- if ($ current_token_life >= $ this ->_app ->site ->reset_password_timeout || $ current_token_life < 0 ){
648- // Reset the password reset flag
649- // TODO: should we do this here, or just when there is a new reset request?
636+ // Compare to appropriate expiration time
637+ if ($ flag_new_user )
638+ $ expiration = $ this ->_app ->site ->create_password_expiration ;
639+ else
640+ $ expiration = $ this ->_app ->site ->reset_password_timeout ;
641+
642+ if ($ current_token_life >= $ expiration || $ current_token_life < 0 ){
643+ // Reset the password reset flag so that they'll be able to submit another request
650644 $ user ->flag_password_reset = "0 " ;
651645 $ user ->store ();
652646 $ ms ->addMessageTranslated ("danger " , "FORGOTPASS_OLD_TOKEN " );
@@ -656,7 +650,7 @@ public function resetPassword(){
656650 // Reset the password flag
657651 $ user ->flag_password_reset = "0 " ;
658652
659- // Hash the user's password and update
653+ // Hash the user's new password and update
660654 $ user ->password = Authentication::hashPassword ($ data ['password ' ]);
661655
662656 if (!$ user ->password ){
@@ -665,16 +659,41 @@ public function resetPassword(){
665659 }
666660
667661 // Store the updated info
668- $ user ->store ();
669- $ ms ->addMessageTranslated ("success " , "ACCOUNT_PASSWORD_UPDATED " );
662+ $ user ->store ();
663+
664+ // Log out any existing user, and create a new session
665+ if (!$ this ->_app ->user ->isGuest ()) {
666+ $ this ->_app ->user ->logout (true );
667+ // Use native PHP sessions
668+ session_cache_limiter (false );
669+ session_name ("UserFrosting " );
670+ // First, initialize the PHP session
671+ session_start ();
672+ }
673+
674+ // Log in the user
675+ $ user ->login ();
676+ session_regenerate_id ();
677+
678+ // Assume identity
679+ $ this ->_app ->user = $ user ;
680+
681+ // Store user id in session
682+ $ _SESSION ["userfrosting " ]["user_id " ] = $ user ->id ;
683+
684+ // Setup logged in user environment
685+ $ this ->_app ->setupAuthenticatedEnvironment ();
686+ $ ms = $ this ->_app ->alerts ;
687+ $ ms ->addMessageTranslated ("success " , "ACCOUNT_WELCOME " , $ this ->_app ->user ->export ());
688+ $ ms ->addMessageTranslated ("success " , "ACCOUNT_PASSWORD_UPDATED " );
670689 }
671690
672691 /**
673692 * Processes a request to cancel a password reset request.
674693 *
675694 * This is provided so that users can cancel a password reset request, if they made it in error or if it was not initiated by themselves.
676695 * Processes the request from the password reset link, checking that:
677- * 1. The provided activation token is associated with an existing user account.
696+ * 1. The provided secret token is associated with an existing user account.
678697 * Request type: GET
679698 */
680699 public function denyResetPassword (){
0 commit comments