Skip to content

Commit c099f50

Browse files
committed
♻️ SASL XOAUTH2: inherit, move, test, document
* ♻️ Inherit from Authenticator base class * 🚚 Move to SASL module and rename file * ✅ Add new tests * 📖 Add many many docs * ✨ Add `initial_response? => true`
1 parent 431a907 commit c099f50

File tree

6 files changed

+156
-25
lines changed

6 files changed

+156
-25
lines changed

lib/net/imap.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -987,7 +987,7 @@ def starttls(options = {}, verify = true)
987987
# +PLAIN+:: See SASL::PlainAuthenticator.
988988
# Login using clear-text username and password.
989989
#
990-
# +XOAUTH2+:: See XOauth2Authenticator.
990+
# +XOAUTH2+:: See SASL::XOAuth2Authenticator.
991991
# Login using a username and OAuth2 access token.
992992
# Non-standard and obsoleted by +OAUTHBEARER+, but widely
993993
# supported.

lib/net/imap/authenticators.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,9 @@ def authenticators
6868
Net::IMAP.extend Net::IMAP::Authenticators
6969

7070
require_relative "sasl/plain_authenticator"
71+
require_relative "sasl/xoauth2_authenticator"
7172

73+
# deprecated
7274
require_relative "authenticators/login"
7375
require_relative "authenticators/cram_md5"
7476
require_relative "authenticators/digest_md5"
75-
require_relative "authenticators/xoauth2"

lib/net/imap/authenticators/xoauth2.rb

Lines changed: 0 additions & 20 deletions
This file was deleted.

lib/net/imap/sasl/authenticator.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ module SASL
2222
# +PLAIN+:: See SASL::PlainAuthenticator.
2323
# Login using clear-text username and password.
2424
#
25-
# +XOAUTH2+:: See XOauth2Authenticator.
25+
# +XOAUTH2+:: See SASL::XOAuth2Authenticator.
2626
# Login using a username and OAuth2 access token.
2727
# Non-standard and obsoleted by +OAUTHBEARER+, but widely
2828
# supported.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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 "+XOAUTH2+" SASL mechanism. This mechanism was
10+
# originally created for GMail and widely adopted by hosted email providers.
11+
# +XOAUTH2+ has been documented by
12+
# Google[https://developers.google.com/gmail/imap/xoauth2-protocol],
13+
# Microsoft[https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth],
14+
# and Yahoo[https://senders.yahooinc.com/developer/documentation].
15+
#
16+
# This mechanism requires an OAuth2 +access_token+ which has been authorized
17+
# with the appropriate OAuth2 scopes to access IMAP. These scopes are not
18+
# standardized---consult each email service provider's documentation.
19+
#
20+
# Although this mechanism was never standardized and has been obsoleted by
21+
# "+OAUTHBEARER+", it is still very widely supported.
22+
class XOAuth2Authenticator < Authenticator
23+
register Net::IMAP
24+
25+
##
26+
# :call-seq:
27+
# new(username, oauth2_token, **) -> auth_ctx
28+
# new(authcid:, oauth2_token:, **) -> auth_ctx
29+
# new(**) {|propname, auth_ctx| propval } -> auth_ctx
30+
#
31+
# Creates an Authenticator for the "+XOAUTH2+" SASL mechanism, as
32+
# specified by
33+
# Google[https://developers.google.com/gmail/imap/xoauth2-protocol], and
34+
# also documented by
35+
# Microsoft[https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth],
36+
# and Yahoo[https://senders.yahooinc.com/developer/documentation]. To use
37+
# this, see Net::IMAP#authenticate or your client's authentication
38+
# method.
39+
#
40+
# === Properties
41+
#
42+
# Both properties may be sent either as positional arguments or as
43+
# keyword arguments. If the same property is sent as both a positional
44+
# arg and a keyword arg, an ArgumentError is raised.
45+
#
46+
# * #authcid, #username --- the identity whose #password is used.
47+
# Only <tt>:authcid</tt> will be sent to callbacks.
48+
# * #oauth2_token --- An OAuth2.0 access token assigned to #username,
49+
# authorized to access IMAP.
50+
#
51+
# See the documentation for each property method for more details.
52+
#
53+
# See Net::IMAP::SASL::Authenticator@Properties for a detailed description
54+
# of property assignment, lazy loading, and callbacks.
55+
#
56+
def initialize(username_arg = nil, oauth2_token_arg = nil,
57+
username: nil, authcid: nil, oauth2_token: nil,
58+
**, &callback)
59+
super
60+
propinit :authcid, authcid, username, username_arg, required: true
61+
propinit :oauth2_token, oauth2_token, oauth2_token_arg, required: true
62+
end
63+
64+
##
65+
# method: authcid
66+
# :call-seq: authcid -> string
67+
#
68+
# Authentication identity: the identity that matches the #oauth2_token.
69+
#
70+
# RFC-2831[https://tools.ietf.org/html/rfc2831] uses the term +username+.
71+
# "Authentication identity" is the generic term used by
72+
# RFC-4422[https://tools.ietf.org/html/rfc4422].
73+
# RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs
74+
# abbreviate to +authcid+. #username is available as an alias for
75+
# #authcid, but only <tt>:authcid</tt> will be sent to callbacks.
76+
property :authcid
77+
alias username authcid
78+
79+
##
80+
# method: oauth2_token
81+
# :call-seq: oauth2_token -> string
82+
#
83+
# An OAuth2 access token for #authcid, which has been authorized with
84+
# appropriate OAuth2 scopes to access IMAP.
85+
property :oauth2_token
86+
87+
# Returns the XOAUTH2 formatted response, which combines the +authcid+
88+
# with the +oauth2_token+.
89+
#
90+
# [Note]
91+
# For simplicity, the same response is always generated, even when the
92+
# server sends error data in its challenge. However, the \SASL
93+
# specification requires an empty response to acknowledge the error.
94+
# This has little effect in practice, any response to the error results
95+
# in an tagged error response: generally +NO+, but potentially +BAD+.
96+
def process(data)
97+
@last_server_response = data
98+
"user=%s\1auth=Bearer %s\1\1" % [username, oauth2_token]
99+
end
100+
101+
# Stores the most recent server "challenge". When authentication fails,
102+
# this may hold information about the failure reason, as JSON.
103+
attr_reader :last_server_response
104+
105+
end
106+
end
107+
108+
XOauth2Authenticator = SASL::XOAuth2Authenticator # :nodoc:
109+
deprecate_constant :XOauth2Authenticator
110+
111+
end
112+
end

