From 0201cefe3aa77da9fd2731e6945fb2226bd920fc Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 20 Dec 2022 09:47:39 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20Add=20SASL=20EXTERNAL=20mechanis?= =?UTF-8?q?m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `EXTERNAL` SASL mechanism is specified by the core SASL specification, in RFC4422. --- lib/net/imap.rb | 6 +++ lib/net/imap/sasl.rb | 7 +++ lib/net/imap/sasl/authenticators.rb | 1 + lib/net/imap/sasl/external_authenticator.rb | 53 +++++++++++++++++++++ test/net/imap/test_imap_authenticators.rb | 29 +++++++++++ 5 files changed, 96 insertions(+) create mode 100644 lib/net/imap/sasl/external_authenticator.rb diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 2fd6e1532..a07d58586 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -1008,6 +1008,12 @@ def starttls(options = {}, verify = true) # Allows the user to gain access to public services or resources without # authenticating or disclosing an identity. # + # +EXTERNAL+:: + # See ExternalAuthenticator[Net::IMAP::SASL::ExternalAuthenticator]. + # + # Authenticates using already established credentials, such as a TLS + # certificate or IPsec. + # # +OAUTHBEARER+:: # See OAuthBearerAuthenticator[rdoc-ref:Net::IMAP::SASL::OAuthBearerAuthenticator]. # diff --git a/lib/net/imap/sasl.rb b/lib/net/imap/sasl.rb index 8efcee9c9..7ea0edc71 100644 --- a/lib/net/imap/sasl.rb +++ b/lib/net/imap/sasl.rb @@ -33,6 +33,12 @@ class IMAP # Allows the user to gain access to public services or resources without # authenticating or disclosing an identity. # + # +EXTERNAL+:: + # See ExternalAuthenticator[Net::IMAP::SASL::ExternalAuthenticator]. + # + # Authenticates using already established credentials, such as a TLS + # certificate or IPsec. + # # +OAUTHBEARER+:: # See OAuthBearerAuthenticator. # @@ -85,6 +91,7 @@ module SASL autoload :GS2Header, "#{sasl_dir}/gs2_header" autoload :AnonymousAuthenticator, "#{sasl_dir}/anonymous_authenticator" + autoload :ExternalAuthenticator, "#{sasl_dir}/external_authenticator" autoload :OAuthBearerAuthenticator, "#{sasl_dir}/oauthbearer_authenticator" autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator" autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator" diff --git a/lib/net/imap/sasl/authenticators.rb b/lib/net/imap/sasl/authenticators.rb index d2cb8cd33..ed55d1c6e 100644 --- a/lib/net/imap/sasl/authenticators.rb +++ b/lib/net/imap/sasl/authenticators.rb @@ -34,6 +34,7 @@ def initialize(use_defaults: false) @authenticators = {} if use_defaults add_authenticator "Anonymous" + add_authenticator "External" add_authenticator "OAuthBearer" add_authenticator "Plain" add_authenticator "XOAuth2" diff --git a/lib/net/imap/sasl/external_authenticator.rb b/lib/net/imap/sasl/external_authenticator.rb new file mode 100644 index 000000000..e49af4731 --- /dev/null +++ b/lib/net/imap/sasl/external_authenticator.rb @@ -0,0 +1,53 @@ +# 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: + # new(authzid: 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. + # + # 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 + end + + # Authorization identity: an identity to act as or on behalf of. + # + # If not explicitly provided, the server defaults to using the identity + # that was authenticated by the external credentials. + attr_reader :authzid + + # :call-seq: + # initial_response? -> true + # + # +EXTERNAL+ can send an initial client response. + def initial_response?; true end + + # Returns #authzid, or an empty string if there is no authzid. + def process(_) + authzid || "" + end + + end + end + end +end diff --git a/test/net/imap/test_imap_authenticators.rb b/test/net/imap/test_imap_authenticators.rb index d7966e18a..79ce20762 100644 --- a/test/net/imap/test_imap_authenticators.rb +++ b/test/net/imap/test_imap_authenticators.rb @@ -135,6 +135,35 @@ def test_anonymous_length_over_255 assert_raise(ArgumentError) { anonymous("a" * 256).process(nil) } end + # ---------------------- + # EXTERNAL + # ---------------------- + + def external(...) + Net::IMAP::SASL.authenticator("EXTERNAL", ...) + 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("kwarg", external(authzid: "kwarg").process(nil)) + end + + def test_external_utf8 + assert_equal("", external.process(nil)) + assert_equal("🏴󠁧󠁢󠁥󠁮󠁧󠁿 England", + external(authzid: "🏴󠁧󠁢󠁥󠁮󠁧󠁿 England").process(nil)) + end + + def test_external_invalid + assert_raise(ArgumentError) { external(authzid: "bad\0contains NULL") } + assert_raise(ArgumentError) { external(authzid: "invalid utf8\x80") } + assert_raise(ArgumentError) { external("invalid positional argument") } + end + # ---------------------- # LOGIN (obsolete) # ----------------------