lib/rack/auth/openid.rb in rack-0.9.1 vs lib/rack/auth/openid.rb in rack-1.0.0

- old
+ new

@@ -1,20 +1,32 @@ # 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 'rack/request' +require 'rack/utils' +require 'rack/auth/abstract/handler' +require 'uri' require 'openid' #gem require 'openid/extension' #gem require 'openid/store/memory' #gem module Rack + class Request + def openid_request + @env['rack.auth.openid.request'] + end + + def openid_response + @env['rack.auth.openid.response'] + end + end + 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. + + # Rack::Auth::OpenID provides a simple method for setting up an OpenID + # Consumer. 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 @@ -24,415 +36,445 @@ # 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. + # Consumer functionality. # # This library strongly intends to utilize the OpenID 2.0 features of the - # ruby-openid library, while maintaining OpenID 1.0 compatiblity. + # ruby-openid library, which provides 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 OpenID + class NoSession < RuntimeError; end + class BadExtension < RuntimeError; end # Required for ruby-openid - OIDStore = ::OpenID::Store::Memory.new - HTML = '<html><head><title>%s</title></head><body>%s</body></html>' + ValidStatus = [:success, :setup_needed, :cancel, :failure] - # 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. + # with their identity. This is required, also treated as the trust_root + # in OpenID 1.x exchanges. # - # 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. + # provided, return_to will be the current url which allows flexibility + # with caveats. # # <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. + # <tt>:store</tt> defined what OpenID Store to use for persistant + # information. By default a Store::Memory will be used. # - # === URL options + # <tt>:immediate</tt> as true will make initial requests to be of an + # immediate type. This is false by default. See OpenID specification + # documentation. # - # <tt>:login_good</tt> is the url to go to after the authentication - # process has completed. + # <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. # - # <tt>:login_fail</tt> is the url to go to after the authentication - # process has failed. + # == Examples # - # <tt>:login_quit</tt> is the url to go to after the authentication - # process - # has been cancelled. + # simple_oid = OpenID.new('http://mysite.com/') # - # === Response options + # return_oid = OpenID.new('http://mysite.com/', { + # :return_to => 'http://mysite.com/openid' + # }) # - # <tt>:no_session</tt> should be a rack response to be returned if no or - # an incompatible session is found. + # complex_oid = OpenID.new('http://mysite.com/', + # :immediate => true, + # :extensions => { + # ::OpenID::SReg => [['email'],['nickname']] + # } + # ) # - # <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. + # = Advanced # - # <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. + # Most of the functionality of this library is encapsulated such that + # expansion and overriding functions isn't difficult nor tricky. + # Alternately, to avoid opening up singleton objects or subclassing, a + # wrapper rack middleware can be composed to act upon Auth::OpenID's + # responses. See #check and #finish for locations of pertinent data. # - # === Extensions + # == Responses # - # <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 + # To change the responses that Auth::OpenID returns, override the methods + # #redirect, #bad_request, #unauthorized, #access_denied, and + # #foreign_server_failure. # - # The hash is iterated over and passed to #add_extension for processing. - # Please see #add_extension for further documentation. + # Additionally #confirm_post_params is used when the URI would exceed + # length limits on a GET request when doing the initial verification + # request. + # + # == Processing + # + # To change methods of processing completed transactions, override the + # methods #success, #setup_needed, #cancel, and #failure. Please ensure + # the returned object is a rack compatible response. + # + # The first argument is an OpenID::Response, the second is a + # Rack::Request of the current request, the last is the hash used in + # ruby-openid handling, which can be found manually at + # env['rack.session'][:openid]. + # + # This is useful if you wanted to expand the processing done, such as + # setting up user accounts. + # + # oid_app = Rack::Auth::OpenID.new realm, :return_to => return_to + # def oid_app.success oid, request, session + # user = Models::User[oid.identity_url] + # user ||= Models::User.create_from_openid oid + # request['rack.session'][:user] = user.id + # redirect MyApp.site_home + # end + # + # site_map['/openid'] = oid_app + # map = Rack::URLMap.new site_map + # ... + 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 + raise ArgumentError, "Invalid realm: #{realm}" \ + unless realm.absolute? \ + and realm.fragment.nil? \ + and realm.scheme =~ /^https?$/ \ + and realm.host =~ /^(\*\.)?#{URI::REGEXP::PATTERN::URIC_NO_SLASH}+/ + realm.path = '/' if realm.path.empty? + @realm = realm.to_s - [: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 + if ruri = options[:return_to] + ruri = URI(ruri) + raise ArgumentError, "Invalid return_to: #{ruri}" \ + unless ruri.absolute? \ + and ruri.scheme =~ /^https?$/ \ + and ruri.fragment.nil? + raise ArgumentError, "return_to #{ruri} not within realm #{realm}" \ + unless self.within_realm?(ruri) + @return_to = ruri.to_s 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 + @session_key = options[:session_key] || 'rack.session' + @openid_param = options[:openid_param] || 'openid_identifier' + @store = options[:store] || ::OpenID::Store::Memory.new + @immediate = !!options[:immediate] - # TODO: extension support + @extensions = {} 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 = {} + # Undocumented, semi-experimental + @anonymous = !!options[:anonymous] end - attr_reader :options, :extensions + attr_reader :realm, :return_to, :session_key, :openid_param, :store, + :immediate, :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>. + # Sets up and uses session data at <tt>:openid</tt> within the session. + # Errors in this setup will raise a NoSession exception. # + # If the parameter 'openid.mode' is set, which implies a followup from + # the openid server, processing is passed to #finish and the result is + # returned. However, if there is no appropriate openid information in the + # session, a 400 error is returned. + # # 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. + # If neither of these conditions are met, #unauthorized is called. + 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') + env_session = env[@session_key] + unless env_session and env_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') + session = (env_session[:openid] ||= {}) + unless session and session.is_a?(Hash) + raise NoSession, 'Incompatible openid session' end - request = Rack::Request.new env - consumer = ::OpenID::Consumer.new session, @options[:store] + request = Rack::Request.new(env) + consumer = ::OpenID::Consumer.new(session, @store) - if request.params['openid.mode'] - finish consumer, session, request - elsif request.params[@options[:openid_param]] - check consumer, session, request + if mode = request.GET['openid.mode'] + if session.key?(:openid_param) + finish(consumer, session, request) + else + bad_request + end + elsif request.GET[@openid_param] + check(consumer, session, request) else - env['rack.errors'].puts "No valid params provided." - bad_request + unauthorized 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. + # If all parameters fit within the max length of a URI, a 303 redirect + # will be returned. Otherwise #confirm_post_params will be called. + # + # Any messages from OpenID's request are logged to env['rack.errors'] + # + # <tt>env['rack.auth.openid.request']</tt> is the openid checkid request + # instance. + # + # <tt>session[:openid_param]</tt> is set to the openid identifier + # provided by the user. + # + # <tt>session[:return_to]</tt> is set to the return_to uri given to the + # identity provider. + 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 + oid = consumer.begin(req.GET[@openid_param], @anonymous) req.env['rack.auth.openid.request'] = oid + req.env['rack.errors'].puts(oid.message) + p oid if $DEBUG - 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) + 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}, [] ] + session[:openid_param] = req.GET[openid_param] + return_to_uri = return_to ? return_to : req.url + session[:return_to] = return_to_uri + immediate = session.key?(:setup_needed) ? false : immediate + + if oid.send_redirect?(realm, return_to_uri, immediate) + uri = oid.redirect_url(realm, return_to_uri, immediate) + redirect(uri) 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 ] + confirm_post_params(oid, realm, return_to_uri, immediate) 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.' ] + req.env['rack.errors'].puts([e.message, *e.backtrace]*"\n") + return 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. - # + # This is the final portion of authentication. + # If successful, a redirect to the realm is be returned. # 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. + # Any messages from OpenID's response are logged to env['rack.errors'] # - # 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. + # <tt>env['rack.auth.openid.response']</tt> will contain the openid + # response. + def finish(consumer, session, req) - oid = consumer.complete(req.params, req.url) - pp oid if $DEBUG + oid = consumer.complete(req.GET, req.url) req.env['rack.auth.openid.response'] = oid + req.env['rack.errors'].puts(oid.message) + p oid if $DEBUG - 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 @options.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 @options.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] + raise unless ValidStatus.include?(oid.status) + __send__(oid.status, oid, req, session) 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 + # * class Request, should have OpenID::Extension as an ancestor + # * class Response, should have OpenID::Extension as an ancestor + # * string NS_URI, which defining the namespace of the extension # # 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 !(m = %w'Request Response NS_URI' - - ext.constants.map{ |c| c.to_s }).empty? - raise ArgumentError, "#{ext.inspect} missing #{m*', '}" + + def add_extension(ext, *args) + raise BadExtension unless valid_extension?(ext) + extensions[ext] = args + return ext::NS_URI + end + + # Checks the validitity, in the context of usage, of a submitted + # extension. + + def valid_extension?(ext) + if not %w[NS_URI Request Response].all?{|c| ext.const_defined?(c) } + raise ArgumentError, 'Extension is missing constants.' + elsif not ext::Response.respond_to?(:from_success_response) + raise ArgumentError, 'Response is missing required method.' end + return true + rescue + return false + end - consts = [ext::Request, ext::Response] + # Checks the provided uri to ensure it'd be considered within the realm. + # is currently not compatible with wildcard realms. - 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" + def within_realm? uri + uri = URI.parse(uri.to_s) + realm = URI.parse(self.realm) + return false unless uri.absolute? + return false unless uri.path[0, realm.path.size] == realm.path + return false unless uri.host == realm.host or realm.host[/^\*\./] + # for wildcard support, is awkward with URI limitations + realm_match = Regexp.escape(realm.host). + sub(/^\*\./,"^#{URI::REGEXP::PATTERN::URIC_NO_SLASH}+.")+'$' + return false unless uri.host.match(realm_match) + return true + end + alias_method :include?, :within_realm? + + protected + + ### These methods define some of the boilerplate responses. + + # Returns an html form page for posting to an Identity Provider if the + # GET request would exceed the upper URI length limit. + + def confirm_post_params(oid, realm, return_to, immediate) + Rack::Response.new.finish do |r| + r.write '<html><head><title>Confirm...</title></head><body>' + r.write oid.form_markup(realm, return_to, immediate) + r.write '</body></html>' end + 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" + # Returns a 303 redirect with the destination of that provided by the + # argument. + + def redirect(uri) + [ 303, {'Content-Length'=>'0', 'Content-Type'=>'text/plain', + 'Location' => uri}, + [] ] + end + + # Returns an empty 400 response. + + def bad_request + [ 400, {'Content-Type'=>'text/plain', 'Content-Length'=>'0'}, + [''] ] + end + + # Returns a basic unauthorized 401 response. + + def unauthorized + [ 401, {'Content-Type' => 'text/plain', 'Content-Length' => '13'}, + ['Unauthorized.'] ] + end + + # Returns a basic access denied 403 response. + + def access_denied + [ 403, {'Content-Type' => 'text/plain', 'Content-Length' => '14'}, + ['Access denied.'] ] + end + + # Returns a 503 response to be used if communication with the remote + # OpenID server fails. + + def foreign_server_failure + [ 503, {'Content-Type'=>'text/plain', 'Content-Length' => '23'}, + ['Foreign server failure.'] ] + end + + private + + ### These methods are called after a transaction is completed, depending + # on its outcome. These should all return a rack compatible response. + # You'd want to override these to provide additional functionality. + + # Called to complete processing on a successful transaction. + # Within the openid session, :openid_identity and :openid_identifier are + # set to the user friendly and the standard representation of the + # validated identity. All other data in the openid session is cleared. + + def success(oid, request, session) + session.clear + session[:openid_identity] = oid.display_identifier + session[:openid_identifier] = oid.identity_url + extensions.keys.each do |ext| + label = ext.name[/[^:]+$/].downcase + response = ext::Response.from_success_response(oid) + session[label] = response.data end - @extensions[ext] = args - return ext::NS_URI + redirect(realm) 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} + # Called if the Identity Provider indicates further setup by the user is + # required. + # The identifier is retrived from the openid session at :openid_param. + # And :setup_needed is set to true to prevent looping. + + def setup_needed(oid, request, session) + identifier = session[:openid_param] + session[:setup_needed] = true + redirect req.script_name + '?' + openid_param + '=' + identifier + end + + # Called if the user indicates they wish to cancel identification. + # Data within openid session is cleared. + + def cancel(oid, request, session) + session.clear + access_denied + end + + # Called if the Identity Provider indicates the user is unable to confirm + # their identity. Data within the openid session is left alone, in case + # of swarm auth attacks. + + def failure(oid, request, session) + unauthorized + end + end + + # A class developed out of the request to use OpenID as an authentication + # middleware. The request will be sent to the OpenID instance unless the + # block evaluates to true. For example in rackup, you can use it as such: + # + # use Rack::Session::Pool + # use Rack::Auth::OpenIDAuth, realm, openid_options do |env| + # env['rack.session'][:authkey] == a_string + # end + # run RackApp + # + # Or simply: + # + # app = Rack::Auth::OpenIDAuth.new app, realm, openid_options, &auth + + class OpenIDAuth < Rack::Auth::AbstractHandler + attr_reader :oid + def initialize(app, realm, options={}, &auth) + @oid = OpenID.new(realm, options) + super(app, &auth) + end + + def call(env) + to = auth.call(env) ? @app : @oid + to.call env end end end end