Share your own site's authentication with Tender

There are two formats for passing your site's user details to Tender so have Tender automatically create a corresponding user profile: Multipass and HMAC cookies. Multipass tokens are the newer and preferred method. HMAC cookies are the older format.

Before using either of these methods, enable the API key under Account & Settings "Extras" and then under "Single Sign-On".

Multipass

A Multipass is simply a hash of keys and values, provided as an AES encrypted JSON hash. The keys are:

Name Value
name The name of the user (optional, also accepts `username` and `display_name`)
email The user's email address, e.g. user@gmail.com
unique_id A unique string identifying this user for just this Tender site. Usually this would be their unique ID in your system. Also accepts `guid`.
alternate_id Alternate unique ID if a matching user by the given `unique_id` is not found. This is good for updating a user's `unique_id` value without creating a new user profile.
trusted Boolean specifying whether the user is 'trusted'. Comments by trusted users are never checked for spam. Any user with at least 3 valid comments is automatically trusted.
expires Token expiry date. Example strftime format: "%a %b %d %H:%M:%S %Z %Y"
session_expires Expiry date of the resulting Tender session. Normally Tender SSO sessions end with the browser session, but this allows you to persist sessions. Example strftime format: "%a %b %d %H:%M:%S %Z %Y"
to Redirect the user to this URL after logging in. This is useful for 'bouncing' a user to Tender to log them in, then back to your own app. It will be fairly transparent to the user.

Those are the main values. Any other keys and values can be added for extra application-specific information. Common examples are a link to an administrative view of the user in your app. Any values ending in *_url are automatically linked. If you want to use a format that is also compatible with UserVoice, display_name and login are accepted in place of name. Also, be sure to add a guid field for UserVoice.

If you haven't been setting the unique_id attributes, you'll want to migrate your current Tender user data.

Once your JSON hash is constructed, you'll want to encrypt it with an AES key using your site key as a password, and your api key as a salt.

Special Development/Production Note

When passing unique IDs in SSO, it's a good idea to namespace them by including a prefix (e.g. prod-123, dev-123) etc. Otherwise you'll get conflicts between test/staging and production users.

Special Custom Domain Note

We recommend that support staff always use your tenderapp.com hostname with SSL. This will not raise certificate warnings. Since custom hostnames do not support SSL, it's fine for customers who want to use your public-facing support site, but not ideal for staff or administrators.

Building the Multipass

  1. Pick an expiration date for the hash, like 5 minutes from now: Fri Jan 08 00:24:23 UTC 2010
  2. Start with a JSON hash of data: {"email":"rick@entp.com","expires":"Fri Jan 08 00:24:23 UTC 2010"}
  3. Encrypt with AES and Base64 the resulting data:

