Skip to content

Commit cb52a2d

Browse files
authored
🔀 Merge pull request #184 from ruby/sasl/prerelease-api-changes
✨ Minor updates to SASL::Authenticators API
2 parents 146ad37 + 8a1ac02 commit cb52a2d

File tree

8 files changed

+67
-55
lines changed

8 files changed

+67
-55
lines changed

‎lib/net/imap.rb

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,21 +1251,19 @@ def authenticate(mechanism, *creds, sasl_ir: true, **props, &callback)
12511251
authenticator = SASL.authenticator(mechanism, *creds, **props, &callback)
12521252
cmdargs = ["AUTHENTICATE", mechanism]
12531253
if sasl_ir && capable?("SASL-IR") && auth_capable?(mechanism) &&
1254-
SASL.initial_response?(authenticator)
1254+
authenticator.respond_to?(:initial_response?) &&
1255+
authenticator.initial_response?
12551256
response = authenticator.process(nil)
12561257
cmdargs << (response.empty? ? "=" : [response].pack("m0"))
12571258
end
1258-
result = send_command(*cmdargs) do |resp|
1259-
if resp.instance_of?(ContinuationRequest)
1260-
challenge = resp.data.text.unpack1("m")
1261-
response = authenticator.process(challenge)
1262-
response = [response].pack("m0")
1263-
put_string(response + CRLF)
1264-
end
1265-
end
1266-
unless SASL.done?(authenticator)
1259+
result = send_command_with_continuations(*cmdargs) {|data|
1260+
challenge = data.unpack1("m")
1261+
response = authenticator.process challenge
1262+
[response].pack("m0")
1263+
}
1264+
if authenticator.respond_to?(:done?) && !authenticator.done?
12671265
logout!
1268-
raise SASL::AuthenticationFailed, "authentication ended prematurely"
1266+
raise SASL::AuthenticationIncomplete, result
12691267
end
12701268
@capabilities = capabilities_from_resp_code result
12711269
result
@@ -2570,6 +2568,18 @@ def capabilities_from_resp_code(resp)
25702568

25712569
#############################
25722570

2571+
# Calls send_command, yielding the text of each ContinuationRequest and
2572+
# responding with each block result. Returns TaggedResponse. Raises
2573+
# NoResponseError or BadResponseError.
2574+
def send_command_with_continuations(cmd, *args)
2575+
send_command(cmd, *args) do |server_response|
2576+
if server_response.instance_of?(ContinuationRequest)
2577+
client_response = yield server_response.data.text
2578+
put_string(client_response + CRLF)
2579+
end
2580+
end
2581+
end
2582+
25732583
def send_command(cmd, *args, &block)
25742584
synchronize do
25752585
args.each do |i|

‎lib/net/imap/sasl.rb

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,18 @@ module SASL
114114
# messages has not passed integrity checks.
115115
AuthenticationFailed = Class.new(Error)
116116

117+
# Indicates that authentication cannot proceed because one of the server's
118+
# ended authentication prematurely.
119+
class AuthenticationIncomplete < AuthenticationFailed
120+
# The success response from the server
121+
attr_reader :response
122+
123+
def initialize(response, message = "authentication ended prematurely")
124+
super(message)
125+
@response = response
126+
end
127+
end
128+
117129
# autoloading to avoid loading all of the regexps when they aren't used.
118130
sasl_stringprep_rb = File.expand_path("sasl/stringprep", __dir__)
119131
autoload :StringPrep, sasl_stringprep_rb
@@ -141,9 +153,7 @@ module SASL
141153
autoload :LoginAuthenticator, "#{sasl_dir}/login_authenticator"
142154

143155
# Returns the default global SASL::Authenticators instance.
144-
def self.authenticators
145-
@authenticators ||= Authenticators.new(use_defaults: true)
146-
end
156+
def self.authenticators; @authenticators ||= Authenticators.new end
147157

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

161-
# Returns whether +authenticator+ is client-first and supports sending an
162-
# "initial response".
163-
def initial_response?(authenticator)
164-
authenticator.respond_to?(:initial_response?) &&
165-
authenticator.initial_response?
166-
end
167-
168-
# Returns whether +authenticator+ considers the authentication exchange to
169-
# be complete.
170-
#
171-
# The authentication should not succeed if this returns false, but
172-
# returning true does *not* indicate success. Authentication succeeds
173-
# when this method returns true and the server responds with a
174-
# protocol-specific success.
175-
def done?(authenticator)
176-
!authenticator.respond_to?(:done?) || authenticator.done?
177-
end
178-
179171
end
180172
end
181173
end

