From 631d8cb0e60711b35390f4d140af626ff6d3d9c2 Mon Sep 17 00:00:00 2001 From: Drake Date: Wed, 6 Dec 2006 11:57:36 +0000 Subject: [PATCH] Switch to profile-based password recovery. --- docs/changelog/7.x.x.txt | 2 + docs/gotcha.txt | 10 + .../default-password-recovery-template.tmpl | 47 +++++ docs/upgrades/upgrade_7.2.3-7.3.0.pl | 15 ++ lib/WebGUI/Auth/WebGUI.pm | 185 +++++++++++++----- lib/WebGUI/Operation/ProfileSettings.pm | 7 + lib/WebGUI/ProfileField.pm | 13 ++ lib/WebGUI/i18n/English/AuthWebGUI.pm | 15 ++ lib/WebGUI/i18n/English/WebGUIProfile.pm | 10 + 9 files changed, 250 insertions(+), 54 deletions(-) create mode 100644 docs/upgrades/templates-7.3.0/default-password-recovery-template.tmpl diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index 57483294b..ebeae6412 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -42,6 +42,8 @@ - fix: IP addresses for adminModeSubnets not using X-Forwarded-For properly - The Events Calendar is now the new Calendar with some fun new features. All your existing Events Calendars will be migrated automatically. + - Major change: password recovery is now based on profile fields rather than + email account access *** PLEASE READ THE GOTCHAS *** 7.2.3 diff --git a/docs/gotcha.txt b/docs/gotcha.txt index 054af3a64..21ceb2e75 100644 --- a/docs/gotcha.txt +++ b/docs/gotcha.txt @@ -14,6 +14,16 @@ save you many hours of grief. running the entire test suite prior to SVN commits easier to do since it won't take so long. + * Password recovery has been redone. It is now based on profile fields + rather than email access. Since there's no real way to migrate the + latter to one to the other, this upgrade disables password recovery; + before enabling it again, use the profile fields editor to set certain + fields as required for password recovery. Then any user who enters all + of those fields correctly can recover their password. The template + variables are also different, so if you have a custom password recovery + template, you will have to update it. See the new default password + recovery template for an example of how to use the new variables. + 7.2.0 -------------------------------------------------------------------- * NOTE: if you tried to upgrade to 7.2.0 and it failed during the diff --git a/docs/upgrades/templates-7.3.0/default-password-recovery-template.tmpl b/docs/upgrades/templates-7.3.0/default-password-recovery-template.tmpl new file mode 100644 index 000000000..8270df0bb --- /dev/null +++ b/docs/upgrades/templates-7.3.0/default-password-recovery-template.tmpl @@ -0,0 +1,47 @@ +#PBtmpl0000000000000014 +#namespace:Auth/WebGUI/Recovery2 +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
    + +
  • +
    +
  • +