test/net/imap/sasl/test_authenticators.rb

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,48 @@ def test_plain_no_null_chars
3535
# XOAUTH2
3636
# ----------------------
3737

38-
def test_xoauth2
38+
def test_xoauth2_authenticator_matches_mechanism
39+
assert_kind_of(Net::IMAP::SASL::XOAuth2Authenticator,
40+
Net::IMAP.authenticator("XOAUTH2", "user", "tok"))
41+
end
42+
43+
def xoauth2(*args, **kwargs, &block)
44+
Net::IMAP.authenticator("XOAUTH2", *args, **kwargs, &block)
45+
end
46+
47+
def test_xoauth2_response
3948
assert_equal(
4049
"user=username\1auth=Bearer token\1\1",
41-
Net::IMAP::XOauth2Authenticator.new("username", "token").process(nil)
50+
xoauth2("username", "token").process(nil)
51+
)
52+
end
53+
54+
def test_xoauth2_kwargs
55+
assert_equal(
56+
"user=arg\1auth=Bearer kwarg\1\1",
57+
xoauth2("arg", oauth2_token: "kwarg").process(nil)
58+
)
59+
assert_equal(
60+
"user=authc\1auth=Bearer kwarg\1\1",
61+
xoauth2(authcid: "authc", oauth2_token: "kwarg").process(nil)
62+
)
63+
assert_equal(
64+
"user=user\1auth=Bearer kwarg\1\1",
65+
xoauth2(username: "user", oauth2_token: "kwarg").process(nil)
66+
)
67+
end
68+
69+
def test_xoauth2_callbacks
70+
assert_equal(
71+
"user=authc ID\1auth=Bearer lazy\1\1",
72+
xoauth2(authcid: "authc ID", oauth2_token: proc{ "lazy" }).process(nil)
73+
)
74+
assert_equal(
75+
"user=u\1auth=Bearer t\1\1",
76+
xoauth2 {|prop, authctx|
77+
assert_kind_of(Net::IMAP::SASL::XOAuth2Authenticator, authctx)
78+
case prop when :authcid then "u" when :oauth2_token then "t" end
79+
}.process(nil)
4280
)
4381
end
4482

0 commit comments

Comments
 (0)