Skip to content

Commit d3af1ec

Browse files
committed
♻️ SASL PLAIN: SASL-IR, refactor, move, document
* ✨ Add `initial_response? => true` * ♻️ Inherit from Authenticator base class. * 🚚 Move to sasl directory and SASL namespace. * 🗑️ Deprecate original constant name.
1 parent 2be1fa7 commit d3af1ec

File tree

4 files changed

+129
-43
lines changed

4 files changed

+129
-43
lines changed

lib/net/imap/authenticators/plain.rb

Lines changed: 0 additions & 40 deletions
This file was deleted.

lib/net/imap/sasl.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,12 @@ module SASL
6262
sasl_dir = File.expand_path("sasl", __dir__)
6363
autoload :Authenticator, "#{sasl_dir}/authenticator"
6464
autoload :Authenticators, "#{sasl_dir}/authenticators"
65+
autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
6566

6667
# Authenticators are all lazy loaded
6768
def self.authenticators
6869
@authenticators ||= SASL::Authenticators.new.tap do |registry|
69-
registry.add_authenticator "Plain", PlainAuthenticator
70+
registry.add_authenticator "Plain"
7071
registry.add_authenticator "XOauth2", XOauth2Authenticator
7172
registry.add_authenticator "Login", LoginAuthenticator # deprecated
7273
registry.add_authenticator "Cram-MD5", CramMD5Authenticator # deprecated
@@ -101,7 +102,6 @@ def saslprep(string, **opts)
101102
end
102103
end
103104

104-
require_relative "authenticators/plain"
105105
require_relative "authenticators/login"
106106
require_relative "authenticators/cram_md5"
107107
require_relative "authenticators/digest_md5"
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "authenticator"
4+
5+
module Net
6+
class IMAP < Protocol
7+
module SASL
8+
9+
# Authenticator for the "+PLAIN+" SASL mechanism, specified in
10+
# RFC-4616[https://tools.ietf.org/html/rfc4616]. Use via
11+
# Net::IMAP#authenticate.
12+
#
13+
# +PLAIN+ authentication sends the password in cleartext.
14+
# RFC-3501[https://tools.ietf.org/html/rfc3501] encourages servers to
15+
# disable cleartext authentication until after TLS has been negotiated.
16+
# RFC-8314[https://tools.ietf.org/html/rfc8314] recommends TLS version 1.2
17+
# or greater be used for all traffic, and deprecate cleartext access ASAP.
18+
# +PLAIN+ can be secured by TLS encryption.
19+
class PlainAuthenticator < Authenticator
20+
21+
##
22+
# :call-seq:
23+
# new(username, password, authzid = nil, **) -> auth_ctx
24+
# new(authcid:, password:, authzid: nil, **) -> auth_ctx
25+
# new(**) {|propname, auth_ctx| propval } -> auth_ctx
26+
#
27+
# Creates an Authenticator for the "+PLAIN+" SASL mechanism.
28+
#
29+
# Called by Net::IMAP#authenticate and similar methods on other clients.
30+
#
31+
# === Properties
32+
#
33+
# * #authcid ― Identity whose #password is used. Aliased as #username.
34+
# * #password ― Password or passphrase associated with this #authcid.
35+
# * #authzid ― Alternate identity to act as or on behalf of. Optional.
36+
#
37+
# See the documentation on each property method for more details.
38+
#
39+
# All three properties may be sent as either positional or keyword
40+
# arguments. See Net::IMAP::SASL::Authenticator@Properties for a
41+
# detailed description of property assignment, lazy loading, and
42+
# callbacks.
43+
#
44+
def initialize(username_arg = nil, password_arg = nil, authzid_arg = nil,
45+
authcid: nil, password: nil, authzid: nil,
46+
username: nil, # alias for authcid
47+
**, &callback)
48+
super
49+
propinit(:authcid, authcid, username, username_arg)
50+
propinit(:authzid, authzid, authzid_arg)
51+
propinit(:password, password, password_arg)
52+
end
53+
54+
# :call-seq:
55+
# initial_response? -> true
56+
#
57+
# +PLAIN+ can send an initial client response.
58+
def initial_response?; true end
59+
60+
##
61+
# method: authcid
62+
# :call-seq: authcid -> string or nil
63+
#
64+
# Authentication identity: the identity that matches the #password.
65+
#
66+
# RFC-2831[https://tools.ietf.org/html/rfc2831] uses the term +username+.
67+
# "Authentication identity" is the generic term used by
68+
# RFC-4422[https://tools.ietf.org/html/rfc4422].
69+
# RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs
70+
# abbreviate to +authcid+. #username is available as an alias for
71+
# #authcid, but only <tt>:authcid</tt> will be sent to callbacks.
72+
property :authcid
73+
alias username authcid
74+
75+
##
76+
# method: password
77+
# :call-seq: password -> string or nil
78+
#
79+
# A password or passphrase that matches the #authcid.
80+
property :password
81+
82+
##
83+
# method: authzid
84+
# :call-seq: authzid -> string or nil
85+
#
86+
# Authorization identity: an identity to act as or on behalf of. The
87+
# identity form is application protocol specific. If not provided or
88+
# left blank, the server derives an authorization identity from the
89+
# authentication identity. The server is responsible for verifying the
90+
# client's credentials and verifying that the identity it associates with
91+
# the client's authentication identity is allowed to act as (or on behalf
92+
# of) the authorization identity.
93+
#
94+
# For example, an administrator or superuser might take on another role:
95+
#
96+
# imap.authenticate "PLAIN", "root", ->{passwd}, authzid: "user"
97+
#
98+
property :authzid
99+
100+
##
101+
# Responds with the client's credentials.
102+
def process(data) [authzid, authcid, password].join("\0") end
103+
104+
private
105+
106+
NULL = -"\0".b
107+
private_constant :NULL
108+
109+
def propset(name, value)
110+
case name
111+
when :authcid, :password, :authzid
112+
raise ArgumentError, "#{name} contains NULL" if value&.include?(NULL)
113+
end
114+
super
115+
end
116+
117+
end
118+
end
119+
120+
PlainAuthenticator = SASL::PlainAuthenticator # :nodoc:
121+
deprecate_constant :PlainAuthenticator
122+
123+
end
124+
end

test/net/imap/test_imap_authenticators.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ def plain(*args, **kwargs, &block)
2020
end
2121

2222
def test_plain_authenticator_matches_mechanism
23-
assert_kind_of(Net::IMAP::PlainAuthenticator, plain("user", "pass"))
23+
assert_kind_of(Net::IMAP::SASL::PlainAuthenticator, plain("user", "pass"))
2424
end
2525

2626
def test_plain_response
2727
assert_equal("\0authc\0passwd", plain("authc", "passwd").process(nil))
28+
assert_equal("authz\0user\0pass",
29+
plain("user", "pass", "authz").process(nil))
2830
assert_equal("authz\0user\0pass",
2931
plain("user", "pass", authzid: "authz").process(nil))
3032
end

0 commit comments

Comments
 (0)