|
| 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 |
0 commit comments