mlanthaler: Newly implemented "I've lost my password" feature that works also with encrypted passwords (ticket #48).

There are some (cosmetically) things that should be fixed, but everything work as it should. 
Will fix those things after my vacation. 
(merged from branches/gsoc)


git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@41976 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2007-09-16 00:32:48 +00:00
parent 357c18b692
commit f54e9db8b9
7 changed files with 317 additions and 130 deletions

View File

@ -1,19 +1,19 @@
<?php <?php
/** /**
* Standard Change Password Form * Standard Change Password Form
*/ */
class ChangePasswordForm extends Form { class ChangePasswordForm extends Form {
function __construct($controller, $name, $fields = null, $actions = null) { function __construct($controller, $name, $fields = null, $actions = null) {
if(!$fields) { if(!$fields) {
$fields = new FieldSet( $fields = new FieldSet();
new EncryptField("OldPassword","Your old password"), if(Member::currentUser()) {
new EncryptField("NewPassword1", "New Password"), $fields->push(new EncryptField("OldPassword","Your old password"));
new EncryptField("NewPassword2", "Confirm New Password") }
);
$fields->push(new EncryptField("NewPassword1", "New Password"));
$fields->push(new EncryptField("NewPassword2", "Confirm New Password"));
} }
if(!$actions) { if(!$actions) {
$actions = new FieldSet( $actions = new FieldSet(
@ -24,31 +24,61 @@ class ChangePasswordForm extends Form {
parent::__construct($controller, $name, $fields, $actions); parent::__construct($controller, $name, $fields, $actions);
} }
function changePassword($data){ /**
if($member = Member::currentUser()){ * Change the password
if($data['OldPassword'] != $member->Password){ *
* @param array $data The user submitted data
*/
function changePassword(array $data) {
if($member = Member::currentUser()) {
// The user was logged in, check the current password
if($member->checkPassword($data['OldPassword']) == false) {
$this->clearMessage(); $this->clearMessage();
$this->sessionMessage("Your current password does not match, please try again", "bad"); $this->sessionMessage(
Director::redirectBack(); "Your current password does not match, please try again", "bad");
}else if($data[NewPassword1] == $data[NewPassword2]){
$member->Password = $data[NewPassword1] ;
$member->sendinfo('changePassword');
$member->write();
$this->clearMessage();
$this->sessionMessage("Your password has been changed, and a copy emailed to you.", "good");
Director::redirectBack();
}
else{
$this->clearMessage();
$this->sessionMessage("Your have entered your new password differently, try again", "bad");
Director::redirectBack(); Director::redirectBack();
} }
} }
else {
Director::redirect('loginpage'); if(!$member) {
if(Session::get('AutoLoginHash')) {
$member = Member::autoLoginHash(Session::get('AutoLoginHash'));
}
// The user is not logged in and no valid auto login hash is available
if(!$member) {
Session::clear('AutoLoginHash');
Director::redirect('loginpage');
}
}
// Check the new password
if($data['NewPassword1'] == $data['NewPassword2']) {
$member->Password = $data['NewPassword1'];
$member->AutoLoginHash = null;
$member->write();
$member->sendinfo('changePassword',
array('CleartextPassword' => $data['NewPassword1']));
$this->clearMessage();
$this->sessionMessage(
"Your password has been changed, and a copy emailed to you.",
"good");
Session::clear('AutoLoginHash');
Director::redirect(Security::Link('login'));
} else {
$this->clearMessage();
$this->sessionMessage(
"Your have entered your new password differently, try again",
"bad");
Director::redirectBack();
} }
} }
} }
?> ?>

View File

@ -10,7 +10,7 @@ class Member extends DataObject {
'NumVisit' => "Int", 'NumVisit' => "Int",
'LastVisited' => 'Datetime', 'LastVisited' => 'Datetime',
'Bounced' => 'Boolean', 'Bounced' => 'Boolean',
'AutoLoginHash' => 'Varchar(10)', 'AutoLoginHash' => 'Varchar(30)',
'AutoLoginExpired' => 'Datetime', 'AutoLoginExpired' => 'Datetime',
'BlacklistedEmail' => 'Boolean', 'BlacklistedEmail' => 'Boolean',
'PasswordEncryption' => "Enum('none', 'none')", 'PasswordEncryption' => "Enum('none', 'none')",
@ -30,6 +30,7 @@ class Member extends DataObject {
static $indexes = array( static $indexes = array(
'Email' => true, 'Email' => true,
'AutoLoginHash' => 'unique (AutoLoginHash)'
); );
@ -53,6 +54,21 @@ class Member extends DataObject {
} }
/**
* Check if the passed password matches the stored one
*
* @param string $password The clear text password to check
* @return bool Returns TRUE if the passed password is valid, otherwise
* FALSE.
*/
public function checkPassword($password) {
$encryption_details = Security::encrypt_password($password, $this->Salt,
$this->PasswordEncryption);
return ($this->Password === $encryption_details['password']);
}
/** /**
* Logs this member in * Logs this member in
* *
@ -131,30 +147,40 @@ class Member extends DataObject {
/** /**
* Generate an auto login hash * Generate an auto login hash
* *
* @todo This is relative insecure, check if we should fix it (Markus) * This creates an auto login hash that can be used to reset the password.
*
* @param int $lifetime The lifetime of the auto login hash in days
* (by default 2 days)
*
* @todo Make it possible to handle database errors such as a "duplicate
* key" error
*/ */
function generateAutologinHash() { function generateAutologinHash($lifetime = 2) {
$linkHash = sprintf('%10d', time() );
while( DataObject::get_one( 'Member', "`AutoLoginHash`='$linkHash'" ) ) do {
$linkHash = sprintf('%10d', abs( time() * rand( 1, 10 ) ) ); $hash = substr(base_convert(md5(uniqid(mt_rand(), true)), 16, 36),
0, 30);
} while(DataObject::get_one('Member', "`AutoLoginHash` = '$hash'"));
$this->AutoLoginHash = $linkHash; $this->AutoLoginHash = $hash;
$this->AutoLoginExpired = date('Y-m-d', time() + ( 60 * 60 * 24 * 14 ) ); $this->AutoLoginExpired = date('Y-m-d', time() + (86400 * $lifetime));
$this->write(); $this->write();
} }
/** /**
* Log a member in with an auto login hash link * Return the member for the auto login hash
*
* @param bool $login Should the member be logged in?
*/ */
static function autoLoginHash($RAW_hash) { static function autoLoginHash($RAW_hash, $login = false) {
$SQL_hash = Convert::raw2sql($RAW_hash); $SQL_hash = Convert::raw2sql($RAW_hash);
$member = DataObject::get_one('Member',"`AutoLoginHash`='$SQL_hash' AND `AutoLoginExpired` > NOW()"); $member = DataObject::get_one('Member',"`AutoLoginHash`='" . $SQL_hash .
"' AND `AutoLoginExpired` > NOW()");
if($member) if($login && $member)
$member->logIn(); $member->logIn();
return $member; return $member;
@ -166,8 +192,10 @@ class Member extends DataObject {
* *
* @param string $type Information type to send ("signup", * @param string $type Information type to send ("signup",
* "changePassword" or "forgotPassword") * "changePassword" or "forgotPassword")
* @param array $data Additional data to pass to the email (can be used in
* the template)
*/ */
function sendInfo($type = 'signup') { function sendInfo($type = 'signup', $data = null) {
switch($type) { switch($type) {
case "signup": case "signup":
$e = new Member_SignupEmail(); $e = new Member_SignupEmail();
@ -179,6 +207,12 @@ class Member extends DataObject {
$e = new Member_ForgotPasswordEmail(); $e = new Member_ForgotPasswordEmail();
break; break;
} }
if(is_array($data)) {
foreach($data as $key => $value)
$e->$key = $value;
}
$e->populateTemplate($this); $e->populateTemplate($this);
$e->send(); $e->send();
} }
@ -1008,7 +1042,7 @@ class Member_ChangePasswordEmail extends Email_Template {
*/ */
class Member_ForgotPasswordEmail extends Email_Template { class Member_ForgotPasswordEmail extends Email_Template {
protected $from = ''; // setting a blank from address uses the site's default administrator email protected $from = ''; // setting a blank from address uses the site's default administrator email
protected $subject = "Your password"; protected $subject = "Your password reset link";
protected $ss_template = 'ForgotPasswordEmail'; protected $ss_template = 'ForgotPasswordEmail';
protected $to = '$Email'; protected $to = '$Email';
} }

View File

@ -31,14 +31,8 @@ class MemberAuthenticator extends Authenticator {
$member = DataObject::get_one("Member", $member = DataObject::get_one("Member",
"Email = '$SQL_user' AND Password IS NOT NULL"); "Email = '$SQL_user' AND Password IS NOT NULL");
if($member) { if($member && ($member->checkPassword($RAW_data['Password']) == false)) {
$encryption_details = $member = null;
Security::encrypt_password($RAW_data['Password'], $member->Salt,
$member->PasswordEncryption);
// Check if the entered password is valid
if(($member->Password != $encryption_details['password']))
$member = null;
} }

View File

@ -168,25 +168,25 @@ class MemberLoginForm extends LoginForm {
function forgotPassword($data) { function forgotPassword($data) {
$SQL_data = Convert::raw2sql($data); $SQL_data = Convert::raw2sql($data);
if($data['Email'] && $member = DataObject::get_one("Member", if(($data['Email']) && ($member = DataObject::get_one("Member",
"Member.Email = '$SQL_data[Email]'")) { "Member.Email = '$SQL_data[Email]'"))) {
if(!$member->Password) {
$member->createNewPassword(); $member->generateAutologinHash();
$member->write();
} $member->sendInfo('forgotPassword', array('PasswordResetLink' =>
Security::getPasswordResetLink($member->AutoLoginHash)));
$member->sendInfo('forgotPassword');
Director::redirect('Security/passwordsent/' . urlencode($data['Email'])); Director::redirect('Security/passwordsent/' . urlencode($data['Email']));
} else if($data['Email']) { } else if($data['Email']) {
$this->sessionMessage( $this->sessionMessage(
"Sorry, but I don't recognise the e-mail address. Maybe you need to sign up, or perhaps you used another e-mail address?", "Sorry, but I don't recognise the e-mail address. Maybe you need " .
"to sign up, or perhaps you used another e-mail address?",
"bad"); "bad");
Director::redirectBack(); Director::redirectBack();
} else { } else {
Director::redirect("Security/lostpassword"); Director::redirect("Security/lostpassword");
} }
} }

View File

@ -119,7 +119,7 @@ class Security extends Controller {
/** /**
* Get the login form to process according to the submitted data * Get the login form to process according to the submitted data
*/ */
function LoginForm() { protected function LoginForm() {
if(is_array($_REQUEST) && isset($_REQUEST['AuthenticationMethod'])) if(is_array($_REQUEST) && isset($_REQUEST['AuthenticationMethod']))
{ {
$authenticator = trim($_REQUEST['AuthenticationMethod']); $authenticator = trim($_REQUEST['AuthenticationMethod']);
@ -142,7 +142,7 @@ class Security extends Controller {
* *
* @todo Check how to activate/deactivate authentication methods * @todo Check how to activate/deactivate authentication methods
*/ */
function GetLoginForms() protected function GetLoginForms()
{ {
$forms = array(); $forms = array();
@ -163,7 +163,7 @@ class Security extends Controller {
* @param string $action Name of the action * @param string $action Name of the action
* @return string Returns the link to the given action * @return string Returns the link to the given action
*/ */
function Link($action = null) { public static function Link($action = null) {
return "Security/$action"; return "Security/$action";
} }
@ -176,7 +176,7 @@ class Security extends Controller {
* responsible for sending the user where-ever * responsible for sending the user where-ever
* they should go. * they should go.
*/ */
function logout($redirect = true) { public function logout($redirect = true) {
if($member = Member::currentUser()) if($member = Member::currentUser())
$member->logOut(); $member->logOut();
@ -190,7 +190,7 @@ class Security extends Controller {
* *
* @return string Returns the "login" page as HTML code. * @return string Returns the "login" page as HTML code.
*/ */
function login() { public function login() {
Requirements::javascript("jsparty/behaviour.js"); Requirements::javascript("jsparty/behaviour.js");
Requirements::javascript("jsparty/loader.js"); Requirements::javascript("jsparty/loader.js");
Requirements::javascript("jsparty/prototype.js"); Requirements::javascript("jsparty/prototype.js");
@ -245,53 +245,25 @@ class Security extends Controller {
/** /**
* Show the "lost password" page * Show the "lost password" page
* *
* @return string Returns the "lost password " page as HTML code. * @return string Returns the "lost password" page as HTML code.
*/ */
function lostpassword() { public function lostpassword() {
Requirements::javascript("jsparty/prototype.js"); Requirements::javascript('jsparty/prototype.js');
Requirements::javascript("jsparty/behaviour.js"); Requirements::javascript('jsparty/behaviour.js');
Requirements::javascript("jsparty/loader.js"); Requirements::javascript('jsparty/loader.js');
Requirements::javascript("jsparty/prototype_improvements.js"); Requirements::javascript('jsparty/prototype_improvements.js');
Requirements::javascript("jsparty/scriptaculous/effects.js"); Requirements::javascript('jsparty/scriptaculous/effects.js');
$tmpPage = new Page(); $tmpPage = new Page();
$tmpPage->Title = "Lost Password"; $tmpPage->Title = 'Lost Password';
$tmpPage->URLSegment = "Security"; $tmpPage->URLSegment = 'Security';
$controller = new Page_Controller($tmpPage); $controller = new Page_Controller($tmpPage);
$customisedController = $controller->customise(array( $customisedController = $controller->customise(array(
"Content" => 'Content' =>
"<p>Enter your e-mail address and we will send you a password</p>", '<p>Enter your e-mail address and we will send you a link with ' .
"Form" => $this->LostPasswordForm(), 'which you can reset your password</p>',
)); 'Form' => $this->LostPasswordForm(),
//Controller::$currentController = $controller;
return $customisedController->renderWith("Page");
}
/**
* Show the "password sent" page
*
* @return string Returns the "password sent" page as HTML code.
*/
function passwordsent() {
Requirements::javascript("jsparty/behaviour.js");
Requirements::javascript("jsparty/loader.js");
Requirements::javascript("jsparty/prototype.js");
Requirements::javascript("jsparty/prototype_improvements.js");
Requirements::javascript("jsparty/scriptaculous/effects.js");
$tmpPage = new Page();
$tmpPage->Title = "Lost Password";
$tmpPage->URLSegment = "Security";
$controller = new Page_Controller($tmpPage);
$email = $this->urlParams['ID'];
$customisedController = $controller->customise(array(
"Title" => "Password sent to '$email'",
"Content" =>
"<p>Thank you, your password has been sent to '$email'.</p>",
)); ));
//Controller::$currentController = $controller; //Controller::$currentController = $controller;
@ -304,13 +276,169 @@ class Security extends Controller {
* *
* @return Form Returns the lost password form * @return Form Returns the lost password form
*/ */
function LostPasswordForm() { public function LostPasswordForm() {
return new MemberLoginForm($this, "LostPasswordForm", new FieldSet( return new MemberLoginForm($this, 'LostPasswordForm',
new EmailField("Email", "Email address") new FieldSet(new EmailField('Email', 'E-mail address')),
), new FieldSet( new FieldSet(new FormAction('forgotPassword',
new FormAction("forgotPassword", "Send me my password") 'Send me the password reset link')),
), false false);
); }
/**
* Show the "password sent" page
*
* @return string Returns the "password sent" page as HTML code.
*/
public function passwordsent() {
Requirements::javascript('jsparty/behaviour.js');
Requirements::javascript('jsparty/loader.js');
Requirements::javascript('jsparty/prototype.js');
Requirements::javascript('jsparty/prototype_improvements.js');
Requirements::javascript('jsparty/scriptaculous/effects.js');
$tmpPage = new Page();
$tmpPage->Title = 'Lost Password';
$tmpPage->URLSegment = 'Security';
$controller = new Page_Controller($tmpPage);
$email = Convert::raw2xml($this->urlParams['ID']);
$customisedController = $controller->customise(array(
'Title' => "Password reset link sent to '$email'",
'Content' =>
"<p>Thank you! The password reset link has been sent to '$email'.</p>",
));
//Controller::$currentController = $controller;
return $customisedController->renderWith("Page");
}
/**
* Create a link to the password reset form
*
* @param string $autoLoginHash The auto login hash
*/
public static function getPasswordResetLink($autoLoginHash) {
$autoLoginHash = urldecode($autoLoginHash);
return self::Link('changepassword') . "?h=$autoLoginHash";
}
/**
* Show the "change password" page
*
* @return string Returns the "change password" page as HTML code.
*/
public function changepassword() {
$tmpPage = new Page();
$tmpPage->Title = 'Change your password';
$tmpPage->URLSegment = 'Security';
$controller = new Page_Controller($tmpPage);
if(isset($_REQUEST['h']) && Member::autoLoginHash($_REQUEST['h'])) {
// The auto login hash is valid, store it for the change password form
Session::set('AutoLoginHash', $_REQUEST['h']);
$customisedController = $controller->customise(array(
'Content' =>
'<p>Please enter a new password.</p>',
'Form' => $this->ChangePasswordForm(),
));
} elseif(Member::currentUser()) {
// let a logged in user change his password
$customisedController = $controller->customise(array(
'Content' => '<p>You can change your password below.</p>',
'Form' => $this->ChangePasswordForm()));
} else {
// show an error message if the auto login hash is invalid and the
// user is not logged in
if(isset($_REQUEST['h'])) {
$customisedController = $controller->customise(array('Content' =>
"<p>The password reset link is invalid or expired.</p>\n" .
'<p>You can request a new one <a href="' .
$this->Link('lostpassword') .
'">here</a> or change your password after you <a href="' .
$this->link('login') . '">logged in</a>.</p>'));
} else {
self::permissionFailure($this, 'You must be logged in in order to change your password!');
die();
}
}
Controller::$currentController = $controller;
return $customisedController->renderWith('Page');
}
/**
* Create a link to the password reset form
*
* @param string $autoLoginHash The auto login hash
*/
public static function getPasswordResetLink($autoLoginHash) {
$autoLoginHash = urldecode($autoLoginHash);
return self::Link('changepassword') . "?h=$autoLoginHash";
}
/**
* Show the "change password" page
*
* @return string Returns the "change password" page as HTML code.
*/
public function changepassword() {
$tmpPage = new Page();
$tmpPage->Title = 'Change your password';
$tmpPage->URLSegment = 'Security';
$controller = new Page_Controller($tmpPage);
if(isset($_REQUEST['h']) && Member::autoLoginHash($_REQUEST['h'])) {
// The auto login hash is valid, store it for the change password form
Session::set('AutoLoginHash', $_REQUEST['h']);
$customisedController = $controller->customise(array(
'Content' =>
'<p>Please enter a new password.</p>',
'Form' => $this->ChangePasswordForm(),
));
} elseif(Member::currentUser()) {
// let a logged in user change his password
$customisedController = $controller->customise(array(
'Content' => '<p>You can change your password below.</p>',
'Form' => $this->ChangePasswordForm()));
} else {
// show an error message if the auto login hash is invalid and the
// user is not logged in
if(isset($_REQUEST['h'])) {
$customisedController = $controller->customise(array('Content' =>
"<p>The password reset link is invalid or expired.</p>\n" .
'<p>You can request a new one <a href="' .
$this->Link('lostpassword') .
'">here</a> or change your password after you <a href="' .
$this->link('login') . '">logged in</a>.</p>'));
} else {
self::permissionFailure($this, 'You must be logged in in order to change your password!');
die();
}
}
Controller::$currentController = $controller;
return $customisedController->renderWith('Page');
}
/**
* Factory method for the lost password form
*
* @return Form Returns the lost password form
*/
public function ChangePasswordForm() {
return new ChangePasswordForm($this, 'ChangePasswordForm');
} }
@ -321,7 +449,7 @@ class Security extends Controller {
* @return bool|Member Returns FALSE if authentication fails, otherwise * @return bool|Member Returns FALSE if authentication fails, otherwise
* the member object * the member object
*/ */
static function authenticate($RAW_email, $RAW_password) { public static function authenticate($RAW_email, $RAW_password) {
$SQL_email = Convert::raw2sql($RAW_email); $SQL_email = Convert::raw2sql($RAW_email);
$SQL_password = Convert::raw2sql($RAW_password); $SQL_password = Convert::raw2sql($RAW_password);
@ -344,9 +472,8 @@ class Security extends Controller {
* @return Member Returns a member object that has administrator * @return Member Returns a member object that has administrator
* privileges. * privileges.
*/ */
static function findAnAdministrator($username = 'admin', static function findAnAdministrator($username = 'admin', $password = 'password') {
$password = 'password') { $permission = DataObject::get_one("Permission", "`Code` = 'ADMIN'", true, "ID");
$permission = DataObject::get_one("Permission", "`Code` = 'ADMIN'");
$adminGroup = null; $adminGroup = null;
if($permission) $adminGroup = DataObject::get_one("Group", "`ID` = '{$permission->GroupID}'", true, "ID"); if($permission) $adminGroup = DataObject::get_one("Group", "`ID` = '{$permission->GroupID}'", true, "ID");
@ -387,7 +514,7 @@ class Security extends Controller {
* @param $username String * @param $username String
* @param $password String (Cleartext) * @param $password String (Cleartext)
*/ */
static function setDefaultAdmin( $username, $password ) { public static function setDefaultAdmin($username, $password) {
if( self::$username || self::$password ) if( self::$username || self::$password )
return; return;
@ -404,7 +531,7 @@ class Security extends Controller {
* @param boolean $strictPathChecking To enable or disable strict patch * @param boolean $strictPathChecking To enable or disable strict patch
* checking. * checking.
*/ */
static function setStrictPathChecking($strictPathChecking) { public static function setStrictPathChecking($strictPathChecking) {
self::$strictPathChecking = $strictPathChecking; self::$strictPathChecking = $strictPathChecking;
} }
@ -414,7 +541,7 @@ class Security extends Controller {
* *
* @return boolean Status of strict path checking * @return boolean Status of strict path checking
*/ */
static function getStrictPathChecking() { public static function getStrictPathChecking() {
return self::$strictPathChecking; return self::$strictPathChecking;
} }
@ -595,7 +722,7 @@ class Security extends Controller {
* *
* To run this action, the user needs to have administrator rights! * To run this action, the user needs to have administrator rights!
*/ */
function encryptallpasswords() { public function encryptallpasswords() {
// Only administrators can run this method // Only administrators can run this method
if(!Member::currentUser() || !Member::currentUser()->isAdmin()) { if(!Member::currentUser() || !Member::currentUser()->isAdmin()) {
Security::permissionFailure($this, Security::permissionFailure($this,

View File

@ -0,0 +1,6 @@
<p>Hi $FirstName,</p>
<p>You changed your password for $BaseHref.<br />
You can now use the following credentials to log in:</p>
<p>E-mail: $Email<br />
Password: $CleartextPassword</p>

View File

@ -1,8 +1,4 @@
<p>Hi $FirstName,</p> <p>Hi $FirstName,</p>
<p>Here's your password for <a href="home/">$BaseHref</a>.</p> <p>Here's is your <a href="$PasswordResetLink">password reset link</a> for $BaseHref</p>
<p>
Email: $Email<br />
Password: $Password
</p>