+
diff --git a/docs/upgrades/upgrade_7.2.3-7.3.0.pl b/docs/upgrades/upgrade_7.2.3-7.3.0.pl index 258dd95c9..3fef66349 100644 --- a/docs/upgrades/upgrade_7.2.3-7.3.0.pl +++ b/docs/upgrades/upgrade_7.2.3-7.3.0.pl @@ -23,6 +23,7 @@ addWikiAssets($session); deleteOldFiles($session); addFileFieldsToDataForm($session); makeRSSFromParentAlwaysHidden($session); +addProfileFieldsOnPasswordRecovery($session); addNewCalendar($session); migrateCalendars($session); removeOldCalendar($session); @@ -278,6 +279,20 @@ sub removeOldCalendar { $session->config->deleteFromArray("assets","WebGUI::Asset::Wobject::EventsCalendar"); } +#------------------------------------------------- +sub addProfileFieldsOnPasswordRecovery { + my $session = shift; + print "\tAdding requiredForPasswordRecovery to userProfileField rows.\n" unless $quiet; + $session->db->write($_) for(<<'EOT', + ALTER TABLE userProfileField + ADD COLUMN requiredForPasswordRecovery int(11) NOT NULL default '0' +EOT + ); + + $session->setting->set('webguiPasswordRecovery', 0); + $session->setting->add('webguiPasswordRecoveryRequireUsername', 1); + $session->setting->set('webguiPasswordRecoveryTemplate', 'PBtmpl0000000000000014'); +} # ---- DO NOT EDIT BELOW THIS LINE ---- diff --git a/lib/WebGUI/Auth/WebGUI.pm b/lib/WebGUI/Auth/WebGUI.pm index fcca836d3..8f566280f 100644 --- a/lib/WebGUI/Auth/WebGUI.pm +++ b/lib/WebGUI/Auth/WebGUI.pm @@ -55,7 +55,7 @@ sub _hasMixedCaseCharacters { =head2 _isValidPassword ( ) - Validates the password9. + Validates the password. =cut @@ -427,11 +427,11 @@ sub editUserSettingsForm { -value=>$self->session->setting->get("webguiPasswordRecovery"), -label=>$i18n->get(6) ); - $f->textarea( - -name=>"webguiRecoverPasswordEmail", - -label=>$i18n->get(134, 'WebGUI'), - -value=>$self->session->setting->get("webguiRecoverPasswordEmail") - ); + $f->yesNo( + -name=>"webguiPasswordRecoveryRequireUsername", + -value=>$self->session->setting->get("webguiPasswordRecoveryRequireUsername"), + -label=>$i18n->get('require username for password recovery') + ); $f->yesNo( -name=>"webguiValidateEmail", -value=>$self->session->setting->get("webguiValidateEmail"), @@ -469,7 +469,7 @@ sub editUserSettingsForm { $f->template( -name=>"webguiPasswordRecoveryTemplate", -value=>$self->session->setting->get("webguiPasswordRecoveryTemplate"), - -namespace=>"Auth/WebGUI/Recovery", + -namespace=>"Auth/WebGUI/Recovery2", -label=>$i18n->get("password recovery template") ); return $f->printRowsOnly; @@ -491,7 +491,7 @@ sub editUserSettingsFormSave { $s->set("webguiChangeUsername", $f->process("webguiChangeUsername","yesNo")); $s->set("webguiChangePassword", $f->process("webguiChangePassword","yesNo")); $s->set("webguiPasswordRecovery", $f->process("webguiPasswordRecovery","yesNo")); - $s->set("webguiRecoverPasswordEmail", $f->process("webguiRecoverPasswordEmail","textarea")); + $s->set("webguiPasswordRecoveryRequireUsername", $f->process("webguiPasswordRecoveryRequireUsername","yesNo")); $s->set("webguiValidateEmail", $f->process("webguiValidateEmail","yesNo")); $s->set("webguiUseCaptcha", $f->process("webguiUseCaptcha","yesNo")); $s->set("webguiAccountTemplate", $f->process("webguiAccountTemplate","template")); @@ -570,63 +570,140 @@ sub new { #------------------------------------------------------------------- sub recoverPassword { my $self = shift; - return $self->displayLogin if($self->userId ne "1"); - my $template = 'Auth/WebGUI/Recovery'; - my $vars; + return $self->displayLogin unless $self->session->setting->get('webguiPasswordRecovery') and $self->userId eq '1'; + my @fields = @{WebGUI::ProfileField->getPasswordRecoveryFields($self->session)}; + return $self->displayLogin unless @fields; + + my $vars = {}; my $i18n = WebGUI::International->new($self->session); $vars->{title} = $i18n->get(71); - $vars->{'recover.form.header'} = "\n\n".WebGUI::Form::formHeader($self->session,{}); - $vars->{'recover.form.hidden'} = WebGUI::Form::hidden($self->session,{"name"=>"op","value"=>"auth"}); - $vars->{'recover.form.hidden'} .= WebGUI::Form::hidden($self->session,{"name"=>"method","value"=>"recoverPasswordFinish"}); + $vars->{'recoverFormHeader'} = "\n\n".WebGUI::Form::formHeader($self->session,{}); + $vars->{'recoverFormHidden'} = WebGUI::Form::hidden($self->session,{"name"=>"op","value"=>"auth"}); + $vars->{'recoverFormHidden'} .= WebGUI::Form::hidden($self->session,{"name"=>"method","value"=>"recoverPasswordFinish"}); - $vars->{'recover.form.submit'} = WebGUI::Form::submit($self->session,{}); - $vars->{'recover.form.footer'} = WebGUI::Form::formFooter($self->session,); - $vars->{'login.url'} = $self->session->url->page('op=auth;method=init'); - $vars->{'login.label'} = $i18n->get(58); + $vars->{'recoverFormSubmit'} = WebGUI::Form::submit($self->session,{}); + $vars->{'recoverFormFooter'} = WebGUI::Form::formFooter($self->session,); + $vars->{'loginUrl'} = $self->session->url->page('op=auth;method=init'); + $vars->{'loginLabel'} = $i18n->get(58); + + $vars->{'anonymousRegistrationIsAllowed'} = ($self->session->setting->get("anonymousRegistration")); + $vars->{'createAccountUrl'} = $self->session->url->page('op=auth;method=createAccount'); + $vars->{'createAccountLabel'} = $i18n->get(67); + $vars->{'recoverMessage'} = $_[0] if ($_[0]); + + # Semi-duplication with WebGUI::Auth::createAccount. -.- + $vars->{'recoverFormProfile'} = []; + foreach my $field (@fields) { + my ($id, $formField, $label) = ($field->getId, $field->formField, $field->getLabel); + push @{$vars->{'recoverFormProfile'}}, + +{ 'id' => $id, 'formElement' => $formField, 'label' => $label }; + + my $prefix = 'recoverFormProfileField' . ucfirst($id); + $vars->{$prefix.'FormElement'} = $formField; + $vars->{$prefix.'Label'} = $label; + } + + if ($self->getSetting('passwordRecoveryRequireUsername')) { + $vars->{'recoverFormUsername'} = WebGUI::Form::text($self->session, {name => 'authWebGUI.username'}); + $vars->{'recoverFormUsernameLabel'} = $i18n->get(50); + } - $vars->{'anonymousRegistration.isAllowed'} = ($self->session->setting->get("anonymousRegistration")); - $vars->{'createAccount.url'} = $self->session->url->page('op=auth;method=createAccount'); - $vars->{'createAccount.label'} = $i18n->get(67); - $vars->{'recover.message'} = $_[0] if ($_[0]); - $vars->{'recover.form.email'} = WebGUI::Form::text($self->session,{"name"=>"email"}); - $vars->{'recover.form.email.label'} = $i18n->get(56); return WebGUI::Asset::Template->new($self->session,$self->getPasswordRecoveryTemplateId)->process($vars); } #------------------------------------------------------------------- sub recoverPasswordFinish { - my $self = shift; + my $self = shift; my $i18n = WebGUI::International->new($self->session); - return $self->recoverPassword('
  • '.$i18n->get(743).'
