Skip to content

Commit

Permalink
✨ Aliases for username <=> authcid or authzid
Browse files Browse the repository at this point in the history
Some of the RFCs are clearer than others about the distinction between
authentication identity and authorization identity.  And `username` is
used by some specifications to mean `authcid` and by others to mean
`authzid`.  To clarify the different roles of `username`, I've updated
every relevant mechanism to have `authcid` and/or `authzid` that use it
as an alias to username (or vice versa).  Additionally, the docs for
many mechanisms has been improved.
  • Loading branch information
nevans committed Oct 10, 2023
1 parent 573b680 commit edb6040
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 61 deletions.
5 changes: 3 additions & 2 deletions lib/net/imap/sasl/anonymous_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ class AnonymousAuthenticator
# this, see Net::IMAP#authenticate or your client's authentication
# method.
#
# #anonymous_message is an optional message which is sent to the server.
# It may be sent as a positional argument or as a keyword argument.
# ==== Parameters
#
# * _optional_ #anonymous_message — a message to send to the server.
#
# Any other keyword arguments are silently ignored.
def initialize(anon_msg = nil, anonymous_message: nil, **)
Expand Down
22 changes: 16 additions & 6 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,23 +45,32 @@ 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.
# * #authzid ― Alternate identity to act as or on behalf of. Optional.
# * +warn_deprecation+ — Set to +false+ to silence the warning.
# * #username ― Authentication identity that is associated with #password.
# * #authcid ― An alias for +username+.
# * #password ― A password or passphrase associated with this #username.
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
# * _optional_ +warn_deprecation+ — Set to +false+ to silence the warning.
#
# Any other keyword arguments are silently ignored.
#
# When +authzid+ is not set, the server should derive the authorization
# identity from the authentication identity.
#
# See the documentation for each attribute for more details.
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
30 changes: 25 additions & 5 deletions lib/net/imap/sasl/external_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,44 @@ module SASL
# established external to SASL, for example by TLS certificate or IPsec.
class ExternalAuthenticator

# Authorization identity: an identity to act as or on behalf of.
# Authorization identity: an identity to act as or on behalf of. The
# identity form is application protocol specific. If not provided or
# left blank, the server derives an authorization identity from the
# authentication identity. The server is responsible for verifying the
# client's credentials and verifying that the identity it associates
# with the client's authentication identity is allowed to act as (or on
# behalf of) the authorization identity.
#
# For example, an administrator or superuser might take on another role:
#
# imap.authenticate "PLAIN", "root", passwd, authzid: "user"
#
# If not explicitly provided, the server defaults to using the identity
# that was authenticated by the external credentials.
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
# this, see Net::IMAP#authenticate or your client's authentication
# method.
#
# #authzid is an optional identity to act as or on behalf of.
# ==== Parameters
#
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
# * _optional_ #username ― An alias for #authzid.
#
# Note that, unlike some other authenticators, the +username+ keyword
# parameter sets the authorization identity and not the authentication
# identity. The authenticated identity is already 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
97 changes: 64 additions & 33 deletions lib/net/imap/sasl/oauthbearer_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ module SASL
class OAuthAuthenticator
include GS2Header

# Authorization identity: an identity to act as or on behalf of.
# Authorization identity: an identity to act as or on behalf of. The
# identity form is application protocol specific. If not provided or
# left blank, the server derives an authorization identity from the
# authentication identity. The server is responsible for verifying the
# client's credentials and verifying that the identity it associates
# with the client's authentication identity is allowed to act as (or on
# behalf of) the authorization identity.
#
# For example, an administrator or superuser might take on another role:
#
# imap.authenticate "PLAIN", "root", passwd, authzid: "user"
#
# If no explicit authorization identity is provided, it is usually
# derived from the authentication identity. For the OAuth-based
# mechanisms, the authentication identity is the identity established by
# the OAuth credential.
attr_reader :authzid
alias username authzid

# Hostname to which the client connected.
attr_reader :host
Expand All @@ -39,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 @@ -47,29 +55,39 @@ class OAuthAuthenticator
# Creates an RFC7628[https://tools.ietf.org/html/rfc7628] OAuth
# authenticator.
#
# === Options
# ==== Parameters
#
# See child classes for required parameter(s). The following parameters
# are all optional, but it is worth noting that <b>application protocols
# are allowed to require</b> #authzid (or other parameters, such as
# #host or #port) <b>as are specific server implementations</b>.
#
# See child classes for required configuration parameter(s). The
# following parameters are all optional, but protocols or servers may
# add requirements for #authzid, #host, #port, or any other parameter.
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
# * _optional_ #username — An alias for #authzid.
#
# * #authzid ― Identity to act as or on behalf of.
# * #host — Hostname to which the client connected.
# * #port — Service port to which the client connected.
# * #mthd — HTTP method
# * #path — HTTP path data
# * #post — HTTP post data
# * #qs — HTTP query string
# Note that, unlike some other authenticators, the +username+ keyword
# parameter sets the authorization identity and not the authentication
# identity. The authenticated identity is established with 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 @@ -116,32 +134,45 @@ def authorization; raise "must be implemented by subclass" end
# the bearer token.
class OAuthBearerAuthenticator < OAuthAuthenticator

