Skip to content

Commit

Permalink
🔒 Clarify usage of username vs authcid vs authzid
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
nevans committed Oct 19, 2023
1 parent 5bc8bff commit 5f9027e
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 19 deletions.
14 changes: 10 additions & 4 deletions lib/net/imap/sasl/digest_md5_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ class Net::IMAP::SASL::DigestMD5Authenticator
# "Authentication identity" is the generic term used by
# RFC-4422[https://tools.ietf.org/html/rfc4422].
# RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs abbreviate
# that to +authcid+. So +authcid+ is available as an alias for #username.
# this to +authcid+.
attr_reader :username
alias authcid username

# A password or passphrase that matches the #username.
#
Expand All @@ -44,16 +45,19 @@ class Net::IMAP::SASL::DigestMD5Authenticator
# :call-seq:
# new(username, password, authzid = nil, **options) -> authenticator
# new(username:, password:, authzid: nil, **options) -> authenticator
# new(authcid:, password:, authzid: nil, **options) -> authenticator
#
# Creates an Authenticator for the "+DIGEST-MD5+" SASL mechanism.
#
# Called by Net::IMAP#authenticate and similar methods on other clients.
#
# ==== Parameters
#
# * #username — Identity whose #password is used.
# * #password — A password or passphrase associated with this #username.
# * #authcid ― Authentication identity that is associated with #password.
#
# #username ― An alias for +authcid+.
#
# * #password ― A password or passphrase associated with this #authcid.
#
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
#
Expand All @@ -65,8 +69,10 @@ class Net::IMAP::SASL::DigestMD5Authenticator
# Any other keyword arguments are silently ignored.
def initialize(user = nil, pass = nil, authz = nil,
username: nil, password: nil, authzid: nil,
authcid: nil,
warn_deprecation: true, **)
username ||= user or raise ArgumentError, "missing username"
username = authcid || username || user or
raise ArgumentError, "missing username (authcid)"
password ||= pass or raise ArgumentError, "missing password"
authzid ||= authz
if warn_deprecation
Expand Down
13 changes: 12 additions & 1 deletion lib/net/imap/sasl/external_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ class ExternalAuthenticator
# imap.authenticate "PLAIN", "root", passwd, authzid: "user"
#
attr_reader :authzid
alias username authzid

# :call-seq:
# new(authzid: nil, **) -> authenticator
# new(username: nil, **) -> authenticator
# new(username = nil, **) -> authenticator
#
# Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as
# specified in RFC-4422[https://tools.ietf.org/html/rfc4422]. To use
Expand All @@ -38,8 +41,16 @@ class ExternalAuthenticator
#
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
#
# _optional_ #username ― An alias for #authzid.
#
# Note that, unlike some other authenticators, +username+ sets the
# _authorization_ identity and not the _authentication_ identity. The
# authentication identity is established for the client by the
# external credentials.
#
# Any other keyword parameters are quietly ignored.
def initialize(authzid: nil, **)
def initialize(user = nil, authzid: nil, username: nil, **)
authzid ||= username || user
@authzid = authzid&.to_str&.encode "UTF-8"
if @authzid&.match?(/\u0000/u) # also validates UTF8 encoding
raise ArgumentError, "contains NULL"
Expand Down
25 changes: 23 additions & 2 deletions lib/net/imap/sasl/oauthbearer_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class OAuthAuthenticator
# imap.authenticate "PLAIN", "root", passwd, authzid: "user"
#
attr_reader :authzid
alias username authzid

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

# The query string. (optional)
attr_reader :qs
alias query qs

# Stores the most recent server "challenge". When authentication fails,
# this may hold information about the failure reason, as JSON.
Expand All @@ -61,23 +63,34 @@ class OAuthAuthenticator
# #host or #port) <b>as are specific server implementations</b>.
#
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
#
# _optional_ #username — An alias for #authzid.
#
# Note that, unlike some other authenticators, +username+ sets the
# _authorization_ identity and not the _authentication_ identity. The
# authentication identity is established for the client by the OAuth
# token.
#
# * _optional_ #host — Hostname to which the client connected.
# * _optional_ #port — Service port to which the client connected.
# * _optional_ #mthd — HTTP method
# * _optional_ #path — HTTP path data
# * _optional_ #post — HTTP post data
# * _optional_ #qs — HTTP query string
#
# _optional_ #query — An alias for #qs
#
# Any other keyword parameters are quietly ignored.
def initialize(authzid: nil, host: nil, port: nil,
username: nil, query: nil,
mthd: nil, path: nil, post: nil, qs: nil, **)
@authzid = authzid
@authzid = authzid || username
@host = host
@port = port
@mthd = mthd
@path = path
@post = post
@qs = qs
@qs = qs || query
@done = false
end

