From: Aditya Garg Date: Fri, 20 Jun 2025 06:40:27 +0000 (+0530) Subject: imap-send: add support for OAuth2.0 authentication X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=103d7b12b7adeee88a95e642ffd105a25335bfef;p=thirdparty%2Fgit.git 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 Signed-off-by: Junio C Hamano --- diff --git a/Documentation/config/imap.adoc b/Documentation/config/imap.adoc index 3d28f72643..29b998d5ff 100644 --- a/Documentation/config/imap.adoc +++ b/Documentation/config/imap.adoc @@ -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. diff --git a/Documentation/git-imap-send.adoc b/Documentation/git-imap-send.adoc index 26ccf4e433..8adf0e5aac 100644 --- a/Documentation/git-imap-send.adoc +++ b/Documentation/git-imap-send.adoc @@ -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 diff --git a/imap-send.c b/imap-send.c index f55399cd9e..6c33318102 100644 --- a/imap-send.c +++ b/imap-send.c @@ -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)