lib/spark_api/authentication/oauth2.rb
require 'uri'
module SparkApi
module Authentication
#=OAuth2 Authentication
# Auth implementation to the API using the OAuth2 service endpoint. Current adheres to the 10
# draft of the OAuth2 specification. With OAuth2, the application supplies credentials for the
# application, and a separate a user authentication flow dictactes the active user for
# requests.
#
#===Setup
# When using this authentication method, there is a bit more setup involved to make the client
# work. All applications need to extend the BaseOAuth2Provider class to supply the application
# specific configuration. Also depending on the application type (command line, native, or web
# based), the user authentication step will be handled differently.
#==OAuth2
# Implementation the BaseAuth interface for API style authentication
class OAuth2 < BaseAuth
def initialize(client)
super(client)
@provider = client.oauth2_provider
end
def session
@provider.load_session()
end
def session=(s)
@provider.save_session(s)
end
def authenticate
granter = OAuth2Impl::GrantTypeBase.create(@client, @provider, session)
self.session = granter.authenticate
session
end
# Perform an HTTP request (no data)
def request(method, path, body, options={})
escaped_path = Addressable::URI.escape(path)
connection = @client.connection(true) # SSL Only!
connection.headers.merge!(options.delete(:override_headers) || {})
connection.headers.merge!(self.auth_header)
unless (@client.api_user.nil? || options[:ApiUser])
options.merge!(:ApiUser => "#{@client.api_user}")
end
parameter_string = options.size > 0 ? "?#{build_url_parameters(options)}" : ""
request_path = "#{escaped_path}#{parameter_string}"
SparkApi.logger.debug { "Request: #{request_path}" }
if body.nil?
response = connection.send(method, request_path)
else
SparkApi.logger.debug { "Data: #{body}" }
response = connection.send(method, request_path, body)
end
response
end
def logout
@provider.save_session(nil)
end
def authorization_url()
params = {
"client_id" => @provider.client_id,
"response_type" => "code",
"redirect_uri" => @provider.redirect_uri
}
"#{@provider.authorization_uri}?#{build_url_parameters(params)}"
end
# Create a sparkbar token based on the current user's access token
def sparkbar_token()
raise ClientError, "OAuth2Provider must configure the sparkbar_uri to use sparkbar tokens" if provider.sparkbar_uri.nil?
SparkApi.logger.debug { "[sparkbar] create token to #{provider.sparkbar_uri}" }
uri = URI.parse(provider.sparkbar_uri)
request_path = "#{uri.path}"
SparkApi.logger.info { "[sparkbar] create token to #{request_path}, #{session.access_token.inspect}" }
response = sparkbar_connection("#{uri.scheme}://#{uri.host}").post(request_path, "access_token=#{session.access_token}").body
token = response["token"]
SparkApi.logger.debug { "[sparkbar] New token created #{token}" }
token
end
protected
attr_reader :provider, :client
def auth_header
{"Authorization"=> "OAuth #{session.access_token}"}
end
# Faraday handle to the sparkbar
def sparkbar_connection(endpoint)
opts = {
:headers => client.headers
}
opts[:headers].delete(:content_type)
opts[:ssl] = {:verify => false } unless @client.ssl_verify
opts[:url] = endpoint
conn = Faraday::Connection.new(opts) do |conn|
conn.response :sparkbar_impl
conn.adapter Faraday.default_adapter
end
end
end
#==OpenId/OAuth 2 Hybrid
# Uses OpenId for Authentication, but also uses OAuth2 for authorization.
class OpenIdOAuth2Hybrid < OAuth2
def authorization_url(parameters={})
params = openid_parameters.merge(parameters)
params["openid.spark.combined_flow"] = true
build_openid_uri(params)
end
protected
def build_openid_uri(params)
"#{@provider.authorization_uri}?#{build_url_parameters(params)}"
end
def openid_parameters
{
"openid.mode" => "checkid_setup",
"openid.spark.client_id" => @provider.client_id,
"openid.return_to" => @provider.redirect_uri
}
end
end
class OpenId < OpenIdOAuth2Hybrid
def authorization_url(parameters={})
params = openid_parameters.merge(parameters)
build_openid_uri(params)
end
def authenticate
raise RuntimeError, "API Authorization not available with an OpenId-only Auth instance"
end
def request(method, path, body, options={})
raise RuntimeError, "API Data not available with an OpenId-only Auth instance"
end
end
# Representation of a session with the api using oauth2
class OAuthSession
SESSION_ATTRIBUTES = [:access_token, :expires_in, :scope, :refresh_token, :refresh_timeout, :start_time]
attr_accessor *SESSION_ATTRIBUTES
def initialize(o={})
options = OptionsHash.new(o)
@access_token = options["access_token"]
@expires_in = options["expires_in"]
@scope = options["scope"]
@refresh_token = options["refresh_token"]
@start_time = options.fetch("start_time", DateTime.now)
@refresh_timeout = options.fetch("refresh_timeout", 43200)
if @start_time.is_a? String
@start_time = DateTime.parse(@start_time)
end
end
# Is the user session token expired?
def expired?
return false if @expires_in.nil?
@start_time + Rational(@expires_in - @refresh_timeout, 86400) < DateTime.now
end
def to_json(*a)
to_hash.to_json(*a)
end
def to_hash
hash = {}
SESSION_ATTRIBUTES.each do |k|
value = self.send(k)
hash[k.to_s] = value unless value.nil?
end
hash
end
end
#=OAuth2 configuration provider for applications
# Applications planning to use OAuth2 authentication with the API must extend this class as
# part of the client configuration, providing values for the following attributes:
# @authorization_uri - User oauth2 login page for the Spark platform
# @access_uri - Location of the OAuth2 access token resource for the api. OAuth2 code and
# credentials will be sent to this uri to generate an access token.
# @redirect_uri - Application uri to redirect to
# @client_id - OAuth2 provided application identifier
# @client_secret - OAuth2 provided password for the client id
class BaseOAuth2Provider
attr_accessor *Configuration::OAUTH2_KEYS
# Requirements for authorization_code grant type
attr_accessor :code
attr_accessor :grant_type
def initialize(opts={})
Configuration::OAUTH2_KEYS.each do |key|
send("#{key}=", opts[key]) if opts.include? key
end
@grant_type = :authorization_code
end
def grant_type
# backwards compatibility check
@grant_type.nil? ? :authorization_code : @grant_type
end
# Application using the client must handle user redirect for user authentication. For
# command line applications, this method is called prior to initial client requests so that
# the process can notify the user to go to the url and retrieve the access_code for the app.
# In a web based web application, this method can be mostly ignored. However, the web based
# application is then responsible for ensuring the code is saved to the the provider instance
# prior to any client requests are performed (or the error below will be thrown).
def redirect(url)
raise "To be implemented by client application"
end
#==For any persistence to be supported outside application process, the application shall
# implement the following methods for storing and retrieving the user OAuth2 session
# (e.g. to and from memcached).
# Load the current OAuth session
# returns - active OAuthSession or nil
def load_session
nil
end
# Save current session
# session - active OAuthSession
def save_session(session)
end
# Provides a default session time out
# returns - the session timeout length (in seconds)
def session_timeout
86400 # 1.day
end
end
module OAuth2Impl
require 'spark_api/authentication/oauth2_impl/faraday_middleware'
require 'spark_api/authentication/oauth2_impl/grant_type_base'
require 'spark_api/authentication/oauth2_impl/grant_type_refresh'
require 'spark_api/authentication/oauth2_impl/grant_type_code'
require 'spark_api/authentication/oauth2_impl/grant_type_password'
require 'spark_api/authentication/oauth2_impl/cli_provider'
require 'spark_api/authentication/oauth2_impl/simple_provider'
require 'spark_api/authentication/oauth2_impl/single_session_provider'
# Loads a provider class from a string
def self.load_provider(string, args={})
constant = Object
string.split("::").compact.each { |name| constant = constant.const_get(name) unless name == ""}
constant.new(args)
rescue => e
raise ArgumentError, "The value '#{string}' is an invalid class name for an oauth2 provider: #{e.message}"
end
end
end
end