diff --git a/lib/net/imap/sasl.rb b/lib/net/imap/sasl.rb index c13de790..a50ce8be 100644 --- a/lib/net/imap/sasl.rb +++ b/lib/net/imap/sasl.rb @@ -43,6 +43,10 @@ class IMAP # resources without authenticating or disclosing an # identity. # + # +EXTERNAL+:: See ExternalAuthenticator. + # Login using already established credentials, such as a TLS + # certificate or IPsec. + # # === Deprecated mechanisms # # Obsolete mechanisms are available for backwards compatibility. Using @@ -68,6 +72,7 @@ module SASL autoload :Authenticator, "#{sasl_dir}/authenticator" autoload :Authenticators, "#{sasl_dir}/authenticators" autoload :AnonymousAuthenticator, "#{sasl_dir}/anonymous_authenticator" + autoload :ExternalAuthenticator, "#{sasl_dir}/external_authenticator" autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator" autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator" @@ -79,6 +84,7 @@ module SASL def self.authenticators @authenticators ||= SASL::Authenticators.new.tap do |registry| registry.add_authenticator "Anonymous" + registry.add_authenticator "External" registry.add_authenticator "Plain" registry.add_authenticator "XOAuth2" registry.add_authenticator "Login" # deprecated diff --git a/lib/net/imap/sasl/external_authenticator.rb b/lib/net/imap/sasl/external_authenticator.rb new file mode 100644 index 00000000..fb5f0fd4 --- /dev/null +++ b/lib/net/imap/sasl/external_authenticator.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require_relative "authenticator" + +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 < Authenticator + + # :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 + # new {|propname, auth_ctx| propval } -> 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. + # + # ==== Properties + # Only one property, 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. + # + # See Net::IMAP::SASL::Authenticator@Properties for a detailed + # description of property assignment, lazy loading, and callbacks. + def initialize(authzid_arg = nil, authzid: nil) + super + propinit :authzid, authzid, authzid_arg + end + + property :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 + + private + + NULL = "\0" + + def propset(name, value) + if name == :authzid && !value.nil? + raise ArgumentError, "#{name} contains NULL" if value.include? NULL + value = value.encode "UTF-8" + unless value.valid_encoding? + raise ArgumentError, "#{name} isn't valid UTF-8" + end + end + super(name, value) + end + + end + end + end +end diff --git a/test/net/imap/test_imap_authenticators.rb b/test/net/imap/test_imap_authenticators.rb index 64c92ebc..3a4d6185 100644 --- a/test/net/imap/test_imap_authenticators.rb +++ b/test/net/imap/test_imap_authenticators.rb @@ -86,6 +86,37 @@ def test_xoauth2_callbacks ) 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 + # ---------------------- # ANONYMOUS # ----------------------