Single Sign On

There are two main reasons for using JRNI single sign-on:

You want to enable customers to use their existing authentication credentials to be authorised to make new appointments or make changes to existing ones.

You want to enable staff to use their existing authentication credentials to be authorised to make administrative changes.

In both cases a token will need to be generated that contains details of the customer or staff member. This SSO token is exchanged for an Auth-Token and the individual will be added to the system if they don’t already exist.

Ensure you include and correctly set the Expiry Date and Time when creating the SSO Token.

Member Login

curl -X POST  
  <https://{host}.bookingbug.com/api/v1/login/sso/{company_id}>  
  -H App-Id:1234  
  -H 'Content-Type: application/json'  
  -d '{  
      "token": "abcde"  
  }'

Admin Login

curl -X POST  
  <https://{host}.jrni.com/api/v1/login/admin_sso/{company_id}>  
  -H App-Id:1234  
  -H 'Content-Type: application/json'  
  -d '{  
      "token": "abcde"
       }'

SSO Token Generation Examples

namespace BookingBug {
  public class TokenGenerator {
    public static string create(JsonObject data) {
      string CompanyId = "{Your Company ID or Affiliate ID}";
      string SecureKey = "{Your Secure Key}";
      string initVector = "OpenSSL for Ruby"; // DO NOT CHANGE

      // please change this as necessary to ensure that the SSO token is valid for 1 hour after this sciprt runs
      string expires = DateTime.UtcNow.AddHours(1).ToString("yyyy-MM-ddTHH:mm:ssZ");
      data["expires"] = expires;

      byte[] initVectorBytes = Encoding.UTF8.GetBytes(initVector);
      byte[] keyBytesLong;
      using( SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider() ) {
        keyBytesLong = sha.ComputeHash( Encoding.UTF8.GetBytes( SecureKey + CompanyId ) );
      }
      byte[] keyBytes = new byte[16];
      Array.Copy(keyBytesLong, keyBytes, 16);

      byte[] textBytes = Encoding.UTF8.GetBytes(data.ToString());
      for (int i = 0; i < 16; i++) {
        textBytes[i] ^= initVectorBytes[i];
      }

      // Encrypt the string to an array of bytes
      byte[] encrypted = encryptStringToBytes_AES(textBytes, keyBytes, initVectorBytes);
      string encoded = Convert.ToBase64String(encrypted);
      return HttpUtility.UrlEncode(encoded);
    }

    static byte[] encryptStringToBytes_AES(byte[] textBytes, byte[] Key, byte[] IV) {
      // Declare the stream used to encrypt to an in memory
      // array of bytes and the RijndaelManaged object
      // used to encrypt the data.
      using( MemoryStream msEncrypt = new MemoryStream() )
      using( RijndaelManaged aesAlg = new RijndaelManaged() )
      {
        // Provide the RijndaelManaged object with the specified key and IV.
        aesAlg.Mode = CipherMode.CBC;
        aesAlg.Padding = PaddingMode.PKCS7;
        aesAlg.KeySize = 128;
        aesAlg.BlockSize = 128;
        aesAlg.Key = Key;
        aesAlg.IV = IV;
        // Create an encrytor to perform the stream transform.
        ICryptoTransform encryptor = aesAlg.CreateEncryptor();

        // Create the streams used for encryption.
        using( CryptoStream csEncrypt = new CryptoStream( msEncrypt, encryptor, CryptoStreamMode.Write ) ) {
          csEncrypt.Write( textBytes, 0, textBytes.Length );
          csEncrypt.FlushFinalBlock();
        }

        byte[] encrypted = msEncrypt.ToArray();
        // Return the encrypted bytes from the memory stream.
        return encrypted;
      }
    }
  }
}

class MainClass
{
    static void Main(string[] args)
    {
        JsonObject jsonObject = new JsonObject();
        jsonObject["first_name"] = "John";
        jsonObject["last_name"] = "Smith";
        jsonObject["email"] = "[email protected]";
        jsonObject["mobile"] = "0123456789";
        jsonObject["reference"] = "external-reference";
        System.Console.WriteLine(BookingBug.TokenGenerator.create(jsonObject));
    }
}
package com.bookingbug;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Date;
import java.text.SimpleDateFormat;

import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.net.URLCodec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.json.simple.JSONObject;


/**
 * Singleton class for creating a BookingBug SSO Token. See {@link #main(String[])} for usage example.
 * Requires commons-codec library {@link http://commons.apache.org/codec/}.
 */
public class TokenGenerator {
  private static final String COMPANY_ID = "{Your Company ID or Affiliate ID}";
  private static final String SECURE_KEY = "{Your Secure Key}";
  private static final byte[] INIT_VECTOR = "OpenSSL for Ruby".getBytes();
  private SecretKeySpec secretKeySpec;
  private IvParameterSpec ivSpec;
  private URLCodec urlCodec = new URLCodec("ASCII");
  private Base64 base64 = new Base64();
  private static TokenGenerator INSTANCE = new TokenGenerator();

