Authentication Model | Password Recovery

Leaving the question of password recovery alone for a second, is this enough for handling login and sign-up processes? There are still two non-technical problems that need to be solved.

Problem One. Every controller that changes persistent data is a potential security vulnerability, so you must always check that the request was willingly sent by an authorized user. Checking that he is authorized is a simple matter (just look at the session) as long as you don’t forget to do so (proper model design helps). Checking that it was sent willingly is harder: an XSRF attack (cross-site request forgery) consists in inserting a bit of code in a page on another server that sends a GET or POST request to your server. When an user that is authenticated and authorized on your server visits that page, the code automatically sends the request without asking him. A typical example is a log-out page “http://mydomain/logout” which logs out the user. I can insert code on my page that visits that address automatically: “<img src=’http://mydomain/logout’/>“. So, every user on your website that visits my page is logged out.

To prevent XSRF attacks, a classic approach is to look at the referrer: the page from which the request originates. If a request comes from a page where the user can willingly ask for the operation to be performed, it is allowed. Otherwise, the operation displays a page that explains a confirmation is required, with a confirm button that re-submits the request. If the user’s browser does not have correct support for referrer information, this will be annoying, but then again most browsers dohave correct support, so this is ultimately your decision.

Problem Two. The user sessions do expire, which means sometimes users will be trying to perform an operation without being authenticated. This, of course, will lead them back to the login screen and lose whatever they were doing.

The smart thing to do, of course, is to remember what they were doing (the GET data if they were trying to read something, and the POST data if they were doing something else) before they are sent to the login screen. Once they log in, they are redirected to either the original GET controller or, if they were doing a POST, to a “confirm” GET screen which explains that they were trying to do something before they were interrupted, and that they can click a button to finish what they were doing.

The two problems are tied, and their solutions will share a lot of code, namely:

  • Remembering the entire request and storing it safely for later reuse.
  • Displaying an explanation page about what they were doing and whether they want to do it or give up and return to the home page instead.
  • Generating a form that is able to submit the request again on demand.

The proposed architecture for solving this is the following:

  • A new function RequestUtils::Save(), returns a complete representation of the request being used, and a new function RequestUtils::Load() displays an HTML form for performing that request at will.
  • When a controller detects that it will not be able to satisfy a request (because there is an XSRF risk or because the user is not authenticated), it stores the request in $_SESSION['confirm/request:'.uniqid()] and remembers the unique identifier, then redirects either to “/login?next=uid” or “/confirm?next=uid“. While it would be possible to store all data in the session and leave the URLs clean, a given user may have several requests active at any point in time, so one must make sure no accidental overwriting (and therefore, data loss) happens.
  • The confirm controller extracts the request data from the session (as well as the description). If the request was a GET, a plain redirect happens. If the request was a POST, the confirm controller displays the description and a form that can repeat the request. The user can then click the “confirm” button (which posts the request again) or click a link to return to the home page.

The sad part is that there’s no clean way to represent a request completely: ideally, one would have to save the entire session data, the entire database, in addition to user-sent data, for things to behave as expected. Since this is impossible, some amount of sacrifice will have to be accepted. This is where our initial decision of avoiding persistent data as much as possible comes into play: most requests should not depend on session data or database data to configure their operation, all that data should instead be submitted as GET or POST data. An exception is when heavy amounts of data are stored, which will be done by storing the data in the SESSION at a unique identifier that is then transmitted through GET/POST.

Therefore, we ultimately only care about saving all POST data and all GET data. This leads to the RequestUtils::Save and RequestUtils::Load functions below:

 public static function Save($me$description)
  {
      $id uniqid();
      $data = array('me' => $me, 
                    'desc' => $description,
                    'get' => $_GET,
                    'post' => $_POST, 
                    'method' => $_SERVER['REQUEST_METHOD']);

      SessionUtils::Write('confirm', array($id => $data));
      return $id;
  }

  public static function Load($id)
  {
      $data SessionUtils::Read('confirm', array($id));
      if (!isset($data[$id]))
          return false;
      $data $data[$id];

      $action DomainConfig::Url($data['me'], $data['get']);

      if ($data['method'] == 'GET')
          return $action;
      
      return array($action$data['desc'], $data['post']);
  }

The former saves the operation to be performed to a session and returns an identifier intended to allow retrieval of that session. The latter loads the data from the session, then returns a redirect address (for GET requests) and a description and POST data (for POST requests).

Note that this approach does not handle file uploads. This is to be expected: we don’t want to accept file uploads on the server for arbitrary users that are not logged in. If an user wants to upload a file, they will have to log in first (although arguably we can solve the problem with JavaScript or Flash, which is probably what we will end up doing for uploads anyway).

