lib/salesforce_flo/authentication/oauth_wrapper.rb
# Copyright © 2017, Salesforce.com, Inc.
# All Rights Reserved.
# Licensed under the BSD 3-Clause license.
# For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
require 'webrick'
require 'launchy'
require 'json'
require 'restforce'
module SalesforceFlo
module Authentication
class OauthWrapper
OAUTH_ENDPOINT = '/services/oauth2/authorize'
DEFAULT_AUTH_HOST = 'https://login.salesforce.com'
DEFAULT_CLIENT_ID = '3MVG9CEn_O3jvv0zPd34OzgiH037XR5Deez3GW8PpsMdzoxecdKUW1s.8oYU9GoLS2Tykr4qTrCizaQBjRXNT'
DEFAULT_REDIRECT_HOSTNAME = 'localhost'
DEFAULT_LISTEN_PORT = '3835'
# Creates a new OauthWrapper instance
#
# @param [Hash] opts The options needed to create the provider
# @option opts [String] :client_id The client id of the connected app for Oauth authorization
# @option opts [String] :redirect_hostname (http://localhost:3835) The hostname portion of the uri
# that the user will be redirected to at the end of the Oauth authorization flow. This MUST match the
# redirect URL specified in the connected app settings.
# @option opts [String] :port (3835) The port that the user will be redirected to at the end of the Oauth
# @option opts [String] :auth_host (https://login.salesforce.com) The hostname where the user will be directed for
# authentication. This is useful if your org utilizes a custom domain.
# flow. This will be appended to the redirect_hostname
# @option opts [#call] :client An object that produces a client when called with initialization options
# @raise [ArgumentError] If client object does not respond_to?(:call)
#
def initialize(opts={})
@client_id = opts.fetch(:client_id, DEFAULT_CLIENT_ID)
@redirect_hostname = opts.fetch(:redirect_hostname, DEFAULT_REDIRECT_HOSTNAME)
@port = opts.fetch(:port, DEFAULT_LISTEN_PORT)
@auth_host = opts.fetch(:auth_host, DEFAULT_AUTH_HOST)
@client = opts.fetch(:client, -> (options) { Restforce.new(options) })
raise ArgumentError.new(':client must respond to #call, try a lambda') unless @client.respond_to?(:call)
end
# Starts a temporary webserver on the specified port, and initiates an Oauth authorization flow, which will
# redirect the user back to localhost on the specified port.
#
# @param [Hash] opts Options that will be passed to the client when called, which will be merged with the response
# from the salesforce that includes the access token
# @return The result of invoking #call on the client object
def call(opts={})
server = WEBrick::HTTPServer.new(Port: @port)
auth_details = {}
server.mount_proc('/') do |req, res|
res.body = js_template
end
server.mount_proc('/send_token') do |req, res|
auth_details = JSON.parse(req.body)
res.body = 'token sent'
server.shutdown # server will shutdown after completing the request
end
trap "INT" do server.shutdown end
Launchy.open("#{@auth_host}#{OAUTH_ENDPOINT}?#{oauth_query_string}")
server.start
merged_options = opts.merge(auth_details).merge(client_id: @client_id, api_version: '39.0').inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
merged_options[:oauth_token] = merged_options[:access_token] if merged_options[:access_token]
@client.call(merged_options)
end
private
def oauth_query_string
query_string = URI.encode_www_form([['client_id', @client_id], ['response_type', 'token'], ['redirect_uri', redirect_uri]])
end
def redirect_uri
"http://#{@redirect_hostname}:#{@port}"
end
def js_template
<<-TEMPLATE
<html>
<head></head>
<body onload="sendHashParams()">
OAuth authentication completed successfully. This window will close shortly.
<script>
function getHashParams() {
var hashParams = {};
var e,
a = /\\+/g, // Regex for replacing addition symbol with a space
r = /([^&;=]+)=?([^&;]*)/g,
d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
q = window.location.hash.substring(1);
while (e = r.exec(q))
hashParams[d(e[1])] = d(e[2]);
return hashParams;
}
function sendHashParams() {
xhr = new XMLHttpRequest();
var url = "http://localhost:#{@port}/send_token";
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-type", "application/json");
var data = JSON.stringify(getHashParams());
xhr.send(data);
setTimeout(function() {
window.close;
}, (10 * 1000))
}
</script>
</body>
</html>
TEMPLATE
end
end
end
end