An Edge Case of Time in AWS PHP SDK

When Amazon Web Services rolled out their version 4 signature we started seeing sporadic errors on a few projects when we created pre-authenticated link to S3 resources with a relative timestamp. Trying to track down the errors wasn’t easy. It seemed that it would occur rarely while executing the same exact code. Our code was simply to get a pre-authenticated URL that would expire in 7 days, the max duration V4 signatures are allowed to be valid. The error we’d get was “The expiration date of a signature version 4 presigned URL must be less than one week”. Weird, we kept passing in “7 days” as the expiration time. After the error occurred a couple of times over a few weeks I decided to look into it.

The code throwing the error was located right in the SignatureV4 class. The error is thrown when the end timestamp minus the start timestamp for the signature was greater than a week. Looking through the way the timestamps were generated it went something like this:

  1. Generate the start timestamp as current time for the signature assuming one is not passed.
  2. Do a few other quick things not related to this problem.
  3. Do a check to insure that the end minus start timestamp is less than a week in seconds.

So a rough example with straight PHP could of the above steps for a ‘7 days’ expiration would be as follows:

<?php
// Generate the start timestamp
$start = time();

// Do other stuff....

// If the end timestamp is over 7 weeks in seconds throw an error
if( strtotime('+7 days') - $start > 86400) {
  throw new \Exception('must be less than 7 weeks');
}

Straight forward enough, right? the problem lies when a second “rolls” between generating the $start and the end timestamp check. For example, if you generate the $start at 2017-08-20 12:01:01.999999. Let’s say this gets assigned the timestamp of 2017-08-20 12:01:01. Then the check for the 7 weeks occurs at 2017-08-27 12:01:02.0000 it’ll throw an exception as duration between the start and end it’s actually now for 86,401 seconds total. It turns outs triggering this error is easier than you’d think. Run this script locally:

<?php
while(true){
    $start = time();
    // Sleeps below are not even necessary.
    // time_nanosleep(0,1);
    //or can use usleep(1); 
    $end = strtotime('1 week');
    if($end-$start > 604800)
    {
        echo "Failed!! ".date('Y-m-d H:i:s',$start).' vs '.date('Y-m-d H:i:s',$end).' start: '.$start.' end: '.$end;
        exit;
    }
}

That will throw an exception within a few seconds of running most likely.

After I figured out the error, the next step was to submit a issue to make sure I’m not misunderstanding how the library should be used. The simplest fix for me was to generate the end expiration timestamp before generating the start timestamp. After I made the PR, Kevin S. from AWS pointed out that while this fixed the problem, the duration still wasn’t guaranteed to always be the same for the same relative time period. For example, if you created 1000 presigned URLs all with ‘+7 days’ as the valid period, some may be 86400 in duration others may be 86399. This isn’t a huge problem, but Kevin made a great point that we could solve the problem by locking the relative timestamp for the end based on the start timestamp. After adding that to the PR it was accepted. As of release 3.32.4 the fix is now included in the SDK.

Musing: Thoughts on Seattle vs. Uber and an AirBnB orgy

In the last two weeks, there’s been two frontpage stories coming out of the “sharing economy” space. First up, news broke that Seattle passed new regulation to limit the number of drivers that Uber or Lyft can have on the road at any time, effectively hamstringing both services. Then, over the weekend a story started circulating about an Airbnb stay gone awry involving an orgy, Twitter, and of course the cops. While the stories are wildly different, they both sit at the intersection of the new “sharing economy” and the role of government regulation. Because of this, both stories sparked an intense debate everywhere from The Wall Street Journal to Hacker News. The attitudes and viewpoints of the discourse were interesting and revealing about people’s attitudes towards regulation in the taxi and hospitality space.

The opinion towards the new regulations in Seattle were overwhelmingly negative, ranging from claims of stifling innovation to accusations of outright corruption. Empirically, it seems that most people, even outside of early adopters, have generally positive feelings about Uber and Lyft. It might be because of the horrible experiences people have had in normal cabs or because of the perception of hackney companies as entrenched monopolies but I think it’s primarily due to people’s perception of risk surrounding Uber or Lyft. As a non-user, the perception of how “risky” it is to have Uber drivers operating in your city is probably near zero. Given how small a percentage of total drivers they’ll make up and that “licensed cab drivers” typically already operate in the area, I don’t think many people feel threatened by having additional drivers potentially ferrying people around their city. Because of this, it’s been easier for people to take hold of the “sharing economy” narrative where Lyft or Uber who were empowered to make a living are suddenly shut out by corrupt, entrenched interests.

