Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔒 SASL: Clarify usage of username vs authcid vs authzid #187

Merged
merged 5 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
10 changes: 7 additions & 3 deletions lib/net/imap/sasl/cram_md5_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@
# of cleartext and recommends TLS version 1.2 or greater be used for all
# traffic. With TLS +CRAM-MD5+ is okay, but so is +PLAIN+
class Net::IMAP::SASL::CramMD5Authenticator
def initialize(user, password, warn_deprecation: true, **_ignored)
def initialize(user = nil, pass = nil,
authcid: nil, username: nil,
password: nil,
warn_deprecation: true,
**)
if warn_deprecation
warn "WARNING: CRAM-MD5 mechanism is deprecated." # TODO: recommend SCRAM
end
require "digest/md5"
@user = user
@password = password
@user = authcid || username || user
@password = password || pass
@done = false
end

Expand Down
26 changes: 19 additions & 7 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,34 @@ 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.
# * #authcid ― Authentication identity that is associated with #password.
#
# See the documentation for each attribute for more details.
# #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.
#
# When +authzid+ is not set, the server should derive the authorization
# identity from the authentication identity.
#
# * _optional_ +warn_deprecation+ — Set to +false+ to silence the warning.
#
# 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
31 changes: 26 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,45 @@ 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, +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
10 changes: 7 additions & 3 deletions lib/net/imap/sasl/login_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,16 @@ class Net::IMAP::SASL::LoginAuthenticator
STATE_DONE = :DONE
private_constant :STATE_USER, :STATE_PASSWORD, :STATE_DONE

def initialize(user, password, warn_deprecation: true, **_ignored)
def initialize(user = nil, pass = nil,
authcid: nil, username: nil,
password: nil,
warn_deprecation: true,
**)
if warn_deprecation
warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead."
end
@user = user
@password = password
@user = authcid || username || user
@password = password || pass
@state = STATE_USER
end

Expand Down
106 changes: 69 additions & 37 deletions lib/net/imap/sasl/oauthbearer_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,25 @@ 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.
# Hostname to which the client connected. (optional)
attr_reader :host

# Service port to which the client connected.
# Service port to which the client connected. (optional)
attr_reader :port

# HTTP method. (optional)
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,42 @@ 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>.
#
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
#
# _optional_ #username — An alias for #authzid.
#
# 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.
# 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.
#
# * #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
# * _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,34 +137,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, **options) -> authenticator
# new(authzid, oauth2_token, **options) -> authenticator
# new(oauth2_token:, **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] (see OAuthAuthenticator).
# 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, +username+ sets the
# _authorization_ identity and not the _authentication_ identity. The
# authentication identity is established for the client by
# #oauth2_token.
#
# * #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.
# * _optional_ #host — Hostname to which the client connected.
# * _optional_ #port — Service port to which the client connected.
#
def initialize(oauth2_token_arg = nil, oauth2_token: nil, **args, &blk)
super(**args, &blk) # handles authzid, host, port, etc
oauth2_token && oauth2_token_arg and
raise ArgumentError, "conflicting values for oauth2_token"
# Although only oauth2_token is required by this mechanism, it is worth
# noting that <b><em>application protocols are allowed to
# require</em></b> #authzid (<em>or other parameters, such as</em> #host
# _or_ #port) <b><em>as are specific server implementations</em></b>.
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 || oauth2_token_arg or
raise ArgumentError, "missing oauth2_token"
end
Expand Down
27 changes: 17 additions & 10 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,31 @@ 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.
# * #authcid ― Authentication identity that is associated with #password.
#
# See attribute documentation for more details.
# #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.
#
# When +authzid+ is not set, the server should derive the authorization
# identity from the authentication identity.
#
# 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
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
Loading