The next step is to build the confirm controller:

<?php // controllers/confirm.php

  $data RequestUtils::Read(array('id'), $_GET);

  if (!isset($data['id'])) {
      RedirectUtils::SeeOther(DomainConfig::Url('/'));
  }

  $saved RequestUtils::Load($data['id']);

  if (!isset($saved)) {
      RedirectUtils::SeeOther(DomainConfig::Url('/'));
  }

  if (is_string($saved)) {
      RedirectUtils::SeeOther($saved);
  }

  ConfirmView::Render($saved[0], $saved[1], $saved[2]);

And the confirm view, used by the confirm controller:

<?php // views/confirm.php

  class ConfirmView
  {
      public static function Render($action$description, array $post)
      {
          $lazy = new LazyObj('ConfirmView::RenderContents'$action$description$post);
          PlainPageView::Render('Please Confirm''confirm'$lazy);
      }

      public static function RenderContents($action$description, array $post)
      {
          $home DomainConfig::Url('/');

          echo $description;
?><p>Is this what you want to do?</p><?php         
?><form action="<?=$action?>" method="POST"><?php

          foreach ($post as $key => $value) {
              $key htmlspecialchars($key);
              $value htmlspecialchars($value);

?><input type="hidden" name="<?=$key?>" value="<?=$value?>"/><?php
          }

?><button type="submit">Confirm</button> or <?php         
?><a href="<?=$home?>"return to the main page</a>.<?php       
?></form><?php

      }
  }

That is done. So, now, every time we want to ask the user to confirm a certain operation (or resume an operation after logging in) we can redirect them to the confirm controller with the appropriate suspended request identifier (the one returned by “save”).

However, having the request identifier survive all the way through the login process requires passing it around. So, for instance, it has to be added to the set of data used by the login page view. In LoginPageView::RenderLoginForm :

$login_url DomainConfig::Url('/do-login',
                               array('next' => $values['next']));

$reset_url DomainConfig::Url('/reset-password',
                               array('for'  => $values['login_mail'],
                                     'next' => $values['next']));

Similarly, in LoginPageView::RenderSignupForm :

$signup_url DomainConfig::Url('/do-signup',
                                array('next' => $values['next']));

And in controllers/login.php:

$next RequestUtils::Read(array('next'), $_GET);
$values['next'] = $next['next'];

The modifications for the do-signup and do-login servers are slightly more complex, yet similar, since they have to forward the data if errors happen, and resume the request if they succeed. Both undergo the same modifications, in the on_error function:

  function on_error($errors)
  {
      SessionUtils::Write('login/errors'$errors);
      $next RequestUtils::Read(array('next'), $_GET);
      RedirectUtils::SeeOther(DomainConfig::Url('/login'$next));
  }

And on the final line, which is preceded by:

  $next RequestUtils::Read(array('next'), $_GET);
  if ($next['next']) {
      $url DomainConfig::Url('/confirm', array('id' => $next['next']));
      RedirectUtils::SeeOther($url);
  }

For practical purposes, we can add two utility functions for handling the redirects appropriately:

  • RequestUtils::RequiresAuthentication checks whether the user is authenticated, and otherwise redirects to the login page with a suspended request.
  • RequestUtils::AllowFrom checks the referer against the provided list, and otherwise redirects to the confirm page with a suspended request. Only applies to POST requests, as GET requests are assumed to be innocuous (and they should be).

These functions look like this:

  public static function RequiresAuthentication($me$desc)
  {
      $check Sessionutils::Read('authentication', array('token'));
      if (!isset($check['token'])) {
          $save self::Save($me$desc);
          RedirectUtils::SeeOther(DomainConfig::Url('/login', array('next' => $save)));
      }
  }

  public static function AllowFrom($referrers$me$desc)
  {
      $referrer $_SERVER['HTTP_REFERER'];
      list($referrer, ) = explode('?'$referrer2);

      if ($referrer == DomainConfig::Url('/confirm'))
          return;
      if ($_SERVER['REQUEST_METHOD'] != 'POST')
          return;
      if (in_array($referrer$referrers))
          return;

      $save self::Save($me$desc);
      RedirectUtils::SeeOther(DomainConfig::Url('/confirm', array('id' => $save)));
  }

The key point is, do not forget to use these functions wherever necessary. They will usually be placed at the top of the protected controllers. For instance, in my do-login controller:

  RequestUtils::AllowFrom(array(DomainConfig::Url('/login')),
                          '/do-login', 
                          '<p>You are trying to log into JITBrain.</p>');

Authentication Model | Password Recovery

0 Responses to “11. Delayed Requests”


  1. No Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>



1150 feed subscribers
(readers who polled a feed this week)