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