From: "M Hickford via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: "Eric Sunshine" <sunshine@sunshineco.com>,
"Jeff King" <peff@peff.net>, Cheetham <mjcheetham@outlook.com>,
Dennington <lessleydennington@gmail.com>,
"Martin Ågren" <martin.agren@gmail.com>,
"Calvin Wan" <calvinwan@google.com>,
"M Hickford" <mirth.hickford@gmail.com>,
"M Hickford" <mirth.hickford@gmail.com>
Subject: [PATCH v4] credential: new attribute password_expiry_utc
Date: Sat, 18 Feb 2023 06:32:57 +0000 [thread overview]
Message-ID: <pull.1443.v4.git.git.1676701977347.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.1443.v3.git.git.1675545372271.gitgitgadget@gmail.com>
From: M Hickford <mirth.hickford@gmail.com>
Some passwords have an expiry date known at generation. This may be
years away for a personal access token or hours for an OAuth access
token.
When multiple credential helpers are configured, `credential fill` tries
each helper in turn until it has a username and password, returning
early. If Git authentication succeeds, `credential approve`
stores the successful credential in all helpers. If authentication
fails, `credential reject` erases matching credentials in all helpers.
Helpers implement corresponding operations: get, store, erase.
The credential protocol has no expiry attribute, so helpers cannot
store expiry information. Even if a helper returned an improvised
expiry attribute, git credential discards unrecognised attributes
between operations and between helpers.
This is a particular issue when a storage helper and a
credential-generating helper are configured together:
[credential]
helper = storage # eg. cache or osxkeychain
helper = generate # eg. oauth
`credential approve` stores the generated credential in both helpers
without expiry information. Later `credential fill` may return an
expired credential from storage. There is no workaround, no matter how
clever the second helper. The user sees authentication fail (a retry
will succeed).
Introduce a password expiry attribute. In `credential fill`, ignore
expired passwords and continue to query subsequent helpers.
In the example above, `credential fill` ignores the expired password
and a fresh credential is generated. If authentication succeeds,
`credential approve` replaces the expired password in storage.
If authentication fails, the expired credential is erased by
`credential reject`. It is unnecessary but harmless for storage
helpers to self prune expired credentials.
Add support for the new attribute to credential-cache.
Eventually, I hope to see support in other popular storage helpers.
Example usage in a credential-generating helper
https://github.com/hickford/git-credential-oauth/pull/16
Signed-off-by: M Hickford <mirth.hickford@gmail.com>
---
credential: new attribute password_expiry_utc
details in commit message
Changes in patch v4:
* Set errno = 0 to fix tests on Windows and FreeBSD (thanks Lessley
Dennington)
* Clarify rationale as discussed at review club
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1443%2Fhickford%2Fpassword-expiry-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1443/hickford/password-expiry-v4
Pull-Request: https://github.com/git/git/pull/1443
Range-diff vs v3:
1: 1846815a5c1 ! 1: ae8bddbb30a credential: new attribute password_expiry_utc
@@ Commit message
Helpers implement corresponding operations: get, store, erase.
The credential protocol has no expiry attribute, so helpers cannot
- store expiry information. (Even if a helper returned an improvised
+ store expiry information. Even if a helper returned an improvised
expiry attribute, git credential discards unrecognised attributes
- between operations and between helpers.)
+ between operations and between helpers.
- As a workaround, whenever monolithic helper Git Credential Manager (GCM)
- retrieves an OAuth credential from its storage, it makes a HTTP request
- to check whether the OAuth token has expired [1]. This complicates and
- slows the authentication happy path.
-
- Worse is the case that a storage helper and a credential-generating
- helper are configured together:
+ This is a particular issue when a storage helper and a
+ credential-generating helper are configured together:
[credential]
helper = storage # eg. cache or osxkeychain
- helper = generate # eg. oauth or manager
+ helper = generate # eg. oauth
`credential approve` stores the generated credential in both helpers
without expiry information. Later `credential fill` may return an
expired credential from storage. There is no workaround, no matter how
- clever the second helper.
+ clever the second helper. The user sees authentication fail (a retry
+ will succeed).
Introduce a password expiry attribute. In `credential fill`, ignore
expired passwords and continue to query subsequent helpers.
- In the example above, `credential fill` ignores the expired credential
+ In the example above, `credential fill` ignores the expired password
and a fresh credential is generated. If authentication succeeds,
- `credential approve` replaces the expired credential in storage.
+ `credential approve` replaces the expired password in storage.
If authentication fails, the expired credential is erased by
`credential reject`. It is unnecessary but harmless for storage
helpers to self prune expired credentials.
Add support for the new attribute to credential-cache.
- Eventually, I hope to see support in other storage helpers.
+ Eventually, I hope to see support in other popular storage helpers.
Example usage in a credential-generating helper
https://github.com/hickford/git-credential-oauth/pull/16
- [1] https://github.com/GitCredentialManager/git-credential-manager/blob/66b94e489ad8cc1982836355493e369770b30211/src/shared/GitLab/GitLabHostProvider.cs#L217
-
Signed-off-by: M Hickford <mirth.hickford@gmail.com>
## Documentation/git-credential.txt ##
@@ Documentation/gitcredentials.txt: helper::
variable, each helper will be tried in turn, and may provide a username,
password, or nothing. Once Git has acquired both a username and a
-password, no more helpers will be tried.
-+unexpired password, no more helpers will be tried.
++non-expired password, no more helpers will be tried.
+
If `credential.helper` is configured to the empty string, this resets
the helper list to empty (so you may override a helper set by a
@@ credential.c: int credential_read(struct credential *c, FILE *fp)
free(c->path);
c->path = xstrdup(value);
+ } else if (!strcmp(key, "password_expiry_utc")) {
++ errno = 0;
+ c->password_expiry_utc = parse_timestamp(value, NULL, 10);
-+ if (c->password_expiry_utc == 0 || errno)
++ if (c->password_expiry_utc == 0 || errno == ERANGE)
+ c->password_expiry_utc = TIME_MAX;
} else if (!strcmp(key, "url")) {
credential_from_url(c, value);
@@ credential.c: void credential_fill(struct credential *c)
for (i = 0; i < c->helpers.nr; i++) {
credential_do(c, c->helpers.items[i].string, "get");
+ if (c->password_expiry_utc < time(NULL)) {
++ /* Discard expired password */
+ FREE_AND_NULL(c->password);
++ /* Reset expiry to maintain consistency */
+ c->password_expiry_utc = TIME_MAX;
+ }
if (c->username && c->password)
@@ t/t0300-credentials.sh: test_expect_success 'credential_fill continues through p
+ EOF
+'
+
-+test_expect_success 'credential_fill continues through expired password' '
++test_expect_success 'credential_fill ignores expired password' '
+ check fill "verbatim-with-expiry one two 5" "verbatim three four" <<-\EOF
+ protocol=http
+ host=example.com
@@ t/t0300-credentials.sh: test_expect_success 'do not bother storing password-less
EOF
'
-+test_expect_success 'credential_approve does not store expired credential' '
++test_expect_success 'credential_approve does not store expired password' '
+ check approve useless <<-\EOF
+ protocol=http
+ host=example.com
@@ t/t0300-credentials.sh: test_expect_success 'credential_reject calls all helpers
EOF
'
-+test_expect_success 'credential_reject erases expired credential' '
++test_expect_success 'credential_reject erases credential regardless of expiry' '
+ check reject useless <<-\EOF
+ protocol=http
+ host=example.com
Documentation/git-credential.txt | 6 ++
Documentation/gitcredentials.txt | 2 +-
builtin/credential-cache--daemon.c | 3 +
credential.c | 20 ++++++-
credential.h | 2 +
t/t0300-credentials.sh | 94 ++++++++++++++++++++++++++++++
6 files changed, 125 insertions(+), 2 deletions(-)
diff --git a/Documentation/git-credential.txt b/Documentation/git-credential.txt
index ac2818b9f66..29d184ab824 100644
--- a/Documentation/git-credential.txt
+++ b/Documentation/git-credential.txt
@@ -144,6 +144,12 @@ Git understands the following attributes:
The credential's password, if we are asking it to be stored.
+`password_expiry_utc`::
+
+ Generated passwords such as an OAuth access token may have an expiry date.
+ When reading credentials from helpers, `git credential fill` ignores expired
+ passwords. Represented as Unix time UTC, seconds since 1970.
+
`url`::
When this special attribute is read by `git credential`, the
diff --git a/Documentation/gitcredentials.txt b/Documentation/gitcredentials.txt
index 4522471c337..100f045bb1a 100644
--- a/Documentation/gitcredentials.txt
+++ b/Documentation/gitcredentials.txt
@@ -167,7 +167,7 @@ helper::
If there are multiple instances of the `credential.helper` configuration
variable, each helper will be tried in turn, and may provide a username,
password, or nothing. Once Git has acquired both a username and a
-password, no more helpers will be tried.
+non-expired password, no more helpers will be tried.
+
If `credential.helper` is configured to the empty string, this resets
the helper list to empty (so you may override a helper set by a
diff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c
index f3c89831d4a..338058be7f9 100644
--- a/builtin/credential-cache--daemon.c
+++ b/builtin/credential-cache--daemon.c
@@ -127,6 +127,9 @@ static void serve_one_client(FILE *in, FILE *out)
if (e) {
fprintf(out, "username=%s\n", e->item.username);
fprintf(out, "password=%s\n", e->item.password);
+ if (e->item.password_expiry_utc != TIME_MAX)
+ fprintf(out, "password_expiry_utc=%"PRItime"\n",
+ e->item.password_expiry_utc);
}
}
else if (!strcmp(action.buf, "exit")) {
diff --git a/credential.c b/credential.c
index f6389a50684..f32011343f9 100644
--- a/credential.c
+++ b/credential.c
@@ -7,6 +7,7 @@
#include "prompt.h"
#include "sigchain.h"
#include "urlmatch.h"
+#include "git-compat-util.h"
void credential_init(struct credential *c)
{
@@ -234,6 +235,11 @@ int credential_read(struct credential *c, FILE *fp)
} else if (!strcmp(key, "path")) {
free(c->path);
c->path = xstrdup(value);
+ } else if (!strcmp(key, "password_expiry_utc")) {
+ errno = 0;
+ 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, "url")) {
credential_from_url(c, value);
} else if (!strcmp(key, "quit")) {
@@ -269,6 +275,11 @@ 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);
+ if (c->password_expiry_utc != TIME_MAX) {
+ char *s = xstrfmt("%"PRItime, c->password_expiry_utc);
+ credential_write_item(fp, "password_expiry_utc", s, 0);
+ free(s);
+ }
}
static int run_credential_helper(struct credential *c,
@@ -342,6 +353,12 @@ void credential_fill(struct credential *c)
for (i = 0; i < c->helpers.nr; i++) {
credential_do(c, c->helpers.items[i].string, "get");
+ if (c->password_expiry_utc < time(NULL)) {
+ /* Discard expired password */
+ FREE_AND_NULL(c->password);
+ /* Reset expiry to maintain consistency */
+ c->password_expiry_utc = TIME_MAX;
+ }
if (c->username && c->password)
return;
if (c->quit)
@@ -360,7 +377,7 @@ void credential_approve(struct credential *c)
if (c->approved)
return;
- if (!c->username || !c->password)
+ if (!c->username || !c->password || c->password_expiry_utc < time(NULL))
return;
credential_apply_config(c);
@@ -381,6 +398,7 @@ void credential_reject(struct credential *c)
FREE_AND_NULL(c->username);
FREE_AND_NULL(c->password);
+ c->password_expiry_utc = TIME_MAX;
c->approved = 0;
}
diff --git a/credential.h b/credential.h
index f430e77fea4..935b28a70f1 100644
--- a/credential.h
+++ b/credential.h
@@ -126,10 +126,12 @@ struct credential {
char *protocol;
char *host;
char *path;
+ timestamp_t password_expiry_utc;
};
#define CREDENTIAL_INIT { \
.helpers = STRING_LIST_INIT_DUP, \
+ .password_expiry_utc = TIME_MAX, \
}
/* Initialize a credential structure, setting all fields to empty. */
diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh
index 3485c0534e6..c66d91e82d8 100755
--- a/t/t0300-credentials.sh
+++ b/t/t0300-credentials.sh
@@ -35,6 +35,16 @@ test_expect_success 'setup helper scripts' '
test -z "$pass" || echo password=$pass
EOF
+ write_script git-credential-verbatim-with-expiry <<-\EOF &&
+ user=$1; shift
+ pass=$1; shift
+ pexpiry=$1; shift
+ . ./dump
+ test -z "$user" || echo username=$user
+ test -z "$pass" || echo password=$pass
+ test -z "$pexpiry" || echo password_expiry_utc=$pexpiry
+ EOF
+
PATH="$PWD:$PATH"
'
@@ -109,6 +119,43 @@ test_expect_success 'credential_fill continues through partial response' '
EOF
'
+test_expect_success 'credential_fill populates password_expiry_utc' '
+ check fill "verbatim-with-expiry one two 9999999999" <<-\EOF
+ protocol=http
+ host=example.com
+ --
+ protocol=http
+ host=example.com
+ username=one
+ password=two
+ password_expiry_utc=9999999999
+ --
+ verbatim-with-expiry: get
+ verbatim-with-expiry: protocol=http
+ verbatim-with-expiry: host=example.com
+ EOF
+'
+
+test_expect_success 'credential_fill ignores expired password' '
+ check fill "verbatim-with-expiry one two 5" "verbatim three four" <<-\EOF
+ protocol=http
+ host=example.com
+ --
+ protocol=http
+ host=example.com
+ username=three
+ password=four
+ --
+ verbatim-with-expiry: get
+ verbatim-with-expiry: protocol=http
+ verbatim-with-expiry: host=example.com
+ verbatim: get
+ verbatim: protocol=http
+ verbatim: host=example.com
+ verbatim: username=one
+ EOF
+'
+
test_expect_success 'credential_fill passes along metadata' '
check fill "verbatim one two" <<-\EOF
protocol=ftp
@@ -149,6 +196,24 @@ test_expect_success 'credential_approve calls all helpers' '
EOF
'
+test_expect_success 'credential_approve stores password expiry' '
+ check approve useless <<-\EOF
+ protocol=http
+ host=example.com
+ username=foo
+ password=bar
+ password_expiry_utc=9999999999
+ --
+ --
+ useless: store
+ useless: protocol=http
+ useless: host=example.com
+ useless: username=foo
+ useless: password=bar
+ useless: password_expiry_utc=9999999999
+ EOF
+'
+
test_expect_success 'do not bother storing password-less credential' '
check approve useless <<-\EOF
protocol=http
@@ -159,6 +224,17 @@ test_expect_success 'do not bother storing password-less credential' '
EOF
'
+test_expect_success 'credential_approve does not store expired password' '
+ check approve useless <<-\EOF
+ protocol=http
+ host=example.com
+ username=foo
+ password=bar
+ password_expiry_utc=5
+ --
+ --
+ EOF
+'
test_expect_success 'credential_reject calls all helpers' '
check reject useless "verbatim one two" <<-\EOF
@@ -181,6 +257,24 @@ test_expect_success 'credential_reject calls all helpers' '
EOF
'
+test_expect_success 'credential_reject erases credential regardless of expiry' '
+ check reject useless <<-\EOF
+ protocol=http
+ host=example.com
+ username=foo
+ password=bar
+ password_expiry_utc=5
+ --
+ --
+ useless: erase
+ useless: protocol=http
+ useless: host=example.com
+ useless: username=foo
+ useless: password=bar
+ useless: password_expiry_utc=5
+ EOF
+'
+
test_expect_success 'usernames can be preserved' '
check fill "verbatim \"\" three" <<-\EOF
protocol=http
base-commit: 2fc9e9ca3c7505bc60069f11e7ef09b1aeeee473
--
gitgitgadget
next prev parent reply other threads:[~2023-02-18 6:33 UTC|newest]
Thread overview: 26+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-01-28 14:04 [PATCH] credential: new attribute password_expiry_utc M Hickford via GitGitGadget
2023-01-29 20:17 ` Junio C Hamano
2023-02-01 8:29 ` M Hickford
2023-02-01 18:50 ` Junio C Hamano
2023-01-30 0:59 ` Eric Sunshine
2023-02-05 6:49 ` M Hickford
2023-02-01 9:39 ` [PATCH v2] " M Hickford via GitGitGadget
2023-02-01 12:10 ` Jeff King
2023-02-01 17:12 ` Junio C Hamano
2023-02-02 0:12 ` Jeff King
2023-02-01 20:02 ` Matthew John Cheetham
2023-02-02 0:23 ` Jeff King
2023-02-05 6:45 ` M Hickford
2023-02-06 18:59 ` Matthew John Cheetham
2023-02-05 6:34 ` M Hickford
2023-02-04 21:16 ` [PATCH v3] " M Hickford via GitGitGadget
2023-02-14 1:59 ` Junio C Hamano
2023-02-14 22:36 ` M Hickford
2023-02-17 21:44 ` Lessley Dennington
2023-02-17 21:59 ` Junio C Hamano
2023-02-18 8:00 ` M Hickford
2023-02-14 8:03 ` Martin Ågren
2023-02-16 19:16 ` Calvin Wan
2023-02-18 8:00 ` M Hickford
2023-02-18 6:32 ` M Hickford via GitGitGadget [this message]
2023-02-22 19:22 ` [PATCH v4] " Calvin Wan
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=pull.1443.v4.git.git.1676701977347.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=calvinwan@google.com \
--cc=git@vger.kernel.org \
--cc=lessleydennington@gmail.com \
--cc=martin.agren@gmail.com \
--cc=mirth.hickford@gmail.com \
--cc=mjcheetham@outlook.com \
--cc=peff@peff.net \
--cc=sunshine@sunshineco.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.