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`) |
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
- Pick an expiration date for the hash, like 5 minutes from now:
Fri Jan 08 00:24:23 UTC 2010
- Start with a JSON hash of data:
{"email":"rick@entp.com","expires":"Fri Jan 08 00:24:23 UTC 2010"}
- 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:
- Remove any newlines:
s.gsub(/\n/, '')
. - Remove trailing
=
characters:s.gsub(/\=$/, '')
. - Convert any
+
characters to-
:s.gsub(/\+/, '-')
- 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.
- You'll want to setup a login url in your Site Settings with
something like: `http://yourapp.com/multipass?to=http://help.yourapp.com
-
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)}"
-
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", 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