]> git.ipfire.org Git - thirdparty/git.git/commitdiff
imap-send: add support for OAuth2.0 authentication
authorAditya Garg <gargaditya08@live.com>
Fri, 20 Jun 2025 06:40:27 +0000 (12:10 +0530)
committerJunio C Hamano <gitster@pobox.com>
Fri, 20 Jun 2025 15:11:16 +0000 (08:11 -0700)
OAuth2.0 is a new way of authentication supported by various email providers
these days. OAUTHBEARER and XOAUTH2 are the two most common mechanisms used
for OAuth2.0. OAUTHBEARER is described in RFC5801[1] and RFC7628[2], whereas
XOAUTH2 is Google's proprietary mechanism (See [3]).

[1]: https://datatracker.ietf.org/doc/html/rfc5801
[2]: https://datatracker.ietf.org/doc/html/rfc7628
[3]: https://developers.google.com/workspace/gmail/imap/xoauth2-protocol#initial_client_response

Signed-off-by: Aditya Garg <gargaditya08@live.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/config/imap.adoc
Documentation/git-imap-send.adoc
imap-send.c

index 3d28f7264374e69f45afc9efb201136cb2049c8b..29b998d5ff85b058ef4e13cbb6cb0069e1aa1bf0 100644 (file)
@@ -40,5 +40,6 @@ imap.authMethod::
        Specify the authentication method for authenticating with the IMAP server.
        If Git was built with the NO_CURL option, or if your curl version is older
        than 7.34.0, or if you're running git-imap-send with the `--no-curl`
-       option, the only supported method is 'CRAM-MD5'. If this is not set
-       then 'git imap-send' uses the basic IMAP plaintext LOGIN command.
+       option, the only supported methods are `CRAM-MD5`, `OAUTHBEARER` and
+       `XOAUTH2`. If this is not set then `git imap-send` uses the basic IMAP
+       plaintext `LOGIN` command.
index 26ccf4e433b44f2df3886a0d761c57323c1b1b11..8adf0e5aac46f017ce74d29e662bd6c08529810d 100644 (file)
@@ -102,12 +102,18 @@ Using Gmail's IMAP interface:
 
 ---------
 [imap]
-       folder = "[Gmail]/Drafts"
-       host = imaps://imap.gmail.com
-       user = user@gmail.com
-       port = 993
+    folder = "[Gmail]/Drafts"
+    host = imaps://imap.gmail.com
+    user = user@gmail.com
+    port = 993
 ---------
 
+Gmail does not allow using your regular password for `git imap-send`.
+If you have multi-factor authentication set up on your Gmail account, you
+can generate an app-specific password for use with `git imap-send`.
+Visit https://security.google.com/settings/security/apppasswords to create
+it. Alternatively, use OAuth2.0 authentication as described below.
+
 [NOTE]
 You might need to instead use: `folder = "[Google Mail]/Drafts"` if you get an error
 that the "Folder doesn't exist".
@@ -116,6 +122,35 @@ that the "Folder doesn't exist".
 If your Gmail account is set to another language than English, the name of the "Drafts"
 folder will be localized.
 
+If you want to use OAuth2.0 based authentication, you can specify
+`OAUTHBEARER` or `XOAUTH2` mechanism in your config. It is more secure
+than using app-specific passwords, and also does not enforce the need of
+having multi-factor authentication. You will have to use an OAuth2.0
+access token in place of your password when using this authentication.
+
+---------
+[imap]
+    folder = "[Gmail]/Drafts"
+    host = imaps://imap.gmail.com
+    user = user@gmail.com
+    port = 993
+    authmethod = OAUTHBEARER
+---------
+
+Using Outlook's IMAP interface:
+
+Unlike Gmail, Outlook only supports OAuth2.0 based authentication. Also, it
+supports only `XOAUTH2` as the mechanism.
+
+---------
+[imap]
+    folder = "Drafts"
+    host = imaps://outlook.office365.com
+    user = user@outlook.com
+    port = 993
+    authmethod = XOAUTH2
+---------
+
 Once the commits are ready to be sent, run the following command:
 
   $ git format-patch --cover-letter -M --stdout origin/master | git imap-send
