class OpenID::Consumer::AssociationManager

An object that manages creating and storing associations for an OpenID provider endpoint

Public Class Methods

create_session(session_type) click to toggle source
# File lib/openid/consumer/associationmanager.rb, line 89
def self.create_session(session_type)
  case session_type
  when 'no-encryption'
    NoEncryptionSession.new
  when 'DH-SHA1'
    DiffieHellmanSHA1Session.new
  when 'DH-SHA256'
    DiffieHellmanSHA256Session.new
  else
    raise ArgumentError, "Unknown association session type: "                                "#{session_type.inspect}"
  end
end
new(store, server_url, compatibility_mode=false, negotiator=nil) click to toggle source
# File lib/openid/consumer/associationmanager.rb, line 103
def initialize(store, server_url, compatibility_mode=false,
               negotiator=nil)
  @store = store
  @server_url = server_url
  @compatibility_mode = compatibility_mode
  @negotiator = negotiator || DefaultNegotiator
end

Protected Class Methods

extract_expires_in(message) click to toggle source
# File lib/openid/consumer/associationmanager.rb, line 272
def self.extract_expires_in(message)
  # expires_in should be a base-10 string.
  expires_in_str = message.get_arg(OPENID_NS, 'expires_in', NO_DEFAULT)
  if !(/\A\d+\Z/ =~ expires_in_str)
    raise ProtocolError, "Invalid expires_in field: #{expires_in_str}"
  end
  expires_in_str.to_i
end

Public Instance Methods

get_association() click to toggle source
# File lib/openid/consumer/associationmanager.rb, line 111
def get_association
  if @store.nil?
    return nil
  end

  assoc = @store.get_association(@server_url)
  if assoc.nil? || assoc.expires_in <= 0
    assoc = negotiate_association
    if !assoc.nil?
      @store.store_association(@server_url, assoc)
    end
  end

  return assoc
end
negotiate_association() click to toggle source
# File lib/openid/consumer/associationmanager.rb, line 127
def negotiate_association
  assoc_type, session_type = @negotiator.get_allowed_type
  begin
    return request_association(assoc_type, session_type)
  rescue ServerError => why
    supported_types = extract_supported_association_type(why, assoc_type)
    if !supported_types.nil?
      # Attempt to create an association from the assoc_type and
      # session_type that the server told us it supported.
      assoc_type, session_type = supported_types
      begin
        return request_association(assoc_type, session_type)
      rescue ServerError => why
        Util.log("Server #{@server_url} refused its suggested "                         "association type: session_type=#{session_type}, "                         "assoc_type=#{assoc_type}")
        return nil
      end
    end
  rescue InvalidOpenIDNamespace
    Util.log("Server #{@server_url} returned a malformed association "                     "response.  Falling back to check_id mode for this request.")
    return nil
  end
end

Protected Instance Methods

create_associate_request(assoc_type, session_type) click to toggle source

Create an association request for the given assoc_type and session_type. Returns a pair of the association session object and the request message that will be sent to the server.

# File lib/openid/consumer/associationmanager.rb, line 215
def create_associate_request(assoc_type, session_type)
  assoc_session = self.class.create_session(session_type)
  args = {
    'mode' => 'associate',
    'assoc_type' => assoc_type,
  }

  if !@compatibility_mode
    args['ns'] = OPENID2_NS
  end

  # Leave out the session type if we're in compatibility mode
  # *and* it's no-encryption.
  if !@compatibility_mode ||
      assoc_session.class.session_type != 'no-encryption'
    args['session_type'] = assoc_session.class.session_type
  end

  args.merge!(assoc_session.get_request)
  message = Message.from_openid_args(args)
  return assoc_session, message
end
extract_association(assoc_response, assoc_session) click to toggle source

Attempt to extract an association from the response, given the association response message and the established association session.

