Skip to content

Commit

Permalink
credential: new attribute oauth_refresh_token
Browse files Browse the repository at this point in the history
Git authentication with OAuth access token is supported by every popular
Git host including GitHub, GitLab and BitBucket [1][2][3]. Credential
helpers Git Credential Manager (GCM) and git-credential-oauth generate
OAuth credentials [4][5]. Following RFC 6749, the application prints a
link for the user to authorize access in browser. A loopback redirect
communicates the response including access token to the application.

For security, RFC 6749 recommends that OAuth response also includes
expiry date and refresh token [6]. After expiry, applications can use
the refresh token to generate a new access token without user
reauthorization in browser. GitLab and BitBucket set the expiry at two
hours [2][3]. (GitHub doesn't populate expiry or refresh token.)

However the Git credential protocol has no attribute to store the OAuth
refresh token (unrecognised attributes are silently discarded). This
means that the user has to regularly reauthorize the helper in browser.
On a browserless system, this is particularly intrusive, requiring a
second device.

Introduce a new attribute oauth_refresh_token. This is especially
useful when a storage helper and a read-only OAuth helper are configured
together. Recall that `credential fill` calls each helper until it has a
non-expired password.

```
[credential]
	helper = storage  # eg. cache or osxkeychain
	helper = oauth
```

The OAuth helper can use the stored refresh token forwarded by
`credential fill` to generate a fresh access token without opening the
browser. See
https://github.com/hickford/git-credential-oauth/pull/3/files
for an implementation tested with this patch.

Add support for the new attribute to credential-cache. Eventually, I
hope to see support in other popular storage helpers.

Alternatives considered: ask helpers to store all unrecognised
attributes. This seems excessively complex for no obvious gain.
Helpers would also need extra information to distinguish between
confidential and non-confidential attributes.

Workarounds: GCM abuses the helper get/store/erase contract to store the
refresh token during credential *get* as the password for a fictitious
host [7] (I wrote this hack). This workaround is only feasible for a
monolithic helper with its own storage.

[1] https://github.blog/2012-09-21-easier-builds-and-deployments-using-git-over-https-and-oauth/
[2] https://docs.gitlab.com/ee/api/oauth2.html#access-git-over-https-with-access-token
[3] https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/#Cloning-a-repository-with-an-access-token
[4] https://github.com/GitCredentialManager/git-credential-manager
[5] https://github.com/hickford/git-credential-oauth
[6] https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
[7] https://github.com/GitCredentialManager/git-credential-manager/blob/66b94e489ad8cc1982836355493e369770b30211/src/shared/GitLab/GitLabHostProvider.cs#L207

Signed-off-by: M Hickford <mirth.hickford@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
  • Loading branch information
hickford authored and gitster committed Apr 21, 2023
1 parent 73876f4 commit a5c7656
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Documentation/git-credential.txt
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ Git understands the following attributes:
When reading credentials from helpers, `git credential fill` ignores expired
passwords. Represented as Unix time UTC, seconds since 1970.

`oauth_refresh_token`::

An OAuth refresh token may accompany a password that is an OAuth access
token. Helpers must treat this attribute as confidential like the password
attribute. Git itself has no special behaviour for this attribute.

`url`::

When this special attribute is read by `git credential`, the
Expand Down
3 changes: 3 additions & 0 deletions builtin/credential-cache--daemon.c
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ static void serve_one_client(FILE *in, FILE *out)
if (e->item.password_expiry_utc != TIME_MAX)
fprintf(out, "password_expiry_utc=%"PRItime"\n",
e->item.password_expiry_utc);
if (e->item.oauth_refresh_token)
fprintf(out, "oauth_refresh_token=%s\n",
e->item.oauth_refresh_token);
}
}
else if (!strcmp(action.buf, "exit")) {
Expand Down
6 changes: 6 additions & 0 deletions credential.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ void credential_clear(struct credential *c)
free(c->path);
free(c->username);
free(c->password);
free(c->oauth_refresh_token);
string_list_clear(&c->helpers, 0);

credential_init(c);
Expand Down Expand Up @@ -240,6 +241,9 @@ int credential_read(struct credential *c, FILE *fp)
c->password_expiry_utc = parse_timestamp(value, NULL, 10);
if (c->password_expiry_utc == 0 || errno == ERANGE)
c->password_expiry_utc = TIME_MAX;
} else if (!strcmp(key, "oauth_refresh_token")) {
free(c->oauth_refresh_token);
c->oauth_refresh_token = xstrdup(value);
} else if (!strcmp(key, "url")) {
credential_from_url(c, value);
} else if (!strcmp(key, "quit")) {
Expand Down Expand Up @@ -275,6 +279,7 @@ void credential_write(const struct credential *c, FILE *fp)
credential_write_item(fp, "path", c->path, 0);
credential_write_item(fp, "username", c->username, 0);
credential_write_item(fp, "password", c->password, 0);
credential_write_item(fp, "oauth_refresh_token", c->oauth_refresh_token, 0);
if (c->password_expiry_utc != TIME_MAX) {
char *s = xstrfmt("%"PRItime, c->password_expiry_utc);
credential_write_item(fp, "password_expiry_utc", s, 0);
Expand Down Expand Up @@ -398,6 +403,7 @@ void credential_reject(struct credential *c)

FREE_AND_NULL(c->username);
FREE_AND_NULL(c->password);
FREE_AND_NULL(c->oauth_refresh_token);
c->password_expiry_utc = TIME_MAX;
c->approved = 0;
}
Expand Down
1 change: 1 addition & 0 deletions credential.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ struct credential {
char *protocol;
char *host;
char *path;
char *oauth_refresh_token;
timestamp_t password_expiry_utc;
};

Expand Down
30 changes: 30 additions & 0 deletions t/lib-credential.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ helper_test_clean() {
reject $1 https example.com store-user
reject $1 https example.com user1
reject $1 https example.com user2
reject $1 https example.com user4
reject $1 http path.tld user
reject $1 https timeout.tld user
reject $1 https sso.tld
Expand Down Expand Up @@ -298,6 +299,35 @@ helper_test_timeout() {
'
}

helper_test_oauth_refresh_token() {
HELPER=$1

test_expect_success "helper ($HELPER) stores oauth_refresh_token" '
check approve $HELPER <<-\EOF
protocol=https
host=example.com
username=user4
password=pass
oauth_refresh_token=xyzzy
EOF
'

test_expect_success "helper ($HELPER) gets oauth_refresh_token" '
check fill $HELPER <<-\EOF
protocol=https
host=example.com
username=user4
--
protocol=https
host=example.com
username=user4
password=pass
oauth_refresh_token=xyzzy
--
EOF
'
}

write_script askpass <<\EOF
echo >&2 askpass: $*
what=$(echo $1 | cut -d" " -f1 | tr A-Z a-z | tr -cd a-z)
Expand Down
18 changes: 18 additions & 0 deletions t/t0300-credentials.sh
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,24 @@ test_expect_success 'credential_approve stores password expiry' '
EOF
'

test_expect_success 'credential_approve stores oauth refresh token' '
check approve useless <<-\EOF
protocol=http
host=example.com
username=foo
password=bar
oauth_refresh_token=xyzzy
--
--
useless: store
useless: protocol=http
useless: host=example.com
useless: username=foo
useless: password=bar
useless: oauth_refresh_token=xyzzy
EOF
'

test_expect_success 'do not bother storing password-less credential' '
check approve useless <<-\EOF
protocol=http
Expand Down
1 change: 1 addition & 0 deletions t/t0301-credential-cache.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ test_atexit 'git credential-cache exit'

# test that the daemon works with no special setup
helper_test cache
helper_test_oauth_refresh_token cache

test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '
test_when_finished "
Expand Down

0 comments on commit a5c7656

Please sign in to comment.