diff --git a/actions/login.php b/actions/login.php index c775fa6924..8ea3c800b7 100644 --- a/actions/login.php +++ b/actions/login.php @@ -76,15 +76,10 @@ class LoginAction extends Action { parent::handle($args); - $disabled = common_config('logincommand','disabled'); - $disabled = isset($disabled) && $disabled; - if (common_is_real_login()) { $this->clientError(_('Already logged in.')); } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { $this->checkLogin(); - } else if (!$disabled && isset($args['user_id']) && isset($args['token'])){ - $this->checkLogin($args['user_id'],$args['token']); } else { common_ensure_session(); $this->showForm(); @@ -103,46 +98,30 @@ class LoginAction extends Action function checkLogin($user_id=null, $token=null) { - if(isset($token) && isset($user_id)){ - //Token based login (from the LoginCommand) - $login_token = Login_token::staticGet('user_id',$user_id); - if($login_token && $login_token->token == $token){ - if($login_token->modified > time()+2*60){ - //token has expired - //delete the token as it is useless - $login_token->delete(); - $this->showForm(_('Invalid or expired token.')); - return; - }else{ - //delete the token so it cannot be reused - $login_token->delete(); - //it's a valid token - let them log in - $user = User::staticGet('id', $user_id); - //$user = User::staticGet('nickname', "candrews"); - } - }else{ - $this->showForm(_('Invalid or expired token.')); - return; - } - }else{ - // Regular form submission login + // XXX: login throttle - // XXX: login throttle + // CSRF protection - token set in NoticeForm + $token = $this->trimmed('token'); + if (!$token || $token != common_session_token()) { + $st = common_session_token(); + if (empty($token)) { + common_log(LOG_WARNING, 'No token provided by client.'); + } else if (empty($st)) { + common_log(LOG_WARNING, 'No session token stored.'); + } else { + common_log(LOG_WARNING, 'Token = ' . $token . ' and session token = ' . $st); + } - // CSRF protection - token set in NoticeForm - $token = $this->trimmed('token'); - if (!$token || $token != common_session_token()) { - $this->clientError(_('There was a problem with your session token. '. - 'Try again, please.')); - return; - } - - $nickname = $this->trimmed('nickname'); - $password = $this->arg('password'); - - $user = common_check_user($nickname, $password); + $this->clientError(_('There was a problem with your session token. '. + 'Try again, please.')); + return; } + $nickname = $this->trimmed('nickname'); + $password = $this->arg('password'); + + $user = common_check_user($nickname, $password); + if (!$user) { $this->showForm(_('Incorrect username or password.')); return; @@ -165,6 +144,7 @@ class LoginAction extends Action if ($url) { // We don't have to return to it again common_set_returnto(null); + $url = common_inject_session($url); } else { $url = common_local_url('all', array('nickname' => @@ -240,9 +220,9 @@ class LoginAction extends Action function showContent() { $this->elementStart('form', array('method' => 'post', - 'id' => 'form_login', - 'class' => 'form_settings', - 'action' => common_local_url('login'))); + 'id' => 'form_login', + 'class' => 'form_settings', + 'action' => common_local_url('login'))); $this->elementStart('fieldset'); $this->element('legend', null, _('Login to site')); $this->elementStart('ul', 'form_data'); @@ -255,7 +235,7 @@ class LoginAction extends Action $this->elementStart('li'); $this->checkbox('rememberme', _('Remember me'), false, _('Automatically login in the future; ' . - 'not for shared computers!')); + 'not for shared computers!')); $this->elementEnd('li'); $this->elementEnd('ul'); $this->submit('submit', _('Login')); diff --git a/actions/otp.php b/actions/otp.php new file mode 100644 index 0000000000..acf84aee81 --- /dev/null +++ b/actions/otp.php @@ -0,0 +1,145 @@ +. + * + * @category Login + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Allow one-time password login + * + * This action will automatically log in the user identified by the user_id + * parameter. A login_token record must be constructed beforehand, typically + * by code where the user is already authenticated. + * + * @category Login + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class OtpAction extends Action +{ + var $user; + var $token; + var $rememberme; + var $returnto; + var $lt; + + function prepare($args) + { + parent::prepare($args); + + if (common_is_real_login()) { + $this->clientError(_('Already logged in.')); + return false; + } + + $id = $this->trimmed('user_id'); + + if (empty($id)) { + $this->clientError(_('No user ID specified.')); + return false; + } + + $this->user = User::staticGet('id', $id); + + if (empty($this->user)) { + $this->clientError(_('No such user.')); + return false; + } + + $this->token = $this->trimmed('token'); + + if (empty($this->token)) { + $this->clientError(_('No login token specified.')); + return false; + } + + $this->lt = Login_token::staticGet('user_id', $id); + + if (empty($this->lt)) { + $this->clientError(_('No login token requested.')); + return false; + } + + if ($this->lt->token != $this->token) { + $this->clientError(_('Invalid login token specified.')); + return false; + } + + if ($this->lt->modified > time() + Login_token::TIMEOUT) { + //token has expired + //delete the token as it is useless + $this->lt->delete(); + $this->lt = null; + $this->clientError(_('Login token expired.')); + return false; + } + + $this->rememberme = $this->boolean('rememberme'); + $this->returnto = $this->trimmed('returnto'); + + return true; + } + + function handle($args) + { + parent::handle($args); + + // success! + if (!common_set_user($this->user)) { + $this->serverError(_('Error setting user. You are probably not authorized.')); + return; + } + + // We're now logged in; disable the lt + + $this->lt->delete(); + $this->lt = null; + + if ($this->rememberme) { + common_rememberme($this->user); + } + + if (!empty($this->returnto)) { + $url = $this->returnto; + // We don't have to return to it again + common_set_returnto(null); + } else { + $url = common_local_url('all', + array('nickname' => + $this->user->nickname)); + } + + common_redirect($url, 303); + } +} diff --git a/actions/register.php b/actions/register.php index 96015c2190..6981373467 100644 --- a/actions/register.php +++ b/actions/register.php @@ -259,6 +259,7 @@ class RegisterAction extends Action // Re-init language env in case it changed (not yet, but soon) common_init_language(); + $this->showSuccess(); } else { $this->showForm(_('Invalid username or password.')); diff --git a/classes/Login_token.php b/classes/Login_token.php index 746cd7f229..51dc61262e 100644 --- a/classes/Login_token.php +++ b/classes/Login_token.php @@ -40,6 +40,8 @@ class Login_token extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + const TIMEOUT = 120; // seconds after which to timeout the token + /* DB_DataObject calculates the sequence key(s) by taking the first key returned by the keys() function. In this case, the keys() function returns user_id as the first key. user_id is not a sequence, but @@ -52,4 +54,29 @@ class Login_token extends Memcached_DataObject { return array(false,false); } + + function makeNew($user) + { + $login_token = Login_token::staticGet('user_id', $user->id); + + if (!empty($login_token)) { + $login_token->delete(); + } + + $login_token = new Login_token(); + + $login_token->user_id = $user->id; + $login_token->token = common_good_rand(16); + $login_token->created = common_sql_now(); + + $result = $login_token->insert(); + + if (!$result) { + common_log_db_error($login_token, 'INSERT', __FILE__); + throw new Exception(sprintf(_('Could not create login token for %s'), + $user->nickname)); + } + + return $login_token; + } } diff --git a/classes/Status_network.php b/classes/Status_network.php index b3117640d8..776f6abb03 100644 --- a/classes/Status_network.php +++ b/classes/Status_network.php @@ -150,9 +150,19 @@ class Status_network extends DB_DataObject } if (!empty($sn)) { - if (!empty($sn->hostname) && 0 != strcasecmp($sn->hostname, $servername)) { - $sn->redirectToHostname(); + + // Redirect to the right URL + + if (!empty($sn->hostname) && + empty($_SERVER['HTTPS']) && + 0 != strcasecmp($sn->hostname, $servername)) { + $sn->redirectTo('http://'.$sn->hostname.$_SERVER['REQUEST_URI']); + } else if (!empty($_SERVER['HTTPS']) && + 0 != strcasecmp($sn->hostname, $servername) && + 0 != strcasecmp($sn->nickname.'.'.$wildcard, $servername)) { + $sn->redirectTo('https://'.$sn->nickname.'.'.$wildcard.$_SERVER['REQUEST_URI']); } + $dbhost = (empty($sn->dbhost)) ? 'localhost' : $sn->dbhost; $dbuser = (empty($sn->dbuser)) ? $sn->nickname : $sn->dbuser; $dbpass = $sn->dbpass; @@ -160,7 +170,11 @@ class Status_network extends DB_DataObject $config['db']['database'] = "mysqli://$dbuser:$dbpass@$dbhost/$dbname"; - $config['site']['name'] = $sn->sitename; + $config['site']['name'] = $sn->sitename; + + if (!empty($sn->hostname)) { + $config['site']['server'] = $sn->hostname; + } if (!empty($sn->theme)) { $config['site']['theme'] = $sn->theme; @@ -179,11 +193,8 @@ class Status_network extends DB_DataObject // (C) 2006 by Heiko Richler http://www.richler.de/ // LGPL - function redirectToHostname() + function redirectTo($destination) { - $destination = 'http://'.$this->hostname; - $destination .= $_SERVER['REQUEST_URI']; - $old = 'http'. (($_SERVER['HTTPS'] == 'on') ? 'S' : ''). '://'. diff --git a/lib/command.php b/lib/command.php index 5a1a8bf33a..f846fb823f 100644 --- a/lib/command.php +++ b/lib/command.php @@ -85,7 +85,7 @@ class NudgeCommand extends Command { $recipient = User::staticGet('nickname', $this->other); if(! $recipient){ - $channel->error($this->user, sprintf(_('Could not find a user with nickname %s.'), + $channel->error($this->user, sprintf(_('Could not find a user with nickname %s'), $this->other)); }else{ if ($recipient->id == $this->user->id) { @@ -96,7 +96,7 @@ class NudgeCommand extends Command } // XXX: notify by IM // XXX: notify by SMS - $channel->output($this->user, sprintf(_('Nudge sent to %s.'), + $channel->output($this->user, sprintf(_('Nudge sent to %s'), $recipient->nickname)); } } @@ -149,7 +149,7 @@ class FavCommand extends Command $notice = Notice::staticGet(substr($this->other,1)); if (!$notice) { - $channel->error($this->user, _('Notice with that id does not exist.')); + $channel->error($this->user, _('Notice with that id does not exist')); return; } $recipient = $notice->getProfile(); @@ -165,7 +165,7 @@ class FavCommand extends Command } $notice = $recipient->getCurrentNotice(); if (!$notice) { - $channel->error($this->user, _('User has no last notice.')); + $channel->error($this->user, _('User has no last notice')); return; } } @@ -214,7 +214,7 @@ class JoinCommand extends Command } if ($cur->isMember($group)) { - $channel->error($cur, _('You are already a member of that group.')); + $channel->error($cur, _('You are already a member of that group')); return; } if (Group_block::isBlocked($group, $cur->getProfile())) { @@ -231,12 +231,12 @@ class JoinCommand extends Command $result = $member->insert(); if (!$result) { common_log_db_error($member, 'INSERT', __FILE__); - $channel->error($cur, sprintf(_('Could not join user %1$s to group %2$s.'), + $channel->error($cur, sprintf(_('Could not join user %s to group %s'), $cur->nickname, $group->nickname)); return; } - $channel->output($cur, sprintf(_('%1$s joined group %2$s'), + $channel->output($cur, sprintf(_('%s joined group %s'), $cur->nickname, $group->nickname)); } @@ -281,12 +281,12 @@ class DropCommand extends Command $result = $member->delete(); if (!$result) { common_log_db_error($member, 'INSERT', __FILE__); - $channel->error($cur, sprintf(_('Could not remove user %1$s to group %2$s.'), + $channel->error($cur, sprintf(_('Could not remove user %s to group %s'), $cur->nickname, $group->nickname)); return; } - $channel->output($cur, sprintf(_('%1$s left group %2$s'), + $channel->output($cur, sprintf(_('%s left group %s'), $cur->nickname, $group->nickname)); } @@ -315,7 +315,7 @@ class WhoisCommand extends Command $whois = sprintf(_("%1\$s (%2\$s)"), $recipient->nickname, $recipient->profileurl); if ($recipient->fullname) { - $whois .= "\n" . sprintf(_('Full name: %s'), $recipient->fullname); + $whois .= "\n" . sprintf(_('Fullname: %s'), $recipient->fullname); } if ($recipient->location) { $whois .= "\n" . sprintf(_('Location: %s'), $recipient->location); @@ -355,7 +355,7 @@ class MessageCommand extends Command $this->text = common_shorten_links($this->text); if (Message::contentTooLong($this->text)) { - $channel->error($this->user, sprintf(_('Message too long - maximum is %1$d characters, you sent %2$d.'), + $channel->error($this->user, sprintf(_('Message too long - maximum is %d characters, you sent %d'), Message::maxContent(), mb_strlen($this->text))); return; } @@ -373,7 +373,7 @@ class MessageCommand extends Command $message = Message::saveNew($this->user->id, $other->id, $this->text, $channel->source()); if ($message) { $message->notify(); - $channel->output($this->user, sprintf(_('Direct message to %s sent.'), $this->other)); + $channel->output($this->user, sprintf(_('Direct message to %s sent'), $this->other)); } else { $channel->error($this->user, _('Error sending direct message.')); } @@ -396,7 +396,7 @@ class RepeatCommand extends Command $notice = Notice::staticGet(substr($this->other,1)); if (!$notice) { - $channel->error($this->user, _('Notice with that id does not exist.')); + $channel->error($this->user, _('Notice with that id does not exist')); return; } $recipient = $notice->getProfile(); @@ -412,19 +412,19 @@ class RepeatCommand extends Command } $notice = $recipient->getCurrentNotice(); if (!$notice) { - $channel->error($this->user, _('User has no last notice.')); + $channel->error($this->user, _('User has no last notice')); return; } } if($this->user->id == $notice->profile_id) { - $channel->error($this->user, _('Cannot repeat your own notice.')); + $channel->error($this->user, _('Cannot repeat your own notice')); return; } if ($recipient->hasRepeated($notice->id)) { - $channel->error($this->user, _('Already repeated that notice.')); + $channel->error($this->user, _('Already repeated that notice')); return; } @@ -432,7 +432,7 @@ class RepeatCommand extends Command if ($repeat) { common_broadcast_notice($repeat); - $channel->output($this->user, sprintf(_('Notice from %s repeated.'), $recipient->nickname)); + $channel->output($this->user, sprintf(_('Notice from %s repeated'), $recipient->nickname)); } else { $channel->error($this->user, _('Error repeating notice.')); } @@ -457,7 +457,7 @@ class ReplyCommand extends Command $notice = Notice::staticGet(substr($this->other,1)); if (!$notice) { - $channel->error($this->user, _('Notice with that id does not exist.')); + $channel->error($this->user, _('Notice with that id does not exist')); return; } $recipient = $notice->getProfile(); @@ -473,7 +473,7 @@ class ReplyCommand extends Command } $notice = $recipient->getCurrentNotice(); if (!$notice) { - $channel->error($this->user, _('User has no last notice.')); + $channel->error($this->user, _('User has no last notice')); return; } } @@ -488,7 +488,7 @@ class ReplyCommand extends Command $this->text = common_shorten_links($this->text); if (Notice::contentTooLong($this->text)) { - $channel->error($this->user, sprintf(_('Notice too long - maximum is %1$d characters, you sent %2$d.'), + $channel->error($this->user, sprintf(_('Notice too long - maximum is %d characters, you sent %d'), Notice::maxContent(), mb_strlen($this->text))); return; } @@ -497,7 +497,7 @@ class ReplyCommand extends Command array('reply_to' => $notice->id)); if ($notice) { - $channel->output($this->user, sprintf(_('Reply to %s sent.'), $recipient->nickname)); + $channel->output($this->user, sprintf(_('Reply to %s sent'), $recipient->nickname)); } else { $channel->error($this->user, _('Error saving notice.')); } @@ -529,7 +529,7 @@ class GetCommand extends Command } $notice = $target->getCurrentNotice(); if (!$notice) { - $channel->error($this->user, _('User has no last notice.')); + $channel->error($this->user, _('User has no last notice')); return; } $notice_content = $notice->content; @@ -553,7 +553,7 @@ class SubCommand extends Command { if (!$this->other) { - $channel->error($this->user, _('Specify the name of the user to subscribe to.')); + $channel->error($this->user, _('Specify the name of the user to subscribe to')); return; } @@ -581,7 +581,7 @@ class UnsubCommand extends Command function execute($channel) { if(!$this->other) { - $channel->error($this->user, _('Specify the name of the user to unsubscribe from.')); + $channel->error($this->user, _('Specify the name of the user to unsubscribe from')); return; } @@ -647,28 +647,20 @@ class LoginCommand extends Command $disabled = common_config('logincommand','disabled'); $disabled = isset($disabled) && $disabled; if($disabled) { - $channel->error($this->user, _('Login command is disabled.')); + $channel->error($this->user, _('Login command is disabled')); return; } - $login_token = Login_token::staticGet('user_id',$this->user->id); - if($login_token){ - $login_token->delete(); - } - $login_token = new Login_token(); - $login_token->user_id = $this->user->id; - $login_token->token = common_good_rand(16); - $login_token->created = common_sql_now(); - $result = $login_token->insert(); - if (!$result) { - common_log_db_error($login_token, 'INSERT', __FILE__); - $channel->error($this->user, sprintf(_('Could not create login token for %s.'), - $this->user->nickname)); - return; + + try { + $login_token = Login_token::makeNew($this->user); + } catch (Exception $e) { + $channel->error($this->user, $e->getMessage()); } + $channel->output($this->user, - sprintf(_('This link is useable only once, and is good for only 2 minutes: %s.'), - common_local_url('login', - array('user_id'=>$login_token->user_id, 'token'=>$login_token->token)))); + sprintf(_('This link is useable only once, and is good for only 2 minutes: %s'), + common_local_url('otp', + array('user_id' => $login_token->user_id, 'token' => $login_token->token)))); } } diff --git a/lib/router.php b/lib/router.php index 785e78fd05..6b87ed27f6 100644 --- a/lib/router.php +++ b/lib/router.php @@ -88,7 +88,10 @@ class Router $m->connect('doc/:title', array('action' => 'doc')); - $m->connect('main/login?user_id=:user_id&token=:token', array('action'=>'login'), array('user_id'=> '[0-9]+', 'token'=>'.+')); + $m->connect('main/otp/:user_id/:token', + array('action' => 'otp'), + array('user_id' => '[0-9]+', + 'token' => '.+')); // main stuff is repetitive diff --git a/lib/util.php b/lib/util.php index 23a22ad8cf..1237d718bd 100644 --- a/lib/util.php +++ b/lib/util.php @@ -171,15 +171,27 @@ function common_ensure_session() if (common_config('sessions', 'handle')) { Session::setSaveHandler(); } + if (array_key_exists(session_name(), $_GET)) { + $id = $_GET[session_name()]; + common_log(LOG_INFO, 'Setting session from GET parameter: '.$id); + } else if (array_key_exists(session_name(), $_COOKIE)) { + $id = $_COOKIE[session_name()]; + common_log(LOG_INFO, 'Setting session from COOKIE: '.$id); + } + if (isset($id)) { + session_id($id); + setcookie(session_name(), $id); + } @session_start(); if (!isset($_SESSION['started'])) { $_SESSION['started'] = time(); - if (!empty($c)) { + if (!empty($id)) { common_log(LOG_WARNING, 'Session cookie "' . $_COOKIE[session_name()] . '" ' . ' is set but started value is null'); } } } + common_debug("Session ID = " . session_id()); } // Three kinds of arguments: @@ -825,9 +837,39 @@ function common_path($relative, $ssl=false) } } + $relative = common_inject_session($relative, $serverpart); + return $proto.'://'.$serverpart.'/'.$pathpart.$relative; } +function common_inject_session($url, $serverpart = null) +{ + if (common_have_session()) { + + if (empty($serverpart)) { + $serverpart = parse_url($url, PHP_URL_HOST); + } + + $currentServer = $_SERVER['HTTP_HOST']; + + // Are we pointing to another server (like an SSL server?) + + if (!empty($currentServer) && + 0 != strcasecmp($currentServer, $serverpart)) { + // Pass the session ID as a GET parameter + $sesspart = session_name() . '=' . session_id(); + $i = strpos($url, '?'); + if ($i === false) { // no GET params, just append + $url .= '?' . $sesspart; + } else { + $url = substr($url, 0, $i + 1).$sesspart.'&'.substr($url, $i + 1); + } + } + } + + return $url; +} + function common_date_string($dt) { // XXX: do some sexy date formatting diff --git a/plugins/OpenID/finishopenidlogin.php b/plugins/OpenID/finishopenidlogin.php index 987fa92138..d25ce696cf 100644 --- a/plugins/OpenID/finishopenidlogin.php +++ b/plugins/OpenID/finishopenidlogin.php @@ -363,6 +363,7 @@ class FinishopenidloginAction extends Action if ($url) { # We don't have to return to it again common_set_returnto(null); + $url = common_inject_session($url); } else { $url = common_local_url('all', array('nickname' =>