Skip to content

Commit 920e6be

Browse files
committed
✨ SASL EXTERNAL: Add mechanism
1 parent 582b342 commit 920e6be

File tree

3 files changed

+116
-0
lines changed

3 files changed

+116
-0
lines changed

lib/net/imap/sasl.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ class IMAP
4343
# resources without authenticating or disclosing an
4444
# identity.
4545
#
46+
# +EXTERNAL+:: See ExternalAuthenticator.
47+
# Login using already established credentials, such as a TLS
48+
# certificate or IPsec.
49+
#
4650
# === Deprecated mechanisms
4751
#
4852
# <em>Obsolete mechanisms are available for backwards compatibility. Using
@@ -68,6 +72,7 @@ module SASL
6872
autoload :Authenticator, "#{sasl_dir}/authenticator"
6973
autoload :Authenticators, "#{sasl_dir}/authenticators"
7074
autoload :AnonymousAuthenticator, "#{sasl_dir}/anonymous_authenticator"
75+
autoload :ExternalAuthenticator, "#{sasl_dir}/external_authenticator"
7176
autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
7277
autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"
7378

@@ -79,6 +84,7 @@ module SASL
7984
def self.authenticators
8085
@authenticators ||= SASL::Authenticators.new.tap do |registry|
8186
registry.add_authenticator "Anonymous"
87+
registry.add_authenticator "External"
8288
registry.add_authenticator "Plain"
8389
registry.add_authenticator "XOAuth2"
8490
registry.add_authenticator "Login" # deprecated
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "authenticator"
4+
5+
module Net
6+
class IMAP < Protocol
7+
module SASL
8+
9+
# Authenticator for the "+EXTERNAL+" SASL mechanism, as specified by
10+
# RFC-4422[https://tools.ietf.org/html/rfc4422]. See
11+
# Net::IMAP#authenticate.
12+
#
13+
# The EXTERNAL mechanism requests that the server use client credentials
14+
# established external to SASL, for example by TLS certificate or IPsec.
15+
#
16+
class ExternalAuthenticator < Authenticator
17+
18+
# :call-seq:
19+
# initial_response? -> true
20+
#
21+
# +EXTERNAL+ can send an initial client response.
22+
def initial_response?; true end
23+
24+
##
25+
# :call-seq:
26+
# new -> authenticator
27+
# new(authzid, **) -> authenticator
28+
# new(authzid:, **) -> authenticator
29+
# new {|propname, auth_ctx| propval } -> authenticator
30+
#
31+
# Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as
32+
# specified in RFC-4422[https://tools.ietf.org/html/rfc4422]. To use
33+
# this, see Net::IMAP#authenticate or your client's authentication
34+
# method.
35+
#
36+
# ==== Properties
37+
# Only one property, which is optional:
38+
#
39+
# * #authzid -- the identity to act as. Leave blank to use the identity
40+
# associated with the client's credentials.
41+
#
42+
# May be sent as a positional argument or as a keyword argument.
43+
#
44+
# See Net::IMAP::SASL::Authenticator@Properties for a detailed
45+
# description of property assignment, lazy loading, and callbacks.
46+
def initialize(authzid_arg = nil, authzid: nil)
47+
super
48+
propinit :authzid, authzid, authzid_arg
49+
end
50+
51+
property :authzid
52+
53+
def process(_)
54+
return "" if authzid.nil?
55+
if /\u0000/u.match?(authzid) # also validates UTF8 encoding
56+
raise DataFormatError, "authzid contains NULL"
57+
end
58+
authzid.encode "UTF-8"
59+
end
60+
61+
private
62+
63+
NULL = "\0"
64+
65+
def propset(name, value)
66+
if name == :authzid && !value.nil?
67+
raise ArgumentError, "#{name} contains NULL" if value.include? NULL
68+
value = value.encode "UTF-8"
69+
unless value.valid_encoding?
70+
raise ArgumentError, "#{name} isn't valid UTF-8"
71+
end
72+
end
73+
super(name, value)
74+
end
75+
76+
end
77+
end
78+
end
79+
end

test/net/imap/test_imap_authenticators.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,37 @@ def test_xoauth2_supports_initial_response
9191
assert Net::IMAP::SASL.initial_response?(xoauth2("foo", "bar"))
9292
end
9393

94+
# ----------------------
95+
# EXTERNAL
96+
# ----------------------
97+
98+
def external(*args, **kwargs, &block)
99+
Net::IMAP::SASL.authenticator("EXTERNAL", *args, **kwargs, &block)
100+
end
101+
102+
def test_external_matches_mechanism
103+
assert_kind_of(Net::IMAP::SASL::ExternalAuthenticator, external)
104+
end
105+
106+
def test_external_response
107+
assert_equal("", external.process(nil))
108+
assert_equal("hello world", external("hello world").process(nil))
109+
assert_equal("kwargs",
110+
external(authzid: "kwargs").process(nil))
111+
end
112+
113+
def test_external_utf8
114+
assert_equal("", external.process(nil))
115+
assert_equal("🏴󠁧󠁢󠁥󠁮󠁧󠁿 England", external("🏴󠁧󠁢󠁥󠁮󠁧󠁿 England").process(nil))
116+
assert_equal("kwargs",
117+
external(authzid: "kwargs").process(nil))
118+
end
119+
120+
def test_external_invalid
121+
assert_raise(ArgumentError) { external("bad\0contains NULL") }
122+
assert_raise(ArgumentError) { external("invalid utf8\x80") }
123+
end
124+
94125
# ----------------------
95126
# ANONYMOUS
96127
# ----------------------

0 commit comments

Comments
 (0)