HTTP authentication with sfGuard

Your application problably provides web services like a REST/SOAP API or RSS feeds. Such services are often publicly available. Nevertheless, you probably want to restrict access to these features, as the rest of the application.

Since web services are meant to be programatically callable, you cannot use the standard form/post way. One common solution is to to use HTTP authentication.

That said, it could be a good idea to use the same user restriction logic for that method.

About forms

In symfony 1.1 and above, the sfGuard authentification logic has moved to the bundled signin form.

One of the goal of the new form system, which is a full framework by itself, is to enable reuse of components.
It means that the new form object now embeds the request, validation and error handling.

To enable a HTTP authentication form, we simply need to customize the signin form presentation, and enable HTTP as symfony’s request parameters.

Example

This example assumes that you have a symfony application running with sfGuard installed and set up.

The first thing to do is to override the default signin action of the sfGuardAuth module in the application :

~apps/
 |~api/
 | |+config/
 | |+i18n/
 | |+lib/
 | `~modules/
 |   |+rss/
 |   `~sfGuardAuth/
 |     |~actions/
 |     | `-actions.class.php

In the signin methode, we set 401 status code to the response and add the WWW_Authenticate header, to enable HTTP authentification. Then, we simply map HTTP values to the request parameters expected by sfGuard. If user is validated, we redirect the user to the requested page :

class sfGuardAuthActions extends sfActions
{
  public function executeSignin($request)
  {
    $user = $this->getUser();

    if ($user->isAuthenticated())
    {
      return $this->redirect('@homepage');
    }

    $message = 'Authentification required';

    $this->form = new sfGuardFormSignin;

    if (isset($_SERVER['PHP_AUTH_USER']))
    {
      $request->setParameter('signin', array(
        'username' =>$_SERVER['PHP_AUTH_USER'],
        'password' =>$_SERVER['PHP_AUTH_PW'],
      ));

      $this->form->bind($request->getParameter('signin'));
      if ($this->form->isValid())
      {
        $values = $this->form->getValues();
        $this->getUser()->signin($values['user']);

        return $this->redirect($request->getUri());
      }
      else
      {
        $message = $this->form->getErrorSchema();
      }
    }

    $header_message = "Basic realm=\"$message\"";

    $this->getResponse()->setStatusCode(401);
    $this->getResponse()->setHttpHeader('WWW_Authenticate', $header_message);

    return sfView::NONE;
  }
}

You can see that we reuse sfGuard’s error messages in the HTTP header, to get the exact same authentification logic as a normal login form:

The same behavior can be observed using a HTTP request with parameters encoded directly into the URL:

$ curl -v "http://login:pass@apps.localhost/api/api_dev.php/index"
* About to connect() to apps.localhost port 80 (#0)
*   Trying 127.0.0.1... connected
* Connected to apps.localhost (127.0.0.1) port 80 (#0)
* Server auth using Basic with user 'as'
> GET /api/api_dev.php/index HTTP/1.1
> Authorization: Basic YXM6
> User-Agent: curl/7.18.2 (i386-apple-darwin8.11.1) libcurl/7.18.2 zlib/1.2.3
> Host: apps.localhost
> Accept: */*
>
< HTTP/1.1 401 Unauthorized

< Server: Apache/2.0.59 (Unix) PHP/5.2.5 DAV/2
< X-Powered-By: PHP/5.2.5
< Set-Cookie: symfony=2711f6b7440442a3746fc38771072ca7; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
< Pragma: no-cache
* Authentication problem. Ignoring this.
< Www-Authenticate: Basic realm="password [Required.]
username [The username and/or password is invalid.]"
< Content-Length: 0
< Content-Type: text/html; charset=utf-8
<
* Connection #0 to host apps.localhost left intact
* Closing connection #0
About these ads

  1. yitzi

    hi, thanks alot for this page – it really helps!
    I’m having a slight problem though, I’ve overridden the actions.class.php file as instructed and the login works fine, problem is logging out – it doesn’t. regardless of what i set @homepage to it goes there but stays logged in. I tried moving the signout method to the new actions.class.php but to no avail. any ideas?

  2. nicolas.martin

    I think the issue is related to the fact that both global variables $_SERVER['PHP_AUTH_USER'] and $_SERVER['PHP_AUTH_PW'] remains set event after the signOut() execution.

    sfGuard does not handle HTTP natively, so it may have something to do with the sfGuardUser class.

    If you have any solution though, feel free to post it. I will update the post to add this missing part.

  3. yitzi

    That sounds right, I do see those two set even after I logout, however how to get rid of them?? I tried unset() and settinf them to ” but somehow they keep on coming back? where is symfony hiding these? is it a browser thing?

  4. yitzi

    After further research into HTTP Authentication it seems HTTP does not support a typical “logout” this is the way I got around that:

    apps/backend/modules/sfGuardAuth/actions/actions.class.php:

    public function executeSignout($request)
    {
    $this->getUser()->signOut();

    $signout_url = sfConfig::get(‘app_sf_guard_plugin_success_signout_url’, $request->getReferer());
    if(isset($_SERVER['PHP_AUTH_USER']) || isset($_SERVER['PHP_AUTH_PW'])){

    $this->getResponse()->setHttpHeader(‘WWW-Authenticate’, ‘Basic realm=Secret’);
    $this->getResponse()->setHttpHeader(‘status’, ‘401 Unauthorized’);
    $this->redirect(‘http://nouser:nopass@localhost/…….’);
    }else{

    $this->redirect(” != $signout_url ? $signout_url : ‘@homepage’);
    }
    }

    //return sfView::SUCCESS ;
    }

    The only problem with it is that it asks for credentials again, not pretty, but it works.
    Thanks for your help.

  5. Frank

    Hi Nicolas, thanks for this nice article. I do not use the sfGuardPlugin but i need the HTTP Auth mechanism and had some problems in the implementation.
    I did this stuff in the preExecute action of an custom parent actions class at first. This worked quite well the first steps, but you loose the benefits of sfUser and the whole credentials system in this way.

    Your mentioned approach does only work, if the session storage parameter “auto_start” is enabled. The last redirect call will end up in an endless loop, because symfony will loose the session. I want to write a stateless and sessionless API and wonder if this may work in any case. Otherwise the clients may wonder, why the are recieving a cookie…

    A solution would be to switch from an redirect to an forward call, but therefore you have to analyze the uri and ask the routing about the needed module and action. Quite hacky i think.

    • nicolas.martin

      Feel free to push this implementation one step further and publish it !
      I would be definitely interested in reading a universal way to achieve an HTTP authentication !

  6. lost_and_unfound

    This is such a nice implementation, especially for your own REST / API.

    Problem I had… curl does not play to well with Symfony 1.4 (dont know about previous versions) when using ->redirect().

    The http header uses / responds with 302 and location.

    I used your script and modified it to work very nicely with curl and produce a full http header output and not just the meta redirection. This also auths against sfDoctrineGuard and uses the form (bind) to do the auth, however, the settings.yml _csrf_ token needs to be disabled.


    getUser();

    if ($user->isAuthenticated())
    return $this->redirect(‘@homepage’);

    $message = ‘Gimme pass';

    $this->form = new sfGuardFormSignin;
    $this->form->disableCSRFProtection();

    if (isset($_SERVER['PHP_AUTH_USER']))
    {
    $request->setParameter(‘signin’, array(
    ‘username’ => $_SERVER['PHP_AUTH_USER'],
    ‘password’ => $_SERVER['PHP_AUTH_PW'],
    ));

    $this->form->bind($request->getParameter(‘signin’));
    if ($this->form->isValid())
    {
    $values = $this->form->getValues();
    $this->getUser()->signin($values['user']);

    $module = $request->getParameter(‘module’);
    $action = $request->getParameter(‘action’);

    return $this->forward($module, $action);
    }
    else
    $message = $this->form->getErrorSchema();
    }

    $header_message = ‘Basic realm=”‘ . $message . ‘”‘;

    $this->getResponse()->setStatusCode(401);
    $this->getResponse()->setHttpHeader(‘WWW_Authenticate’, $header_message);

    return sfView::NONE;
    }

    }

  1. 1 Writing functional tests for HTTP auth secured actions « ESL Developer Blog

    [...] for finding the correct lines. The HTTP auth mechanism works like it is explained in the blog post HTTP authentication with sfGuard, although the sfGuard plugin is not used in our application. This secure action takes the [...]




Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s



Follow

Get every new post delivered to your Inbox.

%d bloggers like this: