Skip to content

Commit 5f9027e

Browse files
committed
🔒 Clarify usage of username vs authcid vs authzid
Different SASL mechanisms use the term "username" differently. In general the pattern seems to be the following: * Some mechanisms avoid using the term `username` at all, and instead use the terms Authentication Identity (`authcid`) and Authorization Identity (`authzid`). * Older or non-standard mechanisms may not distinguish clearly between `authcid` and `authzid`. `username` may be semantically equivalent to `authcid`, `authzid`, or both. * When the mechanism supports an explicit `authcid` and an `authzid`, `username` commonly refers to the `authcid`. * When the authentication identity is derived from other credentials, `username` commonly refers to the `authzid`. Every mechanism's keyword arguments, positional arguments, and documentation is updated to match this terminology. Aliases have been added from `username` to `authcid` or `authzid`—or in the other direction, from `authcid` or `authzd` to `username`.
1 parent 5bc8bff commit 5f9027e

File tree

7 files changed

+108
-19
lines changed

7 files changed

+108
-19
lines changed

lib/net/imap/sasl/digest_md5_authenticator.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ class Net::IMAP::SASL::DigestMD5Authenticator
2020
# "Authentication identity" is the generic term used by
2121
# RFC-4422[https://tools.ietf.org/html/rfc4422].
2222
# RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs abbreviate
23-
# that to +authcid+. So +authcid+ is available as an alias for #username.
23+
# this to +authcid+.
2424
attr_reader :username
25+
alias authcid username
2526

2627
# A password or passphrase that matches the #username.
2728
#
@@ -44,16 +45,19 @@ class Net::IMAP::SASL::DigestMD5Authenticator
4445
# :call-seq:
4546
# new(username, password, authzid = nil, **options) -> authenticator
4647
# new(username:, password:, authzid: nil, **options) -> authenticator
48+
# new(authcid:, password:, authzid: nil, **options) -> authenticator
4749
#
4850
# Creates an Authenticator for the "+DIGEST-MD5+" SASL mechanism.
4951
#
5052
# Called by Net::IMAP#authenticate and similar methods on other clients.
5153
#
5254
# ==== Parameters
5355
#
54-
# * #username — Identity whose #password is used.
55-
# * #password — A password or passphrase associated with this #username.
56+
# * #authcid ― Authentication identity that is associated with #password.
5657
#
58+
# #username ― An alias for +authcid+.
59+
#
60+
# * #password ― A password or passphrase associated with this #authcid.
5761
#
5862
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
5963
#
@@ -65,8 +69,10 @@ class Net::IMAP::SASL::DigestMD5Authenticator
6569
# Any other keyword arguments are silently ignored.
6670
def initialize(user = nil, pass = nil, authz = nil,
6771
username: nil, password: nil, authzid: nil,
72+
authcid: nil,
6873
warn_deprecation: true, **)
69-
username ||= user or raise ArgumentError, "missing username"
74+
username = authcid || username || user or
75+
raise ArgumentError, "missing username (authcid)"
7076
password ||= pass or raise ArgumentError, "missing password"
7177
authzid ||= authz
7278
if warn_deprecation

lib/net/imap/sasl/external_authenticator.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ class ExternalAuthenticator
2525
# imap.authenticate "PLAIN", "root", passwd, authzid: "user"
2626
#
2727
attr_reader :authzid
28+
alias username authzid
2829

2930
# :call-seq:
3031
# new(authzid: nil, **) -> authenticator
32+
# new(username: nil, **) -> authenticator
33+
# new(username = nil, **) -> authenticator
3134
#
3235
# Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as
3336
# specified in RFC-4422[https://tools.ietf.org/html/rfc4422]. To use
@@ -38,8 +41,16 @@ class ExternalAuthenticator
3841
#
3942
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
4043
#
44+
# _optional_ #username ― An alias for #authzid.
45+
#
46+
# Note that, unlike some other authenticators, +username+ sets the
47+
# _authorization_ identity and not the _authentication_ identity. The
48+
# authentication identity is established for the client by the
49+
# external credentials.
50+
#
4151
# Any other keyword parameters are quietly ignored.
42-
def initialize(authzid: nil, **)
52+
def initialize(user = nil, authzid: nil, username: nil, **)
53+
authzid ||= username || user
4354
@authzid = authzid&.to_str&.encode "UTF-8"
4455
if @authzid&.match?(/\u0000/u) # also validates UTF8 encoding
4556
raise ArgumentError, "contains NULL"

