Skip to content

Commit 831df73

Browse files
committed
🔒 Add SASL EXTERNAL mechanism
The `EXTERNAL` SASL mechanism is specified by the core SASL specification, in RFC4422.
1 parent b6003f1 commit 831df73

File tree

5 files changed

+96
-0
lines changed

5 files changed

+96
-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+
# Authenticates using already established credentials, such as a TLS
1015+
# certificate or IPsec.
1016+
#
10111017
# +OAUTHBEARER+::
10121018
# See OAuthBearerAuthenticator[rdoc-ref:Net::IMAP::SASL::OAuthBearerAuthenticator].
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+
# Authenticates using already established credentials, such as a TLS
40+
# certificate or IPsec.
41+
#
3642
# +OAUTHBEARER+::
3743
# See OAuthBearerAuthenticator.
3844
#
@@ -85,6 +91,7 @@ module SASL
8591
autoload :GS2Header, "#{sasl_dir}/gs2_header"
8692

8793
autoload :AnonymousAuthenticator, "#{sasl_dir}/anonymous_authenticator"
94+
autoload :ExternalAuthenticator, "#{sasl_dir}/external_authenticator"
8895
autoload :OAuthBearerAuthenticator, "#{sasl_dir}/oauthbearer_authenticator"
8996
autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
9097
autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"

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 "OAuthBearer"
3839
add_authenticator "Plain"
3940
add_authenticator "XOAuth2"
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
class ExternalAuthenticator
14+
15+
# :call-seq:
16+
# new(authzid: nil, **) -> authenticator
17+
#
18+
# Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as
19+
# specified in RFC-4422[https://tools.ietf.org/html/rfc4422]. To use
20+
# this, see Net::IMAP#authenticate or your client's authentication
21+
# method.
22+
#
23+
# #authzid is an optional identity to act as or on behalf of.
24+
#
25+
# Any other keyword parameters are quietly ignored.
26+
def initialize(authzid: nil)
27+
@authzid = authzid&.to_str&.encode "UTF-8"
28+
if @authzid&.match?(/\u0000/u) # also validates UTF8 encoding
29+
raise ArgumentError, "contains NULL"
30+
end
31+
end
32+
33+
# Authorization identity: an identity to act as or on behalf of.
34+
#
35+
# If not explicitly provided, the server defaults to using the identity
36+
# that was authenticated by the external credentials.
37+
attr_reader :authzid
38+
39+
# :call-seq:
40+
# initial_response? -> true
41+
#
42+
# +EXTERNAL+ can send an initial client response.
43+
def initial_response?; true end
44+
45+
# Returns #authzid, or an empty string if there is no authzid.
46+
def process(_)
47+
authzid || ""
48+
end
49+
50+
end
51+
end
52+
end
53+
end

test/net/imap/test_imap_authenticators.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,35 @@ def test_anonymous_length_over_255
135135
assert_raise(ArgumentError) { anonymous("a" * 256).process(nil) }
136136
end
137137

138+
# ----------------------
139+
# EXTERNAL
140+
# ----------------------
141+
142+
def external(...)
143+
Net::IMAP::SASL.authenticator("EXTERNAL", ...)
144+
end
145+
146+
def test_external_matches_mechanism
147+
assert_kind_of(Net::IMAP::SASL::ExternalAuthenticator, external)
148+
end
149+
150+
def test_external_response
151+
assert_equal("", external.process(nil))
152+
assert_equal("kwarg", external(authzid: "kwarg").process(nil))
153+
end
154+
155+
def test_external_utf8
156+
assert_equal("", external.process(nil))
157+
assert_equal("🏴󠁧󠁢󠁥󠁮󠁧󠁿 England",
158+
external(authzid: "🏴󠁧󠁢󠁥󠁮󠁧󠁿 England").process(nil))
159+
end
160+
161+
def test_external_invalid
162+
assert_raise(ArgumentError) { external(authzid: "bad\0contains NULL") }
163+
assert_raise(ArgumentError) { external(authzid: "invalid utf8\x80") }
164+
assert_raise(ArgumentError) { external("invalid positional argument") }
165+
end
166+
138167
# ----------------------
139168
# LOGIN (obsolete)
140169
# ----------------------

0 commit comments

Comments
 (0)