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