ruby-openid2 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +136 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +54 -0
- data/LICENSE.txt +210 -0
- data/README.md +81 -0
- data/SECURITY.md +15 -0
- data/lib/hmac/hmac.rb +110 -0
- data/lib/hmac/sha1.rb +11 -0
- data/lib/hmac/sha2.rb +25 -0
- data/lib/openid/association.rb +246 -0
- data/lib/openid/consumer/associationmanager.rb +354 -0
- data/lib/openid/consumer/checkid_request.rb +179 -0
- data/lib/openid/consumer/discovery.rb +516 -0
- data/lib/openid/consumer/discovery_manager.rb +144 -0
- data/lib/openid/consumer/html_parse.rb +142 -0
- data/lib/openid/consumer/idres.rb +513 -0
- data/lib/openid/consumer/responses.rb +147 -0
- data/lib/openid/consumer/session.rb +36 -0
- data/lib/openid/consumer.rb +406 -0
- data/lib/openid/cryptutil.rb +112 -0
- data/lib/openid/dh.rb +84 -0
- data/lib/openid/extension.rb +38 -0
- data/lib/openid/extensions/ax.rb +552 -0
- data/lib/openid/extensions/oauth.rb +88 -0
- data/lib/openid/extensions/pape.rb +170 -0
- data/lib/openid/extensions/sreg.rb +268 -0
- data/lib/openid/extensions/ui.rb +49 -0
- data/lib/openid/fetchers.rb +277 -0
- data/lib/openid/kvform.rb +113 -0
- data/lib/openid/kvpost.rb +62 -0
- data/lib/openid/message.rb +555 -0
- data/lib/openid/protocolerror.rb +7 -0
- data/lib/openid/server.rb +1571 -0
- data/lib/openid/store/filesystem.rb +260 -0
- data/lib/openid/store/interface.rb +73 -0
- data/lib/openid/store/memcache.rb +109 -0
- data/lib/openid/store/memory.rb +79 -0
- data/lib/openid/store/nonce.rb +72 -0
- data/lib/openid/trustroot.rb +597 -0
- data/lib/openid/urinorm.rb +72 -0
- data/lib/openid/util.rb +119 -0
- data/lib/openid/version.rb +5 -0
- data/lib/openid/yadis/accept.rb +141 -0
- data/lib/openid/yadis/constants.rb +16 -0
- data/lib/openid/yadis/discovery.rb +151 -0
- data/lib/openid/yadis/filters.rb +192 -0
- data/lib/openid/yadis/htmltokenizer.rb +290 -0
- data/lib/openid/yadis/parsehtml.rb +50 -0
- data/lib/openid/yadis/services.rb +44 -0
- data/lib/openid/yadis/xrds.rb +160 -0
- data/lib/openid/yadis/xri.rb +86 -0
- data/lib/openid/yadis/xrires.rb +87 -0
- data/lib/openid.rb +27 -0
- data/lib/ruby-openid.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +331 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,1571 @@
|
|
1
|
+
require_relative "cryptutil"
|
2
|
+
require_relative "util"
|
3
|
+
require_relative "dh"
|
4
|
+
require_relative "store/nonce"
|
5
|
+
require_relative "trustroot"
|
6
|
+
require_relative "association"
|
7
|
+
require_relative "message"
|
8
|
+
|
9
|
+
require "time"
|
10
|
+
|
11
|
+
module OpenID
|
12
|
+
module Server
|
13
|
+
HTTP_OK = 200
|
14
|
+
HTTP_REDIRECT = 302
|
15
|
+
HTTP_ERROR = 400
|
16
|
+
|
17
|
+
BROWSER_REQUEST_MODES = %w[checkid_setup checkid_immediate]
|
18
|
+
|
19
|
+
ENCODE_KVFORM = ["kvform"].freeze
|
20
|
+
ENCODE_URL = ["URL/redirect"].freeze
|
21
|
+
ENCODE_HTML_FORM = ["HTML form"].freeze
|
22
|
+
|
23
|
+
UNUSED = nil
|
24
|
+
|
25
|
+
class OpenIDRequest
|
26
|
+
attr_accessor :message, :mode
|
27
|
+
|
28
|
+
# I represent an incoming OpenID request.
|
29
|
+
#
|
30
|
+
# Attributes:
|
31
|
+
# mode:: The "openid.mode" of this request
|
32
|
+
def initialize
|
33
|
+
@mode = nil
|
34
|
+
@message = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def namespace
|
38
|
+
raise "Request has no message" if @message.nil?
|
39
|
+
|
40
|
+
@message.get_openid_namespace
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# A request to verify the validity of a previous response.
|
45
|
+
#
|
46
|
+
# See OpenID Specs, Verifying Directly with the OpenID Provider
|
47
|
+
# <http://openid.net/specs/openid-authentication-2_0-12.html#verifying_signatures>
|
48
|
+
class CheckAuthRequest < OpenIDRequest
|
49
|
+
# The association handle the response was signed with.
|
50
|
+
attr_accessor :assoc_handle
|
51
|
+
|
52
|
+
# The message with the signature which wants checking.
|
53
|
+
attr_accessor :signed
|
54
|
+
|
55
|
+
# An association handle the client is asking about the validity
|
56
|
+
# of. May be nil.
|
57
|
+
attr_accessor :invalidate_handle
|
58
|
+
|
59
|
+
attr_accessor :sig
|
60
|
+
|
61
|
+
# Construct me.
|
62
|
+
#
|
63
|
+
# These parameters are assigned directly as class attributes.
|
64
|
+
#
|
65
|
+
# Parameters:
|
66
|
+
# assoc_handle:: the association handle for this request
|
67
|
+
# signed:: The signed message
|
68
|
+
# invalidate_handle:: An association handle that the relying
|
69
|
+
# party is checking to see if it is invalid
|
70
|
+
def initialize(assoc_handle, signed, invalidate_handle = nil)
|
71
|
+
super()
|
72
|
+
|
73
|
+
@mode = "check_authentication"
|
74
|
+
@required_fields = %w[identity return_to response_nonce].freeze
|
75
|
+
|
76
|
+
@sig = nil
|
77
|
+
@assoc_handle = assoc_handle
|
78
|
+
@signed = signed
|
79
|
+
@invalidate_handle = invalidate_handle
|
80
|
+
end
|
81
|
+
|
82
|
+
# Construct me from an OpenID::Message.
|
83
|
+
def self.from_message(message, _op_endpoint = UNUSED)
|
84
|
+
assoc_handle = message.get_arg(OPENID_NS, "assoc_handle")
|
85
|
+
invalidate_handle = message.get_arg(OPENID_NS, "invalidate_handle")
|
86
|
+
|
87
|
+
signed = message.copy
|
88
|
+
# openid.mode is currently check_authentication because
|
89
|
+
# that's the mode of this request. But the signature
|
90
|
+
# was made on something with a different openid.mode.
|
91
|
+
# http://article.gmane.org/gmane.comp.web.openid.general/537
|
92
|
+
signed.set_arg(OPENID_NS, "mode", "id_res") if signed.has_key?(OPENID_NS, "mode")
|
93
|
+
|
94
|
+
obj = new(assoc_handle, signed, invalidate_handle)
|
95
|
+
obj.message = message
|
96
|
+
obj.sig = message.get_arg(OPENID_NS, "sig")
|
97
|
+
|
98
|
+
if !obj.assoc_handle or
|
99
|
+
!obj.sig
|
100
|
+
msg = format(
|
101
|
+
"%s request missing required parameter from message %s",
|
102
|
+
obj.mode,
|
103
|
+
message,
|
104
|
+
)
|
105
|
+
raise ProtocolError.new(message, msg)
|
106
|
+
end
|
107
|
+
|
108
|
+
obj
|
109
|
+
end
|
110
|
+
|
111
|
+
# Respond to this request.
|
112
|
+
#
|
113
|
+
# Given a Signatory, I can check the validity of the signature
|
114
|
+
# and the invalidate_handle. I return a response with an
|
115
|
+
# is_valid (and, if appropriate invalidate_handle) field.
|
116
|
+
def answer(signatory)
|
117
|
+
is_valid = signatory.verify(@assoc_handle, @signed)
|
118
|
+
# Now invalidate that assoc_handle so it this checkAuth
|
119
|
+
# message cannot be replayed.
|
120
|
+
signatory.invalidate(@assoc_handle, true)
|
121
|
+
response = OpenIDResponse.new(self)
|
122
|
+
valid_str = is_valid ? "true" : "false"
|
123
|
+
response.fields.set_arg(OPENID_NS, "is_valid", valid_str)
|
124
|
+
|
125
|
+
if @invalidate_handle
|
126
|
+
assoc = signatory.get_association(@invalidate_handle, false)
|
127
|
+
unless assoc
|
128
|
+
response.fields.set_arg(
|
129
|
+
OPENID_NS, "invalidate_handle", @invalidate_handle
|
130
|
+
)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
response
|
135
|
+
end
|
136
|
+
|
137
|
+
def to_s
|
138
|
+
ih = nil
|
139
|
+
|
140
|
+
ih = if @invalidate_handle
|
141
|
+
format(" invalidate? %s", @invalidate_handle)
|
142
|
+
else
|
143
|
+
""
|
144
|
+
end
|
145
|
+
|
146
|
+
format(
|
147
|
+
"<%s handle: %s sig: %s: signed: %s%s>",
|
148
|
+
self.class,
|
149
|
+
@assoc_handle,
|
150
|
+
@sig,
|
151
|
+
@signed,
|
152
|
+
ih,
|
153
|
+
)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class BaseServerSession
|
158
|
+
attr_reader :session_type
|
159
|
+
|
160
|
+
def initialize(session_type, allowed_assoc_types)
|
161
|
+
@session_type = session_type
|
162
|
+
@allowed_assoc_types = allowed_assoc_types.dup.freeze
|
163
|
+
end
|
164
|
+
|
165
|
+
def allowed_assoc_type?(typ)
|
166
|
+
@allowed_assoc_types.member?(typ)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# An object that knows how to handle association requests with
|
171
|
+
# no session type.
|
172
|
+
#
|
173
|
+
# See OpenID Specs, Section 8: Establishing Associations
|
174
|
+
# <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
|
175
|
+
class PlainTextServerSession < BaseServerSession
|
176
|
+
# The session_type for this association session. There is no
|
177
|
+
# type defined for plain-text in the OpenID specification, so we
|
178
|
+
# use 'no-encryption'.
|
179
|
+
attr_reader :session_type
|
180
|
+
|
181
|
+
def initialize
|
182
|
+
super("no-encryption", %w[HMAC-SHA1 HMAC-SHA256])
|
183
|
+
end
|
184
|
+
|
185
|
+
def self.from_message(_unused_request)
|
186
|
+
new
|
187
|
+
end
|
188
|
+
|
189
|
+
def answer(secret)
|
190
|
+
{"mac_key" => Util.to_base64(secret)}
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# An object that knows how to handle association requests with the
|
195
|
+
# Diffie-Hellman session type.
|
196
|
+
#
|
197
|
+
# See OpenID Specs, Section 8: Establishing Associations
|
198
|
+
# <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
|
199
|
+
class DiffieHellmanSHA1ServerSession < BaseServerSession
|
200
|
+
# The Diffie-Hellman algorithm values for this request
|
201
|
+
attr_accessor :dh
|
202
|
+
|
203
|
+
# The public key sent by the consumer in the associate request
|
204
|
+
attr_accessor :consumer_pubkey
|
205
|
+
|
206
|
+
# The session_type for this association session.
|
207
|
+
attr_reader :session_type
|
208
|
+
|
209
|
+
def initialize(dh, consumer_pubkey)
|
210
|
+
super("DH-SHA1", ["HMAC-SHA1"])
|
211
|
+
|
212
|
+
@hash_func = CryptUtil.method(:sha1)
|
213
|
+
@dh = dh
|
214
|
+
@consumer_pubkey = consumer_pubkey
|
215
|
+
end
|
216
|
+
|
217
|
+
# Construct me from OpenID Message
|
218
|
+
#
|
219
|
+
# Raises ProtocolError when parameters required to establish the
|
220
|
+
# session are missing.
|
221
|
+
def self.from_message(message)
|
222
|
+
dh_modulus = message.get_arg(OPENID_NS, "dh_modulus")
|
223
|
+
dh_gen = message.get_arg(OPENID_NS, "dh_gen")
|
224
|
+
if (!dh_modulus and dh_gen) or
|
225
|
+
(!dh_gen and dh_modulus)
|
226
|
+
|
227
|
+
missing = if !dh_modulus
|
228
|
+
"modulus"
|
229
|
+
else
|
230
|
+
"generator"
|
231
|
+
end
|
232
|
+
|
233
|
+
raise ProtocolError.new(
|
234
|
+
message,
|
235
|
+
format(
|
236
|
+
"If non-default modulus or generator is " +
|
237
|
+
"supplied, both must be supplied. Missing %s",
|
238
|
+
missing,
|
239
|
+
),
|
240
|
+
)
|
241
|
+
end
|
242
|
+
|
243
|
+
if dh_modulus or dh_gen
|
244
|
+
dh_modulus = CryptUtil.base64_to_num(dh_modulus)
|
245
|
+
dh_gen = CryptUtil.base64_to_num(dh_gen)
|
246
|
+
dh = DiffieHellman.new(dh_modulus, dh_gen)
|
247
|
+
else
|
248
|
+
dh = DiffieHellman.from_defaults
|
249
|
+
end
|
250
|
+
|
251
|
+
consumer_pubkey = message.get_arg(OPENID_NS, "dh_consumer_public")
|
252
|
+
unless consumer_pubkey
|
253
|
+
raise ProtocolError.new(
|
254
|
+
message,
|
255
|
+
format(
|
256
|
+
"Public key for DH-SHA1 session " +
|
257
|
+
"not found in message %s",
|
258
|
+
message,
|
259
|
+
),
|
260
|
+
)
|
261
|
+
end
|
262
|
+
|
263
|
+
consumer_pubkey = CryptUtil.base64_to_num(consumer_pubkey)
|
264
|
+
|
265
|
+
new(dh, consumer_pubkey)
|
266
|
+
end
|
267
|
+
|
268
|
+
def answer(secret)
|
269
|
+
mac_key = @dh.xor_secret(
|
270
|
+
@hash_func,
|
271
|
+
@consumer_pubkey,
|
272
|
+
secret,
|
273
|
+
)
|
274
|
+
{
|
275
|
+
"dh_server_public" => CryptUtil.num_to_base64(@dh.public),
|
276
|
+
"enc_mac_key" => Util.to_base64(mac_key),
|
277
|
+
}
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
class DiffieHellmanSHA256ServerSession < DiffieHellmanSHA1ServerSession
|
282
|
+
def initialize(*args)
|
283
|
+
super
|
284
|
+
@session_type = "DH-SHA256"
|
285
|
+
@hash_func = CryptUtil.method(:sha256)
|
286
|
+
@allowed_assoc_types = ["HMAC-SHA256"].freeze
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# A request to establish an association.
|
291
|
+
#
|
292
|
+
# See OpenID Specs, Section 8: Establishing Associations
|
293
|
+
# <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
|
294
|
+
class AssociateRequest < OpenIDRequest
|
295
|
+
# An object that knows how to handle association requests of a
|
296
|
+
# certain type.
|
297
|
+
attr_accessor :session
|
298
|
+
|
299
|
+
# The type of association. Supported values include HMAC-SHA256
|
300
|
+
# and HMAC-SHA1
|
301
|
+
attr_accessor :assoc_type
|
302
|
+
|
303
|
+
@@session_classes = {
|
304
|
+
"no-encryption" => PlainTextServerSession,
|
305
|
+
"DH-SHA1" => DiffieHellmanSHA1ServerSession,
|
306
|
+
"DH-SHA256" => DiffieHellmanSHA256ServerSession,
|
307
|
+
}
|
308
|
+
|
309
|
+
# Construct me.
|
310
|
+
#
|
311
|
+
# The session is assigned directly as a class attribute. See my
|
312
|
+
# class documentation for its description.
|
313
|
+
def initialize(session, assoc_type)
|
314
|
+
super()
|
315
|
+
@session = session
|
316
|
+
@assoc_type = assoc_type
|
317
|
+
|
318
|
+
@mode = "associate"
|
319
|
+
end
|
320
|
+
|
321
|
+
# Construct me from an OpenID Message.
|
322
|
+
def self.from_message(message, _op_endpoint = UNUSED)
|
323
|
+
if message.is_openid1
|
324
|
+
session_type = message.get_arg(OPENID_NS, "session_type")
|
325
|
+
if session_type == "no-encryption"
|
326
|
+
Util.log("Received OpenID 1 request with a no-encryption " +
|
327
|
+
"association session type. Continuing anyway.")
|
328
|
+
elsif !session_type
|
329
|
+
session_type = "no-encryption"
|
330
|
+
end
|
331
|
+
else
|
332
|
+
session_type = message.get_arg(OPENID2_NS, "session_type")
|
333
|
+
unless session_type
|
334
|
+
raise ProtocolError.new(
|
335
|
+
message,
|
336
|
+
"session_type missing from request",
|
337
|
+
)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
session_class = @@session_classes[session_type]
|
342
|
+
|
343
|
+
unless session_class
|
344
|
+
raise ProtocolError.new(
|
345
|
+
message,
|
346
|
+
format("Unknown session type %s", session_type),
|
347
|
+
)
|
348
|
+
end
|
349
|
+
|
350
|
+
begin
|
351
|
+
session = session_class.from_message(message)
|
352
|
+
rescue ArgumentError => e
|
353
|
+
# XXX
|
354
|
+
raise ProtocolError.new(
|
355
|
+
message,
|
356
|
+
format(
|
357
|
+
"Error parsing %s session: %s",
|
358
|
+
session_type,
|
359
|
+
e,
|
360
|
+
),
|
361
|
+
)
|
362
|
+
end
|
363
|
+
|
364
|
+
assoc_type = message.get_arg(OPENID_NS, "assoc_type", "HMAC-SHA1")
|
365
|
+
unless session.allowed_assoc_type?(assoc_type)
|
366
|
+
msg = format(
|
367
|
+
"Session type %s does not support association type %s",
|
368
|
+
session_type,
|
369
|
+
assoc_type,
|
370
|
+
)
|
371
|
+
raise ProtocolError.new(message, msg)
|
372
|
+
end
|
373
|
+
|
374
|
+
obj = new(session, assoc_type)
|
375
|
+
obj.message = message
|
376
|
+
obj
|
377
|
+
end
|
378
|
+
|
379
|
+
# Respond to this request with an association.
|
380
|
+
#
|
381
|
+
# assoc:: The association to send back.
|
382
|
+
#
|
383
|
+
# Returns a response with the association information, encrypted
|
384
|
+
# to the consumer's public key if appropriate.
|
385
|
+
def answer(assoc)
|
386
|
+
response = OpenIDResponse.new(self)
|
387
|
+
response.fields.update_args(OPENID_NS, {
|
388
|
+
"expires_in" => format("%d", assoc.expires_in),
|
389
|
+
"assoc_type" => @assoc_type,
|
390
|
+
"assoc_handle" => assoc.handle,
|
391
|
+
})
|
392
|
+
response.fields.update_args(
|
393
|
+
OPENID_NS,
|
394
|
+
@session.answer(assoc.secret),
|
395
|
+
)
|
396
|
+
unless @session.session_type == "no-encryption" and
|
397
|
+
@message.is_openid1
|
398
|
+
response.fields.set_arg(
|
399
|
+
OPENID_NS, "session_type", @session.session_type
|
400
|
+
)
|
401
|
+
end
|
402
|
+
|
403
|
+
response
|
404
|
+
end
|
405
|
+
|
406
|
+
# Respond to this request indicating that the association type
|
407
|
+
# or association session type is not supported.
|
408
|
+
def answer_unsupported(message, preferred_association_type = nil,
|
409
|
+
preferred_session_type = nil)
|
410
|
+
raise ProtocolError.new(@message) if @message.is_openid1
|
411
|
+
|
412
|
+
response = OpenIDResponse.new(self)
|
413
|
+
response.fields.set_arg(OPENID_NS, "error_code", "unsupported-type")
|
414
|
+
response.fields.set_arg(OPENID_NS, "error", message)
|
415
|
+
|
416
|
+
if preferred_association_type
|
417
|
+
response.fields.set_arg(
|
418
|
+
OPENID_NS, "assoc_type", preferred_association_type
|
419
|
+
)
|
420
|
+
end
|
421
|
+
|
422
|
+
if preferred_session_type
|
423
|
+
response.fields.set_arg(
|
424
|
+
OPENID_NS, "session_type", preferred_session_type
|
425
|
+
)
|
426
|
+
end
|
427
|
+
|
428
|
+
response
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
# A request to confirm the identity of a user.
|
433
|
+
#
|
434
|
+
# This class handles requests for openid modes
|
435
|
+
# +checkid_immediate+ and +checkid_setup+ .
|
436
|
+
class CheckIDRequest < OpenIDRequest
|
437
|
+
# Provided in smart mode requests, a handle for a previously
|
438
|
+
# established association. nil for dumb mode requests.
|
439
|
+
attr_accessor :assoc_handle
|
440
|
+
|
441
|
+
# Is this an immediate-mode request?
|
442
|
+
attr_accessor :immediate
|
443
|
+
|
444
|
+
# The URL to send the user agent back to to reply to this
|
445
|
+
# request.
|
446
|
+
attr_accessor :return_to
|
447
|
+
|
448
|
+
# The OP-local identifier being checked.
|
449
|
+
attr_accessor :identity
|
450
|
+
|
451
|
+
# The claimed identifier. Not present in OpenID 1.x
|
452
|
+
# messages.
|
453
|
+
attr_accessor :claimed_id
|
454
|
+
|
455
|
+
# This URL identifies the party making the request, and the user
|
456
|
+
# will use that to make her decision about what answer she
|
457
|
+
# trusts them to have. Referred to as "realm" in OpenID 2.0.
|
458
|
+
attr_accessor :trust_root
|
459
|
+
|
460
|
+
# mode:: +checkid_immediate+ or +checkid_setup+
|
461
|
+
attr_accessor :mode
|
462
|
+
|
463
|
+
attr_accessor :op_endpoint
|
464
|
+
|
465
|
+
# These parameters are assigned directly as attributes,
|
466
|
+
# see the #CheckIDRequest class documentation for their
|
467
|
+
# descriptions.
|
468
|
+
#
|
469
|
+
# Raises #MalformedReturnURL when the +return_to+ URL is not
|
470
|
+
# a URL.
|
471
|
+
def initialize(identity, return_to, op_endpoint, trust_root = nil,
|
472
|
+
immediate = false, assoc_handle = nil, claimed_id = nil)
|
473
|
+
@assoc_handle = assoc_handle
|
474
|
+
@identity = identity
|
475
|
+
@claimed_id = (claimed_id or identity)
|
476
|
+
@return_to = return_to
|
477
|
+
@trust_root = (trust_root or return_to)
|
478
|
+
@op_endpoint = op_endpoint
|
479
|
+
@message = nil
|
480
|
+
|
481
|
+
if immediate
|
482
|
+
@immediate = true
|
483
|
+
@mode = "checkid_immediate"
|
484
|
+
else
|
485
|
+
@immediate = false
|
486
|
+
@mode = "checkid_setup"
|
487
|
+
end
|
488
|
+
|
489
|
+
if @return_to and
|
490
|
+
!TrustRoot::TrustRoot.parse(@return_to)
|
491
|
+
raise MalformedReturnURL.new(nil, @return_to)
|
492
|
+
end
|
493
|
+
|
494
|
+
return if trust_root_valid
|
495
|
+
|
496
|
+
raise UntrustedReturnURL.new(nil, @return_to, @trust_root)
|
497
|
+
end
|
498
|
+
|
499
|
+
# Construct me from an OpenID message.
|
500
|
+
#
|
501
|
+
# message:: An OpenID checkid_* request Message
|
502
|
+
#
|
503
|
+
# op_endpoint:: The endpoint URL of the server that this
|
504
|
+
# message was sent to.
|
505
|
+
#
|
506
|
+
# Raises:
|
507
|
+
# ProtocolError:: When not all required parameters are present
|
508
|
+
# in the message.
|
509
|
+
#
|
510
|
+
# MalformedReturnURL:: When the +return_to+ URL is not a URL.
|
511
|
+
#
|
512
|
+
# UntrustedReturnURL:: When the +return_to+ URL is
|
513
|
+
# outside the +trust_root+.
|
514
|
+
def self.from_message(message, op_endpoint)
|
515
|
+
obj = allocate
|
516
|
+
obj.message = message
|
517
|
+
obj.op_endpoint = op_endpoint
|
518
|
+
mode = message.get_arg(OPENID_NS, "mode")
|
519
|
+
if mode == "checkid_immediate"
|
520
|
+
obj.immediate = true
|
521
|
+
obj.mode = "checkid_immediate"
|
522
|
+
else
|
523
|
+
obj.immediate = false
|
524
|
+
obj.mode = "checkid_setup"
|
525
|
+
end
|
526
|
+
|
527
|
+
obj.return_to = message.get_arg(OPENID_NS, "return_to")
|
528
|
+
if message.is_openid1 and !obj.return_to
|
529
|
+
msg = format(
|
530
|
+
"Missing required field 'return_to' from %s",
|
531
|
+
message,
|
532
|
+
)
|
533
|
+
raise ProtocolError.new(message, msg)
|
534
|
+
end
|
535
|
+
|
536
|
+
obj.identity = message.get_arg(OPENID_NS, "identity")
|
537
|
+
obj.claimed_id = message.get_arg(OPENID_NS, "claimed_id")
|
538
|
+
if message.is_openid1
|
539
|
+
unless obj.identity
|
540
|
+
s = "OpenID 1 message did not contain openid.identity"
|
541
|
+
raise ProtocolError.new(message, s)
|
542
|
+
end
|
543
|
+
elsif obj.identity and !obj.claimed_id
|
544
|
+
s = ("OpenID 2.0 message contained openid.identity but not " +
|
545
|
+
"claimed_id")
|
546
|
+
raise ProtocolError.new(message, s)
|
547
|
+
elsif obj.claimed_id and !obj.identity
|
548
|
+
s = ("OpenID 2.0 message contained openid.claimed_id but not " +
|
549
|
+
"identity")
|
550
|
+
raise ProtocolError.new(message, s)
|
551
|
+
end
|
552
|
+
|
553
|
+
# There's a case for making self.trust_root be a TrustRoot
|
554
|
+
# here. But if TrustRoot isn't currently part of the "public"
|
555
|
+
# API, I'm not sure it's worth doing.
|
556
|
+
trust_root_param = if message.is_openid1
|
557
|
+
"trust_root"
|
558
|
+
else
|
559
|
+
"realm"
|
560
|
+
end
|
561
|
+
trust_root = message.get_arg(OPENID_NS, trust_root_param)
|
562
|
+
trust_root = obj.return_to if trust_root.nil? || trust_root.empty?
|
563
|
+
obj.trust_root = trust_root
|
564
|
+
|
565
|
+
if !message.is_openid1 and !obj.return_to and !obj.trust_root
|
566
|
+
raise ProtocolError.new(message, "openid.realm required when " +
|
567
|
+
"openid.return_to absent")
|
568
|
+
end
|
569
|
+
|
570
|
+
obj.assoc_handle = message.get_arg(OPENID_NS, "assoc_handle")
|
571
|
+
|
572
|
+
# Using TrustRoot.parse here is a bit misleading, as we're not
|
573
|
+
# parsing return_to as a trust root at all. However, valid
|
574
|
+
# URLs are valid trust roots, so we can use this to get an
|
575
|
+
# idea if it is a valid URL. Not all trust roots are valid
|
576
|
+
# return_to URLs, however (particularly ones with wildcards),
|
577
|
+
# so this is still a little sketchy.
|
578
|
+
if obj.return_to and
|
579
|
+
!TrustRoot::TrustRoot.parse(obj.return_to)
|
580
|
+
raise MalformedReturnURL.new(message, obj.return_to)
|
581
|
+
end
|
582
|
+
|
583
|
+
# I first thought that checking to see if the return_to is
|
584
|
+
# within the trust_root is premature here, a
|
585
|
+
# logic-not-decoding thing. But it was argued that this is
|
586
|
+
# really part of data validation. A request with an invalid
|
587
|
+
# trust_root/return_to is broken regardless of application,
|
588
|
+
# right?
|
589
|
+
raise UntrustedReturnURL.new(message, obj.return_to, obj.trust_root) unless obj.trust_root_valid
|
590
|
+
|
591
|
+
obj
|
592
|
+
end
|
593
|
+
|
594
|
+
# Is the identifier to be selected by the IDP?
|
595
|
+
def id_select
|
596
|
+
# So IDPs don't have to import the constant
|
597
|
+
@identity == IDENTIFIER_SELECT
|
598
|
+
end
|
599
|
+
|
600
|
+
# Is my return_to under my trust_root?
|
601
|
+
def trust_root_valid
|
602
|
+
return true unless @trust_root
|
603
|
+
|
604
|
+
tr = TrustRoot::TrustRoot.parse(@trust_root)
|
605
|
+
raise MalformedTrustRoot.new(@message, @trust_root) unless tr
|
606
|
+
|
607
|
+
return tr.validate_url(@return_to) if @return_to
|
608
|
+
|
609
|
+
true
|
610
|
+
end
|
611
|
+
|
612
|
+
# Does the relying party publish the return_to URL for this
|
613
|
+
# response under the realm? It is up to the provider to set a
|
614
|
+
# policy for what kinds of realms should be allowed. This
|
615
|
+
# return_to URL verification reduces vulnerability to
|
616
|
+
# data-theft attacks based on open proxies,
|
617
|
+
# corss-site-scripting, or open redirectors.
|
618
|
+
#
|
619
|
+
# This check should only be performed after making sure that
|
620
|
+
# the return_to URL matches the realm.
|
621
|
+
#
|
622
|
+
# Raises DiscoveryFailure if the realm
|
623
|
+
# URL does not support Yadis discovery (and so does not
|
624
|
+
# support the verification process).
|
625
|
+
#
|
626
|
+
# Returns true if the realm publishes a document with the
|
627
|
+
# return_to URL listed
|
628
|
+
def return_to_verified
|
629
|
+
TrustRoot.verify_return_to(@trust_root, @return_to)
|
630
|
+
end
|
631
|
+
|
632
|
+
# Respond to this request.
|
633
|
+
#
|
634
|
+
# allow:: Allow this user to claim this identity, and allow the
|
635
|
+
# consumer to have this information?
|
636
|
+
#
|
637
|
+
# server_url:: DEPRECATED. Passing op_endpoint to the
|
638
|
+
# #Server constructor makes this optional.
|
639
|
+
#
|
640
|
+
# When an OpenID 1.x immediate mode request does
|
641
|
+
# not succeed, it gets back a URL where the request
|
642
|
+
# may be carried out in a not-so-immediate fashion.
|
643
|
+
# Pass my URL in here (the fully qualified address
|
644
|
+
# of this server's endpoint, i.e.
|
645
|
+
# <tt>http://example.com/server</tt>), and I will
|
646
|
+
# use it as a base for the URL for a new request.
|
647
|
+
#
|
648
|
+
# Optional for requests where
|
649
|
+
# #CheckIDRequest.immediate is false or +allow+ is
|
650
|
+
# true.
|
651
|
+
#
|
652
|
+
# identity:: The OP-local identifier to answer with. Only for use
|
653
|
+
# when the relying party requested identifier selection.
|
654
|
+
#
|
655
|
+
# claimed_id:: The claimed identifier to answer with,
|
656
|
+
# for use with identifier selection in the case where the
|
657
|
+
# claimed identifier and the OP-local identifier differ,
|
658
|
+
# i.e. when the claimed_id uses delegation.
|
659
|
+
#
|
660
|
+
# If +identity+ is provided but this is not,
|
661
|
+
# +claimed_id+ will default to the value of +identity+.
|
662
|
+
# When answering requests that did not ask for identifier
|
663
|
+
# selection, the response +claimed_id+ will default to
|
664
|
+
# that of the request.
|
665
|
+
#
|
666
|
+
# This parameter is new in OpenID 2.0.
|
667
|
+
#
|
668
|
+
# Returns an OpenIDResponse object containing a OpenID id_res message.
|
669
|
+
#
|
670
|
+
# Raises NoReturnToError if the return_to is missing.
|
671
|
+
#
|
672
|
+
# Version 2.0 deprecates +server_url+ and adds +claimed_id+.
|
673
|
+
def answer(allow, server_url = nil, identity = nil, claimed_id = nil)
|
674
|
+
raise NoReturnToError unless @return_to
|
675
|
+
|
676
|
+
unless server_url
|
677
|
+
if @message.is_openid2 and !@op_endpoint
|
678
|
+
# In other words, that warning I raised in
|
679
|
+
# Server.__init__? You should pay attention to it now.
|
680
|
+
raise "#{self} should be constructed with " \
|
681
|
+
"op_endpoint to respond to OpenID 2.0 " \
|
682
|
+
"messages."
|
683
|
+
end
|
684
|
+
|
685
|
+
server_url = @op_endpoint
|
686
|
+
end
|
687
|
+
|
688
|
+
mode = if allow
|
689
|
+
"id_res"
|
690
|
+
elsif @message.is_openid1
|
691
|
+
if @immediate
|
692
|
+
"id_res"
|
693
|
+
else
|
694
|
+
"cancel"
|
695
|
+
end
|
696
|
+
elsif @immediate
|
697
|
+
"setup_needed"
|
698
|
+
else
|
699
|
+
"cancel"
|
700
|
+
end
|
701
|
+
|
702
|
+
response = OpenIDResponse.new(self)
|
703
|
+
|
704
|
+
if claimed_id and @message.is_openid1
|
705
|
+
raise VersionError, "claimed_id is new in OpenID 2.0 and not " \
|
706
|
+
"available for #{@message.get_openid_namespace}"
|
707
|
+
end
|
708
|
+
|
709
|
+
claimed_id = identity if identity and !claimed_id
|
710
|
+
|
711
|
+
if allow
|
712
|
+
if @identity == IDENTIFIER_SELECT
|
713
|
+
unless identity
|
714
|
+
raise ArgumentError, "This request uses IdP-driven " \
|
715
|
+
"identifier selection.You must supply " \
|
716
|
+
"an identifier in the response."
|
717
|
+
end
|
718
|
+
|
719
|
+
response_identity = identity
|
720
|
+
response_claimed_id = claimed_id
|
721
|
+
|
722
|
+
elsif @identity
|
723
|
+
if identity and (@identity != identity)
|
724
|
+
raise ArgumentError, "Request was for identity #{@identity}, " \
|
725
|
+
"cannot reply with identity #{identity}"
|
726
|
+
end
|
727
|
+
|
728
|
+
response_identity = @identity
|
729
|
+
response_claimed_id = @claimed_id
|
730
|
+
else
|
731
|
+
if identity
|
732
|
+
raise ArgumentError, "This request specified no identity " \
|
733
|
+
"and you supplied #{identity}"
|
734
|
+
end
|
735
|
+
response_identity = nil
|
736
|
+
end
|
737
|
+
|
738
|
+
if @message.is_openid1 and !response_identity
|
739
|
+
raise ArgumentError, "Request was an OpenID 1 request, so " \
|
740
|
+
"response must include an identifier."
|
741
|
+
end
|
742
|
+
|
743
|
+
response.fields.update_args(OPENID_NS, {
|
744
|
+
"mode" => mode,
|
745
|
+
"op_endpoint" => server_url,
|
746
|
+
"return_to" => @return_to,
|
747
|
+
"response_nonce" => Nonce.mk_nonce,
|
748
|
+
})
|
749
|
+
|
750
|
+
if response_identity
|
751
|
+
response.fields.set_arg(OPENID_NS, "identity", response_identity)
|
752
|
+
if @message.is_openid2
|
753
|
+
response.fields.set_arg(
|
754
|
+
OPENID_NS,
|
755
|
+
"claimed_id",
|
756
|
+
response_claimed_id,
|
757
|
+
)
|
758
|
+
end
|
759
|
+
end
|
760
|
+
else
|
761
|
+
response.fields.set_arg(OPENID_NS, "mode", mode)
|
762
|
+
if @immediate
|
763
|
+
if @message.is_openid1 and !server_url
|
764
|
+
raise ArgumentError, "setup_url is required for allow=false " \
|
765
|
+
"in OpenID 1.x immediate mode."
|
766
|
+
end
|
767
|
+
|
768
|
+
# Make a new request just like me, but with
|
769
|
+
# immediate=false.
|
770
|
+
setup_request = self.class.new(
|
771
|
+
@identity,
|
772
|
+
@return_to,
|
773
|
+
@op_endpoint,
|
774
|
+
@trust_root,
|
775
|
+
false,
|
776
|
+
@assoc_handle,
|
777
|
+
@claimed_id,
|
778
|
+
)
|
779
|
+
setup_request.message = Message.new(@message.get_openid_namespace)
|
780
|
+
setup_url = setup_request.encode_to_url(server_url)
|
781
|
+
response.fields.set_arg(OPENID_NS, "user_setup_url", setup_url)
|
782
|
+
end
|
783
|
+
end
|
784
|
+
|
785
|
+
response
|
786
|
+
end
|
787
|
+
|
788
|
+
def encode_to_url(server_url)
|
789
|
+
# Encode this request as a URL to GET.
|
790
|
+
#
|
791
|
+
# server_url:: The URL of the OpenID server to make this
|
792
|
+
# request of.
|
793
|
+
raise NoReturnToError unless @return_to
|
794
|
+
|
795
|
+
# Imported from the alternate reality where these classes are
|
796
|
+
# used in both the client and server code, so Requests are
|
797
|
+
# Encodable too. That's right, code imported from alternate
|
798
|
+
# realities all for the love of you, id_res/user_setup_url.
|
799
|
+
q = {
|
800
|
+
"mode" => @mode,
|
801
|
+
"identity" => @identity,
|
802
|
+
"claimed_id" => @claimed_id,
|
803
|
+
"return_to" => @return_to,
|
804
|
+
}
|
805
|
+
|
806
|
+
if @trust_root
|
807
|
+
if @message.is_openid1
|
808
|
+
q["trust_root"] = @trust_root
|
809
|
+
else
|
810
|
+
q["realm"] = @trust_root
|
811
|
+
end
|
812
|
+
end
|
813
|
+
|
814
|
+
q["assoc_handle"] = @assoc_handle if @assoc_handle
|
815
|
+
|
816
|
+
response = Message.new(@message.get_openid_namespace)
|
817
|
+
response.update_args(@message.get_openid_namespace, q)
|
818
|
+
response.to_url(server_url)
|
819
|
+
end
|
820
|
+
|
821
|
+
def cancel_url
|
822
|
+
# Get the URL to cancel this request.
|
823
|
+
#
|
824
|
+
# Useful for creating a "Cancel" button on a web form so that
|
825
|
+
# operation can be carried out directly without another trip
|
826
|
+
# through the server.
|
827
|
+
#
|
828
|
+
# (Except you may want to make another trip through the
|
829
|
+
# server so that it knows that the user did make a decision.)
|
830
|
+
#
|
831
|
+
# Returns a URL as a string.
|
832
|
+
raise NoReturnToError unless @return_to
|
833
|
+
|
834
|
+
if @immediate
|
835
|
+
raise ArgumentError.new("Cancel is not an appropriate response to " +
|
836
|
+
"immediate mode requests.")
|
837
|
+
end
|
838
|
+
|
839
|
+
response = Message.new(@message.get_openid_namespace)
|
840
|
+
response.set_arg(OPENID_NS, "mode", "cancel")
|
841
|
+
response.to_url(@return_to)
|
842
|
+
end
|
843
|
+
|
844
|
+
def to_s
|
845
|
+
format(
|
846
|
+
"<%s id:%s im:%s tr:%s ah:%s>",
|
847
|
+
self.class,
|
848
|
+
@identity,
|
849
|
+
@immediate,
|
850
|
+
@trust_root,
|
851
|
+
@assoc_handle,
|
852
|
+
)
|
853
|
+
end
|
854
|
+
end
|
855
|
+
|
856
|
+
# I am a response to an OpenID request.
|
857
|
+
#
|
858
|
+
# Attributes:
|
859
|
+
# signed:: A list of the names of the fields which should be signed.
|
860
|
+
#
|
861
|
+
# Implementer's note: In a more symmetric client/server
|
862
|
+
# implementation, there would be more types of #OpenIDResponse
|
863
|
+
# object and they would have validated attributes according to
|
864
|
+
# the type of response. But as it is, Response objects in a
|
865
|
+
# server are basically write-only, their only job is to go out
|
866
|
+
# over the wire, so this is just a loose wrapper around
|
867
|
+
# #OpenIDResponse.fields.
|
868
|
+
class OpenIDResponse
|
869
|
+
# The #OpenIDRequest I respond to.
|
870
|
+
attr_accessor :request
|
871
|
+
|
872
|
+
# An #OpenID::Message with the data to be returned.
|
873
|
+
# Keys are parameter names with no
|
874
|
+
# leading openid. e.g. identity and mac_key
|
875
|
+
# never openid.identity.
|
876
|
+
attr_accessor :fields
|
877
|
+
|
878
|
+
def initialize(request)
|
879
|
+
# Make a response to an OpenIDRequest.
|
880
|
+
@request = request
|
881
|
+
@fields = Message.new(request.namespace)
|
882
|
+
end
|
883
|
+
|
884
|
+
def to_s
|
885
|
+
format(
|
886
|
+
"%s for %s: %s",
|
887
|
+
self.class,
|
888
|
+
@request.class,
|
889
|
+
@fields,
|
890
|
+
)
|
891
|
+
end
|
892
|
+
|
893
|
+
# form_tag_attrs is a hash of attributes to be added to the form
|
894
|
+
# tag. 'accept-charset' and 'enctype' have defaults that can be
|
895
|
+
# overridden. If a value is supplied for 'action' or 'method',
|
896
|
+
# it will be replaced.
|
897
|
+
# Returns the form markup for this response.
|
898
|
+
def to_form_markup(form_tag_attrs = nil)
|
899
|
+
@fields.to_form_markup(@request.return_to, form_tag_attrs)
|
900
|
+
end
|
901
|
+
|
902
|
+
# Wraps the form tag from to_form_markup in a complete HTML document
|
903
|
+
# that uses javascript to autosubmit the form.
|
904
|
+
def to_html(form_tag_attrs = nil)
|
905
|
+
Util.auto_submit_html(to_form_markup(form_tag_attrs))
|
906
|
+
end
|
907
|
+
|
908
|
+
def render_as_form
|
909
|
+
# Returns true if this response's encoding is
|
910
|
+
# ENCODE_HTML_FORM. Convenience method for server authors.
|
911
|
+
which_encoding == ENCODE_HTML_FORM
|
912
|
+
end
|
913
|
+
|
914
|
+
def needs_signing
|
915
|
+
# Does this response require signing?
|
916
|
+
@fields.get_arg(OPENID_NS, "mode") == "id_res"
|
917
|
+
end
|
918
|
+
|
919
|
+
# implements IEncodable
|
920
|
+
|
921
|
+
def which_encoding
|
922
|
+
# How should I be encoded?
|
923
|
+
# returns one of ENCODE_URL or ENCODE_KVFORM.
|
924
|
+
return ENCODE_KVFORM unless BROWSER_REQUEST_MODES.member?(@request.mode)
|
925
|
+
|
926
|
+
if @fields.is_openid2 and
|
927
|
+
encode_to_url.length > OPENID1_URL_LIMIT
|
928
|
+
ENCODE_HTML_FORM
|
929
|
+
else
|
930
|
+
ENCODE_URL
|
931
|
+
end
|
932
|
+
end
|
933
|
+
|
934
|
+
def encode_to_url
|
935
|
+
# Encode a response as a URL for the user agent to GET.
|
936
|
+
# You will generally use this URL with a HTTP redirect.
|
937
|
+
@fields.to_url(@request.return_to)
|
938
|
+
end
|
939
|
+
|
940
|
+
def add_extension(extension_response)
|
941
|
+
# Add an extension response to this response message.
|
942
|
+
#
|
943
|
+
# extension_response:: An object that implements the
|
944
|
+
# #OpenID::Extension interface for adding arguments to an OpenID
|
945
|
+
# message.
|
946
|
+
extension_response.to_message(@fields)
|
947
|
+
end
|
948
|
+
|
949
|
+
def encode_to_kvform
|
950
|
+
# Encode a response in key-value colon/newline format.
|
951
|
+
#
|
952
|
+
# This is a machine-readable format used to respond to
|
953
|
+
# messages which came directly from the consumer and not
|
954
|
+
# through the user agent.
|
955
|
+
#
|
956
|
+
# see: OpenID Specs,
|
957
|
+
# <a href="http://openid.net/specs.bml#keyvalue">Key-Value Colon/Newline format</a>
|
958
|
+
@fields.to_kvform
|
959
|
+
end
|
960
|
+
|
961
|
+
def copy
|
962
|
+
Marshal.load(Marshal.dump(self))
|
963
|
+
end
|
964
|
+
end
|
965
|
+
|
966
|
+
# I am a response to an OpenID request in terms a web server
|
967
|
+
# understands.
|
968
|
+
#
|
969
|
+
# I generally come from an #Encoder, either directly or from
|
970
|
+
# #Server.encodeResponse.
|
971
|
+
class WebResponse
|
972
|
+
# The HTTP code of this response as an integer.
|
973
|
+
attr_accessor :code
|
974
|
+
|
975
|
+
# #Hash of headers to include in this response.
|
976
|
+
attr_accessor :headers
|
977
|
+
|
978
|
+
# The body of this response.
|
979
|
+
attr_accessor :body
|
980
|
+
|
981
|
+
def initialize(code = HTTP_OK, headers = nil, body = "")
|
982
|
+
# Construct me.
|
983
|
+
#
|
984
|
+
# These parameters are assigned directly as class attributes,
|
985
|
+
# see my class documentation for their
|
986
|
+
# descriptions.
|
987
|
+
@code = code
|
988
|
+
@headers = headers || {}
|
989
|
+
@body = body
|
990
|
+
end
|
991
|
+
end
|
992
|
+
|
993
|
+
# I sign things.
|
994
|
+
#
|
995
|
+
# I also check signatures.
|
996
|
+
#
|
997
|
+
# All my state is encapsulated in a store, which means I'm not
|
998
|
+
# generally pickleable but I am easy to reconstruct.
|
999
|
+
class Signatory
|
1000
|
+
# The number of seconds a secret remains valid. Defaults to 14 days.
|
1001
|
+
attr_accessor :secret_lifetime
|
1002
|
+
|
1003
|
+
# keys have a bogus server URL in them because the filestore
|
1004
|
+
# really does expect that key to be a URL. This seems a little
|
1005
|
+
# silly for the server store, since I expect there to be only
|
1006
|
+
# one server URL.
|
1007
|
+
@@_normal_key = "http://localhost/|normal"
|
1008
|
+
@@_dumb_key = "http://localhost/|dumb"
|
1009
|
+
|
1010
|
+
def self._normal_key
|
1011
|
+
@@_normal_key
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
def self._dumb_key
|
1015
|
+
@@_dumb_key
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
attr_accessor :store
|
1019
|
+
|
1020
|
+
# Create a new Signatory. store is The back-end where my
|
1021
|
+
# associations are stored.
|
1022
|
+
def initialize(store)
|
1023
|
+
Util.truthy_assert(store)
|
1024
|
+
@store = store
|
1025
|
+
@secret_lifetime = 14 * 24 * 60 * 60
|
1026
|
+
end
|
1027
|
+
|
1028
|
+
# Verify that the signature for some data is valid.
|
1029
|
+
def verify(assoc_handle, message)
|
1030
|
+
assoc = get_association(assoc_handle, true)
|
1031
|
+
unless assoc
|
1032
|
+
Util.log(format(
|
1033
|
+
"failed to get assoc with handle %s to verify " +
|
1034
|
+
"message %s",
|
1035
|
+
assoc_handle,
|
1036
|
+
message,
|
1037
|
+
))
|
1038
|
+
return false
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
begin
|
1042
|
+
valid = assoc.check_message_signature(message)
|
1043
|
+
rescue StandardError => e
|
1044
|
+
Util.log(format(
|
1045
|
+
"Error in verifying %s with %s: %s",
|
1046
|
+
message,
|
1047
|
+
assoc,
|
1048
|
+
e,
|
1049
|
+
))
|
1050
|
+
return false
|
1051
|
+
end
|
1052
|
+
|
1053
|
+
valid
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
# Sign a response.
|
1057
|
+
#
|
1058
|
+
# I take an OpenIDResponse, create a signature for everything in
|
1059
|
+
# its signed list, and return a new copy of the response object
|
1060
|
+
# with that signature included.
|
1061
|
+
def sign(response)
|
1062
|
+
signed_response = response.copy
|
1063
|
+
assoc_handle = response.request.assoc_handle
|
1064
|
+
if assoc_handle
|
1065
|
+
# normal mode disabling expiration check because even if the
|
1066
|
+
# association is expired, we still need to know some
|
1067
|
+
# properties of the association so that we may preserve
|
1068
|
+
# those properties when creating the fallback association.
|
1069
|
+
assoc = get_association(assoc_handle, false, false)
|
1070
|
+
|
1071
|
+
if !assoc or assoc.expires_in <= 0
|
1072
|
+
# fall back to dumb mode
|
1073
|
+
signed_response.fields.set_arg(
|
1074
|
+
OPENID_NS, "invalidate_handle", assoc_handle
|
1075
|
+
)
|
1076
|
+
assoc_type = assoc ? assoc.assoc_type : "HMAC-SHA1"
|
1077
|
+
if assoc and assoc.expires_in <= 0
|
1078
|
+
# now do the clean-up that the disabled checkExpiration
|
1079
|
+
# code didn't get to do.
|
1080
|
+
invalidate(assoc_handle, false)
|
1081
|
+
end
|
1082
|
+
assoc = create_association(true, assoc_type)
|
1083
|
+
end
|
1084
|
+
else
|
1085
|
+
# dumb mode.
|
1086
|
+
assoc = create_association(true)
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
begin
|
1090
|
+
signed_response.fields = assoc.sign_message(signed_response.fields)
|
1091
|
+
rescue KVFormError => e
|
1092
|
+
raise EncodingError, e
|
1093
|
+
end
|
1094
|
+
signed_response
|
1095
|
+
end
|
1096
|
+
|
1097
|
+
# Make a new association.
|
1098
|
+
def create_association(dumb = true, assoc_type = "HMAC-SHA1")
|
1099
|
+
secret = CryptUtil.random_string(OpenID.get_secret_size(assoc_type))
|
1100
|
+
uniq = Util.to_base64(CryptUtil.random_string(4))
|
1101
|
+
handle = format("{%s}{%x}{%s}", assoc_type, Time.now.to_i, uniq)
|
1102
|
+
|
1103
|
+
assoc = Association.from_expires_in(
|
1104
|
+
secret_lifetime, handle, secret, assoc_type
|
1105
|
+
)
|
1106
|
+
|
1107
|
+
key = if dumb
|
1108
|
+
@@_dumb_key
|
1109
|
+
else
|
1110
|
+
@@_normal_key
|
1111
|
+
end
|
1112
|
+
|
1113
|
+
@store.store_association(key, assoc)
|
1114
|
+
assoc
|
1115
|
+
end
|
1116
|
+
|
1117
|
+
# Get the association with the specified handle.
|
1118
|
+
def get_association(assoc_handle, dumb, check_expiration = true)
|
1119
|
+
# Hmm. We've created an interface that deals almost entirely
|
1120
|
+
# with assoc_handles. The only place outside the Signatory
|
1121
|
+
# that uses this (and thus the only place that ever sees
|
1122
|
+
# Association objects) is when creating a response to an
|
1123
|
+
# association request, as it must have the association's
|
1124
|
+
# secret.
|
1125
|
+
|
1126
|
+
raise ArgumentError.new("assoc_handle must not be None") unless assoc_handle
|
1127
|
+
|
1128
|
+
key = if dumb
|
1129
|
+
@@_dumb_key
|
1130
|
+
else
|
1131
|
+
@@_normal_key
|
1132
|
+
end
|
1133
|
+
|
1134
|
+
assoc = @store.get_association(key, assoc_handle)
|
1135
|
+
if assoc and assoc.expires_in <= 0
|
1136
|
+
Util.log(format(
|
1137
|
+
"requested %sdumb key %s is expired (by %s seconds)",
|
1138
|
+
(!dumb) ? "not-" : "",
|
1139
|
+
assoc_handle,
|
1140
|
+
assoc.expires_in,
|
1141
|
+
))
|
1142
|
+
if check_expiration
|
1143
|
+
@store.remove_association(key, assoc_handle)
|
1144
|
+
assoc = nil
|
1145
|
+
end
|
1146
|
+
end
|
1147
|
+
|
1148
|
+
assoc
|
1149
|
+
end
|
1150
|
+
|
1151
|
+
# Invalidates the association with the given handle.
|
1152
|
+
def invalidate(assoc_handle, dumb)
|
1153
|
+
key = if dumb
|
1154
|
+
@@_dumb_key
|
1155
|
+
else
|
1156
|
+
@@_normal_key
|
1157
|
+
end
|
1158
|
+
|
1159
|
+
@store.remove_association(key, assoc_handle)
|
1160
|
+
end
|
1161
|
+
end
|
1162
|
+
|
1163
|
+
# I encode responses in to WebResponses.
|
1164
|
+
#
|
1165
|
+
# If you don't like WebResponses, you can do
|
1166
|
+
# your own handling of OpenIDResponses with
|
1167
|
+
# OpenIDResponse.whichEncoding,
|
1168
|
+
# OpenIDResponse.encodeToURL, and
|
1169
|
+
# OpenIDResponse.encodeToKVForm.
|
1170
|
+
class Encoder
|
1171
|
+
@@responseFactory = WebResponse
|
1172
|
+
|
1173
|
+
# Encode a response to a WebResponse.
|
1174
|
+
#
|
1175
|
+
# Raises EncodingError when I can't figure out how to encode
|
1176
|
+
# this message.
|
1177
|
+
def encode(response)
|
1178
|
+
encode_as = response.which_encoding
|
1179
|
+
if encode_as == ENCODE_KVFORM
|
1180
|
+
wr = @@responseFactory.new(
|
1181
|
+
HTTP_OK,
|
1182
|
+
nil,
|
1183
|
+
response.encode_to_kvform,
|
1184
|
+
)
|
1185
|
+
wr.code = HTTP_ERROR if response.is_a?(Exception)
|
1186
|
+
elsif encode_as == ENCODE_URL
|
1187
|
+
location = response.encode_to_url
|
1188
|
+
wr = @@responseFactory.new(
|
1189
|
+
HTTP_REDIRECT,
|
1190
|
+
{"location" => location},
|
1191
|
+
)
|
1192
|
+
elsif encode_as == ENCODE_HTML_FORM
|
1193
|
+
wr = @@responseFactory.new(
|
1194
|
+
HTTP_OK,
|
1195
|
+
nil,
|
1196
|
+
response.to_form_markup,
|
1197
|
+
)
|
1198
|
+
else
|
1199
|
+
# Can't encode this to a protocol message. You should
|
1200
|
+
# probably render it to HTML and show it to the user.
|
1201
|
+
raise EncodingError.new(response)
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
wr
|
1205
|
+
end
|
1206
|
+
end
|
1207
|
+
|
1208
|
+
# I encode responses in to WebResponses, signing
|
1209
|
+
# them when required.
|
1210
|
+
class SigningEncoder < Encoder
|
1211
|
+
attr_accessor :signatory
|
1212
|
+
|
1213
|
+
# Create a SigningEncoder given a Signatory
|
1214
|
+
def initialize(signatory)
|
1215
|
+
@signatory = signatory
|
1216
|
+
end
|
1217
|
+
|
1218
|
+
# Encode a response to a WebResponse, signing it first if
|
1219
|
+
# appropriate.
|
1220
|
+
#
|
1221
|
+
# Raises EncodingError when I can't figure out how to encode this
|
1222
|
+
# message.
|
1223
|
+
#
|
1224
|
+
# Raises AlreadySigned when this response is already signed.
|
1225
|
+
def encode(response)
|
1226
|
+
# the is_a? is a bit of a kludge... it means there isn't
|
1227
|
+
# really an adapter to make the interfaces quite match.
|
1228
|
+
if !response.is_a?(Exception) and response.needs_signing
|
1229
|
+
unless @signatory
|
1230
|
+
raise ArgumentError.new(
|
1231
|
+
format(
|
1232
|
+
"Must have a store to sign this request: %s",
|
1233
|
+
response,
|
1234
|
+
),
|
1235
|
+
response,
|
1236
|
+
)
|
1237
|
+
end
|
1238
|
+
|
1239
|
+
raise AlreadySigned.new(response) if response.fields.has_key?(OPENID_NS, "sig")
|
1240
|
+
|
1241
|
+
response = @signatory.sign(response)
|
1242
|
+
end
|
1243
|
+
|
1244
|
+
super
|
1245
|
+
end
|
1246
|
+
end
|
1247
|
+
|
1248
|
+
# I decode an incoming web request in to a OpenIDRequest.
|
1249
|
+
class Decoder
|
1250
|
+
@@handlers = {
|
1251
|
+
"checkid_setup" => CheckIDRequest.method(:from_message),
|
1252
|
+
"checkid_immediate" => CheckIDRequest.method(:from_message),
|
1253
|
+
"check_authentication" => CheckAuthRequest.method(:from_message),
|
1254
|
+
"associate" => AssociateRequest.method(:from_message),
|
1255
|
+
}
|
1256
|
+
|
1257
|
+
attr_accessor :server
|
1258
|
+
|
1259
|
+
# Construct a Decoder. The server is necessary because some
|
1260
|
+
# replies reference their server.
|
1261
|
+
def initialize(server)
|
1262
|
+
@server = server
|
1263
|
+
end
|
1264
|
+
|
1265
|
+
# I transform query parameters into an OpenIDRequest.
|
1266
|
+
#
|
1267
|
+
# If the query does not seem to be an OpenID request at all, I
|
1268
|
+
# return nil.
|
1269
|
+
#
|
1270
|
+
# Raises ProtocolError when the query does not seem to be a valid
|
1271
|
+
# OpenID request.
|
1272
|
+
def decode(query)
|
1273
|
+
return if query.nil? or query.empty?
|
1274
|
+
|
1275
|
+
begin
|
1276
|
+
message = Message.from_post_args(query)
|
1277
|
+
rescue InvalidOpenIDNamespace => e
|
1278
|
+
query = query.dup
|
1279
|
+
query["openid.ns"] = OPENID2_NS
|
1280
|
+
message = Message.from_post_args(query)
|
1281
|
+
raise ProtocolError.new(message, e.to_s)
|
1282
|
+
end
|
1283
|
+
|
1284
|
+
mode = message.get_arg(OPENID_NS, "mode")
|
1285
|
+
unless mode
|
1286
|
+
msg = format("No mode value in message %s", message)
|
1287
|
+
raise ProtocolError.new(message, msg)
|
1288
|
+
end
|
1289
|
+
|
1290
|
+
handler = @@handlers.fetch(mode, method(:default_decoder))
|
1291
|
+
handler.call(message, @server.op_endpoint)
|
1292
|
+
end
|
1293
|
+
|
1294
|
+
# Called to decode queries when no handler for that mode is
|
1295
|
+
# found.
|
1296
|
+
#
|
1297
|
+
# This implementation always raises ProtocolError.
|
1298
|
+
def default_decoder(message, _server)
|
1299
|
+
mode = message.get_arg(OPENID_NS, "mode")
|
1300
|
+
msg = format("Unrecognized OpenID mode %s", mode)
|
1301
|
+
raise ProtocolError.new(message, msg)
|
1302
|
+
end
|
1303
|
+
end
|
1304
|
+
|
1305
|
+
# I handle requests for an OpenID server.
|
1306
|
+
#
|
1307
|
+
# Some types of requests (those which are not checkid requests)
|
1308
|
+
# may be handed to my handleRequest method, and I will take care
|
1309
|
+
# of it and return a response.
|
1310
|
+
#
|
1311
|
+
# For your convenience, I also provide an interface to
|
1312
|
+
# Decoder.decode and SigningEncoder.encode through my methods
|
1313
|
+
# decodeRequest and encodeResponse.
|
1314
|
+
#
|
1315
|
+
# All my state is encapsulated in an store, which means I'm not
|
1316
|
+
# generally pickleable but I am easy to reconstruct.
|
1317
|
+
class Server
|
1318
|
+
@@signatoryClass = Signatory
|
1319
|
+
@@encoderClass = SigningEncoder
|
1320
|
+
@@decoderClass = Decoder
|
1321
|
+
|
1322
|
+
# The back-end where my associations and nonces are stored.
|
1323
|
+
attr_accessor :store
|
1324
|
+
|
1325
|
+
# I'm using this for associate requests and to sign things.
|
1326
|
+
attr_accessor :signatory
|
1327
|
+
|
1328
|
+
# I'm using this to encode things.
|
1329
|
+
attr_accessor :encoder
|
1330
|
+
|
1331
|
+
# I'm using this to decode things.
|
1332
|
+
attr_accessor :decoder
|
1333
|
+
|
1334
|
+
# I use this instance of OpenID::AssociationNegotiator to
|
1335
|
+
# determine which kinds of associations I can make and how.
|
1336
|
+
attr_accessor :negotiator
|
1337
|
+
|
1338
|
+
# My URL.
|
1339
|
+
attr_accessor :op_endpoint
|
1340
|
+
|
1341
|
+
# op_endpoint is new in library version 2.0.
|
1342
|
+
def initialize(store, op_endpoint)
|
1343
|
+
@store = store
|
1344
|
+
@signatory = @@signatoryClass.new(@store)
|
1345
|
+
@encoder = @@encoderClass.new(@signatory)
|
1346
|
+
@decoder = @@decoderClass.new(self)
|
1347
|
+
@negotiator = DefaultNegotiator.copy
|
1348
|
+
@op_endpoint = op_endpoint
|
1349
|
+
end
|
1350
|
+
|
1351
|
+
# Handle a request.
|
1352
|
+
#
|
1353
|
+
# Give me a request, I will give you a response. Unless it's a
|
1354
|
+
# type of request I cannot handle myself, in which case I will
|
1355
|
+
# raise RuntimeError. In that case, you can handle it yourself,
|
1356
|
+
# or add a method to me for handling that request type.
|
1357
|
+
def handle_request(request)
|
1358
|
+
begin
|
1359
|
+
handler = method("openid_" + request.mode)
|
1360
|
+
rescue NameError
|
1361
|
+
raise format(
|
1362
|
+
"%s has no handler for a request of mode %s.",
|
1363
|
+
self,
|
1364
|
+
request.mode,
|
1365
|
+
).to_s
|
1366
|
+
end
|
1367
|
+
|
1368
|
+
handler.call(request)
|
1369
|
+
end
|
1370
|
+
|
1371
|
+
# Handle and respond to check_authentication requests.
|
1372
|
+
def openid_check_authentication(request)
|
1373
|
+
request.answer(@signatory)
|
1374
|
+
end
|
1375
|
+
|
1376
|
+
# Handle and respond to associate requests.
|
1377
|
+
def openid_associate(request)
|
1378
|
+
assoc_type = request.assoc_type
|
1379
|
+
session_type = request.session.session_type
|
1380
|
+
if @negotiator.allowed?(assoc_type, session_type)
|
1381
|
+
assoc = @signatory.create_association(
|
1382
|
+
false,
|
1383
|
+
assoc_type,
|
1384
|
+
)
|
1385
|
+
request.answer(assoc)
|
1386
|
+
else
|
1387
|
+
message = format(
|
1388
|
+
"Association type %s is not supported with " +
|
1389
|
+
"session type %s",
|
1390
|
+
assoc_type,
|
1391
|
+
session_type,
|
1392
|
+
)
|
1393
|
+
preferred_assoc_type, preferred_session_type = @negotiator.get_allowed_type
|
1394
|
+
request.answer_unsupported(
|
1395
|
+
message,
|
1396
|
+
preferred_assoc_type,
|
1397
|
+
preferred_session_type,
|
1398
|
+
)
|
1399
|
+
end
|
1400
|
+
end
|
1401
|
+
|
1402
|
+
# Transform query parameters into an OpenIDRequest.
|
1403
|
+
# query should contain the query parameters as a Hash with
|
1404
|
+
# each key mapping to one value.
|
1405
|
+
#
|
1406
|
+
# If the query does not seem to be an OpenID request at all, I
|
1407
|
+
# return nil.
|
1408
|
+
def decode_request(query)
|
1409
|
+
@decoder.decode(query)
|
1410
|
+
end
|
1411
|
+
|
1412
|
+
# Encode a response to a WebResponse, signing it first if
|
1413
|
+
# appropriate.
|
1414
|
+
#
|
1415
|
+
# Raises EncodingError when I can't figure out how to encode this
|
1416
|
+
# message.
|
1417
|
+
#
|
1418
|
+
# Raises AlreadySigned When this response is already signed.
|
1419
|
+
def encode_response(response)
|
1420
|
+
@encoder.encode(response)
|
1421
|
+
end
|
1422
|
+
end
|
1423
|
+
|
1424
|
+
# A message did not conform to the OpenID protocol.
|
1425
|
+
class ProtocolError < Exception
|
1426
|
+
# The query that is failing to be a valid OpenID request.
|
1427
|
+
attr_accessor :openid_message
|
1428
|
+
attr_accessor :reference, :contact
|
1429
|
+
|
1430
|
+
# text:: A message about the encountered error.
|
1431
|
+
def initialize(message, text = nil, reference = nil, contact = nil)
|
1432
|
+
@openid_message = message
|
1433
|
+
@reference = reference
|
1434
|
+
@contact = contact
|
1435
|
+
Util.truthy_assert(!message.is_a?(String))
|
1436
|
+
super(text)
|
1437
|
+
end
|
1438
|
+
|
1439
|
+
# Get the return_to argument from the request, if any.
|
1440
|
+
def get_return_to
|
1441
|
+
return if @openid_message.nil?
|
1442
|
+
|
1443
|
+
@openid_message.get_arg(OPENID_NS, "return_to")
|
1444
|
+
end
|
1445
|
+
|
1446
|
+
# Did this request have a return_to parameter?
|
1447
|
+
def has_return_to
|
1448
|
+
!get_return_to.nil?
|
1449
|
+
end
|
1450
|
+
|
1451
|
+
# Generate a Message object for sending to the relying party,
|
1452
|
+
# after encoding.
|
1453
|
+
def to_message
|
1454
|
+
namespace = @openid_message.get_openid_namespace
|
1455
|
+
reply = Message.new(namespace)
|
1456
|
+
reply.set_arg(OPENID_NS, "mode", "error")
|
1457
|
+
reply.set_arg(OPENID_NS, "error", to_s)
|
1458
|
+
|
1459
|
+
reply.set_arg(OPENID_NS, "contact", @contact.to_s) if @contact
|
1460
|
+
|
1461
|
+
reply.set_arg(OPENID_NS, "reference", @reference.to_s) if @reference
|
1462
|
+
|
1463
|
+
reply
|
1464
|
+
end
|
1465
|
+
|
1466
|
+
# implements IEncodable
|
1467
|
+
|
1468
|
+
def encode_to_url
|
1469
|
+
to_message.to_url(get_return_to)
|
1470
|
+
end
|
1471
|
+
|
1472
|
+
def encode_to_kvform
|
1473
|
+
to_message.to_kvform
|
1474
|
+
end
|
1475
|
+
|
1476
|
+
def to_form_markup
|
1477
|
+
to_message.to_form_markup(get_return_to)
|
1478
|
+
end
|
1479
|
+
|
1480
|
+
def to_html
|
1481
|
+
Util.auto_submit_html(to_form_markup)
|
1482
|
+
end
|
1483
|
+
|
1484
|
+
# How should I be encoded?
|
1485
|
+
#
|
1486
|
+
# Returns one of ENCODE_URL, ENCODE_KVFORM, or None. If None,
|
1487
|
+
# I cannot be encoded as a protocol message and should be
|
1488
|
+
# displayed to the user.
|
1489
|
+
def which_encoding
|
1490
|
+
if has_return_to
|
1491
|
+
if @openid_message.is_openid2 and
|
1492
|
+
encode_to_url.length > OPENID1_URL_LIMIT
|
1493
|
+
return ENCODE_HTML_FORM
|
1494
|
+
else
|
1495
|
+
return ENCODE_URL
|
1496
|
+
end
|
1497
|
+
end
|
1498
|
+
|
1499
|
+
return if @openid_message.nil?
|
1500
|
+
|
1501
|
+
mode = @openid_message.get_arg(OPENID_NS, "mode")
|
1502
|
+
return ENCODE_KVFORM if mode && !BROWSER_REQUEST_MODES.member?(mode)
|
1503
|
+
|
1504
|
+
# If your request was so broken that you didn't manage to
|
1505
|
+
# include an openid.mode, I'm not going to worry too much
|
1506
|
+
# about returning you something you can't parse.
|
1507
|
+
nil
|
1508
|
+
end
|
1509
|
+
end
|
1510
|
+
|
1511
|
+
# Raised when an operation was attempted that is not compatible
|
1512
|
+
# with the protocol version being used.
|
1513
|
+
class VersionError < Exception
|
1514
|
+
end
|
1515
|
+
|
1516
|
+
# Raised when a response to a request cannot be generated
|
1517
|
+
# because the request contains no return_to URL.
|
1518
|
+
class NoReturnToError < Exception
|
1519
|
+
end
|
1520
|
+
|
1521
|
+
# Could not encode this as a protocol message.
|
1522
|
+
#
|
1523
|
+
# You should probably render it and show it to the user.
|
1524
|
+
class EncodingError < Exception
|
1525
|
+
# The response that failed to encode.
|
1526
|
+
attr_reader :response
|
1527
|
+
|
1528
|
+
def initialize(response)
|
1529
|
+
super
|
1530
|
+
@response = response
|
1531
|
+
end
|
1532
|
+
end
|
1533
|
+
|
1534
|
+
# This response is already signed.
|
1535
|
+
class AlreadySigned < EncodingError
|
1536
|
+
end
|
1537
|
+
|
1538
|
+
# A return_to is outside the trust_root.
|
1539
|
+
class UntrustedReturnURL < ProtocolError
|
1540
|
+
attr_reader :return_to, :trust_root
|
1541
|
+
|
1542
|
+
def initialize(message, return_to, trust_root)
|
1543
|
+
super(message)
|
1544
|
+
@return_to = return_to
|
1545
|
+
@trust_root = trust_root
|
1546
|
+
end
|
1547
|
+
|
1548
|
+
def to_s
|
1549
|
+
format(
|
1550
|
+
"return_to %s not under trust_root %s",
|
1551
|
+
@return_to,
|
1552
|
+
@trust_root,
|
1553
|
+
)
|
1554
|
+
end
|
1555
|
+
end
|
1556
|
+
|
1557
|
+
# The return_to URL doesn't look like a valid URL.
|
1558
|
+
class MalformedReturnURL < ProtocolError
|
1559
|
+
attr_reader :return_to
|
1560
|
+
|
1561
|
+
def initialize(openid_message, return_to)
|
1562
|
+
@return_to = return_to
|
1563
|
+
super(openid_message)
|
1564
|
+
end
|
1565
|
+
end
|
1566
|
+
|
1567
|
+
# The trust root is not well-formed.
|
1568
|
+
class MalformedTrustRoot < ProtocolError
|
1569
|
+
end
|
1570
|
+
end
|
1571
|
+
end
|