]> granicus.if.org Git - neomutt/commitdiff
Initial support for OAUTHBEARER for IMAP.
authorBrandon Long <blong@fiction.net>
Mon, 11 Jun 2018 17:39:49 +0000 (10:39 -0700)
committerRichard Russon <rich@flatcap.org>
Mon, 11 Jun 2018 17:39:49 +0000 (10:39 -0700)
Gmail supports RFC 7628 for using OAUTH with IMAP, and they really don't
like you using password based auth.  You can still enable "less secure
apps" and then generate an application specific password, but I figured it
was time to support it.

Being mutt, I punted on some of the "hard" work to an external script, ie
getting/refreshing the OAUTH tokens.  This avoids the issue of how do you
have a client-id and client-secret for an open source project, and the fact
that OAUTH discovery is still nascent, so you'd likely need separate things
for each of the providers.

At least for Gmail, you can use the oauth2.py script from Google's
gmail-oauth2-tools:
https://github.com/google/gmail-oauth2-tools/blob/master/python/oauth2.py

You'd need to get your own oauth client credentials for Gmail here:
https://console.developers.google.com/apis/credentials

Then, you'd use oauth2.py with --generate_oauth2_token to get a refresh
token, and configure mutt with:

set imap_authenticators="oauthbearer"
set imap_user="<email_address>"
set imap_pass=`/path/to/oauth2.py --quiet --user=<email_address>
--client_id=<client_id> --client_secret=<client_secret>
--refresh_token=<refresh_token>`

For this patch, I didn't add any new configuration, but I'm open to
suggestions on that.

The patch also only support SASL-IR to reduce round-trips to the server,
but it's certainly possible to change that if we think there are
OAUTHBEARER IMAP servers that don't support SASL-IR.  It also requires the
connection to be encrypted as the access token is re-usable for an hour or
so.  Again, Gmail only allows encrypted IMAP connections, not sure if any
OAUTHBEARER services allow non-encrypted.

Turns out that auth failure leaves you in SASL mode, so I have a hack to
issue a noop command on error.  Not sure if that's just OAUTHBEARER
oddness, or whether I should be using lower level mutt imap functions.

Makefile.autosetup
imap/auth.c
imap/auth.h
imap/auth_oauth.c [new file with mode: 0644]
imap/command.c
imap/imap_private.h

index 0eb31b612248c7c6ed9005a6805711d9fb229e7b..091fa92ef5f1490c1843058f1728ecb59272f7e4 100644 (file)
@@ -155,7 +155,7 @@ ALLOBJS+=   $(LIBNCRYPTOBJS)
 # libimap
 LIBIMAP=       libimap.a
 LIBIMAPOBJS=   imap/auth.o imap/auth_anon.o imap/auth_cram.o \
-               imap/auth_login.o imap/auth_plain.o imap/browse.o \
+               imap/auth_login.o imap/auth_oauth.o imap/auth_plain.o imap/browse.o \
                imap/command.o imap/imap.o imap/message.o imap/utf7.o \
                imap/util.o
 @if USE_GSS
index dbb16756337df965d56e17e6e411f5fbdd607438..12aa88a2175fbc02c116ad9d1575ac20d9c21f85 100644 (file)
@@ -53,7 +53,7 @@ static const struct ImapAuth imap_authenticators[] = {
 #ifndef USE_SASL
   { imap_auth_cram_md5, "cram-md5" },
 #endif
-  { imap_auth_login, "login" },
+  { imap_auth_login, "login" },       { imap_auth_oauth, "oauthbearer" },
 };
 
 /**
index 58933435d3456d15cddeb636b66943aad77530fd..bdf9fb01eaf7367c1797f8d3055624a261532c0d 100644 (file)
@@ -63,5 +63,6 @@ enum ImapAuthRes imap_auth_gss(struct ImapData *idata, const char *method);
 #ifdef USE_SASL
 enum ImapAuthRes imap_auth_sasl(struct ImapData *idata, const char *method);
 #endif
+enum ImapAuthRes imap_auth_oauth(struct ImapData *idata, const char *method);
 
 #endif /* _IMAP_AUTH_H */
