diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index bc6b7bea..ba67df6a 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -1242,6 +1242,9 @@ def starttls(**options)
# +SASL-IR+ capability, below). Defaults to the #config value for
# {sasl_ir}[rdoc-ref:Config#sasl_ir], which defaults to +true+.
#
+ # The +registry+ kwarg can be used to select the mechanism implementation
+ # from a custom registry. See SASL.authenticator and SASL::Authenticators.
+ #
# All other arguments are forwarded to the registered SASL authenticator for
# the requested mechanism. The documentation for each individual
# mechanism must be consulted for its specific parameters.
diff --git a/lib/net/imap/sasl.rb b/lib/net/imap/sasl.rb
index 359ad390..985b08dd 100644
--- a/lib/net/imap/sasl.rb
+++ b/lib/net/imap/sasl.rb
@@ -114,8 +114,8 @@ module SASL
# messages has not passed integrity checks.
AuthenticationFailed = Class.new(Error)
- # Indicates that authentication cannot proceed because one of the server's
- # ended authentication prematurely.
+ # Indicates that authentication cannot proceed because the server ended
+ # authentication prematurely.
class AuthenticationIncomplete < AuthenticationFailed
# The success response from the server
attr_reader :response
@@ -159,7 +159,10 @@ def initialize(response, message = "authentication ended prematurely")
# Returns the default global SASL::Authenticators instance.
def self.authenticators; @authenticators ||= Authenticators.new end
- # Delegates to registry.new See Authenticators#new.
+ # Creates a new SASL authenticator, using SASL::Authenticators#new.
+ #
+ # +registry+ defaults to SASL.authenticators. All other arguments are
+ # forwarded to to registry.new.
def self.authenticator(*args, registry: authenticators, **kwargs, &block)
registry.new(*args, **kwargs, &block)
end
diff --git a/lib/net/imap/sasl/authentication_exchange.rb b/lib/net/imap/sasl/authentication_exchange.rb
index 3276580c..8a31f926 100644
--- a/lib/net/imap/sasl/authentication_exchange.rb
+++ b/lib/net/imap/sasl/authentication_exchange.rb
@@ -9,39 +9,70 @@ module SASL
# TODO: catch exceptions in #process and send #cancel_response.
# TODO: raise an error if the command succeeds after being canceled.
# TODO: use with more clients, to verify the API can accommodate them.
+ # TODO: pass ClientAdapter#service to SASL.authenticator
#
- # Create an AuthenticationExchange from a client adapter and a mechanism
- # authenticator:
- # def authenticate(mechanism, ...)
- # authenticator = SASL.authenticator(mechanism, ...)
- # SASL::AuthenticationExchange.new(
- # sasl_adapter, mechanism, authenticator
- # ).authenticate
- # end
- #
- # private
+ # An AuthenticationExchange represents a single attempt to authenticate
+ # a SASL client to a SASL server. It is created from a client adapter, a
+ # mechanism name, and a mechanism authenticator. When #authenticate is
+ # called, it will send the appropriate authenticate command to the server,
+ # returning the client response on success and raising an exception on
+ # failure.
#
- # def sasl_adapter = MyClientAdapter.new(self, &method(:send_command))
+ # In most cases, the client will not need to use
+ # SASL::AuthenticationExchange directly at all. Instead, use
+ # SASL::ClientAdapter#authenticate. If customizations are needed, the
+ # custom client adapter is probably the best place for that code.
#
- # Or delegate creation of the authenticator to ::build:
# def authenticate(...)
- # SASL::AuthenticationExchange.build(sasl_adapter, ...)
- # .authenticate
+ # MyClient::SASLAdapter.new(self).authenticate(...)
# end
#
- # As a convenience, ::authenticate combines ::build and #authenticate:
+ # SASL::ClientAdapter#authenticate delegates to ::authenticate, like so:
+ #
# def authenticate(...)
+ # sasl_adapter = MyClient::SASLAdapter.new(self)
# SASL::AuthenticationExchange.authenticate(sasl_adapter, ...)
# end
#
- # Likewise, ClientAdapter#authenticate delegates to #authenticate:
- # def authenticate(...) = sasl_adapter.authenticate(...)
+ # ::authenticate simply delegates to ::build and #authenticate, like so:
+ #
+ # def authenticate(...)
+ # sasl_adapter = MyClient::SASLAdapter.new(self)
+ # SASL::AuthenticationExchange
+ # .build(sasl_adapter, ...)
+ # .authenticate
+ # end
+ #
+ # And ::build delegates to SASL.authenticator and ::new, like so:
+ #
+ # def authenticate(mechanism, ...)
+ # sasl_adapter = MyClient::SASLAdapter.new(self)
+ # authenticator = SASL.authenticator(mechanism, ...)
+ # SASL::AuthenticationExchange
+ # .new(sasl_adapter, mechanism, authenticator)
+ # .authenticate
+ # end
#
class AuthenticationExchange
# Convenience method for build(...).authenticate
+ #
+ # See also: SASL::ClientAdapter#authenticate
def self.authenticate(...) build(...).authenticate end
- # Use +registry+ to override the global Authenticators registry.
+ # Convenience method to combine the creation of a new authenticator and
+ # a new Authentication exchange.
+ #
+ # +client+ must be an instance of SASL::ClientAdapter.
+ #
+ # +mechanism+ must be a SASL mechanism name, as a string or symbol.
+ #
+ # +sasl_ir+ allows or disallows sending an "initial response", depending
+ # also on whether the server capabilities, mechanism authenticator, and
+ # client adapter all support it. Defaults to +true+.
+ #
+ # +mechanism+, +args+, +kwargs+, and +block+ are all forwarded to
+ # SASL.authenticator. Use the +registry+ kwarg to override the global
+ # SASL::Authenticators registry.
def self.build(client, mechanism, *args, sasl_ir: true, **kwargs, &block)
authenticator = SASL.authenticator(mechanism, *args, **kwargs, &block)
new(client, mechanism, authenticator, sasl_ir: sasl_ir)
diff --git a/lib/net/imap/sasl/client_adapter.rb b/lib/net/imap/sasl/client_adapter.rb
index ea9e993a..8573ff64 100644
--- a/lib/net/imap/sasl/client_adapter.rb
+++ b/lib/net/imap/sasl/client_adapter.rb
@@ -19,31 +19,53 @@ module SASL
class ClientAdapter
include ProtocolAdapters::Generic
- attr_reader :client, :command_proc
+ # The client that handles communication with the protocol server.
+ attr_reader :client
# +command_proc+ can used to avoid exposing private methods on #client.
- # It should run a command with the arguments sent to it, yield each
- # continuation payload, respond to the server with the result of each
- # yield, and return the result. Non-successful results *MUST* raise an
- # exception. Exceptions in the block *MUST* cause the command to fail.
+ # It's value is set by the block that is passed to ::new, and it is used
+ # by the default implementation of #run_command. Subclasses that
+ # override #run_command may use #command_proc for any other purpose they
+ # find useful.
#
- # Subclasses that override #run_command may use #command_proc for
- # other purposes.
+ # In the default implementation of #run_command, command_proc is called
+ # with the protocols authenticate +command+ name, the +mechanism+ name,
+ # an _optional_ +initial_response+ argument, and a +continuations+
+ # block. command_proc must run the protocol command with the arguments
+ # sent to it, _yield_ the payload of each continuation, respond to the
+ # continuation with the result of each _yield_, and _return_ the
+ # command's successful result. Non-successful results *MUST* raise
+ # an exception.
+ attr_reader :command_proc
+
+ # By default, this simply sets the #client and #command_proc attributes.
+ # Subclasses may override it, for example: to set the appropriate
+ # command_proc automatically.
def initialize(client, &command_proc)
@client, @command_proc = client, command_proc
end
- # Delegates to AuthenticationExchange.authenticate.
+ # Attempt to authenticate #client to the server.
+ #
+ # By default, this simply delegates to
+ # AuthenticationExchange.authenticate.
def authenticate(...) AuthenticationExchange.authenticate(self, ...) end
- # Do the protocol and server both support an initial response?
+ # Do the protocol, server, and client all support an initial response?
+ #
+ # By default, this simply delegates to client.sasl_ir_capable?.
def sasl_ir_capable?; client.sasl_ir_capable? end
# Does the server advertise support for the mechanism?
+ #
+ # By default, this simply delegates to client.auth_capable?.
def auth_capable?(mechanism); client.auth_capable?(mechanism) end
- # Runs the authenticate command with +mechanism+ and +initial_response+.
- # When +initial_response+ is nil, an initial response must NOT be sent.
+ # Calls command_proc with +command_name+ (see
+ # SASL::ProtocolAdapters::Generic#command_name),
+ # +mechanism+, +initial_response+, and a +continuations_handler+ block.
+ # The +initial_response+ is optional; when it's nil, it won't be sent to
+ # command_proc.
#
# Yields each continuation payload, responds to the server with the
# result of each yield, and returns the result. Non-successful results
@@ -51,10 +73,10 @@ def auth_capable?(mechanism); client.auth_capable?(mechanism) end
# command to fail.
#
# Subclasses that override this may use #command_proc differently.
- def run_command(mechanism, initial_response = nil, &block)
+ def run_command(mechanism, initial_response = nil, &continuations_handler)
command_proc or raise Error, "initialize with block or override"
args = [command_name, mechanism, initial_response].compact
- command_proc.call(*args, &block)
+ command_proc.call(*args, &continuations_handler)
end
# Returns an array of server responses errors raised by run_command.
@@ -62,9 +84,13 @@ def run_command(mechanism, initial_response = nil, &block)
def response_errors; [] end
# Drop the connection gracefully.
+ #
+ # By default, this simply delegates to client.drop_connection.
def drop_connection; client.drop_connection end
# Drop the connection abruptly.
+ #
+ # By default, this simply delegates to client.drop_connection!.
def drop_connection!; client.drop_connection! end
end
end