mislav_contacts 0.2.7

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.
@@ -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