From ada7f3fdbb74bbc850f0d95b6a95d263f075cd11 Mon Sep 17 00:00:00 2001 From: Michael James Gratton Date: Tue, 29 May 2018 05:51:11 +0200 Subject: [PATCH] Add support for (X)OAuth2 SMTP authentication. --- po/POTFILES.in | 1 + src/CMakeLists.txt | 1 + src/engine/meson.build | 1 + src/engine/smtp/smtp-capabilities.vala | 6 +- src/engine/smtp/smtp-client-session.vala | 80 +++++++++++++------ .../smtp/smtp-oauth2-authenticator.vala | 55 +++++++++++++ 6 files changed, 119 insertions(+), 25 deletions(-) create mode 100644 src/engine/smtp/smtp-oauth2-authenticator.vala diff --git a/po/POTFILES.in b/po/POTFILES.in index d2d6a45a..bffa7e0c 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -370,6 +370,7 @@ src/engine/smtp/smtp-data-format.vala src/engine/smtp/smtp-error.vala src/engine/smtp/smtp-greeting.vala src/engine/smtp/smtp-login-authenticator.vala +src/engine/smtp/smtp-oauth2-authenticator.vala src/engine/smtp/smtp-plain-authenticator.vala src/engine/smtp/smtp-request.vala src/engine/smtp/smtp-response-code.vala diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 74534d72..36c3f5c0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -288,6 +288,7 @@ engine/smtp/smtp-data-format.vala engine/smtp/smtp-error.vala engine/smtp/smtp-greeting.vala engine/smtp/smtp-login-authenticator.vala +engine/smtp/smtp-oauth2-authenticator.vala engine/smtp/smtp-plain-authenticator.vala engine/smtp/smtp-request.vala engine/smtp/smtp-response.vala diff --git a/src/engine/meson.build b/src/engine/meson.build index 61c298f4..c95d5ea3 100644 --- a/src/engine/meson.build +++ b/src/engine/meson.build @@ -284,6 +284,7 @@ geary_engine_vala_sources = files( 'smtp/smtp-error.vala', 'smtp/smtp-greeting.vala', 'smtp/smtp-login-authenticator.vala', + 'smtp/smtp-oauth2-authenticator.vala', 'smtp/smtp-plain-authenticator.vala', 'smtp/smtp-request.vala', 'smtp/smtp-response.vala', diff --git a/src/engine/smtp/smtp-capabilities.vala b/src/engine/smtp/smtp-capabilities.vala index b6f85733..c7be093b 100644 --- a/src/engine/smtp/smtp-capabilities.vala +++ b/src/engine/smtp/smtp-capabilities.vala @@ -5,12 +5,14 @@ */ public class Geary.Smtp.Capabilities : Geary.GenericCapabilities { + public const string STARTTLS = "starttls"; public const string AUTH = "auth"; - + public const string AUTH_PLAIN = "plain"; public const string AUTH_LOGIN = "login"; - + public const string AUTH_OAUTH2 = "xoauth2"; + public const string NAME_SEPARATOR = " "; public const string VALUE_SEPARATOR = " "; diff --git a/src/engine/smtp/smtp-client-session.vala b/src/engine/smtp/smtp-client-session.vala index de622338..34ea7873 100644 --- a/src/engine/smtp/smtp-client-session.vala +++ b/src/engine/smtp/smtp-client-session.vala @@ -55,34 +55,64 @@ public class Geary.Smtp.ClientSession { // Returns authenticator used for successful authentication, otherwise throws exception private async Authenticator attempt_authentication_async(Credentials creds, Cancellable? cancellable) throws Error { - // build an authentication style ordering to attempt, going from reported capabilities to standard - // fallbacks, while avoiding repetition ... this is necessary due to server bugs that report - // an authentication type is available but actually isn't, see + // build an authentication style ordering to attempt, going + // from reported capabilities to standard fallbacks, while + // avoiding repetition ... this is necessary due to server + // bugs that report an authentication type is available but + // actually isn't, see + // // http://redmine.yorba.org/issues/6091 + // // and + // // http://comments.gmane.org/gmane.mail.pine.general/4004 Gee.ArrayList auth_order = new Gee.ArrayList(String.stri_equal); - - // start with advertised authentication styles, in order of our preference (PLAIN - // only requires one round-trip) - if (cx.capabilities != null) { - if (cx.capabilities.has_setting(Capabilities.AUTH, Capabilities.AUTH_PLAIN)) + + switch (creds.supported_method) { + case Credentials.Method.PASSWORD: + // start with advertised authentication styles, in order of our preference (PLAIN + // only requires one round-trip) + if (cx.capabilities != null) { + if (cx.capabilities.has_setting(Capabilities.AUTH, Capabilities.AUTH_PLAIN)) + auth_order.add(Capabilities.AUTH_PLAIN); + + if (cx.capabilities.has_setting(Capabilities.AUTH, Capabilities.AUTH_LOGIN)) + auth_order.add(Capabilities.AUTH_LOGIN); + } + + // fallback on commonly-implemented styles, again in our order of preference + if (!auth_order.contains(Capabilities.AUTH_PLAIN)) auth_order.add(Capabilities.AUTH_PLAIN); - - if (cx.capabilities.has_setting(Capabilities.AUTH, Capabilities.AUTH_LOGIN)) + + if (!auth_order.contains(Capabilities.AUTH_LOGIN)) auth_order.add(Capabilities.AUTH_LOGIN); + + if (auth_order.is_empty) { + throw new SmtpError.AUTHENTICATION_FAILED( + "Unable to authenticate using PASSWORD credentials against %s", + to_string() + ); + } + break; + + case Credentials.Method.OAUTH2: + if (cx.capabilities != null && + !cx.capabilities.has_setting(Capabilities.AUTH, + Capabilities.AUTH_OAUTH2)) { + throw new SmtpError.AUTHENTICATION_FAILED( + "Unable to authenticate using OAUTH2 credentials against %s", + to_string() + ); + } + auth_order.add(Capabilities.AUTH_OAUTH2); + break; + + default: + throw new SmtpError.AUTHENTICATION_FAILED( + "Unsupported auth method: %s", creds.supported_method.to_string() + ); } - - // fallback on commonly-implemented styles, again in our order of preference - if (!auth_order.contains(Capabilities.AUTH_PLAIN)) - auth_order.add(Capabilities.AUTH_PLAIN); - - if (!auth_order.contains(Capabilities.AUTH_LOGIN)) - auth_order.add(Capabilities.AUTH_LOGIN); - - // in current situation, should always have one authentication type to attempt - assert(auth_order.size > 0); - + // go through the list, in order, until one style is accepted do { Authenticator? authenticator; @@ -90,11 +120,15 @@ public class Geary.Smtp.ClientSession { case Capabilities.AUTH_PLAIN: authenticator = new PlainAuthenticator(creds); break; - + case Capabilities.AUTH_LOGIN: authenticator = new LoginAuthenticator(creds); break; - + + case Capabilities.AUTH_OAUTH2: + authenticator = new OAuth2Authenticator(creds); + break; + default: assert_not_reached(); } diff --git a/src/engine/smtp/smtp-oauth2-authenticator.vala b/src/engine/smtp/smtp-oauth2-authenticator.vala new file mode 100644 index 00000000..f6633cda --- /dev/null +++ b/src/engine/smtp/smtp-oauth2-authenticator.vala @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * Google's proprietary OAuth 2 authentication. + * + * See [[http://tools.ietf.org/html/rfc4616]] + */ + +public class Geary.Smtp.OAuth2Authenticator : Geary.Smtp.Authenticator { + + + private const string OAUTH2_RESP = "user=%s\001auth=Bearer %s\001\001"; + + + public OAuth2Authenticator(Credentials credentials) { + base ("XOAUTH2", credentials); + } + + public override Request initiate() { + return new Request(Command.AUTH, { "xoauth2" }); + } + + public override Memory.Buffer? challenge(int step, Response response) + throws SmtpError { + Memory.Buffer? buf = null; + switch (step) { + case 0: + // The initial AUTH command + buf = new Memory.StringBuffer( + Base64.encode( + OAUTH2_RESP.printf( + credentials.user ?? "", + credentials.token ?? "" + ).data + ) + ); + break; + + case 1: + // Server sent a challenge, which will be a Base64 encoded + // JSON blob and which indicates a login failure. We don't + // really care about that (do we?) though since once + // we acknowledge it with a zero-length string the server + // will respond with a SMTP error. + buf = new Memory.StringBuffer(""); + break; + } + return buf; + } +}