') if ($self->session->form->process("email") eq ""); - return $self->displayLogin unless ($self->session->setting->get("webguiPasswordRecovery")); - - my($sth,$username,$userId,$password,$flag,$message,$output,$encryptedPassword,$authMethod); - $sth = $self->session->db->read("select users.username,users.userId from users, userProfileData where users.userId=userProfileData.userId and - users.authMethod='WebGUI' and userProfileData.fieldName='email' and userProfileData.fieldData=".$self->session->db->quote($self->session->form->process("email"))); - $flag = 0; - while (($username,$userId) = $sth->array) { - my $len = $self->session->setting->get("webguiPasswordLength") || 6; - $password = ""; - for(my $i = 0; $i < $len; $i++) { - $password .= chr(ord('A') + randint(32)); - } - $encryptedPassword = Digest::MD5::md5_base64($password); - $self->saveParams($userId,"WebGUI",{identifier=>$encryptedPassword}); - $self->_logSecurityMessage(); - $self->session->errorHandler->security("recover a password. Password emailed to: ".$self->session->form->process("email")); - $message = $self->session->setting->get("webguiRecoverPasswordEmail"); - $message .= "\n".$i18n->get(50).": ".$username."\n"; - $message .= $i18n->get(51).": ".$password."\n"; - my $mail = WebGUI::Mail::Send->create($self->session, {to=>$self->session->form->process("email"),subject=>$i18n->get(74)}); - $mail->addText($message); - $mail->addFooter; - $mail->send; - $flag++; + my $i18n2 = WebGUI::International->new($self->session, 'AuthWebGUI'); + return $self->displayLogin unless $self->session->setting->get('webguiPasswordRecovery') and $self->userId eq '1'; + + my $username; + if ($self->getSetting('passwordRecoveryRequireUsername')) { + $username = $self->session->form->process('authWebGUI.username'); + return $self->recoverPassword($i18n2->get('password recovery no username')) unless defined $username; + } + + my @fields = @{WebGUI::ProfileField->getPasswordRecoveryFields($self->session)}; + return $self->displayLogin unless @fields; + + my %fieldValues; + my @failedRequiredFields; + foreach my $field (@fields) { + my $value = $field->formProcess; + $fieldValues{$field->getId} = $value; + push @failedRequiredFields, $field unless defined $value; + } + + if (@failedRequiredFields) { + my $errorMessage = '
    ' . join("\n", map { + '
  • ' . $_->getLabel . ' ' . $i18n->get(451) . '
  • ' + } @failedRequiredFields) . '
