Switch to profile-based password recovery.
This commit is contained in:
parent
de1b160c2b
commit
631d8cb0e6
9 changed files with 250 additions and 54 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 ----
|
||||
|
||||
|
|
|
|||
|
|
@ -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>');
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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 ( )
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue