Skip to content

✨ Minor updates to SASL::Authenticators API #184

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

Merged
merged 6 commits into from
Sep 29, 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
32 changes: 21 additions & 11 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1251,21 +1251,19 @@ def authenticate(mechanism, *creds, sasl_ir: true, **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)
authenticator.respond_to?(:initial_response?) &&
authenticator.initial_response?
response = authenticator.process(nil)
cmdargs << (response.empty? ? "=" : [response].pack("m0"))
end
result = send_command(*cmdargs) do |resp|
if resp.instance_of?(ContinuationRequest)
challenge = resp.data.text.unpack1("m")
response = authenticator.process(challenge)
response = [response].pack("m0")
put_string(response + CRLF)
end
end
unless SASL.done?(authenticator)
result = send_command_with_continuations(*cmdargs) {|data|
challenge = data.unpack1("m")
response = authenticator.process challenge
[response].pack("m0")
}
if authenticator.respond_to?(:done?) && !authenticator.done?
logout!
raise SASL::AuthenticationFailed, "authentication ended prematurely"
raise SASL::AuthenticationIncomplete, result
end
@capabilities = capabilities_from_resp_code result
result
Expand Down Expand Up @@ -2570,6 +2568,18 @@ def capabilities_from_resp_code(resp)

#############################

# Calls send_command, yielding the text of each ContinuationRequest and
# responding with each block result. Returns TaggedResponse. Raises
# NoResponseError or BadResponseError.
def send_command_with_continuations(cmd, *args)
send_command(cmd, *args) do |server_response|
if server_response.instance_of?(ContinuationRequest)
client_response = yield server_response.data.text
put_string(client_response + CRLF)
end
end
end

def send_command(cmd, *args, &block)
synchronize do
args.each do |i|
Expand Down
34 changes: 13 additions & 21 deletions lib/net/imap/sasl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ module SASL
# messages has not passed integrity checks.
AuthenticationFailed = Class.new(Error)

# Indicates that authentication cannot proceed because one of the server's
# ended authentication prematurely.
class AuthenticationIncomplete < AuthenticationFailed
# The success response from the server
attr_reader :response

def initialize(response, message = "authentication ended prematurely")
super(message)
@response = response
end
end

# autoloading to avoid loading all of the regexps when they aren't used.
sasl_stringprep_rb = File.expand_path("sasl/stringprep", __dir__)
autoload :StringPrep, sasl_stringprep_rb
Expand Down Expand Up @@ -141,9 +153,7 @@ module SASL
autoload :LoginAuthenticator, "#{sasl_dir}/login_authenticator"

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

# Delegates to ::authenticators. See Authenticators#authenticator.
def self.authenticator(...) authenticators.authenticator(...) end
Expand All @@ -158,24 +168,6 @@ def saslprep(string, **opts)
Net::IMAP::StringPrep::SASLprep.saslprep(string, **opts)
end

# Returns whether +authenticator+ is client-first and supports sending an
# "initial response".
def initial_response?(authenticator)
authenticator.respond_to?(:initial_response?) &&
authenticator.initial_response?
end

# Returns whether +authenticator+ considers the authentication exchange to
# be complete.
#
# The authentication should not succeed if this returns false, but
# returning true does *not* indicate success. Authentication succeeds
# when this method returns true and the server responds with a
# protocol-specific success.
def done?(authenticator)
!authenticator.respond_to?(:done?) || authenticator.done?
end

end
end
end
40 changes: 23 additions & 17 deletions lib/net/imap/sasl/authenticators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,23 @@ class Authenticators
# 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)
# When +use_defaults+ is +false+, the registry will start empty. When
# +use_deprecated+ is +false+, deprecated authenticators will not be
# included with the defaults.
def initialize(use_defaults: true, use_deprecated: true)
@authenticators = {}
if use_defaults
add_authenticator "Anonymous"
add_authenticator "External"
add_authenticator "OAuthBearer"
add_authenticator "Plain"
add_authenticator "Scram-SHA-1"
add_authenticator "Scram-SHA-256"
add_authenticator "XOAuth2"
add_authenticator "Login" # deprecated
add_authenticator "Cram-MD5" # deprecated
add_authenticator "Digest-MD5" # deprecated
end
return unless use_defaults
add_authenticator "Anonymous"
add_authenticator "External"
add_authenticator "OAuthBearer"
add_authenticator "Plain"
add_authenticator "Scram-SHA-1"
add_authenticator "Scram-SHA-256"
add_authenticator "XOAuth2"
return unless use_deprecated
add_authenticator "Login" # deprecated
add_authenticator "Cram-MD5" # deprecated
add_authenticator "Digest-MD5" # deprecated
end

# Returns the names of all registered SASL mechanisms.
Expand Down Expand Up @@ -78,6 +77,12 @@ def add_authenticator(name, authenticator = nil)
@authenticators[key] = authenticator
end

# Removes the authenticator registered for +name+
def remove_authenticator(name)
key = name.upcase.to_sym
@authenticators.delete(key)
end

# :call-seq:
# authenticator(mechanism, ...) -> auth_session
#
Expand All @@ -100,6 +105,7 @@ def authenticator(mechanism, ...)
end
auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
end
alias new authenticator

end

Expand Down
2 changes: 2 additions & 0 deletions lib/net/imap/sasl/cram_md5_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ def initialize(user, password, warn_deprecation: true, **_ignored)
@done = false
end

def initial_response?; false end

def process(challenge)
digest = hmac_md5(challenge, @password)
return @user + " " + digest
Expand Down
2 changes: 2 additions & 0 deletions lib/net/imap/sasl/digest_md5_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ def initialize(user = nil, pass = nil, authz = nil,
@nc, @stage = {}, STAGE_ONE
end

def initial_response?; false end

# Responds to server challenge in two stages.
def process(challenge)
case @stage
Expand Down
2 changes: 2 additions & 0 deletions lib/net/imap/sasl/login_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ def initialize(user, password, warn_deprecation: true, **_ignored)
@state = STATE_USER
end

def initial_response?; false end

def process(data)
case @state
when STATE_USER
Expand Down
2 changes: 1 addition & 1 deletion test/net/imap/test_imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -966,7 +966,7 @@ def test_id
server.state.authenticate(server.config.user)
cmd.done_ok
end
assert_raise(Net::IMAP::SASL::AuthenticationFailed) do
assert_raise(Net::IMAP::SASL::AuthenticationIncomplete) do
imap.authenticate("DIGEST-MD5", "test_user", "test-password",
warn_deprecation: false)
end
Expand Down
8 changes: 3 additions & 5 deletions test/net/imap/test_imap_authenticators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ def test_plain_authenticator_matches_mechanism

def test_plain_supports_initial_response
assert plain("foo", "bar").initial_response?
assert Net::IMAP::SASL.initial_response?(plain("foo", "bar"))
end

def test_plain_response
Expand Down Expand Up @@ -194,7 +193,6 @@ def test_xoauth2_kwargs

def test_xoauth2_supports_initial_response
assert xoauth2("foo", "bar").initial_response?
assert Net::IMAP::SASL.initial_response?(xoauth2("foo", "bar"))
end

# ----------------------
Expand Down Expand Up @@ -276,7 +274,7 @@ def test_login_authenticator_matches_mechanism
end

def test_login_does_not_support_initial_response
refute Net::IMAP::SASL.initial_response?(login("foo", "bar"))
refute login("foo", "bar").initial_response?
end

def test_login_authenticator_deprecated
Expand Down Expand Up @@ -306,7 +304,7 @@ def test_cram_md5_authenticator_matches_mechanism
end

def test_cram_md5_does_not_support_initial_response
refute Net::IMAP::SASL.initial_response?(cram_md5("foo", "bar"))
refute cram_md5("foo", "bar").initial_response?
end

def test_cram_md5_authenticator_deprecated
Expand Down Expand Up @@ -343,7 +341,7 @@ def test_digest_md5_authenticator_deprecated
end

def test_digest_md5_does_not_support_initial_response
refute Net::IMAP::SASL.initial_response?(digest_md5("foo", "bar"))
refute digest_md5("foo", "bar").initial_response?
end

def test_digest_md5_authenticator
Expand Down