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 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.
··· 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 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 103 --------- 104 [imap] 105 - folder = "[Gmail]/Drafts" 106 - host = imaps://imap.gmail.com 107 - user = user@gmail.com 108 - port = 993 109 --------- 110 111 [NOTE] 112 You might need to instead use: `folder = "[Google Mail]/Drafts"` if you get an error 113 that the "Folder doesn't exist". ··· 116 If your Gmail account is set to another language than English, the name of the "Drafts" 117 folder will be localized. 118 119 Once the commits are ready to be sent, run the following command: 120 121 $ git format-patch --cover-letter -M --stdout origin/master | git imap-send ··· 123 Just make sure to disable line wrapping in the email client (Gmail's web 124 interface will wrap lines no matter what, so you need to use a real 125 IMAP client). 126 127 CAUTION 128 -------
··· 102 103 --------- 104 [imap] 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". ··· 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 ··· 158 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 -------
+137 -7
imap-send.c
··· 139 LITERALPLUS, 140 NAMESPACE, 141 STARTTLS, 142 - AUTH_CRAM_MD5 143 }; 144 145 static const char *cap_list[] = { ··· 149 "NAMESPACE", 150 "STARTTLS", 151 "AUTH=CRAM-MD5", 152 }; 153 154 #define RESP_OK 0 ··· 885 return (char *)response_64; 886 } 887 888 static int auth_cram_md5(struct imap_store *ctx, const char *prompt) 889 { 890 int ret; ··· 903 return 0; 904 } 905 906 #else 907 908 #define auth_cram_md5 NULL 909 910 #endif 911 ··· 1117 if (srvc->auth_method) { 1118 if (!strcmp(srvc->auth_method, "CRAM-MD5")) { 1119 if (try_auth_method(srvc, ctx, imap, "CRAM-MD5", AUTH_CRAM_MD5, auth_cram_md5)) 1120 goto bail; 1121 } else { 1122 fprintf(stderr, "Unknown authentication method:%s\n", srvc->host); ··· 1419 1420 server_fill_credential(srvc, cred); 1421 curl_easy_setopt(curl, CURLOPT_USERNAME, srvc->user); 1422 - curl_easy_setopt(curl, CURLOPT_PASSWORD, srvc->pass); 1423 1424 strbuf_addstr(&path, srvc->use_ssl ? "imaps://" : "imap://"); 1425 strbuf_addstr(&path, srvc->host); ··· 1437 curl_easy_setopt(curl, CURLOPT_PORT, srvc->port); 1438 1439 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); 1445 } 1446 1447 if (!srvc->use_ssl)
··· 139 LITERALPLUS, 140 NAMESPACE, 141 STARTTLS, 142 + AUTH_CRAM_MD5, 143 + AUTH_OAUTHBEARER, 144 + AUTH_XOAUTH2, 145 }; 146 147 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 ··· 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; ··· 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 ··· 1221 if (srvc->auth_method) { 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); ··· 1529 1530 server_fill_credential(srvc, cred); 1531 curl_easy_setopt(curl, CURLOPT_USERNAME, srvc->user); 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); ··· 1556 curl_easy_setopt(curl, CURLOPT_PORT, srvc->port); 1557 1558 if (srvc->auth_method) { 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)