Skip to content

Commit

Permalink
📚 Update docs for #authenticate, #login, etc...
Browse files Browse the repository at this point in the history
The documentation on these methods is meant to complement a new
SASL::Authenticator base class and new documentation on each of the
individual authenticator classes.  See #78 and #82.

That base class is added in another PR (#78), but the documentation for
these methods can be updated without it.

Also, somehow I misremembered `LOGINDISABLED`: it only applies to
`LOGIN`, not `AUTHENTICATE`!  This fixes that error.
  • Loading branch information
nevans committed Dec 21, 2022
1 parent 200757e commit 5343336
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 52 deletions.
121 changes: 79 additions & 42 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -602,49 +602,55 @@ def starttls(options = {}, verify = true)
end
end

# :call-seq:
# authenticate(mechanism, ...) -> ok_resp
# authenticate(mech, *creds, **props) {|prop, auth| val } -> ok_resp
# authenticate(mechanism, authnid, credentials, authzid=nil) -> ok_resp
# authenticate(mechanism, **properties) -> ok_resp
# authenticate(mechanism) {|propname, authctx| prop_value } -> 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:
#
# 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. <em>Several obsolete mechanisms are available
# for backwards compatibility. Using deprecated mechanisms will issue
# warnings.</em>
#
# 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.
# to authenticate the client. If successful, the connection enters the
# "_authenticated_" state.
#
# For example:
# +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. <em>The
# documentation for each individual mechanism must be consulted for its
# specific parameters.</em>
#
# imap.authenticate('PLAIN', user, password)
# An exception Net::IMAP::NoResponseError is raised if authentication fails.
#
# A Net::IMAP::NoResponseError is raised if authentication fails.
# Related: #login, #starttls
#
# See Net::IMAP::Authenticators for more information on plugging in your
# own authenticator.
# ==== Supported SASL Mechanisms
#
# Related: #login, #starttls
# +PLAIN+:: See PlainAuthenticator.
# Login using clear-text username and password.
#
# ==== Capabilities
# +XOAUTH2+:: See XOauth2Authenticator.
# Login using a username and OAuth2 access token.
# Non-standard and obsoleted by +OAUTHBEARER+, but widely
# supported.
#
# >>>
# *Deprecated:* <em>Obsolete mechanisms are available for backwards
# compatibility.</em>
#
# Clients MUST NOT attempt to #authenticate or #login when +LOGINDISABLED+
# is listed with the capabilities.
# For +DIGEST-MD5+ see DigestMD5Authenticator.
#
# For +LOGIN+, see LoginAuthenticator.
#
# For +CRAM-MD5+, see CramMD5Authenticator.
#
# <em>Using a deprecated mechanism will print a warning.</em>
#
# 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.
#
# ===== Capabilities
#
# Clients MUST NOT attempt to authenticate with a mechanism unless
# <tt>"AUTH=#{mechanism}"</tt> for that mechanism is a server capability.
Expand All @@ -654,9 +660,37 @@ def starttls(options = {}, verify = true)
# The TaggedResponse to #authenticate may include updated capabilities in
# its ResponseCode.
#
def authenticate(auth_type, *args)
authenticator = self.class.authenticator(auth_type, *args)
send_command("AUTHENTICATE", auth_type) do |resp|
# ===== Example
# If the authenticators ignore unhandled keyword arguments, the same config
# can be used for multiple mechanisms:
#
# password = nil # saved locally, so we don't ask more than once
# accesstok = nil # saved locally...
# creds = {
# authcid: username,
# password: proc { password ||= ui.prompt_for_password },
# oauth2_token: proc { accesstok ||= kms.fresh_access_token },
# }
# capa = imap.capability
# if capa.include? "AUTH=OAUTHBEARER"
# imap.authenticate "OAUTHBEARER", **creds # authcid, oauth2_token
# elsif capa.include? "AUTH=XOAUTH2"
# imap.authenticate "XOAUTH2", **creds # authcid, oauth2_token
# elsif capa.include? "AUTH=SCRAM-SHA-256"
# imap.authenticate "SCRAM-SHA-256", **creds # authcid, password
# elsif capa.include? "AUTH=PLAIN"
# imap.authenticate "PLAIN", **creds # authcid, password
# elsif capa.include? "AUTH=DIGEST-MD5"
# imap.authenticate "DIGEST-MD5", **creds # authcid, password
# elsif capa.include? "LOGINDISABLED"
# raise "the server has disabled login"
# else
# imap.login username, password
# 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")
Expand All @@ -668,16 +702,19 @@ def authenticate(auth_type, *args)

# Sends a {LOGIN command [IMAP4rev1 §6.2.3]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.2.3]
# to identify the client and carries the plaintext +password+ authenticating
# this +user+. Note that, unlike calling #authenticate with an +auth_type+
# of "LOGIN", #login does *not* use the LoginAuthenticator.
# this +user+. If successful, the connection enters the "_authenticated_"
# state.
#
# Using #authenticate is generally preferred over #login. The LOGIN command
# is not the same as #authenticate with the "LOGIN" +mechanism+.
#
# A Net::IMAP::NoResponseError is raised if authentication fails.
#
# Related: #authenticate, #starttls
#
# ==== Capabilities
# Clients MUST NOT attempt to #authenticate or #login when +LOGINDISABLED+
# is listed with the capabilities.
# Clients MUST NOT call #login if +LOGINDISABLED+ is listed with the
# capabilities.
#
# Server capabilities may change after #starttls, #login, and #authenticate.
# Cached capabilities _must_ be invalidated after this method completes.
Expand Down
40 changes: 30 additions & 10 deletions lib/net/imap/authenticators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,42 @@
# 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.
# implemented by +authenticator+ (for instance, <tt>"PLAIN"</tt>).
#
# 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.
#
# If +auth_type+ refers to an existing authenticator, it will be
# replaced by the new one.
# See PlainAuthenticator, XOauth2Authenticator, and DigestMD5Authenticator 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(mech, *creds, **props) {|prop, auth| val } -> authenticator
# authenticator(mechanism, authnid, creds, authzid=nil) -> authenticator
# authenticator(mechanism, **properties) -> authenticator
# authenticator(mechanism) {|propname, authctx| value } -> 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 call signatures documented 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.
#
# The returned object represents a single authentication exchange and <em>must
# not</em> be reused for multiple authentication attempts.
def authenticator(mechanism, *authargs, **properties, &callback)
authenticator = authenticators.fetch(mechanism.upcase) do
raise ArgumentError, 'unknown auth type - "%s"' % mechanism
Expand Down

0 comments on commit 5343336

Please sign in to comment.