lib/net/imap/sasl/oauthbearer_authenticator.rb

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class OAuthAuthenticator
2727
# imap.authenticate "PLAIN", "root", passwd, authzid: "user"
2828
#
2929
attr_reader :authzid
30+
alias username authzid
3031

3132
# Hostname to which the client connected. (optional)
3233
attr_reader :host
@@ -45,6 +46,7 @@ class OAuthAuthenticator
4546

4647
# The query string. (optional)
4748
attr_reader :qs
49+
alias query qs
4850

4951
# Stores the most recent server "challenge". When authentication fails,
5052
# this may hold information about the failure reason, as JSON.
@@ -61,23 +63,34 @@ class OAuthAuthenticator
6163
# #host or #port) <b>as are specific server implementations</b>.
6264
#
6365
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
66+
#
67+
# _optional_ #username — An alias for #authzid.
68+
#
69+
# Note that, unlike some other authenticators, +username+ sets the
70+
# _authorization_ identity and not the _authentication_ identity. The
71+
# authentication identity is established for the client by the OAuth
72+
# token.
73+
#
6474
# * _optional_ #host — Hostname to which the client connected.
6575
# * _optional_ #port — Service port to which the client connected.
6676
# * _optional_ #mthd — HTTP method
6777
# * _optional_ #path — HTTP path data
6878
# * _optional_ #post — HTTP post data
6979
# * _optional_ #qs — HTTP query string
7080
#
81+
# _optional_ #query — An alias for #qs
82+
#
7183
# Any other keyword parameters are quietly ignored.
7284
def initialize(authzid: nil, host: nil, port: nil,
85+
username: nil, query: nil,
7386
mthd: nil, path: nil, post: nil, qs: nil, **)
74-
@authzid = authzid
87+
@authzid = authzid || username
7588
@host = host
7689
@port = port
7790
@mthd = mthd
7891
@path = path
7992
@post = post
80-
@qs = qs
93+
@qs = qs || query
8194
@done = false
8295
end
8396

@@ -144,6 +157,14 @@ class OAuthBearerAuthenticator < OAuthAuthenticator
144157
# The most common ones are:
145158
#
146159
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
160+
#
161+
# _optional_ #username — An alias for #authzid.
162+
#
163+
# Note that, unlike some other authenticators, +username+ sets the
164+
# _authorization_ identity and not the _authentication_ identity. The
165+
# authentication identity is established for the client by
166+
# #oauth2_token.
167+
#
147168
# * _optional_ #host — Hostname to which the client connected.
148169
# * _optional_ #port — Service port to which the client connected.
149170
#

lib/net/imap/sasl/plain_authenticator.rb

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class Net::IMAP::SASL::PlainAuthenticator
2222
# RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs abbreviate
2323
# this to +authcid+.
2424
attr_reader :username
25+
alias authcid username
2526

2627
# A password or passphrase that matches the #username.
2728
attr_reader :password
@@ -42,15 +43,19 @@ class Net::IMAP::SASL::PlainAuthenticator
4243
# :call-seq:
4344
# new(username, password, authzid: nil, **) -> authenticator
4445
# new(username:, password:, authzid: nil, **) -> authenticator
46+
# new(authcid:, password:, authzid: nil, **) -> authenticator
4547
#
4648
# Creates an Authenticator for the "+PLAIN+" SASL mechanism.
4749
#
4850
# Called by Net::IMAP#authenticate and similar methods on other clients.
4951
#
5052
# ==== Parameters
5153
#
52-
# * #username ― Identity whose +password+ is used.
53-
# * #password ― Password or passphrase associated with this username+.
54+
# * #authcid ― Authentication identity that is associated with #password.
55+
#
56+
# #username ― An alias for #authcid.
57+
#
58+
# * #password ― A password or passphrase associated with the #authcid.
5459
#
5560
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
5661
#
@@ -59,12 +64,14 @@ class Net::IMAP::SASL::PlainAuthenticator
5964
#
6065
# Any other keyword parameters are quietly ignored.
6166
def initialize(user = nil, pass = nil,
67+
authcid: nil,
6268
username: nil, password: nil, authzid: nil, **)
63-
[username, user].compact.count == 1 or
64-
raise ArgumentError, "conflicting values for username"
65-
[password, pass].compact.count == 1 or
69+
[authcid, username, user].compact.count <= 1 or
70+
raise ArgumentError, "conflicting values for username (authcid)"
71+
[password, pass].compact.count <= 1 or
6672
raise ArgumentError, "conflicting values for password"
67-
username ||= user or raise ArgumentError, "missing username"
73+
username ||= authcid || user or
74+
raise ArgumentError, "missing username (authcid)"
6875
password ||= pass or raise ArgumentError, "missing password"
6976
raise ArgumentError, "username contains NULL" if username.include?(NULL)
7077
raise ArgumentError, "password contains NULL" if password.include?(NULL)

