reward_station 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +2 -2
  3. data/README.md +121 -0
  4. data/{spec/fixtures/savon/reward_station → lib/responses}/award_points/award_points.xml +0 -0
  5. data/{spec/fixtures/savon/reward_station → lib/responses}/award_points/award_points_invalid_token.xml +0 -0
  6. data/{spec/fixtures/savon/reward_station → lib/responses}/return_point_summary/return_point_summary.xml +0 -0
  7. data/{spec/fixtures/savon/reward_station → lib/responses}/return_point_summary/return_point_summary_invalid_token.xml +0 -0
  8. data/{spec/fixtures/savon/reward_station → lib/responses}/return_popular_products/return_popular_products.xml +0 -0
  9. data/{spec/fixtures/savon/reward_station → lib/responses}/return_popular_products/return_popular_products_invalid_token.xml +0 -0
  10. data/{spec/fixtures/savon/reward_station → lib/responses}/return_token/return_token.xml +0 -0
  11. data/{spec/fixtures/savon/reward_station → lib/responses}/return_token/return_token_invalid.xml +0 -0
  12. data/{spec/fixtures/savon/reward_station → lib/responses}/return_user/return_user.xml +0 -0
  13. data/{spec/fixtures/savon/reward_station → lib/responses}/return_user/return_user_invalid_token.xml +0 -0
  14. data/{spec/fixtures/savon/reward_station → lib/responses}/return_user/return_user_invalid_user.xml +0 -0
  15. data/{spec/fixtures/savon/reward_station → lib/responses}/update_user/create_user.xml +0 -0
  16. data/{spec/fixtures/savon/reward_station → lib/responses}/update_user/create_user_exists.xml +0 -0
  17. data/lib/reward_station.rb +10 -1
  18. data/lib/reward_station/errors.rb +31 -0
  19. data/lib/reward_station/service.rb +167 -0
  20. data/lib/reward_station/version.rb +1 -1
  21. data/lib/saml/auth_response.rb +174 -0
  22. data/lib/savon/macros.rb +12 -0
  23. data/lib/savon/mock.rb +70 -0
  24. data/lib/savon/mock_response.rb +45 -0
  25. data/reward_station.gemspec +5 -3
  26. data/spec/reward_station/service_spec.rb +326 -0
  27. data/spec/spec_helper.rb +1 -8
  28. metadata +44 -24
  29. data/lib/xceleration/reward_station.rb +0 -122
  30. data/spec/savon_helper.rb +0 -129
  31. data/spec/xceleration/reward_station_spec.rb +0 -348
data/.gitignore CHANGED
@@ -2,3 +2,4 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+ .idea
data/Gemfile CHANGED
@@ -1,8 +1,8 @@
1
1
  source :rubygems
2
2
 
3
- gemspec
3
+ gem 'rake'
4
4
 
5
- gem 'savon', '0.9.6'
5
+ gemspec
6
6
 
7
7
  group 'development' do
8
8
  gem 'rspec', '2.6.0'
data/README.md CHANGED
@@ -1,5 +1,126 @@
1
1
  ### Xceleration Reward Station
2
2
 
3
+ Client library for Xceleration rewardstation.com service
4
+
3
5
 
4
6
  ## Basic Usage
5
7
 
