Updated README.rdoc again
[feedcatcher.git] / vendor / rails / actionpack / lib / action_controller / vendor / rack-1.0 / rack / auth / openid.rb
1 # AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
2
3 gem 'ruby-openid', '~> 2' if defined? Gem
4 require 'rack/request'
5 require 'rack/utils'
6 require 'rack/auth/abstract/handler'
7 require 'uri'
8 require 'openid' #gem
9 require 'openid/extension' #gem
10 require 'openid/store/memory' #gem
11
12 module Rack
13 class Request
14 def openid_request
15 @env['rack.auth.openid.request']
16 end
17
18 def openid_response
19 @env['rack.auth.openid.response']
20 end
21 end
22
23 module Auth
24
25 # Rack::Auth::OpenID provides a simple method for setting up an OpenID
26 # Consumer. It requires the ruby-openid library from janrain to operate,
27 # as well as a rack method of session management.
28 #
29 # The ruby-openid home page is at http://openidenabled.com/ruby-openid/.
30 #
31 # The OpenID specifications can be found at
32 # http://openid.net/specs/openid-authentication-1_1.html
33 # and
34 # http://openid.net/specs/openid-authentication-2_0.html. Documentation
35 # for published OpenID extensions and related topics can be found at
36 # http://openid.net/developers/specs/.
37 #
38 # It is recommended to read through the OpenID spec, as well as
39 # ruby-openid's documentation, to understand what exactly goes on. However
40 # a setup as simple as the presented examples is enough to provide
41 # Consumer functionality.
42 #
43 # This library strongly intends to utilize the OpenID 2.0 features of the
44 # ruby-openid library, which provides OpenID 1.0 compatiblity.
45 #
46 # NOTE: Due to the amount of data that this library stores in the
47 # session, Rack::Session::Cookie may fault.
48
49 class OpenID
50
51 class NoSession < RuntimeError; end
52 class BadExtension < RuntimeError; end
53 # Required for ruby-openid
54 ValidStatus = [:success, :setup_needed, :cancel, :failure]
55
56 # = Arguments
57 #
58 # The first argument is the realm, identifying the site they are trusting
59 # with their identity. This is required, also treated as the trust_root
60 # in OpenID 1.x exchanges.
61 #
62 # The optional second argument is a hash of options.
63 #
64 # == Options
65 #
66 # <tt>:return_to</tt> defines the url to return to after the client
67 # authenticates with the openid service provider. This url should point
68 # to where Rack::Auth::OpenID is mounted. If <tt>:return_to</tt> is not
69 # provided, return_to will be the current url which allows flexibility
70 # with caveats.
71 #
72 # <tt>:session_key</tt> defines the key to the session hash in the env.
73 # It defaults to 'rack.session'.
74 #
75 # <tt>:openid_param</tt> defines at what key in the request parameters to
76 # find the identifier to resolve. As per the 2.0 spec, the default is
77 # 'openid_identifier'.
78 #
79 # <tt>:store</tt> defined what OpenID Store to use for persistant
80 # information. By default a Store::Memory will be used.
81 #
82 # <tt>:immediate</tt> as true will make initial requests to be of an
83 # immediate type. This is false by default. See OpenID specification
84 # documentation.
85 #
86 # <tt>:extensions</tt> should be a hash of openid extension
87 # implementations. The key should be the extension main module, the value
88 # should be an array of arguments for extension::Request.new.
89 # The hash is iterated over and passed to #add_extension for processing.
90 # Please see #add_extension for further documentation.
91 #
92 # == Examples
93 #
94 # simple_oid = OpenID.new('http://mysite.com/')
95 #
96 # return_oid = OpenID.new('http://mysite.com/', {
97 # :return_to => 'http://mysite.com/openid'
98 # })
99 #
100 # complex_oid = OpenID.new('http://mysite.com/',
101 # :immediate => true,
102 # :extensions => {
103 # ::OpenID::SReg => [['email'],['nickname']]
104 # }
105 # )
106 #
107 # = Advanced
108 #
109 # Most of the functionality of this library is encapsulated such that
110 # expansion and overriding functions isn't difficult nor tricky.
111 # Alternately, to avoid opening up singleton objects or subclassing, a
112 # wrapper rack middleware can be composed to act upon Auth::OpenID's
113 # responses. See #check and #finish for locations of pertinent data.
114 #
115 # == Responses
116 #
117 # To change the responses that Auth::OpenID returns, override the methods
118 # #redirect, #bad_request, #unauthorized, #access_denied, and
119 # #foreign_server_failure.
120 #
121 # Additionally #confirm_post_params is used when the URI would exceed
122 # length limits on a GET request when doing the initial verification
123 # request.
124 #
125 # == Processing
126 #
127 # To change methods of processing completed transactions, override the
128 # methods #success, #setup_needed, #cancel, and #failure. Please ensure
129 # the returned object is a rack compatible response.
130 #
131 # The first argument is an OpenID::Response, the second is a
132 # Rack::Request of the current request, the last is the hash used in
133 # ruby-openid handling, which can be found manually at
134 # env['rack.session'][:openid].
135 #
136 # This is useful if you wanted to expand the processing done, such as
137 # setting up user accounts.
138 #
139 # oid_app = Rack::Auth::OpenID.new realm, :return_to => return_to
140 # def oid_app.success oid, request, session
141 # user = Models::User[oid.identity_url]
142 # user ||= Models::User.create_from_openid oid
143 # request['rack.session'][:user] = user.id
144 # redirect MyApp.site_home
145 # end
146 #
147 # site_map['/openid'] = oid_app
148 # map = Rack::URLMap.new site_map
149 # ...
150
151 def initialize(realm, options={})
152 realm = URI(realm)
153 raise ArgumentError, "Invalid realm: #{realm}" \
154 unless realm.absolute? \
155 and realm.fragment.nil? \
156 and realm.scheme =~ /^https?$/ \
157 and realm.host =~ /^(\*\.)?#{URI::REGEXP::PATTERN::URIC_NO_SLASH}+/
158 realm.path = '/' if realm.path.empty?
159 @realm = realm.to_s
160
161 if ruri = options[:return_to]
162 ruri = URI(ruri)
163 raise ArgumentError, "Invalid return_to: #{ruri}" \
164 unless ruri.absolute? \
165 and ruri.scheme =~ /^https?$/ \
166 and ruri.fragment.nil?
167 raise ArgumentError, "return_to #{ruri} not within realm #{realm}" \
168 unless self.within_realm?(ruri)
169 @return_to = ruri.to_s
170 end
171
172 @session_key = options[:session_key] || 'rack.session'
173 @openid_param = options[:openid_param] || 'openid_identifier'
174 @store = options[:store] || ::OpenID::Store::Memory.new
175 @immediate = !!options[:immediate]
176
177 @extensions = {}
178 if extensions = options.delete(:extensions)
179 extensions.each do |ext, args|
180 add_extension ext, *args
181 end
182 end
183
184 # Undocumented, semi-experimental
185 @anonymous = !!options[:anonymous]
186 end
187
188 attr_reader :realm, :return_to, :session_key, :openid_param, :store,
189 :immediate, :extensions
190
191 # Sets up and uses session data at <tt>:openid</tt> within the session.
192 # Errors in this setup will raise a NoSession exception.
193 #
194 # If the parameter 'openid.mode' is set, which implies a followup from
195 # the openid server, processing is passed to #finish and the result is
196 # returned. However, if there is no appropriate openid information in the
197 # session, a 400 error is returned.
198 #
199 # If the parameter specified by <tt>options[:openid_param]</tt> is
200 # present, processing is passed to #check and the result is returned.
201 #
202 # If neither of these conditions are met, #unauthorized is called.
203
204 def call(env)
205 env['rack.auth.openid'] = self
206 env_session = env[@session_key]
207 unless env_session and env_session.is_a?(Hash)
208 raise NoSession, 'No compatible session'
209 end
210 # let us work in our own namespace...
211 session = (env_session[:openid] ||= {})
212 unless session and session.is_a?(Hash)
213 raise NoSession, 'Incompatible openid session'
214 end
215
216 request = Rack::Request.new(env)
217 consumer = ::OpenID::Consumer.new(session, @store)
218
219 if mode = request.GET['openid.mode']
220 if session.key?(:openid_param)
221 finish(consumer, session, request)
222 else
223 bad_request
224 end
225 elsif request.GET[@openid_param]
226 check(consumer, session, request)
227 else
228 unauthorized
229 end
230 end
231
232 # As the first part of OpenID consumer action, #check retrieves the data
233 # required for completion.
234 #
235 # If all parameters fit within the max length of a URI, a 303 redirect
236 # will be returned. Otherwise #confirm_post_params will be called.
237 #
238 # Any messages from OpenID's request are logged to env['rack.errors']
239 #
240 # <tt>env['rack.auth.openid.request']</tt> is the openid checkid request
241 # instance.
242 #
243 # <tt>session[:openid_param]</tt> is set to the openid identifier
244 # provided by the user.
245 #
246 # <tt>session[:return_to]</tt> is set to the return_to uri given to the
247 # identity provider.
248
249 def check(consumer, session, req)
250 oid = consumer.begin(req.GET[@openid_param], @anonymous)
251 req.env['rack.auth.openid.request'] = oid
252 req.env['rack.errors'].puts(oid.message)
253 p oid if $DEBUG
254
255 ## Extension support
256 extensions.each do |ext,args|
257 oid.add_extension(ext::Request.new(*args))
258 end
259
260 session[:openid_param] = req.GET[openid_param]
261 return_to_uri = return_to ? return_to : req.url
262 session[:return_to] = return_to_uri
263 immediate = session.key?(:setup_needed) ? false : immediate
264
265 if oid.send_redirect?(realm, return_to_uri, immediate)
266 uri = oid.redirect_url(realm, return_to_uri, immediate)
267 redirect(uri)
268 else
269 confirm_post_params(oid, realm, return_to_uri, immediate)
270 end
271 rescue ::OpenID::DiscoveryFailure => e
272 # thrown from inside OpenID::Consumer#begin by yadis stuff
273 req.env['rack.errors'].puts([e.message, *e.backtrace]*"\n")
274 return foreign_server_failure
275 end
276
277 # This is the final portion of authentication.
278 # If successful, a redirect to the realm is be returned.
279 # Data gathered from extensions are stored in session[:openid] with the
280 # extension's namespace uri as the key.
281 #
282 # Any messages from OpenID's response are logged to env['rack.errors']
283 #
284 # <tt>env['rack.auth.openid.response']</tt> will contain the openid
285 # response.
286
287 def finish(consumer, session, req)
288 oid = consumer.complete(req.GET, req.url)
289 req.env['rack.auth.openid.response'] = oid
290 req.env['rack.errors'].puts(oid.message)
291 p oid if $DEBUG
292
293 raise unless ValidStatus.include?(oid.status)
294 __send__(oid.status, oid, req, session)
295 end
296
297 # The first argument should be the main extension module.
298 # The extension module should contain the constants:
299 # * class Request, should have OpenID::Extension as an ancestor
300 # * class Response, should have OpenID::Extension as an ancestor
301 # * string NS_URI, which defining the namespace of the extension
302 #
303 # All trailing arguments will be passed to extension::Request.new in
304 # #check.
305 # The openid response will be passed to
306 # extension::Response#from_success_response, #get_extension_args will be
307 # called on the result to attain the gathered data.
308 #
309 # This method returns the key at which the response data will be found in
310 # the session, which is the namespace uri by default.
311
312 def add_extension(ext, *args)
313 raise BadExtension unless valid_extension?(ext)
314 extensions[ext] = args
315 return ext::NS_URI
316 end
317
318 # Checks the validitity, in the context of usage, of a submitted
319 # extension.
320
321 def valid_extension?(ext)
322 if not %w[NS_URI Request Response].all?{|c| ext.const_defined?(c) }
323 raise ArgumentError, 'Extension is missing constants.'
324 elsif not ext::Response.respond_to?(:from_success_response)
325 raise ArgumentError, 'Response is missing required method.'
326 end
327 return true
328 rescue
329 return false
330 end
331
332 # Checks the provided uri to ensure it'd be considered within the realm.
333 # is currently not compatible with wildcard realms.
334
335 def within_realm? uri
336 uri = URI.parse(uri.to_s)
337 realm = URI.parse(self.realm)
338 return false unless uri.absolute?
339 return false unless uri.path[0, realm.path.size] == realm.path
340 return false unless uri.host == realm.host or realm.host[/^\*\./]
341 # for wildcard support, is awkward with URI limitations
342 realm_match = Regexp.escape(realm.host).
343 sub(/^\*\./,"^#{URI::REGEXP::PATTERN::URIC_NO_SLASH}+.")+'$'
344 return false unless uri.host.match(realm_match)
345 return true
346 end
347 alias_method :include?, :within_realm?
348
349 protected
350
351 ### These methods define some of the boilerplate responses.
352
353 # Returns an html form page for posting to an Identity Provider if the
354 # GET request would exceed the upper URI length limit.
355
356 def confirm_post_params(oid, realm, return_to, immediate)
357 Rack::Response.new.finish do |r|
358 r.write '<html><head><title>Confirm...</title></head><body>'
359 r.write oid.form_markup(realm, return_to, immediate)
360 r.write '</body></html>'
361 end
362 end
363
364 # Returns a 303 redirect with the destination of that provided by the
365 # argument.
366
367 def redirect(uri)
368 [ 303, {'Content-Length'=>'0', 'Content-Type'=>'text/plain',
369 'Location' => uri},
370 [] ]
371 end
372
373 # Returns an empty 400 response.
374
375 def bad_request
376 [ 400, {'Content-Type'=>'text/plain', 'Content-Length'=>'0'},
377 [''] ]
378 end
379
380 # Returns a basic unauthorized 401 response.
381
382 def unauthorized
383 [ 401, {'Content-Type' => 'text/plain', 'Content-Length' => '13'},
384 ['Unauthorized.'] ]
385 end
386
387 # Returns a basic access denied 403 response.
388
389 def access_denied
390 [ 403, {'Content-Type' => 'text/plain', 'Content-Length' => '14'},
391 ['Access denied.'] ]
392 end
393
394 # Returns a 503 response to be used if communication with the remote
395 # OpenID server fails.
396
397 def foreign_server_failure
398 [ 503, {'Content-Type'=>'text/plain', 'Content-Length' => '23'},
399 ['Foreign server failure.'] ]
400 end
401
402 private
403
404 ### These methods are called after a transaction is completed, depending
405 # on its outcome. These should all return a rack compatible response.
406 # You'd want to override these to provide additional functionality.
407
408 # Called to complete processing on a successful transaction.
409 # Within the openid session, :openid_identity and :openid_identifier are
410 # set to the user friendly and the standard representation of the
411 # validated identity. All other data in the openid session is cleared.
412
413 def success(oid, request, session)
414 session.clear
415 session[:openid_identity] = oid.display_identifier
416 session[:openid_identifier] = oid.identity_url
417 extensions.keys.each do |ext|
418 label = ext.name[/[^:]+$/].downcase
419 response = ext::Response.from_success_response(oid)
420 session[label] = response.data
421 end
422 redirect(realm)
423 end
424
425 # Called if the Identity Provider indicates further setup by the user is
426 # required.
427 # The identifier is retrived from the openid session at :openid_param.
428 # And :setup_needed is set to true to prevent looping.
429
430 def setup_needed(oid, request, session)
431 identifier = session[:openid_param]
432 session[:setup_needed] = true
433 redirect req.script_name + '?' + openid_param + '=' + identifier
434 end
435
436 # Called if the user indicates they wish to cancel identification.
437 # Data within openid session is cleared.
438
439 def cancel(oid, request, session)
440 session.clear
441 access_denied
442 end
443
444 # Called if the Identity Provider indicates the user is unable to confirm
445 # their identity. Data within the openid session is left alone, in case
446 # of swarm auth attacks.
447
448 def failure(oid, request, session)
449 unauthorized
450 end
451 end
452
453 # A class developed out of the request to use OpenID as an authentication
454 # middleware. The request will be sent to the OpenID instance unless the
455 # block evaluates to true. For example in rackup, you can use it as such:
456 #
457 # use Rack::Session::Pool
458 # use Rack::Auth::OpenIDAuth, realm, openid_options do |env|
459 # env['rack.session'][:authkey] == a_string
460 # end
461 # run RackApp
462 #
463 # Or simply:
464 #
465 # app = Rack::Auth::OpenIDAuth.new app, realm, openid_options, &auth
466
467 class OpenIDAuth < Rack::Auth::AbstractHandler
468 attr_reader :oid
469 def initialize(app, realm, options={}, &auth)
470 @oid = OpenID.new(realm, options)
471 super(app, &auth)
472 end
473
474 def call(env)
475 to = auth.call(env) ? @app : @oid
476 to.call env
477 end
478 end
479 end
480 end