'; + return $self->recoverPassword($errorMessage); + } + + my @fieldNames = keys %fieldValues; + my @fieldValues = values %fieldValues; + my $joins = join(' ', map{"INNER JOIN userProfileData AS p$_ ON u.userId = p$_.userId AND p$_.fieldName = ".$self->session->db->quote($fieldNames[$_])} (0..$#fieldNames)); + my $wheres = join(' ', map{"AND p$_.fieldData = ?"} (0..$#fieldNames)); + $wheres .= ' AND u.username = ?' if defined $username; + my $sql = "SELECT u.userId FROM users AS u $joins WHERE u.authMethod = 'WebGUI' $wheres"; + my @userIds = $self->session->db->buildArray($sql, [@fieldValues, (defined($username)? ($username) : ())]); + + if (@userIds == 0) { + return $self->recoverPassword($i18n2->get('password recovery no results')); + } elsif (@userIds > 1) { + return $self->recoverPassword($i18n2->get('password recovery multiple results')); + } + + # Exactly one result. + my $userId = $userIds[0]; + my ($password, $passwordConfirm) = ($self->session->form->process('authWebGUI.identifier'), $self->session->form->process('authWebGUI.identifierConfirm')); + + unless (defined $password and defined $passwordConfirm) { + my $vars = {}; + $vars->{title} = $i18n->get(71); + $vars->{'recoverFormHeader'} = "\n\n" . WebGUI::Form::formHeader($self->session, {}); + $vars->{'recoverFormHidden'} = + (WebGUI::Form::hidden($self->session, {name => 'op', value => 'auth'}) + . WebGUI::Form::hidden($self->session, {name => 'method', value => 'recoverPasswordFinish'}) + . (defined($username)? + WebGUI::Form::hidden($self->session, {name => 'authWebGUI.username', + value => $username}) : '') + . join('', map{WebGUI::Form::hidden($self->session, {name => $_, value => $fieldValues{$_}})} + keys %fieldValues)); + $vars->{'recoverFormSubmit'} = WebGUI::Form::submit($self->session, {}); + $vars->{'recoverFormFooter'} = WebGUI::Form::formFooter($self->session); + + # Duplication with above in recoverPassword. + $vars->{'loginUrl'} = $self->session->url->page('op=auth;method=init'); + $vars->{'loginLabel'} = $i18n->get(58); + + $vars->{'anonymousRegistrationIsAllowed'} = ($self->session->setting->get("anonymousRegistration")); + $vars->{'createAccountUrl'} = $self->session->url->page('op=auth;method=createAccount'); + $vars->{'createAccountLabel'} = $i18n->get(67); + # End duplication. + + $vars->{'recoverFormPassword'} = WebGUI::Form::password($self->session, {name => 'authWebGUI.identifier'}); + $vars->{'recoverFormPasswordConfirm'} = WebGUI::Form::password($self->session, {name => 'authWebGUI.identifierConfirm'}); + $vars->{'recoverFormPasswordLabel'} = $i18n->get(51); + $vars->{'recoverFormPasswordConfirmLabel'} = $i18n2->get(2); + + # Mrgh. z.z + $vars->{'doingRecovery'} = 1; + return WebGUI::Asset::Template->new($self->session, $self->getPasswordRecoveryTemplateId)->process($vars); + } + + if ($self->_isValidPassword($password, $passwordConfirm)) { + $self->user(WebGUI::User->new($self->session, $userId)); + $self->saveParams($userId, $self->authMethod, + { identifier => Digest::MD5::md5_base64($password), + passwordLastUpdated => $self->session->datetime->time }); + $self->_logSecurityMessage; + return $self->SUPER::login; + } else { + return $self->recoverPassword('
  • '.$self->error.'
