Skip to content

Commit

Permalink
🔒 Add SASL EXTERNAL mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
nevans committed Sep 10, 2023
1 parent ad5cbfc commit 059698f
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 0 deletions.
6 changes: 6 additions & 0 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,12 @@ def starttls(options = {}, verify = true)
# Each mechanism has different properties and requirements. Please consult
# the documentation for the specific mechanisms you are using:
#
# +EXTERNAL+::
# See ExternalAuthenticator[Net::IMAP::SASL::ExternalAuthenticator].
#
# Login using already established credentials, such as a TLS certificate
# or IPsec.
#
# +PLAIN+::
# See PlainAuthenticator[Net::IMAP::SASL::PlainAuthenticator].
#
Expand Down
7 changes: 7 additions & 0 deletions lib/net/imap/sasl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ class IMAP
# Each mechanism has different properties and requirements. Please consult
# the documentation for the specific mechanisms you are using:
#
# +EXTERNAL+::
# See ExternalAuthenticator[Net::IMAP::SASL::ExternalAuthenticator].
#
# Login using already established credentials, such as a TLS certificate
# or IPsec.
#
# +PLAIN+::
# See PlainAuthenticator[Net::IMAP::SASL::PlainAuthenticator].
#
Expand Down Expand Up @@ -70,6 +76,7 @@ module SASL
sasl_dir = File.expand_path("sasl", __dir__)
autoload :Authenticators, "#{sasl_dir}/authenticators"

autoload :ExternalAuthenticator, "#{sasl_dir}/external_authenticator"
autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"

Expand Down
1 change: 1 addition & 0 deletions lib/net/imap/sasl/authenticators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Authenticators
def initialize(use_defaults: false)
@authenticators = {}
if use_defaults
add_authenticator "External"
add_authenticator "Plain"
add_authenticator "XOAuth2"
add_authenticator "Login" # deprecated
Expand Down
77 changes: 77 additions & 0 deletions lib/net/imap/sasl/external_authenticator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

module Net
class IMAP < Protocol
module SASL

# Authenticator for the "+EXTERNAL+" SASL mechanism, as specified by
# RFC-4422[https://tools.ietf.org/html/rfc4422]. See
# Net::IMAP#authenticate.
#
# The EXTERNAL mechanism requests that the server use client credentials
# established external to SASL, for example by TLS certificate or IPsec.
#
class ExternalAuthenticator

# :call-seq:
# initial_response? -> true
#
# +EXTERNAL+ can send an initial client response.
def initial_response?; true end

##
# :call-seq:
# new -> authenticator
# new(authzid, **) -> authenticator
# new(authzid:, **) -> 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.
#
# ==== Configuration parameters
# Only one parameter, which is optional:
#
# * #authzid -- the identity to act as. Leave blank to use the identity
# associated with the client's credentials.
#
# May be sent as a positional argument or as a keyword argument.
#
def initialize(authzid_arg = nil, authzid: nil)
@authzid = authzid || authzid_arg
[authzid, authzid_arg].compact.count <= 1 or
raise ArgumentError, "conflicting values for authzid"
if @authzid
raise ArgumentError, "contains NULL" if @authzid.include? NULL
@authzid = @authzid.encode "UTF-8"
@authzid.valid_encoding? or raise ArgumentError, "not valid UTF-8"
end
end

attr_reader :authzid

def process(_)
return "" if authzid.nil?
if /\u0000/u.match?(authzid) # also validates UTF8 encoding
raise DataFormatError, "authzid contains NULL"
end
authzid.encode "UTF-8"
end

def authzid=(value)
raise ArgumentError, "contains NULL" if value.include? NULL
value = value.encode "UTF-8"
value.valid_encoding? or raise ArgumentError, "not valid UTF-8"
super(value)
end

private

NULL = "\0"
private_constant :NULL

end
end
end
end
31 changes: 31 additions & 0 deletions test/net/imap/test_imap_authenticators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,37 @@ def test_xoauth2_supports_initial_response
assert Net::IMAP::SASL.initial_response?(xoauth2("foo", "bar"))
end

# ----------------------
# EXTERNAL
# ----------------------

def external(*args, **kwargs, &block)
Net::IMAP::SASL.authenticator("EXTERNAL", *args, **kwargs, &block)
end

def test_external_matches_mechanism
assert_kind_of(Net::IMAP::SASL::ExternalAuthenticator, external)
end

def test_external_response
assert_equal("", external.process(nil))
assert_equal("hello world", external("hello world").process(nil))
assert_equal("kwargs",
external(authzid: "kwargs").process(nil))
end

def test_external_utf8
assert_equal("", external.process(nil))
assert_equal("🏴󠁧󠁢󠁥󠁮󠁧󠁿 England", external("🏴󠁧󠁢󠁥󠁮󠁧󠁿 England").process(nil))
assert_equal("kwargs",
external(authzid: "kwargs").process(nil))
end

def test_external_invalid
assert_raise(ArgumentError) { external("bad\0contains NULL") }
assert_raise(ArgumentError) { external("invalid utf8\x80") }
end

# ----------------------
# LOGIN (obsolete)
# ----------------------
Expand Down

0 comments on commit 059698f

Please sign in to comment.