SSO

Please note that the Expensify API is not accepting new applications at this time. Enterprise customers looking to integrate with Expensify should visit the Integration Server documentation.

All of the the Expensify APIs use a common "single signon" (SSO) framework comprising four pieces of information, along with an optional mechanism for securely communicating this in encrypted form when over an insecure channel.

Note: Jump down to "Constructing the SSO Object" to skip right to the meat.

SSO Components

This framework relies on four pieces of information:

  • partnerName
  • partnerPassword
  • partnerUserID
  • partnerUserSecret

The first two are assigned to you when you register for API access: partnerName is typically your domain name, and partnerPassword is just a random string. The first can be shared publicly, and the second should be kept secure. The others are described below.

partnerUserID

The partnerUserID is the unique ID by which you identify the user in your system. This is often just the user's email address (eg, "dbarrett@expensify.com"), but is sometimes a condensed nickname (eg, "dbarrett"), or even just a serial number. Its only requirement is that it be unique and unchanging. It needs to be unique so there is no confusion as to which of your users is accessing Expensify. And it needs to be unchanging such that the link established between the user's account and Expensify is never disrupted accidentally. (For example, if you use the user's email address as their partnerUserID, and if you then allow the user to change their email address, we won't be able to find the corresponding Expensify account the next you call our API.) The partnerUserID will be displayed to the user in their Expensify settings page.

partnerUserSecret

The partnerUserSecret is some bit about the user that only you know. This is used to authenticate your access to this user's account in the future. Its only requirement is that it be confidential and unchanging. It needs to be confidential such that no attacker could guess the value (assuming they first somehow compromised your partnerPassword). And it needs to be unchanging such that even if the user (for example) changes their password, you will still be able to authenticate to their Expensify account. The ideal partnerUserSecret is a simple random number that you store with their account and tell nobody.

Communicating SSO over Insecure Channels

Care must be taken to securely manage two of the SSO elements: partnerPassword and partnerUserSecret. Should one or both of those become known to an attacker, they could access private user data by impersonating the user, or even impersonating you. The most important element of this security is using encrypted communication channels. Accordingly, the only way to access the Expensify web services API is over HTTPS (HTTP over SSL). This provides effective protection against network eavesdropping and (so long as you take care to validate our Expensify serverside certificate) man-in-the-middle attacks. However, there are two major scenarios in which this isn't enough:

  • The first scenario is when you use the whitelabel API to direct users to a skinned version of the Expensify website, or even embed Expensify into your website. In either case, the user can access the original URL by viewing the HTML source of the webpage, or even by just looking at the browser address bar. Without additional precautions, your partnerPassword would be plainly visible to your users.

  • The second case scenario is when you use AJAX to access the web services API. Even though the URL is not visible to the user, the JavaScript code that constructs the URL is -- and somewhere in that code must be your partnerPassword. Without additional precautions, your partnerPassword could be obtained by analyzing your JavaScript.

(And there are other scenarios, such as if you can't disable URL logging on your server application engine, or if you maintain a compartmentalized development and deployment environment.)

For these scenarios and more, it's more appropriate to protect this sensitive information using an SSO object.

Constructing the SSO Object

The "SSO object" is simply a bin2hex() encoded, AES-256 encrypted JSON object with the following fields:

  • arandom: Any random number, to prevent replay attacks.
  • expires: A Unix timestamp set 5 minutes in the future from when you generate the SSO.
  • partnerPassword: (see above)
  • partnerUserSecret: (see above)

For example, a valid JSON SSO (unencrypted) could be:

{"arandom":329719,"expires":1242444603,"partnerPassword":"fh2ore872jd","partnerUserSecret":"s2inwn3h3j"}

This would be then be encrypted with the AES key you were provided, and then encoded using PHP's bin2hex() function. The result might look something like:

eyJleHBpcmF0aW9uIjoxMjQyNDQ0NjAzLCJ1c2VybmFtZSI6ImRiYXJyZXR0IiwicGFzc3dvcmQiOiJzMHMzY3IzdCIsImV
tYWlsIjoiZGJhcnJldHRAZXhwZW5zaWZ5LmNvbSJ9Cg==

You can supply an SSO object to any API call as an alternative to providing the partnerPassword or partnerUserSecret. For example, the two API calls are equivalent:

http://www.expensify.com/api
    ?command=GetReport
    &reportID=1
    &partnerName=yoursite.com
    &partnerUserID=dbarrett@expensify.com
    &partnerPassword=fh2ore872jd
    &partnerUserSecret=s2inwn3h3j

http://www.expensify.com/api
    ?command=GetReport
    &reportID=1
    &partnerName=yoursite.com
    &partnerUserID=dbarrett@expensify.com
    &sso=eyJleHBpcmF0aW9uIjoxMjQyNDQ0NjAzLCJ1c2VybmFtZSI6ImRiYXJyZXR0IiwicGFzc3dvcmQiOiJzMHMzY3
         IzdCIsImVtYWlsIjoiZGJhcnJldHRAZXhwZW5zaWZ5LmNvbSJ9Cg==

Both accomplish the same thing, except the second securely communicates the partnerPassword and partnerUserSecret.

Here's some sample PHP code to generate the SSO:

// Generate the JSON-encoding
$ssoData = array(
    "arandom"           => rand(),
    "expires"           => time() + 60*5,
    "partnerPassword"   => ...partner password...,
    "partnerUserSecret" => ...user password...
);
$clearText = json_encode( $ssoData );

// Create the SSO
$IV  = pack('H*',  ...iv...); // 128-bit (16-byte) iv
$AES = pack('H*',  ...key...); // 256-bit (32-byte) key
assert( $cipher = mcrypt_module_open( MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '' ) );
assert( mcrypt_generic_init( $cipher, $AES, $IV ) >= 0 );
$sso = mcrypt_generic( $cipher, $clearText );
mcrypt_generic_deinit( $cipher );