Skip to content

Commit

Permalink
🚧 username vs authzid vs authcid
Browse files Browse the repository at this point in the history
  • Loading branch information
nevans committed Oct 4, 2023
1 parent a9459de commit 56459e1
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 108 deletions.
7 changes: 4 additions & 3 deletions lib/net/imap/sasl/anonymous_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,17 @@ class AnonymousAuthenticator
attr_reader :anonymous_message

# :call-seq:
# new(anonymous_message = "", **) -> authenticator
# new(anonymous_message: "", **) -> authenticator
# new(anonymous_message = "", **) -> authenticator
#
# Creates an Authenticator for the "+ANONYMOUS+" SASL mechanism, as
# specified in RFC-4505[https://tools.ietf.org/html/rfc4505]. To use
# 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
37 changes: 25 additions & 12 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 @@ -42,34 +43,46 @@ class Net::IMAP::SASL::DigestMD5Authenticator
attr_reader :authzid

# :call-seq:
# new(username, password, authzid = nil, **options) -> authenticator
# new(authcid:, password:, authzid: nil, **options) -> authenticator
# new(username:, password:, authzid: nil, **options) -> authenticator
# new(username, 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,
warn_deprecation: true, **)
username ||= user or raise ArgumentError, "missing username"
password ||= pass or raise ArgumentError, "missing password"
authzid ||= authz
authcid: nil, username: nil,
authzid: nil,
password: nil,
warn_deprecation: true,
**)
@username = authcid || username || user
@password = password || pass
@authzid = authzid || authz
@username or raise ArgumentError, "missing username (authcid)"
@password or raise ArgumentError, "missing password"

if warn_deprecation
warn "WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331."
# TODO: recommend SCRAM instead.
end
require "digest/md5"
require "strscan"
@username, @password, @authzid = username, password, authzid
@nc, @stage = {}, STAGE_ONE
end

Expand Down
40 changes: 30 additions & 10 deletions lib/net/imap/sasl/external_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,48 @@ 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

# An alias for #authzid.
alias username authzid

# :call-seq:
# new(authzid: nil, **) -> authenticator
# 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 established for the client
# with the OAuth credential.
#
# Any other keyword parameters are quietly ignored.
def initialize(authzid: nil, **)
@authzid = authzid&.to_str&.encode "UTF-8"
if @authzid&.match?(/\u0000/u) # also validates UTF8 encoding
raise ArgumentError, "contains NULL"
end
def initialize(user = nil, authzid: nil, username: nil, **)
@authzid = authzid || username || user
@authzid &&= @authzid.to_str.encode "UTF-8"
raise ArgumentError, "contains NULL" if @authzid&.match?(/\u0000/u)
@done = false
end

Expand Down
115 changes: 75 additions & 40 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,44 @@ 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 for the client
# with the #oauth2_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.
#
# 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 OAuth credential.
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,36 +139,48 @@ 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
#
# Only +oauth2_token+ is required by the mechanism, however protocols
# and servers may add requirements for #authzid, #host, #port, or any
# other parameter.
#
# * #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.
#
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"
@oauth2_token = oauth2_token || oauth2_token_arg or
raise ArgumentError, "missing oauth2_token"
# ==== 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.
#
# 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.
#
# 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>. At the time this was written,
# <em>Gmail and Yahoo Mail both required</em> +authzid+.
#
# 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 || oauth2_token_arg
@oauth2_token or raise ArgumentError, "missing oauth2_token"
end

# :call-seq:
Expand Down
42 changes: 25 additions & 17 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 @@ -40,34 +41,41 @@ class Net::IMAP::SASL::PlainAuthenticator
attr_reader :authzid

# :call-seq:
# new(username, password, authzid: nil, **) -> authenticator
# new(authcid:, password:, authzid: nil, **) -> authenticator
# new(username:, password:, authzid: nil, **) -> authenticator
# new(username, 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 ― 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.
#
# * #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.
# Any other keyword parameters are quietly ignored.
#
# When +authzid+ is not set, the server should derive the authorization
# identity from the authentication identity.
#
# See attribute documentation for more details.
def initialize(user = nil, pass = 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"
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)
raise ArgumentError, "authzid contains NULL" if authzid&.include?(NULL)
@username = username
@password = password
authcid: nil, username: nil,
authzid: nil,
password: nil,
**)
@username = authcid || username || user
@password = password || pass
@authzid = authzid
@username or raise ArgumentError, "missing username (authcid)"
@password or raise ArgumentError, "missing password"
raise ArgumentError, "username contains NULL" if @username.include?(NULL)
raise ArgumentError, "password contains NULL" if @password.include?(NULL)
raise ArgumentError, "authzid contains NULL" if @authzid&.include?(NULL)

@done = false
end

Expand Down
Loading

0 comments on commit 56459e1

Please sign in to comment.