Skip to content

Commit e26467a

Browse files
AdityaGarg8gitster
authored andcommitted
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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 9964fcd commit e26467a

File tree

3 files changed

+215
-13
lines changed

3 files changed

+215
-13
lines changed

Documentation/config/imap.adoc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@ imap.authMethod::
4040
Specify the authentication method for authenticating with the IMAP server.
4141
If Git was built with the NO_CURL option, or if your curl version is older
4242
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

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,18 @@ Using Gmail's IMAP interface:
102102

103103
---------
104104
[imap]
105-
folder = "[Gmail]/Drafts"
106-
host = imaps://imap.gmail.com
107-
108-
port = 993
105+
folder = "[Gmail]/Drafts"
106+
host = imaps://imap.gmail.com
107+
108+
port = 993
109109
---------
110110

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 can generate
113+
an app-specific password for use with `git imap-send`.
114+
Visit https://security.google.com/settings/security/apppasswords to create it.
115+
Alternatively, use OAuth2.0 authentication as described below.
116+
111117
[NOTE]
112118
You might need to instead use: `folder = "[Google Mail]/Drafts"` if you get an error
113119
that the "Folder doesn't exist".
@@ -116,6 +122,35 @@ that the "Folder doesn't exist".
116122
If your Gmail account is set to another language than English, the name of the "Drafts"
117123
folder will be localized.
118124

125+
If you want to use OAuth2.0 based authentication, you can specify `OAUTHBEARER`
126+
or `XOAUTH2` mechanism in your config. It is more secure than using app-specific
127+
passwords, and also does not enforce the need of having multi-factor authentication.
128+
You will have to use an OAuth2.0 access token in place of your password when using this
129+
authentication.
130+
131+
---------
132+
[imap]
133+
folder = "[Gmail]/Drafts"
134+
host = imaps://imap.gmail.com
135+
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+
150+
port = 993
151+
authmethod = XOAUTH2
152+
---------
153+
119154
Once the commits are ready to be sent, run the following command:
120155

121156
$ 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
124159
interface will wrap lines no matter what, so you need to use a real
125160
IMAP client).
126161

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+
127166
CAUTION
128167
-------
129168
It is still your responsibility to make sure that the email message

imap-send.c

Lines changed: 169 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ enum CAPABILITY {
139139
LITERALPLUS,
140140
NAMESPACE,
141141
STARTTLS,
142-
AUTH_CRAM_MD5
142+
AUTH_CRAM_MD5,
143+
AUTH_OAUTHBEARER,
144+
AUTH_XOAUTH2
143145
};
144146

145147
static const char *cap_list[] = {
@@ -149,6 +151,8 @@ static const char *cap_list[] = {
149151
"NAMESPACE",
150152
"STARTTLS",
151153
"AUTH=CRAM-MD5",
154+
"AUTH=OAUTHBEARER",
155+
"AUTH=XOAUTH2",
152156
};
153157

154158
#define RESP_OK 0
@@ -885,6 +889,66 @@ static char *cram(const char *challenge_64, const char *user, const char *pass)
885889
return (char *)response_64;
886890
}
887891