Contrast this with the reactions from the same people to the Airbnb story, which ranged from disgust that someone would rent out their condo to strangers through feelings that Airbnb should be held liable for the actions of an independent third party. There’s no argument that the Airbnb story is significantly more disturbing, but it isn’t the first time something like this has happened and it certainly won’t be the last. So why such a different, visceral reaction? It’s perceived risk. As a non-user, the perceived risk to having Airbnb operate in your area is undeniably significant. Take a typical condo apartment building where every occupant is either a member of a condo association or a vetted rental tenant. Introducing the possibility of short term, “random occupants” certainly sounds risky and unnerving to anyone living in the building. Anyone considering the situation immediately evaluates worst case scenarios, “what if they’re criminals?” or “drug dealers?” and so on. Compared to driving, where interactions with strangers is a given, introducing “random interactions” into a scenario where it’s unexpected seems to push people towards favoring legislation.

As a whole, the emergence of the new “sharing economy” is probably a net positive. Despite that, companies are certainly thumbing their nose at government regulation and entrenched players which is going to cause ruffled feathers along the way. To win the hearts and minds, companies will definitely need to manage the perception of how risky their services are both to users and non-users alike.

Symfony2: Logging users in programatically with Doctrine2 and FOSUserBundle

Recently I was doing a fairly common task on Symfony2, logging in a user programatically. Often applications do this on registration, via auto login links, complex login forms, etc. This time I was using an auto login link that expires that users get via email. I came across the issue that it seemed the first time the page loaded I was logged in properly but then as soon as I redirect or navigated anywhere I was logged out.

Here is the basic workflow we were using:

  1. Create an auto login link, basically just an entity which had an expiration date and a special url hash
  2. When clicked forward to action which gets the related entity, and then retrieves the user.
  3. Login the user programatically
  4. Redirect to whatever we want them to see

The issue was somewhere between step 3 and 4 something was amiss, if I eliminated step 3 the profiler toolbar showed I was properly logged in as expect, as soon as I redirect it showed me as unauthenticated. Here is the code for the most part:

<?php

    /**
     * @Route("/login-token/{token}/{userHash}", name="participant_autologin_token")
     * @ParamConverter("autologinToken", class="MyBundle:AutologinToken")
     * @Template()
     */
    public function autoLoginAction(Request $request, AutologinToken $autologinToken, $userHash){
        
        $csrf_token = $this->container->get('form.csrf_provider')->generateCsrfToken('authenticate');
        $now = new \DateTime();
        
        if( $autologinToken->getExpiresAt() != NULL && $autologinToken->getExpiresAt() < $now ){
            return array("csrf_token" => $csrf_token, "last_username" => "", "nextUrl" => $autologinToken->getUrl());
        }        
        
        if( $autologinToken->getUser()->getAutologinToken() != $userHash ){
            return array("csrf_token" => $csrf_token, "last_username" => "", "error" => "Sorry! There is a problem with your link. Please contact support.");    
        }
        
        $user = $autologinToken->getUser();

        $providerKey = $this->container->getParameter('fos_user.firewall_name');
        $token = new UsernamePasswordToken($user, null, $providerKey, $user->getRoles());
        $this->get('security.context')->setToken($token);
        
        $event = new InteractiveLoginEvent($request, $token);
        $this->get("event_dispatcher")->dispatch("security.authentication", $event);

        return $this->redirect( $autologinToken->getUrl() );     
    }

Fairly simple, used a ParamConverter to convert the incoming request to an entity. After a while of troubleshooting, I noticed that the ‘$user’ in this case wasn’t an actual Entity, it was a ProxyClass that Doctrine2 had generated. I had read that Doctrine2 ProxyClasses when serialized don’t properly bring over some of their attributes, namely the ID. This caused an issue with FOSUserBundle as the UserProvider looks up the user by their ID. Since the ID was blank this kept causing it to not find my user on the next page load.

