Q:

Zendesk Single Sign-on gem for rails 3

Does anyone know of a maintained gem that handles user authentication for the Zendesk API through an existing Rails 3 application?
I asked Zendesk IT and got sent to https://github.com/tobias/zendesk_remote_auth, but it does not look rails 3 compatible and has not been updated since 2009.

A:

I think the article in our docs gives the impression that Zendesk SSO is difficult when in fact it is pretty easy (http://www.zendesk.com/api/remote-authentication).
# reference http://www.zendesk.com/api/remote-authentication
# you need to be a Zendesk account admin to enable remote auth (if you have not already)
# go to Settings > Security, click "Enabled" next to Single Sign-On

# three important things to pay attention to:
# Remote Login URL, Remote Logout URL, and shared secret token

# for testing on a Rails 3 application running on localhost, fill in the Remote Login URL to map
# to http://localhost:3000/zendesk/login (we will need to make sure routes for that exist)

# fill in Remote Logout URL to http://localhost:3000/zendesk/logout

# copy the secret token, you'll need it later

# first, let's create those routes in config/routes.rb
namespace :zendesk do
  match "/login" => "zendesk#login"    # will match /zendesk/login
  match "/logout" => "zendesk#logout"  # will match /zendesk/logout
end

# Above I've mapped those requests to a controller named "zendesk" but it can be named anything

# next we want to add our secret token to the application, I added this in an initializer
# config/initializers/zendesk_auth.rb
ZENDESK_REMOTE_AUTH_TOKEN = "< your token >"
ZENDESK_REMOTE_AUTH_URL   = "http://yourcompany.zendesk.com/access/remote/"

# Assuming we have a controller called zendesk, in zendesk_controller.rb

require "digest/md5"
class ZendeskController < ApplicationController

  def index
    @zendesk_remote_auth_url = ZENDESK_REMOTE_AUTH_URL
  end

  def login
    timestamp = params[:timestamp] || Time.now.utc.to_i
    # hard coded for example purposes
    # really you would want to do something like current_user.name and current_user.email
    # and you'd probably want this in a helper to hide all this implementation from the controller
    string = "First Last" + "first.last@gmail.com" + ZENDESK_REMOTE_AUTH_TOKEN + timestamp.to_s
    hash = Digest::MD5.hexdigest(string)
    @zendesk_remote_auth_url = "http://yourcompany.zendesk.com/access/remote/?name=First%20Last&email=first.last@gmail.com&timestamp=#{timestamp}&hash=#{hash}"
    redirect_to @zendesk_remote_auth_url
  end

  def logout
    flash[:notice] = params[:message]
  end
end

# Note that the above index action defines an instance variable @zendesk_remote_auth_url
# in my example I simple put a link on the corresponding view that hits ZENDESK_REMOTE_AUTH_URL, doing so
# will cause Zendesk to hit your applications Remote Login URL (you defined in your Zendesk SSO settings) and pass a timestamp back in the URL parameters
# BUT, it is entirely possible to avoid this extra step if you just want to go to /zendesk/login in your app
# notice I am either using a params[:timestamp] if one exists or creating a new timestamp with Time.now

This example is quite simplistic but I just want to illustrate the basic mechanics of Zendesk SSO. Note that I'm not touching the more complicated issue of creating new users or editing existing ones, just logging in users who have an existing Zendesk account.

A:

There is an updated example code from zendesk
# Using JWT from Ruby is straight forward. The below example expects you to have `jwt`
# in your Gemfile, you can read more about that gem at https://github.com/progrium/ruby-jwt.
# Assuming that you've set your shared secret and Zendesk subdomain in the environment, you
# can use Zendesk SSO from your controller like this example.

class ZendeskSessionController < ApplicationController
  # Configuration
  ZENDESK_SHARED_SECRET = ENV["ZENDESK_SHARED_SECRET"]
  ZENDESK_SUBDOMAIN     = ENV["ZENDESK_SUBDOMAIN"]

  def create
    if user = User.authenticate(params[:login], params[:password])
      # If the submitted credentials pass, then log user into Zendesk
      sign_into_zendesk(user)
    else
      render :new, :notice => "Invalid credentials"
    end
  end

  private

  def sign_into_zendesk(user)
    # This is the meat of the business, set up the parameters you wish
    # to forward to Zendesk. All parameters are documented in this page.
    iat = Time.now.to_i
    jti = "#{iat}/#{rand(36**64).to_s(36)}"

    payload = JWT.encode({
      :iat   => iat, # Seconds since epoch, determine when this token is stale
      :jti   => jti, # Unique token id, helps prevent replay attacks
      :name  => user.name,
      :email => user.email,
    }, ZENDESK_SHARED_SECRET)

    redirect_to zendesk_sso_url(payload)
  end

  def zendesk_sso_url(payload)
    "https://#{ZENDESK_SUBDOMAIN}.zendesk.com/access/jwt?jwt=#{payload}"
  end
end

