flexmls_api 0.3.6 → 0.4.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +6 -6
- data/Gemfile.lock +6 -6
- data/README.md +5 -3
- data/Rakefile +2 -1
- data/VERSION +1 -1
- data/lib/flexmls_api/authentication.rb +25 -54
- data/lib/flexmls_api/authentication/api_auth.rb +100 -0
- data/lib/flexmls_api/authentication/base_auth.rb +47 -0
- data/lib/flexmls_api/authentication/oauth2.rb +219 -0
- data/lib/flexmls_api/client.rb +7 -1
- data/lib/flexmls_api/configuration.rb +5 -2
- data/lib/flexmls_api/faraday.rb +6 -2
- data/lib/flexmls_api/models.rb +2 -0
- data/lib/flexmls_api/models/base.rb +5 -1
- data/lib/flexmls_api/models/contact.rb +1 -0
- data/lib/flexmls_api/models/custom_fields.rb +2 -2
- data/lib/flexmls_api/models/finders.rb +2 -2
- data/lib/flexmls_api/models/idx_link.rb +1 -1
- data/lib/flexmls_api/models/listing.rb +31 -5
- data/lib/flexmls_api/models/market_statistics.rb +1 -1
- data/lib/flexmls_api/models/note.rb +43 -0
- data/lib/flexmls_api/models/standard_fields.rb +43 -0
- data/lib/flexmls_api/models/subresource.rb +5 -2
- data/lib/flexmls_api/models/system_info.rb +7 -0
- data/lib/flexmls_api/models/tour_of_home.rb +24 -0
- data/lib/flexmls_api/request.rb +13 -28
- data/spec/fixtures/add_note.json +11 -0
- data/spec/fixtures/agent_shared_note.json +11 -0
- data/spec/fixtures/agent_shared_note_empty.json +7 -0
- data/spec/fixtures/authentication_failure.json +7 -0
- data/spec/fixtures/count.json +10 -0
- data/spec/fixtures/errors/expired.json +7 -0
- data/spec/fixtures/generic_delete.json +1 -0
- data/spec/fixtures/generic_failure.json +5 -0
- data/spec/fixtures/oauth2_access.json +3 -0
- data/spec/fixtures/oauth2_error.json +3 -0
- data/spec/fixtures/session.json +1 -1
- data/spec/fixtures/standardfields.json +188 -0
- data/spec/fixtures/standardfields_city.json +1031 -0
- data/spec/fixtures/standardfields_nearby.json +53 -0
- data/spec/fixtures/standardfields_stateorprovince.json +36 -0
- data/spec/fixtures/tour_of_homes.json +23 -0
- data/spec/spec_helper.rb +22 -5
- data/spec/unit/flexmls_api/authentication/api_auth_spec.rb +159 -0
- data/spec/unit/flexmls_api/authentication/oauth2_spec.rb +183 -0
- data/spec/unit/flexmls_api/authentication_spec.rb +10 -2
- data/spec/unit/flexmls_api/configuration_spec.rb +2 -2
- data/spec/unit/flexmls_api/faraday_spec.rb +3 -7
- data/spec/unit/flexmls_api/models/base_spec.rb +1 -1
- data/spec/unit/flexmls_api/models/contact_spec.rb +8 -4
- data/spec/unit/flexmls_api/models/document_spec.rb +2 -5
- data/spec/unit/flexmls_api/models/listing_spec.rb +46 -9
- data/spec/unit/flexmls_api/models/note_spec.rb +90 -0
- data/spec/unit/flexmls_api/models/photo_spec.rb +2 -2
- data/spec/unit/flexmls_api/models/system_info_spec.rb +37 -3
- data/spec/unit/flexmls_api/models/tour_of_home_spec.rb +43 -0
- data/spec/unit/flexmls_api/models/video_spec.rb +2 -4
- data/spec/unit/flexmls_api/models/virtual_tour_spec.rb +2 -2
- data/spec/unit/flexmls_api/paginate_spec.rb +11 -8
- data/spec/unit/flexmls_api/request_spec.rb +31 -16
- data/spec/unit/flexmls_api/standard_fields_spec.rb +86 -0
- data/spec/unit/flexmls_api_spec.rb +6 -27
- metadata +119 -76
data/Gemfile
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
source :rubygems
|
2
2
|
|
3
|
-
gem '
|
4
|
-
gem '
|
5
|
-
gem 'faraday_middleware'
|
6
|
-
gem 'multi_json'
|
7
|
-
gem 'json'
|
8
|
-
gem 'yajl-ruby'
|
3
|
+
gem 'curb', '0.7.8'
|
4
|
+
gem 'faraday', '0.5.3'
|
5
|
+
gem 'faraday_middleware', '0.3.1'
|
6
|
+
gem 'multi_json', '0.0.5'
|
7
|
+
gem 'json', '1.4.6'
|
8
|
+
gem 'yajl-ruby', '0.7.8'
|
9
9
|
gem 'builder', '2.1.2'
|
10
10
|
|
11
11
|
gem 'will_paginate', '~> 3.0.pre2'
|
data/Gemfile.lock
CHANGED
@@ -46,15 +46,15 @@ PLATFORMS
|
|
46
46
|
DEPENDENCIES
|
47
47
|
builder (= 2.1.2)
|
48
48
|
ci_reporter (>= 1.6.3)
|
49
|
-
curb
|
50
|
-
faraday
|
51
|
-
faraday_middleware
|
49
|
+
curb (= 0.7.8)
|
50
|
+
faraday (= 0.5.3)
|
51
|
+
faraday_middleware (= 0.3.1)
|
52
52
|
jeweler
|
53
|
-
json
|
54
|
-
multi_json
|
53
|
+
json (= 1.4.6)
|
54
|
+
multi_json (= 0.0.5)
|
55
55
|
rcov
|
56
56
|
rspec
|
57
57
|
typhoeus
|
58
58
|
webmock
|
59
59
|
will_paginate (~> 3.0.pre2)
|
60
|
-
yajl-ruby
|
60
|
+
yajl-ruby (= 0.7.8)
|
data/README.md
CHANGED
@@ -34,9 +34,13 @@ Usage Examples
|
|
34
34
|
|
35
35
|
Authentication
|
36
36
|
--------------
|
37
|
-
Authentication is handled transparently by the request framework in the gem, so you should never need to manually make an authentication request.
|
37
|
+
Authentication is handled transparently by the request framework in the gem, so you should never need to manually make an authentication request. More than one mode of authentication is supported, so the client needs to be configured accordingly.
|
38
38
|
|
39
|
+
#### API Authentication (Default)
|
40
|
+
Usually supplied for a single user, this authentication mode is the simplest, and is setup as the default. The example usage above demonstrates how to get started using this authentication mode.
|
39
41
|
|
42
|
+
#### OAuth2 Authentication
|
43
|
+
Authentication mode the separates application and user authentication. This mode requires further setup which is described in lib/flexmls_api/authentication/oauth2.rb
|
40
44
|
|
41
45
|
Error Codes
|
42
46
|
---------------------
|
@@ -124,5 +128,3 @@ Error Codes
|
|
124
128
|
</tbody>
|
125
129
|
</table>
|
126
130
|
|
127
|
-
|
128
|
-
|
data/Rakefile
CHANGED
@@ -14,7 +14,8 @@ begin
|
|
14
14
|
gemspec.email = "api-support@flexmls.com"
|
15
15
|
gemspec.homepage = "https://github.com/flexmls/flexmls_api"
|
16
16
|
gemspec.authors = ["Brandon Hornseth", "Wade McEwen"]
|
17
|
-
|
17
|
+
# Need to skip spec/reports for CI builds
|
18
|
+
gemspec.files = FileList["[A-Z]*", "{lib,spec/fixtures,spec/unit}/**/*", "spec/*.rb"]
|
18
19
|
gemspec.add_development_dependency "rspec"
|
19
20
|
gemspec.add_development_dependency "jeweler"
|
20
21
|
gemspec.add_development_dependency "curb"
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.5
|
@@ -1,11 +1,18 @@
|
|
1
|
+
|
1
2
|
require 'openssl'
|
2
3
|
require 'faraday'
|
3
4
|
require 'faraday_middleware'
|
4
5
|
require 'yajl'
|
5
6
|
require 'date'
|
7
|
+
|
8
|
+
require File.expand_path('../authentication/base_auth', __FILE__)
|
9
|
+
require File.expand_path('../authentication/api_auth', __FILE__)
|
10
|
+
require File.expand_path('../authentication/oauth2', __FILE__)
|
11
|
+
|
6
12
|
module FlexmlsApi
|
7
|
-
# =
|
8
|
-
#
|
13
|
+
# =Authentication
|
14
|
+
# Mixin module for handling client authentication and reauthentication to the flexmls api. Makes
|
15
|
+
# use of the configured authentication mode (API Auth by default).
|
9
16
|
module Authentication
|
10
17
|
|
11
18
|
# Main authentication step. Run before any api request unless the user session exists and is
|
@@ -16,58 +23,32 @@ module FlexmlsApi
|
|
16
23
|
# *raises*
|
17
24
|
# FlexmlsApi::ClientError when authentication fails
|
18
25
|
def authenticate
|
19
|
-
sig = sign("#{@api_secret}ApiKey#{@api_key}")
|
20
|
-
FlexmlsApi.logger.debug("Authenticating to #{@endpoint}")
|
21
26
|
start_time = Time.now
|
22
|
-
request_path = "/#{version}/session?ApiKey=#{api_key}&ApiSig=#{sig}"
|
23
|
-
resp = connection(true).post request_path, ""
|
24
27
|
request_time = Time.now - start_time
|
25
|
-
|
26
|
-
|
27
|
-
FlexmlsApi.logger.debug("
|
28
|
-
|
28
|
+
new_session = @authenticator.authenticate
|
29
|
+
FlexmlsApi.logger.info("[#{(request_time * 1000).to_i}ms]")
|
30
|
+
FlexmlsApi.logger.debug("Session: #{new_session.inspect}")
|
31
|
+
new_session
|
32
|
+
end
|
33
|
+
|
34
|
+
# Test to see if there is an active session
|
35
|
+
def authenticated?
|
36
|
+
@authenticator.authenticated?
|
29
37
|
end
|
30
38
|
|
31
39
|
# Delete the current session
|
32
40
|
def logout
|
33
41
|
FlexmlsApi.logger.info("Logging out.")
|
34
|
-
|
35
|
-
@session = nil
|
42
|
+
@authenticator.logout
|
36
43
|
end
|
37
44
|
|
38
|
-
#
|
45
|
+
# Fetch the active session object
|
39
46
|
def session
|
40
|
-
@session
|
41
|
-
end
|
42
|
-
|
43
|
-
# Builds an ordered list of key value pairs and concatenates it all as one big string. Used
|
44
|
-
# specifically for signing a request.
|
45
|
-
def build_param_string(param_hash)
|
46
|
-
return "" if param_hash.nil?
|
47
|
-
sorted = param_hash.sort do |a,b|
|
48
|
-
a.to_s <=> b.to_s
|
49
|
-
end
|
50
|
-
params = ""
|
51
|
-
sorted.each do |key,val|
|
52
|
-
params += key.to_s + val.to_s
|
53
|
-
end
|
54
|
-
params
|
47
|
+
@authenticator.session
|
55
48
|
end
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
# roles, tokens and expiration
|
60
|
-
class Session
|
61
|
-
attr_accessor :auth_token, :expires, :roles
|
62
|
-
def initialize(options={})
|
63
|
-
@auth_token = options["AuthToken"]
|
64
|
-
@expires = DateTime.parse options["Expires"]
|
65
|
-
@roles = options["Roles"]
|
66
|
-
end
|
67
|
-
# Is the user session token expired?
|
68
|
-
def expired?
|
69
|
-
DateTime.now > @expires
|
70
|
-
end
|
49
|
+
# Save the active session object
|
50
|
+
def session=(active_session)
|
51
|
+
@authenticator.session=active_session
|
71
52
|
end
|
72
53
|
|
73
54
|
# Main connection object for running requests. Bootstraps the Faraday abstraction layer with
|
@@ -92,7 +73,7 @@ module FlexmlsApi
|
|
92
73
|
conn
|
93
74
|
end
|
94
75
|
|
95
|
-
# HTTP request headers
|
76
|
+
# HTTP request headers for client requests
|
96
77
|
def headers
|
97
78
|
{
|
98
79
|
:accept => 'application/json',
|
@@ -102,15 +83,5 @@ module FlexmlsApi
|
|
102
83
|
}
|
103
84
|
end
|
104
85
|
|
105
|
-
# Sign a request
|
106
|
-
def sign(sig)
|
107
|
-
Digest::MD5.hexdigest(sig)
|
108
|
-
end
|
109
|
-
|
110
|
-
# Sign a request with request data.
|
111
|
-
def sign_token(path, params = {}, post_data="")
|
112
|
-
sign("#{@api_secret}ApiKey#{@api_key}ServicePath/#{version}#{path}#{build_param_string(params)}#{post_data}")
|
113
|
-
end
|
114
|
-
|
115
86
|
end
|
116
87
|
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module FlexmlsApi
|
2
|
+
|
3
|
+
module Authentication
|
4
|
+
|
5
|
+
#=API Authentication
|
6
|
+
# Auth implementation for the API's original hash based authentication design. This is the
|
7
|
+
# default authentication strategy used by the client. API Auth rely's on the user's API key
|
8
|
+
# and secret and the active user is tied to the key owner.
|
9
|
+
|
10
|
+
#==ApiAuth
|
11
|
+
# Implementation the BaseAuth interface for API style authentication
|
12
|
+
class ApiAuth < BaseAuth
|
13
|
+
|
14
|
+
def initialize(client)
|
15
|
+
@client = client
|
16
|
+
end
|
17
|
+
|
18
|
+
def authenticate
|
19
|
+
sig = sign("#{@client.api_secret}ApiKey#{@client.api_key}")
|
20
|
+
FlexmlsApi.logger.debug("Authenticating to #{@client.endpoint}")
|
21
|
+
start_time = Time.now
|
22
|
+
request_path = "/#{@client.version}/session?ApiKey=#{@client.api_key}&ApiSig=#{sig}"
|
23
|
+
resp = @client.connection(true).post request_path, ""
|
24
|
+
request_time = Time.now - start_time
|
25
|
+
FlexmlsApi.logger.info("[#{(request_time * 1000).to_i}ms] Api: POST #{request_path}")
|
26
|
+
@session = Session.new(resp.body.results.first)
|
27
|
+
FlexmlsApi.logger.debug("Authentication: #{@session.inspect}")
|
28
|
+
@session
|
29
|
+
end
|
30
|
+
|
31
|
+
def logout
|
32
|
+
@client.delete("/session/#{@session.auth_token}") unless @session.nil?
|
33
|
+
@session = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
# Builds an ordered list of key value pairs and concatenates it all as one big string. Used
|
37
|
+
# specifically for signing a request.
|
38
|
+
def build_param_string(param_hash)
|
39
|
+
return "" if param_hash.nil?
|
40
|
+
sorted = param_hash.sort do |a,b|
|
41
|
+
a.to_s <=> b.to_s
|
42
|
+
end
|
43
|
+
params = ""
|
44
|
+
sorted.each do |key,val|
|
45
|
+
params += key.to_s + val.to_s
|
46
|
+
end
|
47
|
+
params
|
48
|
+
end
|
49
|
+
|
50
|
+
# Sign a request
|
51
|
+
def sign(sig)
|
52
|
+
Digest::MD5.hexdigest(sig)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Sign a request with request data.
|
56
|
+
def sign_token(path, params = {}, post_data="")
|
57
|
+
sign("#{@client.api_secret}ApiKey#{@client.api_key}ServicePath#{path}#{build_param_string(params)}#{post_data}")
|
58
|
+
end
|
59
|
+
|
60
|
+
# Perform an HTTP request (no data)
|
61
|
+
def request(method, path, body, options)
|
62
|
+
request_opts = {
|
63
|
+
"AuthToken" => @session.auth_token
|
64
|
+
}
|
65
|
+
unless @client.api_user.nil?
|
66
|
+
request_opts.merge!(:ApiUser => "#{@client.api_user}")
|
67
|
+
end
|
68
|
+
request_opts.merge!(options)
|
69
|
+
sig = sign_token(path, request_opts, body)
|
70
|
+
request_path = "#{path}?#{build_url_parameters({"ApiSig"=>sig}.merge(request_opts))}"
|
71
|
+
FlexmlsApi.logger.debug("Request: #{request_path}")
|
72
|
+
if body.nil?
|
73
|
+
response = @client.connection.send(method, request_path)
|
74
|
+
else
|
75
|
+
FlexmlsApi.logger.debug("Data: #{body}")
|
76
|
+
response = @client.connection.send(method, request_path, body)
|
77
|
+
end
|
78
|
+
response
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
# ==Session class
|
84
|
+
# Handle on the api user session information as return by the api session service, including
|
85
|
+
# roles, tokens and expiration
|
86
|
+
class Session
|
87
|
+
attr_accessor :auth_token, :expires, :roles
|
88
|
+
def initialize(options={})
|
89
|
+
@auth_token = options["AuthToken"]
|
90
|
+
@expires = DateTime.parse options["Expires"]
|
91
|
+
@roles = options["Roles"]
|
92
|
+
end
|
93
|
+
# Is the user session token expired?
|
94
|
+
def expired?
|
95
|
+
DateTime.now > @expires
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module FlexmlsApi
|
2
|
+
|
3
|
+
module Authentication
|
4
|
+
#=Authentication Base
|
5
|
+
# This base class defines the basic interface supported by all client authentication
|
6
|
+
# implementations.
|
7
|
+
class BaseAuth
|
8
|
+
attr_accessor :session
|
9
|
+
# All ihheriting classes should accept the flexmls_api client as a part of initialization
|
10
|
+
def initialize(client)
|
11
|
+
@client = client
|
12
|
+
end
|
13
|
+
|
14
|
+
# Perform requests to authenticate the client with the API
|
15
|
+
def authenticate
|
16
|
+
raise "Implement me!"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Called prior to running authenticate (except in case of api authentication errors)
|
20
|
+
def authenticated?
|
21
|
+
!(session.nil? || session.expired?)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Terminate the active session
|
25
|
+
def logout
|
26
|
+
raise "Implement me!"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Perform an HTTP request (no data)
|
30
|
+
def request(method, path, body, options)
|
31
|
+
raise "Implement me!"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Format a hash as request parameters
|
35
|
+
#
|
36
|
+
# :returns:
|
37
|
+
# Stringized form of the parameters as needed for an HTTP request
|
38
|
+
def build_url_parameters(parameters={})
|
39
|
+
array = parameters.map do |key,value|
|
40
|
+
escaped_value = CGI.escape("#{value}")
|
41
|
+
"#{key}=#{escaped_value}"
|
42
|
+
end
|
43
|
+
array.join "&"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module FlexmlsApi
|
4
|
+
|
5
|
+
module Authentication
|
6
|
+
#=OAuth2 Authentication
|
7
|
+
# Auth implementation to the API using the OAuth2 service endpoint. Current adheres to the 10
|
8
|
+
# draft of the OAuth2 specification. With OAuth2, the application supplies credentials for the
|
9
|
+
# application, and a separate a user authentication flow dictactes the active user for
|
10
|
+
# requests.
|
11
|
+
#
|
12
|
+
#===Setup
|
13
|
+
# When using this authentication method, there is a bit more setup involved to make the client
|
14
|
+
# work. All applications need to extend the BaseOAuth2Provider class to supply the application
|
15
|
+
# specific configuration. Also depending on the application type (command line, native, or web
|
16
|
+
# based), the user authentication step will be handled differently.
|
17
|
+
|
18
|
+
#==OAuth2
|
19
|
+
# Implementation the BaseAuth interface for API style authentication
|
20
|
+
class OAuth2 < BaseAuth
|
21
|
+
|
22
|
+
def session
|
23
|
+
@provider.load_session()
|
24
|
+
end
|
25
|
+
def session=(s)
|
26
|
+
@provider.save_session(s)
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(client)
|
30
|
+
@client = client
|
31
|
+
@provider = client.oauth2_provider
|
32
|
+
end
|
33
|
+
|
34
|
+
# Generate the appropriate request uri for authorizing this application for current user.
|
35
|
+
def authorization_url
|
36
|
+
params = {
|
37
|
+
"client_id" => @provider.client_id,
|
38
|
+
"response_type" => "code",
|
39
|
+
"redirect_uri" => @provider.redirect_uri
|
40
|
+
}
|
41
|
+
"#{@provider.authorization_uri}?#{build_url_parameters(params)}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def token_params
|
45
|
+
params = {
|
46
|
+
"client_id" => @provider.client_id,
|
47
|
+
"client_secret" => @provider.client_secret,
|
48
|
+
"grant_type" => "authorization_code",
|
49
|
+
"code" => @provider.code,
|
50
|
+
"redirect_uri" => @provider.redirect_uri
|
51
|
+
}
|
52
|
+
"?#{build_url_parameters(params)}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def authenticate
|
56
|
+
if(@provider.code.nil?)
|
57
|
+
FlexmlsApi.logger.debug("Redirecting to provider to get the authorization code")
|
58
|
+
@provider.redirect(authorization_url)
|
59
|
+
end
|
60
|
+
FlexmlsApi.logger.debug("Authenticating to #{@provider.access_uri}")
|
61
|
+
uri = URI.parse(@provider.access_uri)
|
62
|
+
request_path = "#{uri.path}#{token_params}"
|
63
|
+
response = oauth_access_connection("#{uri.scheme}://#{uri.host}").post(request_path, "").body
|
64
|
+
response.expires_in = @provider.session_timeout if response.expires_in.nil?
|
65
|
+
self.session=response
|
66
|
+
response
|
67
|
+
end
|
68
|
+
|
69
|
+
# Perform an HTTP request (no data)
|
70
|
+
def request(method, path, body, options)
|
71
|
+
connection = @client.connection(true) # SSL Only!
|
72
|
+
connection.headers.merge!(self.auth_header)
|
73
|
+
request_opts = {}
|
74
|
+
request_opts.merge!(options)
|
75
|
+
request_path = "#{path}?#{build_url_parameters({:access_token => session.access_token}.merge(options))}"
|
76
|
+
FlexmlsApi.logger.debug("Request: #{request_path}")
|
77
|
+
if body.nil?
|
78
|
+
response = connection.send(method, request_path)
|
79
|
+
else
|
80
|
+
FlexmlsApi.logger.debug("Data: #{body}")
|
81
|
+
response = connection.send(method, request_path, body)
|
82
|
+
end
|
83
|
+
response
|
84
|
+
end
|
85
|
+
|
86
|
+
def logout
|
87
|
+
@provider.save_session(nil)
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
protected
|
92
|
+
|
93
|
+
def auth_header
|
94
|
+
{"Authorization"=> "OAuth #{session.access_token}"}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Setup a faraday connection for dealing with an OAuth2 endpoint
|
98
|
+
def oauth_access_connection(endpoint)
|
99
|
+
opts = {
|
100
|
+
:headers => @client.headers
|
101
|
+
}
|
102
|
+
opts[:ssl] = {:verify => false }
|
103
|
+
opts[:url] = endpoint
|
104
|
+
conn = Faraday::Connection.new(opts) do |builder|
|
105
|
+
builder.adapter Faraday.default_adapter
|
106
|
+
builder.use Faraday::Response::ParseJson
|
107
|
+
builder.use FlexmlsApi::Authentication::FlexmlsOAuth2Middleware
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Representation of a session with the api using oauth2
|
113
|
+
class OAuthSession
|
114
|
+
attr_accessor :access_token, :expires_in, :scope, :refresh_token
|
115
|
+
def initialize(options={})
|
116
|
+
@access_token = options["access_token"]
|
117
|
+
# TODO The current oauth2 service does not send an expiration time. I'm setting it to default to 1 hour.
|
118
|
+
@expires_in = options["expires_in"]
|
119
|
+
@scope = options["scope"]
|
120
|
+
@refresh_token = options["refresh_token"]
|
121
|
+
@start_time = DateTime.now
|
122
|
+
end
|
123
|
+
# Is the user session token expired?
|
124
|
+
def expired?
|
125
|
+
@start_time + Rational(@expires_in, 24*60*60) < DateTime.now
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
#=OAuth2 configuration provider for applications
|
130
|
+
# Applications planning to use OAuth2 authentication with the API must extend this class as
|
131
|
+
# part of the client configuration, providing values for the following attributes:
|
132
|
+
# @authorization_uri - User oauth2 login page for flexmls
|
133
|
+
# @access_uri - Location of the OAuth2 access token resource for the api. OAuth2 code and
|
134
|
+
# credentials will be sent to this uri to generate an access token.
|
135
|
+
# @redirect_uri - Application uri to redirect to
|
136
|
+
# @client_id - OAuth2 provided application identifier
|
137
|
+
# @client_secret - OAuth2 provided password for the client id
|
138
|
+
class BaseOAuth2Provider
|
139
|
+
|
140
|
+
attr_accessor :authorization_uri, :code, :access_uri, :redirect_uri, :client_id, :client_secret
|
141
|
+
|
142
|
+
# Application using the client must handle user redirect for user authentication. For
|
143
|
+
# command line applications, this method is called prior to initial client requests so that
|
144
|
+
# the process can notify the user to go to the url and retrieve the access_code for the app.
|
145
|
+
# In a web based web application, this method can be mostly ignored. However, the web based
|
146
|
+
# application is then responsible for ensuring the code is saved to the the provider instance
|
147
|
+
# prior to any client requests are performed (or the error below will be thrown).
|
148
|
+
def redirect(url)
|
149
|
+
raise "To be implemented by client application"
|
150
|
+
end
|
151
|
+
|
152
|
+
#==For any persistence to be supported outside application process, the application shall
|
153
|
+
# implement the following methods for storing and retrieving the user OAuth2 session
|
154
|
+
# (e.g. to and from memcached).
|
155
|
+
|
156
|
+
# Load the current OAuth session
|
157
|
+
# returns - active OAuthSession or nil
|
158
|
+
def load_session
|
159
|
+
nil
|
160
|
+
end
|
161
|
+
|
162
|
+
# Save current session
|
163
|
+
# session - active OAuthSession
|
164
|
+
def save_session(session)
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
# Provides a default session time out
|
169
|
+
# returns - the session timeout length (in seconds)
|
170
|
+
def session_timeout
|
171
|
+
3600
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
#==OAuth2 Faraday response middleware
|
177
|
+
# HTTP Response after filter to package oauth2 responses and bubble up basic api errors.
|
178
|
+
class FlexmlsOAuth2Middleware < Faraday::Response::Middleware
|
179
|
+
begin
|
180
|
+
def self.register_on_complete(env)
|
181
|
+
env[:response].on_complete do |finished_env|
|
182
|
+
validate_and_build_response(finished_env)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
rescue LoadError, NameError => e
|
186
|
+
self.load_error = e
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.validate_and_build_response(finished_env)
|
190
|
+
body = finished_env[:body]
|
191
|
+
FlexmlsApi.logger.debug("Response Body: #{body.inspect}")
|
192
|
+
unless body.is_a?(Hash)
|
193
|
+
raise InvalidResponse, "The server response could not be understood"
|
194
|
+
end
|
195
|
+
case finished_env[:status]
|
196
|
+
when 200..299
|
197
|
+
FlexmlsApi.logger.debug("Success!")
|
198
|
+
session = OAuthSession.new(body)
|
199
|
+
else
|
200
|
+
# Handle the WWW-Authenticate Response Header Field if present. This can be returned by
|
201
|
+
# OAuth2 implementations and wouldn't hurt to log.
|
202
|
+
auth_header_error = finished_env[:request_headers]["WWW-Authenticate"]
|
203
|
+
FlexmlsApi.logger.warn("Authentication error #{auth_header_error}") unless auth_header_error.nil?
|
204
|
+
raise ClientError.new(0, finished_env[:status]), body["error"]
|
205
|
+
end
|
206
|
+
FlexmlsApi.logger.debug("Session= #{session.inspect}")
|
207
|
+
finished_env[:body] = session
|
208
|
+
end
|
209
|
+
|
210
|
+
def initialize(app)
|
211
|
+
super
|
212
|
+
@parser = nil
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|