Skip to content

Commit 6d03dac

Browse files
committed
📚 Document AUTHENTICATE command, Authenticators [🚧WIP:DRY]
These two pieces of documentation are meant to complement a new SASL::Authenticator base class and each of the individual authenticators.
1 parent 3dee045 commit 6d03dac

File tree

2 files changed

+136
-42
lines changed

2 files changed

+136
-42
lines changed

lib/net/imap.rb

Lines changed: 72 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -577,43 +577,58 @@ def starttls(options = {}, verify = true)
577577
end
578578
end
579579

580+
##
581+
# :call-seq:
582+
# authenticate(mechanism, ...) -> ok_resp
583+
# authenticate(mechanism) -> ok_resp
584+
# authenticate(mechanism, username, password) -> ok_resp
585+
# authenticate(mechanism, authcid, secret, authzid) -> ok_resp
586+
# authenticate(mechanism, *credentials) -> ok_resp
587+
# authenticate(mechanism, **properties_and_callbacks) -> ok_resp
588+
# authenticate(mechanism) {|name, auth_ctx| prop_value } -> ok_resp
589+
# authenticate(mech, *creds, **props) {|prop, auth| val } -> ok_resp
590+
#
580591
# Sends an {AUTHENTICATE command [IMAP4rev1
581592
# §6.2.2]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.2.2]) to
582593
# authenticate the client.
583594
#
584-
# The +auth_type+ parameter is a string that
585-
# represents the authentication mechanism to be used. Currently Net::IMAP
586-
# supports the following mechanisms:
595+
# +mechanism+ is the name of the \SASL authentication mechanism to be used.
596+
# All other arguments are forwarded to the authenticator for the requested
597+
# mechanism. The listed call signatures are suggestions. <em>The
598+
# documentation for each individual mechanism must be consulted for its
599+
# specific parameters.</em>
587600
#
588-
# PLAIN:: Login using cleartext user and password. Secure with TLS.
589-
# See PlainAuthenticator.
590-
# CRAM-MD5:: DEPRECATED: Use PLAIN (or DIGEST-MD5) with TLS.
591-
# DIGEST-MD5:: DEPRECATED by RFC6331. Must be secured using TLS.
592-
# See DigestMD5Authenticator.
593-
# LOGIN:: DEPRECATED: Use PLAIN.
594-
#
595-
# Most mechanisms require two args: authentication identity (e.g. username)
596-
# and credentials (e.g. a password). But each mechanism requires and allows
597-
# different arguments; please consult the documentation for the specific
598-
# mechanisms you are using. <em>Several obsolete mechanisms are available
599-
# for backwards compatibility. Using deprecated mechanisms will issue
600-
# warnings.</em>
601-
#
602-
# Servers do not support all mechanisms and clients must not attempt to use
603-
# a mechanism unless "AUTH=#{mechanism}" is listed as a #capability.
604-
# Clients must not attempt to authenticate or #login when +LOGINDISABLED+ is
605-
# listed with the capabilities. Server capabilities, especially auth
606-
# mechanisms, do change after calling #starttls so they need to be checked
607-
# again.
601+
# <em>In general</em>, all of a mechanism's properties can be set by keyword
602+
# argument or callback, but mechanisms may allow common properties to be set
603+
# with positional arguments. See SASL::Authenticator@Properties and
604+
# SASL::Authenticator@Callbacks for more details.
608605
#
609-
# For example:
606+
# An exception Net::IMAP::NoResponseError is raised if authentication fails.
610607
#
611-
# imap.authenticate('PLAIN', user, password)
608+
# ==== Supported SASL Mechanisms
612609
#
613-
# A Net::IMAP::NoResponseError is raised if authentication fails.
610+
# Net::IMAP currently supports the following mechanisms:
611+
#
612+
# PLAIN:: Login using clear-text user and password. Secure with TLS.
613+
# See SASL::PlainAuthenticator.
614+
# XOAUTH2:: Login using a username and OAuth2 access token. Non-standard
615+
# and obsoleted by +OAUTHBEARER+, but still widely supported.
616+
# See SASL::XOAuth2Authenticator.
614617
#
615-
# See Net::IMAP::Authenticators for more information on plugging in your
616-
# own authenticator.
618+
# See Net::IMAP::Authenticators for information on plugging in
619+
# authenticators for other mechanisms. See the {SASL mechanism
620+
# registry}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
621+
# for information on these and other SASL mechanisms.
622+
#
623+
# ===== Deprecated mechanisms
624+
#
625+
# <em>Obsolete mechanisms are available for backwards compatibility.
626+
# Using a deprecated mechanism will print a warning.</em>
627+
#
628+
# DIGEST-MD5:: DEPRECATED by RFC6331. Must be secured using TLS.
629+
# See SASL::DigestMD5Authenticator.
630+
# CRAM-MD5:: DEPRECATED: Use +PLAIN+ (or SCRAM-*)
631+
# LOGIN:: DEPRECATED: Use +PLAIN+ with TLS.
617632
#
618633
# ==== Capabilities
619634
#
@@ -626,9 +641,35 @@ def starttls(options = {}, verify = true)
626641
# Server capabilities may change after #starttls, #login, and #authenticate.
627642
# Any cached capabilities must be invalidated when this method completes.
628643
#
629-
def authenticate(auth_type, *args)
630-
authenticator = self.class.authenticator(auth_type, *args)
631-
send_command("AUTHENTICATE", auth_type) do |resp|
644+
# ==== Example
645+
# Because unhandled keyword arguments are ignored, the same config can be
646+
# used for multiple authenticator types.
647+
# password = nil # saved locally, so we don't ask more than once
648+
# creds = {
649+
# authcid: username,
650+
# password: proc { password ||= ui.prompt_for_password },
651+
# oauth2_token: proc { kms.lookup(username, :access_token) },
652+
# }
653+
# capa = imap.capability
654+
# if capa.include? "LOGINDISABLED"
655+
# raise "the server has disabled login"
656+
# elsif oauth2_token and capa.include? "AUTH=OAUTHBEARER"
657+
# imap.authenticate "OAUTHBEARER", **creds # authcid, oauth2_token
658+
# elsif oauth2_token and capa.include? "AUTH=XOAUTH2"
659+
# imap.authenticate "XOAUTH2", **creds # authcid, oauth2_token
660+
# elsif password and capa.include? "AUTH=SCRAM-SHA-256"
661+
# imap.authenticate "SCRAM-SHA-256", **creds # authcid, password
662+
# elsif password and capa.include? "AUTH=PLAIN"
663+
# imap.authenticate "PLAIN", **creds # authcid, password
664+
# elsif password and capa.include? "AUTH=DIGEST-MD5"
665+
# imap.authenticate "DIGEST-MD5", **creds # authcid, password
666+
# else
667+
# raise "no acceptable authentication mechanism is available"
668+
# end
669+
#
670+
def authenticate(mechanism, *args, **props, &cb)
671+
authenticator = self.class.authenticator(mechanism, *args, **props, &cb)
672+
send_command("AUTHENTICATE", mechanism) do |resp|
632673
if resp.instance_of?(ContinuationRequest)
633674
data = authenticator.process(resp.data.text.unpack("m")[0])
634675
s = [data].pack("m0")

