Skip to content

Commit

Permalink
🚚 Move SASL.authenticator registry from Net::IMAP
Browse files Browse the repository at this point in the history
The original methods (`Net::IMAP.authenticator` and
`Net::IMAP.add_authenticator`) both still work, by delegation to the
new methods.  But they have been deprecated and will issue warnings.
  • Loading branch information
nevans committed Sep 9, 2023
1 parent d264b33 commit 0e1e416
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 68 deletions.
5 changes: 1 addition & 4 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1074,10 +1074,7 @@ def starttls(options = {}, verify = true)
# completes. If the TaggedResponse to #authenticate includes updated
# capabilities, they will be cached.
def authenticate(mechanism, *creds, sasl_ir: true, **props, &callback)
authenticator = self.class.authenticator(mechanism,
*creds,
**props,
&callback)
authenticator = SASL.authenticator(mechanism, *creds, **props, &callback)
cmdargs = ["AUTHENTICATE", mechanism]
if sasl_ir && capable?("SASL-IR") && auth_capable?(mechanism) &&
SASL.initial_response?(authenticator)
Expand Down
76 changes: 20 additions & 56 deletions lib/net/imap/authenticators.rb
Original file line number Diff line number Diff line change
@@ -1,67 +1,31 @@
# frozen_string_literal: true

# Registry for SASL authenticators used by Net::IMAP.
# Backward compatible delegators from Net::IMAP to Net::IMAP::SASL.
module Net::IMAP::Authenticators

# Adds an authenticator for Net::IMAP#authenticate to use. +mechanism+ is the
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
# 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.
#
# See PlainAuthenticator, XOAuth2Authenticator, and DigestMD5Authenticator for
# examples.
def add_authenticator(auth_type, authenticator)
authenticators[auth_type] = authenticator
# Deprecated. Use Net::IMAP::SASL.add_authenticator instead.
def add_authenticator(...)
warn(
"%s.%s is deprecated. Use %s.%s instead." % [
Net::IMAP, __method__, Net::IMAP::SASL, __method__
],
uplevel: 1
)
Net::IMAP::SASL.add_authenticator(...)
end

# :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, ...)
auth = authenticators.fetch(mechanism.upcase) do
raise ArgumentError, 'unknown auth type - "%s"' % mechanism
end
auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
# Deprecated. Use Net::IMAP::SASL.authenticator instead.
def authenticator(...)
warn(
"%s.%s is deprecated. Use %s.%s instead." % [
Net::IMAP, __method__, Net::IMAP::SASL, __method__
],
uplevel: 1
)
Net::IMAP::SASL.authenticator(...)
end

private

def authenticators
@authenticators ||= {}
end

end

class Net::IMAP
extend Authenticators
add_authenticator "PLAIN", SASL::PlainAuthenticator
add_authenticator "XOAUTH2", SASL::XOAuth2Authenticator

add_authenticator "CRAM-MD5", SASL::CramMD5Authenticator # deprecated
add_authenticator "LOGIN", SASL::LoginAuthenticator # deprecated
add_authenticator "DIGEST-MD5", SASL::DigestMD5Authenticator # deprecated
Net::IMAP.extend self
end

class Net::IMAP
Expand Down
13 changes: 13 additions & 0 deletions lib/net/imap/sasl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,26 @@ module SASL
autoload :BidiStringError, sasl_stringprep_rb

sasl_dir = File.expand_path("sasl", __dir__)
autoload :Authenticators, "#{sasl_dir}/authenticators"

autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"

autoload :CramMD5Authenticator, "#{sasl_dir}/cram_md5_authenticator"
autoload :DigestMD5Authenticator, "#{sasl_dir}/digest_md5_authenticator"
autoload :LoginAuthenticator, "#{sasl_dir}/login_authenticator"

# Returns the default global SASL::Authenticators instance.
def self.authenticators
@authenticators ||= Authenticators.new(use_defaults: true)
end

# Delegates to ::authenticators. See Authenticators#authenticator.
def self.authenticator(...) authenticators.authenticator(...) end

# Delegates to ::authenticators. See Authenticators#add_authenticator.
def self.add_authenticator(...) authenticators.add_authenticator(...) end

module_function

# See Net::IMAP::StringPrep::SASLprep#saslprep.
Expand Down
92 changes: 92 additions & 0 deletions lib/net/imap/sasl/authenticators.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# frozen_string_literal: true

module Net::IMAP::SASL

# Registry for SASL authenticators
#
# Registered authenticators must respond to +#new+ or +#call+ (e.g. a class or
# a proc), receiving any credentials and options and returning an
# authenticator instance. The returned object represents a single
# authentication exchange and <em>must not</em> be reused for multiple
# authentication attempts.
#
# An authenticator instance object must respond to +#process+, receiving the
# server's challenge and returning the client's response. Optionally, it may
# also respond to +#initial_response?+ and +#done?+. When
# +#initial_response?+ returns +true+, +#process+ may be called the first
# time with +nil+. When +#done?+ returns +false+, the exchange is incomplete
# and an exception should be raised if the exchange terminates prematurely.
#
# See the source for PlainAuthenticator, XOAuth2Authenticator, and
# ScramSHA1Authenticator for examples.
class Authenticators