Since these Base64 strings are being passed around the web, Tender prefers a URL-safe variant. However, if your Base64 string is properly escaped for URLs, it'll still work. Here's how the Base64 can be converted to the URL-safe variant:

  1. Remove any newlines: s.gsub(/\n/, '').
  2. Remove trailing = characters: s.gsub(/\=$/, '').
  3. Convert any + characters to -: s.gsub(/\+/, '-')
  4. Convert any / characters to _: s.gsub(/\//, '_')

You can use the Multipass debugger in the Extras section of your Tender dashboard if you need more help.

Encryption notes

If you are looking at your own implementation, here are a few useful details about the encryption process:

  • The ivec is the magic value "OpenSSL for Ruby"
  • You need to XOR the ivec against the first 16 bytes of the JSON payload
  • You should use CBC mode for AES with PKCS#7 padding
  • The AES key is the first 16 bytes of SHA1DIGEST(APIKey + SiteKey)

Quick Ruby example

There is a Multipass gem available on github that implements the encoding and decoding of Multipass strings. This is the same library that Tender uses.

  1. You'll want to setup a login url in your Site Settings with something like: `http://yourapp.com/multipass?to=http://help.yourapp.com
  2. In your app, you'll want to validate the user's login somehow, and redirect them to http://help.yourapp.com?sso=MULTIPASS. Be sure to CGI escape the multipass string if you build the URL manually.

    redirect_to "#{params[:to]}?sso=#{CGI.escape(current_user.multipass)}"
    
  3. A user model might look something like this:

  class User < ActiveRecord::Base
    # initialize the multipass object
    def self.multipass
      # for `yourapp.tenderapp.com`, `yourapp` is your SITE KEY
      @multipass ||= MultiPass.new('yourapp', 'API KEY')
    end

    # create a multipass for this user object
    def multipass
      self.class.multipass.encode(:email => email, :name => name, :expires => 30.minutes.from_now,
        :external_url => "http://yourapp.com/admin/users/#{id}")
    end
  end

PHP example

You should be able to use this PHP code to get multipass working.

<?php
$account_key = $_GET['site_key'];
$api_key     = $_GET['api_key'];

$salted = $api_key . $account_key;
$hash = hash('sha1',$salted,true);
$saltedHash = substr($hash,0,16);
$iv = "OpenSSL for Ruby";

// use an expires date in the future, of course
$user_data = array(
  "email" => 'test@example.com',
  "name" => 'test',
  "expires" => '2011-07-06 23:28:40Z'
);

if($_GET['email'])
  $user_data['email'] = $_GET['email'];
if($_GET['name'])
  $user_data['name'] = $_GET['name'];
if($_GET['expires'])
  $user_data['expires'] = $_GET['expires'];

$data = json_encode($user_data);
?>
<form>
  <div>
    <label>Site Key: <input type="text" name="site_key" value="<?= $account_key ?>" /></label><br />
    <label>API Key: <input type="text" name="api_key" value="<?= $api_key ?>" /></label><br />
    <label>Email <input type="text" name="email" value="<?= $user_data['email'] ?>" /></label><br />
    <label>Name <input type="text" name="name" value="<?= $user_data['name'] ?>" /></label><br />
    <label>Expires <input type="text" name="expires" value="<?= $user_data['expires'] ?>" /></label><br />
    <input type="submit" value="Check" />
  </div>
</form>

<pre><code>
User Data:
<? print_r($user_data); ?>

JSON:
<? print_r($data); ?>
</code></pre>

<?
if($_GET['site_key'] && $_GET['api_key']) {
  // double XOR first block
  for ($i = 0; $i < 16; $i++)
  {
   $data[$i] = $data[$i] ^ $iv[$i];
  }

  $pad = 16 - (strlen($data) % 16);
  $data = $data . str_repeat(chr($pad), $pad);
    
  $cipher = mcrypt_module_open(MCRYPT_RIJNDAEL_128,'','cbc','');
  mcrypt_generic_init($cipher, $saltedHash, $iv);
  $encryptedData = mcrypt_generic($cipher,$data);
  mcrypt_generic_deinit($cipher);
  $encryptedData = base64_encode($encryptedData);
  $encryptedData = preg_replace('/\=$/', '', $encryptedData);
  $encryptedData = preg_replace('/\n/', '', $encryptedData);
  $encryptedData = preg_replace('/\+/', '-', $encryptedData);
  $encryptedData = preg_replace('/\//', '_', $encryptedData);

?>
<pre><code>
Encrypted: ?sso=<?= urlencode($encryptedData) ?>
</code></pre>
<p><a href="http://<?= $account_key ?>.tenderapp.com?sso=<?= urlencode($encryptedData) ?>">login!</a></p>
<? } ?>

Other Languages

There is a repository of Tender-compatible Multipass/SSO implementations on GitHub, courtesy of UserVoice. This repository provides examples in several popular languages.

Still having problems?

If you're having issues, make sure you followed the above instructions carefully. For example, use CGI.escape on the return URL, not URI.encode (ruby).

Some PHP users report that the following function should be used to generate the expiry string:

date("r", strtotime("+30 minutes"));

HMAC Cookie Logins

If you would like to use your existing site's login cookies to authenticate or create users with Tender, you will need to set up your Tender to share the domain of your app; for example, help.yourapp.com or support.yourapp.com.

Your application will need to calculate the result of HMAC signing (using SHA1) the following string. You should do this for the user when they log in.

"help.yourapp.com/user@gmail.com/1228117891"

If the name field is given, sign the name at the end of the string:

"help.yourapp.com/user@gmail.com/1228117891/Ricky Bobby"

The number is the expiry of the login token, and is the number of seconds since epoch (generally, the int representation of a Time object).

We suggest using OpenSSL's HMAC implementation.

Language-specific implementations

Django

See this plugin: http://github.com/johnboxall/django-tenderize/tree/master

Ruby on Rails

If you are using Rails, it looks something like this: the token generator in the User model, and cookie-setting code in the controller (or in AuthenticatedSystem).

require 'openssl'
class User < ActiveRecord::Base

  def tender_token(expires)
    method = OpenSSL::Digest::Digest.new("SHA1")
    string = "help.yourapp.com/#{email}/#{expires}"
    OpenSSL::HMAC.hexdigest(method, TENDER_SECRET, string)
  end

end

class SessionsController

  def login
    if user = User.authenticate(params[:login], params[:password])
      expiry = 1.week.from_now.to_i
      cookies[:tender_email] = { :value => user.email, :domain => ".yourapp.com" }
      cookies[:tender_expires] = { :value => expiry.to_s, :domain => ".yourapp.com" }
      cookies[:tender_hash] = { :value => user.tender_token(expiry), :domain => ".yourapp.com" }
    end
    redirect_to "/"
  end

end

Of course you'll want to assign TENDER_SECRET in an initializer. The secret is a string that you'll find on your Site Settings tab under Tender's admin. Do NOT share this with anyone -- it will enable them to login to Tender as anyone.

Here is a summary of the cookie settings:

Name Value
tender_email The user's email address, e.g. user@gmail.com
tender_expires Token expiry date, integer (seconds since epoch), e.g. 1228117891
tender_hash The HMAC result of "host/email/expires"

Notes:

  • Since you're setting cookies on ".yourapp.com" you'll need to make sure you're not logging people in on "www.yourapp.com&quot;, because browser security settings won't let the user create a wildcard cookie.
  • Make sure your timestamps are integers like "1228117891"

To test if your code works, try signing this string

"help.yourapp.com/user@gmail.com/1228117891"

with the secret, "monkey". You should see this as the hash:

1937bf7e8dc9f475cc9490933eb36e5f7807398a