Skip to content

Commit b4e149e

Browse files
committed
✨ SASL EXTERNAL: Add mechanism
1 parent 1e3e48e commit b4e149e

File tree

5 files changed

+120
-0
lines changed

5 files changed

+120
-0
lines changed

lib/net/imap.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,10 @@ def starttls(options = {}, verify = true)
992992
# Non-standard and obsoleted by +OAUTHBEARER+, but widely
993993
# supported.
994994
#
995+
# +EXTERNAL+:: See SASL::ExternalAuthenticator.
996+
# Login using already established credentials, such as a TLS
997+
# certificate or IPsec.
998+
#
995999
# +ANONYMOUS+:: See SASL::AnonymousAuthenticator.
9961000
# Allow the user to gain access to public services or
9971001
# resources without authenticating or disclosing an

lib/net/imap/authenticators.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def authenticators
6868
Net::IMAP.extend Net::IMAP::Authenticators
6969

7070
require_relative "sasl/anonymous_authenticator"
71+
require_relative "sasl/external_authenticator"
7172
require_relative "sasl/plain_authenticator"
7273
require_relative "sasl/xoauth2_authenticator"
7374

lib/net/imap/sasl/authenticator.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ module SASL
2727
# Non-standard and obsoleted by +OAUTHBEARER+, but widely
2828
# supported.
2929
#
30+
# +EXTERNAL+:: See SASL::ExternalAuthenticator.
31+
# Login using already established credentials, such as a TLS
32+
# certificate or IPsec.
33+
#
3034
# +ANONYMOUS+:: See SASL::AnonymousAuthenticator.
3135
# Allow the user to gain access to public services or
3236
# resources without authenticating or disclosing an
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
register Net::IMAP
18+
19+
# :call-seq:
20+
# initial_response? -> true
21+
#
22+
# +EXTERNAL+ can send an initial client response.
23+
def initial_response?; true end
24+
25+
##
26+
# :call-seq:
27+
# new -> authenticator
28+
# new(authzid, **) -> authenticator
29+
# new(authzid:, **) -> authenticator
30+
# new {|propname, auth_ctx| propval } -> authenticator
31+
#
32+
# Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as
33+
# specified in RFC-4422[https://tools.ietf.org/html/rfc4422]. To use
34+
# this, see Net::IMAP#authenticate or your client's authentication
35+
# method.
36+
#
37+
# ==== Properties
38+
# Only one property, which is optional:
39+
#
40+
# * #authzid -- the identity to act as. Leave blank to use the identity
41+
# associated with the client's credentials.
42+
#
43+
# May be sent as a positional argument or as a keyword argument.
44+
#
45+
# See Net::IMAP::SASL::Authenticator@Properties for a detailed
46+
# description of property assignment, lazy loading, and callbacks.
47+
def initialize(authzid_arg = nil, authzid: nil)
48+
super
49+
propinit :authzid, authzid, authzid_arg
50+
end
51+
52+
property :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+
private
63+
64+
NULL = "\0"
65+
66+
def propset(name, value)
67+
if name == :authzid && !value.nil?
68+
raise ArgumentError, "#{name} contains NULL" if value.include? NULL
69+
value = value.encode "UTF-8"
70+
unless value.valid_encoding?
71+
raise ArgumentError, "#{name} isn't valid UTF-8"
72+
end
73+
end
74+
super(name, value)
75+
end
76+
77+
end
78+
end
79+
end
80+
end

test/net/imap/sasl/test_authenticators.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,37 @@ def test_xoauth2_callbacks
8080
)
8181
end
8282

83+
# ----------------------
84+
# EXTERNAL
85+
# ----------------------
86+
87+
def external(*args, **kwargs, &block)
88+
Net::IMAP.authenticator("EXTERNAL", *args, **kwargs, &block)
89+
end
90+
91+
def test_external_matches_mechanism
92+
assert_kind_of(Net::IMAP::SASL::ExternalAuthenticator, external)
93+
end
94+
95+
def test_external_response
96+
assert_equal("", external.process(nil))
97+
assert_equal("hello world", external("hello world").process(nil))
98+
assert_equal("kwargs",
99+
external(authzid: "kwargs").process(nil))
100+
end
101+
102+
def test_external_utf8
103+
assert_equal("", external.process(nil))
104+
assert_equal("🏴󠁧󠁢󠁥󠁮󠁧󠁿 England", external("🏴󠁧󠁢󠁥󠁮󠁧󠁿 England").process(nil))
105+
assert_equal("kwargs",
106+
external(authzid: "kwargs").process(nil))
107+
end
108+
109+
def test_external_invalid
110+
assert_raise(ArgumentError) { external("bad\0contains NULL") }
111+
assert_raise(ArgumentError) { external("invalid utf8\x80") }
112+
end
113+
83114
# ----------------------
84115
# ANONYMOUS
85116
# ----------------------

0 commit comments

Comments
 (0)