Skip to content

Commit

Permalink
📚 Document AUTHENTICATE command, Authenticators [🚧WIP:DRY]
Browse files Browse the repository at this point in the history
These two pieces of documentation are meant to complement a new
SASL::Authenticator base class and each of the individual
authenticators.
  • Loading branch information
nevans committed Nov 23, 2022
1 parent 2b8255f commit dff5d5c
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 42 deletions.
103 changes: 72 additions & 31 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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. <em>The
# documentation for each individual mechanism must be consulted for its
# specific parameters.</em>
#
# 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.
# <em>In general</em>, 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
#
# <em>Obsolete mechanisms are available for backwards compatibility.
# Using a deprecated mechanism will print a warning.</em>
#
# 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
#
Expand All @@ -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")
Expand Down
75 changes: 64 additions & 11 deletions lib/net/imap/authenticators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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, <tt>"PLAIN"</tt>).
#
# 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 <em>must
# not</em> 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
Expand Down

0 comments on commit dff5d5c

Please sign in to comment.