lib/net/imap/authenticators.rb

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,75 @@
33
# Registry for SASL authenticators used by Net::IMAP.
44
module Net::IMAP::Authenticators
55

6-
# Adds an authenticator for use with Net::IMAP#authenticate. +auth_type+ is the
6+
# Adds an authenticator for Net::IMAP#authenticate to use. +mechanism+ is the
77
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
8-
# supported by +authenticator+ (for instance, "+PLAIN+"). The +authenticator+
9-
# is an object which defines a +#process+ method to handle authentication with
10-
# the server. See Net::IMAP::PlainAuthenticator, Net::IMAP::LoginAuthenticator,
11-
# Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator for
12-
# examples.
13-
#
14-
# If +auth_type+ refers to an existing authenticator, it will be
15-
# replaced by the new one.
8+
# implemented by +authenticator+ (for instance, <tt>"PLAIN"</tt>).
9+
#
10+
# If +mechanism+ refers to an existing authenticator, a warning will be
11+
# printed and the old authenticator will be replaced.
12+
#
13+
# The +authenticator+ must respond to +#new+ (or #call), receiving the
14+
# authenticator configuration and return a configured authentication session.
15+
# The authenticator session must respond to +#process+, receiving the server's
16+
# challenge and returning the client's response. See PlainAuthenticator,
17+
# XOauth2Authenticator, DigestMD5Authenticator, etc for examples.
1618
def add_authenticator(auth_type, authenticator)
1719
authenticators[auth_type] = authenticator
1820
end
1921

