reward_station 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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