git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2437 e93f8b46-1217-0410-a6f0-8f06a7374b81
396 lines
16 KiB
Ruby
396 lines
16 KiB
Ruby
require "openid/consumer/idres.rb"
|
|
require "openid/consumer/checkid_request.rb"
|
|
require "openid/consumer/associationmanager.rb"
|
|
require "openid/consumer/responses.rb"
|
|
require "openid/consumer/discovery_manager"
|
|
require "openid/consumer/discovery"
|
|
require "openid/message"
|
|
require "openid/yadis/discovery"
|
|
require "openid/store/nonce"
|
|
|
|
module OpenID
|
|
# OpenID support for Relying Parties (aka Consumers).
|
|
#
|
|
# This module documents the main interface with the OpenID consumer
|
|
# library. The only part of the library which has to be used and
|
|
# isn't documented in full here is the store required to create an
|
|
# Consumer instance.
|
|
#
|
|
# = OVERVIEW
|
|
#
|
|
# The OpenID identity verification process most commonly uses the
|
|
# following steps, as visible to the user of this library:
|
|
#
|
|
# 1. The user enters their OpenID into a field on the consumer's
|
|
# site, and hits a login button.
|
|
#
|
|
# 2. The consumer site discovers the user's OpenID provider using
|
|
# the Yadis protocol.
|
|
#
|
|
# 3. The consumer site sends the browser a redirect to the OpenID
|
|
# provider. This is the authentication request as described in
|
|
# the OpenID specification.
|
|
#
|
|
# 4. The OpenID provider's site sends the browser a redirect back to
|
|
# the consumer site. This redirect contains the provider's
|
|
# response to the authentication request.
|
|
#
|
|
# The most important part of the flow to note is the consumer's site
|
|
# must handle two separate HTTP requests in order to perform the
|
|
# full identity check.
|
|
#
|
|
# = LIBRARY DESIGN
|
|
#
|
|
# This consumer library is designed with that flow in mind. The
|
|
# goal is to make it as easy as possible to perform the above steps
|
|
# securely.
|
|
#
|
|
# At a high level, there are two important parts in the consumer
|
|
# library. The first important part is this module, which contains
|
|
# the interface to actually use this library. The second is
|
|
# openid/store/interface.rb, which describes the interface to use if
|
|
# you need to create a custom method for storing the state this
|
|
# library needs to maintain between requests.
|
|
#
|
|
# In general, the second part is less important for users of the
|
|
# library to know about, as several implementations are provided
|
|
# which cover a wide variety of situations in which consumers may
|
|
# use the library.
|
|
#
|
|
# The Consumer class has methods corresponding to the actions
|
|
# necessary in each of steps 2, 3, and 4 described in the overview.
|
|
# Use of this library should be as easy as creating an Consumer
|
|
# instance and calling the methods appropriate for the action the
|
|
# site wants to take.
|
|
#
|
|
# This library automatically detects which version of the OpenID
|
|
# protocol should be used for a transaction and constructs the
|
|
# proper requests and responses. Users of this library do not need
|
|
# to worry about supporting multiple protocol versions; the library
|
|
# supports them implicitly. Depending on the version of the
|
|
# protocol in use, the OpenID transaction may be more secure. See
|
|
# the OpenID specifications for more information.
|
|
#
|
|
# = SESSIONS, STORES, AND STATELESS MODE
|
|
#
|
|
# The Consumer object keeps track of two types of state:
|
|
#
|
|
# 1. State of the user's current authentication attempt. Things
|
|
# like the identity URL, the list of endpoints discovered for
|
|
# that URL, and in case where some endpoints are unreachable, the
|
|
# list of endpoints already tried. This state needs to be held
|
|
# from Consumer.begin() to Consumer.complete(), but it is only
|
|
# applicable to a single session with a single user agent, and at
|
|
# the end of the authentication process (i.e. when an OP replies
|
|
# with either <tt>id_res</tt>. or <tt>cancel</tt> it may be
|
|
# discarded.
|
|
#
|
|
# 2. State of relationships with servers, i.e. shared secrets
|
|
# (associations) with servers and nonces seen on signed messages.
|
|
# This information should persist from one session to the next
|
|
# and should not be bound to a particular user-agent.
|
|
#
|
|
# These two types of storage are reflected in the first two
|
|
# arguments of Consumer's constructor, <tt>session</tt> and
|
|
# <tt>store</tt>. <tt>session</tt> is a dict-like object and we
|
|
# hope your web framework provides you with one of these bound to
|
|
# the user agent. <tt>store</tt> is an instance of Store.
|
|
#
|
|
# Since the store does hold secrets shared between your application
|
|
# and the OpenID provider, you should be careful about how you use
|
|
# it in a shared hosting environment. If the filesystem or database
|
|
# permissions of your web host allow strangers to read from them, do
|
|
# not store your data there! If you have no safe place to store
|
|
# your data, construct your consumer with nil for the store, and it
|
|
# will operate only in stateless mode. Stateless mode may be
|
|
# slower, put more load on the OpenID provider, and trusts the
|
|
# provider to keep you safe from replay attacks.
|
|
#
|
|
# Several store implementation are provided, and the interface is
|
|
# fully documented so that custom stores can be used as well. See
|
|
# the documentation for the Consumer class for more information on
|
|
# the interface for stores. The implementations that are provided
|
|
# allow the consumer site to store the necessary data in several
|
|
# different ways, including several SQL databases and normal files
|
|
# on disk.
|
|
#
|
|
# = IMMEDIATE MODE
|
|
#
|
|
# In the flow described above, the user may need to confirm to the
|
|
# OpenID provider that it's ok to disclose his or her identity. The
|
|
# provider may draw pages asking for information from the user
|
|
# before it redirects the browser back to the consumer's site. This
|
|
# is generally transparent to the consumer site, so it is typically
|
|
# ignored as an implementation detail.
|
|
#
|
|
# There can be times, however, where the consumer site wants to get
|
|
# a response immediately. When this is the case, the consumer can
|
|
# put the library in immediate mode. In immediate mode, there is an
|
|
# extra response possible from the server, which is essentially the
|
|
# server reporting that it doesn't have enough information to answer
|
|
# the question yet.
|
|
#
|
|
# = USING THIS LIBRARY
|
|
#
|
|
# Integrating this library into an application is usually a
|
|
# relatively straightforward process. The process should basically
|
|
# follow this plan:
|
|
#
|
|
# Add an OpenID login field somewhere on your site. When an OpenID
|
|
# is entered in that field and the form is submitted, it should make
|
|
# a request to the your site which includes that OpenID URL.
|
|
#
|
|
# First, the application should instantiate a Consumer with a
|
|
# session for per-user state and store for shared state using the
|
|
# store of choice.
|
|
#
|
|
# Next, the application should call the <tt>begin</tt> method of
|
|
# Consumer instance. This method takes the OpenID URL as entered by
|
|
# the user. The <tt>begin</tt> method returns a CheckIDRequest
|
|
# object.
|
|
#
|
|
# Next, the application should call the redirect_url method on the
|
|
# CheckIDRequest object. The parameter <tt>return_to</tt> is the
|
|
# URL that the OpenID server will send the user back to after
|
|
# attempting to verify his or her identity. The <tt>realm</tt>
|
|
# parameter is the URL (or URL pattern) that identifies your web
|
|
# site to the user when he or she is authorizing it. Send a
|
|
# redirect to the resulting URL to the user's browser.
|
|
#
|
|
# That's the first half of the authentication process. The second
|
|
# half of the process is done after the user's OpenID Provider sends
|
|
# the user's browser a redirect back to your site to complete their
|
|
# login.
|
|
#
|
|
# When that happens, the user will contact your site at the URL
|
|
# given as the <tt>return_to</tt> URL to the redirect_url call made
|
|
# above. The request will have several query parameters added to
|
|
# the URL by the OpenID provider as the information necessary to
|
|
# finish the request.
|
|
#
|
|
# Get a Consumer instance with the same session and store as before
|
|
# and call its complete() method, passing in all the received query
|
|
# arguments and URL currently being handled.
|
|
#
|
|
# There are multiple possible return types possible from that
|
|
# method. These indicate the whether or not the login was
|
|
# successful, and include any additional information appropriate for
|
|
# their type.
|
|
class Consumer
|
|
attr_accessor :session_key_prefix
|
|
|
|
# Initialize a Consumer instance.
|
|
#
|
|
# You should create a new instance of the Consumer object with
|
|
# every HTTP request that handles OpenID transactions.
|
|
#
|
|
# session: the session object to use to store request information.
|
|
# The session should behave like a hash.
|
|
#
|
|
# store: an object that implements the interface in Store.
|
|
def initialize(session, store)
|
|
@session = session
|
|
@store = store
|
|
@session_key_prefix = 'OpenID::Consumer::'
|
|
end
|
|
|
|
# Start the OpenID authentication process. See steps 1-2 in the
|
|
# overview for the Consumer class.
|
|
#
|
|
# user_url: Identity URL given by the user. This method performs a
|
|
# textual transformation of the URL to try and make sure it is
|
|
# normalized. For example, a user_url of example.com will be
|
|
# normalized to http://example.com/ normalizing and resolving any
|
|
# redirects the server might issue.
|
|
#
|
|
# anonymous: A boolean value. Whether to make an anonymous
|
|
# request of the OpenID provider. Such a request does not ask for
|
|
# an authorization assertion for an OpenID identifier, but may be
|
|
# used with extensions to pass other data. e.g. "I don't care who
|
|
# you are, but I'd like to know your time zone."
|
|
#
|
|
# Returns a CheckIDRequest object containing the discovered
|
|
# information, with a method for building a redirect URL to the
|
|
# server, as described in step 3 of the overview. This object may
|
|
# also be used to add extension arguments to the request, using
|
|
# its add_extension_arg method.
|
|
#
|
|
# Raises DiscoveryFailure when no OpenID server can be found for
|
|
# this URL.
|
|
def begin(openid_identifier, anonymous=false)
|
|
manager = discovery_manager(openid_identifier)
|
|
service = manager.get_next_service(&method(:discover))
|
|
|
|
if service.nil?
|
|
raise DiscoveryFailure.new("No usable OpenID services were found "\
|
|
"for #{openid_identifier.inspect}", nil)
|
|
else
|
|
begin_without_discovery(service, anonymous)
|
|
end
|
|
end
|
|
|
|
# Start OpenID verification without doing OpenID server
|
|
# discovery. This method is used internally by Consumer.begin()
|
|
# after discovery is performed, and exists to provide an interface
|
|
# for library users needing to perform their own discovery.
|
|
#
|
|
# service: an OpenID service endpoint descriptor. This object and
|
|
# factories for it are found in the openid/consumer/discovery.rb
|
|
# module.
|
|
#
|
|
# Returns an OpenID authentication request object.
|
|
def begin_without_discovery(service, anonymous)
|
|
assoc = association_manager(service).get_association
|
|
checkid_request = CheckIDRequest.new(assoc, service)
|
|
checkid_request.anonymous = anonymous
|
|
|
|
if service.compatibility_mode
|
|
rt_args = checkid_request.return_to_args
|
|
rt_args[Consumer.openid1_return_to_nonce_name] = Nonce.mk_nonce
|
|
rt_args[Consumer.openid1_return_to_claimed_id_name] =
|
|
service.claimed_id
|
|
end
|
|
|
|
self.last_requested_endpoint = service
|
|
return checkid_request
|
|
end
|
|
|
|
# Called to interpret the server's response to an OpenID
|
|
# request. It is called in step 4 of the flow described in the
|
|
# Consumer overview.
|
|
#
|
|
# query: A hash of the query parameters for this HTTP request.
|
|
# Note that in rails, this is <b>not</b> <tt>params</tt> but
|
|
# <tt>params.reject{|k,v|request.path_parameters[k]}</tt>
|
|
# because <tt>controller</tt> and <tt>action</tt> and other
|
|
# "path parameters" are included in params.
|
|
#
|
|
# current_url: Extract the URL of the current request from your
|
|
# application's web request framework and specify it here to have it
|
|
# checked against the openid.return_to value in the response. Do not
|
|
# just pass <tt>args['openid.return_to']</tt> here; that will defeat the
|
|
# purpose of this check. (See OpenID Authentication 2.0 section 11.1.)
|
|
#
|
|
# If the return_to URL check fails, the status of the completion will be
|
|
# FAILURE.
|
|
|
|
#
|
|
# Returns a subclass of Response. The type of response is
|
|
# indicated by the status attribute, which will be one of
|
|
# SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
|
|
def complete(query, current_url)
|
|
message = Message.from_post_args(query)
|
|
mode = message.get_arg(OPENID_NS, 'mode', 'invalid')
|
|
begin
|
|
meth = method('complete_' + mode)
|
|
rescue NameError
|
|
meth = method(:complete_invalid)
|
|
end
|
|
response = meth.call(message, current_url)
|
|
cleanup_last_requested_endpoint
|
|
if [SUCCESS, CANCEL].member?(response.status)
|
|
cleanup_session
|
|
end
|
|
return response
|
|
end
|
|
|
|
protected
|
|
|
|
def session_get(name)
|
|
@session[session_key(name)]
|
|
end
|
|
|
|
def session_set(name, val)
|
|
@session[session_key(name)] = val
|
|
end
|
|
|
|
def session_key(suffix)
|
|
@session_key_prefix + suffix
|
|
end
|
|
|
|
def last_requested_endpoint
|
|
session_get('last_requested_endpoint')
|
|
end
|
|
|
|
def last_requested_endpoint=(endpoint)
|
|
session_set('last_requested_endpoint', endpoint)
|
|
end
|
|
|
|
def cleanup_last_requested_endpoint
|
|
@session[session_key('last_requested_endpoint')] = nil
|
|
end
|
|
|
|
def discovery_manager(openid_identifier)
|
|
DiscoveryManager.new(@session, openid_identifier, @session_key_prefix)
|
|
end
|
|
|
|
def cleanup_session
|
|
discovery_manager(nil).cleanup(true)
|
|
end
|
|
|
|
|
|
def discover(identifier)
|
|
OpenID.discover(identifier)
|
|
end
|
|
|
|
def negotiator
|
|
DefaultNegotiator
|
|
end
|
|
|
|
def association_manager(service)
|
|
AssociationManager.new(@store, service.server_url,
|
|
service.compatibility_mode, negotiator)
|
|
end
|
|
|
|
def handle_idres(message, current_url)
|
|
IdResHandler.new(message, current_url, @store, last_requested_endpoint)
|
|
end
|
|
|
|
def complete_invalid(message, unused_return_to)
|
|
mode = message.get_arg(OPENID_NS, 'mode', '<No mode set>')
|
|
return FailureResponse.new(last_requested_endpoint,
|
|
"Invalid openid.mode: #{mode}")
|
|
end
|
|
|
|
def complete_cancel(unused_message, unused_return_to)
|
|
return CancelResponse.new(last_requested_endpoint)
|
|
end
|
|
|
|
def complete_error(message, unused_return_to)
|
|
error = message.get_arg(OPENID_NS, 'error')
|
|
contact = message.get_arg(OPENID_NS, 'contact')
|
|
reference = message.get_arg(OPENID_NS, 'reference')
|
|
|
|
return FailureResponse.new(last_requested_endpoint,
|
|
error, contact, reference)
|
|
end
|
|
|
|
def complete_setup_needed(message, unused_return_to)
|
|
if message.is_openid1
|
|
return complete_invalid(message, nil)
|
|
else
|
|
setup_url = message.get_arg(OPENID2_NS, 'user_setup_url')
|
|
return SetupNeededResponse.new(last_requested_endpoint, setup_url)
|
|
end
|
|
end
|
|
|
|
def complete_id_res(message, current_url)
|
|
if message.is_openid1
|
|
setup_url = message.get_arg(OPENID1_NS, 'user_setup_url')
|
|
if !setup_url.nil?
|
|
return SetupNeededResponse.new(last_requested_endpoint, setup_url)
|
|
end
|
|
end
|
|
|
|
begin
|
|
idres = handle_idres(message, current_url)
|
|
rescue OpenIDError => why
|
|
return FailureResponse.new(last_requested_endpoint, why.message)
|
|
else
|
|
return SuccessResponse.new(idres.endpoint, message,
|
|
idres.signed_fields)
|
|
end
|
|
end
|
|
end
|
|
end
|