aurelian-contacts 0.3.1

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,95 @@
1
+ require 'contacts/yahoo'
2
+
3
+ describe Contacts::Yahoo do
4
+
5
+ before(:each) do
6
+ @path = Dir.getwd + '/spec/feeds/'
7
+ @yahoo = Contacts::Yahoo.new(@path + 'contacts.yml')
8
+ end
9
+
10
+ it 'should generate an athentication URL' do
11
+ auth_url = @yahoo.get_authentication_url()
12
+ auth_url.should match(/https:\/\/api.login.yahoo.com\/WSLogin\/V1\/wslogin\?appid=i%3DB%26p%3DUw70JGIdHWVRbpqYItcMw--&ts=.*&sig=.*/)
13
+ end
14
+
15
+ it 'should generate an athentication URL with appdata' do
16
+ auth_url = @yahoo.get_authentication_url("foobar")
17
+ auth_url.should match(/https:\/\/api.login.yahoo.com\/WSLogin\/V1\/wslogin\?appid=i%3DB%26p%3DUw70JGIdHWVRbpqYItcMw--&ts=.*&appdata=foobar&sig=.*/)
18
+ end
19
+
20
+ it 'should have a simple interface to grab the contacts' do
21
+ @yahoo.expects(:access_user_credentials).returns(read_file('yh_credential.xml'))
22
+ @yahoo.expects(:access_address_book_api).returns(read_file('yh_contacts.txt'))
23
+
24
+ redirect_path = '/?appid=i%3DB%26p%3DUw70JGIdHWVRbpqYItcMw--&token=AB.KoEg8vBwvJKFkwfcDTJEMKhGeAD6KhiDe0aZLCvoJzMeQG00-&appdata=&ts=1218501215&sig=d381fba89c7e9d3c14788720733c3fbf'
25
+
26
+ results = @yahoo.contacts(redirect_path)
27
+ results.size.should == 3 end
28
+
29
+ it 'should validate yahoo redirect signature' do
30
+ redirect_path = '/?appid=i%3DB%26p%3DUw70JGIdHWVRbpqYItcMw--&token=AB.KoEg8vBwvJKFkwfcDTJEMKhGeAD6KhiDe0aZLCvoJzMeQG00-&appdata=&ts=1218501215&sig=d381fba89c7e9d3c14788720733c3fbf'
31
+
32
+ @yahoo.validate_signature(redirect_path).should be_true
33
+ @yahoo.token.should == 'AB.KoEg8vBwvJKFkwfcDTJEMKhGeAD6KhiDe0aZLCvoJzMeQG00-'
34
+ end
35
+
36
+ it 'should detect when the redirect is not valid' do
37
+ redirect_path = '/?appid=i%3DB%26p%3DUw70JGIdHWVRbpqYItcMw--&token=AB.KoEg8vBwvJKFkwfcDTJEMKhGeAD6KhiDe0aZLCvoJzMeQG00-&appdata=&ts=1218501215&sig=de4fe4ebd50a8075f75dcc23f6aca04f'
38
+
39
+ lambda{ @yahoo.validate_signature(redirect_path) }.should raise_error
40
+ end
41
+
42
+ it 'should generate the credential request URL' do
43
+ redirect_path = '/?appid=i%3DB%26p%3DUw70JGIdHWVRbpqYItcMw--&token=AB.KoEg8vBwvJKFkwfcDTJEMKhGeAD6KhiDe0aZLCvoJzMeQG00-&appdata=&ts=1218501215&sig=d381fba89c7e9d3c14788720733c3fbf'
44
+ @yahoo.validate_signature(redirect_path)
45
+
46
+ @yahoo.get_credential_url.should match(/https:\/\/api.login.yahoo.com\/WSLogin\/V1\/wspwtoken_login\?appid=i%3DB%26p%3DUw70JGIdHWVRbpqYItcMw--&ts=.*&token=.*&sig=.*/)
47
+ end
48
+
49
+ it 'should parse the credential XML' do
50
+ @yahoo.parse_credentials(read_file('yh_credential.xml'))
51
+
52
+ @yahoo.wssid.should == 'tr.jZsW/ulc'
53
+ @yahoo.cookie.should == 'Y=cdunlEx76ZEeIdWyeJNOegxfy.jkeoULJCnc7Q0Vr8D5P.u.EE2vCa7G2MwBoULuZhvDZuJNqhHwF3v5RJ4dnsWsEDGOjYV1k6snoln3RlQmx0Ggxs0zAYgbaA4BFQk5ieAkpipq19l6GoD_k8IqXRfJN0Q54BbekC_O6Tj3zl2wV3YQK6Mi2MWBQFSBsO26Tw_1yMAF8saflF9EX1fQl4N.1yBr8UXb6LLDiPQmlISq1_c6S6rFbaOhSZMgO78f2iqZmUAk9RmCHrqPJiHEo.mJlxxHaQsuqTMf7rwLEHqK__Gi_bLypGtaslqeWyS0h2J.B5xwRC8snfEs3ct_kLXT3ngP_pK3MeMf2pe1TiJ4JXVciY9br.KJFUgNd4J6rmQsSFj4wPLoMGCETfVc.M8KLiaFHasZqXDyCE7tvd1khAjQ_xLfQKlg1GlBOWmbimQ1FhdHnsVj3svXjEGquRh8JI2sHIQrzoiqAPBf9WFKQcH0t_1dxf4MOH.7gJaYDPEozCW5EcCsYjuHup9xJKxyTddh5pk8yUg5bURzA.TwPalExMKsbv.RWFBhzWKuTp5guNcqjmUHcCoT19_qFENHX41Xf3texAnsDDGj'
54
+ end
55
+
56
+ it 'should parse the contacts json response' do
57
+ json = read_file('yh_contacts.txt')
58
+
59
+ Contacts::Yahoo.parse_contacts(json).should have_contact('Hugo Barauna', 'hugo.barauna@gmail.com', 4)
60
+ # Contacts::Yahoo.parse_contacts(json).should have_contact('Nina Benchimol',
61
+ # ['nina@hotmail.com','nina2@yahoo.com'],
62
+ # 5,
63
+ # [{"type" => "msn", "value" => "windowslive@msn.com"}],
64
+ # [{"type" => "mobile", "value" => "808 456 7890"}],
65
+ # [{"type" => "home", "value" => "123 Home Street, Super City, HI, 96815, United States"}]
66
+ # )
67
+ Contacts::Yahoo.parse_contacts(json).should have_contact('carl_larsson','',30)
68
+ end
69
+
70
+ it 'should can be initialized by a YAML file' do
71
+ @yahoo.appid.should == 'i%3DB%26p%3DUw70JGIdHWVRbpqYItcMw--'
72
+ @yahoo.secret.should == 'a34f389cbd135de4618eed5e23409d34450'
73
+ end
74
+
75
+ def read_file(file)
76
+ File.open(@path + file, 'r+').read
77
+ end
78
+
79
+ def have_contact(name, email, cid, ims = [], phones = [], addresses = [])
80
+ email = [email] if email.is_a? String
81
+ matcher_class = Class.new()
82
+ matcher_class.instance_eval do
83
+ define_method(:matches?) {|some_contacts| some_contacts.any? {|a_contact|
84
+ a_contact.name == name && \
85
+ ((a_contact.emails - email).empty?) && \
86
+ ((a_contact.ims - ims).empty?) && \
87
+ ((a_contact.phones - phones).empty?) && \
88
+ ((a_contact.addresses - addresses).empty?) && \
89
+ a_contact.service_id == cid
90
+ }
91
+ }
92
+ end
93
+ matcher_class.new
94
+ end
95
+ end
@@ -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 'digest/sha2'
27
+ require 'hmac-sha2'
28
+ require 'net/https'
29
+ require 'rexml/document'
30
+
31
+ class WindowsLiveLogin
32
+
33
+ #####################################################################
34
+ # Stub implementation for logging errors. If you want to enable
35
+ # debugging output using the default mechanism, specify true.
36
+ # By default, debug information will be printed to the standard
37
+ # error output and should be visible in the web server logs.
38
+ #####################################################################
39
+ def setDebug(flag)
40
+ @debug = flag
41
+ end
42
+
43
+ #####################################################################
44
+ # Stub implementation for logging errors. By default, this function
45
+ # does nothing if the debug flag has not been set with setDebug.
46
+ # Otherwise, it tries to log the error message.
47
+ #####################################################################
48
+ def debug(error)
49
+ return unless @debug
50
+ return if error.nil? or error.empty?
51
+ warn("Windows Live ID Authentication SDK #{error}")
52
+ nil
53
+ end
54
+
55
+ #####################################################################
56
+ # Stub implementation for handling a fatal error.
57
+ #####################################################################
58
+ def fatal(error)
59
+ debug(error)
60
+ raise(error)
61
+ end
62
+
63
+ #####################################################################
64
+ # Initialize the WindowsLiveLogin module with the application ID,
65
+ # secret key, and security algorithm.
66
+ #
67
+ # We recommend that you employ strong measures to protect the
68
+ # secret key. The secret key should never be exposed to the Web
69
+ # or other users.
70
+ #
71
+ # Be aware that if you do not supply these settings at
72
+ # initialization time, you may need to set the corresponding
73
+ # properties manually.
74
+ #
75
+ # For Delegated Authentication, you may optionally specify the
76
+ # privacy policy URL and return URL. If you do not specify these
77
+ # values here, the default values that you specified when you
78
+ # registered your application will be used.
79
+ #
80
+ # The 'force_delauth_nonprovisioned' flag also indicates whether
81
+ # your application is registered for Delegated Authentication
82
+ # (that is, whether it uses an application ID and secret key). We
83
+ # recommend that your Delegated Authentication application always
84
+ # be registered for enhanced security and functionality.
85
+ #####################################################################
86
+ def initialize(appid=nil, secret=nil, securityalgorithm=nil,
87
+ force_delauth_nonprovisioned=nil,
88
+ policyurl=nil, returnurl=nil)
89
+ self.force_delauth_nonprovisioned = force_delauth_nonprovisioned
90
+ self.appid = appid if appid
91
+ self.secret = secret if secret
92
+ self.securityalgorithm = securityalgorithm if securityalgorithm
93
+ self.policyurl = policyurl if policyurl
94
+ self.returnurl = returnurl if returnurl
95
+ end
96
+
97
+ #####################################################################
98
+ # Initialize the WindowsLiveLogin module from a settings file.
99
+ #
100
+ # 'settingsFile' specifies the location of the XML settings file
101
+ # that contains the application ID, secret key, and security
102
+ # algorithm. The file is of the following format:
103
+ #
104
+ # <windowslivelogin>
105
+ # <appid>APPID</appid>
106
+ # <secret>SECRET</secret>
107
+ # <securityalgorithm>wsignin1.0</securityalgorithm>
108
+ # </windowslivelogin>
109
+ #
110
+ # In a Delegated Authentication scenario, you may also specify
111
+ # 'returnurl' and 'policyurl' in the settings file, as shown in the
112
+ # Delegated Authentication samples.
113
+ #
114
+ # We recommend that you store the WindowsLiveLogin settings file
115
+ # in an area on your server that cannot be accessed through the
116
+ # Internet. This file contains important confidential information.
117
+ #####################################################################
118
+ def self.initFromXml(settingsFile)
119
+ o = self.new
120
+ settings = o.parseSettings(settingsFile)
121
+
122
+ o.setDebug(settings['debug'] == 'true')
123
+ o.force_delauth_nonprovisioned =
124
+ (settings['force_delauth_nonprovisioned'] == 'true')
125
+
126
+ o.appid = settings['appid']
127
+ o.secret = settings['secret']
128
+ o.oldsecret = settings['oldsecret']
129
+ o.oldsecretexpiry = settings['oldsecretexpiry']
130
+ o.securityalgorithm = settings['securityalgorithm']
131
+ o.policyurl = settings['policyurl']
132
+ o.returnurl = settings['returnurl']
133
+ o.baseurl = settings['baseurl']
134
+ o.secureurl = settings['secureurl']
135
+ o.consenturl = settings['consenturl']
136
+ o
137
+ end
138
+
139
+ #####################################################################
140
+ # Sets the application ID. Use this method if you did not specify
141
+ # an application ID at initialization.
142
+ #####################################################################
143
+ def appid=(appid)
144
+ if (appid.nil? or appid.empty?)
145
+ return if force_delauth_nonprovisioned
146
+ fatal("Error: appid: Null application ID.")
147
+ end
148
+ if (not appid =~ /^\w+$/)
149
+ fatal("Error: appid: Application ID must be alpha-numeric: " + appid)
150
+ end
151
+ @appid = appid
152
+ end
153
+
154
+ #####################################################################
155
+ # Returns the application ID.
156
+ #####################################################################
157
+ def appid
158
+ if (@appid.nil? or @appid.empty?)
159
+ fatal("Error: appid: App ID was not set. Aborting.")
160
+ end
161
+ @appid
162
+ end
163
+
164
+ #####################################################################
165
+ # Sets your secret key. Use this method if you did not specify
166
+ # a secret key at initialization.
167
+ #####################################################################
168
+ def secret=(secret)
169
+ if (secret.nil? or secret.empty?)
170
+ return if force_delauth_nonprovisioned
171
+ fatal("Error: secret=: Secret must be non-null.")
172
+ end
173
+ if (secret.size < 16)
174
+ fatal("Error: secret=: Secret must be at least 16 characters.")
175
+ end
176
+ @signkey = derive(secret, "SIGNATURE")
177
+ @cryptkey = derive(secret, "ENCRYPTION")
178
+ end
179
+
180
+ #####################################################################
181
+ # Sets your old secret key.
182
+ #
183
+ # Use this property to set your old secret key if you are in the
184
+ # process of transitioning to a new secret key. You may need this
185
+ # property because the Windows Live ID servers can take up to
186
+ # 24 hours to propagate a new secret key after you have updated
187
+ # your application settings.
188
+ #
189
+ # If an old secret key is specified here and has not expired
190
+ # (as determined by the oldsecretexpiry setting), it will be used
191
+ # as a fallback if token decryption fails with the new secret
192
+ # key.
193
+ #####################################################################
194
+ def oldsecret=(secret)
195
+ return if (secret.nil? or secret.empty?)
196
+ if (secret.size < 16)
197
+ fatal("Error: oldsecret=: Secret must be at least 16 characters.")
198
+ end
199
+ @oldsignkey = derive(secret, "SIGNATURE")
200
+ @oldcryptkey = derive(secret, "ENCRYPTION")
201
+ end
202
+
203
+ #####################################################################
204
+ # Sets the expiry time for your old secret key.
205
+ #
206
+ # After this time has passed, the old secret key will no longer be
207
+ # used even if token decryption fails with the new secret key.
208
+ #
209
+ # The old secret expiry time is represented as the number of seconds
210
+ # elapsed since January 1, 1970.
211
+ #####################################################################
212
+ def oldsecretexpiry=(timestamp)
213
+ return if (timestamp.nil? or timestamp.empty?)
214
+ timestamp = timestamp.to_i
215
+ fatal("Error: oldsecretexpiry=: Invalid timestamp: #{timestamp}") if (timestamp <= 0)
216
+ @oldsecretexpiry = Time.at timestamp
217
+ end
218
+
219
+ #####################################################################
220
+ # Gets the old secret key expiry time.
221
+ #####################################################################
222
+ attr_accessor :oldsecretexpiry
223
+
224
+ #####################################################################
225
+ # Sets or gets the version of the security algorithm being used.
226
+ #####################################################################
227
+ attr_accessor :securityalgorithm
228
+
229
+ def securityalgorithm
230
+ if(@securityalgorithm.nil? or @securityalgorithm.empty?)
231
+ "wsignin1.0"
232
+ else
233
+ @securityalgorithm
234
+ end
235
+ end
236
+
237
+ #####################################################################
238
+ # Sets a flag that indicates whether Delegated Authentication
239
+ # is non-provisioned (i.e. does not use an application ID or secret
240
+ # key).
241
+ #####################################################################
242
+ attr_accessor :force_delauth_nonprovisioned
243
+
244
+ #####################################################################
245
+ # Sets the privacy policy URL, to which the Windows Live ID consent
246
+ # service redirects users to view the privacy policy of your Web
247
+ # site for Delegated Authentication.
248
+ #####################################################################
249
+ def policyurl=(policyurl)
250
+ if ((policyurl.nil? or policyurl.empty?) and force_delauth_nonprovisioned)
251
+ fatal("Error: policyurl=: Invalid policy URL specified.")
252
+ end
253
+ @policyurl = policyurl
254
+ end
255
+
256
+ #####################################################################
257
+ # Gets the privacy policy URL for your site.
258
+ #####################################################################
259
+ def policyurl
260
+ if (@policyurl.nil? or @policyurl.empty?)
261
+ 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.")
262
+ raise("Error: policyurl: Policy URL must be set in a Del Auth non-provisioned scenario. Aborting.") if force_delauth_nonprovisioned
263
+ end
264
+ @policyurl
265
+ end
266
+
267
+ #####################################################################
268
+ # Sets the return URL--the URL on your site to which the consent
269
+ # service redirects users (along with the action, consent token,
270
+ # and application context) after they have successfully provided
271
+ # consent information for Delegated Authentication. This value will
272
+ # override the return URL specified during registration.
273
+ #####################################################################
274
+ def returnurl=(returnurl)
275
+ if ((returnurl.nil? or returnurl.empty?) and force_delauth_nonprovisioned)
276
+ fatal("Error: returnurl=: Invalid return URL specified.")
277
+ end
278
+ @returnurl = returnurl
279
+ end
280
+
281
+
282
+ #####################################################################
283
+ # Returns the return URL of your site.
284
+ #####################################################################
285
+ def returnurl
286
+ if ((@returnurl.nil? or @returnurl.empty?) and force_delauth_nonprovisioned)
287
+ fatal("Error: returnurl: Return URL must be set in a Del Auth non-provisioned scenario. Aborting.")
288
+ end
289
+ @returnurl
290
+ end
291
+
292
+ #####################################################################
293
+ # Sets or gets the base URL to use for the Windows Live Login server. You
294
+ # should not have to change this property. Furthermore, we recommend
295
+ # that you use the Sign In control instead of the URL methods
296
+ # provided here.
297
+ #####################################################################
298
+ attr_accessor :baseurl
299
+
300
+ def baseurl
301
+ if(@baseurl.nil? or @baseurl.empty?)
302
+ "http://login.live.com/"
303
+ else
304
+ @baseurl
305
+ end
306
+ end
307
+
308
+ #####################################################################
309
+ # Sets or gets the secure (HTTPS) URL to use for the Windows Live Login
310
+ # server. You should not have to change this property.
311
+ #####################################################################
312
+ attr_accessor :secureurl
313
+
314
+ def secureurl
315
+ if(@secureurl.nil? or @secureurl.empty?)
316
+ "https://login.live.com/"
317
+ else
318
+ @secureurl
319
+ end
320
+ end
321
+
322
+ #####################################################################
323
+ # Sets or gets the Consent Base URL to use for the Windows Live Consent
324
+ # server. You should not have to use or change this property directly.
325
+ #####################################################################
326
+ attr_accessor :consenturl
327
+
328
+ def consenturl
329
+ if(@consenturl.nil? or @consenturl.empty?)
330
+ "https://consent.live.com/"
331
+ else
332
+ @consenturl
333
+ end
334
+ end
335
+ end
336
+
337
+ #######################################################################
338
+ # Implementation of the basic methods needed for Web Authentication.
339
+ #######################################################################
340
+ class WindowsLiveLogin
341
+ #####################################################################
342
+ # Returns the sign-in URL to use for the Windows Live Login server.
343
+ # We recommend that you use the Sign In control instead.
344
+ #
345
+ # If you specify it, 'context' will be returned as-is in the sign-in
346
+ # response for site-specific use.
347
+ #####################################################################
348
+ def getLoginUrl(context=nil, market=nil)
349
+ url = baseurl + "wlogin.srf?appid=#{appid}"
350
+ url += "&alg=#{securityalgorithm}"
351
+ url += "&appctx=#{CGI.escape(context)}" if context
352
+ url += "&mkt=#{CGI.escape(market)}" if market
353
+ url
354
+ end
355
+
356
+ #####################################################################
357
+ # Returns the sign-out URL to use for the Windows Live Login server.
358
+ # We recommend that you use the Sign In control instead.
359
+ #####################################################################
360
+ def getLogoutUrl(market=nil)
361
+ url = baseurl + "logout.srf?appid=#{appid}"
362
+ url += "&mkt=#{CGI.escape(market)}" if market
363
+ url
364
+ end
365
+
366
+ #####################################################################
367
+ # Holds the user information after a successful sign-in.
368
+ #
369
+ # 'timestamp' is the time as obtained from the SSO token.
370
+ # 'id' is the pairwise unique ID for the user.
371
+ # 'context' is the application context that was originally passed to
372
+ # the sign-in request, if any.
373
+ # 'token' is the encrypted Web Authentication token that contains the
374
+ # UID. This can be cached in a cookie and the UID can be retrieved by
375
+ # calling the processToken method.
376
+ # 'usePersistentCookie?' indicates whether the application is
377
+ # expected to store the user token in a session or persistent
378
+ # cookie.
379
+ #####################################################################
380
+ class User
381
+ attr_reader :timestamp, :id, :context, :token
382
+
383
+ def usePersistentCookie?
384
+ @usePersistentCookie
385
+ end
386
+
387
+
388
+ #####################################################################
389
+ # Initialize the User with time stamp, userid, flags, context and token.
390
+ #####################################################################
391
+ def initialize(timestamp, id, flags, context, token)
392
+ self.timestamp = timestamp
393
+ self.id = id
394
+ self.flags = flags
395
+ self.context = context
396
+ self.token = token
397
+ end
398
+
399
+ private
400
+ attr_writer :timestamp, :id, :flags, :context, :token
401
+
402
+ #####################################################################
403
+ # Sets or gets the Unix timestamp as obtained from the SSO token.
404
+ #####################################################################
405
+ def timestamp=(timestamp)
406
+ raise("Error: User: Null timestamp in token.") unless timestamp
407
+ timestamp = timestamp.to_i
408
+ raise("Error: User: Invalid timestamp: #{timestamp}") if (timestamp <= 0)
409
+ @timestamp = Time.at timestamp
410
+ end
411
+
412
+ #####################################################################
413
+ # Sets or gets the pairwise unique ID for the user.
414
+ #####################################################################
415
+ def id=(id)
416
+ raise("Error: User: Null id in token.") unless id
417
+ raise("Error: User: Invalid id: #{id}") unless (id =~ /^\w+$/)
418
+ @id = id
419
+ end
420
+
421
+ #####################################################################
422
+ # Sets or gets the usePersistentCookie flag for the user.
423
+ #####################################################################
424
+ def flags=(flags)
425
+ @usePersistentCookie = false
426
+ if flags
427
+ @usePersistentCookie = ((flags.to_i % 2) == 1)
428
+ end
429
+ end
430
+ end
431
+
432
+ #####################################################################
433
+ # Processes the sign-in response from the Windows Live sign-in server.
434
+ #
435
+ # 'query' contains the preprocessed POST table, such as that
436
+ # returned by CGI.params or Rails. (The unprocessed POST string
437
+ # could also be used here but we do not recommend it).
438
+ #
439
+ # This method returns a User object on successful sign-in; otherwise
440
+ # it returns nil.
441
+ #####################################################################
442
+ def processLogin(query)
443
+ query = parse query
444
+ unless query
445
+ debug("Error: processLogin: Failed to parse query.")
446
+ return
447
+ end
448
+ action = query['action']
449
+ unless action == 'login'
450
+ debug("Warning: processLogin: query action ignored: #{action}.")
451
+ return
452
+ end
453
+ token = query['stoken']
454
+ context = CGI.unescape(query['appctx']) if query['appctx']
455
+ processToken(token, context)
456
+ end
457
+
458
+ #####################################################################
459
+ # Decodes and validates a Web Authentication token. Returns a User
460
+ # object on success. If a context is passed in, it will be returned
461
+ # as the context field in the User object.
462
+ #####################################################################
463
+ def processToken(token, context=nil)
464
+ if token.nil? or token.empty?
465
+ debug("Error: processToken: Null/empty token.")
466
+ return
467
+ end
468
+ stoken = decodeAndValidateToken token
469
+ stoken = parse stoken
470
+ unless stoken
471
+ debug("Error: processToken: Failed to decode/validate token: #{token}")
472
+ return
473
+ end
474
+ sappid = stoken['appid']
475
+ unless sappid == appid
476
+ debug("Error: processToken: Application ID in token did not match ours: #{sappid}, #{appid}")
477
+ return
478
+ end
479
+ begin
480
+ user = User.new(stoken['ts'], stoken['uid'], stoken['flags'],
481
+ context, token)
482
+ return user
483
+ rescue Exception => e
484
+ debug("Error: processToken: Contents of token considered invalid: #{e}")
485
+ return
486
+ end
487
+ end
488
+
489
+ #####################################################################
490
+ # Returns an appropriate content type and body response that the
491
+ # application handler can return to signify a successful sign-out
492
+ # from the application.
493
+ #
494
+ # When a user signs out of Windows Live or a Windows Live
495
+ # application, a best-effort attempt is made at signing the user out
496
+ # from all other Windows Live applications the user might be signed
497
+ # in to. This is done by calling the handler page for each
498
+ # application with 'action' set to 'clearcookie' in the query
499
+ # string. The application handler is then responsible for clearing
500
+ # any cookies or data associated with the sign-in. After successfully
501
+ # signing the user out, the handler should return a GIF (any GIF)
502
+ # image as response to the 'action=clearcookie' query.
503
+ #####################################################################
504
+ def getClearCookieResponse()
505
+ type = "image/gif"
506
+ content = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7"
507
+ content = Base64.decode64(content)
508
+ return type, content
509
+ end
510
+ end
511
+
512
+ #######################################################################
513
+ # Implementation of the basic methods needed for Delegated
514
+ # Authentication.
515
+ #######################################################################
516
+ class WindowsLiveLogin
517
+ #####################################################################
518
+ # Returns the consent URL to use for Delegated Authentication for
519
+ # the given comma-delimited list of offers.
520
+ #
521
+ # If you specify it, 'context' will be returned as-is in the consent
522
+ # response for site-specific use.
523
+ #
524
+ # The registered/configured return URL can also be overridden by
525
+ # specifying 'ru' here.
526
+ #
527
+ # You can change the language in which the consent page is displayed
528
+ # by specifying a culture ID (For example, 'fr-fr' or 'en-us') in the
529
+ # 'market' parameter.
530
+ #####################################################################
531
+ def getConsentUrl(offers, context=nil, ru=nil, market=nil)
532
+ if (offers.nil? or offers.empty?)
533
+ fatal("Error: getConsentUrl: Invalid offers list.")
534
+ end
535
+ url = consenturl + "Delegation.aspx?ps=#{CGI.escape(offers)}"
536
+ url += "&appctx=#{CGI.escape(context)}" if context
537
+ ru = returnurl if (ru.nil? or ru.empty?)
538
+ url += "&ru=#{CGI.escape(ru)}" if ru
539
+ pu = policyurl
540
+ url += "&pl=#{CGI.escape(pu)}" if pu
541
+ url += "&mkt=#{CGI.escape(market)}" if market
542
+ url += "&app=#{getAppVerifier()}" unless force_delauth_nonprovisioned
543
+ url
544
+ end
545
+
546
+ #####################################################################
547
+ # Returns the URL to use to download a new consent token, given the
548
+ # offers and refresh token.
549
+ # The registered/configured return URL can also be overridden by
550
+ # specifying 'ru' here.
551
+ #####################################################################
552
+ def getRefreshConsentTokenUrl(offers, refreshtoken, ru)
553
+ if (offers.nil? or offers.empty?)
554
+ fatal("Error: getRefreshConsentTokenUrl: Invalid offers list.")
555
+ end
556
+ if (refreshtoken.nil? or refreshtoken.empty?)
557
+ fatal("Error: getRefreshConsentTokenUrl: Invalid refresh token.")
558
+ end
559
+ url = consenturl + "RefreshToken.aspx?ps=#{CGI.escape(offers)}"
560
+ url += "&reft=#{refreshtoken}"
561
+ ru = returnurl if (ru.nil? or ru.empty?)
562
+ url += "&ru=#{CGI.escape(ru)}" if ru
563
+ url += "&app=#{getAppVerifier()}" unless force_delauth_nonprovisioned
564
+ url
565
+ end
566
+
567
+ #####################################################################
568
+ # Returns the URL for the consent-management user interface.
569
+ # You can change the language in which the consent page is displayed
570
+ # by specifying a culture ID (For example, 'fr-fr' or 'en-us') in the
571
+ # 'market' parameter.
572
+ #####################################################################
573
+ def getManageConsentUrl(market=nil)
574
+ url = consenturl + "ManageConsent.aspx"
575
+ url += "?mkt=#{CGI.escape(market)}" if market
576
+ url
577
+ end
578
+
579
+ class ConsentToken
580
+ attr_reader :delegationtoken, :refreshtoken, :sessionkey, :expiry
581
+ attr_reader :offers, :offers_string, :locationid, :context
582
+ attr_reader :decodedtoken, :token
583
+
584
+ #####################################################################
585
+ # Indicates whether the delegation token is set and has not expired.
586
+ #####################################################################
587
+ def isValid?
588
+ return false unless delegationtoken
589
+ return ((Time.now.to_i-300) < expiry.to_i)
590
+ end
591
+
592
+ #####################################################################
593
+ # Refreshes the current token and replace it. If operation succeeds
594
+ # true is returned to signify success.
595
+ #####################################################################
596
+ def refresh
597
+ ct = @wll.refreshConsentToken(self)
598
+ return false unless ct
599
+ copy(ct)
600
+ true
601
+ end
602
+
603
+ #####################################################################
604
+ # Initialize the ConsentToken module with the WindowsLiveLogin,
605
+ # delegation token, refresh token, session key, expiry, offers,
606
+ # location ID, context, decoded token, and raw token.
607
+ #####################################################################
608
+ def initialize(wll, delegationtoken, refreshtoken, sessionkey, expiry,
609
+ offers, locationid, context, decodedtoken, token)
610
+ @wll = wll
611
+ self.delegationtoken = delegationtoken
612
+ self.refreshtoken = refreshtoken
613
+ self.sessionkey = sessionkey
614
+ self.expiry = expiry
615
+ self.offers = offers
616
+ self.locationid = locationid
617
+ self.context = context
618
+ self.decodedtoken = decodedtoken
619
+ self.token = token
620
+ end
621
+
622
+ private
623
+ attr_writer :delegationtoken, :refreshtoken, :sessionkey, :expiry
624
+ attr_writer :offers, :offers_string, :locationid, :context
625
+ attr_writer :decodedtoken, :token, :locationid
626
+
627
+ #####################################################################
628
+ # Sets the delegation token.
629
+ #####################################################################
630
+ def delegationtoken=(delegationtoken)
631
+ if (delegationtoken.nil? or delegationtoken.empty?)
632
+ raise("Error: ConsentToken: Null delegation token.")
633
+ end
634
+ @delegationtoken = delegationtoken
635
+ end
636
+
637
+ #####################################################################
638
+ # Sets the session key.
639
+ #####################################################################
640
+ def sessionkey=(sessionkey)
641
+ if (sessionkey.nil? or sessionkey.empty?)
642
+ raise("Error: ConsentToken: Null session key.")
643
+ end
644
+ @sessionkey = @wll.u64(sessionkey)
645
+ end
646
+
647
+ #####################################################################
648
+ # Sets the expiry time of the delegation token.
649
+ #####################################################################
650
+ def expiry=(expiry)
651
+ if (expiry.nil? or expiry.empty?)
652
+ raise("Error: ConsentToken: Null expiry time.")
653
+ end
654
+ expiry = expiry.to_i
655
+ raise("Error: ConsentToken: Invalid expiry: #{expiry}") if (expiry <= 0)
656
+ @expiry = Time.at expiry
657
+ end
658
+
659
+ #####################################################################
660
+ # Sets the offers/actions for which the user granted consent.
661
+ #####################################################################
662
+ def offers=(offers)
663
+ if (offers.nil? or offers.empty?)
664
+ raise("Error: ConsentToken: Null offers.")
665
+ end
666
+
667
+ @offers_string = ""
668
+ @offers = []
669
+
670
+ offers = CGI.unescape(offers)
671
+ offers = offers.split(";")
672
+ offers.each{|offer|
673
+ offer = offer.split(":")[0]
674
+ @offers_string += "," unless @offers_string.empty?
675
+ @offers_string += offer
676
+ @offers.push(offer)
677
+ }
678
+ end
679
+
680
+ #####################################################################
681
+ # Sets the LocationID.
682
+ #####################################################################
683
+ def locationid=(locationid)
684
+ if (locationid.nil? or locationid.empty?)
685
+ raise("Error: ConsentToken: Null Location ID.")
686
+ end
687
+ @locationid = locationid
688
+ end
689
+
690
+ #####################################################################
691
+ # Makes a copy of the ConsentToken object.
692
+ #####################################################################
693
+ def copy(consenttoken)
694
+ @delegationtoken = consenttoken.delegationtoken
695
+ @refreshtoken = consenttoken.refreshtoken
696
+ @sessionkey = consenttoken.sessionkey
697
+ @expiry = consenttoken.expiry
698
+ @offers = consenttoken.offers
699
+ @locationid = consenttoken.locationid
700
+ @offers_string = consenttoken.offers_string
701
+ @decodedtoken = consenttoken.decodedtoken
702
+ @token = consenttoken.token
703
+ end
704
+ end
705
+
706
+ #####################################################################
707
+ # Processes the POST response from the Delegated Authentication
708
+ # service after a user has granted consent. The processConsent
709
+ # function extracts the consent token string and returns the result
710
+ # of invoking the processConsentToken method.
711
+ #####################################################################
712
+ def processConsent(query)
713
+ query = parse query
714
+ unless query
715
+ debug("Error: processConsent: Failed to parse query.")
716
+ return
717
+ end
718
+ action = query['action']
719
+ unless action == 'delauth'
720
+ debug("Warning: processConsent: query action ignored: #{action}.")
721
+ return
722
+ end
723
+ responsecode = query['ResponseCode']
724
+ unless responsecode == 'RequestApproved'
725
+ debug("Error: processConsent: Consent was not successfully granted: #{responsecode}")
726
+ return
727
+ end
728
+ token = query['ConsentToken']
729
+ context = CGI.unescape(query['appctx']) if query['appctx']
730
+ processConsentToken(token, context)
731
+ end
732
+
733
+ #####################################################################
734
+ # Processes the consent token string that is returned in the POST
735
+ # response by the Delegated Authentication service after a
736
+ # user has granted consent.
737
+ #####################################################################
738
+ def processConsentToken(token, context=nil)
739
+ if token.nil? or token.empty?
740
+ debug("Error: processConsentToken: Null token.")
741
+ return
742
+ end
743
+ decodedtoken = token
744
+ parsedtoken = parse(CGI.unescape(decodedtoken))
745
+ unless parsedtoken
746
+ debug("Error: processConsentToken: Failed to parse token: #{token}")
747
+ return
748
+ end
749
+ eact = parsedtoken['eact']
750
+ if eact
751
+ decodedtoken = decodeAndValidateToken eact
752
+ unless decodedtoken
753
+ debug("Error: processConsentToken: Failed to decode/validate token: #{token}")
754
+ return
755
+ end
756
+ parsedtoken = parse(decodedtoken)
757
+ decodedtoken = CGI.escape(decodedtoken)
758
+ end
759
+ begin
760
+ consenttoken = ConsentToken.new(self,
761
+ parsedtoken['delt'],
762
+ parsedtoken['reft'],
763
+ parsedtoken['skey'],
764
+ parsedtoken['exp'],
765
+ parsedtoken['offer'],
766
+ parsedtoken['lid'],
767
+ context, decodedtoken, token)
768
+ return consenttoken
769
+ rescue Exception => e
770
+ debug("Error: processConsentToken: Contents of token considered invalid: #{e}")
771
+ return
772
+ end
773
+ end
774
+
775
+ #####################################################################
776
+ # Attempts to obtain a new, refreshed token and return it. The
777
+ # original token is not modified.
778
+ #####################################################################
779
+ def refreshConsentToken(consenttoken, ru=nil)
780
+ if consenttoken.nil?
781
+ debug("Error: refreshConsentToken: Null consent token.")
782
+ return
783
+ end
784
+ refreshConsentToken2(consenttoken.offers_string, consenttoken.refreshtoken, ru)
785
+ end
786
+
787
+ #####################################################################
788
+ # Helper function to obtain a new, refreshed token and return it.
789
+ # The original token is not modified.
790
+ #####################################################################
791
+ def refreshConsentToken2(offers_string, refreshtoken, ru=nil)
792
+ url = nil
793
+ begin
794
+ url = getRefreshConsentTokenUrl(offers_string, refreshtoken, ru)
795
+ ret = fetch url
796
+ ret.value # raises exception if fetch failed
797
+ body = ret.body
798
+ body.scan(/\{"ConsentToken":"(.*)"\}/){|match|
799
+ return processConsentToken("#{match}")
800
+ }
801
+ debug("Error: refreshConsentToken2: Failed to extract token: #{body}")
802
+ rescue Exception => e
803
+ debug("Error: Failed to refresh consent token: #{e}")
804
+ end
805
+ return
806
+ end
807
+ end
808
+
809
+ #######################################################################
810
+ # Common methods.
811
+ #######################################################################
812
+ class WindowsLiveLogin
813
+
814
+ #####################################################################
815
+ # Decodes and validates the token.
816
+ #####################################################################
817
+ def decodeAndValidateToken(token, cryptkey=@cryptkey, signkey=@signkey,
818
+ internal_allow_recursion=true)
819
+ haveoldsecret = false
820
+ if (oldsecretexpiry and (Time.now.to_i < oldsecretexpiry.to_i))
821
+ haveoldsecret = true if (@oldcryptkey and @oldsignkey)
822
+ end
823
+ haveoldsecret = (haveoldsecret and internal_allow_recursion)
824
+
825
+ stoken = decodeToken(token, cryptkey)
826
+ stoken = validateToken(stoken, signkey) if stoken
827
+ if (stoken.nil? and haveoldsecret)
828
+ debug("Warning: Failed to validate token with current secret, attempting old secret.")
829
+ stoken = decodeAndValidateToken(token, @oldcryptkey, @oldsignkey, false)
830
+ end
831
+ stoken
832
+ end
833
+
834
+ #####################################################################
835
+ # Decodes the given token string; returns undef on failure.
836
+ #
837
+ # First, the string is URL-unescaped and base64 decoded.
838
+ # Second, the IV is extracted from the first 16 bytes of the string.
839
+ # Finally, the string is decrypted using the encryption key.
840
+ #####################################################################
841
+ def decodeToken(token, cryptkey=@cryptkey)
842
+ if (cryptkey.nil? or cryptkey.empty?)
843
+ fatal("Error: decodeToken: Secret key was not set. Aborting.")
844
+ end
845
+ token = u64(token)
846
+ if (token.nil? or (token.size <= 16) or !(token.size % 16).zero?)
847
+ debug("Error: decodeToken: Attempted to decode invalid token.")
848
+ return
849
+ end
850
+ iv = token[0..15]
851
+ crypted = token[16..-1]
852
+ begin
853
+ aes128cbc = OpenSSL::Cipher::AES128.new("CBC")
854
+ aes128cbc.decrypt
855
+ aes128cbc.iv = iv
856
+ aes128cbc.key = cryptkey
857
+ decrypted = aes128cbc.update(crypted) + aes128cbc.final
858
+ rescue Exception => e
859
+ debug("Error: decodeToken: Decryption failed: #{token}, #{e}")
860
+ return
861
+ end
862
+ decrypted
863
+ end
864
+
865
+ #####################################################################
866
+ # Creates a signature for the given string by using the signature
867
+ # key.
868
+ #####################################################################
869
+ def signToken(token, signkey=@signkey)
870
+ if (signkey.nil? or signkey.empty?)
871
+ fatal("Error: signToken: Secret key was not set. Aborting.")
872
+ end
873
+ begin
874
+ return HMAC::SHA256.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 = 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