‎lib/net/imap/sasl/authenticators.rb

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,23 @@ class Authenticators
2626
# This class is usually not instantiated directly. Use SASL.authenticators
2727
# to reuse the default global registry.
2828
#
29-
# By default, the registry will be empty--without any registrations. When
30-
# +add_defaults+ is +true+, authenticators for all standard mechanisms will
31-
# be registered.
32-
#
33-
def initialize(use_defaults: false)
29+
# When +use_defaults+ is +false+, the registry will start empty. When
30+
# +use_deprecated+ is +false+, deprecated authenticators will not be
31+
# included with the defaults.
32+
def initialize(use_defaults: true, use_deprecated: true)
3433
@authenticators = {}
35-
if use_defaults
36-
add_authenticator "Anonymous"
37-
add_authenticator "External"
38-
add_authenticator "OAuthBearer"
39-
add_authenticator "Plain"
40-
add_authenticator "Scram-SHA-1"
41-
add_authenticator "Scram-SHA-256"
42-
add_authenticator "XOAuth2"
43-
add_authenticator "Login" # deprecated
44-
add_authenticator "Cram-MD5" # deprecated
45-
add_authenticator "Digest-MD5" # deprecated
46-
end
34+
return unless use_defaults
35+
add_authenticator "Anonymous"
36+
add_authenticator "External"
37+
add_authenticator "OAuthBearer"
38+
add_authenticator "Plain"
39+
add_authenticator "Scram-SHA-1"
40+
add_authenticator "Scram-SHA-256"
41+
add_authenticator "XOAuth2"
42+
return unless use_deprecated
43+
add_authenticator "Login" # deprecated
44+
add_authenticator "Cram-MD5" # deprecated
45+
add_authenticator "Digest-MD5" # deprecated
4746
end
4847

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

80+
# Removes the authenticator registered for +name+
81+
def remove_authenticator(name)
82+
key = name.upcase.to_sym
83+
@authenticators.delete(key)
84+
end
85+
8186
# :call-seq:
8287
# authenticator(mechanism, ...) -> auth_session
8388
#
@@ -100,6 +105,7 @@ def authenticator(mechanism, ...)
100105
end
101106
auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
102107
end
108+
alias new authenticator
103109

104110
end
105111

‎lib/net/imap/sasl/cram_md5_authenticator.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ def initialize(user, password, warn_deprecation: true, **_ignored)
2424
@done = false
2525
end
2626

27+
def initial_response?; false end
28+
2729
def process(challenge)
2830
digest = hmac_md5(challenge, @password)
2931
return @user + " " + digest

‎lib/net/imap/sasl/digest_md5_authenticator.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ def initialize(user = nil, pass = nil, authz = nil,
7373
@nc, @stage = {}, STAGE_ONE
7474
end
7575

76+
def initial_response?; false end
77+
7678
# Responds to server challenge in two stages.
7779
def process(challenge)
7880
case @stage

‎lib/net/imap/sasl/login_authenticator.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ def initialize(user, password, warn_deprecation: true, **_ignored)
3232
@state = STATE_USER
3333
end
3434

35+
def initial_response?; false end
36+
3537
def process(data)
3638
case @state
3739
when STATE_USER

‎test/net/imap/test_imap.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -966,7 +966,7 @@ def test_id
966966
server.state.authenticate(server.config.user)
967967
cmd.done_ok
968968
end
969-
assert_raise(Net::IMAP::SASL::AuthenticationFailed) do
969+
assert_raise(Net::IMAP::SASL::AuthenticationIncomplete) do
970970
imap.authenticate("DIGEST-MD5", "test_user", "test-password",
971971
warn_deprecation: false)
972972
end

‎test/net/imap/test_imap_authenticators.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ def test_plain_authenticator_matches_mechanism
4141

4242
def test_plain_supports_initial_response
4343
assert plain("foo", "bar").initial_response?
44-
assert Net::IMAP::SASL.initial_response?(plain("foo", "bar"))
4544
end
4645

4746
def test_plain_response
@@ -194,7 +193,6 @@ def test_xoauth2_kwargs
194193

195194
def test_xoauth2_supports_initial_response
196195
assert xoauth2("foo", "bar").initial_response?
197-
assert Net::IMAP::SASL.initial_response?(xoauth2("foo", "bar"))
198196
end
199197

200198
# ----------------------
@@ -276,7 +274,7 @@ def test_login_authenticator_matches_mechanism
276274
end
277275

278276
def test_login_does_not_support_initial_response
279-
refute Net::IMAP::SASL.initial_response?(login("foo", "bar"))
277+
refute login("foo", "bar").initial_response?
280278
end
281279

282280
def test_login_authenticator_deprecated
@@ -306,7 +304,7 @@ def test_cram_md5_authenticator_matches_mechanism
306304
end
307305

308306
def test_cram_md5_does_not_support_initial_response
309-
refute Net::IMAP::SASL.initial_response?(cram_md5("foo", "bar"))
307+
refute cram_md5("foo", "bar").initial_response?
310308
end
311309

312310
def test_cram_md5_authenticator_deprecated
@@ -343,7 +341,7 @@ def test_digest_md5_authenticator_deprecated
343341
end
344342

345343
def test_digest_md5_does_not_support_initial_response
346-
refute Net::IMAP::SASL.initial_response?(digest_md5("foo", "bar"))
344+
refute digest_md5("foo", "bar").initial_response?
347345
end
348346

349347
def test_digest_md5_authenticator

0 commit comments

Comments
 (0)