aurelian-contacts 0.3.1

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