Skip to content

Commit 28192a0

Browse files
committed
🔒 SASL PLAIN: Refactor and document
1 parent 8764a3a commit 28192a0

File tree

2 files changed

+78
-23
lines changed

2 files changed

+78
-23
lines changed
Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,95 @@
11
# frozen_string_literal: true
22

33
# Authenticator for the "+PLAIN+" SASL mechanism, specified in
4-
# RFC4616[https://tools.ietf.org/html/rfc4616]. See Net::IMAP#authenticate.
4+
# RFC-4616[https://tools.ietf.org/html/rfc4616]. See Net::IMAP#authenticate.
55
#
66
# +PLAIN+ authentication sends the password in cleartext.
7-
# RFC3501[https://tools.ietf.org/html/rfc3501] encourages servers to disable
7+
# RFC-3501[https://tools.ietf.org/html/rfc3501] encourages servers to disable
88
# cleartext authentication until after TLS has been negotiated.
9-
# RFC8314[https://tools.ietf.org/html/rfc8314] recommends TLS version 1.2 or
9+
# RFC-8314[https://tools.ietf.org/html/rfc8314] recommends TLS version 1.2 or
1010
# greater be used for all traffic, and deprecate cleartext access ASAP. +PLAIN+
1111
# can be secured by TLS encryption.
1212
class Net::IMAP::SASL::PlainAuthenticator
1313

14+
##
15+
# :call-seq:
16+
# new(username, password, authzid = nil, **) -> auth_ctx
17+
# new(authcid:, password:, authzid: nil, **) -> auth_ctx
18+
#
19+
# Creates an Authenticator for the "+PLAIN+" SASL mechanism.
20+
#
21+
# Called by Net::IMAP#authenticate and similar methods on other clients.
22+
#
23+
# === Properties
24+
#
25+
# * #authcid ― Identity whose #password is used. Aliased as #username.
26+
# * #password ― Password or passphrase associated with this #authcid.
27+
# * #authzid ― Alternate identity to act as or on behalf of. Optional.
28+
#
29+
# See the documentation on each attribute for more details.
30+
#
31+
# All three properties may be sent as either positional or keyword arguments.
32+
def initialize(username_arg = nil, password_arg = nil, authzid_arg = nil,
33+
authcid: nil, username: nil, password: nil, authzid: nil, **)
34+
@authcid = authcid || username || username_arg
35+
@password = password || password_arg
36+
@authzid = authzid || authzid_arg
37+
@authcid or raise ArgumentError, "missing authcid (username)"
38+
@password or raise ArgumentError, "missing password"
39+
[authcid, username, username_arg].compact.count == 1 or
40+
raise ArgumentError, "conflicting values for authcid (username)"
41+
[password, password_arg].compact.count == 1 or
42+
raise ArgumentError, "conflicting values for password (username)"
43+
[authzid, authzid_arg].compact.count <= 1 or
44+
raise ArgumentError, "conflicting values for authzid (username)"
45+
guard_against_null_char(:authcid, @authcid)
46+
guard_against_null_char(:authzid, @authzid)
47+
guard_against_null_char(:password, @password)
48+
end
49+
50+
# :call-seq:
51+
# initial_response? -> true
52+
#
53+
# +PLAIN+ can send an initial client response.
1454
def initial_response?; true end
1555

16-
def process(data)
17-
return "#@authzid\0#@username\0#@password"
18-
end
56+
# Authentication identity: the identity that matches the #password.
57+
#
58+
# RFC-2831[https://tools.ietf.org/html/rfc2831] uses the term +username+.
59+
# "Authentication identity" is the generic term used by
60+
# RFC-4422[https://tools.ietf.org/html/rfc4422].
61+
# RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs abbreviate
62+
# to +authcid+. #username is available as an alias for #authcid, but only
63+
# <tt>:authcid</tt> will be sent to callbacks.
64+
attr_reader :authcid
65+
alias username authcid
1966

20-
# :nodoc:
21-
NULL = -"\0".b
67+
# A password or passphrase that matches the #authcid.
68+
attr_reader :password
69+
70+
# Authorization identity: an identity to act as or on behalf of. The identity
71+
# form is application protocol specific. If not provided or left blank, the
72+
# server derives an authorization identity from the authentication identity.
73+
# The server is responsible for verifying the client's credentials and
74+
# verifying that the identity it associates with the client's authentication
75+
# identity is allowed to act as (or on behalf of) the authorization identity.
76+
#
77+
# For example, an administrator or superuser might take on another role:
78+
#
79+
# imap.authenticate "PLAIN", "root", passwd, authzid: "user"
80+
#
81+
attr_reader :authzid
82+
83+
# Responds with the client's credentials.
84+
def process(data) [authzid, authcid, password].join("\0") end
2285

2386
private
2487

25-
# +username+ is the authentication identity, the identity whose +password+ is
26-
# used. +username+ is referred to as +authcid+ by
27-
# RFC4616[https://tools.ietf.org/html/rfc4616].
28-
#
29-
# +authzid+ is the authorization identity (identity to act as). It can
30-
# usually be left blank. When +authzid+ is left blank (nil or empty string)
31-
# the server will derive an identity from the credentials and use that as the
32-
# authorization identity.
33-
def initialize(username, password, authzid: nil)
34-
raise ArgumentError, "username contains NULL" if username&.include?(NULL)
35-
raise ArgumentError, "password contains NULL" if password&.include?(NULL)
36-
raise ArgumentError, "authzid contains NULL" if authzid&.include?(NULL)
37-
@username = username
38-
@password = password
39-
@authzid = authzid
88+
NULL = -"\0".b
89+
private_constant :NULL
90+
91+
def guard_against_null_char(name, value)
92+
raise ArgumentError, "#{name} contains NULL" if value&.include?(NULL)
4093
end
4194

4295
end

test/net/imap/test_imap_authenticators.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ def test_plain_supports_initial_response
4646

4747
def test_plain_response
4848
assert_equal("\0authc\0passwd", plain("authc", "passwd").process(nil))
49+
assert_equal("authz\0user\0pass",
50+
plain("user", "pass", "authz").process(nil))
4951
assert_equal("authz\0user\0pass",
5052
plain("user", "pass", authzid: "authz").process(nil))
5153
end

0 commit comments

Comments
 (0)