892+
static char *oauthbearer_base64(const char *user, const char *access_token)
893+
{
894+
int raw_len, b64_len;
895+
char *raw, *b64;
896+
897+
/* Compose the OAUTHBEARER string
898+
*
899+
* "n,a=" {User} ",^Ahost=" {Host} "^Aport=" {Port} "^Aauth=Bearer " {Access Token} "^A^A
900+
*
901+
* The first part `n,a=" {User} ",` is the gs2 header described in RFC5801.
902+
* * gs2-cb-flag `n` -> client does not support CB
903+
* * gs2-authzid `a=" {User} "`
904+
*
905+
* The second part are key value pairs containing host, port and auth as
906+
* described in RFC7628.
907+
*
908+
* https://datatracker.ietf.org/doc/html/rfc5801
909+
* https://datatracker.ietf.org/doc/html/rfc7628
910+
*/
911+
raw_len = strlen(user) + strlen(access_token) + 20;
912+
raw = xmallocz(raw_len + 1);
913+
snprintf(raw, raw_len + 1, "n,a=%s,\001auth=Bearer %s\001\001", user, access_token);
914+
915+
/* Base64 encode */
916+
b64 = xmallocz(ENCODED_SIZE(strlen(raw)));
917+
b64_len = EVP_EncodeBlock((unsigned char *)b64, (unsigned char *)raw, strlen(raw));
918+
free(raw);
919+
920+
if (b64_len < 0) {
921+
free(b64);
922+
return NULL;
923+
}
924+
return b64;
925+
}
926+
927+
static char *xoauth2_base64(const char *user, const char *access_token)
928+
{
929+
int raw_len, b64_len;
930+
char *raw, *b64;
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_len = strlen(user) + strlen(access_token) + 20;
937+
raw = xmallocz(raw_len + 1);
938+
snprintf(raw, raw_len + 1, "user=%s\001auth=Bearer %s\001\001", user, access_token);
939+
940+
/* Base64 encode */
941+
b64 = xmallocz(ENCODED_SIZE(strlen(raw)));
942+
b64_len = EVP_EncodeBlock((unsigned char *)b64, (unsigned char *)raw, strlen(raw));
943+
free(raw);
944+
945+
if (b64_len < 0) {
946+
free(b64);
947+
return NULL;
948+
}
949+
return b64;
950+
}
951+
888952
#else
889953

890954
static char *cram(const char *challenge_64 UNUSED,
@@ -895,6 +959,20 @@ static char *cram(const char *challenge_64 UNUSED,
895959
"you have to build git-imap-send with OpenSSL library.");
896960
}
897961

962+
static char *oauthbearer_base64(const char *user UNUSED,
963+
const char *access_token UNUSED)
964+
{
965+
die("You are trying to use OAUTHBEARER authenticate method "
966+
"with OpenSSL library, but its support has not been compiled in.");
967+
}
968+
969+
static char *xoauth2_base64(const char *user UNUSED,
970+
const char *access_token UNUSED)
971+
{
972+
die("You are trying to use XOAUTH2 authenticate method "
973+
"with OpenSSL library, but its support has not been compiled in.");
974+
}
975+
898976
#endif
899977

900978
static int auth_cram_md5(struct imap_store *ctx, const char *prompt)
@@ -913,6 +991,46 @@ static int auth_cram_md5(struct imap_store *ctx, const char *prompt)
913991
return 0;
914992
}
915993