# Create a new Authenticators registry.
#
# This class is usually not instantiated directly. Use SASL.authenticators
# to reuse the default global registry.
#
# By default, the registry will be empty--without any registrations. When
# +add_defaults+ is +true+, authenticators for all standard mechanisms will
# be registered.
#
def initialize(use_defaults: false)
@authenticators = {}
if use_defaults
add_authenticator "PLAIN", PlainAuthenticator
add_authenticator "XOAUTH2", XOAuth2Authenticator
add_authenticator "LOGIN", LoginAuthenticator # deprecated
add_authenticator "CRAM-MD5", CramMD5Authenticator # deprecated
add_authenticator "DIGEST-MD5", DigestMD5Authenticator # deprecated
end
end

# Returns the names of all registered SASL mechanisms.
def names; @authenticators.keys end

# :call-seq:
# add_authenticator(mechanism)
# add_authenticator(mechanism, authenticator_class)
# add_authenticator(mechanism, authenticator_proc)
#
# Registers an authenticator for #authenticator to use. +mechanism+ is the
# name of the
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
# implemented by +authenticator_class+ (for instance, <tt>"PLAIN"</tt>).
#
# If +mechanism+ refers to an existing authenticator, a warning will be
# printed and the old authenticator will be replaced.
#
# When only a single argument is given, the authenticator class will be
# lazily loaded from <tt>Net::IMAP::SASL::#{name}Authenticator</tt> (case is
# preserved and non-alphanumeric characters are removed..
def add_authenticator(auth_type, authenticator)
@authenticators[auth_type] = authenticator
end

# :call-seq:
# authenticator(mechanism, ...) -> auth_session
#
# Builds an authenticator instance using the authenticator registered to
# +mechanism+. The returned object represents a single authentication
# exchange and <em>must not</em> be reused for multiple authentication
# attempts.
#
# All arguments (except +mechanism+) are forwarded to the registered
# authenticator's +#new+ or +#call+ method. Each authenticator must
# document its own arguments.
#
# [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.
def authenticator(mechanism, ...)
auth = @authenticators.fetch(mechanism.upcase) do
raise ArgumentError, 'unknown auth type - "%s"' % mechanism
end
auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
end

end

end
22 changes: 14 additions & 8 deletions test/net/imap/test_imap_authenticators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@

class IMAPAuthenticatorsTest < Test::Unit::TestCase

def test_net_imap_authenticator_deprecated
assert_warn(/Net::IMAP\.authenticator .+deprecated./) do
Net::IMAP.authenticator("PLAIN", "user", "pass")
end
end

# ----------------------
# PLAIN
# ----------------------

def plain(...) Net::IMAP.authenticator("PLAIN", ...) end
def plain(...) Net::IMAP::SASL.authenticator("PLAIN", ...) end

def test_plain_authenticator_matches_mechanism
assert_kind_of(Net::IMAP::SASL::PlainAuthenticator, plain("user", "pass"))
Expand All @@ -36,7 +42,7 @@ def test_plain_no_null_chars
# XOAUTH2
# ----------------------

def xoauth2(...) Net::IMAP.authenticator("XOAUTH2", ...) end
def xoauth2(...) Net::IMAP::SASL.authenticator("XOAUTH2", ...) end

def test_xoauth2_authenticator_matches_mechanism
assert_kind_of(Net::IMAP::SASL::XOAuth2Authenticator, xoauth2("user", "tok"))
Expand All @@ -59,7 +65,7 @@ def test_xoauth2_supports_initial_response
# ----------------------

def login(*args, warn_deprecation: false, **kwargs, &block)
Net::IMAP.authenticator(
Net::IMAP::SASL.authenticator(
"LOGIN", *args, warn_deprecation: warn_deprecation, **kwargs, &block
)
end
Expand All @@ -74,7 +80,7 @@ def test_login_does_not_support_initial_response

def test_login_authenticator_deprecated
assert_warn(/LOGIN.+deprecated.+PLAIN/) do
Net::IMAP.authenticator("LOGIN", "user", "pass")
Net::IMAP::SASL.authenticator("LOGIN", "user", "pass")
end
end

Expand All @@ -89,7 +95,7 @@ def test_login_responses
# ----------------------

def cram_md5(*args, warn_deprecation: false, **kwargs, &block)
Net::IMAP.authenticator(
Net::IMAP::SASL.authenticator(
"CRAM-MD5", *args, warn_deprecation: warn_deprecation, **kwargs, &block
)
end
Expand All @@ -104,7 +110,7 @@ def test_cram_md5_does_not_support_initial_response

def test_cram_md5_authenticator_deprecated
assert_warn(/CRAM-MD5.+deprecated./) do
Net::IMAP.authenticator("CRAM-MD5", "user", "pass")
Net::IMAP::SASL.authenticator("CRAM-MD5", "user", "pass")
end
end

Expand All @@ -119,7 +125,7 @@ def test_cram_md5_authenticator
# ----------------------

def digest_md5(*args, warn_deprecation: false, **kwargs, &block)
Net::IMAP.authenticator(
Net::IMAP::SASL.authenticator(
"DIGEST-MD5", *args, warn_deprecation: warn_deprecation, **kwargs, &block
)
end
Expand All @@ -131,7 +137,7 @@ def test_digest_md5_authenticator_matches_mechanism

def test_digest_md5_authenticator_deprecated
assert_warn(/DIGEST-MD5.+deprecated.+RFC6331/) do
Net::IMAP.authenticator("DIGEST-MD5", "user", "pass")
Net::IMAP::SASL.authenticator("DIGEST-MD5", "user", "pass")
end
end

Expand Down

0 comments on commit 0e1e416

Please sign in to comment.