# File lib/openid/consumer/associationmanager.rb, line 284
def extract_association(assoc_response, assoc_session)
  # Extract the common fields from the response, raising an
  # exception if they are not found
  assoc_type = assoc_response.get_arg(OPENID_NS, 'assoc_type',
                                      NO_DEFAULT)
  assoc_handle = assoc_response.get_arg(OPENID_NS, 'assoc_handle',
                                        NO_DEFAULT)
  expires_in = self.class.extract_expires_in(assoc_response)

  # OpenID 1 has funny association session behaviour.
  if assoc_response.is_openid1
      session_type = get_openid1_session_type(assoc_response)
  else
    session_type = assoc_response.get_arg(OPENID2_NS, 'session_type',
                                          NO_DEFAULT)
  end

  # Session type mismatch
  if assoc_session.class.session_type != session_type
    if (assoc_response.is_openid1 and session_type == 'no-encryption')
      # In OpenID 1, any association request can result in a
      # 'no-encryption' association response. Setting
      # assoc_session to a new no-encryption session should
      # make the rest of this function work properly for
      # that case.
      assoc_session = NoEncryptionSession.new
    else
      # Any other mismatch, regardless of protocol version
      # results in the failure of the association session
      # altogether.
      raise ProtocolError, "Session type mismatch. Expected "                                  "#{assoc_session.class.session_type}, got "                                  "#{session_type}"
    end
  end

  # Make sure assoc_type is valid for session_type
  if !assoc_session.class.allowed_assoc_types.member?(assoc_type)
    raise ProtocolError, "Unsupported assoc_type for session "                                "#{assoc_session.class.session_type} "                                "returned: #{assoc_type}"
  end

  # Delegate to the association session to extract the secret
  # from the response, however is appropriate for that session
  # type.
  begin
    secret = assoc_session.extract_secret(assoc_response)
  rescue Message::KeyNotFound, ArgumentError => why
    raise ProtocolError, "Malformed response for "                                "#{assoc_session.class.session_type} "                                "session: #{why.message}"
  end


  return Association.from_expires_in(expires_in, assoc_handle, secret,
                                     assoc_type)
end
extract_supported_association_type(server_error, assoc_type) click to toggle source
# File lib/openid/consumer/associationmanager.rb, line 154
def extract_supported_association_type(server_error, assoc_type)
  # Any error message whose code is not 'unsupported-type' should
  # be considered a total failure.
  if (server_error.error_code != 'unsupported-type' or
      server_error.message.is_openid1)
    Util.log("Server error when requesting an association from "                    "#{@server_url}: #{server_error.error_text}")
    return nil
  end

  # The server didn't like the association/session type that we
  # sent, and it sent us back a message that might tell us how to
  # handle it.
  Util.log("Unsupported association type #{assoc_type}: "                  "#{server_error.error_text}")

  # Extract the session_type and assoc_type from the error message
  assoc_type = server_error.message.get_arg(OPENID_NS, 'assoc_type')
  session_type = server_error.message.get_arg(OPENID_NS, 'session_type')

  if assoc_type.nil? or session_type.nil?
    Util.log("Server #{@server_url} responded with unsupported "                    "association session but did not supply a fallback.")
    return nil
  elsif !@negotiator.allowed?(assoc_type, session_type)
    Util.log("Server sent unsupported session/association type: "                    "session_type=#{session_type}, assoc_type=#{assoc_type}")
    return nil
  else
    return [assoc_type, session_type]
  end
end
get_openid1_session_type(assoc_response) click to toggle source

Given an association response message, extract the OpenID 1.X session type. Returns the association type for this message

This function mostly takes care of the 'no-encryption' default behavior in OpenID 1.

If the association type is plain-text, this function will return 'no-encryption'

# File lib/openid/consumer/associationmanager.rb, line 246
def get_openid1_session_type(assoc_response)
  # If it's an OpenID 1 message, allow session_type to default
  # to nil (which signifies "no-encryption")
  session_type = assoc_response.get_arg(OPENID_NS, 'session_type')

  # Handle the differences between no-encryption association
  # respones in OpenID 1 and 2:

  # no-encryption is not really a valid session type for
  # OpenID 1, but we'll accept it anyway, while issuing a
  # warning.
  if session_type == 'no-encryption'
    Util.log("WARNING: #{@server_url} sent 'no-encryption'"                    "for OpenID 1.X")

  # Missing or empty session type is the way to flag a
  # 'no-encryption' response. Change the session type to
  # 'no-encryption' so that it can be handled in the same
  # way as OpenID 2 'no-encryption' respones.
  elsif session_type == '' || session_type.nil?
    session_type = 'no-encryption'
  end

  return session_type
end
request_association(assoc_type, session_type) click to toggle source

Make and process one association request to this endpoint's OP endpoint URL. Returns an association object or nil if the association processing failed. Raises ServerError when the remote OpenID server returns an error.

# File lib/openid/consumer/associationmanager.rb, line 191
def request_association(assoc_type, session_type)
  assoc_session, args = create_associate_request(assoc_type, session_type)

  begin
    response = OpenID.make_kv_post(args, @server_url)
    return extract_association(response, assoc_session)
  rescue HTTPStatusError => why
    Util.log("Got HTTP status error when requesting association: #{why}")
    return nil
  rescue Message::KeyNotFound => why
    Util.log("Missing required parameter in response from "                    "#{@server_url}: #{why}")
    return nil

  rescue ProtocolError => why
    Util.log("Protocol error processing response from #{@server_url}: "                    "#{why}")
    return nil
  end
end