lperichon-contacts 1.0.3 → 1.0.4

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