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>
This commit is contained in:
committed by
Junio C Hamano
parent
b9e766604d
commit
103d7b12b7
@@ -40,5 +40,6 @@ imap.authMethod::
|
|||||||
Specify the authentication method for authenticating with the IMAP server.
|
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
|
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`
|
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
|
option, the only supported methods are `CRAM-MD5`, `OAUTHBEARER` and
|
||||||
then 'git imap-send' uses the basic IMAP plaintext LOGIN command.
|
`XOAUTH2`. If this is not set then `git imap-send` uses the basic IMAP
|
||||||
|
plaintext `LOGIN` command.
|
||||||
|
|||||||
@@ -108,6 +108,12 @@ Using Gmail's IMAP interface:
|
|||||||
port = 993
|
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]
|
[NOTE]
|
||||||
You might need to instead use: `folder = "[Google Mail]/Drafts"` if you get an error
|
You might need to instead use: `folder = "[Google Mail]/Drafts"` if you get an error
|
||||||
that the "Folder doesn't exist".
|
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"
|
If your Gmail account is set to another language than English, the name of the "Drafts"
|
||||||
folder will be localized.
|
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:
|
Once the commits are ready to be sent, run the following command:
|
||||||
|
|
||||||
$ git format-patch --cover-letter -M --stdout origin/master | git imap-send
|
$ 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
|
interface will wrap lines no matter what, so you need to use a real
|
||||||
IMAP client).
|
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
|
CAUTION
|
||||||
-------
|
-------
|
||||||
It is still your responsibility to make sure that the email message
|
It is still your responsibility to make sure that the email message
|
||||||
|
|||||||
132
imap-send.c
132
imap-send.c
@@ -139,7 +139,9 @@ enum CAPABILITY {
|
|||||||
LITERALPLUS,
|
LITERALPLUS,
|
||||||
NAMESPACE,
|
NAMESPACE,
|
||||||
STARTTLS,
|
STARTTLS,
|
||||||
AUTH_CRAM_MD5
|
AUTH_CRAM_MD5,
|
||||||
|
AUTH_OAUTHBEARER,
|
||||||
|
AUTH_XOAUTH2,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char *cap_list[] = {
|
static const char *cap_list[] = {
|
||||||
@@ -149,6 +151,8 @@ static const char *cap_list[] = {
|
|||||||
"NAMESPACE",
|
"NAMESPACE",
|
||||||
"STARTTLS",
|
"STARTTLS",
|
||||||
"AUTH=CRAM-MD5",
|
"AUTH=CRAM-MD5",
|
||||||
|
"AUTH=OAUTHBEARER",
|
||||||
|
"AUTH=XOAUTH2",
|
||||||
};
|
};
|
||||||
|
|
||||||
#define RESP_OK 0
|
#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;
|
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)
|
static int auth_cram_md5(struct imap_store *ctx, const char *prompt)
|
||||||
{
|
{
|
||||||
int ret;
|
int ret;
|
||||||
@@ -903,9 +965,51 @@ static int auth_cram_md5(struct imap_store *ctx, const char *prompt)
|
|||||||
return 0;
|
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
|
#else
|
||||||
|
|
||||||
#define auth_cram_md5 NULL
|
#define auth_cram_md5 NULL
|
||||||
|
#define auth_oauthbearer NULL
|
||||||
|
#define auth_xoauth2 NULL
|
||||||
|
|
||||||
#endif
|
#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 (!strcmp(srvc->auth_method, "CRAM-MD5")) {
|
||||||
if (try_auth_method(srvc, ctx, imap, "CRAM-MD5", AUTH_CRAM_MD5, auth_cram_md5))
|
if (try_auth_method(srvc, ctx, imap, "CRAM-MD5", AUTH_CRAM_MD5, auth_cram_md5))
|
||||||
goto bail;
|
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 {
|
} else {
|
||||||
fprintf(stderr, "Unknown authentication method:%s\n", srvc->host);
|
fprintf(stderr, "Unknown authentication method:%s\n", srvc->host);
|
||||||
goto bail;
|
goto bail;
|
||||||
@@ -1419,6 +1529,15 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred)
|
|||||||
|
|
||||||
server_fill_credential(srvc, cred);
|
server_fill_credential(srvc, cred);
|
||||||
curl_easy_setopt(curl, CURLOPT_USERNAME, srvc->user);
|
curl_easy_setopt(curl, CURLOPT_USERNAME, srvc->user);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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);
|
curl_easy_setopt(curl, CURLOPT_PASSWORD, srvc->pass);
|
||||||
|
|
||||||
strbuf_addstr(&path, srvc->use_ssl ? "imaps://" : "imap://");
|
strbuf_addstr(&path, srvc->use_ssl ? "imaps://" : "imap://");
|
||||||
@@ -1437,12 +1556,23 @@ static CURL *setup_curl(struct imap_server_conf *srvc, struct credential *cred)
|
|||||||
curl_easy_setopt(curl, CURLOPT_PORT, srvc->port);
|
curl_easy_setopt(curl, CURLOPT_PORT, srvc->port);
|
||||||
|
|
||||||
if (srvc->auth_method) {
|
if (srvc->auth_method) {
|
||||||
|
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;
|
struct strbuf auth = STRBUF_INIT;
|
||||||
strbuf_addstr(&auth, "AUTH=");
|
strbuf_addstr(&auth, "AUTH=");
|
||||||
strbuf_addstr(&auth, srvc->auth_method);
|
strbuf_addstr(&auth, srvc->auth_method);
|
||||||
curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, auth.buf);
|
curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, auth.buf);
|
||||||
strbuf_release(&auth);
|
strbuf_release(&auth);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!srvc->use_ssl)
|
if (!srvc->use_ssl)
|
||||||
curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_TRY);
|
curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_TRY);
|
||||||
|
|||||||
Reference in New Issue
Block a user