8
+ #Initialization
9
+ reward_station = RewardStation::Service.new :client_id => "112112", :client_password => "fsdftr#"
10
+
11
+ # Return Token
12
+ Request access token
13
+
14
+ token = reward_station.return_token
15
+
16
+ # Award Points
17
+ Update award points
18
+
19
+ user_id = "130"
20
+ points = 10
21
+ description = "Action 'Call to client' "
22
+ program_id = 90 # optional
23
+ point_reasond_code_id = 129 # optional
24
+
25
+ confirmation_number = reward_station.award_points user_id, points, description, program_id, point_reason_code_id
26
+
27
+ # Create User
28
+
29
+ user_attributes = reward_station.create_user :organization_id => '150',
30
+ :email => 'john5@company.com',
31
+ :first_name => 'John',
32
+ :last_name => 'Smith',
33
+ :user_name => 'john5@company.com',
34
+ :balance => 0
35
+ puts user_attributes.inspect
36
+ # {
37
+ # :user_id => '6727',
38
+ # :client_id => '100080',
39
+ # :user_name => 'john5@company.com',
40
+ # :email => 'john5@company.com',
41
+ # :encrypted_password => nil,
42
+ # :first_name => 'John',
43
+ # :last_name => 'Smith',
44
+ # :address_one => nil,
45
+ # :address_two => nil,
46
+ # :city => nil,
47
+ # :state_code => nil,
48
+ # :province => nil,
49
+ # :postal_code => nil,
50
+ # :country_code => 'USA',
51
+ # :phone => nil,
52
+ # :organization_id => '150',
53
+ # :organization_name => nil,
54
+ # :rep_type_id => '0',
55
+ # :client_region_id => '0',
56
+ #
57
+ # :is_active => true,
58
+ # :point_balance => '0',
59
+ # :manager_id => '0',
60
+ # :error_message => nil
61
+ # }
62
+
63
+
64
+ ## Single-Sign-On
65
+
66
+ Basic SSO logic implemented in SAML::AuthResponse class. Example usage of AuthResponse:
67
+
68
+ #SessionController
69
+
70
+ require 'saml/auth_response'
71
+
72
+ class SessionController < ApplicationController
73
+
74
+ def create
75
+ @user_session = UserSession.new(params[:user_session])
76
+ if @user_session.save
77
+ if session[:saml_request].present?
78
+ sso_params(session[:saml_request], session[:relay_state], current_user)
79
+ session[:saml_request] = session[:relay_state] = nil
80
+ render :template => "session/signon"
81
+ return
82
+ ...
83
+ end
84
+ ...
85
+ end
86
+ end
87
+
88
+ def signon
89
+ if current_user.present?
90
+ sso_params(params[:SAMLRequest], params[:RelayState], current_user)
91
+
92
+ render :template => "session/signon"
93
+ else
94
+ session[:saml_request] = params[:SAMLRequest]
95
+ session[:relay_state] = params[:RelayState]
96
+ redirect_to signin_url
97
+ end
98
+ end
99
+
100
+ def destroy
101
+ current_user_session.destroy
102
+ redirect_to signin_url
103
+ end
104
+
105
+ protected
106
+
107
+ def sso_params saml_request, relay_state, user
108
+ @saml_response = SAML::AuthResponse.new(saml_request).response_url(user.xceleration_id)
109
+ @relay_state = relay_state
110
+ end
111
+ end
112
+
113
+ # signon.html.erb
114
+
115
+ <html>
116
+ <body>
117
+ <form id="sso_form" action="http://www6.rewardstation.net/sso/100080/AssertionService.aspx?binding=urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" method="post">
118
+ <input type="hidden" name="SAMLResponse" value="<%= @saml_response %>"/>
119
+ <input type="hidden" name="RelayState" value="<%= @relay_state %>"/>
120
+ </form>
121
+ <script type="text/javascript">
122
+ document.getElementById('sso_form').submit();
123
+ </script>
124
+ </body>
125
+ </html>
126
+
@@ -1 +1,10 @@
1
- require 'xceleration/reward_station'
1
+ require 'savon'
2
+
3
+ require "savon/mock"
4
+ require 'savon/macros'
5
+ require 'savon/mock_response'
6
+
7
+ require 'reward_station/errors'
8
+ require 'reward_station/service'
9
+
10
+
@@ -0,0 +1,31 @@
1
+ module RewardStation
2
+
3
+ module NestingError
4
+ attr_reader :cause
5
+
6
+ def initialize message = nil, cause = $!
7
+ @cause = cause
8
+ super(message || cause && cause.message)
9
+ end
10
+
11
+ def set_backtrace bt
12
+ if cause
13
+ cause.backtrace.reverse.each do |line|
14
+ bt.last == line ? bt.pop : break
15
+ end
16
+ bt << "cause: #{cause.class.name}: #{cause}"
17
+ bt.concat cause.backtrace
18
+ end
19
+ super bt
20
+ end
21
+ end
22
+
23
+ class InvalidAccount < StandardError; end
24
+ class InvalidToken < StandardError; end
25
+ class InvalidUser < StandardError; end
26
+ class UserAlreadyExists < StandardError; end
27
+
28
+ class ConnectionError < StandardError
29
+ include NestingError
30
+ end
31
+ end
@@ -0,0 +1,167 @@
1
+ module RewardStation
2
+ class Service
3
+
4
+ def initialize options = {}
5
+ [:client_id, :client_password].each do |arg|
6
+ raise ArgumentError, "Missing required option '#{arg}'" unless options.has_key? arg
7
+ end
8
+
9
+ @client_id = options[:client_id]
10
+ @client_password = options[:client_password]
11
+ @token = options[:token]
12
+ @organization_id = options[:organization_id]
13
+
14
+ @program_id = options[:program_id]
15
+ @point_reason_code_id = options[:point_reason_code_id]
16
+
17
+ if options[:new_token_callback]
18
+ raise ArgumentError, "new_token_callback option should be proc or lambda" unless options[:new_token_callback].is_a?(Proc)
19
+ @new_token_callback = options[:new_token_callback]
20
+ end
21
+
22
+ @mode = :default
23
+ if options[:mode]
24
+ raise ArgumentError, "supported modes :default and :mock" unless [:mock, :default].include?(options[:mode].to_sym)
25
+ @mode = options[:mode].to_sym
26
+ end
27
+ end
28
+
29
+ def new_token_callback &block
30
+ @new_token_callback = block
31
+ end
32
+
33
+ class << self
34
+ def client
35
+ @@client ||= Savon::Client.new do |wsdl|
36
+ wsdl.document = File.join(File.dirname(__FILE__), '..', 'wsdl', 'reward_services.xml')
37
+ end
38
+ end
39
+
40
+ def logger
41
+ @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
42
+ end
43
+ end
44
+
45
+ def logger
46
+ Service.logger
47
+ end
48
+
49
+ def return_token
50
+ result = request :return_token, :body => {
51
+ 'AccountNumber' => @client_id,
52
+ 'AccountCode' => @client_password
53
+ }
54
+
55
+ logger.info "xceleration token #{result[:token]}"
56
+
57
+ result[:token]
58
+ end
59
+
60
+ def return_user user_id
61
+ request_with_token(:return_user, :body => { 'UserID' => user_id} )[:user_profile]
62
+ end
63
+
64
+ def award_points user_id, points, description, program_id = nil, point_reason_code_id = nil
65
+ request_with_token(:award_points, :body => {
66
+ 'UserID' => user_id,
67
+ 'Points' => points,
68
+ 'ProgramID' => program_id || @program_id,
69
+ 'PointReasonCodeID' => point_reason_code_id || @point_reason_code_id,
70
+ 'Description' => description
71
+ })[:confirmation_number]
72
+ end
73
+
74
+
75
+ def return_point_summary user_id
76
+ request_with_token(:return_point_summary, :body => {
77
+ 'clientId' => @client_id,
78
+ 'userId' => user_id
79
+ })[:point_summary_collection][:point_summary]
80
+ end
81
+
82
+ def update_user user_id, attrs = {}
83
+
84
+ organization_id = attrs[:organization_id] || @organization_id
85
+ email = attrs[:email] || ""
86
+ first_name = attrs[:first_name] || ""
87
+ last_name = attrs[:last_name] || ""
88
+ user_name = attrs[:user_name] || email
89
+ balance = attrs[:balance] || 0
90
+
91
+ request_with_token(:update_user , :body => {
92
+ 'updateUser' => {
93
+ 'UserID' => user_id,
94
+ 'ClientID' => @client_id,
95
+ 'UserName' => user_name,
96
+ 'FirstName' => first_name,
97
+ 'LastName' => last_name,
98
+ 'CountryCode' => 'USA',
99
+ 'Email' => email,
100
+ 'IsActive' => true,
101
+ 'PointBalance' => balance,
102
+ 'OrganizationID' => organization_id
103
+ }
104
+ })[:update_user]
105
+ end
106
+
107
+
108
+ def create_user attrs = {}
109
+ update_user -1, attrs
110
+ end
111
+
112
+ def return_popular_products user_id
113
+ request_with_token(:return_popular_products , :body => { 'userId' => user_id} )[:products][:product]
114
+ end
115
+
116
+ protected
117
+
118
+ def mock?
119
+ @mode == :mock
120
+ end
121
+
122
+ def update_token
123
+ @token = return_token
124
+ @new_token_callback.call(@token) if @new_token_callback
125
+ end
126
+
127
+ def inject_token params = {}
128
+ (params[:body] ||= {})['Token'] = @token
129
+ end
130
+
131
+ def request_with_token method_name, params
132
+ update_token unless @token
133
+ inject_token params
134
+ request method_name, params
135
+ rescue InvalidToken
136
+ update_token
137
+ inject_token params
138
+ request method_name, params
139
+ end
140
+
141
+ def request method_name, params
142
+
143
+ if mock?
144
+ #TODO
145
+ end
146
+
147
+ response = Service.client.request(:wsdl, method_name , params).to_hash
148
+
149
+ logger.debug response.inspect
150
+
151
+ result = response[:"#{method_name}_response"][:"#{method_name}_result"]
152
+
153
+ unless (error_message = result.delete(:error_message).to_s).nil?
154
+ raise InvalidToken if error_message.start_with?("Invalid Token")
155
+ raise InvalidAccount if error_message.start_with?("Invalid Account Number")
156
+ raise InvalidUser if error_message.start_with?("Invalid User")
157
+ raise UserAlreadyExists if error_message.start_with?("User Name:") && error_message.end_with?("Please enter a different user name.")
158
+ end
159
+
160
+ result
161
+ rescue Savon::SOAP::Fault, Savon::HTTP::Error => ex
162
+ logger.error ex.to_s
163
+ logger.error ex.backtrace.inspect
164
+ raise ConnectionError.new
165
+ end
166
+ end
167
+ end
@@ -1,3 +1,3 @@
1
1
  module RewardStation
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -0,0 +1,174 @@
1
+ module SAML
2
+ class AuthResponse
3
+
4
+ attr_accessor :request, :document, :logger
5
+ include Onelogin::Saml::Codeing
6
+
7
+ def initialize(request, logger = nil)
8
+ raise ArgumentError.new("Response cannot be nil") if request.nil?
9
+ self.logger = logger || ActiveRecord::Base.logger
10
+ self.request = inflate(decode(request))
11
+ self.document = Nokogiri::XML(self.request)
12
+ end
13
+
14
+ def get_sp_response_to
15
+ document.xpath("//samlp:AuthnRequest").first["ID"]
16
+ end
17
+
18
+ def get_sp_destination
19
+ Settings.sso.sp_destination
20
+ end
21
+
22
+ def get_idp_issuer
23
+ Settings.host
24
+ end
25
+
26
+ # def get_sp_audience
27
+ # Settings.sso.sp_audience
28
+ # end
29
+
30
+
31
+ #
32
+ def xml_time_format time
33
+ time.strftime("%Y-%m-%dT%H:%M:%SZ")
34
+ end
35
+
36
+
37
+ def response_url(name_id, params={})
38
+ prepared_result = create(name_id)
39
+ base64_request = encode(prepared_result)
40
+ # deflated_request = deflate(create(name_id))
41
+ # base64_request = encode(deflated_request)
42
+ base64_request.gsub!(/\s/,"")
43
+ # encoded_response = escape(base64_request)
44
+ #
45
+ # encoded_response
46
+ ## request_params = "?SAMLResponse=" + encoded_response
47
+ #
48
+ # params.each_pair do |key, value|
49
+ # request_params << "&#{key}=#{escape(value.to_s)}"
50
+ # end
51
+ # Settings.sso.sp_destination+ request_params
52
+ end
53
+
54
+ def create(name_id)
55
+ time_line = Time.now.utc
56
+ request_id = UUID.new.generate
57
+ assert_id = UUID.new.generate
58
+
59
+ time = xml_time_format(time_line)
60
+ assertion = build_assertion_content(name_id, assert_id, time_line)
61
+
62
+ # assertion_parsed = Nokogiri::XML(assertion)
63
+ # assertion_parsed.xpath("//saml:Assertion//saml:Issuer").first.add_next_sibling(make_signature(assertion, assert_id))
64
+
65
+ response = Builder::XmlMarkup.new
66
+ response.tag!('samlp:Response', {"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
67
+ "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
68
+ "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#",
69
+ "ID" => request_id,
70
+ "Version" => "2.0",
71
+ "InResponseTo" => get_sp_response_to,
72
+ "IssueInstant" => time,
73
+ "Destination" => get_sp_destination}) do
74
+ response.tag!("saml:Issuer", get_idp_issuer)
75
+ response.tag!("samlp:Status") do
76
+ response.tag!("samlp:StatusCode", "Value" => "urn:oasis:names:tc:SAML:2.0:status:Success")
77
+ end
78
+ response << build_assertion_content(name_id, assert_id, time_line, make_signature(assertion, assert_id))
79
+ #todo if insert node this way - need to canonicalize. nokogiri 1.4.4 has no possibility to do it. only it's branch can do it:(
80
+ # response << assertion_parsed.xpath("//saml:Assertion").canonicalize.to_s
81
+ end
82
+ response.target!
83
+ end
84
+
85
+
86
+ def build_assertion_content(name_id, assert_id, time_line, sign =nil)
87
+ time_and_ten = xml_time_format(time_line + 10.minutes)
88
+ time = xml_time_format(time_line)
89
+
90
+ xml = Builder::XmlMarkup.new
91
+
92
+ xml.tag!('saml:Assertion', {"xmlns:saml"=>"urn:oasis:names:tc:SAML:2.0:assertion", "ID"=>assert_id, "Version"=>"2.0", "IssueInstant"=> time}) do
93
+ xml.tag!("saml:Issuer", get_idp_issuer)
94
+
95
+ xml << sign if sign.present?
96
+
97
+ xml.tag! "saml:Subject" do
98
+ xml.tag!("saml:NameID", {"Format"=>"urn:oasis:names:tc:SAML:2.0:nameid-format:transient"}, name_id)
99
+ xml.tag!("saml:SubjectConfirmation", {"Method" => "urn:oasis:names:tc:SAML:2.0:cm:bearer"}) do
100
+ xml.tag!("saml:SubjectConfirmationData", {"InResponseTo" => get_sp_response_to, "Recipient" => get_sp_destination, "NotOnOrAfter" => time_and_ten})
101
+ end
102
+ end
103
+
104
+ # xml.tag!("saml:Conditions", {"NotBefore" => time, "NotOnOrAfter" => time_and_ten}) do
105
+ # xml.tag!("saml:AudienceRestriction") do
106
+ # xml.tag!("saml:Audience", get_sp_audience)
107
+ # end
108
+ # end
109
+
110
+ xml.tag!("saml:AuthnStatement", {"AuthnInstant" => time, "SessionIndex" => assert_id}) do
111
+ xml.tag!("saml:AuthnContext") do
112
+ xml.tag!("saml:AuthnContextClassRef", "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport")
113
+ end
114
+ end
115
+ end
116
+ xml.target!
117
+ end
118
+
119
+ def make_signature(assertion, assert_id)
120
+ certificate = File.read("#{Rails.root}/config/cert/#{Rails.env}/idp.ignite.com.crt")
121
+ # certificate = File.read("#{Rails.root}/config/cert/development/IdpCertificate.cer")
122
+ certificate.gsub!("-----BEGIN CERTIFICATE-----", "")
123
+ certificate.gsub!("-----END CERTIFICATE-----", "")
124
+
125
+ sign_info_xml_builder = Builder::XmlMarkup.new
126
+ sign_info_xml_builder.tag!("ds:SignedInfo", {"xmlns:ds"=>"http://www.w3.org/2000/09/xmldsig#"}) do
127
+ sign_info_xml_builder.tag!("ds:CanonicalizationMethod", {"Algorithm"=>"http://www.w3.org/2001/10/xml-exc-c14n#"})
128
+ sign_info_xml_builder.tag!("ds:SignatureMethod", {"Algorithm"=>"http://www.w3.org/2000/09/xmldsig#rsa-sha1"})
129
+ sign_info_xml_builder.tag!("ds:Reference", {"URI" => "##{assert_id}"}) do
130
+ sign_info_xml_builder.tag!("ds:Transforms") do
131
+ sign_info_xml_builder.tag!("ds:Transform", {"Algorithm"=>"http://www.w3.org/2000/09/xmldsig#enveloped-signature"})
132
+ sign_info_xml_builder.tag!("ds:Transform", {"Algorithm"=>"http://www.w3.org/2001/10/xml-exc-c14n#"})
133
+ end
134
+ sign_info_xml_builder.tag!("ds:DigestMethod", {"Algorithm"=>"http://www.w3.org/2000/09/xmldsig#sha1"})
135
+ sign_info_xml_builder.tag!("ds:DigestValue", {"URI" => assert_id}, signature_digest_value(assertion))
136
+ end
137
+ end
138
+ sign_info_xml = sign_info_xml_builder.target!
139
+ s_value = signature_sign_value(sign_info_xml)
140
+ xml = Builder::XmlMarkup.new
141
+ xml.tag!('ds:Signature', {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}) do
142
+ xml << sign_info_xml
143
+ xml.tag!("ds:SignatureValue", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}, s_value)
144
+ xml.tag!('ds:KeyInfo') do
145
+ xml.tag!('ds:X509Data') do
146
+ xml.tag!('ds:X509Certificate', certificate)
147
+ end
148
+ end
149
+ end
150
+ xml.target!
151
+ end
152
+
153
+
154
+ def signature_sign_value(xml)
155
+ document = XMLSecurity::SignedDocument.new(xml)
156
+ canoner = XML::Util::XmlCanonicalizer.new(false, true)
157
+ signed_info_element = REXML::XPath.first(document, "//ds:SignedInfo", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"})
158
+ canon_string = canoner.canonicalize(signed_info_element)
159
+ # private_key = OpenSSL::PKey::RSA.new(File.read("#{Rails.root}/config/cert/#{Rails.env}/idp.ignite.com.key"))
160
+ #private_key = OpenSSL::PKey::RSA.new(File.read("#{Rails.root}/config/cert/development/SPkey.key"))
161
+ private_key = OpenSSL::PKey::RSA.new(File.read("#{Rails.root}/config/cert/development/IgniteKeyDecrypted.key"))
162
+
163
+ sig = private_key.sign(OpenSSL::Digest::SHA1.new, canon_string)
164
+ Base64.encode64(sig).chomp
165
+ end
166
+
167
+ def signature_digest_value(xml)
168
+ document = XMLSecurity::SignedDocument.new(xml)
169
+ canoner = XML::Util::XmlCanonicalizer.new(false, true)
170
+ canon_hashed_element = canoner.canonicalize(document)
171
+ Base64.encode64(Digest::SHA1.digest(canon_hashed_element)).chomp
172
+ end
173
+ end
174
+ end