lib/net/imap/sasl/scram_authenticator.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class ScramAuthenticator
6060
# :call-seq:
6161
# new(username, password, **options) -> auth_ctx
6262
# new(username:, password:, **options) -> auth_ctx
63+
# new(authcid:, password:, **options) -> auth_ctx
6364
#
6465
# Creates an authenticator for one of the "+SCRAM-*+" SASL mechanisms.
6566
# Each subclass defines #digest to match a specific mechanism.
@@ -68,14 +69,18 @@ class ScramAuthenticator
6869
#
6970
# === Parameters
7071
#
71-
# * #username ― Identity whose #password is used. Aliased as #authcid.
72+
# * #authcid ― Identity whose #password is used.
73+
#
74+
# #username - An alias for #authcid.
7275
# * #password ― Password or passphrase associated with this #username.
7376
# * _optional_ #authzid ― Alternate identity to act as or on behalf of.
7477
# * _optional_ #min_iterations - Overrides the default value (4096).
7578
#
7679
# Any other keyword parameters are quietly ignored.
7780
def initialize(username_arg = nil, password_arg = nil,
78-
username: nil, password: nil, authcid: nil, authzid: nil,
81+
authcid: nil, username: nil,
82+
authzid: nil,
83+
password: nil,
7984
min_iterations: 4096, # see both RFC5802 and RFC7677
8085
cnonce: nil, # must only be set in tests
8186
**options)
@@ -92,6 +97,7 @@ def initialize(username_arg = nil, password_arg = nil,
9297
@min_iterations = Integer min_iterations
9398
@min_iterations.positive? or
9499
raise ArgumentError, "min_iterations must be positive"
100+
95101
@cnonce = cnonce || SecureRandom.base64(32)
96102
end
97103

lib/net/imap/sasl/xoauth2_authenticator.rb

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,19 @@ class Net::IMAP::SASL::XOAuth2Authenticator
3434
# relying only the identity and scope authorized by the token.
3535
attr_reader :username
3636

37+
# Note that, unlike most other authenticators, #username is an alias for the
38+
# authorization identity and not the authentication identity. The
39+
# authenticated identity is established for the client by the #oauth2_token.
40+
alias authzid username
41+
3742
# An OAuth2 access token which has been authorized with the appropriate OAuth2
3843
# scopes to use the service for #username.
3944
attr_reader :oauth2_token
4045

4146
# :call-seq:
4247
# new(username, oauth2_token, **) -> authenticator
4348
# new(username:, oauth2_token:, **) -> authenticator
49+
# new(authzid:, oauth2_token:, **) -> authenticator
4450
#
4551
# Creates an Authenticator for the "+XOAUTH2+" SASL mechanism, as specified by
4652
# Google[https://developers.google.com/gmail/imap/xoauth2-protocol],
@@ -50,13 +56,21 @@ class Net::IMAP::SASL::XOAuth2Authenticator
5056
# === Properties
5157
#
5258
# * #username --- the username for the account being accessed.
59+
#
60+
# #authzid --- an alias for #username.
61+
#
62+
# Note that, unlike some other authenticators, +username+ sets the
63+
# _authorization_ identity and not the _authentication_ identity. The
64+
# authenticated identity is established for the client with the OAuth token.
65+
#
5366
# * #oauth2_token --- An OAuth2.0 access token which is authorized to access
5467
# the service for #username.
5568
#
5669
# Any other keyword parameters are quietly ignored.
57-
def initialize(user = nil, token = nil, username: nil, oauth2_token: nil, **)
58-
@username = username || user or
59-
raise ArgumentError, "missing username"
70+
def initialize(user = nil, token = nil, username: nil, oauth2_token: nil,
71+
authzid: nil, **)
72+
@username = authzid || username || user or
73+
raise ArgumentError, "missing username (authzid)"
6074
@oauth2_token = oauth2_token || token or
6175
raise ArgumentError, "missing oauth2_token"
6276
[username, user].compact.count == 1 or

test/net/imap/test_imap_authenticators.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,34 @@ def test_plain_supports_initial_response
4545

4646
def test_plain_response
4747
assert_equal("\0authc\0passwd", plain("authc", "passwd").process(nil))
48+
end
49+
50+
def test_plain_authzid
4851
assert_equal("authz\0user\0pass",
4952
plain("user", "pass", authzid: "authz").process(nil))
5053
end
5154

55+
def test_plain_kw_params
56+
assert_equal(
57+
"zid\0cid\0p",
58+
plain(authcid: "cid", password: "p", authzid: "zid").process(nil)
59+
)
60+
end
61+
62+
def test_plain_username_kw_sets_both_authcid_and_authzid
63+
assert_equal(
64+
"\0uname\0passwd",
65+
plain(username: "uname", password: "passwd").process(nil)
66+
)
67+
end
68+
5269
def test_plain_no_null_chars
5370
assert_raise(ArgumentError) { plain("bad\0user", "pass") }
5471
assert_raise(ArgumentError) { plain("user", "bad\0pass") }
72+
assert_raise(ArgumentError) { plain(authcid: "bad\0user", password: "p") }
73+
assert_raise(ArgumentError) { plain(username: "bad\0user", password: "p") }
74+
assert_raise(ArgumentError) { plain(username: "u", password: "bad\0pass") }
75+
assert_raise(ArgumentError) { plain("u", "p", authzid: "bad\0authz") }
5576
assert_raise(ArgumentError) { plain("u", "p", authzid: "bad\0authz") }
5677
end
5778

@@ -244,7 +265,11 @@ def test_external_matches_mechanism
244265

245266
def test_external_response
246267
assert_equal("", external.process(nil))
268+
assert_equal("", external.process(""))
247269
assert_equal("kwarg", external(authzid: "kwarg").process(nil))
270+
assert_equal("username", external(username: "username").process(nil))
271+
assert_equal("z", external("p", authzid: "z", username: "u").process(nil))
272+
assert_equal("positional", external("positional").process(nil))
248273
end
249274

250275
def test_external_utf8
@@ -256,7 +281,6 @@ def test_external_utf8
256281
def test_external_invalid
257282
assert_raise(ArgumentError) { external(authzid: "bad\0contains NULL") }
258283
assert_raise(ArgumentError) { external(authzid: "invalid utf8\x80") }
259-
assert_raise(ArgumentError) { external("invalid positional argument") }
260284
end
261285

262286
# ----------------------

0 commit comments

Comments
 (0)