Uncategorized

PHP login handling tutorial – sessions & cookies included

I see a lot of people trying to write code to authenticate users logging into a PHP website. This is some code I cobbled together in december of 2008 and it has worked problem free since then.

// I've already sanitized all GET, POST, and COOKIE data at this point.
function check_login() {
  global $DB;

  $login_justnow=false;

  // if the user isn't logged in and they're POSTing a login request, process it
  if(!get_session('user/id') && isset($_POST['login']) {
    $name=$_POST['login_name'];  $remember_me=isset($_POST['remember_me'])?1:0;
    $pass=md5($_POST['login_pass']);
    $user_id=$DB->QueryXY("SELECT id FROM `users` WHERE name='$name' AND pass='$pass' AND confirmed='1' LIMIT 1");
    if(isset($user_id)) {
      account_login($user_id,$remember_me);
      $login_justnow=true;
      $name=get_session("user/given_name")?', '.get_session("user/given_name"):'';
      add_notice("Welcome$name!");
    } else {
      add_error("Login failed.");
      account_logout();
    }
  }

  // if the user isn't logged in but has a COOKIE, process it
  if(!get_session("user/id") && isset($_COOKIE["remember_me"]) ) {
    list($user_id,$cookie_code)=@unserialize(stripslashes($_COOKIE["remember_me"]));
    if(isset($user_id) && isset($cookie_code)) {
      $cookie=md5($cookie_code);
      $result=$DB->QueryArray("SELECT * FROM `users` WHERE id='$user_id' AND cookie='$cookie' AND confirmed='1' LIMIT 1");
      if(count($result)) {
        account_login($user_id,true);
        $login_justnow=true;
        $name=get_session("user/given_name")?', '.get_session("user/given_name"):'';
        add_notice("Welcome$name!");
      }
    }
  }

  // if the user's session says they're logged in, process it
  if(get_session("user/id") && $login_justnow===false) {
    $user_id=get_session("user/id");
    $cookie=get_session("user/cookie");
    $ip=get_session("user/ip");
    $session=session_id();

    $query="SELECT * FROM `users` WHERE id='$user_id' AND ip='$ip' AND session='$session' AND cookie='$cookie' AND confirmed='1' LIMIT 1";
    $result=$DB->DoQuery($query);

    if($DB->NumRows($result)) {
      account_login($user_id,false);
    } else {
      add_error("Session security failed.");
      account_logout();
    }
    $DB->EndQuery($result);
  }

  // check if the user actually has rights to this part of the site - your implementation may vary
}

function account_login($user_id,$remember_me) {
  global $DB;

  if(user_is_logged_in())
    return;

  // update cookie
  if($remember_me==true) {
    $cookie_code=generate_random_string();
    $cookie_str=serialize(array($user_id, $cookie_code));
    setcookie('remember_me', $cookie_str, time() + 60*60*24*30, '/');
    add_session("user/cookie",$cookie_code);
  } else {
    remove_session("user/cookie");
  }

  // update session security
  $ip=$_SERVER['REMOTE_ADDR'];
  $session=session_id();
  $cookie_code=get_session("user/cookie");
  $cookie=md5($cookie_code);
  $DB->DoQuery("UPDATE `users` SET last_on=NOW(), session='$session'"
    .(($cookie_code!='')?", cookie='".$cookie."'":"")
    .", ip='$ip' WHERE id='$user_id' LIMIT 1");

  // update session info
  $result=$DB->DoQuery("SELECT * FROM `users` WHERE id='$user_id' LIMIT 1");
  $row=$DB->FetchAssoc($result);
  foreach($row as $k=>$v) {
    add_session("user/".$k,$v);
  }
  $DB->EndQuery($result);

  // check if any other part of your system needs to know about a user logging in.
}

function user_is_logged_in() {
 return get_session("user/id")!=0;
}

function generate_random_string($length=32) {
 $random="";
 srand((double)microtime()*1000000);
 $char_list = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 $char_list.= "abcdefghijklmnopqrstuvwxyz";
 $char_list.= "1234567890";

 for($i=0;$i<$length;++$i) {
 $random.=substr($char_list,(rand()%(strlen($char_list))), 1);
 }

 return $random;
}
Uncategorized

Updating PHP cookies without breaking the experience

PHP cookies are a great idea for storing data temporarily. Sure, they have their security problems but (arguably) they do more good than bad. The trickiest part about cookies is when you update your PHP and it forces you to change the contents of your cookies. You don’t want your faithful site visitors to see an error message! Consider the following naive implementation:

function save() {
  setcookie("username","foo",60*60*24*30);
}

function load() {
  return isset($_COOKIE["username"])?$_COOKIE["username"]:'';
}

It doesn’t take a lot of imagination to see how this could break. Let’s start by making it easier to store lots of info in a single cookie.

function save() {
  $data=array('username'=>'foo', 'something else'=>'bar');
  $str=serialize($data);
  setcookie("userdata",$str,60*60*24*30);
}

function load() {
  $data=array('username'=>'','something else'=>'');
  if(isset($_COOKIE['userdata'])) {
     $data=unserialize($_COOKIE['userdata']);
  }
  return $data;
}

Ok, but what about if we need to add more data to our cookies later? Everyone with an old cookie will be missing parts of $data and we’ll have to catch that.

function save() {
  $data=array('version'='1.0', 'username'=>'foo', 'something else'=>'bar');
  $str=serialize($data);
  setcookie("userdata",$str,60*60*24*30);
}

function load() {
  $data=array('username'=>'','something else'=>'');
  $old=false;
  if(isset($_COOKIE['userdata'])) {
    try {
      $data=unserialize($_COOKIE['userdata']);
    }
    catch(Exception $e) {
      $old=true;  // this version is so old & busted we can't read it at all.
    }
    if(!$old) {
      // Use the $data['version'] to help us figure out what pieces of $data need to be filled in.
    }
  }
  return $data;
}

And… voila!