consumer.rb
395 lines
| 15.5 KiB
| text/x-ruby
|
RubyLexer
|
r2376 | 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 | ||||