There are a number of ways you can fix this, two that come to mind is to override the ‘refreshUser’ method of the UserProvider to look up by username as that is properly serialized from Proxy objects. Instead, as this was only for this one action and I wanted to be more efficient I switched the query to do a join to the user from the get go. This means when you do getUser Doctrine will return the actual Entity and not a Proxy class. Here is my update annotation:

<?php
    /**
     * @Route("/login-token/{token}/{userHash}", name="participant_autologin_token")
     * @ParamConverter("autologinToken", class="MyBundle:AutologinToken", options={"repository_method" = "findByTokenJoinWithUser"})
     * @Template()
     */
    public function autoLoginAction(Request $request, AutologinToken $autologinToken, $userHash){
    ....
    }

For more on how to use joins and entity repository specific consult the current manual.

Good luck!

HTTPs, Reverse Proxys, and Port 80!?

Recently we were getting ready to deploy a new project which functions only over SSL. The project is deployed on AWS using the Elastic Load Balancers (ELB). We have the ELB doing the SSL termination to reduce the load on the server and to help simply management of the SSL certs. Anyways the the point of this short post. One of the developers noticed that on some of the internal links she kept getting a link something like “https://dev.app.com:80/….”, it was properly generating the link to HTTPS but then specify port 80. Of course your browser really does not like that as its conflicting calls of port 80 and 443. After a quick look into the project we found that we had yet to enable the proxy headers and specify the proxy(s), it was we had to turn on trust_proxy_headers. However, doing this did not fix the issue. You must in addition to enable the headers specify which ones you trust. This can be easily done via the following:

<?php

use Symfony\Component\ClassLoader\ApcClassLoader;
use Symfony\Component\HttpFoundation\Request;

$loader = require_once __DIR__.'/../app/bootstrap.php.cache';

// Use APC for autoloading to improve performance.
// Change 'sf2' to a unique prefix in order to prevent cache key conflicts
// with other applications also using APC.
/*
$loader = new ApcClassLoader('sf2', $loader);
$loader->register(true);
*/

require_once __DIR__.'/../app/AppKernel.php';
//require_once __DIR__.'/../app/AppCache.php';

$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
//$kernel = new AppCache($kernel);
$request = Request::createFromGlobals();

Request::setTrustedProxies(array('192.168.1.1'));

// If it is only available via the proxy:
//Request::setTrustedProxies(array($request->server->get('REMOTE_ADDR')));

$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

Here is a very simple example of how you could specify them. You just let it know the IP’s of the proxy(s) and it will then properly generate your links.

You can read up on this more in the Symfony documentation on trusting proxies.

Anyways just wanted to put throw this out there incase you see this and realize you forgot to configure the proxy in your app!

Symfony2 and Impersonating Users, a Heads Up

Recently I was working on a project in which it admins were able to impersonate other users. It’s a fairly easy task to add to Symfony2, merely adding a switch_user reference to your firewall can make it possible, consult the Symfony docs for more on that. One thing I noticed was that every now and then when testing I would get weird errors after switching between multiple users, however it didn’t always happen. After some digging around, it turns out when you switch user it does not clear that sessions attributes, ie if you set attribute ‘hello’ to value ‘world’ it would persist after you’ve impersonated another user. This caused a few issues as on this application we used the session to store a few things like which set of database connections you currently use.

After looking at the SecurityBundle configuration setup it was clear that there wasn’t any options to have it clear all session attributes on switch user. At this point it was clear I needed to use an event listener as the firewall dispatched the SwitchUserEvent when a user successfully switched user. Below is an excerpt from my services.yml

services:
    my.login_listener:
        class: Setfive\DemoBundle\Listener\LoginListener
        tags:
            - { name: kernel.event_listener, event: security.switch_user, method: onSecuritySwitchUser }

This makes it so that it will call the following code on a successful impersonation of a user:

<?php
namespace Setfive\DemoBundle\Listener;

use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;

class LoginListener
{
  
    /**
     * Fired on switch user, you can remove attributes or whatever you want here.
     * @param SwitchUserEvent $event
     */
    public function onSecuritySwitchUser(SwitchUserEvent $event)
    {
        $session = $event->getRequest()->getSession();
        $session->remove('partThatShouldNotCarryOver');
        
    }
}

It’s as simple as that, you can get the actual user by calling $event->getTargetUser(). Long story short, the session can have some tainted values when using switch user as all attributes are not cleared.