Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚚 Move and rename SASL authenticators #165

Merged
merged 2 commits into from
Sep 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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