'); } - $sth->finish(); - - return $self->displayLogin('
  • '.$i18n->get(75).'
') if($flag); - return $self->recoverPassword('
  • '.$i18n->get(76).'
'); } #------------------------------------------------------------------- diff --git a/lib/WebGUI/Operation/ProfileSettings.pm b/lib/WebGUI/Operation/ProfileSettings.pm index ac0b4492d..5e40e43bd 100644 --- a/lib/WebGUI/Operation/ProfileSettings.pm +++ b/lib/WebGUI/Operation/ProfileSettings.pm @@ -272,6 +272,12 @@ sub www_editProfileField { -hoverHelp => $i18n->get('showAtRegistration hoverHelp'), -value => $data->{showAtRegistration} ); + $f->yesNo( + -name => 'requiredForPasswordRecovery', + -label => $i18n->get('requiredForPasswordRecovery label'), + -hoverHelp => $i18n->get('requiredForPasswordRecovery hoverHelp'), + -value => $data->{requiredForPasswordRecovery} + ); if ($data->{fieldType} eq "Image") { $f->yesNo( -name=>"forceImageOnly", @@ -343,6 +349,7 @@ sub www_editProfileFieldSave { visible=>$session->form->yesNo("visible"), required=>$session->form->yesNo("required"), showAtRegistration=>$session->form->yesNo("showAtRegistration"), + requiredForPasswordRecovery=>$session->form->yesNo("requiredForPasswordRecovery"), possibleValues=>$session->form->textarea("possibleValues"), dataDefault=>$session->form->textarea("dataDefault"), fieldType=>$session->form->fieldType("fieldType"), diff --git a/lib/WebGUI/ProfileField.pm b/lib/WebGUI/ProfileField.pm index 0c35d3d77..60013a384 100644 --- a/lib/WebGUI/ProfileField.pm +++ b/lib/WebGUI/ProfileField.pm @@ -343,6 +343,19 @@ sub getRegistrationFields { return $class->_listFieldsWhere($session, "f.showAtRegistration = 1"); } +=head2 getPasswordRecoveryFields ( session ) + +Returns an array reference of profile field objects that are required +for password recovery. Class method. + +=cut + +sub getPasswordRecoveryFields { + my $class = shift; + my $session = shift; + return $class->_listFieldsWhere($session, "f.requiredForPasswordRecovery = 1"); +} + #------------------------------------------------------------------- =head2 isEditable ( ) diff --git a/lib/WebGUI/i18n/English/AuthWebGUI.pm b/lib/WebGUI/i18n/English/AuthWebGUI.pm index 170269016..d46d0c645 100644 --- a/lib/WebGUI/i18n/English/AuthWebGUI.pm +++ b/lib/WebGUI/i18n/English/AuthWebGUI.pm @@ -483,6 +483,21 @@ our $I18N = { lastUpdated => 1128919828, }, + 'require username for password recovery' => { + message => q|Require Username for Password Recovery?|, + lastUpdated => 1165402566, + }, + + 'password recovery no results' => { + message => q|No users were found matching that profile data. Please try again.|, + lastUpdated => 1165402566, + }, + + 'password recovery multiple results' => { + message => q|Sorry, password recovery cannot be performed for this account. Please contact an administrator.|, + lastUpdated => 1165402566, + }, + }; 1; diff --git a/lib/WebGUI/i18n/English/WebGUIProfile.pm b/lib/WebGUI/i18n/English/WebGUIProfile.pm index 7b9fbe8a1..b88f02e48 100644 --- a/lib/WebGUI/i18n/English/WebGUIProfile.pm +++ b/lib/WebGUI/i18n/English/WebGUIProfile.pm @@ -351,6 +351,16 @@ new categories of profile settings. message => "Show an entry for this field at the registration screen for newly-registering users. The field will not actually be required unless Required is also set.", lastUpdated => 1164237018 }, + + 'requiredForPasswordRecovery label' => { + message => "Required for password recovery?", + lastUpdated => 1165401097 + }, + + 'requiredForPasswordRecovery hoverHelp' => { + message => "Require users to enter this field for password recovery. Only users that enter all such fields correctly and uniquely to them will be able to perform password recovery.", + lastUpdated => 1165401097 + }, }; 1;