Note: This feature is only available to users with an Enterprise plan.

Overview

We provide Single Sign-On (SSO) capability to firms who wish to allow their users (e.g. members, clients, customers, franchisees, staff, etc.) to be seamlessly and securely logged into our dashboard without being prompted for a username and password.

For security, SSO is currently available only on client user accounts, not admin or staff accounts. If SSO has been enabled for your firm, you can enable SSO capability on individual accounts by toggling it on under Manage > Users > Edit:

sso-config

Note that when adding a new SSO user, if you choose “Has SSO enabled?” on user creation, the system skips the user confirmation email steps. SSO users don’t generally own their accounts directly or know/use their passwords, so this workflow supports Admin control over SSO users.

Authorization

SSO Access. To enable SSO on your account, you must be an Enterprise customer and request this feature as part of your package. Once SSO is enabled for your account, your primary admin and staff users will have the power to grant SSO access to any and all of your sub-users.

SSO Secret. For SSO credentials, we use an open standard (RFC 7519) called JSON Web Token (JWT). JWT is a compact and self-contained way for securely transmitting information between parties as a JSON object. JWTs can be verified and trusted because they are digitally signed. In our implementation, that must signed with the HMAC algorithm using a secret. You will be issued this secret for your accounts when SSO is enabled.

Signed Token. Your SSO secret allows you to generate a signed token with two pieces of data:

  1. the email of the user you want to login (the email used to identify that user in our system); and
  2. a timestamp to verify the token is not old and ensure that the session expires

While this approach makes use of standard algorithms that are part of any cryptography library, there are numerous free and open-source JWT libraries available that make generating the necessary token a snap.

Once you’ve generated a valid token, append it to our authorization URL and send your users along: they will be logged in with their designated access, but without needing to remember or enter a username or password. Make sure that the authorization URL uses your white-labeled domain or subdomain.

Example Code

A simple PHP example class for JWT:

<?php

/**
 * JSON Web Token implementation, based on this spec:
 * http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06
 *
 * PHP version 5
 *
 * @category Authentication
 * @package  Authentication_JWT
 * @author   Neuman Vong <neuman@twilio.com>
 * @author   Anant Narayanan <anant@php.net>
 * @license  http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
 * @link     https://github.com/firebase/php-jwt
 */
class JWT
{
  /**
   * Decodes a JWT string into a PHP object.
   *
   * @param string      $jwt    The JWT
   * @param string|null $key    The secret key
   * @param bool        $verify Don't skip verification process 
   *
   * @return object      The JWT's payload as a PHP object
   * @throws UnexpectedValueException Provided JWT was invalid
   * @throws DomainException          Algorithm was not provided
   * 
   * @uses jsonDecode
   * @uses urlsafeB64Decode
   */
  public static function decode($jwt, $key = null, $verify = true)
  {
    $tks = explode('.', $jwt);
    if (count($tks) != 3) {
      throw new UnexpectedValueException('Wrong number of segments');
    }
    list($headb64, $bodyb64, $cryptob64) = $tks;
    if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))) {
      throw new UnexpectedValueException('Invalid segment encoding');
    }
    if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64))) {
      throw new UnexpectedValueException('Invalid segment encoding');
    }
    $sig = JWT::urlsafeB64Decode($cryptob64);
    if ($verify) {
      if (empty($header->alg)) {
        throw new DomainException('Empty algorithm');
      }
      if ($sig != JWT::sign("$headb64.$bodyb64", $key, $header->alg)) {
        throw new UnexpectedValueException('Signature verification failed');
      }
    }
    return $payload;
  }

  /**
   * Converts and signs a PHP object or array into a JWT string.
   *
   * @param object|array $payload PHP object or array
   * @param string       $key     The secret key
   * @param string       $algo    The signing algorithm. Supported
   *                              algorithms are 'HS256', 'HS384' and 'HS512'
   *
   * @return string      A signed JWT
   * @uses jsonEncode
   * @uses urlsafeB64Encode
   */
  public static function encode($payload, $key, $algo = 'HS256')
  {
    $header = array('typ' => 'JWT', 'alg' => $algo);

    $segments = array();
    $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header));
    $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload));
    $signing_input = implode('.', $segments);

    $signature = JWT::sign($signing_input, $key, $algo);
    $segments[] = JWT::urlsafeB64Encode($signature);

    return implode('.', $segments);
  }

  /**
   * Sign a string with a given key and algorithm.
   *
   * @param string $msg    The message to sign
   * @param string $key    The secret key
   * @param string $method The signing algorithm. Supported
   *                       algorithms are 'HS256', 'HS384' and 'HS512'
   *
   * @return string          An encrypted message
   * @throws DomainException Unsupported algorithm was specified
   */
  public static function sign($msg, $key, $method = 'HS256')
  {
    $methods = array(
      'HS256' => 'sha256',
      'HS384' => 'sha384',
      'HS512' => 'sha512',
    );
    if (empty($methods[$method])) {
      throw new DomainException('Algorithm not supported');
    }
    return hash_hmac($methods[$method], $msg, $key, true);
  }

  /**
   * Decode a JSON string into a PHP object.
   *
   * @param string $input JSON string
   *
   * @return object          Object representation of JSON string
   * @throws DomainException Provided string was invalid JSON
   */
  public static function jsonDecode($input)
  {
    $obj = json_decode($input);
    if (function_exists('json_last_error') && $errno = json_last_error()) {
      JWT::_handleJsonError($errno);
    } else if ($obj === null && $input !== 'null') {
      throw new DomainException('Null result with non-null input');
    }
    return $obj;
  }

  /**
   * Encode a PHP object into a JSON string.
   *
   * @param object|array $input A PHP object or array
   *
   * @return string          JSON representation of the PHP object or array
   * @throws DomainException Provided object could not be encoded to valid JSON
   */
  public static function jsonEncode($input)
  {
    $json = json_encode($input);
    if (function_exists('json_last_error') && $errno = json_last_error()) {
      JWT::_handleJsonError($errno);
    } else if ($json === 'null' && $input !== null) {
      throw new DomainException('Null result with non-null input');
    }
    return $json;
  }

  /**
   * Decode a string with URL-safe Base64.
   *
   * @param string $input A Base64 encoded string
   *
   * @return string A decoded string
   */
  public static function urlsafeB64Decode($input)
  {
    $remainder = strlen($input) % 4;
    if ($remainder) {
      $padlen = 4 - $remainder;
      $input .= str_repeat('=', $padlen);
    }
    return base64_decode(strtr($input, '-_', '+/'));
  }

  /**
   * Encode a string with URL-safe Base64.
   *
   * @param string $input The string you want encoded
   *
   * @return string The base64 encode of what you passed in
   */
  public static function urlsafeB64Encode($input)
  {
    return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
  }

  /**
   * Helper method to create a JSON error.
   *
   * @param int $errno An error number from json_last_error()
   *
   * @return void
   */
  private static function _handleJsonError($errno)
  {
    $messages = array(
      JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
      JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
      JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON'
    );
    throw new DomainException(
      isset($messages[$errno])
      ? $messages[$errno]
      : 'Unknown JSON error: ' . $errno
    );
  }

}

?>

And an example showing how to use it:

<?php

const AUTH_BASE_URL = 'https://www.grade.us/sso/authorize/';
$secret = 'your secret';
date_default_timezone_set('UTC');
$data = array(
  'email' => 'master-demo@reviewsoftware.org',
  'date' => date("Y-m-d H:i:s")
);
$token = JWT::encode($data, $secret, 'HS256');
echo AUTH_BASE_URL . $token; //

You can also download some functional examples here.