diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index e879d594..4e689b3e 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -577,43 +577,58 @@ def starttls(options = {}, verify = true)
end
end
+ ##
+ # :call-seq:
+ # authenticate(mechanism, ...) -> ok_resp
+ # authenticate(mechanism) -> ok_resp
+ # authenticate(mechanism, username, password) -> ok_resp
+ # authenticate(mechanism, authcid, secret, authzid) -> ok_resp
+ # authenticate(mechanism, *credentials) -> ok_resp
+ # authenticate(mechanism, **properties_and_callbacks) -> ok_resp
+ # authenticate(mechanism) {|name, auth_ctx| prop_value } -> ok_resp
+ # authenticate(mech, *creds, **props) {|prop, auth| val } -> ok_resp
+ #
# Sends an {AUTHENTICATE command [IMAP4rev1
# ยง6.2.2]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.2.2]) to
# authenticate the client.
#
- # The +auth_type+ parameter is a string that
- # represents the authentication mechanism to be used. Currently Net::IMAP
- # supports the following mechanisms:
+ # +mechanism+ is the name of the \SASL authentication mechanism to be used.
+ # All other arguments are forwarded to the authenticator for the requested
+ # mechanism. The listed call signatures are suggestions. The
+ # documentation for each individual mechanism must be consulted for its
+ # specific parameters.
#
- # PLAIN:: Login using cleartext user and password. Secure with TLS.
- # See PlainAuthenticator.
- # CRAM-MD5:: DEPRECATED: Use PLAIN (or DIGEST-MD5) with TLS.
- # DIGEST-MD5:: DEPRECATED by RFC6331. Must be secured using TLS.
- # See DigestMD5Authenticator.
- # LOGIN:: DEPRECATED: Use PLAIN.
- #
- # Most mechanisms require two args: authentication identity (e.g. username)
- # and credentials (e.g. a password). But each mechanism requires and allows
- # different arguments; please consult the documentation for the specific
- # mechanisms you are using. Several obsolete mechanisms are available
- # for backwards compatibility. Using deprecated mechanisms will issue
- # warnings.
- #
- # Servers do not support all mechanisms and clients must not attempt to use
- # a mechanism unless "AUTH=#{mechanism}" is listed as a #capability.
- # Clients must not attempt to authenticate or #login when +LOGINDISABLED+ is
- # listed with the capabilities. Server capabilities, especially auth
- # mechanisms, do change after calling #starttls so they need to be checked
- # again.
+ # In general, all of a mechanism's properties can be set by keyword
+ # argument or callback, but mechanisms may allow common properties to be set
+ # with positional arguments. See SASL::Authenticator@Properties and
+ # SASL::Authenticator@Callbacks for more details.
#
- # For example:
+ # An exception Net::IMAP::NoResponseError is raised if authentication fails.
#
- # imap.authenticate('PLAIN', user, password)
+ # ==== Supported SASL Mechanisms
#
- # A Net::IMAP::NoResponseError is raised if authentication fails.
+ # Net::IMAP currently supports the following mechanisms:
+ #
+ # PLAIN:: Login using clear-text user and password. Secure with TLS.
+ # See SASL::PlainAuthenticator.
+ # XOAUTH2:: Login using a username and OAuth2 access token. Non-standard
+ # and obsoleted by +OAUTHBEARER+, but still widely supported.
+ # See SASL::XOAuth2Authenticator.
#
- # See Net::IMAP::Authenticators for more information on plugging in your
- # own authenticator.
+ # See Net::IMAP::Authenticators for information on plugging in
+ # authenticators for other mechanisms. See the {SASL mechanism
+ # registry}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
+ # for information on these and other SASL mechanisms.
+ #
+ # ===== Deprecated mechanisms
+ #
+ # Obsolete mechanisms are available for backwards compatibility.
+ # Using a deprecated mechanism will print a warning.
+ #
+ # DIGEST-MD5:: DEPRECATED by RFC6331. Must be secured using TLS.
+ # See SASL::DigestMD5Authenticator.
+ # CRAM-MD5:: DEPRECATED: Use +PLAIN+ (or SCRAM-*)
+ # LOGIN:: DEPRECATED: Use +PLAIN+ with TLS.
#
# ==== Capabilities
#
@@ -626,9 +641,35 @@ def starttls(options = {}, verify = true)
# Server capabilities may change after #starttls, #login, and #authenticate.
# Any cached capabilities must be invalidated when this method completes.
#
- def authenticate(auth_type, *args)
- authenticator = self.class.authenticator(auth_type, *args)
- send_command("AUTHENTICATE", auth_type) do |resp|
+ # ==== Example
+ # Because unhandled keyword arguments are ignored, the same config can be
+ # used for multiple authenticator types.
+ # password = nil # saved locally, so we don't ask more than once
+ # creds = {
+ # authcid: username,
+ # password: proc { password ||= ui.prompt_for_password },
+ # oauth2_token: proc { kms.lookup(username, :access_token) },
+ # }
+ # capa = imap.capability
+ # if capa.include? "LOGINDISABLED"
+ # raise "the server has disabled login"
+ # elsif oauth2_token and capa.include? "AUTH=OAUTHBEARER"
+ # imap.authenticate "OAUTHBEARER", **creds # authcid, oauth2_token
+ # elsif oauth2_token and capa.include? "AUTH=XOAUTH2"
+ # imap.authenticate "XOAUTH2", **creds # authcid, oauth2_token
+ # elsif password and capa.include? "AUTH=SCRAM-SHA-256"
+ # imap.authenticate "SCRAM-SHA-256", **creds # authcid, password
+ # elsif password and capa.include? "AUTH=PLAIN"
+ # imap.authenticate "PLAIN", **creds # authcid, password
+ # elsif password and capa.include? "AUTH=DIGEST-MD5"
+ # imap.authenticate "DIGEST-MD5", **creds # authcid, password
+ # else
+ # raise "no acceptable authentication mechanism is available"
+ # end
+ #
+ def authenticate(mechanism, *args, **props, &cb)
+ authenticator = self.class.authenticator(mechanism, *args, **props, &cb)
+ send_command("AUTHENTICATE", mechanism) do |resp|
if resp.instance_of?(ContinuationRequest)
data = authenticator.process(resp.data.text.unpack("m")[0])
s = [data].pack("m0")
diff --git a/lib/net/imap/authenticators.rb b/lib/net/imap/authenticators.rb
index d6f5ff69..59f72c1d 100644
--- a/lib/net/imap/authenticators.rb
+++ b/lib/net/imap/authenticators.rb
@@ -3,22 +3,75 @@
# Registry for SASL authenticators used by Net::IMAP.
module Net::IMAP::Authenticators
- # Adds an authenticator for use with Net::IMAP#authenticate. +auth_type+ is the
+ # Adds an authenticator for Net::IMAP#authenticate to use. +mechanism+ is the
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
- # supported by +authenticator+ (for instance, "+PLAIN+"). The +authenticator+
- # is an object which defines a +#process+ method to handle authentication with
- # the server. See Net::IMAP::PlainAuthenticator, Net::IMAP::LoginAuthenticator,
- # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator for
- # examples.
- #
- # If +auth_type+ refers to an existing authenticator, it will be
- # replaced by the new one.
+ # implemented by +authenticator+ (for instance, "PLAIN").
+ #
+ # If +mechanism+ refers to an existing authenticator, a warning will be
+ # printed and the old authenticator will be replaced.
+ #
+ # The +authenticator+ must respond to +#new+ (or #call), receiving the
+ # authenticator configuration and return a configured authentication session.
+ # The authenticator session must respond to +#process+, receiving the server's
+ # challenge and returning the client's response. See PlainAuthenticator,
+ # XOauth2Authenticator, DigestMD5Authenticator, etc for examples.
def add_authenticator(auth_type, authenticator)
authenticators[auth_type] = authenticator
end
- # Builds an authenticator for Net::IMAP#authenticate. +args+ will be passed
- # directly to the chosen authenticator's +#initialize+.
+ # :call-seq:
+ # authenticator(mechanism, ...) -> authenticator
+ # authenticator(mechanism) -> authenticator
+ # authenticator(mechanism, username, password) -> authenticator
+ # authenticator(mechanism, authcid, secret, authzid) -> authenticator
+ # authenticator(mechanism, *credentials) -> authenticator
+ # authenticator(mechanism, **properties_and_callbacks) -> authenticator
+ # authenticator(mechanism) {|name, auth_ctx| prop_value } -> authenticator
+ # authenticator(mech, *creds, **props) {|prop, auth| val } -> authenticator
+ #
+ # Builds a new authentication session context for +mechanism+.
+ #
+ # [Note]
+ # This method is intended for internal use by connection protocol code only.
+ # Protocol client users should see refer to their client's documentation,
+ # e.g. Net::IMAP#authenticate for Net::IMAP.
+ #
+ # The returned object represents a single authentication exchange and must
+ # not be reused for multiple authentication attempts.
+ #
+ # The documented call signatures for this method are recommendations for
+ # authenticator implementors. All arguments (other than +mechanism+) are
+ # forwarded to the registered authenticator's +#new+ (or +#call+) method, and
+ # each authenticator must document its own arguments.
+ #
+ # In general, mechanisms may be configured by positional arguments (convenient
+ # for common scenarios), keyword arguments (handles any static property), a
+ # callback, or a combination of the three. For example:
+ #
+ # # using positional parameters -- convenient for common scenarios
+ # sasl_exchange = authenticator("PLAIN", "username", "password")
+ # sasl_exchange.process(nil) # => "\0username\0password"
+ #
+ # # using keyword parameters -- can handle any static property
+ # sasl_exchange = authenticator(
+ # "PLAIN", authcid: "cid", password: "pass", authzid: "zid"
+ # )
+ # sasl_exchange.process(nil) # => "zid\0cid\0pass"
+ #
+ # # using a callback -- can be used for dynamic value lookup
+ # sasl_exchange = authenticator("PLAIN") do |prop, _|
+ # case prop
+ # when :authcid then prompt_for("Username? ")
+ # when :password then password_prompt
+ # when :authzid then prompt_for("User to act on behalf of? ")
+ # end
+ # end
+ #
+ # # can combine all three: callback > keyword > positional
+ # sasl_exchange = authenticator("PLAIN", "foo", authzid: "bar") do |prop, _|
+ # prop == :password and password_prompt
+ # end
+ #
def authenticator(mechanism, *authargs, **properties, &callback)
authenticator = authenticators.fetch(mechanism.upcase) do
raise ArgumentError, 'unknown auth type - "%s"' % mechanism