You've already forked postfixadmin
mirror of
https://github.com/postfixadmin/postfixadmin.git
synced 2025-07-31 10:04:20 +03:00
Harden password reset process
The improvements are: - Die with an explicit message when a user is trying to reset his lost password and the option is disabled in config - Redirect user to main page after password change using relative URL - Don't leak info whether user exists or has recovery info defined - Throttle password reset requests to prevent brute force attacks - Show phone/alt email fields in mailbox/admin edit form only when the password reset option is enabled - Make database upgrade code compatible with other databases types - Use the existing password generator to generate OTP. It is now stored in database, unique to each user, valid only for 1 hour and can only by used once.
This commit is contained in:
committed by
Adrien Crivelli
parent
8bb6000072
commit
ffb84283c2
@ -129,10 +129,6 @@ $CONF['admin_email'] = '';
|
|||||||
// This will be used as signature in notification messages
|
// This will be used as signature in notification messages
|
||||||
$CONF['admin_name'] = 'Postmaster';
|
$CONF['admin_name'] = 'Postmaster';
|
||||||
|
|
||||||
// Site admin phone number
|
|
||||||
// This will be used if a user cannot access his/her email and needs support
|
|
||||||
$CONF['admin_phone'] = '';
|
|
||||||
|
|
||||||
// Mail Server
|
// Mail Server
|
||||||
// Hostname (FQDN) of your mail server.
|
// Hostname (FQDN) of your mail server.
|
||||||
// This is used to send email to Postfix in order to create mailboxes.
|
// This is used to send email to Postfix in order to create mailboxes.
|
||||||
@ -588,16 +584,34 @@ $CONF['create_mailbox_subdirs_hostoptions'] = array();
|
|||||||
|
|
||||||
// Optional:
|
// Optional:
|
||||||
// Allows a user to reset his forgotten password with a code sent by email/SMS
|
// Allows a user to reset his forgotten password with a code sent by email/SMS
|
||||||
$CONF['forgotten_user_password_reset'] = false; # INSECURE, DO NOT ENABLE! See https://github.com/postfixadmin/postfixadmin/pull/18 for details
|
$CONF['forgotten_user_password_reset'] = true;
|
||||||
// Allows an admin to reset his forgotten password with a code sent by email/SMS
|
// Allows an admin to reset his forgotten password with a code sent by email/SMS
|
||||||
$CONF['forgotten_admin_password_reset'] = false; # INSECURE, DO NOT ENABLE! see https://github.com/postfixadmin/postfixadmin/pull/18 for details
|
$CONF['forgotten_admin_password_reset'] = false;
|
||||||
|
|
||||||
// Clickatell gateway to send SMS code for password reset
|
// Name of the function to send a SMS
|
||||||
// API type: HTTP
|
// Please use a name that begins with "x_" to prevent collisions
|
||||||
$CONF['clickatell_api_id'] = '';
|
// This function must accept 2 parameters: phone number and message,
|
||||||
$CONF['clickatell_user'] = '';
|
// and return true on success or false on failure
|
||||||
$CONF['clickatell_password'] = '';
|
$CONF['sms_send_function'] = '';
|
||||||
$CONF['clickatell_sender'] = '';
|
|
||||||
|
/*
|
||||||
|
// Example of send SMS function using Clickatell HTTP API
|
||||||
|
function x_send_sms_clickatell($to, $message) {
|
||||||
|
|
||||||
|
$clickatell_api_id = 'CHANGEME';
|
||||||
|
$clickatell_user = 'CHANGEME';
|
||||||
|
$clickatell_password = 'CHANGEME';
|
||||||
|
$clickatell_sender = 'CHANGEME';
|
||||||
|
|
||||||
|
$url = 'https://api.clickatell.com/http/sendmsg?api_id=%s&user=%s&password=%s&to=%s&from=%s&text=%s';
|
||||||
|
|
||||||
|
$url = sprintf($url, $clickatell_api_id, $clickatell_user, $clickatell_password, $to, $clickatell_sender, urlencode($message));
|
||||||
|
|
||||||
|
$result = file_get_contents($url);
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Theme Config
|
// Theme Config
|
||||||
// Specify your own logo and CSS file
|
// Specify your own logo and CSS file
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
$version = '3.1';
|
$version = '3.1';
|
||||||
$min_db_version = 1836; # update (at least) before a release with the latest function numbrer in upgrade.php
|
$min_db_version = 1837; # update (at least) before a release with the latest function numbrer in upgrade.php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check_session
|
* check_session
|
||||||
@ -89,6 +89,23 @@ function authentication_require_role($role) {
|
|||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a user or admin session
|
||||||
|
*
|
||||||
|
* @param String $username the user or admin name
|
||||||
|
* @param boolean $is_admin true if the user is an admin, false otherwise
|
||||||
|
* @return boolean true on success
|
||||||
|
*/
|
||||||
|
function init_session($username, $is_admin = false) {
|
||||||
|
$status = session_regenerate_id(true);
|
||||||
|
$_SESSION['sessid'] = array();
|
||||||
|
$_SESSION['sessid']['roles'] = array();
|
||||||
|
$_SESSION['sessid']['roles'][] = $is_admin ? 'admin' : 'user';
|
||||||
|
$_SESSION['sessid']['username'] = $username;
|
||||||
|
$_SESSION['PFA_token'] = md5(uniqid(rand(), true));
|
||||||
|
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an error message for display on the next page that is rendered.
|
* Add an error message for display on the next page that is rendered.
|
||||||
@ -806,7 +823,7 @@ function encode_header ($string, $default_charset = "utf-8") {
|
|||||||
//
|
//
|
||||||
function generate_password () {
|
function generate_password () {
|
||||||
// length of the generated password
|
// length of the generated password
|
||||||
$length = 8;
|
$length = 12;
|
||||||
|
|
||||||
// define possible characters
|
// define possible characters
|
||||||
$possible = "2345678923456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ"; # skip 0 and 1 to avoid confusion with O and l
|
$possible = "2345678923456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ"; # skip 0 and 1 to avoid confusion with O and l
|
||||||
@ -1942,21 +1959,4 @@ function getRemoteAddr() {
|
|||||||
return $REMOTE_ADDR;
|
return $REMOTE_ADDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a hash for a username valid for one day
|
|
||||||
*
|
|
||||||
* @param String $username user name
|
|
||||||
* @return String password recovery code
|
|
||||||
*/
|
|
||||||
function getPasswordRecoveryCode($username)
|
|
||||||
{
|
|
||||||
$username = trim(strtolower($username));
|
|
||||||
$date = date('Y-m-d');
|
|
||||||
|
|
||||||
$code = substr(strtoupper(md5('SECRET SALTING PHRASE' . $username . $date)), 0, 6);
|
|
||||||
|
|
||||||
return $code;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */
|
/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */
|
||||||
|
@ -154,7 +154,7 @@ $PALANG['pCreate_mailbox_mail'] = 'Send Welcome mail';
|
|||||||
$PALANG['pCreate_mailbox_result_error'] = 'Creating the mailbox %s failed!';
|
$PALANG['pCreate_mailbox_result_error'] = 'Creating the mailbox %s failed!';
|
||||||
$PALANG['pCreate_mailbox_result_success'] = 'The mailbox %s has been added to the mailbox table.';
|
$PALANG['pCreate_mailbox_result_success'] = 'The mailbox %s has been added to the mailbox table.';
|
||||||
$PALANG['pCreate_mailbox_result_succes_nosubfolders'] = 'The mailbox %s has been added to the mailbox table, but none (or only some) of the predefined sub-folders could be created.';
|
$PALANG['pCreate_mailbox_result_succes_nosubfolders'] = 'The mailbox %s has been added to the mailbox table, but none (or only some) of the predefined sub-folders could be created.';
|
||||||
$PALANG['mailbox_updated'] = "The mailbox %s has been updated.";
|
$PALANG['mailbox_updated'] = "The mailbox %s has been updated.";
|
||||||
$PALANG['mailbox_update_failed'] = "Updating the mailbox %s failed!";
|
$PALANG['mailbox_update_failed'] = "Updating the mailbox %s failed!";
|
||||||
|
|
||||||
$PALANG['pEdit_mailbox_welcome'] = 'Edit a mailbox for your domain.';
|
$PALANG['pEdit_mailbox_welcome'] = 'Edit a mailbox for your domain.';
|
||||||
@ -180,11 +180,9 @@ $PALANG['pPassword_result_success'] = 'The password for %s has been changed.';
|
|||||||
|
|
||||||
$PALANG['pPassword_recovery_title'] = 'Follow the instructions to reset your password.';
|
$PALANG['pPassword_recovery_title'] = 'Follow the instructions to reset your password.';
|
||||||
$PALANG['pPassword_recovery_button'] = 'Send me the code';
|
$PALANG['pPassword_recovery_button'] = 'Send me the code';
|
||||||
$PALANG['pPassword_recovery_email_body'] = "Hello,\n\nUse the following link to change your email password:\n%s\n\nRegards,\n\n" . $CONF['admin_name'];
|
$PALANG['pPassword_recovery_email_body'] = "Hello,\n\nUse the following link to change your email password :\n%s\n\nRegards,\n\n" . $CONF['admin_name'];
|
||||||
$PALANG['pPassword_recovery_email_sent'] = 'An email was sent to:';
|
|
||||||
$PALANG['pPassword_recovery_sms_body'] = "Hello,\nThe code to change your password is: %s\n" . $CONF['admin_name'];
|
$PALANG['pPassword_recovery_sms_body'] = "Hello,\nThe code to change your password is: %s\n" . $CONF['admin_name'];
|
||||||
$PALANG['pPassword_recovery_sms_sent'] = 'An SMS was sent to:';
|
$PALANG['pPassword_recovery_processed'] = "We processed your request. If you entered a valid username, you'll receive an email/SMS with a password code.";
|
||||||
$PALANG['pPassword_recovery_no_alternative'] = 'No alternative contact info were found. Please contact the support at ' . $CONF['admin_email'] . 'or by phone to ' . $CONF['admin_phone'];
|
|
||||||
$PALANG['pPassword_password_code'] = 'Code sent by email/SMS';
|
$PALANG['pPassword_password_code'] = 'Code sent by email/SMS';
|
||||||
$PALANG['pPassword_code_text_error'] = 'Invalid code';
|
$PALANG['pPassword_code_text_error'] = 'Invalid code';
|
||||||
|
|
||||||
|
@ -179,10 +179,8 @@ $PALANG['pPassword_result_success'] = 'Le mot de passe de %s a été changé !';
|
|||||||
$PALANG['pPassword_recovery_title'] = 'Suivez les instructions pour réinitialiser votre mot de passe.';
|
$PALANG['pPassword_recovery_title'] = 'Suivez les instructions pour réinitialiser votre mot de passe.';
|
||||||
$PALANG['pPassword_recovery_button'] = 'Envoyez-moi le code';
|
$PALANG['pPassword_recovery_button'] = 'Envoyez-moi le code';
|
||||||
$PALANG['pPassword_recovery_email_body'] = "Bonjour,\n\nUtilisez le lien suivant pour modifier votre mot de passe :\n%s\n\nSalutations,\n\n" . $CONF['admin_name'];
|
$PALANG['pPassword_recovery_email_body'] = "Bonjour,\n\nUtilisez le lien suivant pour modifier votre mot de passe :\n%s\n\nSalutations,\n\n" . $CONF['admin_name'];
|
||||||
$PALANG['pPassword_recovery_email_sent'] = 'Un code a été envoyé à :';
|
|
||||||
$PALANG['pPassword_recovery_sms_body'] = "Bonjour,\nLe code pour modifier votre mot de passe: %s\n" . $CONF['admin_name'];
|
$PALANG['pPassword_recovery_sms_body'] = "Bonjour,\nLe code pour modifier votre mot de passe: %s\n" . $CONF['admin_name'];
|
||||||
$PALANG['pPassword_recovery_sms_sent'] = 'Un code a été envoyé par SMS à :';
|
$PALANG['pPassword_recovery_processed'] = "Nous avons traité votre demande. Si le nom d'utilisateur que vous avez saisi est valide, vous recevrez par e-mail/SMS un code de réinitialisation du mot de passe.";
|
||||||
$PALANG['pPassword_recovery_no_alternative'] = "Aucun moyen de contact alternatif n'a été trouvé. Contactez le support à " . $CONF['admin_email'] . ' ou par téléphone ' . $CONF['admin_phone'];
|
|
||||||
$PALANG['pPassword_password_code'] = 'Code reçu par email/SMS';
|
$PALANG['pPassword_password_code'] = 'Code reçu par email/SMS';
|
||||||
$PALANG['pPassword_code_text_error'] = 'Code invalide';
|
$PALANG['pPassword_code_text_error'] = 'Code invalide';
|
||||||
|
|
||||||
@ -339,7 +337,7 @@ $PALANG['pBroadcast_title'] = 'Envoyer un message général';
|
|||||||
$PALANG['pBroadcast_name'] = 'Votre nom';
|
$PALANG['pBroadcast_name'] = 'Votre nom';
|
||||||
$PALANG['pBroadcast_success'] = 'Votre message général a été envoyé.';
|
$PALANG['pBroadcast_success'] = 'Votre message général a été envoyé.';
|
||||||
$PALANG['pAdminMenu_broadcast_message'] = 'message général';
|
$PALANG['pAdminMenu_broadcast_message'] = 'message général';
|
||||||
$PALANG['pBroadcast_error_empty'] = 'Les champs "Nom", "Sujet" et "Message" ne peuvent pas être vides!';
|
$PALANG['pBroadcast_error_empty'] = 'Les champs "Nom", "Sujet" et "Message" ne peuvent pas être vides !';
|
||||||
$PALANG['broadcast_mailboxes_only'] = 'Only send to mailboxes'; # XXX
|
$PALANG['broadcast_mailboxes_only'] = 'Only send to mailboxes'; # XXX
|
||||||
$PALANG['broadcast_to_domains'] = 'Send to domains:'; # XXX
|
$PALANG['broadcast_to_domains'] = 'Send to domains:'; # XXX
|
||||||
$PALANG['pStatus_undeliverable'] = 'Non délivrable ';
|
$PALANG['pStatus_undeliverable'] = 'Non délivrable ';
|
||||||
|
@ -52,13 +52,8 @@ if ($_SERVER['REQUEST_METHOD'] == "POST")
|
|||||||
|
|
||||||
$h = new AdminHandler;
|
$h = new AdminHandler;
|
||||||
if ( $h->login($fUsername, $fPassword) ) {
|
if ( $h->login($fUsername, $fPassword) ) {
|
||||||
session_regenerate_id(true);
|
|
||||||
$_SESSION['sessid'] = array();
|
|
||||||
$_SESSION['sessid']['roles'] = array();
|
|
||||||
$_SESSION['sessid']['roles'][] = 'admin';
|
|
||||||
$_SESSION['sessid']['username'] = $fUsername;
|
|
||||||
|
|
||||||
$_SESSION['PFA_token'] = md5(uniqid(rand(), true));
|
init_session($fUsername, true);
|
||||||
|
|
||||||
# they've logged in, so see if they are a domain admin, as well.
|
# they've logged in, so see if they are a domain admin, as well.
|
||||||
|
|
||||||
|
@ -35,6 +35,8 @@ class AdminHandler extends PFAHandler {
|
|||||||
$domains_grouped = 'group_concat(domain)';
|
$domains_grouped = 'group_concat(domain)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$passwordReset = Config::read('forgotten_admin_password_reset');
|
||||||
|
|
||||||
$this->struct=array(
|
$this->struct=array(
|
||||||
# field name allow display in... type $PALANG label $PALANG description default / options / ...
|
# field name allow display in... type $PALANG label $PALANG description default / options / ...
|
||||||
# editing? form list
|
# editing? form list
|
||||||
@ -47,10 +49,6 @@ class AdminHandler extends PFAHandler {
|
|||||||
/*select*/ 'password as password2'
|
/*select*/ 'password as password2'
|
||||||
),
|
),
|
||||||
|
|
||||||
'phone' => pacol( 1, 1, 0, 'text', 'pCreate_mailbox_phone', 'pCreate_mailbox_phone_desc', ''),
|
|
||||||
|
|
||||||
'email_other' => pacol( 1, 1, 0, 'mail', 'pCreate_mailbox_email', 'pCreate_mailbox_email_desc', ''),
|
|
||||||
|
|
||||||
'superadmin' => pacol( 1, 1, 0, 'bool', 'super_admin' , 'super_admin_desc' , 0
|
'superadmin' => pacol( 1, 1, 0, 'bool', 'super_admin' , 'super_admin_desc' , 0
|
||||||
# TODO: (finally) replace the ALL domain with a column in the admin table
|
# TODO: (finally) replace the ALL domain with a column in the admin table
|
||||||
# TODO: current status: 'superadmin' column exists and is written when storing an admin with AdminHandler,
|
# TODO: current status: 'superadmin' column exists and is written when storing an admin with AdminHandler,
|
||||||
@ -78,6 +76,10 @@ class AdminHandler extends PFAHandler {
|
|||||||
' ) AS __domain on username = __domain_username'),
|
' ) AS __domain on username = __domain_username'),
|
||||||
|
|
||||||
'active' => pacol( 1, 1, 1, 'bool', 'active' , '' , 1 ),
|
'active' => pacol( 1, 1, 1, 'bool', 'active' , '' , 1 ),
|
||||||
|
'phone' => pacol( 1, $passwordReset, 0, 'text', 'pCreate_mailbox_phone', 'pCreate_mailbox_phone_desc', ''),
|
||||||
|
'email_other' => pacol( 1, $passwordReset, 0, 'mail', 'pCreate_mailbox_email', 'pCreate_mailbox_email_desc', ''),
|
||||||
|
'token' => pacol( 1, 0, 0, 'text', '' , '' ),
|
||||||
|
'token_validity' => pacol( 1, 0, 0, 'ts', '' , '' ),
|
||||||
'created' => pacol( 0, 0, 0, 'ts', 'created' , '' ),
|
'created' => pacol( 0, 0, 0, 'ts', 'created' , '' ),
|
||||||
'modified' => pacol( 0, 0, 1, 'ts', 'last_modified' , '' ),
|
'modified' => pacol( 0, 0, 1, 'ts', 'last_modified' , '' ),
|
||||||
);
|
);
|
||||||
|
@ -13,6 +13,7 @@ class MailboxHandler extends PFAHandler {
|
|||||||
|
|
||||||
# init $this->struct, $this->db_table and $this->id_field
|
# init $this->struct, $this->db_table and $this->id_field
|
||||||
protected function initStruct() {
|
protected function initStruct() {
|
||||||
|
$passwordReset = Config::read('forgotten_user_password_reset');
|
||||||
$this->struct=array(
|
$this->struct=array(
|
||||||
# field name allow display in... type $PALANG label $PALANG description default / options / ...
|
# field name allow display in... type $PALANG label $PALANG description default / options / ...
|
||||||
# editing? form list
|
# editing? form list
|
||||||
@ -35,11 +36,13 @@ class MailboxHandler extends PFAHandler {
|
|||||||
# read_from_db_postprocess() also sets 'quotabytes' for use in init()
|
# read_from_db_postprocess() also sets 'quotabytes' for use in init()
|
||||||
# TODO: read used quota from quota/quota2 table
|
# TODO: read used quota from quota/quota2 table
|
||||||
'active' => pacol( 1, 1, 1, 'bool', 'active' , '' , 1 ),
|
'active' => pacol( 1, 1, 1, 'bool', 'active' , '' , 1 ),
|
||||||
'phone' => pacol( 1, 1, 0, 'text', 'pCreate_mailbox_phone' , 'pCreate_mailbox_phone_desc' , ''),
|
|
||||||
'email_other' => pacol( 1, 1, 0, 'mail', 'pCreate_mailbox_email' , 'pCreate_mailbox_email_desc' , ''),
|
|
||||||
'welcome_mail' => pacol( $this->new, $this->new, 0, 'bool', 'pCreate_mailbox_mail' , '' , 1,
|
'welcome_mail' => pacol( $this->new, $this->new, 0, 'bool', 'pCreate_mailbox_mail' , '' , 1,
|
||||||
/*options*/ '',
|
/*options*/ '',
|
||||||
/*not_in_db*/ 1 ),
|
/*not_in_db*/ 1 ),
|
||||||
|
'phone' => pacol( 1, $passwordReset, 0, 'text', 'pCreate_mailbox_phone' , 'pCreate_mailbox_phone_desc' , ''),
|
||||||
|
'email_other' => pacol( 1, $passwordReset, 0, 'mail', 'pCreate_mailbox_email' , 'pCreate_mailbox_email_desc' , ''),
|
||||||
|
'token' => pacol( 1, 0, 0, 'text', '' , '' ),
|
||||||
|
'token_validity'=> pacol( 1, 0, 0, 'ts', '' , '' ),
|
||||||
'created' => pacol( 0, 0, 1, 'ts', 'created' , '' ),
|
'created' => pacol( 0, 0, 1, 'ts', 'created' , '' ),
|
||||||
'modified' => pacol( 0, 0, 1, 'ts', 'last_modified' , '' ),
|
'modified' => pacol( 0, 0, 1, 'ts', 'last_modified' , '' ),
|
||||||
# TODO: add virtual 'notified' column and allow to display who received a vacation response?
|
# TODO: add virtual 'notified' column and allow to display who received a vacation response?
|
||||||
|
@ -780,6 +780,55 @@ abstract class PFAHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate and store a unique password reset token valid for one hour
|
||||||
|
* @param string $username
|
||||||
|
* @return false|string
|
||||||
|
*/
|
||||||
|
function getPasswordRecoveryCode($username) {
|
||||||
|
if ($this->init($username)) {
|
||||||
|
$token = generate_password();
|
||||||
|
$table = table_by_key($this->db_table);
|
||||||
|
$updatedRows = db_update($table, $this->id_field, $username, array(
|
||||||
|
'token' => pacrypt($token),
|
||||||
|
'token_validity' => date("Y-m-d H:i:s", strtotime('+ 1 hour')),
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($updatedRows == 1) {
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify user's one time password reset token
|
||||||
|
* @param string $username
|
||||||
|
* @param string $token
|
||||||
|
* @return boolean true on success (i.e. code matches etc)
|
||||||
|
*/
|
||||||
|
public function checkPasswordRecoveryCode($username, $token) {
|
||||||
|
$username = escape_string($username);
|
||||||
|
|
||||||
|
$table = table_by_key($this->db_table);
|
||||||
|
$active = db_get_boolean(True);
|
||||||
|
$query = "SELECT token FROM $table WHERE " . $this->id_field . "='$username' AND token <> '' AND active='$active' AND NOW() < token_validity";
|
||||||
|
|
||||||
|
$result = db_query ($query);
|
||||||
|
if ($result['rows'] == 1) {
|
||||||
|
$row = db_array($result['result']);
|
||||||
|
$crypt_token = pacrypt($token, $row['token']);
|
||||||
|
|
||||||
|
if($row['token'] == $crypt_token) {
|
||||||
|
db_update($table, $this->id_field, $username, array(
|
||||||
|
'token' => '',
|
||||||
|
'token_validity' => '2000-01-01 00:00:00',
|
||||||
|
));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**************************************************************************
|
/**************************************************************************
|
||||||
* functions to read protected variables
|
* functions to read protected variables
|
||||||
|
@ -15,8 +15,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label"><label>{$PALANG.password}:</label></td>
|
<td class="label"><label>{$PALANG.password}:</label></td>
|
||||||
<td><input class="flat" type="password" name="fPassword" />{if $forgotten_password_reset}<br/>
|
<td><input class="flat" type="password" name="fPassword" />
|
||||||
<a href="password-recover.php">{$PALANG.pUsersLogin_password_recover}</a>{/if}</td>
|
{if $forgotten_password_reset}
|
||||||
|
<br/><a href="password-recover.php">{$PALANG.pUsersLogin_password_recover}</a>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label"><label>{$PALANG.pLogin_language}:</label></td>
|
<td class="label"><label>{$PALANG.pLogin_language}:</label></td>
|
||||||
|
12
upgrade.php
12
upgrade.php
@ -1684,16 +1684,14 @@ function upgrade_1836_mysql() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function upgrade_1837_mysql() {
|
function upgrade_1837() {
|
||||||
# alternative contact means to reset a forgotten password
|
# alternative contact means to reset a forgotten password
|
||||||
foreach(array('admin', 'mailbox') as $table_to_change) {
|
foreach(array('admin', 'mailbox') as $table_to_change) {
|
||||||
$table = table_by_key($table_to_change);
|
$table = table_by_key($table_to_change);
|
||||||
if(!_mysql_field_exists($table, 'phone')) {
|
_db_add_field($table, 'phone', "varchar(30) {UTF-8} NOT NULL DEFAULT ''", 'active');
|
||||||
db_query_parsed("ALTER TABLE `$table` ADD COLUMN `phone` varchar(30) NOT NULL DEFAULT ''");
|
_db_add_field($table, 'email_other', "varchar(255) {UTF-8} NOT NULL DEFAULT ''", 'phone');
|
||||||
}
|
_db_add_field($table, 'token', "varchar(255) {UTF-8} NOT NULL DEFAULT ''", 'email_other');
|
||||||
if(!_mysql_field_exists($table, 'email_other')) {
|
_db_add_field($table, 'token_validity', '{DATETIME}', 'token');
|
||||||
db_query_parsed("ALTER TABLE `$table` ADD COLUMN `email_other` varchar(255) NOT NULL DEFAULT ''");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# TODO MySQL:
|
# TODO MySQL:
|
||||||
|
@ -48,12 +48,9 @@ if ($_SERVER['REQUEST_METHOD'] == "POST")
|
|||||||
|
|
||||||
$h = new MailboxHandler();
|
$h = new MailboxHandler();
|
||||||
if($h->login($fUsername, $fPassword)) {
|
if($h->login($fUsername, $fPassword)) {
|
||||||
session_regenerate_id(true);
|
|
||||||
$_SESSION['sessid'] = array();
|
init_session($fUsername, false);
|
||||||
$_SESSION['sessid']['roles'] = array();
|
|
||||||
$_SESSION['sessid']['roles'][] = 'user';
|
|
||||||
$_SESSION['sessid']['username'] = $fUsername;
|
|
||||||
$_SESSION['PFA_token'] = md5(uniqid(rand(), true));
|
|
||||||
header("Location: main.php");
|
header("Location: main.php");
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Postfix Admin
|
* Postfix Admin
|
||||||
*
|
*
|
||||||
* LICENSE
|
* LICENSE
|
||||||
* This source file is subject to the GPL license that is bundled with
|
* This source file is subject to the GPL license that is bundled with
|
||||||
* this package in the file LICENSE.TXT.
|
* this package in the file LICENSE.TXT.
|
||||||
*
|
*
|
||||||
* Further details on the project are available at http://postfixadmin.sf.net
|
* Further details on the project are available at http://postfixadmin.sf.net
|
||||||
*
|
*
|
||||||
* @version $Id$
|
* @version $Id$
|
||||||
* @license GNU GPL v2 or later.
|
* @license GNU GPL v2 or later.
|
||||||
*
|
*
|
||||||
* File: password-change.php
|
* File: password-change.php
|
||||||
* Used by users and admins to change their forgotten login password.
|
* Used by users and admins to change their forgotten login password.
|
||||||
* Template File: password-change.tpl
|
* Template File: password-change.tpl
|
||||||
@ -34,20 +34,16 @@ if (preg_match('/\/users\//', $_SERVER['REQUEST_URI'])) {
|
|||||||
}
|
}
|
||||||
require_once($rel_path . 'common.php');
|
require_once($rel_path . 'common.php');
|
||||||
|
|
||||||
if ($context == 'admin' && !Config::read('forgotten_admin_password_reset') || $context == 'users' && !Config::read('forgotten_user_password_reset'))
|
if ($context === 'admin' && !Config::read('forgotten_admin_password_reset') || $context === 'users' && !Config::read('forgotten_user_password_reset')) {
|
||||||
{
|
die('Password reset is disabled by configuration option: forgotten_admin_password_reset');
|
||||||
header('HTTP/1.0 403 Forbidden');
|
|
||||||
exit(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'GET')
|
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||||
{
|
|
||||||
$tUsername = safeget('username');
|
$tUsername = safeget('username');
|
||||||
$tCode = safeget('code');
|
$tCode = safeget('code');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'POST')
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
{
|
|
||||||
if(safepost('fCancel')) {
|
if(safepost('fCancel')) {
|
||||||
header('Location: main.php');
|
header('Location: main.php');
|
||||||
exit(0);
|
exit(0);
|
||||||
@ -57,37 +53,33 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST')
|
|||||||
$fPassword2 = safepost('fPassword2');
|
$fPassword2 = safepost('fPassword2');
|
||||||
|
|
||||||
$tUsername = safepost('fUsername');
|
$tUsername = safepost('fUsername');
|
||||||
$tCode = trim(strtoupper(safepost('fCode')));
|
$tCode = trim(safepost('fCode'));
|
||||||
|
|
||||||
if (empty($fPassword) or ($fPassword != $fPassword2)) {
|
if (empty($fPassword) or ($fPassword != $fPassword2)) {
|
||||||
$error = true;
|
$error = true;
|
||||||
flash_error(Config::lang('pPassword_password_text_error'));
|
flash_error(Config::lang('pPassword_password_text_error'));
|
||||||
} elseif (trim(strtoupper($tCode) != getPasswordRecoveryCode($tUsername))) {
|
|
||||||
flash_error(Config::lang('pPassword_code_text_error'));
|
|
||||||
} else {
|
} else {
|
||||||
session_regenerate_id();
|
|
||||||
$_SESSION['sessid']['username'] = $tUsername;
|
$handler = $context === 'admin' ? new AdminHandler : new MailboxHandler;
|
||||||
if ($context == 'users') {
|
if (!$handler->checkPasswordRecoveryCode($tUsername, $tCode)) {
|
||||||
$_SESSION['sessid']['roles'][] = 'user';
|
flash_error(Config::lang('pPassword_code_text_error'));
|
||||||
$handler = new MailboxHandler;
|
|
||||||
} else {
|
} else {
|
||||||
$_SESSION['sessid']['roles'][] = 'admin';
|
|
||||||
$handler = new AdminHandler;
|
init_session($tUsername, $context === 'admin');
|
||||||
}
|
if (!$handler->init($tUsername)) {
|
||||||
if (!$handler->init($tUsername)) {
|
flash_error($handler->errormsg);
|
||||||
flash_error($handler->errormsg);
|
|
||||||
} else {
|
|
||||||
$values = $handler->result;
|
|
||||||
$values[$handler->getId_field()] = $tUsername;
|
|
||||||
$values['password'] = $fPassword;
|
|
||||||
$values['password2'] = $fPassword2;
|
|
||||||
if ($handler->set($values) && $handler->store()) {
|
|
||||||
flash_info(Config::lang_f('pPassword_result_success', $tUsername));
|
|
||||||
header('Location: ' . dirname($_SERVER['REQUEST_URI']) . '/main.php');
|
|
||||||
exit(0);
|
|
||||||
} else {
|
} else {
|
||||||
foreach($handler->errormsg as $msg) {
|
$values = $handler->result;
|
||||||
flash_error($msg);
|
$values['password'] = $fPassword;
|
||||||
|
$values['password2'] = $fPassword2;
|
||||||
|
if ($handler->set($values) && $handler->store()) {
|
||||||
|
flash_info(Config::lang_f('pPassword_result_success', $tUsername));
|
||||||
|
header('Location: main.php');
|
||||||
|
exit(0);
|
||||||
|
} else {
|
||||||
|
foreach($handler->errormsg as $msg) {
|
||||||
|
flash_error($msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,86 +34,62 @@ if (preg_match('/\/users\//', $_SERVER['REQUEST_URI'])) {
|
|||||||
}
|
}
|
||||||
require_once($rel_path . 'common.php');
|
require_once($rel_path . 'common.php');
|
||||||
|
|
||||||
if ($context == 'admin' && !Config::read('forgotten_admin_password_reset') || $context == 'users' && !Config::read('forgotten_user_password_reset'))
|
if ($context === 'admin' && !Config::read('forgotten_admin_password_reset') || $context === 'users' && !Config::read('forgotten_user_password_reset')) {
|
||||||
{
|
die('Password reset is disabled by configuration option: forgotten_admin_password_reset');
|
||||||
header('HTTP/1.0 403 Forbidden');
|
|
||||||
exit(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendCodebyEmail($to, $username, $code)
|
function sendCodebyEmail($to, $username, $code) {
|
||||||
{
|
|
||||||
$fHeaders = "To: " . $to . PHP_EOL;
|
|
||||||
$fHeaders .= "From: " . Config::read('admin_email') . PHP_EOL;
|
|
||||||
$fHeaders .= "Subject: " . encode_header(Config::Lang('pPassword_welcome')) . PHP_EOL;
|
|
||||||
$fHeaders .= "MIME-Version: 1.0" . PHP_EOL;
|
|
||||||
$fHeaders .= "Content-Type: text/plain; charset=utf-8" . PHP_EOL;
|
|
||||||
$fHeaders .= "Content-Transfer-Encoding: 8bit" . PHP_EOL . PHP_EOL;
|
|
||||||
|
|
||||||
$url = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['REQUEST_URI']) . '/password-change.php?username=' . urlencode($username) . '&code=' . $code;
|
$url = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['REQUEST_URI']) . '/password-change.php?username=' . urlencode($username) . '&code=' . $code;
|
||||||
$fHeaders .= Config::lang_f('pPassword_recovery_email_body', $url);
|
|
||||||
|
|
||||||
return smtp_mail($to, Config::read('admin_email') , $fHeaders);
|
return smtp_mail($to, Config::read('admin_email'), Config::Lang('pPassword_welcome'), Config::lang_f('pPassword_recovery_email_body', $url));
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendCodebySMS($to, $username, $code)
|
function sendCodebySMS($to, $username, $code) {
|
||||||
{
|
|
||||||
$text = Config::lang_f('pPassword_recovery_sms_body', $code);
|
$text = Config::lang_f('pPassword_recovery_sms_body', $code);
|
||||||
|
|
||||||
$url = 'https://api.clickatell.com/http/sendmsg?api_id=' . Config::read('clickatell_api_id') . '&user=' . Config::read('clickatell_user') . '&password=' . Config::read('clickatell_password') . "&to=$to" . '&from=' . Config::read('clickatell_sender') . '&text=' . urlencode($text);
|
|
||||||
|
|
||||||
$result = file_get_contents($url);
|
if (Config::read('sms_send_function') && is_callable(Config::read('sms_send_function'))) {
|
||||||
|
$result = call_user_func(Config::read('sms_send_function'), $to, $text);
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
|
||||||
return $result !== false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == "POST")
|
if ($_SERVER['REQUEST_METHOD'] === "POST") {
|
||||||
{
|
$start_time = microtime();
|
||||||
$tUsername = escape_string (safepost('fUsername'));
|
$tUsername = escape_string (safepost('fUsername'));
|
||||||
$table = table_by_key($context == 'users' ? 'mailbox' : 'admin');
|
$handler = $context === 'admin' ? new AdminHandler : new MailboxHandler;
|
||||||
$result = db_query("SELECT * FROM `$table` WHERE username='$tUsername'");
|
$token = $handler->getPasswordRecoveryCode($tUsername);
|
||||||
$eMessage = '';
|
if ($token !== false) {
|
||||||
if ($result['rows'] == 1)
|
|
||||||
{
|
$table = table_by_key($context === 'users' ? 'mailbox' : 'admin');
|
||||||
|
$result = db_query("SELECT * FROM `$table` WHERE username='$tUsername'");
|
||||||
$row = db_array($result['result']);
|
$row = db_array($result['result']);
|
||||||
$code = getPasswordRecoveryCode($tUsername);
|
|
||||||
|
|
||||||
$email_other = trim($row['email_other']);
|
$email_other = trim($row['email_other']);
|
||||||
$phone = trim($row['phone']);
|
$phone = trim($row['phone']);
|
||||||
|
|
||||||
// An active session is required to propagate flash messages to redirected page
|
if ($email_other) {
|
||||||
if ($email_other)
|
sendCodeByEmail($email_other, $tUsername, $token);
|
||||||
{
|
|
||||||
// send email
|
|
||||||
if (sendCodeByEmail($email_other, $tUsername, $code))
|
|
||||||
{
|
|
||||||
flash_info(Config::Lang('pPassword_recovery_email_sent') . ' ' . $email_other);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($phone)
|
|
||||||
{
|
|
||||||
// send phone
|
|
||||||
if (sendCodeBySMS($phone, $tUsername, $code))
|
|
||||||
{
|
|
||||||
flash_info(Config::Lang('pPassword_recovery_sms_sent') . ' ' . $phone);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($email_other || $phone)
|
if ($phone) {
|
||||||
{
|
sendCodeBySMS($phone, $tUsername, $token);
|
||||||
// session_regenerate_id();
|
}
|
||||||
|
|
||||||
|
if ($email_other || $phone) {
|
||||||
header("Location: password-change.php?username=" . $tUsername);
|
header("Location: password-change.php?username=" . $tUsername);
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
flash_error(Config::Lang('pPassword_recovery_no_alternative'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
// throttle password reset requests to prevent brute force attack
|
||||||
flash_error(Config::Lang('pCreate_mailbox_username_text_error1'));
|
$elapsed_time = microtime() - $start_time;
|
||||||
|
if ($elapsed_time < 2 * pow(10, 6)) {
|
||||||
|
usleep(2 * pow(10, 6) - $elapsed_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flash_info(Config::Lang('pPassword_recovery_processed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$smarty->assign ('language_selector', language_selector(), false);
|
$smarty->assign ('language_selector', language_selector(), false);
|
||||||
|
Reference in New Issue
Block a user