oa-more 0.2.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README.rdoc +16 -0
- data/lib/omniauth/more.rb +7 -0
- data/lib/omniauth/strategies/windows_live/windowslivelogin.rb +1143 -0
- data/lib/omniauth/strategies/windows_live.rb +38 -0
- metadata +175 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2010-2011 Michael Bleigh and Intridea, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
= OmniAuth::More
|
2
|
+
|
3
|
+
OmniAuth stratgies for authentication providers that do not
|
4
|
+
fit into one of the other authentication gems.
|
5
|
+
|
6
|
+
== Installation
|
7
|
+
|
8
|
+
To install omniauth as a suite of gems:
|
9
|
+
|
10
|
+
gem install omniauth
|
11
|
+
|
12
|
+
To install just the providers in the "more" gem:
|
13
|
+
|
14
|
+
gem install oa-more
|
15
|
+
|
16
|
+
|
@@ -0,0 +1,1143 @@
|
|
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
|
+
module OmniAuth; module Strategies; class WindowsLive; 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 end end end
|
1143
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'omniauth/core'
|
2
|
+
require 'omniauth/strategies/windows_live/windowslivelogin'
|
3
|
+
|
4
|
+
module OmniAuth
|
5
|
+
module Strategies
|
6
|
+
class WindowsLive
|
7
|
+
include OmniAuth::Strategy
|
8
|
+
|
9
|
+
attr_accessor :app_id, :app_secret
|
10
|
+
|
11
|
+
# Initialize the strategy by providing
|
12
|
+
#
|
13
|
+
# @param app_id [String] The application ID from your registered app with Microsoft.
|
14
|
+
# @param app_secret [String] The secret from your registered app with Microsoft.
|
15
|
+
# @option options [String] :locale A localization string for the login, should be in the form `en-us` or similar.
|
16
|
+
# @option options [String] :state Some state information that is serialized into the query string upon callback.
|
17
|
+
# @option options [Boolean] :ssl Whether or not to use SSL for login. Defaults to `true`.
|
18
|
+
# @option options [Boolean] :force_nonprovisioned When true, forces a non-provisioned (i.e. no app id or secret) mode.
|
19
|
+
def initialize(app, app_id = nil, app_secret = nil, options = {})
|
20
|
+
self.app_id = app_id
|
21
|
+
self.app_secret = app_secret
|
22
|
+
super(app, :windows_live, app_id, app_secret, options)
|
23
|
+
options[:ssl] ||= true
|
24
|
+
options[:locale] ||= 'en-us'
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def consumer
|
30
|
+
WindowsLiveLogin.new app_id, app_secret, options[:security_algorithm], options[:force_nonprovisioned], options[:policy_url], callback_url
|
31
|
+
end
|
32
|
+
|
33
|
+
def request_phase
|
34
|
+
redirect consumer.getLoginUrl(options[:state], options[:locale])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: oa-more
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: true
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
- beta2
|
10
|
+
version: 0.2.0.beta2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Michael Bleigh
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-01-14 00:00:00 -06:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: oa-core
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - "="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
- 2
|
31
|
+
- 0
|
32
|
+
- beta2
|
33
|
+
version: 0.2.0.beta2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rake
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: mg
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ~>
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
- 0
|
60
|
+
- 8
|
61
|
+
version: 0.0.8
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: *id003
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
name: rspec
|
67
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ~>
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
segments:
|
73
|
+
- 1
|
74
|
+
- 3
|
75
|
+
- 0
|
76
|
+
version: 1.3.0
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *id004
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: webmock
|
82
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ~>
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
segments:
|
88
|
+
- 1
|
89
|
+
- 3
|
90
|
+
- 4
|
91
|
+
version: 1.3.4
|
92
|
+
type: :development
|
93
|
+
prerelease: false
|
94
|
+
version_requirements: *id005
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: rack-test
|
97
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ~>
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
segments:
|
103
|
+
- 0
|
104
|
+
- 5
|
105
|
+
- 4
|
106
|
+
version: 0.5.4
|
107
|
+
type: :development
|
108
|
+
prerelease: false
|
109
|
+
version_requirements: *id006
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: json
|
112
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
segments:
|
118
|
+
- 1
|
119
|
+
- 4
|
120
|
+
- 3
|
121
|
+
version: 1.4.3
|
122
|
+
type: :development
|
123
|
+
prerelease: false
|
124
|
+
version_requirements: *id007
|
125
|
+
description: Additional strategies for OmniAuth.
|
126
|
+
email: michael@intridea.com
|
127
|
+
executables: []
|
128
|
+
|
129
|
+
extensions: []
|
130
|
+
|
131
|
+
extra_rdoc_files: []
|
132
|
+
|
133
|
+
files:
|
134
|
+
- lib/omniauth/more.rb
|
135
|
+
- lib/omniauth/strategies/windows_live/windowslivelogin.rb
|
136
|
+
- lib/omniauth/strategies/windows_live.rb
|
137
|
+
- README.rdoc
|
138
|
+
- LICENSE
|
139
|
+
has_rdoc: true
|
140
|
+
homepage: http://github.com/intridea/omniauth
|
141
|
+
licenses: []
|
142
|
+
|
143
|
+
post_install_message:
|
144
|
+
rdoc_options: []
|
145
|
+
|
146
|
+
require_paths:
|
147
|
+
- lib
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
149
|
+
none: false
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
hash: 220420108562034819
|
154
|
+
segments:
|
155
|
+
- 0
|
156
|
+
version: "0"
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
none: false
|
159
|
+
requirements:
|
160
|
+
- - ">"
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
segments:
|
163
|
+
- 1
|
164
|
+
- 3
|
165
|
+
- 1
|
166
|
+
version: 1.3.1
|
167
|
+
requirements: []
|
168
|
+
|
169
|
+
rubyforge_project:
|
170
|
+
rubygems_version: 1.3.7
|
171
|
+
signing_key:
|
172
|
+
specification_version: 3
|
173
|
+
summary: Additional strategies for OmniAuth.
|
174
|
+
test_files: []
|
175
|
+
|