# An OAuth2 bearer token, generally the access token.
# An OAuth 2.0 bearer token. See {RFC-6750}[https://www.rfc-editor.org/rfc/rfc6750]
attr_reader :oauth2_token

# :call-seq:
# new(oauth2_token, **options) -> authenticator
# new(oauth2_token:, **options) -> authenticator
# new(oauth2_token:, authzid: nil, host: nil, port: nil, **options) -> authenticator
# new(oauth2_token:, username: nil, host: nil, port: nil, **options) -> authenticator
# new(oauth2_token, authzid: nil, host: nil, port: nil, **options) -> authenticator
# new(authzid, oauth2_token, host: nil, port: nil, **options) -> authenticator
#
# Creates an Authenticator for the "+OAUTHBEARER+" SASL mechanism.
#
# Called by Net::IMAP#authenticate and similar methods on other clients.
#
# === Options
# ==== Parameters
#
# * #oauth2_token — An OAuth2 bearer token
#
# All other keyword parameters are passed to
# {super}[rdoc-ref:OAuthAuthenticator::new]. The most common ones are:
#
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
# * _optional_ #username — An alias for #authzid.
#
# Only +oauth2_token+ is required by the mechanism, however protocols
# and servers may add requirements for #authzid, #host, #port, or any
# other parameter.
# Note that, unlike some other authenticators, the +username+ keyword
# parameter sets the authorization identity and not the authentication
# identity. The authenticated identity is established for the client
# with the #oauth2_token.
# * _optional_ #host — Hostname to which the client connected.
# * _optional_ #port — Service port to which the client connected.
#
# * #oauth2_token — An OAuth2 bearer token or access token. *Required.*
# May be provided as either regular or keyword argument.
# * #authzid ― Identity to act as or on behalf of.
# * #host — Hostname to which the client connected.
# * #port — Service port to which the client connected.
# * See OAuthAuthenticator documentation for less common parameters.
# Only +oauth2_token+ is required by this mechanism, but it is worth
# noting that <b>application protocols are allowed to require</b>
# #authzid (or other parameters, such as #host or #port) <b>as are
# specific server implementations</b>.
#
def initialize(oauth2_token_arg = nil, oauth2_token: nil, **args, &blk)
super(**args, &blk) # handles authzid, host, port, etc
# See OAuthAuthenticator documentation for other parameters.
def initialize(arg1 = nil, arg2 = nil, oauth2_token: nil, **args, &blk)
username, oauth2_token_arg = arg2.nil? ? [nil, arg1] : [arg1, arg2]
super(username: username, **args, &blk)
oauth2_token && oauth2_token_arg and
raise ArgumentError, "conflicting values for oauth2_token"
@oauth2_token = oauth2_token || oauth2_token_arg or
Expand Down
26 changes: 18 additions & 8 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,25 +43,34 @@ 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
# ==== Parameters
#
# * #username ― Identity whose +password+ is used.
# * #password ― Password or passphrase associated with this username+.
# * #authzid ― Alternate identity to act as or on behalf of. Optional.
# * #username ― Authentication identity that is associated with #password.
# * #authcid ― An alias for #username.
# * #password ― A password or passphrase associated with #username.
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
#
# When +authzid+ is not set, the server should derive the authorization
# identity from the authentication identity.
#
# Any other keyword parameters are quietly ignored.
#
# See attribute documentation for more details.
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
15 changes: 13 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,17 @@ class ScramAuthenticator
#
# === Parameters
#
# * #username ― Identity whose #password is used. Aliased as #authcid.
# * #username ― Identity whose #password is used.
# * #authcid - An alias for #username.
# * #password ― Password or passphrase associated with this #username.
# * #authzid ― Alternate identity to act as or on behalf of. Optional.
# * #min_iterations - Overrides the default value (4096). Optional.
#
# See the documentation on the corresponding attributes for more.
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,10 +96,17 @@ 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

# Authentication identity: the identity that matches the #password.
#
# RFC-2831[https://tools.ietf.org/html/rfc2831] uses the term +username+.
# "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
# this to +authcid+.
attr_reader :username
alias authcid username

Expand Down
Loading

0 comments on commit edb6040

Please sign in to comment.