mislav_contacts 0.2.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ module Contacts
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 2
5
+ TINY = 7
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,163 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), %w{.. .. vendor windowslivelogin}))
2
+
3
+ require 'rubygems'
4
+ require 'hpricot'
5
+ require 'uri'
6
+ require 'yaml'
7
+
8
+ module Contacts
9
+ # = How I can fetch Windows Live Contacts?
10
+ # To gain access to a Windows Live user's data in the Live Contacts service,
11
+ # a third-party developer first must ask the owner for permission. You must
12
+ # do that through Windows Live Delegated Authentication.
13
+ #
14
+ # This library give you access to Windows Live Delegated Authentication System
15
+ # and Windows Live Contacts API. Just follow the steps below and be happy!
16
+ #
17
+ # === Registering your app
18
+ # First of all, follow the steps in this
19
+ # page[http://msdn.microsoft.com/en-us/library/cc287659.aspx] to register your
20
+ # app.
21
+ #
22
+ # === Configuring your Windows Live YAML
23
+ # After registering your app, you will have an *appid*, a <b>secret key</b> and
24
+ # a <b>return URL</b>. Use their values to fill in the config/contacts.yml file.
25
+ # The policy URL field inside the YAML config file must contain the URL
26
+ # of the privacy policy of your Web site for Delegated Authentication.
27
+ #
28
+ # === Authenticating your user and fetching his contacts
29
+ #
30
+ # wl = Contacts::WindowsLive.new
31
+ # auth_url = wl.get_authentication_url
32
+ #
33
+ # Use that *auth_url* to redirect your user to Windows Live. He will authenticate
34
+ # there and Windows Live will POST to your return URL. You have to get the
35
+ # body of that POST, let's call it post_body. (if you're using Rails, you can
36
+ # get the POST body through request.raw_post, in the context of an action inside
37
+ # ActionController)
38
+ #
39
+ # Now, to fetch his contacts, just do this:
40
+ #
41
+ # contacts = wl.contacts(post_body)
42
+ # #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
43
+ # ['William Paginate', 'will.paginate@gmail.com'], ...
44
+ # ]
45
+ #--
46
+ # This class has two responsibilities:
47
+ # 1. Access the Windows Live Contacts API through Delegated Authentication
48
+ # 2. Import contacts from Windows Live and deliver it inside an Array
49
+ #
50
+ class WindowsLive
51
+ CONFIG_FILE = File.dirname(__FILE__) + '/../config/contacts.yml'
52
+
53
+ # Initialize a new WindowsLive object.
54
+ #
55
+ # ==== Paramaters
56
+ # * config_file <String>:: The contacts YAML config file name
57
+ #--
58
+ # You can check an example of a config file inside config/ directory
59
+ #
60
+ def initialize(config_file=CONFIG_FILE)
61
+ confs = YAML.load_file(config_file)['windows_live']
62
+ @wll = WindowsLiveLogin.new(confs['appid'], confs['secret'], confs['security_algorithm'],
63
+ nil, confs['policy_url'], confs['return_url'])
64
+ end
65
+
66
+
67
+ # Windows Live Contacts API need to authenticate the user that is giving you
68
+ # access to his contacts. To do that, you must give him a URL. That method
69
+ # generates that URL. The user must access that URL, and after he has done
70
+ # authentication, hi will be redirected to your application.
71
+ #
72
+ def get_authentication_url
73
+ @wll.getConsentUrl("Contacts.Invite")
74
+ end
75
+
76
+ # After the user has been authenticaded, Windows Live Delegated Authencation
77
+ # Service redirects to your application, through a POST HTTP method. Along
78
+ # with the POST, Windows Live send to you a Consent that you must process
79
+ # to access the user's contacts. This method process the Consent
80
+ # to you.
81
+ #
82
+ # ==== Paramaters
83
+ # * consent <String>:: A string containing the Consent given to you inside
84
+ # the redirection POST from Windows Live
85
+ #
86
+ def process_consent(consent)
87
+ consent.strip!
88
+ consent = URI.unescape(consent)
89
+ @consent_token = @wll.processConsent(consent)
90
+ end
91
+
92
+ # This method return the user's contacts inside an Array in the following
93
+ # format:
94
+ #
95
+ # [
96
+ # ['Brad Fitzgerald', 'fubar@gmail.com'],
97
+ # [nil, 'nagios@hotmail.com'],
98
+ # ['William Paginate', 'will.paginate@yahoo.com'] ...
99
+ # ]
100
+ #
101
+ # ==== Paramaters
102
+ # * consent <String>:: A string containing the Consent given to you inside
103
+ # the redirection POST from Windows Live
104
+ #
105
+ def contacts(consent)
106
+ process_consent(consent)
107
+ contacts_xml = access_live_contacts_api()
108
+ contacts_list = WindowsLive.parse_xml(contacts_xml)
109
+ end
110
+
111
+ # This method access the Windows Live Contacts API Web Service to get
112
+ # the XML contacts document
113
+ #
114
+ def access_live_contacts_api
115
+ http = http = Net::HTTP.new('livecontacts.services.live.com', 443)
116
+ http.use_ssl = true
117
+
118
+ response = nil
119
+ http.start do |http|
120
+ request = Net::HTTP::Get.new("/users/@L@#{@consent_token.locationid}/rest/invitationsbyemail", {"Authorization" => "DelegatedToken dt=\"#{@consent_token.delegationtoken}\""})
121
+ response = http.request(request)
122
+ end
123
+
124
+ return response.body
125
+ end
126
+
127
+ # This method parses the XML Contacts document and returns the contacts
128
+ # inside an Array
129
+ #
130
+ # ==== Paramaters
131
+ # * xml <String>:: A string containing the XML contacts document
132
+ #
133
+ def self.parse_xml(xml)
134
+ doc = Hpricot::XML(xml)
135
+
136
+ contacts = []
137
+ doc.search('/livecontacts/contacts/contact').each do |contact|
138
+ email = contact.at('/preferredemail').inner_text
139
+ email.strip!
140
+
141
+ first_name = last_name = nil
142
+ if first_name = contact.at('/profiles/personal/firstname')
143
+ first_name = first_name.inner_text.strip
144
+ end
145
+
146
+ if last_name = contact.at('/profiles/personal/lastname')
147
+ last_name = last_name.inner_text.strip
148
+ end
149
+
150
+ name = nil
151
+ if !first_name.nil? || !last_name.nil?
152
+ name = "#{first_name} #{last_name}"
153
+ name.strip!
154
+ end
155
+ new_contact = Contact.new(email, name)
156
+ contacts << new_contact
157
+ end
158
+
159
+ return contacts
160
+ end
161
+ end
162
+
163
+ end
@@ -0,0 +1,238 @@
1
+ require 'rubygems'
2
+ require 'hpricot'
3
+ require 'md5'
4
+ require 'net/https'
5
+ require 'uri'
6
+ require 'yaml'
7
+ require 'json' unless defined? ActiveSupport::JSON
8
+
9
+ module Contacts
10
+ # = How I can fetch Yahoo Contacts?
11
+ # To gain access to a Yahoo user's data in the Yahoo Address Book Service,
12
+ # a third-party developer first must ask the owner for permission. You must
13
+ # do that through Yahoo Browser Based Authentication (BBAuth).
14
+ #
15
+ # This library give you access to Yahoo BBAuth and Yahoo Address Book API.
16
+ # Just follow the steps below and be happy!
17
+ #
18
+ # === Registering your app
19
+ # First of all, follow the steps in this
20
+ # page[http://developer.yahoo.com/wsregapp/] to register your app. If you need
21
+ # some help with that form, you can get it
22
+ # here[http://developer.yahoo.com/auth/appreg.html]. Just two tips: inside
23
+ # <b>Required access scopes</b> in that registration form, choose
24
+ # <b>Yahoo! Address Book with Read Only access</b>. Inside
25
+ # <b>Authentication method</b> choose <b>Browser Based Authentication</b>.
26
+ #
27
+ # === Configuring your Yahoo YAML
28
+ # After registering your app, you will have an <b>application id</b> and a
29
+ # <b>shared secret</b>. Use their values to fill in the config/contacts.yml
30
+ # file.
31
+ #
32
+ # === Authenticating your user and fetching his contacts
33
+ #
34
+ # yahoo = Contacts::Yahoo.new
35
+ # auth_url = yahoo.get_authentication_url
36
+ #
37
+ # Use that *auth_url* to redirect your user to Yahoo BBAuth. He will authenticate
38
+ # there and Yahoo will redirect to your application entrypoint URL (that you provided
39
+ # while registering your app with Yahoo). You have to get the path of that
40
+ # redirect, let's call it path (if you're using Rails, you can get it through
41
+ # request.request_uri, in the context of an action inside ActionController)
42
+ #
43
+ # Now, to fetch his contacts, just do this:
44
+ #
45
+ # contacts = wl.contacts(path)
46
+ # #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
47
+ # ['William Paginate', 'will.paginate@gmail.com'], ...
48
+ # ]
49
+ #--
50
+ # This class has two responsibilities:
51
+ # 1. Access the Yahoo Address Book API through Delegated Authentication
52
+ # 2. Import contacts from Yahoo Mail and deliver it inside an Array
53
+ #
54
+ class Yahoo
55
+ AUTH_DOMAIN = "https://api.login.yahoo.com"
56
+ AUTH_PATH = "/WSLogin/V1/wslogin?appid=#appid&ts=#ts"
57
+ CREDENTIAL_PATH = "/WSLogin/V1/wspwtoken_login?appid=#appid&ts=#ts&token=#token"
58
+ ADDRESS_BOOK_DOMAIN = "address.yahooapis.com"
59
+ ADDRESS_BOOK_PATH = "/v1/searchContacts?format=json&fields=name,email&appid=#appid&WSSID=#wssid"
60
+ CONFIG_FILE = File.dirname(__FILE__) + '/../config/contacts.yml'
61
+
62
+ attr_reader :appid, :secret, :token, :wssid, :cookie
63
+
64
+ # Initialize a new Yahoo object.
65
+ #
66
+ # ==== Paramaters
67
+ # * config_file <String>:: The contacts YAML config file name
68
+ #--
69
+ # You can check an example of a config file inside config/ directory
70
+ #
71
+ def initialize(config_file=CONFIG_FILE)
72
+ confs = YAML.load_file(config_file)['yahoo']
73
+ @appid = confs['appid']
74
+ @secret = confs['secret']
75
+ end
76
+
77
+ # Yahoo Address Book API need to authenticate the user that is giving you
78
+ # access to his contacts. To do that, you must give him a URL. This method
79
+ # generates that URL. The user must access that URL, and after he has done
80
+ # authentication, hi will be redirected to your application.
81
+ #
82
+ def get_authentication_url
83
+ path = AUTH_PATH.clone
84
+ path.sub!(/#appid/, @appid)
85
+
86
+ timestamp = Time.now.utc.to_i
87
+ path.sub!(/#ts/, timestamp.to_s)
88
+
89
+ signature = MD5.hexdigest(path + @secret)
90
+ return AUTH_DOMAIN + "#{path}&sig=#{signature}"
91
+ end
92
+
93
+ # This method return the user's contacts inside an Array in the following
94
+ # format:
95
+ #
96
+ # [
97
+ # ['Brad Fitzgerald', 'fubar@gmail.com'],
98
+ # [nil, 'nagios@hotmail.com'],
99
+ # ['William Paginate', 'will.paginate@yahoo.com'] ...
100
+ # ]
101
+ #
102
+ # ==== Paramaters
103
+ # * path <String>:: The path of the redirect request that Yahoo sent to you
104
+ # after authenticating the user
105
+ #
106
+ def contacts(path)
107
+ begin
108
+ validate_signature(path)
109
+ credentials = access_user_credentials()
110
+ parse_credentials(credentials)
111
+ contacts_json = access_address_book_api()
112
+ Yahoo.parse_contacts(contacts_json)
113
+ rescue Exception => e
114
+ "Error #{e.class}: #{e.message}."
115
+ end
116
+ end
117
+
118
+ # This method processes and validates the redirect request that Yahoo send to
119
+ # you. Validation is done to verify that the request was really made by
120
+ # Yahoo. Processing is done to get the token.
121
+ #
122
+ # ==== Paramaters
123
+ # * path <String>:: The path of the redirect request that Yahoo sent to you
124
+ # after authenticating the user
125
+ #
126
+ def validate_signature(path)
127
+ path.match(/^(.+)&sig=(\w{32})$/)
128
+ path_without_sig = $1
129
+ sig = $2
130
+
131
+ if sig == MD5.hexdigest(path_without_sig + @secret)
132
+ path.match(/token=(.+?)&/)
133
+ @token = $1
134
+ return true
135
+ else
136
+ raise 'Signature not valid. This request may not have been sent from Yahoo.'
137
+ end
138
+ end
139
+
140
+ # This method accesses Yahoo to retrieve the user's credentials.
141
+ #
142
+ def access_user_credentials
143
+ url = get_credential_url()
144
+ uri = URI.parse(url)
145
+
146
+ http = http = Net::HTTP.new(uri.host, uri.port)
147
+ http.use_ssl = true
148
+
149
+ response = nil
150
+ http.start do |http|
151
+ request = Net::HTTP::Get.new("#{uri.path}?#{uri.query}")
152
+ response = http.request(request)
153
+ end
154
+
155
+ return response.body
156
+ end
157
+
158
+ # This method generates the URL that you must access to get user's
159
+ # credentials.
160
+ #
161
+ def get_credential_url
162
+ path = CREDENTIAL_PATH.clone
163
+ path.sub!(/#appid/, @appid)
164
+
165
+ path.sub!(/#token/, @token)
166
+
167
+ timestamp = Time.now.utc.to_i
168
+ path.sub!(/#ts/, timestamp.to_s)
169
+
170
+ signature = MD5.hexdigest(path + @secret)
171
+ return AUTH_DOMAIN + "#{path}&sig=#{signature}"
172
+ end
173
+
174
+ # This method parses the user's credentials to generate the WSSID and
175
+ # Coookie that are needed to give you access to user's address book.
176
+ #
177
+ # ==== Paramaters
178
+ # * xml <String>:: A String containing the user's credentials
179
+ #
180
+ def parse_credentials(xml)
181
+ doc = Hpricot::XML(xml)
182
+ @wssid = doc.at('/BBAuthTokenLoginResponse/Success/WSSID').inner_text.strip
183
+ @cookie = doc.at('/BBAuthTokenLoginResponse/Success/Cookie').inner_text.strip
184
+ end
185
+
186
+ # This method accesses the Yahoo Address Book API and retrieves the user's
187
+ # contacts in JSON.
188
+ #
189
+ def access_address_book_api
190
+ http = http = Net::HTTP.new(ADDRESS_BOOK_DOMAIN, 80)
191
+
192
+ response = nil
193
+ http.start do |http|
194
+ path = ADDRESS_BOOK_PATH.clone
195
+ path.sub!(/#appid/, @appid)
196
+ path.sub!(/#wssid/, @wssid)
197
+
198
+ request = Net::HTTP::Get.new(path, {'Cookie' => @cookie})
199
+ response = http.request(request)
200
+ end
201
+
202
+ return response.body
203
+ end
204
+
205
+ # This method parses the JSON contacts document and returns an array
206
+ # contaning all the user's contacts.
207
+ #
208
+ # ==== Parameters
209
+ # * json <String>:: A String of user's contacts in JSON format
210
+ #
211
+ def self.parse_contacts(json)
212
+ contacts = []
213
+ people = if defined? ActiveSupport::JSON
214
+ ActiveSupport::JSON.decode(json)
215
+ else
216
+ JSON.parse(json)
217
+ end
218
+
219
+ people['contacts'].each do |contact|
220
+ name = nil
221
+ email = nil
222
+ contact['fields'].each do |field|
223
+ case field['type']
224
+ when 'email'
225
+ email = field['data']
226
+ email.strip!
227
+ when 'name'
228
+ name = "#{field['first']} #{field['last']}"
229
+ name.strip!
230
+ end
231
+ end
232
+ contacts.push Contact.new(email, name)
233
+ end
234
+ return contacts
235
+ end
236
+
237
+ end
238
+ end
@@ -0,0 +1 @@
1
+ require 'contacts'
@@ -0,0 +1,1151 @@
1
+ #######################################################################
2
+ # This SDK is provide by Microsoft. I just use it for the delegated
3
+ # authentication part. You can find it in http://www.microsoft.com/downloads/details.aspx?FamilyId=24195B4E-6335-4844-A71D-7D395D20E67B&displaylang=en
4
+ #
5
+ # Author: Hugo Baraúna (hugo.barauna@gmail.com)
6
+ #######################################################################
7
+
8
+
9
+ #######################################################################
10
+ #######################################################################
11
+ # FILE: windowslivelogin.rb
12
+ #
13
+ # DESCRIPTION: Sample implementation of Web Authentication and
14
+ # Delegated Authentication protocol in Ruby. Also
15
+ # includes trusted sign-in and application verification
16
+ # sample implementations.
17
+ #
18
+ # VERSION: 1.1
19
+ #
20
+ # Copyright (c) 2008 Microsoft Corporation. All Rights Reserved.
21
+ #######################################################################
22
+
23
+ require 'cgi'
24
+ require 'uri'
25
+ require 'base64'
26
+ require 'openssl'
27
+ require 'net/https'
28
+ require 'rexml/document'
29
+
30
+ class WindowsLiveLogin
31
+
32
+ #####################################################################
33
+ # Stub implementation for logging errors. If you want to enable
34
+ # debugging output using the default mechanism, specify true.
35
+ # By default, debug information will be printed to the standard
36
+ # error output and should be visible in the web server logs.
37
+ #####################################################################
38
+ def setDebug(flag)
39
+ @debug = flag
40
+ end
41
+
42
+ #####################################################################
43
+ # Stub implementation for logging errors. By default, this function
44
+ # does nothing if the debug flag has not been set with setDebug.
45
+ # Otherwise, it tries to log the error message.
46
+ #####################################################################
47
+ def debug(error)
48
+ return unless @debug
49
+ return if error.nil? or error.empty?
50
+ warn("Windows Live ID Authentication SDK #{error}")
51
+ nil
52
+ end
53
+
54
+ #####################################################################
55
+ # Stub implementation for handling a fatal error.
56
+ #####################################################################
57
+ def fatal(error)
58
+ debug(error)
59
+ raise(error)
60
+ end
61
+
62
+ #####################################################################
63
+ # Initialize the WindowsLiveLogin module with the application ID,
64
+ # secret key, and security algorithm.
65
+ #
66
+ # We recommend that you employ strong measures to protect the
67
+ # secret key. The secret key should never be exposed to the Web
68
+ # or other users.
69
+ #
70
+ # Be aware that if you do not supply these settings at
71
+ # initialization time, you may need to set the corresponding
72
+ # properties manually.
73
+ #
74
+ # For Delegated Authentication, you may optionally specify the
75
+ # privacy policy URL and return URL. If you do not specify these
76
+ # values here, the default values that you specified when you
77
+ # registered your application will be used.
78
+ #
79
+ # The 'force_delauth_nonprovisioned' flag also indicates whether
80
+ # your application is registered for Delegated Authentication
81
+ # (that is, whether it uses an application ID and secret key). We
82
+ # recommend that your Delegated Authentication application always
83
+ # be registered for enhanced security and functionality.
84
+ #####################################################################
85
+ def initialize(appid=nil, secret=nil, securityalgorithm=nil,
86
+ force_delauth_nonprovisioned=nil,
87
+ policyurl=nil, returnurl=nil)
88
+ self.force_delauth_nonprovisioned = force_delauth_nonprovisioned
89
+ self.appid = appid if appid
90
+ self.secret = secret if secret
91
+ self.securityalgorithm = securityalgorithm if securityalgorithm
92
+ self.policyurl = policyurl if policyurl
93
+ self.returnurl = returnurl if returnurl
94
+ end
95
+
96
+ #####################################################################
97
+ # Initialize the WindowsLiveLogin module from a settings file.
98
+ #
99
+ # 'settingsFile' specifies the location of the XML settings file
100
+ # that contains the application ID, secret key, and security
101
+ # algorithm. The file is of the following format:
102
+ #
103
+ # <windowslivelogin>
104
+ # <appid>APPID</appid>
105
+ # <secret>SECRET</secret>
106
+ # <securityalgorithm>wsignin1.0</securityalgorithm>
107
+ # </windowslivelogin>
108
+ #
109
+ # In a Delegated Authentication scenario, you may also specify
110
+ # 'returnurl' and 'policyurl' in the settings file, as shown in the
111
+ # Delegated Authentication samples.
112
+ #
113
+ # We recommend that you store the WindowsLiveLogin settings file
114
+ # in an area on your server that cannot be accessed through the
115
+ # Internet. This file contains important confidential information.
116
+ #####################################################################
117
+ def self.initFromXml(settingsFile)
118
+ o = self.new
119
+ settings = o.parseSettings(settingsFile)
120
+
121
+ o.setDebug(settings['debug'] == 'true')
122
+ o.force_delauth_nonprovisioned =
123
+ (settings['force_delauth_nonprovisioned'] == 'true')
124
+
125
+ o.appid = settings['appid']
126
+ o.secret = settings['secret']
127
+ o.oldsecret = settings['oldsecret']
128
+ o.oldsecretexpiry = settings['oldsecretexpiry']
129
+ o.securityalgorithm = settings['securityalgorithm']
130
+ o.policyurl = settings['policyurl']
131
+ o.returnurl = settings['returnurl']
132
+ o.baseurl = settings['baseurl']
133
+ o.secureurl = settings['secureurl']
134
+ o.consenturl = settings['consenturl']
135
+ o
136
+ end
137
+
138
+ #####################################################################
139
+ # Sets the application ID. Use this method if you did not specify
140
+ # an application ID at initialization.
141
+ #####################################################################
142
+ def appid=(appid)
143
+ if (appid.nil? or appid.empty?)
144
+ return if force_delauth_nonprovisioned
145
+ fatal("Error: appid: Null application ID.")
146
+ end
147
+ if (not appid =~ /^\w+$/)
148
+ fatal("Error: appid: Application ID must be alpha-numeric: " + appid)
149
+ end
150
+ @appid = appid
151
+ end
152
+
153
+ #####################################################################
154
+ # Returns the application ID.
155
+ #####################################################################
156
+ def appid
157
+ if (@appid.nil? or @appid.empty?)
158
+ fatal("Error: appid: App ID was not set. Aborting.")
159
+ end
160
+ @appid
161
+ end
162
+
163
+ #####################################################################
164
+ # Sets your secret key. Use this method if you did not specify
165
+ # a secret key at initialization.
166
+ #####################################################################
167
+ def secret=(secret)
168
+ if (secret.nil? or secret.empty?)
169
+ return if force_delauth_nonprovisioned
170
+ fatal("Error: secret=: Secret must be non-null.")
171
+ end
172
+ if (secret.size < 16)
173
+ fatal("Error: secret=: Secret must be at least 16 characters.")
174
+ end
175
+ @signkey = derive(secret, "SIGNATURE")
176
+ @cryptkey = derive(secret, "ENCRYPTION")
177
+ end
178
+
179
+ #####################################################################
180
+ # Sets your old secret key.
181
+ #
182
+ # Use this property to set your old secret key if you are in the
183
+ # process of transitioning to a new secret key. You may need this
184
+ # property because the Windows Live ID servers can take up to
185
+ # 24 hours to propagate a new secret key after you have updated
186
+ # your application settings.
187
+ #
188
+ # If an old secret key is specified here and has not expired
189
+ # (as determined by the oldsecretexpiry setting), it will be used
190
+ # as a fallback if token decryption fails with the new secret
191
+ # key.
192
+ #####################################################################
193
+ def oldsecret=(secret)
194
+ return if (secret.nil? or secret.empty?)
195
+ if (secret.size < 16)
196
+ fatal("Error: oldsecret=: Secret must be at least 16 characters.")
197
+ end
198
+ @oldsignkey = derive(secret, "SIGNATURE")
199
+ @oldcryptkey = derive(secret, "ENCRYPTION")
200
+ end
201
+
202
+ #####################################################################
203
+ # Sets the expiry time for your old secret key.
204
+ #
205
+ # After this time has passed, the old secret key will no longer be
206
+ # used even if token decryption fails with the new secret key.
207
+ #
208
+ # The old secret expiry time is represented as the number of seconds
209
+ # elapsed since January 1, 1970.
210
+ #####################################################################
211
+ def oldsecretexpiry=(timestamp)
212
+ return if (timestamp.nil? or timestamp.empty?)
213
+ timestamp = timestamp.to_i
214
+ fatal("Error: oldsecretexpiry=: Invalid timestamp: #{timestamp}") if (timestamp <= 0)
215
+ @oldsecretexpiry = Time.at timestamp
216
+ end
217
+
218
+ #####################################################################
219
+ # Gets the old secret key expiry time.
220
+ #####################################################################
221
+ attr_accessor :oldsecretexpiry
222
+
223
+ #####################################################################
224
+ # Sets or gets the version of the security algorithm being used.
225
+ #####################################################################
226
+ attr_accessor :securityalgorithm
227
+
228
+ def securityalgorithm
229
+ if(@securityalgorithm.nil? or @securityalgorithm.empty?)
230
+ "wsignin1.0"
231
+ else
232
+ @securityalgorithm
233
+ end
234
+ end
235
+
236
+ #####################################################################
237
+ # Sets a flag that indicates whether Delegated Authentication
238
+ # is non-provisioned (i.e. does not use an application ID or secret
239
+ # key).
240
+ #####################################################################
241
+ attr_accessor :force_delauth_nonprovisioned
242
+
243
+ #####################################################################
244
+ # Sets the privacy policy URL, to which the Windows Live ID consent
245
+ # service redirects users to view the privacy policy of your Web
246
+ # site for Delegated Authentication.
247
+ #####################################################################
248
+ def policyurl=(policyurl)
249
+ if ((policyurl.nil? or policyurl.empty?) and force_delauth_nonprovisioned)
250
+ fatal("Error: policyurl=: Invalid policy URL specified.")
251
+ end
252
+ @policyurl = policyurl
253
+ end
254
+
255
+ #####################################################################
256
+ # Gets the privacy policy URL for your site.
257
+ #####################################################################
258
+ def policyurl
259
+ if (@policyurl.nil? or @policyurl.empty?)
260
+ debug("Warning: In the initial release of Del Auth, a Policy URL must be configured in the SDK for both provisioned and non-provisioned scenarios.")
261
+ raise("Error: policyurl: Policy URL must be set in a Del Auth non-provisioned scenario. Aborting.") if force_delauth_nonprovisioned
262
+ end
263
+ @policyurl
264
+ end
265
+
266
+ #####################################################################
267
+ # Sets the return URL--the URL on your site to which the consent
268
+ # service redirects users (along with the action, consent token,
269
+ # and application context) after they have successfully provided
270
+ # consent information for Delegated Authentication. This value will
271
+ # override the return URL specified during registration.
272
+ #####################################################################
273
+ def returnurl=(returnurl)
274
+ if ((returnurl.nil? or returnurl.empty?) and force_delauth_nonprovisioned)
275
+ fatal("Error: returnurl=: Invalid return URL specified.")
276
+ end
277
+ @returnurl = returnurl
278
+ end
279
+
280
+
281
+ #####################################################################
282
+ # Returns the return URL of your site.
283
+ #####################################################################
284
+ def returnurl
285
+ if ((@returnurl.nil? or @returnurl.empty?) and force_delauth_nonprovisioned)
286
+ fatal("Error: returnurl: Return URL must be set in a Del Auth non-provisioned scenario. Aborting.")
287
+ end
288
+ @returnurl
289
+ end
290
+
291
+ #####################################################################
292
+ # Sets or gets the base URL to use for the Windows Live Login server. You
293
+ # should not have to change this property. Furthermore, we recommend
294
+ # that you use the Sign In control instead of the URL methods
295
+ # provided here.
296
+ #####################################################################
297
+ attr_accessor :baseurl
298
+
299
+ def baseurl
300
+ if(@baseurl.nil? or @baseurl.empty?)
301
+ "http://login.live.com/"
302
+ else
303
+ @baseurl
304
+ end
305
+ end
306
+
307
+ #####################################################################
308
+ # Sets or gets the secure (HTTPS) URL to use for the Windows Live Login
309
+ # server. You should not have to change this property.
310
+ #####################################################################
311
+ attr_accessor :secureurl
312
+
313
+ def secureurl
314
+ if(@secureurl.nil? or @secureurl.empty?)
315
+ "https://login.live.com/"
316
+ else
317
+ @secureurl
318
+ end
319
+ end
320
+
321
+ #####################################################################
322
+ # Sets or gets the Consent Base URL to use for the Windows Live Consent
323
+ # server. You should not have to use or change this property directly.
324
+ #####################################################################
325
+ attr_accessor :consenturl
326
+
327
+ def consenturl
328
+ if(@consenturl.nil? or @consenturl.empty?)
329
+ "https://consent.live.com/"
330
+ else
331
+ @consenturl
332
+ end
333
+ end
334
+ end
335
+
336
+ #######################################################################
337
+ # Implementation of the basic methods needed for Web Authentication.
338
+ #######################################################################
339
+ class WindowsLiveLogin
340
+ #####################################################################
341
+ # Returns the sign-in URL to use for the Windows Live Login server.
342
+ # We recommend that you use the Sign In control instead.
343
+ #
344
+ # If you specify it, 'context' will be returned as-is in the sign-in
345
+ # response for site-specific use.
346
+ #####################################################################
347
+ def getLoginUrl(context=nil, market=nil)
348
+ url = baseurl + "wlogin.srf?appid=#{appid}"
349
+ url += "&alg=#{securityalgorithm}"
350
+ url += "&appctx=#{CGI.escape(context)}" if context
351
+ url += "&mkt=#{CGI.escape(market)}" if market
352
+ url
353
+ end
354
+
355
+ #####################################################################
356
+ # Returns the sign-out URL to use for the Windows Live Login server.
357
+ # We recommend that you use the Sign In control instead.
358
+ #####################################################################
359
+ def getLogoutUrl(market=nil)
360
+ url = baseurl + "logout.srf?appid=#{appid}"
361
+ url += "&mkt=#{CGI.escape(market)}" if market
362
+ url
363
+ end
364
+
365
+ #####################################################################
366
+ # Holds the user information after a successful sign-in.
367
+ #
368
+ # 'timestamp' is the time as obtained from the SSO token.
369
+ # 'id' is the pairwise unique ID for the user.
370
+ # 'context' is the application context that was originally passed to
371
+ # the sign-in request, if any.
372
+ # 'token' is the encrypted Web Authentication token that contains the
373
+ # UID. This can be cached in a cookie and the UID can be retrieved by
374
+ # calling the processToken method.
375
+ # 'usePersistentCookie?' indicates whether the application is
376
+ # expected to store the user token in a session or persistent
377
+ # cookie.
378
+ #####################################################################
379
+ class User
380
+ attr_reader :timestamp, :id, :context, :token
381
+
382
+ def usePersistentCookie?
383
+ @usePersistentCookie
384
+ end
385
+
386
+
387
+ #####################################################################
388
+ # Initialize the User with time stamp, userid, flags, context and token.
389
+ #####################################################################
390
+ def initialize(timestamp, id, flags, context, token)
391
+ self.timestamp = timestamp
392
+ self.id = id
393
+ self.flags = flags
394
+ self.context = context
395
+ self.token = token
396
+ end
397
+
398
+ private
399
+ attr_writer :timestamp, :id, :flags, :context, :token
400
+
401
+ #####################################################################
402
+ # Sets or gets the Unix timestamp as obtained from the SSO token.
403
+ #####################################################################
404
+ def timestamp=(timestamp)
405
+ raise("Error: User: Null timestamp in token.") unless timestamp
406
+ timestamp = timestamp.to_i
407
+ raise("Error: User: Invalid timestamp: #{timestamp}") if (timestamp <= 0)
408
+ @timestamp = Time.at timestamp
409
+ end
410
+
411
+ #####################################################################
412
+ # Sets or gets the pairwise unique ID for the user.
413
+ #####################################################################
414
+ def id=(id)
415
+ raise("Error: User: Null id in token.") unless id
416
+ raise("Error: User: Invalid id: #{id}") unless (id =~ /^\w+$/)
417
+ @id = id
418
+ end
419
+
420
+ #####################################################################
421
+ # Sets or gets the usePersistentCookie flag for the user.
422
+ #####################################################################
423
+ def flags=(flags)
424
+ @usePersistentCookie = false
425
+ if flags
426
+ @usePersistentCookie = ((flags.to_i % 2) == 1)
427
+ end
428
+ end
429
+ end
430
+
431
+ #####################################################################
432
+ # Processes the sign-in response from the Windows Live sign-in server.
433
+ #
434
+ # 'query' contains the preprocessed POST table, such as that
435
+ # returned by CGI.params or Rails. (The unprocessed POST string
436
+ # could also be used here but we do not recommend it).
437
+ #
438
+ # This method returns a User object on successful sign-in; otherwise
439
+ # it returns nil.
440
+ #####################################################################
441
+ def processLogin(query)
442
+ query = parse query
443
+ unless query
444
+ debug("Error: processLogin: Failed to parse query.")
445
+ return
446
+ end
447
+ action = query['action']
448
+ unless action == 'login'
449
+ debug("Warning: processLogin: query action ignored: #{action}.")
450
+ return
451
+ end
452
+ token = query['stoken']
453
+ context = CGI.unescape(query['appctx']) if query['appctx']
454
+ processToken(token, context)
455
+ end
456
+
457
+ #####################################################################
458
+ # Decodes and validates a Web Authentication token. Returns a User
459
+ # object on success. If a context is passed in, it will be returned
460
+ # as the context field in the User object.
461
+ #####################################################################
462
+ def processToken(token, context=nil)
463
+ if token.nil? or token.empty?
464
+ debug("Error: processToken: Null/empty token.")
465
+ return
466
+ end
467
+ stoken = decodeAndValidateToken token
468
+ stoken = parse stoken
469
+ unless stoken
470
+ debug("Error: processToken: Failed to decode/validate token: #{token}")
471
+ return
472
+ end
473
+ sappid = stoken['appid']
474
+ unless sappid == appid
475
+ debug("Error: processToken: Application ID in token did not match ours: #{sappid}, #{appid}")
476
+ return
477
+ end
478
+ begin
479
+ user = User.new(stoken['ts'], stoken['uid'], stoken['flags'],
480
+ context, token)
481
+ return user
482
+ rescue Exception => e
483
+ debug("Error: processToken: Contents of token considered invalid: #{e}")
484
+ return
485
+ end
486
+ end
487
+
488
+ #####################################################################
489
+ # Returns an appropriate content type and body response that the
490
+ # application handler can return to signify a successful sign-out
491
+ # from the application.
492
+ #
493
+ # When a user signs out of Windows Live or a Windows Live
494
+ # application, a best-effort attempt is made at signing the user out
495
+ # from all other Windows Live applications the user might be signed
496
+ # in to. This is done by calling the handler page for each
497
+ # application with 'action' set to 'clearcookie' in the query
498
+ # string. The application handler is then responsible for clearing
499
+ # any cookies or data associated with the sign-in. After successfully
500
+ # signing the user out, the handler should return a GIF (any GIF)
501
+ # image as response to the 'action=clearcookie' query.
502
+ #####################################################################
503
+ def getClearCookieResponse()
504
+ type = "image/gif"
505
+ content = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7"
506
+ content = Base64.decode64(content)
507
+ return type, content
508
+ end
509
+ end
510
+
511
+ #######################################################################
512
+ # Implementation of the basic methods needed for Delegated
513
+ # Authentication.
514
+ #######################################################################
515
+ class WindowsLiveLogin
516
+ #####################################################################
517
+ # Returns the consent URL to use for Delegated Authentication for
518
+ # the given comma-delimited list of offers.
519
+ #
520
+ # If you specify it, 'context' will be returned as-is in the consent
521
+ # response for site-specific use.
522
+ #
523
+ # The registered/configured return URL can also be overridden by
524
+ # specifying 'ru' here.
525
+ #
526
+ # You can change the language in which the consent page is displayed
527
+ # by specifying a culture ID (For example, 'fr-fr' or 'en-us') in the
528
+ # 'market' parameter.
529
+ #####################################################################
530
+ def getConsentUrl(offers, context=nil, ru=nil, market=nil)
531
+ if (offers.nil? or offers.empty?)
532
+ fatal("Error: getConsentUrl: Invalid offers list.")
533
+ end
534
+ url = consenturl + "Delegation.aspx?ps=#{CGI.escape(offers)}"
535
+ url += "&appctx=#{CGI.escape(context)}" if context
536
+ ru = returnurl if (ru.nil? or ru.empty?)
537
+ url += "&ru=#{CGI.escape(ru)}" if ru
538
+ pu = policyurl
539
+ url += "&pl=#{CGI.escape(pu)}" if pu
540
+ url += "&mkt=#{CGI.escape(market)}" if market
541
+ url += "&app=#{getAppVerifier()}" unless force_delauth_nonprovisioned
542
+ url
543
+ end
544
+
545
+ #####################################################################
546
+ # Returns the URL to use to download a new consent token, given the
547
+ # offers and refresh token.
548
+ # The registered/configured return URL can also be overridden by
549
+ # specifying 'ru' here.
550
+ #####################################################################
551
+ def getRefreshConsentTokenUrl(offers, refreshtoken, ru)
552
+ if (offers.nil? or offers.empty?)
553
+ fatal("Error: getRefreshConsentTokenUrl: Invalid offers list.")
554
+ end
555
+ if (refreshtoken.nil? or refreshtoken.empty?)
556
+ fatal("Error: getRefreshConsentTokenUrl: Invalid refresh token.")
557
+ end
558
+ url = consenturl + "RefreshToken.aspx?ps=#{CGI.escape(offers)}"
559
+ url += "&reft=#{refreshtoken}"
560
+ ru = returnurl if (ru.nil? or ru.empty?)
561
+ url += "&ru=#{CGI.escape(ru)}" if ru
562
+ url += "&app=#{getAppVerifier()}" unless force_delauth_nonprovisioned
563
+ url
564
+ end
565
+
566
+ #####################################################################
567
+ # Returns the URL for the consent-management user interface.
568
+ # You can change the language in which the consent page is displayed
569
+ # by specifying a culture ID (For example, 'fr-fr' or 'en-us') in the
570
+ # 'market' parameter.
571
+ #####################################################################
572
+ def getManageConsentUrl(market=nil)
573
+ url = consenturl + "ManageConsent.aspx"
574
+ url += "?mkt=#{CGI.escape(market)}" if market
575
+ url
576
+ end
577
+
578
+ class ConsentToken
579
+ attr_reader :delegationtoken, :refreshtoken, :sessionkey, :expiry
580
+ attr_reader :offers, :offers_string, :locationid, :context
581
+ attr_reader :decodedtoken, :token
582
+
583
+ #####################################################################
584
+ # Indicates whether the delegation token is set and has not expired.
585
+ #####################################################################
586
+ def isValid?
587
+ return false unless delegationtoken
588
+ return ((Time.now.to_i-300) < expiry.to_i)
589
+ end
590
+
591
+ #####################################################################
592
+ # Refreshes the current token and replace it. If operation succeeds
593
+ # true is returned to signify success.
594
+ #####################################################################
595
+ def refresh
596
+ ct = @wll.refreshConsentToken(self)
597
+ return false unless ct
598
+ copy(ct)
599
+ true
600
+ end
601
+
602
+ #####################################################################
603
+ # Initialize the ConsentToken module with the WindowsLiveLogin,
604
+ # delegation token, refresh token, session key, expiry, offers,
605
+ # location ID, context, decoded token, and raw token.
606
+ #####################################################################
607
+ def initialize(wll, delegationtoken, refreshtoken, sessionkey, expiry,
608
+ offers, locationid, context, decodedtoken, token)
609
+ @wll = wll
610
+ self.delegationtoken = delegationtoken
611
+ self.refreshtoken = refreshtoken
612
+ self.sessionkey = sessionkey
613
+ self.expiry = expiry
614
+ self.offers = offers
615
+ self.locationid = locationid
616
+ self.context = context
617
+ self.decodedtoken = decodedtoken
618
+ self.token = token
619
+ end
620
+
621
+ private
622
+ attr_writer :delegationtoken, :refreshtoken, :sessionkey, :expiry
623
+ attr_writer :offers, :offers_string, :locationid, :context
624
+ attr_writer :decodedtoken, :token, :locationid
625
+
626
+ #####################################################################
627
+ # Sets the delegation token.
628
+ #####################################################################
629
+ def delegationtoken=(delegationtoken)
630
+ if (delegationtoken.nil? or delegationtoken.empty?)
631
+ raise("Error: ConsentToken: Null delegation token.")
632
+ end
633
+ @delegationtoken = delegationtoken
634
+ end
635
+
636
+ #####################################################################
637
+ # Sets the session key.
638
+ #####################################################################
639
+ def sessionkey=(sessionkey)
640
+ if (sessionkey.nil? or sessionkey.empty?)
641
+ raise("Error: ConsentToken: Null session key.")
642
+ end
643
+ @sessionkey = @wll.u64(sessionkey)
644
+ end
645
+
646
+ #####################################################################
647
+ # Sets the expiry time of the delegation token.
648
+ #####################################################################
649
+ def expiry=(expiry)
650
+ if (expiry.nil? or expiry.empty?)
651
+ raise("Error: ConsentToken: Null expiry time.")
652
+ end
653
+ expiry = expiry.to_i
654
+ raise("Error: ConsentToken: Invalid expiry: #{expiry}") if (expiry <= 0)
655
+ @expiry = Time.at expiry
656
+ end
657
+
658
+ #####################################################################
659
+ # Sets the offers/actions for which the user granted consent.
660
+ #####################################################################
661
+ def offers=(offers)
662
+ if (offers.nil? or offers.empty?)
663
+ raise("Error: ConsentToken: Null offers.")
664
+ end
665
+
666
+ @offers_string = ""
667
+ @offers = []
668
+
669
+ offers = CGI.unescape(offers)
670
+ offers = offers.split(";")
671
+ offers.each{|offer|
672
+ offer = offer.split(":")[0]
673
+ @offers_string += "," unless @offers_string.empty?
674
+ @offers_string += offer
675
+ @offers.push(offer)
676
+ }
677
+ end
678
+
679
+ #####################################################################
680
+ # Sets the LocationID.
681
+ #####################################################################
682
+ def locationid=(locationid)
683
+ if (locationid.nil? or locationid.empty?)
684
+ raise("Error: ConsentToken: Null Location ID.")
685
+ end
686
+ @locationid = locationid
687
+ end
688
+
689
+ #####################################################################
690
+ # Makes a copy of the ConsentToken object.
691
+ #####################################################################
692
+ def copy(consenttoken)
693
+ @delegationtoken = consenttoken.delegationtoken
694
+ @refreshtoken = consenttoken.refreshtoken
695
+ @sessionkey = consenttoken.sessionkey
696
+ @expiry = consenttoken.expiry
697
+ @offers = consenttoken.offers
698
+ @locationid = consenttoken.locationid
699
+ @offers_string = consenttoken.offers_string
700
+ @decodedtoken = consenttoken.decodedtoken
701
+ @token = consenttoken.token
702
+ end
703
+ end
704
+
705
+ #####################################################################
706
+ # Processes the POST response from the Delegated Authentication
707
+ # service after a user has granted consent. The processConsent
708
+ # function extracts the consent token string and returns the result
709
+ # of invoking the processConsentToken method.
710
+ #####################################################################
711
+ def processConsent(query)
712
+ query = parse query
713
+ unless query
714
+ debug("Error: processConsent: Failed to parse query.")
715
+ return
716
+ end
717
+ action = query['action']
718
+ unless action == 'delauth'
719
+ debug("Warning: processConsent: query action ignored: #{action}.")
720
+ return
721
+ end
722
+ responsecode = query['ResponseCode']
723
+ unless responsecode == 'RequestApproved'
724
+ debug("Error: processConsent: Consent was not successfully granted: #{responsecode}")
725
+ return
726
+ end
727
+ token = query['ConsentToken']
728
+ context = CGI.unescape(query['appctx']) if query['appctx']
729
+ processConsentToken(token, context)
730
+ end
731
+
732
+ #####################################################################
733
+ # Processes the consent token string that is returned in the POST
734
+ # response by the Delegated Authentication service after a
735
+ # user has granted consent.
736
+ #####################################################################
737
+ def processConsentToken(token, context=nil)
738
+ if token.nil? or token.empty?
739
+ debug("Error: processConsentToken: Null token.")
740
+ return
741
+ end
742
+ decodedtoken = token
743
+ parsedtoken = parse(CGI.unescape(decodedtoken))
744
+ unless parsedtoken
745
+ debug("Error: processConsentToken: Failed to parse token: #{token}")
746
+ return
747
+ end
748
+ eact = parsedtoken['eact']
749
+ if eact
750
+ decodedtoken = decodeAndValidateToken eact
751
+ unless decodedtoken
752
+ debug("Error: processConsentToken: Failed to decode/validate token: #{token}")
753
+ return
754
+ end
755
+ parsedtoken = parse(decodedtoken)
756
+ decodedtoken = CGI.escape(decodedtoken)
757
+ end
758
+ begin
759
+ consenttoken = ConsentToken.new(self,
760
+ parsedtoken['delt'],
761
+ parsedtoken['reft'],
762
+ parsedtoken['skey'],
763
+ parsedtoken['exp'],
764
+ parsedtoken['offer'],
765
+ parsedtoken['lid'],
766
+ context, decodedtoken, token)
767
+ return consenttoken
768
+ rescue Exception => e
769
+ debug("Error: processConsentToken: Contents of token considered invalid: #{e}")
770
+ return
771
+ end
772
+ end
773
+
774
+ #####################################################################
775
+ # Attempts to obtain a new, refreshed token and return it. The
776
+ # original token is not modified.
777
+ #####################################################################
778
+ def refreshConsentToken(consenttoken, ru=nil)
779
+ if consenttoken.nil?
780
+ debug("Error: refreshConsentToken: Null consent token.")
781
+ return
782
+ end
783
+ refreshConsentToken2(consenttoken.offers_string, consenttoken.refreshtoken, ru)
784
+ end
785
+
786
+ #####################################################################
787
+ # Helper function to obtain a new, refreshed token and return it.
788
+ # The original token is not modified.
789
+ #####################################################################
790
+ def refreshConsentToken2(offers_string, refreshtoken, ru=nil)
791
+ url = nil
792
+ begin
793
+ url = getRefreshConsentTokenUrl(offers_string, refreshtoken, ru)
794
+ ret = fetch url
795
+ ret.value # raises exception if fetch failed
796
+ body = ret.body
797
+ body.scan(/\{"ConsentToken":"(.*)"\}/){|match|
798
+ return processConsentToken("#{match}")
799
+ }
800
+ debug("Error: refreshConsentToken2: Failed to extract token: #{body}")
801
+ rescue Exception => e
802
+ debug("Error: Failed to refresh consent token: #{e}")
803
+ end
804
+ return
805
+ end
806
+ end
807
+
808
+ #######################################################################
809
+ # Common methods.
810
+ #######################################################################
811
+ class WindowsLiveLogin
812
+
813
+ #####################################################################
814
+ # Decodes and validates the token.
815
+ #####################################################################
816
+ def decodeAndValidateToken(token, cryptkey=@cryptkey, signkey=@signkey,
817
+ internal_allow_recursion=true)
818
+ haveoldsecret = false
819
+ if (oldsecretexpiry and (Time.now.to_i < oldsecretexpiry.to_i))
820
+ haveoldsecret = true if (@oldcryptkey and @oldsignkey)
821
+ end
822
+ haveoldsecret = (haveoldsecret and internal_allow_recursion)
823
+
824
+ stoken = decodeToken(token, cryptkey)
825
+ stoken = validateToken(stoken, signkey) if stoken
826
+ if (stoken.nil? and haveoldsecret)
827
+ debug("Warning: Failed to validate token with current secret, attempting old secret.")
828
+ stoken = decodeAndValidateToken(token, @oldcryptkey, @oldsignkey, false)
829
+ end
830
+ stoken
831
+ end
832
+
833
+ #####################################################################
834
+ # Decodes the given token string; returns undef on failure.
835
+ #
836
+ # First, the string is URL-unescaped and base64 decoded.
837
+ # Second, the IV is extracted from the first 16 bytes of the string.
838
+ # Finally, the string is decrypted using the encryption key.
839
+ #####################################################################
840
+ def decodeToken(token, cryptkey=@cryptkey)
841
+ if (cryptkey.nil? or cryptkey.empty?)
842
+ fatal("Error: decodeToken: Secret key was not set. Aborting.")
843
+ end
844
+ token = u64(token)
845
+ if (token.nil? or (token.size <= 16) or !(token.size % 16).zero?)
846
+ debug("Error: decodeToken: Attempted to decode invalid token.")
847
+ return
848
+ end
849
+ iv = token[0..15]
850
+ crypted = token[16..-1]
851
+ begin
852
+ aes128cbc = OpenSSL::Cipher::AES128.new("CBC")
853
+ aes128cbc.decrypt
854
+ aes128cbc.iv = iv
855
+ aes128cbc.key = cryptkey
856
+ decrypted = aes128cbc.update(crypted) + aes128cbc.final
857
+ rescue Exception => e
858
+ debug("Error: decodeToken: Decryption failed: #{token}, #{e}")
859
+ return
860
+ end
861
+ decrypted
862
+ end
863
+
864
+ #####################################################################
865
+ # Creates a signature for the given string by using the signature
866
+ # key.
867
+ #####################################################################
868
+ def signToken(token, signkey=@signkey)
869
+ if (signkey.nil? or signkey.empty?)
870
+ fatal("Error: signToken: Secret key was not set. Aborting.")
871
+ end
872
+ begin
873
+ digest = OpenSSL::Digest::SHA256.new
874
+ return OpenSSL::HMAC.digest(digest, signkey, token)
875
+ rescue Exception => e
876
+ debug("Error: signToken: Signing failed: #{token}, #{e}")
877
+ return
878
+ end
879
+ end
880
+
881
+ #####################################################################
882
+ # Extracts the signature from the token and validates it.
883
+ #####################################################################
884
+ def validateToken(token, signkey=@signkey)
885
+ if (token.nil? or token.empty?)
886
+ debug("Error: validateToken: Null token.")
887
+ return
888
+ end
889
+ body, sig = token.split("&sig=")
890
+ if (body.nil? or sig.nil?)
891
+ debug("Error: validateToken: Invalid token: #{token}")
892
+ return
893
+ end
894
+ sig = u64(sig)
895
+ return token if (sig == signToken(body, signkey))
896
+ debug("Error: validateToken: Signature did not match.")
897
+ return
898
+ end
899
+ end
900
+
901
+ #######################################################################
902
+ # Implementation of the methods needed to perform Windows Live
903
+ # application verification as well as trusted sign-in.
904
+ #######################################################################
905
+ class WindowsLiveLogin
906
+ #####################################################################
907
+ # Generates an application verifier token. An IP address can
908
+ # optionally be included in the token.
909
+ #####################################################################
910
+ def getAppVerifier(ip=nil)
911
+ token = "appid=#{appid}&ts=#{timestamp}"
912
+ token += "&ip=#{ip}" if ip
913
+ token += "&sig=#{e64(signToken(token))}"
914
+ CGI.escape token
915
+ end
916
+
917
+ #####################################################################
918
+ # Returns the URL that is required to retrieve the application
919
+ # security token.
920
+ #
921
+ # By default, the application security token is generated for
922
+ # the Windows Live site; a specific Site ID can optionally be
923
+ # specified in 'siteid'. The IP address can also optionally be
924
+ # included in 'ip'.
925
+ #
926
+ # If 'js' is nil, a JavaScript Output Notation (JSON) response is
927
+ # returned in the following format:
928
+ #
929
+ # {"token":"<value>"}
930
+ #
931
+ # Otherwise, a JavaScript response is returned. It is assumed that
932
+ # WLIDResultCallback is a custom function implemented to handle the
933
+ # token value:
934
+ #
935
+ # WLIDResultCallback("<tokenvalue>");
936
+ #####################################################################
937
+ def getAppLoginUrl(siteid=nil, ip=nil, js=nil)
938
+ url = secureurl + "wapplogin.srf?app=#{getAppVerifier(ip)}"
939
+ url += "&alg=#{securityalgorithm}"
940
+ url += "&id=#{siteid}" if siteid
941
+ url += "&js=1" if js
942
+ url
943
+ end
944
+
945
+ #####################################################################
946
+ # Retrieves the application security token for application
947
+ # verification from the application sign-in URL.
948
+ #
949
+ # By default, the application security token will be generated for
950
+ # the Windows Live site; a specific Site ID can optionally be
951
+ # specified in 'siteid'. The IP address can also optionally be
952
+ # included in 'ip'.
953
+ #
954
+ # Implementation note: The application security token is downloaded
955
+ # from the application sign-in URL in JSON format:
956
+ #
957
+ # {"token":"<value>"}
958
+ #
959
+ # Therefore we must extract <value> from the string and return it as
960
+ # seen here.
961
+ #####################################################################
962
+ def getAppSecurityToken(siteid=nil, ip=nil)
963
+ url = getAppLoginUrl(siteid, ip)
964
+ begin
965
+ ret = fetch url
966
+ ret.value # raises exception if fetch failed
967
+ body = ret.body
968
+ body.scan(/\{"token":"(.*)"\}/){|match|
969
+ return match
970
+ }
971
+ debug("Error: getAppSecurityToken: Failed to extract token: #{body}")
972
+ rescue Exception => e
973
+ debug("Error: getAppSecurityToken: Failed to get token: #{e}")
974
+ end
975
+ return
976
+ end
977
+
978
+ #####################################################################
979
+ # Returns a string that can be passed to the getTrustedParams
980
+ # function as the 'retcode' parameter. If this is specified as the
981
+ # 'retcode', the application will be used as return URL after it
982
+ # finishes trusted sign-in.
983
+ #####################################################################
984
+ def getAppRetCode
985
+ "appid=#{appid}"
986
+ end
987
+
988
+ #####################################################################
989
+ # Returns a table of key-value pairs that must be posted to the
990
+ # sign-in URL for trusted sign-in. Use HTTP POST to do this. Be aware
991
+ # that the values in the table are neither URL nor HTML escaped and
992
+ # may have to be escaped if you are inserting them in code such as
993
+ # an HTML form.
994
+ #
995
+ # The user to be trusted on the local site is passed in as string
996
+ # 'user'.
997
+ #
998
+ # Optionally, 'retcode' specifies the resource to which successful
999
+ # sign-in is redirected, such as Windows Live Mail, and is typically
1000
+ # a string in the format 'id=2000'. If you pass in the value from
1001
+ # getAppRetCode instead, sign-in will be redirected to the
1002
+ # application. Otherwise, an HTTP 200 response is returned.
1003
+ #####################################################################
1004
+ def getTrustedParams(user, retcode=nil)
1005
+ token = getTrustedToken(user)
1006
+ return unless token
1007
+ token = %{<wst:RequestSecurityTokenResponse xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust"><wst:RequestedSecurityToken><wsse:BinarySecurityToken xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">#{token}</wsse:BinarySecurityToken></wst:RequestedSecurityToken><wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"><wsa:EndpointReference xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"><wsa:Address>uri:WindowsLiveID</wsa:Address></wsa:EndpointReference></wsp:AppliesTo></wst:RequestSecurityTokenResponse>}
1008
+ params = {}
1009
+ params['wa'] = securityalgorithm
1010
+ params['wresult'] = token
1011
+ params['wctx'] = retcode if retcode
1012
+ params
1013
+ end
1014
+
1015
+ #####################################################################
1016
+ # Returns the trusted sign-in token in the format that is needed by a
1017
+ # control doing trusted sign-in.
1018
+ #
1019
+ # The user to be trusted on the local site is passed in as string
1020
+ # 'user'.
1021
+ #####################################################################
1022
+ def getTrustedToken(user)
1023
+ if user.nil? or user.empty?
1024
+ debug('Error: getTrustedToken: Null user specified.')
1025
+ return
1026
+ end
1027
+ token = "appid=#{appid}&uid=#{CGI.escape(user)}&ts=#{timestamp}"
1028
+ token += "&sig=#{e64(signToken(token))}"
1029
+ CGI.escape token
1030
+ end
1031
+
1032
+ #####################################################################
1033
+ # Returns the trusted sign-in URL to use for the Windows Live Login
1034
+ # server.
1035
+ #####################################################################
1036
+ def getTrustedLoginUrl
1037
+ secureurl + "wlogin.srf"
1038
+ end
1039
+
1040
+ #####################################################################
1041
+ # Returns the trusted sign-out URL to use for the Windows Live Login
1042
+ # server.
1043
+ #####################################################################
1044
+ def getTrustedLogoutUrl
1045
+ secureurl + "logout.srf?appid=#{appid}"
1046
+ end
1047
+ end
1048
+
1049
+ #######################################################################
1050
+ # Helper methods.
1051
+ #######################################################################
1052
+ class WindowsLiveLogin
1053
+
1054
+ #######################################################################
1055
+ # Function to parse the settings file.
1056
+ #######################################################################
1057
+ def parseSettings(settingsFile)
1058
+ settings = {}
1059
+ begin
1060
+ file = File.new(settingsFile)
1061
+ doc = REXML::Document.new file
1062
+ root = doc.root
1063
+ root.each_element{|e|
1064
+ settings[e.name] = e.text
1065
+ }
1066
+ rescue Exception => e
1067
+ fatal("Error: parseSettings: Error while reading #{settingsFile}: #{e}")
1068
+ end
1069
+ return settings
1070
+ end
1071
+
1072
+ #####################################################################
1073
+ # Derives the key, given the secret key and prefix as described in the
1074
+ # Web Authentication SDK documentation.
1075
+ #####################################################################
1076
+ def derive(secret, prefix)
1077
+ begin
1078
+ fatal("Nil/empty secret.") if (secret.nil? or secret.empty?)
1079
+ key = prefix + secret
1080
+ key = OpenSSL::Digest::SHA256.digest(key)
1081
+ return key[0..15]
1082
+ rescue Exception => e
1083
+ debug("Error: derive: #{e}")
1084
+ return
1085
+ end
1086
+ end
1087
+
1088
+ #####################################################################
1089
+ # Parses query string and return a table
1090
+ # {String=>String}
1091
+ #
1092
+ # If a table is passed in from CGI.params, we convert it from
1093
+ # {String=>[]} to {String=>String}. I believe Rails uses symbols
1094
+ # instead of strings in general, so we convert from symbols to
1095
+ # strings here also.
1096
+ #####################################################################
1097
+ def parse(input)
1098
+ if (input.nil? or input.empty?)
1099
+ debug("Error: parse: Nil/empty input.")
1100
+ return
1101
+ end
1102
+
1103
+ pairs = {}
1104
+ if (input.class == String)
1105
+ input = input.split('&')
1106
+ input.each{|pair|
1107
+ k, v = pair.split('=')
1108
+ pairs[k] = v
1109
+ }
1110
+ else
1111
+ input.each{|k, v|
1112
+ v = v[0] if (v.class == Array)
1113
+ pairs[k.to_s] = v.to_s
1114
+ }
1115
+ end
1116
+ return pairs
1117
+ end
1118
+
1119
+ #####################################################################
1120
+ # Generates a time stamp suitable for the application verifier token.
1121
+ #####################################################################
1122
+ def timestamp
1123
+ Time.now.to_i.to_s
1124
+ end
1125
+
1126
+ #####################################################################
1127
+ # Base64-encodes and URL-escapes a string.
1128
+ #####################################################################
1129
+ def e64(s)
1130
+ return unless s
1131
+ CGI.escape Base64.encode64(s)
1132
+ end
1133
+
1134
+ #####################################################################
1135
+ # URL-unescapes and Base64-decodes a string.
1136
+ #####################################################################
1137
+ def u64(s)
1138
+ return unless s
1139
+ Base64.decode64 CGI.unescape(s)
1140
+ end
1141
+
1142
+ #####################################################################
1143
+ # Fetches the contents given a URL.
1144
+ #####################################################################
1145
+ def fetch(url)
1146
+ url = URI.parse url
1147
+ http = Net::HTTP.new(url.host, url.port)
1148
+ http.use_ssl = (url.scheme == "https")
1149
+ http.request_get url.request_uri
1150
+ end
1151
+ end