20-
# Builds an authenticator for Net::IMAP#authenticate. +args+ will be passed
21-
# directly to the chosen authenticator's +#initialize+.
22+
# :call-seq:
23+
# authenticator(mechanism, ...) -> authenticator
24+
# authenticator(mechanism) -> authenticator
25+
# authenticator(mechanism, username, password) -> authenticator
26+
# authenticator(mechanism, authcid, secret, authzid) -> authenticator
27+
# authenticator(mechanism, *credentials) -> authenticator
28+
# authenticator(mechanism, **properties_and_callbacks) -> authenticator
29+
# authenticator(mechanism) {|name, auth_ctx| prop_value } -> authenticator
30+
# authenticator(mech, *creds, **props) {|prop, auth| val } -> authenticator
31+
#
32+
# Builds a new authentication session context for +mechanism+.
33+
#
34+
# [Note]
35+
# This method is intended for internal use by connection protocol code only.
36+
# Protocol client users should see refer to their client's documentation,
37+
# e.g. Net::IMAP#authenticate for Net::IMAP.
38+
#
39+
# The returned object represents a single authentication exchange and <em>must
40+
# not</em> be reused for multiple authentication attempts.
41+
#
42+
# The documented call signatures for this method are recommendations for
43+
# authenticator implementors. All arguments (other than +mechanism+) are
44+
# forwarded to the registered authenticator's +#new+ (or +#call+) method, and
45+
# each authenticator must document its own arguments.
46+
#
47+
# In general, mechanisms may be configured by positional arguments (convenient
48+
# for common scenarios), keyword arguments (handles any static property), a
49+
# callback, or a combination of the three. For example:
50+
#
51+
# # using positional parameters -- convenient for common scenarios
52+
# sasl_exchange = authenticator("PLAIN", "username", "password")
53+
# sasl_exchange.process(nil) # => "\0username\0password"
54+
#
55+
# # using keyword parameters -- can handle any static property
56+
# sasl_exchange = authenticator(
57+
# "PLAIN", authcid: "cid", password: "pass", authzid: "zid"
58+
# )
59+
# sasl_exchange.process(nil) # => "zid\0cid\0pass"
60+
#
61+
# # using a callback -- can be used for dynamic value lookup
62+
# sasl_exchange = authenticator("PLAIN") do |prop, _|
63+
# case prop
64+
# when :authcid then prompt_for("Username? ")
65+
# when :password then password_prompt
66+
# when :authzid then prompt_for("User to act on behalf of? ")
67+
# end
68+
# end
69+
#
70+
# # can combine all three: callback > keyword > positional
71+
# sasl_exchange = authenticator("PLAIN", "foo", authzid: "bar") do |prop, _|
72+
# prop == :password and password_prompt
73+
# end
74+
#
2275
def authenticator(mechanism, *authargs, **properties, &callback)
2376
authenticator = authenticators.fetch(mechanism.upcase) do
2477
raise ArgumentError, 'unknown auth type - "%s"' % mechanism

0 commit comments

Comments
 (0)