git/t/t0301-credential-cache.sh
M Hickford a5c76569e7 credential: new attribute oauth_refresh_token
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] 66b94e489a/src/shared/GitLab/GitLabHostProvider.cs (L207)

Signed-off-by: M Hickford <mirth.hickford@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2023-04-21 09:38:30 -07:00

128 lines
3.2 KiB
Bash
Executable file

#!/bin/sh
test_description='credential-cache tests'
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-credential.sh
test -z "$NO_UNIX_SOCKETS" || {
skip_all='skipping credential-cache tests, unix sockets not available'
test_done
}
uname_s=$(uname -s)
case $uname_s in
*MINGW*)
test_path_is_socket () {
# `test -S` cannot detect Win10's Unix sockets
test_path_exists "$1"
}
;;
*)
test_path_is_socket () {
test -S "$1"
}
;;
esac
# don't leave a stale daemon running
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 "
git credential-cache exit &&
rmdir -p .cache/git/credential/
" &&
test_path_is_missing "$HOME/.git-credential-cache" &&
test_path_is_socket "$HOME/.cache/git/credential/socket"
'
XDG_CACHE_HOME="$HOME/xdg"
export XDG_CACHE_HOME
# test behavior when XDG_CACHE_HOME is set
helper_test cache
test_expect_success "use custom XDG_CACHE_HOME if set and default sockets are not created" '
test_when_finished "git credential-cache exit" &&
test_path_is_socket "$XDG_CACHE_HOME/git/credential/socket" &&
test_path_is_missing "$HOME/.git-credential-cache/socket" &&
test_path_is_missing "$HOME/.cache/git/credential/socket"
'
unset XDG_CACHE_HOME
test_expect_success 'credential-cache --socket option overrides default location' '
test_when_finished "
git credential-cache exit --socket \"\$HOME/dir/socket\" &&
rmdir \"\$HOME/dir\"
" &&
check approve "cache --socket \"\$HOME/dir/socket\"" <<-\EOF &&
protocol=https
host=example.com
username=store-user
password=store-pass
EOF
test_path_is_socket "$HOME/dir/socket"
'
test_expect_success "use custom XDG_CACHE_HOME even if xdg socket exists" '
test_when_finished "
git credential-cache exit &&
sane_unset XDG_CACHE_HOME
" &&
check approve cache <<-\EOF &&
protocol=https
host=example.com
username=store-user
password=store-pass
EOF
test_path_is_socket "$HOME/.cache/git/credential/socket" &&
XDG_CACHE_HOME="$HOME/xdg" &&
export XDG_CACHE_HOME &&
check approve cache <<-\EOF &&
protocol=https
host=example.com
username=store-user
password=store-pass
EOF
test_path_is_socket "$XDG_CACHE_HOME/git/credential/socket"
'
test_expect_success 'use user socket if user directory exists' '
test_when_finished "
git credential-cache exit &&
rmdir \"\$HOME/.git-credential-cache/\"
" &&
mkdir -p "$HOME/.git-credential-cache/" &&
chmod 700 "$HOME/.git-credential-cache/" &&
check approve cache <<-\EOF &&
protocol=https
host=example.com
username=store-user
password=store-pass
EOF
test_path_is_socket "$HOME/.git-credential-cache/socket"
'
test_expect_success SYMLINKS 'use user socket if user directory is a symlink to a directory' '
test_when_finished "
git credential-cache exit &&
rmdir \"\$HOME/dir/\" &&
rm \"\$HOME/.git-credential-cache\"
" &&
mkdir -p -m 700 "$HOME/dir/" &&
ln -s "$HOME/dir" "$HOME/.git-credential-cache" &&
check approve cache <<-\EOF &&
protocol=https
host=example.com
username=store-user
password=store-pass
EOF
test_path_is_socket "$HOME/.git-credential-cache/socket"
'
helper_test_timeout cache --timeout=1
test_done