Git fork

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>

authored by

Aditya Garg and committed by
Junio C Hamano
103d7b12 b9e76660

+183 -13
+3 -2
Documentation/config/imap.adoc
··· 40 40 Specify the authentication method for authenticating with the IMAP server. 41 41 If Git was built with the NO_CURL option, or if your curl version is older 42 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.
+43 -4
Documentation/git-imap-send.adoc
··· 102 102 103 103 --------- 104 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 109 --------- 110 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 + 111 117 [NOTE] 112 118 You might need to instead use: `folder = "[Google Mail]/Drafts"` if you get an error 113 119 that the "Folder doesn't exist". ··· 116 122 If your Gmail account is set to another language than English, the name of the "Drafts" 117 123 folder will be localized. 118 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 + 119 154 Once the commits are ready to be sent, run the following command: 120 155 121 156 $ git format-patch --cover-letter -M --stdout origin/master | git imap-send ··· 123 158 Just make sure to disable line wrapping in the email client (Gmail's web 124 159 interface will wrap lines no matter what, so you need to use a real 125 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. 126 165 127 166 CAUTION 128 167 -------
+137 -7
imap-send.c
··· 139 139 LITERALPLUS, 140 140 NAMESPACE, 141 141 STARTTLS, 142 - AUTH_CRAM_MD5 142 + AUTH_CRAM_MD5, 143 + AUTH_OAUTHBEARER, 144 + AUTH_XOAUTH2, 143 145 }; 144 146 145 147 static const char *cap_list[] = { ··· 149 151 "NAMESPACE", 150 152 "STARTTLS", 151 153 "AUTH=CRAM-MD5", 154 + "AUTH=OAUTHBEARER", 155 + "AUTH=XOAUTH2", 152 156 }; 153 157 154 158 #define RESP_OK 0 ··· 885 889 return (char *)response_64; 886 890 } 887 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 + 888 950 static int auth_cram_md5(struct imap_store *ctx, const char *prompt) 889 951 { 890 952 int ret; ··· 903 965 return 0; 904 966 } 905 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 + 906 1008 #else 907 1009 908 1010 #define auth_cram_md5 NULL 1011 + #define auth_oauthbearer NULL 1012 + #define auth_xoauth2 NULL 909 1013 910 1014 #endif 911 1015 ··· 1117 1221 if (srvc->auth_method) { 1118 1222 if (!strcmp(srvc->auth_method, "CRAM-MD5")) { 1119 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)) 1120 1230 goto bail; 1121 1231 } else { 1122 1232 fprintf(stderr, "Unknown authentication method:%s\n", srvc->host); ··· 1419 1529 1420 1530 server_fill_credential(srvc, cred); 1421 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); 1423 1542 1424 1543 strbuf_addstr(&path, srvc->use_ssl ? "imaps://" : "imap://"); 1425 1544 strbuf_addstr(&path, srvc->host); ··· 1437 1556 curl_easy_setopt(curl, CURLOPT_PORT, srvc->port); 1438 1557 1439 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 + } 1445 1575 } 1446 1576 1447 1577 if (!srvc->use_ssl)