  public static TokenGenerator getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new TokenGenerator();
    }
    return INSTANCE;
  }

  private TokenGenerator() {
    String salted = SECURE_KEY + COMPANY_ID;
    byte[] hash = DigestUtils.sha(salted);
    byte[] saltedHash = new byte[16];
    System.arraycopy(hash, 0, saltedHash, 0, 16);

    secretKeySpec = new SecretKeySpec(saltedHash, "AES");
    ivSpec = new IvParameterSpec(INIT_VECTOR);
  }

  private void encrypt(InputStream in, OutputStream out) throws Exception {
    try {
      byte[] buf = new byte[1024];

      Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
      cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivSpec);

      out = new CipherOutputStream(out, cipher);

      int numRead = 0;
      while ((numRead = in.read(buf)) >= 0) {
        out.write(buf, 0, numRead);
      }
      out.close();
    } catch (InvalidKeyException e) {
      e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    } catch (NoSuchPaddingException e) {
      e.printStackTrace();
    } catch (InvalidAlgorithmParameterException e) {
      e.printStackTrace();
    } catch (java.io.IOException e) {
      e.printStackTrace();
    }
  }

  public String create(JSONObject json) throws Exception {
    Date expires = DateUtils.addHours(new Date(), 1);
    json.put("expires", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(expires));
    byte[] data = json.toString().getBytes();

    ByteArrayOutputStream out = new ByteArrayOutputStream();
    for (int i = 0; i < 16; i++) {
      data[i] ^= INIT_VECTOR[i];
    }
    encrypt(new ByteArrayInputStream(data), out);

    String token = new String(urlCodec.encode(base64.encode(out.toByteArray())));
    return token;
  }

  public static void main(String[] args) {
    try {
      JSONObject jsonObj = new JSONObject();
      jsonObj.put("first_name", "John");
      jsonObj.put("last_name", "Smith");
      jsonObj.put("email", "[email protected]");
      jsonObj.put("mobile", "0123456789");
      jsonObj.put("reference", "external-reference");

      System.out.println( TokenGenerator.getInstance().create(jsonObj) );

    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
#!/usr/bin/env ruby

require 'rubygems'
require 'ezcrypto'
require 'json'
require 'cgi'
require 'time'

module BookingBug
  class TokenGenerator
    attr_accessor :data
    attr_accessor :options

    BOOKINGBUG_COMPANY_ID = "{Your Company ID parent or child or Affiliate ID}"
    BOOKINGBUG_SECURE_KEY = "{Your Secure Key}"

    def initialize(options = {})
      options.merge!({:expires => (Time.now + 3600).iso8601})
      key = EzCrypto::Key.with_password BOOKINGBUG_COMPANY_ID, BOOKINGBUG_SECURE_KEY
      @data = key.encrypt64(options.to_json).gsub(/\n/,'')
      @data = CGI.escape(@data)
    end

    def to_s
      @data
    end

    def decrypt
      key = EzCrypto::Key.with_password BOOKINGBUG_COMPANY_ID, BOOKINGBUG_SECURE_KEY
      key.decrypt64(CGI.unescape(@data))
    end
  end
end

token = BookingBug::TokenGenerator.new({
  'first_name' => 'John',
  'email' => '[email protected]',
  'last_name' => 'Smith',
  'mobile' => '0123456789',
  'reference' => 'external-reference'
})

puts token.to_s

SSO Token Generation AES-256-GCM Examples

require 'base64'
require 'openssl'
require 'json'

module BookingBug
  class TokenGenerator
    attr_reader :company_api_secure_key, :token_data, :iv

    def initialize(company_api_secure_key:, token_data:, iv:)
      token_data[:expires] = (Time.now + 3600).strftime('%Y-%m-%dT%H:%M:%S.%L%z') unless token_data[:expires]
      @company_api_secure_key = company_api_secure_key
      @token_data = token_data
      @iv = iv
    end

    def base64_iv
      @base64_iv ||= Base64.strict_encode64(iv)
    end

    def base64_encrypted_token
      @base64_encrypted_token ||= Base64.strict_encode64(open_ssl_cipher_result[:ciphertext])
    end

    def base64_auth_tag
      @base64_auth_tag ||= Base64.strict_encode64(open_ssl_cipher_result[:auth_tag])
    end

    private

    def open_ssl_cipher_result
      return @open_ssl_cipher_result if defined?(@open_ssl_cipher_result)

      cipher = OpenSSL::Cipher.new('aes-256-gcm')
      cipher.encrypt
      cipher.key = Digest::SHA2.new(256).digest(company_api_secure_key)
      cipher.iv = iv
      ciphertext = cipher.update(token_data.to_json) + cipher.final
      auth_tag = cipher.auth_tag
      @open_ssl_cipher_result = {ciphertext: ciphertext, auth_tag: auth_tag}
    end
  end
end

token = BookingBug::TokenGenerator.new(company_api_secure_key: 'company_api_secure_key',
                                       token_data: {
                                         'first_name' => 'John',
                                         'email' => '[email protected]',
                                         'last_name' => 'Smith',
                                         'mobile' => '0123456789',
                                         'reference' => 'external-reference'
                                       },
                                       iv: 'BBBBBBBBBBBB')

puts token.base64_iv
puts token.base64_auth_tag
puts token.base64_encrypted_token