Skip to content

Commit

Permalink
🔀 Merge pull request #165 from nevans/move-and-rename-sasl-authentica…
Browse files Browse the repository at this point in the history
…tors

🚚 Move and rename sasl authenticators
  • Loading branch information
nevans authored Sep 9, 2023
2 parents 4520aa8 + 0e1e416 commit aed3a57
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 82 deletions.
7 changes: 2 additions & 5 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1015,7 +1015,7 @@ def starttls(options = {}, verify = true)
# +PLAIN+:: See PlainAuthenticator.
# Login using clear-text username and password.
#
# +XOAUTH2+:: See XOauth2Authenticator.
# +XOAUTH2+:: See XOAuth2Authenticator.
# Login using a username and OAuth2 access token.
# Non-standard and obsoleted by +OAUTHBEARER+, but widely
# supported.
Expand Down 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
79 changes: 26 additions & 53 deletions lib/net/imap/authenticators.rb
Original file line number Diff line number Diff line change
@@ -1,64 +1,37 @@
# 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(...)
end

private

def authenticators
@authenticators ||= {}
# 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

Net::IMAP.extend self
end

Net::IMAP.extend Net::IMAP::Authenticators
class Net::IMAP
PlainAuthenticator = SASL::PlainAuthenticator # :nodoc:
deprecate_constant :PlainAuthenticator

require_relative "authenticators/plain"

require_relative "authenticators/login"
require_relative "authenticators/cram_md5"
require_relative "authenticators/digest_md5"
require_relative "authenticators/xoauth2"
XOauth2Authenticator = SASL::XOAuth2Authenticator # :nodoc:
deprecate_constant :XOauth2Authenticator
end
21 changes: 21 additions & 0 deletions lib/net/imap/sasl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ module SASL
autoload :ProhibitedCodepoint, sasl_stringprep_rb
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# Additionally, RFC8314[https://tools.ietf.org/html/rfc8314] discourage the use
# of cleartext and recommends TLS version 1.2 or greater be used for all
# traffic. With TLS +CRAM-MD5+ is okay, but so is +PLAIN+
class Net::IMAP::CramMD5Authenticator
class Net::IMAP::SASL::CramMD5Authenticator
def process(challenge)
digest = hmac_md5(challenge, @password)
return @user + " " + digest
Expand Down Expand Up @@ -47,5 +47,4 @@ def hmac_md5(text, key)
return Digest::MD5.hexdigest(k_opad + digest)
end

Net::IMAP.add_authenticator "CRAM-MD5", self
end
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# "+DIGEST-MD5+" has been deprecated by
# {RFC6331}[https://tools.ietf.org/html/rfc6331] and should not be relied on for
# security. It is included for compatibility with existing servers.
class Net::IMAP::DigestMD5Authenticator
class Net::IMAP::SASL::DigestMD5Authenticator
def process(challenge)
case @stage
when STAGE_ONE
Expand Down Expand Up @@ -111,5 +111,4 @@ def qdval(k, v)
end
end

Net::IMAP.add_authenticator "DIGEST-MD5", self
end
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# compatibility with existing servers. See
# {draft-murchison-sasl-login}[https://www.iana.org/go/draft-murchison-sasl-login]
# for both specification and deprecation.
class Net::IMAP::LoginAuthenticator
class Net::IMAP::SASL::LoginAuthenticator
def process(data)
case @state
when STATE_USER
Expand All @@ -42,5 +42,4 @@ def initialize(user, password, warn_deprecation: true, **_ignored)
@state = STATE_USER
end

Net::IMAP.add_authenticator "LOGIN", self
end
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# RFC8314[https://tools.ietf.org/html/rfc8314] recommends TLS version 1.2 or
# greater be used for all traffic, and deprecate cleartext access ASAP. +PLAIN+
# can be secured by TLS encryption.
class Net::IMAP::PlainAuthenticator
class Net::IMAP::SASL::PlainAuthenticator

def initial_response?; true end

Expand Down Expand Up @@ -39,5 +39,4 @@ def initialize(username, password, authzid: nil)
@authzid = authzid
end

Net::IMAP.add_authenticator "PLAIN", self
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class Net::IMAP::XOauth2Authenticator
class Net::IMAP::SASL::XOAuth2Authenticator

def initial_response?; true end

Expand All @@ -19,5 +19,4 @@ def build_oauth2_string(user, oauth2_token)
format("user=%s\1auth=Bearer %s\1\1", user, oauth2_token)
end

Net::IMAP.add_authenticator 'XOAUTH2', self
end
Loading

0 comments on commit aed3a57

Please sign in to comment.