@@ -124,6 +159,10 @@ Just make sure to disable line wrapping in the email client (Gmail's web
 interface will wrap lines no matter what, so you need to use a real
 IMAP client).
 
+In case you are using OAuth2.0 authentication, it is easier to use credential
+helpers to generate tokens. Credential helpers suggested in
+linkgit:git-send-email[1] can be used for `git imap-send` as well.
+
 CAUTION
 -------
 It is still your responsibility to make sure that the email message
index f55399cd9e0fe1f1874901953c2bea725dcc1f3c..6c33318102633bc70959cb718eeb0d5e35147469 100644 (file)
@@ -139,7 +139,9 @@ enum CAPABILITY {
        LITERALPLUS,
        NAMESPACE,
        STARTTLS,
-       AUTH_CRAM_MD5
+       AUTH_CRAM_MD5,
+       AUTH_OAUTHBEARER,
+       AUTH_XOAUTH2,
 };
 
 static const char *cap_list[] = {
@@ -149,6 +151,8 @@ static const char *cap_list[] = {
        "NAMESPACE",
        "STARTTLS",
        "AUTH=CRAM-MD5",
+       "AUTH=OAUTHBEARER",
+       "AUTH=XOAUTH2",
 };
 
 #define RESP_OK    0
@@ -885,6 +889,64 @@ static char *cram(const char *challenge_64, const char *user, const char *pass)
        return (char *)response_64;
 }
 
+static char *oauthbearer_base64(const char *user, const char *access_token)
+{
+       int b64_len;
+       char *raw, *b64;
+
+       /*
+        * Compose the OAUTHBEARER string
+        *
+        * "n,a=" {User} ",^Ahost=" {Host} "^Aport=" {Port} "^Aauth=Bearer " {Access Token} "^A^A
+        *
+        * The first part `n,a=" {User} ",` is the gs2 header described in RFC5801.
+        * * gs2-cb-flag `n` -> client does not support CB
+        * * gs2-authzid `a=" {User} "`
+        *
+        * The second part are key value pairs containing host, port and auth as
+        * described in RFC7628.
+        *
+        * https://datatracker.ietf.org/doc/html/rfc5801
+        * https://datatracker.ietf.org/doc/html/rfc7628
+        */
+       raw = xstrfmt("n,a=%s,\001auth=Bearer %s\001\001", user, access_token);
+
+       /* Base64 encode */
+       b64 = xmallocz(ENCODED_SIZE(strlen(raw)));
+       b64_len = EVP_EncodeBlock((unsigned char *)b64, (unsigned char *)raw, strlen(raw));
+       free(raw);
+
+       if (b64_len < 0) {
+               free(b64);
+               return NULL;
+       }
+       return b64;
+}
+
+static char *xoauth2_base64(const char *user, const char *access_token)
+{
+       int b64_len;
+       char *raw, *b64;
+
+       /*
+        * Compose the XOAUTH2 string
+        * "user=" {User} "^Aauth=Bearer " {Access Token} "^A^A"
+        * https://developers.google.com/workspace/gmail/imap/xoauth2-protocol#initial_client_response
+        */
+       raw = xstrfmt("user=%s\001auth=Bearer %s\001\001", user, access_token);
+
+       /* Base64 encode */
+       b64 = xmallocz(ENCODED_SIZE(strlen(raw)));
+       b64_len = EVP_EncodeBlock((unsigned char *)b64, (unsigned char *)raw, strlen(raw));
+       free(raw);
+
+       if (b64_len < 0) {
+               free(b64);
+               return NULL;
+       }
+       return b64;
+}
+
 static int auth_cram_md5(struct imap_store *ctx, const char *prompt)
 {
        int ret;
@@ -903,9 +965,51 @@ static int auth_cram_md5(struct imap_store *ctx, const char *prompt)
        return 0;
 }
 
