diff options
author | Joshua Peek <josh@joshpeek.com> | 2008-11-22 14:33:00 -0600 |
---|---|---|
committer | Joshua Peek <josh@joshpeek.com> | 2008-11-22 14:33:00 -0600 |
commit | cc67272cba35e50afa73cfec856c1677b204ae7e (patch) | |
tree | 1bdbc4862fe0ea486bf8d2476ab12184e4b71807 /actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/openid.rb | |
parent | 4b36f76e7a997fb03a6cccb08b8272ddccde5a3e (diff) | |
download | rails-cc67272cba35e50afa73cfec856c1677b204ae7e.tar.gz rails-cc67272cba35e50afa73cfec856c1677b204ae7e.tar.bz2 rails-cc67272cba35e50afa73cfec856c1677b204ae7e.zip |
Vendor rack 0.4.0
Diffstat (limited to 'actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/openid.rb')
-rw-r--r-- | actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/openid.rb | 437 |
1 files changed, 437 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/openid.rb b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/openid.rb new file mode 100644 index 0000000000..2bd064ea4a --- /dev/null +++ b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/openid.rb @@ -0,0 +1,437 @@ +# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net + +gem 'ruby-openid', '~> 2' if defined? Gem +require 'rack/auth/abstract/handler' #rack +require 'uri' #std +require 'pp' #std +require 'openid' #gem +require 'openid/extension' #gem +require 'openid/store/memory' #gem + +module Rack + module Auth + # Rack::Auth::OpenID provides a simple method for permitting + # openid based logins. It requires the ruby-openid library from + # janrain to operate, as well as a rack method of session management. + # + # The ruby-openid home page is at http://openidenabled.com/ruby-openid/. + # + # The OpenID specifications can be found at + # http://openid.net/specs/openid-authentication-1_1.html + # and + # http://openid.net/specs/openid-authentication-2_0.html. Documentation + # for published OpenID extensions and related topics can be found at + # http://openid.net/developers/specs/. + # + # It is recommended to read through the OpenID spec, as well as + # ruby-openid's documentation, to understand what exactly goes on. However + # a setup as simple as the presented examples is enough to provide + # functionality. + # + # This library strongly intends to utilize the OpenID 2.0 features of the + # ruby-openid library, while maintaining OpenID 1.0 compatiblity. + # + # All responses from this rack application will be 303 redirects unless an + # error occurs, with the exception of an authentication request requiring + # an HTML form submission. + # + # NOTE: Extensions are not currently supported by this implimentation of + # the OpenID rack application due to the complexity of the current + # ruby-openid extension handling. + # + # NOTE: Due to the amount of data that this library stores in the + # session, Rack::Session::Cookie may fault. + class OpenID < AbstractHandler + class NoSession < RuntimeError; end + # Required for ruby-openid + OIDStore = ::OpenID::Store::Memory.new + HTML = '<html><head><title>%s</title></head><body>%s</body></html>' + + # A Hash of options is taken as it's single initializing + # argument. For example: + # + # simple_oid = OpenID.new('http://mysite.com/') + # + # return_oid = OpenID.new('http://mysite.com/', { + # :return_to => 'http://mysite.com/openid' + # }) + # + # page_oid = OpenID.new('http://mysite.com/', + # :login_good => 'http://mysite.com/auth_good' + # ) + # + # complex_oid = OpenID.new('http://mysite.com/', + # :return_to => 'http://mysite.com/openid', + # :login_good => 'http://mysite.com/user/preferences', + # :auth_fail => [500, {'Content-Type'=>'text/plain'}, + # 'Unable to negotiate with foreign server.'], + # :immediate => true, + # :extensions => { + # ::OpenID::SReg => [['email'],['nickname']] + # } + # ) + # + # = Arguments + # + # The first argument is the realm, identifying the site they are trusting + # with their identity. This is required. + # + # NOTE: In OpenID 1.x, the realm or trust_root is optional and the + # return_to url is required. As this library strives tward ruby-openid + # 2.0, and OpenID 2.0 compatibiliy, the realm is required and return_to + # is optional. However, this implimentation is still backwards compatible + # with OpenID 1.0 servers. + # + # The optional second argument is a hash of options. + # + # == Options + # + # <tt>:return_to</tt> defines the url to return to after the client + # authenticates with the openid service provider. This url should point + # to where Rack::Auth::OpenID is mounted. If <tt>:return_to</tt> is not + # provided, :return_to will be the current url including all query + # parameters. + # + # <tt>:session_key</tt> defines the key to the session hash in the env. + # It defaults to 'rack.session'. + # + # <tt>:openid_param</tt> defines at what key in the request parameters to + # find the identifier to resolve. As per the 2.0 spec, the default is + # 'openid_identifier'. + # + # <tt>:immediate</tt> as true will make immediate type of requests the + # default. See OpenID specification documentation. + # + # === URL options + # + # <tt>:login_good</tt> is the url to go to after the authentication + # process has completed. + # + # <tt>:login_fail</tt> is the url to go to after the authentication + # process has failed. + # + # <tt>:login_quit</tt> is the url to go to after the authentication + # process + # has been cancelled. + # + # === Response options + # + # <tt>:no_session</tt> should be a rack response to be returned if no or + # an incompatible session is found. + # + # <tt>:auth_fail</tt> should be a rack response to be returned if an + # OpenID::DiscoveryFailure occurs. This is typically due to being unable + # to access the identity url or identity server. + # + # <tt>:error</tt> should be a rack response to return if any other + # generic error would occur and <tt>options[:catch_errors]</tt> is true. + # + # === Extensions + # + # <tt>:extensions</tt> should be a hash of openid extension + # implementations. The key should be the extension main module, the value + # should be an array of arguments for extension::Request.new + # + # The hash is iterated over and passed to #add_extension for processing. + # Please see #add_extension for further documentation. + def initialize(realm, options={}) + @realm = realm + realm = URI(realm) + if realm.path.empty? + raise ArgumentError, "Invalid realm path: '#{realm.path}'" + elsif not realm.absolute? + raise ArgumentError, "Realm '#{@realm}' not absolute" + end + + [:return_to, :login_good, :login_fail, :login_quit].each do |key| + if options.key? key and luri = URI(options[key]) + if !luri.absolute? + raise ArgumentError, ":#{key} is not an absolute uri: '#{luri}'" + end + end + end + + if options[:return_to] and ruri = URI(options[:return_to]) + if ruri.path.empty? + raise ArgumentError, "Invalid return_to path: '#{ruri.path}'" + elsif realm.path != ruri.path[0, realm.path.size] + raise ArgumentError, 'return_to not within realm.' \ + end + end + + # TODO: extension support + if extensions = options.delete(:extensions) + extensions.each do |ext, args| + add_extension ext, *args + end + end + + @options = { + :session_key => 'rack.session', + :openid_param => 'openid_identifier', + #:return_to, :login_good, :login_fail, :login_quit + #:no_session, :auth_fail, :error + :store => OIDStore, + :immediate => false, + :anonymous => false, + :catch_errors => false + }.merge(options) + @extensions = {} + end + + attr_reader :options, :extensions + + # It sets up and uses session data at <tt>:openid</tt> within the + # session. It sets up the ::OpenID::Consumer using the store specified by + # <tt>options[:store]</tt>. + # + # If the parameter specified by <tt>options[:openid_param]</tt> is + # present, processing is passed to #check and the result is returned. + # + # If the parameter 'openid.mode' is set, implying a followup from the + # openid server, processing is passed to #finish and the result is + # returned. + # + # If neither of these conditions are met, a 400 error is returned. + # + # If an error is thrown and <tt>options[:catch_errors]</tt> is false, the + # exception will be reraised. Otherwise a 500 error is returned. + def call(env) + env['rack.auth.openid'] = self + session = env[@options[:session_key]] + unless session and session.is_a? Hash + raise(NoSession, 'No compatible session') + end + # let us work in our own namespace... + session = (session[:openid] ||= {}) + unless session and session.is_a? Hash + raise(NoSession, 'Incompatible session') + end + + request = Rack::Request.new env + consumer = ::OpenID::Consumer.new session, @options[:store] + + if request.params['openid.mode'] + finish consumer, session, request + elsif request.params[@options[:openid_param]] + check consumer, session, request + else + env['rack.errors'].puts "No valid params provided." + bad_request + end + rescue NoSession + env['rack.errors'].puts($!.message, *$@) + + @options. ### Missing or incompatible session + fetch :no_session, [ 500, + {'Content-Type'=>'text/plain'}, + $!.message ] + rescue + env['rack.errors'].puts($!.message, *$@) + + if not @options[:catch_error] + raise($!) + end + @options. + fetch :error, [ 500, + {'Content-Type'=>'text/plain'}, + 'OpenID has encountered an error.' ] + end + + # As the first part of OpenID consumer action, #check retrieves the data + # required for completion. + # + # * <tt>session[:openid][:openid_param]</tt> is set to the submitted + # identifier to be authenticated. + # * <tt>session[:openid][:site_return]</tt> is set as the request's + # HTTP_REFERER, unless already set. + # * <tt>env['rack.auth.openid.request']</tt> is the openid checkid + # request instance. + def check(consumer, session, req) + session[:openid_param] = req.params[@options[:openid_param]] + oid = consumer.begin(session[:openid_param], @options[:anonymous]) + pp oid if $DEBUG + req.env['rack.auth.openid.request'] = oid + + session[:site_return] ||= req.env['HTTP_REFERER'] + + # SETUP_NEEDED check! + # see OpenID::Consumer::CheckIDRequest docs + query_args = [@realm, *@options.values_at(:return_to, :immediate)] + query_args[1] ||= req.url + query_args[2] = false if session.key? :setup_needed + pp query_args if $DEBUG + + ## Extension support + extensions.each do |ext,args| + oid.add_extension ext::Request.new(*args) + end + + if oid.send_redirect?(*query_args) + redirect = oid.redirect_url(*query_args) + if $DEBUG + pp redirect + pp Rack::Utils.parse_query(URI(redirect).query) + end + [ 303, {'Location'=>redirect}, [] ] + else + # check on 'action' option. + formbody = oid.form_markup(*query_args) + if $DEBUG + pp formbody + end + body = HTML % ['Confirm...', formbody] + [ 200, {'Content-Type'=>'text/html'}, body.to_a ] + end + rescue ::OpenID::DiscoveryFailure => e + # thrown from inside OpenID::Consumer#begin by yadis stuff + req.env['rack.errors'].puts($!.message, *$@) + + @options. ### Foreign server failed + fetch :auth_fail, [ 503, + {'Content-Type'=>'text/plain'}, + 'Foreign server failure.' ] + end + + # This is the final portion of authentication. Unless any errors outside + # of specification occur, a 303 redirect will be returned with Location + # determined by the OpenID response type. If none of the response type + # :login_* urls are set, the redirect will be set to + # <tt>session[:openid][:site_return]</tt>. If + # <tt>session[:openid][:site_return]</tt> is unset, the realm will be + # used. + # + # Any messages from OpenID's response are appended to the 303 response + # body. + # + # Data gathered from extensions are stored in session[:openid] with the + # extension's namespace uri as the key. + # + # * <tt>env['rack.auth.openid.response']</tt> is the openid response. + # + # The four valid possible outcomes are: + # * failure: <tt>options[:login_fail]</tt> or + # <tt>session[:site_return]</tt> or the realm + # * <tt>session[:openid]</tt> is cleared and any messages are send to + # rack.errors + # * <tt>session[:openid]['authenticated']</tt> is <tt>false</tt> + # * success: <tt>options[:login_good]</tt> or + # <tt>session[:site_return]</tt> or the realm + # * <tt>session[:openid]</tt> is cleared + # * <tt>session[:openid]['authenticated']</tt> is <tt>true</tt> + # * <tt>session[:openid]['identity']</tt> is the actual identifier + # * <tt>session[:openid]['identifier']</tt> is the pretty identifier + # * cancel: <tt>options[:login_good]</tt> or + # <tt>session[:site_return]</tt> or the realm + # * <tt>session[:openid]</tt> is cleared + # * <tt>session[:openid]['authenticated']</tt> is <tt>false</tt> + # * setup_needed: resubmits the authentication request. A flag is set for + # non-immediate handling. + # * <tt>session[:openid][:setup_needed]</tt> is set to <tt>true</tt>, + # which will prevent immediate style openid authentication. + def finish(consumer, session, req) + oid = consumer.complete(req.params, req.url) + pp oid if $DEBUG + req.env['rack.auth.openid.response'] = oid + + goto = session.fetch :site_return, @realm + body = [] + + case oid.status + when ::OpenID::Consumer::FAILURE + session.clear + session['authenticated'] = false + req.env['rack.errors'].puts oid.message + + goto = @options[:login_fail] if @option.key? :login_fail + body << "Authentication unsuccessful.\n" + when ::OpenID::Consumer::SUCCESS + session.clear + + ## Extension support + extensions.each do |ext, args| + session[ext::NS_URI] = ext::Response. + from_success_response(oid). + get_extension_args + end + + session['authenticated'] = true + # Value for unique identification and such + session['identity'] = oid.identity_url + # Value for display and UI labels + session['identifier'] = oid.display_identifier + + goto = @options[:login_good] if @options.key? :login_good + body << "Authentication successful.\n" + when ::OpenID::Consumer::CANCEL + session.clear + session['authenticated'] = false + + goto = @options[:login_fail] if @option.key? :login_fail + body << "Authentication cancelled.\n" + when ::OpenID::Consumer::SETUP_NEEDED + session[:setup_needed] = true + unless o_id = session[:openid_param] + raise('Required values missing.') + end + + goto = req.script_name+ + '?'+@options[:openid_param]+ + '='+o_id + body << "Reauthentication required.\n" + end + body << oid.message if oid.message + [ 303, {'Location'=>goto}, body] + end + + # The first argument should be the main extension module. + # The extension module should contain the constants: + # * class Request, with OpenID::Extension as an ancestor + # * class Response, with OpenID::Extension as an ancestor + # * string NS_URI, which defines the namespace of the extension, should + # be an absolute http uri + # + # All trailing arguments will be passed to extension::Request.new in + # #check. + # The openid response will be passed to + # extension::Response#from_success_response, #get_extension_args will be + # called on the result to attain the gathered data. + # + # This method returns the key at which the response data will be found in + # the session, which is the namespace uri by default. + def add_extension ext, *args + if not ext.is_a? Module + raise TypeError, "#{ext.inspect} is not a module" + elsif not (m = %w'Request Response NS_URI' - ext.constants).empty? + raise ArgumentError, "#{ext.inspect} missing #{m*', '}" + end + + consts = [ext::Request, ext::Response] + + if not consts.all?{|c| c.is_a? Class } + raise TypeError, "#{ext.inspect}'s Request or Response is not a class" + elsif not consts.all?{|c| ::OpenID::Extension > c } + raise ArgumentError, "#{ext.inspect}'s Request or Response not a decendant of OpenID::Extension" + end + + if not ext::NS_URI.is_a? String + raise TypeError, "#{ext.inspect}'s NS_URI is not a string" + elsif not uri = URI(ext::NS_URI) + raise ArgumentError, "#{ext.inspect}'s NS_URI is not a valid uri" + elsif not uri.scheme =~ /^https?$/ + raise ArgumentError, "#{ext.inspect}'s NS_URI is not an http uri" + elsif not uri.absolute? + raise ArgumentError, "#{ext.inspect}'s NS_URI is not and absolute uri" + end + @extensions[ext] = args + return ext::NS_URI + end + + # A conveniance method that returns the namespace of all current + # extensions used by this instance. + def extension_namespaces + @extensions.keys.map{|e|e::NS_URI} + end + end + end +end |