Switch to profile-based password recovery.

This commit is contained in:
Drake 2006-12-06 11:57:36 +00:00
parent de1b160c2b
commit 631d8cb0e6
9 changed files with 250 additions and 54 deletions

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,47 @@
#PBtmpl0000000000000014
#namespace:Auth/WebGUI/Recovery2
<h2><tmpl_var title></h2>
<tmpl_if recoverMessage><tmpl_var recoverMessage></tmpl_if>
<tmpl_var recoverFormHeader>
<tmpl_var recoverFormHidden>
<table>
<tmpl_if doingRecovery>
<tr>
<td class="formDescription" valign="top"><tmpl_var recoverFormPasswordLabel></td>
<td class="tableData"><tmpl_var recoverFormPassword></td>
</tr>
<tr>
<td class="formDescription" valign="top"><tmpl_var recoverFormPasswordConfirmLabel></td>
<td class="tableData"><tmpl_var recoverFormPasswordConfirm></td>
</tr>
<tmpl_else>
<tmpl_if recoverFormUsername>
<tr>
<td class="formDescription" valign="top"><tmpl_var recoverFormUsernameLabel></td>
<td class="tableData"><tmpl_var recoverFormUsername></td>
</tr>
</tmpl_if>
<tmpl_loop recoverFormProfile>
<tr>
<td class="formDescription" valign="top"><tmpl_var label></td>
<td class="tableData"><tmpl_var formElement></td>
</tr>
</tmpl_loop>
</tmpl_if>
<tr>
<td class="formDescription" valign="top"></td>
<td class="tableData"><tmpl_var recoverFormSubmit></td>
</tr>
</table>
<tmpl_var recoverFormFooter>
<div class="accountOptions">
<ul>
<tmpl_if anonymousRegistrationIsAllowed>
<li><a href="<tmpl_var createAccountUrl>"><tmpl_var createAccountLabel></a></li>
</tmpl_if>
<li><a href="<tmpl_var loginUrl>"><tmpl_var loginLabel></a></li>
</ul>
</div>

View file

@ -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 ----

View file

@ -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('<ul><li>'.$i18n->get(743).'</li></ul>') 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 = '<ul>' . join("\n", map {
'<li>' . $_->getLabel . ' ' . $i18n->get(451) . '</li>'
} @failedRequiredFields) . '</ul>';
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('<ul><li>'.$self->error.'</li></ul>');
}
$sth->finish();
return $self->displayLogin('<ul><li>'.$i18n->get(75).'</li></ul>') if($flag);
return $self->recoverPassword('<ul><li>'.$i18n->get(76).'</li></ul>');
}
#-------------------------------------------------------------------

View file

@ -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"),

View file

@ -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 ( )

View file

@ -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;

View file

@ -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;