+static int auth_oauthbearer(struct imap_store *ctx, const char *prompt UNUSED)
+{
+       int ret;
+       char *b64;
+
+       b64 = oauthbearer_base64(ctx->cfg->user, ctx->cfg->pass);
+       if (!b64)
+               return error("OAUTHBEARER: base64 encoding failed");
+
+       /* Send the base64-encoded response */
+       ret = socket_write(&ctx->imap->buf.sock, b64, strlen(b64));
+       if (ret != (int)strlen(b64)) {
+               free(b64);
+               return error("IMAP error: sending OAUTHBEARER response failed");
+       }
+
+       free(b64);
+       return 0;
+}
+
+static int auth_xoauth2(struct imap_store *ctx, const char *prompt UNUSED)
+{
+       int ret;
+       char *b64;
+
+       b64 = xoauth2_base64(ctx->cfg->user, ctx->cfg->pass);
+       if (!b64)
+               return error("XOAUTH2: base64 encoding failed");
+
+       /* Send the base64-encoded response */
+       ret = socket_write(&ctx->imap->buf.sock, b64, strlen(b64));
+       if (ret != (int)strlen(b64)) {
+               free(b64);
+               return error("IMAP error: sending XOAUTH2 response failed");
+       }
+
+       free(b64);
+       return 0;
+}
+
 #else
 
 #define auth_cram_md5 NULL
+#define auth_oauthbearer NULL
+#define auth_xoauth2 NULL
 
 #endif
 
@@ -1118,6 +1222,12 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c
                        if (!strcmp(srvc->auth_method, "CRAM-MD5")) {
                                if (try_auth_method(srvc, ctx, imap, "CRAM-MD5", AUTH_CRAM_MD5, auth_cram_md5))
                                        goto bail;
+                       } else if (!strcmp(srvc->auth_method, "OAUTHBEARER")) {
+                               if (try_auth_method(srvc, ctx, imap, "OAUTHBEARER", AUTH_OAUTHBEARER, auth_oauthbearer))
+                                       goto bail;
+                       } else if (!strcmp(srvc->auth_method, "XOAUTH2")) {
+                               if (try_auth_method(srvc, ctx, imap, "XOAUTH2", AUTH_XOAUTH2, auth_xoauth2))
+                                       goto bail;
                        } else {
                                fprintf(stderr, "Unknown authentication method:%s\n", srvc->host);
                                goto bail;
@@ -1419,7 +1529,16 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred)
 
        server_fill_credential(srvc, cred);
        curl_easy_setopt(curl, CURLOPT_USERNAME, srvc->user);
-       curl_easy_setopt(curl, CURLOPT_PASSWORD, srvc->pass);
+
+       /*
+        * Use CURLOPT_PASSWORD irrespective of whether there is
+        * an auth method specified or not, unless it's OAuth2.0,
+        * where we use CURLOPT_XOAUTH2_BEARER.
+        */
+       if (!srvc->auth_method ||
+           (strcmp(srvc->auth_method, "XOAUTH2") &&
+           strcmp(srvc->auth_method, "OAUTHBEARER")))
+               curl_easy_setopt(curl, CURLOPT_PASSWORD, srvc->pass);
 
        strbuf_addstr(&path, srvc->use_ssl ? "imaps://" : "imap://");
        strbuf_addstr(&path, srvc->host);
@@ -1437,11 +1556,22 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred)
        curl_easy_setopt(curl, CURLOPT_PORT, srvc->port);
 
        if (srvc->auth_method) {
-               struct strbuf auth = STRBUF_INIT;
-               strbuf_addstr(&auth, "AUTH=");
-               strbuf_addstr(&auth, srvc->auth_method);
-               curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, auth.buf);
-               strbuf_release(&auth);
+               if (!strcmp(srvc->auth_method, "XOAUTH2") ||
+                   !strcmp(srvc->auth_method, "OAUTHBEARER")) {
+
+                       /*
+                        * While CURLOPT_XOAUTH2_BEARER looks as if it only supports XOAUTH2,
+                        * upon debugging, it has been found that it is capable of detecting
+                        * the best option out of OAUTHBEARER and XOAUTH2.
+                        */
+                       curl_easy_setopt(curl, CURLOPT_XOAUTH2_BEARER, srvc->pass);
+               } else {
+                       struct strbuf auth = STRBUF_INIT;
+                       strbuf_addstr(&auth, "AUTH=");
+                       strbuf_addstr(&auth, srvc->auth_method);
+                       curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, auth.buf);
+                       strbuf_release(&auth);
+               }
        }
 
        if (!srvc->use_ssl)