Skip to content

Commit 1020e4c

Browse files
committed
🔒 Add SASL EXTERNAL mechanism
1 parent 299be60 commit 1020e4c

File tree

5 files changed

+122
-0
lines changed

5 files changed

+122
-0
lines changed

lib/net/imap.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,12 @@ def starttls(options = {}, verify = true)
10081008
# Allows the user to gain access to public services or resources without
10091009
# authenticating or disclosing an identity.
10101010
#
1011+
# +EXTERNAL+::
1012+
# See ExternalAuthenticator[Net::IMAP::SASL::ExternalAuthenticator].
1013+
#
1014+
# Login using already established credentials, such as a TLS certificate
1015+
# or IPsec.
1016+
#
10111017
# +PLAIN+::
10121018
# See PlainAuthenticator[Net::IMAP::SASL::PlainAuthenticator].
10131019
#

lib/net/imap/sasl.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ class IMAP
3333
# Allows the user to gain access to public services or resources without
3434
# authenticating or disclosing an identity.
3535
#
36+
# +EXTERNAL+::
37+
# See ExternalAuthenticator[Net::IMAP::SASL::ExternalAuthenticator].
38+
#
39+
# Login using already established credentials, such as a TLS certificate
40+
# or IPsec.
41+
#
3642
# +PLAIN+::
3743
# See PlainAuthenticator[Net::IMAP::SASL::PlainAuthenticator].
3844
#
@@ -77,6 +83,7 @@ module SASL
7783
autoload :Authenticators, "#{sasl_dir}/authenticators"
7884

7985
autoload :AnonymousAuthenticator, "#{sasl_dir}/anonymous_authenticator"
86+
autoload :ExternalAuthenticator, "#{sasl_dir}/external_authenticator"
8087
autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
8188
autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"
8289

lib/net/imap/sasl/authenticators.rb

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

test/net/imap/test_imap_authenticators.rb

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

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

0 commit comments

Comments
 (0)