Expand Down Expand Up @@ -144,6 +157,14 @@ class OAuthBearerAuthenticator < OAuthAuthenticator
# The most common ones are:
#
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
#
# _optional_ #username — An alias for #authzid.
#
# Note that, unlike some other authenticators, +username+ sets the
# _authorization_ identity and not the _authentication_ identity. The
# authentication identity is established for the client by
# #oauth2_token.
#
# * _optional_ #host — Hostname to which the client connected.
# * _optional_ #port — Service port to which the client connected.
#
Expand Down
19 changes: 13 additions & 6 deletions lib/net/imap/sasl/plain_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Net::IMAP::SASL::PlainAuthenticator
# RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs abbreviate
# this to +authcid+.
attr_reader :username
alias authcid username

# A password or passphrase that matches the #username.
attr_reader :password
Expand All @@ -42,15 +43,19 @@ class Net::IMAP::SASL::PlainAuthenticator
# :call-seq:
# new(username, password, authzid: nil, **) -> authenticator
# new(username:, password:, authzid: nil, **) -> authenticator
# new(authcid:, password:, authzid: nil, **) -> authenticator
#
# Creates an Authenticator for the "+PLAIN+" SASL mechanism.
#
# Called by Net::IMAP#authenticate and similar methods on other clients.
#
# ==== Parameters
#
# * #username ― Identity whose +password+ is used.
# * #password ― Password or passphrase associated with this username+.
# * #authcid ― Authentication identity that is associated with #password.
#
# #username ― An alias for #authcid.
#
# * #password ― A password or passphrase associated with the #authcid.
#
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
#
Expand All @@ -59,12 +64,14 @@ class Net::IMAP::SASL::PlainAuthenticator
#
# Any other keyword parameters are quietly ignored.
def initialize(user = nil, pass = nil,
authcid: nil,
username: nil, password: nil, authzid: nil, **)
[username, user].compact.count == 1 or
raise ArgumentError, "conflicting values for username"
[password, pass].compact.count == 1 or
[authcid, username, user].compact.count <= 1 or
raise ArgumentError, "conflicting values for username (authcid)"
[password, pass].compact.count <= 1 or
raise ArgumentError, "conflicting values for password"
username ||= user or raise ArgumentError, "missing username"
username ||= authcid || user or
raise ArgumentError, "missing username (authcid)"
password ||= pass or raise ArgumentError, "missing password"
raise ArgumentError, "username contains NULL" if username.include?(NULL)
raise ArgumentError, "password contains NULL" if password.include?(NULL)
Expand Down
10 changes: 8 additions & 2 deletions lib/net/imap/sasl/scram_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class ScramAuthenticator
# :call-seq:
# new(username, password, **options) -> auth_ctx
# new(username:, password:, **options) -> auth_ctx
# new(authcid:, password:, **options) -> auth_ctx
#
# Creates an authenticator for one of the "+SCRAM-*+" SASL mechanisms.
# Each subclass defines #digest to match a specific mechanism.
Expand All @@ -68,14 +69,18 @@ class ScramAuthenticator
#
# === Parameters
#
# * #username ― Identity whose #password is used. Aliased as #authcid.
# * #authcid ― Identity whose #password is used.
#
# #username - An alias for #authcid.
# * #password ― Password or passphrase associated with this #username.
# * _optional_ #authzid ― Alternate identity to act as or on behalf of.
# * _optional_ #min_iterations - Overrides the default value (4096).
#
# Any other keyword parameters are quietly ignored.
def initialize(username_arg = nil, password_arg = nil,
username: nil, password: nil, authcid: nil, authzid: nil,
authcid: nil, username: nil,
authzid: nil,
password: nil,
min_iterations: 4096, # see both RFC5802 and RFC7677
cnonce: nil, # must only be set in tests
**options)
Expand All @@ -92,6 +97,7 @@ def initialize(username_arg = nil, password_arg = nil,
@min_iterations = Integer min_iterations
@min_iterations.positive? or
raise ArgumentError, "min_iterations must be positive"

@cnonce = cnonce || SecureRandom.base64(32)
end

Expand Down
20 changes: 17 additions & 3 deletions lib/net/imap/sasl/xoauth2_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,19 @@ class Net::IMAP::SASL::XOAuth2Authenticator
# relying only the identity and scope authorized by the token.
attr_reader :username

# Note that, unlike most other authenticators, #username is an alias for the
# authorization identity and not the authentication identity. The
# authenticated identity is established for the client by the #oauth2_token.
alias authzid username

# An OAuth2 access token which has been authorized with the appropriate OAuth2
# scopes to use the service for #username.
attr_reader :oauth2_token

# :call-seq:
# new(username, oauth2_token, **) -> authenticator
# new(username:, oauth2_token:, **) -> authenticator
# new(authzid:, oauth2_token:, **) -> authenticator
#
# Creates an Authenticator for the "+XOAUTH2+" SASL mechanism, as specified by
# Google[https://developers.google.com/gmail/imap/xoauth2-protocol],
Expand All @@ -50,13 +56,21 @@ class Net::IMAP::SASL::XOAuth2Authenticator
# === Properties
#
# * #username --- the username for the account being accessed.
#
# #authzid --- an alias for #username.
#
# Note that, unlike some other authenticators, +username+ sets the
# _authorization_ identity and not the _authentication_ identity. The
# authenticated identity is established for the client with the OAuth token.
#
# * #oauth2_token --- An OAuth2.0 access token which is authorized to access
# the service for #username.
#
# Any other keyword parameters are quietly ignored.
def initialize(user = nil, token = nil, username: nil, oauth2_token: nil, **)
@username = username || user or
raise ArgumentError, "missing username"
def initialize(user = nil, token = nil, username: nil, oauth2_token: nil,
authzid: nil, **)
@username = authzid || username || user or
raise ArgumentError, "missing username (authzid)"
@oauth2_token = oauth2_token || token or
raise ArgumentError, "missing oauth2_token"
[username, user].compact.count == 1 or
Expand Down
26 changes: 25 additions & 1 deletion test/net/imap/test_imap_authenticators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,34 @@ def test_plain_supports_initial_response

def test_plain_response
assert_equal("\0authc\0passwd", plain("authc", "passwd").process(nil))
end

def test_plain_authzid
assert_equal("authz\0user\0pass",
plain("user", "pass", authzid: "authz").process(nil))
end

def test_plain_kw_params
assert_equal(
"zid\0cid\0p",
plain(authcid: "cid", password: "p", authzid: "zid").process(nil)
)
end

def test_plain_username_kw_sets_both_authcid_and_authzid
assert_equal(
"\0uname\0passwd",
plain(username: "uname", password: "passwd").process(nil)
)
end

def test_plain_no_null_chars
assert_raise(ArgumentError) { plain("bad\0user", "pass") }
assert_raise(ArgumentError) { plain("user", "bad\0pass") }
assert_raise(ArgumentError) { plain(authcid: "bad\0user", password: "p") }
assert_raise(ArgumentError) { plain(username: "bad\0user", password: "p") }
assert_raise(ArgumentError) { plain(username: "u", password: "bad\0pass") }
assert_raise(ArgumentError) { plain("u", "p", authzid: "bad\0authz") }
assert_raise(ArgumentError) { plain("u", "p", authzid: "bad\0authz") }
end

Expand Down Expand Up @@ -244,7 +265,11 @@ def test_external_matches_mechanism

def test_external_response
assert_equal("", external.process(nil))
assert_equal("", external.process(""))
assert_equal("kwarg", external(authzid: "kwarg").process(nil))
assert_equal("username", external(username: "username").process(nil))
assert_equal("z", external("p", authzid: "z", username: "u").process(nil))
assert_equal("positional", external("positional").process(nil))
end

def test_external_utf8
Expand All @@ -256,7 +281,6 @@ def test_external_utf8
def test_external_invalid
assert_raise(ArgumentError) { external(authzid: "bad\0contains NULL") }
assert_raise(ArgumentError) { external(authzid: "invalid utf8\x80") }
assert_raise(ArgumentError) { external("invalid positional argument") }
end

# ----------------------
Expand Down

0 comments on commit 5f9027e

Please sign in to comment.