diff --git a/imap/auth_oauth.c b/imap/auth_oauth.c
new file mode 100644 (file)
index 0000000..f343d45
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 1999-2001,2005 Brendan Cully <brendan@kublai.com>
+ * Copyright (C) 2018 Brandon Long <blong@fiction.net>
+ * 
+ *     This program is free software; you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation; either version 2 of the License, or
+ *     (at your option) any later version.
+ * 
+ *     This program is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ * 
+ *     You should have received a copy of the GNU General Public License
+ *     along with this program; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+/* IMAP login/authentication code */
+
+#include "config.h"
+#include "imap_private.h"
+#include "mutt/mutt.h"
+#include "conn/conn.h"
+#include "mutt.h"
+#include "auth.h"
+#include "mutt_account.h"
+#include "mutt_logging.h"
+#include "mutt_socket.h"
+#include "muttlib.h"
+
+/* imap_auth_oauth: AUTH=OAUTHBEARER support. See RFC 7628 */
+enum ImapAuthRes imap_auth_oauth(struct ImapData *idata, const char *method)
+{
+  char *ibuf = NULL;
+  char *oauth_buf = NULL;
+  int len, ilen, oalen;
+  int rc;
+
+  /* For now, we only support SASL_IR also and over TLS */
+  if (!mutt_bit_isset(idata->capabilities, AUTH_OAUTHBEARER) ||
+      !mutt_bit_isset(idata->capabilities, SASL_IR) || !idata->conn->ssf)
+    return IMAP_AUTH_UNAVAIL;
+
+  mutt_message(_("Authenticating (OAUTHBEARER)..."));
+
+  /* get auth info */
+  if (mutt_account_getlogin(&idata->conn->account))
+    return IMAP_AUTH_FAILURE;
+
+  /* We get the access token from the "imap_pass" field */
+  if (mutt_account_getpass(&idata->conn->account))
+    return IMAP_AUTH_FAILURE;
+
+  /* Determine the length of the keyed message digest, add 50 for
+   * overhead.
+   */
+  oalen = mutt_str_strlen(idata->conn->account.user) +
+          mutt_str_strlen(idata->conn->account.host) +
+          mutt_str_strlen(idata->conn->account.pass) + 50;
+  oauth_buf = mutt_mem_malloc(oalen);
+
+  snprintf(oauth_buf, oalen, "n,a=%s,\001host=%s\001port=%d\001auth=Bearer %s\001\001",
+           idata->conn->account.user, idata->conn->account.host,
+           idata->conn->account.port, idata->conn->account.pass);
+
+  /* ibuf must be long enough to store the base64 encoding of
+   * oauth_buf, plus the additional debris.
+   */
+
+  ilen = mutt_str_strlen(oauth_buf) * 2 + 30;
+  ibuf = mutt_mem_malloc(ilen);
+  ibuf[0] = '\0';
+
+  mutt_str_strcat(ibuf, ilen, "AUTHENTICATE OAUTHBEARER ");
+  len = mutt_str_strlen(ibuf);
+
+  mutt_b64_encode(oauth_buf, mutt_str_strlen(oauth_buf), (ibuf + len), ilen - len);
+
+  /* This doesn't really contain a password, but the token is good for
+   * an hour, so suppress it anyways.
+   */
+  rc = imap_exec(idata, ibuf, IMAP_CMD_FAIL_OK | IMAP_CMD_PASS);
+
+  FREE(&oauth_buf);
+  FREE(&ibuf);
+
+  if (!rc)
+  {
+    mutt_clear_error();
+    return IMAP_AUTH_SUCCESS;
+  }
+
+  /* The error response was in SASL continuation, so "continue" the SASL
+   * to cause a failure and exit SASL input.
+   */
+  mutt_socket_send(idata->conn, "an noop\r\n");
+
+  mutt_error(_("OAUTHBEARER authentication failed."));
+  mutt_sleep(2);
+  return IMAP_AUTH_FAILURE;
+}
index 6be9c0834a92378fb6472440bb118265290b34aa..f29ad56bc085e568f7d2fc11c741fcea710ff021 100644 (file)
@@ -67,10 +67,23 @@ bool ImapServernoise; ///< Config: (imap) Display server warnings as error messa
  * @note Gmail documents one string but use another, so we support both.
  */
 static const char *const Capabilities[] = {
-  "IMAP4",     "IMAP4rev1",     "STATUS",      "ACL",
-  "NAMESPACE", "AUTH=CRAM-MD5", "AUTH=GSSAPI", "AUTH=ANONYMOUS",
-  "STARTTLS",  "LOGINDISABLED", "IDLE",        "SASL-IR",
-  "ENABLE",    "X-GM-EXT-1",    "X-GM-EXT1",   NULL,
+  "IMAP4",
+  "IMAP4rev1",
+  "STATUS",
+  "ACL",
+  "NAMESPACE",
+  "AUTH=CRAM-MD5",
+  "AUTH=GSSAPI",
+  "AUTH=ANONYMOUS",
+  "AUTH=OAUTHBEARER",
+  "STARTTLS",
+  "LOGINDISABLED",
+  "IDLE",
+  "SASL-IR",
+  "ENABLE",
+  "X-GM-EXT-1",
+  "X-GM-EXT1",
+  NULL,
 };
 
 /**
index 8f68e971fa231713b32124cee7b1654184bc2001..78691db835d64dcf89cbb433745eb99bcff595bb 100644 (file)
@@ -124,17 +124,18 @@ enum ImapCaps
   IMAP4 = 0,
   IMAP4REV1,
   STATUS,
-  ACL,           /**< RFC2086: IMAP4 ACL extension */
-  NAMESPACE,     /**< RFC2342: IMAP4 Namespace */
-  ACRAM_MD5,     /**< RFC2195: CRAM-MD5 authentication */
-  AGSSAPI,       /**< RFC1731: GSSAPI authentication */
-  AUTH_ANON,     /**< AUTH=ANONYMOUS */
-  STARTTLS,      /**< RFC2595: STARTTLS */
-  LOGINDISABLED, /**<           LOGINDISABLED */
-  IDLE,          /**< RFC2177: IDLE */
-  SASL_IR,       /**< SASL initial response draft */
-  ENABLE,        /**< RFC5161 */
-  X_GM_EXT1,     /**< https://developers.google.com/gmail/imap/imap-extensions */
+  ACL,                   /**< RFC2086: IMAP4 ACL extension */
+  NAMESPACE,             /**< RFC2342: IMAP4 Namespace */
+  ACRAM_MD5,             /**< RFC2195: CRAM-MD5 authentication */
+  AGSSAPI,               /**< RFC1731: GSSAPI authentication */
+  AUTH_ANON,             /**< AUTH=ANONYMOUS */
+  AUTH_OAUTHBEARER,      /**< RFC7628: AUTH=OAUTHBEARER */
+  STARTTLS,              /**< RFC2595: STARTTLS */
+  LOGINDISABLED,         /**< RFC2595: LOGINDISABLED */
+  IDLE,                  /**< RFC2177: IDLE */
+  SASL_IR,               /**< SASL initial response draft */
+  ENABLE,                /**< RFC5161 */
+  X_GM_EXT1,             /**< https://developers.google.com/gmail/imap/imap-extensions */
   X_GM_ALT1 = X_GM_EXT1, /**< Alternative capability string */
 
   CAPMAX