imap-send: add support for OAuth2.0 authentication
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>
Aditya Garg committed
Jun 20, 2025 at 12:10 UTC
103d7b12b7adeee88a95e642ffd105a25335bfef
3 files changed
+183
-13
Documentation/config/imap.adoc
+3
-2
@@ -40,5 +40,6 @@ imap.authMethod::
40
Specify the authentication method for authenticating with the IMAP server.
41
If Git was built with the NO_CURL option, or if your curl version is older
42
than 7.34.0, or if you're running git-imap-send with the `--no-curl`
43
- option, the only supported method is 'CRAM-MD5'. If this is not set
44
- then 'git imap-send' uses the basic IMAP plaintext LOGIN command.
43
+ option, the only supported methods are `CRAM-MD5`, `OAUTHBEARER` and
44
+ `XOAUTH2`. If this is not set then `git imap-send` uses the basic IMAP
45
+ plaintext `LOGIN` command.
Documentation/git-imap-send.adoc
+43
-4
@@ -102,12 +102,18 @@ Using Gmail's IMAP interface:
102
103
---------
104
[imap]
105
- folder = "[Gmail]/Drafts"
106
- host = imaps://imap.gmail.com
107
- user = user@gmail.com
108
- port = 993
105
+ folder = "[Gmail]/Drafts"
106
+ host = imaps://imap.gmail.com
107
+ user = user@gmail.com
108
+ port = 993
109
---------
110
111
+Gmail does not allow using your regular password for `git imap-send`.
112
+If you have multi-factor authentication set up on your Gmail account, you
113
+can generate an app-specific password for use with `git imap-send`.
114
+Visit https://security.google.com/settings/security/apppasswords to create
115
+it. Alternatively, use OAuth2.0 authentication as described below.
116
+
117
[NOTE]
118
You might need to instead use: `folder = "[Google Mail]/Drafts"` if you get an error
119
that the "Folder doesn't exist".
@@ -116,6 +122,35 @@ that the "Folder doesn't exist".
122
If your Gmail account is set to another language than English, the name of the "Drafts"
123
folder will be localized.
124
125
+If you want to use OAuth2.0 based authentication, you can specify
126
+`OAUTHBEARER` or `XOAUTH2` mechanism in your config. It is more secure
127
+than using app-specific passwords, and also does not enforce the need of
128
+having multi-factor authentication. You will have to use an OAuth2.0
129
+access token in place of your password when using this authentication.
130
+
131
+---------
132
+[imap]
133
+ folder = "[Gmail]/Drafts"
134
+ host = imaps://imap.gmail.com
135
+ user = user@gmail.com
136
+ port = 993
137
+ authmethod = OAUTHBEARER
138
+---------
139
+
140
+Using Outlook's IMAP interface:
141
+
142
+Unlike Gmail, Outlook only supports OAuth2.0 based authentication. Also, it
143
+supports only `XOAUTH2` as the mechanism.
144
+
145
+---------
146
+[imap]
147
+ folder = "Drafts"
148
+ host = imaps://outlook.office365.com
149
+ user = user@outlook.com
150
+ port = 993
151
+ authmethod = XOAUTH2
152
+---------
153
+
154
Once the commits are ready to be sent, run the following command:
155
156
$ 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
159
interface will wrap lines no matter what, so you need to use a real
160
IMAP client).
161
162
+In case you are using OAuth2.0 authentication, it is easier to use credential
163
+helpers to generate tokens. Credential helpers suggested in
164
+linkgit:git-send-email[1] can be used for `git imap-send` as well.
165
+
166
CAUTION
167
-------
168
It is still your responsibility to make sure that the email message
imap-send.c
+137
-7
@@ -139,7 +139,9 @@ enum CAPABILITY {
139
LITERALPLUS,
140
NAMESPACE,
141
STARTTLS,
142
- AUTH_CRAM_MD5
142
+ AUTH_CRAM_MD5,
143
+ AUTH_OAUTHBEARER,
144
+ AUTH_XOAUTH2,
145
};
146
147
static const char *cap_list[] = {
@@ -149,6 +151,8 @@ static const char *cap_list[] = {
151
"NAMESPACE",
152
"STARTTLS",
153
"AUTH=CRAM-MD5",
154
+ "AUTH=OAUTHBEARER",
155
+ "AUTH=XOAUTH2",
156
};
157
158
#define RESP_OK 0
@@ -885,6 +889,64 @@ static char *cram(const char *challenge_64, const char *user, const char *pass)
889
return (char *)response_64;
890
}
891
892
+static char *oauthbearer_base64(const char *user, const char *access_token)
893
+{
894
+ int b64_len;
895
+ char *raw, *b64;
896
+
897
+ /*
898
+ * Compose the OAUTHBEARER string
899
+ *
900
+ * "n,a=" {User} ",^Ahost=" {Host} "^Aport=" {Port} "^Aauth=Bearer " {Access Token} "^A^A
901
+ *
902
+ * The first part `n,a=" {User} ",` is the gs2 header described in RFC5801.
903
+ * * gs2-cb-flag `n` -> client does not support CB
904
+ * * gs2-authzid `a=" {User} "`
905
+ *
906
+ * The second part are key value pairs containing host, port and auth as
907
+ * described in RFC7628.
908
+ *
909
+ * https://datatracker.ietf.org/doc/html/rfc5801
910
+ * https://datatracker.ietf.org/doc/html/rfc7628
911
+ */
912
+ raw = xstrfmt("n,a=%s,\001auth=Bearer %s\001\001", user, access_token);
913
+
914
+ /* Base64 encode */
915
+ b64 = xmallocz(ENCODED_SIZE(strlen(raw)));
916
+ b64_len = EVP_EncodeBlock((unsigned char *)b64, (unsigned char *)raw, strlen(raw));
917
+ free(raw);
918
+
919
+ if (b64_len < 0) {
920
+ free(b64);
921
+ return NULL;
922
+ }
923
+ return b64;
924
+}
925
+
926
+static char *xoauth2_base64(const char *user, const char *access_token)
927
+{
928
+ int b64_len;
929
+ char *raw, *b64;
930
+
931
+ /*
932
+ * Compose the XOAUTH2 string
933
+ * "user=" {User} "^Aauth=Bearer " {Access Token} "^A^A"
934
+ * https://developers.google.com/workspace/gmail/imap/xoauth2-protocol#initial_client_response
935
+ */
936
+ raw = xstrfmt("user=%s\001auth=Bearer %s\001\001", user, access_token);
937
+
938
+ /* Base64 encode */
939
+ b64 = xmallocz(ENCODED_SIZE(strlen(raw)));
940
+ b64_len = EVP_EncodeBlock((unsigned char *)b64, (unsigned char *)raw, strlen(raw));
941
+ free(raw);
942
+
943
+ if (b64_len < 0) {
944
+ free(b64);
945
+ return NULL;
946
+ }
947
+ return b64;
948
+}
949
+
950
static int auth_cram_md5(struct imap_store *ctx, const char *prompt)
951
{
952
int ret;
@@ -903,9 +965,51 @@ static int auth_cram_md5(struct imap_store *ctx, const char *prompt)
965
return 0;
966
}
967
968
+static int auth_oauthbearer(struct imap_store *ctx, const char *prompt UNUSED)
969
+{
970
+ int ret;
971
+ char *b64;
972
+
973
+ b64 = oauthbearer_base64(ctx->cfg->user, ctx->cfg->pass);
974
+ if (!b64)
975
+ return error("OAUTHBEARER: base64 encoding failed");
976
+
977
+ /* Send the base64-encoded response */
978
+ ret = socket_write(&ctx->imap->buf.sock, b64, strlen(b64));
979
+ if (ret != (int)strlen(b64)) {
980
+ free(b64);
981
+ return error("IMAP error: sending OAUTHBEARER response failed");
982
+ }
983
+
984
+ free(b64);
985
+ return 0;
986
+}
987
+
988
+static int auth_xoauth2(struct imap_store *ctx, const char *prompt UNUSED)
989
+{
990
+ int ret;
991
+ char *b64;
992
+
993
+ b64 = xoauth2_base64(ctx->cfg->user, ctx->cfg->pass);
994
+ if (!b64)
995
+ return error("XOAUTH2: base64 encoding failed");
996
+
997
+ /* Send the base64-encoded response */
998
+ ret = socket_write(&ctx->imap->buf.sock, b64, strlen(b64));
999
+ if (ret != (int)strlen(b64)) {
1000
+ free(b64);
1001
+ return error("IMAP error: sending XOAUTH2 response failed");
1002
+ }
1003
+
1004
+ free(b64);
1005
+ return 0;
1006
+}
1007
+
1008
#else
1009
1010
#define auth_cram_md5 NULL
1011
+#define auth_oauthbearer NULL
1012
+#define auth_xoauth2 NULL
1013
1014
#endif
1015
@@ -1118,6 +1222,12 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c
1222
if (!strcmp(srvc->auth_method, "CRAM-MD5")) {
1223
if (try_auth_method(srvc, ctx, imap, "CRAM-MD5", AUTH_CRAM_MD5, auth_cram_md5))
1224
goto bail;
1225
+ } else if (!strcmp(srvc->auth_method, "OAUTHBEARER")) {
1226
+ if (try_auth_method(srvc, ctx, imap, "OAUTHBEARER", AUTH_OAUTHBEARER, auth_oauthbearer))
1227
+ goto bail;
1228
+ } else if (!strcmp(srvc->auth_method, "XOAUTH2")) {
1229
+ if (try_auth_method(srvc, ctx, imap, "XOAUTH2", AUTH_XOAUTH2, auth_xoauth2))
1230
+ goto bail;
1231
} else {
1232
fprintf(stderr, "Unknown authentication method:%s\n", srvc->host);
1233
goto bail;
@@ -1419,7 +1529,16 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred)
1529
1530
server_fill_credential(srvc, cred);
1531
curl_easy_setopt(curl, CURLOPT_USERNAME, srvc->user);
1422
- curl_easy_setopt(curl, CURLOPT_PASSWORD, srvc->pass);
1532
+
1533
+ /*
1534
+ * Use CURLOPT_PASSWORD irrespective of whether there is
1535
+ * an auth method specified or not, unless it's OAuth2.0,
1536
+ * where we use CURLOPT_XOAUTH2_BEARER.
1537
+ */
1538
+ if (!srvc->auth_method ||
1539
+ (strcmp(srvc->auth_method, "XOAUTH2") &&
1540
+ strcmp(srvc->auth_method, "OAUTHBEARER")))
1541
+ curl_easy_setopt(curl, CURLOPT_PASSWORD, srvc->pass);
1542
1543
strbuf_addstr(&path, srvc->use_ssl ? "imaps://" : "imap://");
1544
strbuf_addstr(&path, srvc->host);
@@ -1437,11 +1556,22 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred)
1556
curl_easy_setopt(curl, CURLOPT_PORT, srvc->port);
1557
1558
if (srvc->auth_method) {
1440
- struct strbuf auth = STRBUF_INIT;
1441
- strbuf_addstr(&auth, "AUTH=");
1442
- strbuf_addstr(&auth, srvc->auth_method);
1443
- curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, auth.buf);
1444
- strbuf_release(&auth);
1559
+ if (!strcmp(srvc->auth_method, "XOAUTH2") ||
1560
+ !strcmp(srvc->auth_method, "OAUTHBEARER")) {
1561
+
1562
+ /*
1563
+ * While CURLOPT_XOAUTH2_BEARER looks as if it only supports XOAUTH2,
1564
+ * upon debugging, it has been found that it is capable of detecting
1565
+ * the best option out of OAUTHBEARER and XOAUTH2.
1566
+ */
1567
+ curl_easy_setopt(curl, CURLOPT_XOAUTH2_BEARER, srvc->pass);
1568
+ } else {
1569
+ struct strbuf auth = STRBUF_INIT;
1570
+ strbuf_addstr(&auth, "AUTH=");
1571
+ strbuf_addstr(&auth, srvc->auth_method);
1572
+ curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, auth.buf);
1573
+ strbuf_release(&auth);
1574
+ }
1575
}
1576
1577
if (!srvc->use_ssl)