994+
static int auth_oauthbearer(struct imap_store *ctx, const char *prompt UNUSED)
995+
{
996+
int ret;
997+
char *b64;
998+
999+
b64 = oauthbearer_base64(ctx->cfg->user, ctx->cfg->pass);
1000+
if (!b64)
1001+
return error("OAUTHBEARER: base64 encoding failed");
1002+
1003+
/* Send the base64-encoded response */
1004+
ret = socket_write(&ctx->imap->buf.sock, b64, strlen(b64));
1005+
if (ret != (int)strlen(b64)) {
1006+
free(b64);
1007+
return error("IMAP error: sending OAUTHBEARER response failed");
1008+
}
1009+
1010+
free(b64);
1011+
return 0;
1012+
}
1013+
1014+
static int auth_xoauth2(struct imap_store *ctx, const char *prompt UNUSED)
1015+
{
1016+
int ret;
1017+
char *b64;
1018+
1019+
b64 = xoauth2_base64(ctx->cfg->user, ctx->cfg->pass);
1020+
if (!b64)
1021+
return error("XOAUTH2: base64 encoding failed");
1022+
1023+
/* Send the base64-encoded response */
1024+
ret = socket_write(&ctx->imap->buf.sock, b64, strlen(b64));
1025+
if (ret != (int)strlen(b64)) {
1026+
free(b64);
1027+
return error("IMAP error: sending XOAUTH2 response failed");
1028+
}
1029+
1030+
free(b64);
1031+
return 0;
1032+
}
1033+
9161034
static void server_fill_credential(struct imap_server_conf *srvc, struct credential *cred)
9171035
{
9181036
if (srvc->user && srvc->pass)
@@ -1104,6 +1222,36 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c
11041222
fprintf(stderr, "IMAP error: AUTHENTICATE CRAM-MD5 failed\n");
11051223
goto bail;
11061224
}
1225+
} else if (!strcmp(srvc->auth_method, "OAUTHBEARER")) {
1226+
if (!CAP(AUTH_OAUTHBEARER)) {
1227+
fprintf(stderr, "You specified "
1228+
"OAUTHBEARER as authentication method, "
1229+
"but %s doesn't support it.\n", srvc->host);
1230+
goto bail;
1231+
}
1232+
/* OAUTHBEARER */
1233+
1234+
memset(&cb, 0, sizeof(cb));
1235+
cb.cont = auth_oauthbearer;
1236+
if (imap_exec(ctx, &cb, "AUTHENTICATE OAUTHBEARER") != RESP_OK) {
1237+
fprintf(stderr, "IMAP error: AUTHENTICATE OAUTHBEARER failed\n");
1238+
goto bail;
1239+
}
1240+
} else if (!strcmp(srvc->auth_method, "XOAUTH2")) {
1241+
if (!CAP(AUTH_XOAUTH2)) {
1242+
fprintf(stderr, "You specified "
1243+
"XOAUTH2 as authentication method, "
1244+
"but %s doesn't support it.\n", srvc->host);
1245+
goto bail;
1246+
}
1247+
/* XOAUTH2 */
1248+
1249+
memset(&cb, 0, sizeof(cb));
1250+
cb.cont = auth_xoauth2;
1251+
if (imap_exec(ctx, &cb, "AUTHENTICATE XOAUTH2") != RESP_OK) {
1252+
fprintf(stderr, "IMAP error: AUTHENTICATE XOAUTH2 failed\n");
1253+
goto bail;
1254+
}
11071255
} else {
11081256
fprintf(stderr, "Unknown authentication method:%s\n", srvc->host);
11091257
goto bail;
@@ -1405,7 +1553,11 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred)
14051553

14061554
server_fill_credential(srvc, cred);
14071555
curl_easy_setopt(curl, CURLOPT_USERNAME, srvc->user);
1408-
curl_easy_setopt(curl, CURLOPT_PASSWORD, srvc->pass);
1556+
1557+
if (!srvc->auth_method ||
1558+
strcmp(srvc->auth_method, "XOAUTH2") ||
1559+
strcmp(srvc->auth_method, "OAUTHBEARER"))
1560+
curl_easy_setopt(curl, CURLOPT_PASSWORD, srvc->pass);
14091561

14101562
strbuf_addstr(&path, srvc->use_ssl ? "imaps://" : "imap://");
14111563
strbuf_addstr(&path, srvc->host);
@@ -1423,11 +1575,21 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred)
14231575
curl_easy_setopt(curl, CURLOPT_PORT, srvc->port);
14241576

14251577
if (srvc->auth_method) {
1426-
struct strbuf auth = STRBUF_INIT;
1427-
strbuf_addstr(&auth, "AUTH=");
1428-
strbuf_addstr(&auth, srvc->auth_method);
1429-
curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, auth.buf);
1430-
strbuf_release(&auth);
1578+
if (!strcmp(srvc->auth_method, "XOAUTH2") ||
1579+
!strcmp(srvc->auth_method, "OAUTHBEARER")) {
1580+
1581+
/* While CURLOPT_XOAUTH2_BEARER looks as if it only supports XOAUTH2,
1582+
* upon debugging, it has been found that it is capable of detecting
1583+
* the best option out of OAUTHBEARER and XOAUTH2.
1584+
*/
1585+
curl_easy_setopt(curl, CURLOPT_XOAUTH2_BEARER, srvc->pass);
1586+
} else {
1587+
struct strbuf auth = STRBUF_INIT;
1588+
strbuf_addstr(&auth, "AUTH=");
1589+
strbuf_addstr(&auth, srvc->auth_method);
1590+
curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, auth.buf);
1591+
strbuf_release(&auth);
1592+
}
14311593
}
14321594

14331595
if (!srvc->use_ssl)

0 commit comments

Comments
 (0)