{"thread":{"id":"59395","subject":"[PATCH] credential: new attribute oauth_refresh_token","startedAt":"2023-03-14T06:46:14Z","lastAt":"2023-05-02T16:20:14Z","messageCount":8,"participants":["M Hickford via GitGitGadget","Junio C Hamano","M Hickford","Jeff King","Felipe Contreras"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"473491","messageId":"pull.1394.git.1678776364753.gitgitgadget@gmail.com","threadId":"59395","inReplyTo":null,"subject":"[PATCH] credential: new attribute oauth_refresh_token","fromName":"M Hickford via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2023-03-14T06:46:04Z","receivedAt":"2023-03-14T06:46:14Z","isPatch":true,"sender":{"key":"mirth.hickford@gmail.com","avatar":"https://avatars.githubusercontent.com/u/105314?v=4"},"body":"From: M Hickford <mirth.hickford@gmail.com>\n\nGit authentication with OAuth access token is supported by every popular\nGit host including GitHub, GitLab and BitBucket [1][2][3]. Credential\nhelpers Git Credential Manager (GCM) and git-credential-oauth generate\nOAuth credentials [4][5]. Following RFC 6749, the application prints a\nlink for the user to authorize access in browser. A loopback redirect\ncommunicates the response including access token to the application.\n\nFor security, RFC 6749 recommends that OAuth response also includes\nexpiry date and refresh token [6]. After expiry, applications can use\nthe refresh token to generate a new access token without user\nreauthorization in browser. GitLab and BitBucket set the expiry at two\nhours [2][3]. (GitHub doesn't populate expiry or refresh token.)\n\nHowever the Git credential protocol has no attribute to store the OAuth\nrefresh token (unrecognised attributes are silently discarded). This\nmeans that the user has to regularly reauthorize the helper in browser.\nOn a browserless system, this is particularly intrusive, requiring a\nsecond device.\n\nIntroduce a new attribute oauth_refresh_token. This is especially\nuseful when a storage helper and a read-only OAuth helper are configured\ntogether. Recall that `credential fill` calls each helper until it has a\nnon-expired password.\n\n```\n[credential]\n\thelper = storage  # eg. cache or osxkeychain\n\thelper = oauth\n```\n\nThe OAuth helper can use the stored refresh token forwarded by\n`credential fill` to generate a fresh access token without opening the\nbrowser. See\nhttps://github.com/hickford/git-credential-oauth/pull/3/files\nfor an implementation tested with this patch.\n\nAdd support for the new attribute to credential-cache. Eventually, I\nhope to see support in other popular storage helpers.\n\nAlternatives considered: ask helpers to store all unrecognised\nattributes. This seems excessively complex for no obvious gain.\nHelpers would also need extra information to distinguish between\nconfidential and non-confidential attributes.\n\nWorkarounds: GCM abuses the helper get/store/erase contract to store the\nrefresh token during credential *get* as the password for a fictitious\nhost [7] (I wrote this hack). This workaround is only feasible for a\nmonolithic helper with its own storage.\n\n[1] https://github.blog/2012-09-21-easier-builds-and-deployments-using-git-over-https-and-oauth/\n[2] https://docs.gitlab.com/ee/api/oauth2.html#access-git-over-https-with-access-token\n[3] https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/#Cloning-a-repository-with-an-access-token\n[4] https://github.com/GitCredentialManager/git-credential-manager\n[5] https://github.com/hickford/git-credential-oauth\n[6] https://datatracker.ietf.org/doc/html/rfc6749#section-5.1\n[7] https://github.com/GitCredentialManager/git-credential-manager/blob/66b94e489ad8cc1982836355493e369770b30211/src/shared/GitLab/GitLabHostProvider.cs#L207\n\nSigned-off-by: M Hickford <mirth.hickford@gmail.com>\n---\n    credential: new attribute oauth_refresh_token\n    \n    A minimal extension to the credential protocol to communicate OAuth\n    refresh token. This is worthwhile because OAuth for Git authentication\n    is ubiquitous. Current workaround is only feasible for a monolithic\n    credential helper, discouraging interoperability between helpers.\n    \n    Details in commit message.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1394%2Fhickford%2Fcredential-refresh-token-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1394/hickford/credential-refresh-token-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/1394\n\n Documentation/git-credential.txt   |  6 ++++++\n builtin/credential-cache--daemon.c |  3 +++\n credential.c                       |  6 ++++++\n credential.h                       |  1 +\n t/t0300-credentials.sh             | 18 ++++++++++++++++++\n 5 files changed, 34 insertions(+)\n\ndiff --git a/Documentation/git-credential.txt b/Documentation/git-credential.txt\nindex 29d184ab824..3677d68ce61 100644\n--- a/Documentation/git-credential.txt\n+++ b/Documentation/git-credential.txt\n@@ -150,6 +150,12 @@ Git understands the following attributes:\n \tWhen reading credentials from helpers, `git credential fill` ignores expired\n \tpasswords. Represented as Unix time UTC, seconds since 1970.\n \n+`oauth_refresh_token`::\n+\n+\tAn OAuth refresh token may accompany a password that is an OAuth access\n+\ttoken. Helpers must treat this attribute as confidential like the password\n+\tattribute. Git itself has no special behaviour for this attribute.\n+\n `url`::\n \n \tWhen this special attribute is read by `git credential`, the\ndiff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c\nindex 338058be7f9..f253a8278dc 100644\n--- a/builtin/credential-cache--daemon.c\n+++ b/builtin/credential-cache--daemon.c\n@@ -130,6 +130,9 @@ static void serve_one_client(FILE *in, FILE *out)\n \t\t\tif (e->item.password_expiry_utc != TIME_MAX)\n \t\t\t\tfprintf(out, \"password_expiry_utc=%\"PRItime\"\\n\",\n \t\t\t\t\te->item.password_expiry_utc);\n+\t\t\tif (e->item.oauth_refresh_token)\n+\t\t\t\tfprintf(out, \"oauth_refresh_token=%s\\n\",\n+\t\t\t\t\te->item.oauth_refresh_token);\n \t\t}\n \t}\n \telse if (!strcmp(action.buf, \"exit\")) {\ndiff --git a/credential.c b/credential.c\nindex f32011343f9..3b5d3d058cf 100644\n--- a/credential.c\n+++ b/credential.c\n@@ -22,6 +22,7 @@ void credential_clear(struct credential *c)\n \tfree(c->path);\n \tfree(c->username);\n \tfree(c->password);\n+\tfree(c->oauth_refresh_token);\n \tstring_list_clear(&c->helpers, 0);\n \n \tcredential_init(c);\n@@ -240,6 +241,9 @@ int credential_read(struct credential *c, FILE *fp)\n \t\t\tc->password_expiry_utc = parse_timestamp(value, NULL, 10);\n \t\t\tif (c->password_expiry_utc == 0 || errno == ERANGE)\n \t\t\t\tc->password_expiry_utc = TIME_MAX;\n+\t\t} else if (!strcmp(key, \"oauth_refresh_token\")) {\n+\t\t\tfree(c->oauth_refresh_token);\n+\t\t\tc->oauth_refresh_token = xstrdup(value);\n \t\t} else if (!strcmp(key, \"url\")) {\n \t\t\tcredential_from_url(c, value);\n \t\t} else if (!strcmp(key, \"quit\")) {\n@@ -275,6 +279,7 @@ void credential_write(const struct credential *c, FILE *fp)\n \tcredential_write_item(fp, \"path\", c->path, 0);\n \tcredential_write_item(fp, \"username\", c->username, 0);\n \tcredential_write_item(fp, \"password\", c->password, 0);\n+\tcredential_write_item(fp, \"oauth_refresh_token\", c->oauth_refresh_token, 0);\n \tif (c->password_expiry_utc != TIME_MAX) {\n \t\tchar *s = xstrfmt(\"%\"PRItime, c->password_expiry_utc);\n \t\tcredential_write_item(fp, \"password_expiry_utc\", s, 0);\n@@ -398,6 +403,7 @@ void credential_reject(struct credential *c)\n \n \tFREE_AND_NULL(c->username);\n \tFREE_AND_NULL(c->password);\n+\tFREE_AND_NULL(c->oauth_refresh_token);\n \tc->password_expiry_utc = TIME_MAX;\n \tc->approved = 0;\n }\ndiff --git a/credential.h b/credential.h\nindex 935b28a70f1..b2eda372461 100644\n--- a/credential.h\n+++ b/credential.h\n@@ -126,6 +126,7 @@ struct credential {\n \tchar *protocol;\n \tchar *host;\n \tchar *path;\n+\tchar *oauth_refresh_token;\n \ttimestamp_t password_expiry_utc;\n };\n \ndiff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\nindex c66d91e82d8..b49fc14a2bd 100755\n--- a/t/t0300-credentials.sh\n+++ b/t/t0300-credentials.sh\n@@ -214,6 +214,24 @@ test_expect_success 'credential_approve stores password expiry' '\n \tEOF\n '\n \n+test_expect_success 'credential_approve stores oauth refresh token' '\n+\tcheck approve useless <<-\\EOF\n+\tprotocol=http\n+\thost=example.com\n+\tusername=foo\n+\tpassword=bar\n+\toauth_refresh_token=xyzzy\n+\t--\n+\t--\n+\tuseless: store\n+\tuseless: protocol=http\n+\tuseless: host=example.com\n+\tuseless: username=foo\n+\tuseless: password=bar\n+\tuseless: oauth_refresh_token=xyzzy\n+\tEOF\n+'\n+\n test_expect_success 'do not bother storing password-less credential' '\n \tcheck approve useless <<-\\EOF\n \tprotocol=http\n\nbase-commit: 73876f4861cd3d187a4682290ab75c9dccadbc56\n-- \ngitgitgadget\n"},{"id":"475783","messageId":"pull.1394.v2.git.1682070479816.gitgitgadget@gmail.com","threadId":"59395","inReplyTo":"pull.1394.git.1678776364753.gitgitgadget@gmail.com","subject":"[PATCH v2] credential: new attribute oauth_refresh_token","fromName":"M Hickford via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2023-04-21T09:47:59Z","receivedAt":"2023-04-21T09:49:04Z","isPatch":true,"sender":{"key":"mirth.hickford@gmail.com","avatar":"https://avatars.githubusercontent.com/u/105314?v=4"},"body":"From: M Hickford <mirth.hickford@gmail.com>\n\nGit authentication with OAuth access token is supported by every popular\nGit host including GitHub, GitLab and BitBucket [1][2][3]. Credential\nhelpers Git Credential Manager (GCM) and git-credential-oauth generate\nOAuth credentials [4][5]. Following RFC 6749, the application prints a\nlink for the user to authorize access in browser. A loopback redirect\ncommunicates the response including access token to the application.\n\nFor security, RFC 6749 recommends that OAuth response also includes\nexpiry date and refresh token [6]. After expiry, applications can use\nthe refresh token to generate a new access token without user\nreauthorization in browser. GitLab and BitBucket set the expiry at two\nhours [2][3]. (GitHub doesn't populate expiry or refresh token.)\n\nHowever the Git credential protocol has no attribute to store the OAuth\nrefresh token (unrecognised attributes are silently discarded). This\nmeans that the user has to regularly reauthorize the helper in browser.\nOn a browserless system, this is particularly intrusive, requiring a\nsecond device.\n\nIntroduce a new attribute oauth_refresh_token. This is especially\nuseful when a storage helper and a read-only OAuth helper are configured\ntogether. Recall that `credential fill` calls each helper until it has a\nnon-expired password.\n\n```\n[credential]\n\thelper = storage  # eg. cache or osxkeychain\n\thelper = oauth\n```\n\nThe OAuth helper can use the stored refresh token forwarded by\n`credential fill` to generate a fresh access token without opening the\nbrowser. See\nhttps://github.com/hickford/git-credential-oauth/pull/3/files\nfor an implementation tested with this patch.\n\nAdd support for the new attribute to credential-cache. Eventually, I\nhope to see support in other popular storage helpers.\n\nAlternatives considered: ask helpers to store all unrecognised\nattributes. This seems excessively complex for no obvious gain.\nHelpers would also need extra information to distinguish between\nconfidential and non-confidential attributes.\n\nWorkarounds: GCM abuses the helper get/store/erase contract to store the\nrefresh token during credential *get* as the password for a fictitious\nhost [7] (I wrote this hack). This workaround is only feasible for a\nmonolithic helper with its own storage.\n\n[1] https://github.blog/2012-09-21-easier-builds-and-deployments-using-git-over-https-and-oauth/\n[2] https://docs.gitlab.com/ee/api/oauth2.html#access-git-over-https-with-access-token\n[3] https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/#Cloning-a-repository-with-an-access-token\n[4] https://github.com/GitCredentialManager/git-credential-manager\n[5] https://github.com/hickford/git-credential-oauth\n[6] https://datatracker.ietf.org/doc/html/rfc6749#section-5.1\n[7] https://github.com/GitCredentialManager/git-credential-manager/blob/66b94e489ad8cc1982836355493e369770b30211/src/shared/GitLab/GitLabHostProvider.cs#L207\n\nSigned-off-by: M Hickford <mirth.hickford@gmail.com>\n---\n    credential: new attribute oauth_refresh_token\n    \n    A minimal extension to the credential protocol to communicate OAuth\n    refresh token. This is worthwhile because OAuth for Git authentication\n    is ubiquitous. Current workaround is only feasible for a monolithic\n    credential helper, discouraging interoperability between helpers.\n    \n    Patch v2 adds an additional test\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1394%2Fhickford%2Fcredential-refresh-token-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1394/hickford/credential-refresh-token-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/1394\n\nRange-diff vs v1:\n\n 1:  299a26919f0 ! 1:  79e77a4426b credential: new attribute oauth_refresh_token\n     @@ credential.c: void credential_clear(struct credential *c)\n       \tfree(c->password);\n      +\tfree(c->oauth_refresh_token);\n       \tstring_list_clear(&c->helpers, 0);\n     + \tstrvec_clear(&c->wwwauth_headers);\n       \n     - \tcredential_init(c);\n      @@ credential.c: int credential_read(struct credential *c, FILE *fp)\n       \t\t\tc->password_expiry_utc = parse_timestamp(value, NULL, 10);\n       \t\t\tif (c->password_expiry_utc == 0 || errno == ERANGE)\n     @@ credential.h: struct credential {\n       };\n       \n      \n     + ## t/lib-credential.sh ##\n     +@@ t/lib-credential.sh: helper_test_clean() {\n     + \treject $1 https example.com store-user\n     + \treject $1 https example.com user1\n     + \treject $1 https example.com user2\n     ++\treject $1 https example.com user4\n     + \treject $1 http path.tld user\n     + \treject $1 https timeout.tld user\n     + \treject $1 https sso.tld\n     +@@ t/lib-credential.sh: helper_test_timeout() {\n     + \t'\n     + }\n     + \n     ++helper_test_oauth_refresh_token() {\n     ++\tHELPER=$1\n     ++\n     ++\ttest_expect_success \"helper ($HELPER) stores oauth_refresh_token\" '\n     ++\t\tcheck approve $HELPER <<-\\EOF\n     ++\t\tprotocol=https\n     ++\t\thost=example.com\n     ++\t\tusername=user4\n     ++\t\tpassword=pass\n     ++\t\toauth_refresh_token=xyzzy\n     ++\t\tEOF\n     ++\t'\n     ++\n     ++\ttest_expect_success \"helper ($HELPER) gets oauth_refresh_token\" '\n     ++\t\tcheck fill $HELPER <<-\\EOF\n     ++\t\tprotocol=https\n     ++\t\thost=example.com\n     ++\t\tusername=user4\n     ++\t\t--\n     ++\t\tprotocol=https\n     ++\t\thost=example.com\n     ++\t\tusername=user4\n     ++\t\tpassword=pass\n     ++\t\toauth_refresh_token=xyzzy\n     ++\t\t--\n     ++\t\tEOF\n     ++\t'\n     ++}\n     ++\n     + write_script askpass <<\\EOF\n     + echo >&2 askpass: $*\n     + what=$(echo $1 | cut -d\" \" -f1 | tr A-Z a-z | tr -cd a-z)\n     +\n       ## t/t0300-credentials.sh ##\n      @@ t/t0300-credentials.sh: test_expect_success 'credential_approve stores password expiry' '\n       \tEOF\n     @@ t/t0300-credentials.sh: test_expect_success 'credential_approve stores password\n       test_expect_success 'do not bother storing password-less credential' '\n       \tcheck approve useless <<-\\EOF\n       \tprotocol=http\n     +\n     + ## t/t0301-credential-cache.sh ##\n     +@@ t/t0301-credential-cache.sh: test_atexit 'git credential-cache exit'\n     + \n     + # test that the daemon works with no special setup\n     + helper_test cache\n     ++helper_test_oauth_refresh_token cache\n     + \n     + test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\n     + \ttest_when_finished \"\n\n\n Documentation/git-credential.txt   |  6 ++++++\n builtin/credential-cache--daemon.c |  3 +++\n credential.c                       |  6 ++++++\n credential.h                       |  1 +\n t/lib-credential.sh                | 30 ++++++++++++++++++++++++++++++\n t/t0300-credentials.sh             | 18 ++++++++++++++++++\n t/t0301-credential-cache.sh        |  1 +\n 7 files changed, 65 insertions(+)\n\ndiff --git a/Documentation/git-credential.txt b/Documentation/git-credential.txt\nindex 3394c036113..0e6d9e85ec7 100644\n--- a/Documentation/git-credential.txt\n+++ b/Documentation/git-credential.txt\n@@ -156,6 +156,12 @@ Git understands the following attributes:\n \tWhen reading credentials from helpers, `git credential fill` ignores expired\n \tpasswords. Represented as Unix time UTC, seconds since 1970.\n \n+`oauth_refresh_token`::\n+\n+\tAn OAuth refresh token may accompany a password that is an OAuth access\n+\ttoken. Helpers must treat this attribute as confidential like the password\n+\tattribute. Git itself has no special behaviour for this attribute.\n+\n `url`::\n \n \tWhen this special attribute is read by `git credential`, the\ndiff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c\nindex 62c09a271d6..9db5f00184d 100644\n--- a/builtin/credential-cache--daemon.c\n+++ b/builtin/credential-cache--daemon.c\n@@ -133,6 +133,9 @@ static void serve_one_client(FILE *in, FILE *out)\n \t\t\tif (e->item.password_expiry_utc != TIME_MAX)\n \t\t\t\tfprintf(out, \"password_expiry_utc=%\"PRItime\"\\n\",\n \t\t\t\t\te->item.password_expiry_utc);\n+\t\t\tif (e->item.oauth_refresh_token)\n+\t\t\t\tfprintf(out, \"oauth_refresh_token=%s\\n\",\n+\t\t\t\t\te->item.oauth_refresh_token);\n \t\t}\n \t}\n \telse if (!strcmp(action.buf, \"exit\")) {\ndiff --git a/credential.c b/credential.c\nindex e6417bf8804..84e32971a92 100644\n--- a/credential.c\n+++ b/credential.c\n@@ -24,6 +24,7 @@ void credential_clear(struct credential *c)\n \tfree(c->path);\n \tfree(c->username);\n \tfree(c->password);\n+\tfree(c->oauth_refresh_token);\n \tstring_list_clear(&c->helpers, 0);\n \tstrvec_clear(&c->wwwauth_headers);\n \n@@ -243,6 +244,9 @@ int credential_read(struct credential *c, FILE *fp)\n \t\t\tc->password_expiry_utc = parse_timestamp(value, NULL, 10);\n \t\t\tif (c->password_expiry_utc == 0 || errno == ERANGE)\n \t\t\t\tc->password_expiry_utc = TIME_MAX;\n+\t\t} else if (!strcmp(key, \"oauth_refresh_token\")) {\n+\t\t\tfree(c->oauth_refresh_token);\n+\t\t\tc->oauth_refresh_token = xstrdup(value);\n \t\t} else if (!strcmp(key, \"url\")) {\n \t\t\tcredential_from_url(c, value);\n \t\t} else if (!strcmp(key, \"quit\")) {\n@@ -278,6 +282,7 @@ void credential_write(const struct credential *c, FILE *fp)\n \tcredential_write_item(fp, \"path\", c->path, 0);\n \tcredential_write_item(fp, \"username\", c->username, 0);\n \tcredential_write_item(fp, \"password\", c->password, 0);\n+\tcredential_write_item(fp, \"oauth_refresh_token\", c->oauth_refresh_token, 0);\n \tif (c->password_expiry_utc != TIME_MAX) {\n \t\tchar *s = xstrfmt(\"%\"PRItime, c->password_expiry_utc);\n \t\tcredential_write_item(fp, \"password_expiry_utc\", s, 0);\n@@ -403,6 +408,7 @@ void credential_reject(struct credential *c)\n \n \tFREE_AND_NULL(c->username);\n \tFREE_AND_NULL(c->password);\n+\tFREE_AND_NULL(c->oauth_refresh_token);\n \tc->password_expiry_utc = TIME_MAX;\n \tc->approved = 0;\n }\ndiff --git a/credential.h b/credential.h\nindex 2b5958cd431..b8e2936d1dc 100644\n--- a/credential.h\n+++ b/credential.h\n@@ -141,6 +141,7 @@ struct credential {\n \tchar *protocol;\n \tchar *host;\n \tchar *path;\n+\tchar *oauth_refresh_token;\n \ttimestamp_t password_expiry_utc;\n };\n \ndiff --git a/t/lib-credential.sh b/t/lib-credential.sh\nindex 5ea8bc9f1dc..33666406d92 100644\n--- a/t/lib-credential.sh\n+++ b/t/lib-credential.sh\n@@ -43,6 +43,7 @@ helper_test_clean() {\n \treject $1 https example.com store-user\n \treject $1 https example.com user1\n \treject $1 https example.com user2\n+\treject $1 https example.com user4\n \treject $1 http path.tld user\n \treject $1 https timeout.tld user\n \treject $1 https sso.tld\n@@ -298,6 +299,35 @@ helper_test_timeout() {\n \t'\n }\n \n+helper_test_oauth_refresh_token() {\n+\tHELPER=$1\n+\n+\ttest_expect_success \"helper ($HELPER) stores oauth_refresh_token\" '\n+\t\tcheck approve $HELPER <<-\\EOF\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user4\n+\t\tpassword=pass\n+\t\toauth_refresh_token=xyzzy\n+\t\tEOF\n+\t'\n+\n+\ttest_expect_success \"helper ($HELPER) gets oauth_refresh_token\" '\n+\t\tcheck fill $HELPER <<-\\EOF\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user4\n+\t\t--\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user4\n+\t\tpassword=pass\n+\t\toauth_refresh_token=xyzzy\n+\t\t--\n+\t\tEOF\n+\t'\n+}\n+\n write_script askpass <<\\EOF\n echo >&2 askpass: $*\n what=$(echo $1 | cut -d\" \" -f1 | tr A-Z a-z | tr -cd a-z)\ndiff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\nindex c66d91e82d8..b49fc14a2bd 100755\n--- a/t/t0300-credentials.sh\n+++ b/t/t0300-credentials.sh\n@@ -214,6 +214,24 @@ test_expect_success 'credential_approve stores password expiry' '\n \tEOF\n '\n \n+test_expect_success 'credential_approve stores oauth refresh token' '\n+\tcheck approve useless <<-\\EOF\n+\tprotocol=http\n+\thost=example.com\n+\tusername=foo\n+\tpassword=bar\n+\toauth_refresh_token=xyzzy\n+\t--\n+\t--\n+\tuseless: store\n+\tuseless: protocol=http\n+\tuseless: host=example.com\n+\tuseless: username=foo\n+\tuseless: password=bar\n+\tuseless: oauth_refresh_token=xyzzy\n+\tEOF\n+'\n+\n test_expect_success 'do not bother storing password-less credential' '\n \tcheck approve useless <<-\\EOF\n \tprotocol=http\ndiff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh\nindex 698b7159f03..c02a3b5969c 100755\n--- a/t/t0301-credential-cache.sh\n+++ b/t/t0301-credential-cache.sh\n@@ -29,6 +29,7 @@ test_atexit 'git credential-cache exit'\n \n # test that the daemon works with no special setup\n helper_test cache\n+helper_test_oauth_refresh_token cache\n \n test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\n \ttest_when_finished \"\n\nbase-commit: 667fcf4e15379790f0b609d6a83d578e69f20301\n-- \ngitgitgadget\n"},{"id":"475793","messageId":"xmqqildpkwnb.fsf@gitster.g","threadId":"59395","inReplyTo":"pull.1394.v2.git.1682070479816.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] credential: new attribute oauth_refresh_token","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-04-21T16:50:00Z","receivedAt":"2023-04-21T16:50:05Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"M Hickford via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>     Patch v2 adds an additional test\n\nWhat we have below shows some differences outside the test, but the\nrange-diff I see locally does show that the change since v1 is\npurely to the tests.\n\n>  Documentation/git-credential.txt   |  6 ++++++\n>  builtin/credential-cache--daemon.c |  3 +++\n>  credential.c                       |  6 ++++++\n>  credential.h                       |  1 +\n>  t/lib-credential.sh                | 30 ++++++++++++++++++++++++++++++\n>  t/t0300-credentials.sh             | 18 ++++++++++++++++++\n>  t/t0301-credential-cache.sh        |  1 +\n>  7 files changed, 65 insertions(+)\n\nWill replace what I kept in 'seen'.\n\nThe original unfortunately did not see anybody interested enough in\nthe topic to review and comment.  Let's hope this time is different\n;-)\n\nTHanks.\n"},{"id":"475969","messageId":"CAGJzqsnAj-yHHY9-79WSF7gCNN-x6scrFkXMxC6xQQMHWeeuKw@mail.gmail.com","threadId":"59395","inReplyTo":"xmqqildpkwnb.fsf@gitster.g","subject":"Re: [PATCH v2] credential: new attribute oauth_refresh_token","fromName":"M Hickford","fromEmail":"mirth.hickford@gmail.com","sentAt":"2023-04-24T20:11:47Z","receivedAt":"2023-04-24T20:12:40Z","isPatch":true,"sender":{"key":"mirth.hickford@gmail.com","avatar":"https://avatars.githubusercontent.com/u/105314?v=4"},"body":"On Fri, 21 Apr 2023 at 17:50, Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"M Hickford via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> >     Patch v2 adds an additional test\n>\n> What we have below shows some differences outside the test, but the\n> range-diff I see locally does show that the change since v1 is\n> purely to the tests.\n\nThank Junio for your reply. Yes I also rebased.\n\n>\n> >  Documentation/git-credential.txt   |  6 ++++++\n> >  builtin/credential-cache--daemon.c |  3 +++\n> >  credential.c                       |  6 ++++++\n> >  credential.h                       |  1 +\n> >  t/lib-credential.sh                | 30 ++++++++++++++++++++++++++++++\n> >  t/t0300-credentials.sh             | 18 ++++++++++++++++++\n> >  t/t0301-credential-cache.sh        |  1 +\n> >  7 files changed, 65 insertions(+)\n>\n> Will replace what I kept in 'seen'.\n>\n> The original unfortunately did not see anybody interested enough in\n> the topic to review and comment.  Let's hope this time is different\n> ;-)\n>\n> THanks.\n"},{"id":"476015","messageId":"20230425064758.GD4061254@coredump.intra.peff.net","threadId":"59395","inReplyTo":"pull.1394.v2.git.1682070479816.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] credential: new attribute oauth_refresh_token","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2023-04-25T06:47:58Z","receivedAt":"2023-04-25T06:48:24Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Fri, Apr 21, 2023 at 09:47:59AM +0000, M Hickford via GitGitGadget wrote:\n\n> Git authentication with OAuth access token is supported by every popular\n> Git host including GitHub, GitLab and BitBucket [1][2][3]. Credential\n> helpers Git Credential Manager (GCM) and git-credential-oauth generate\n> OAuth credentials [4][5]. Following RFC 6749, the application prints a\n> link for the user to authorize access in browser. A loopback redirect\n> communicates the response including access token to the application.\n> \n> For security, RFC 6749 recommends that OAuth response also includes\n> expiry date and refresh token [6]. After expiry, applications can use\n> the refresh token to generate a new access token without user\n> reauthorization in browser. GitLab and BitBucket set the expiry at two\n> hours [2][3]. (GitHub doesn't populate expiry or refresh token.)\n> \n> However the Git credential protocol has no attribute to store the OAuth\n> refresh token (unrecognised attributes are silently discarded). This\n> means that the user has to regularly reauthorize the helper in browser.\n> On a browserless system, this is particularly intrusive, requiring a\n> second device.\n> \n> Introduce a new attribute oauth_refresh_token. This is especially\n> useful when a storage helper and a read-only OAuth helper are configured\n> together. Recall that `credential fill` calls each helper until it has a\n> non-expired password.\n> \n> ```\n> [credential]\n> \thelper = storage  # eg. cache or osxkeychain\n> \thelper = oauth\n> ```\n\nOK. I don't have much knowledge of OAuth, but taking the notion of \"this\nis a useful thing for oauth clients to store\" as a given, the\nimplementation seems reasonable.\n\nIt does feel a bit funny, in that nothing _except_ credential-cache is\ngoing to understand or store this thing. In general, I'd expect most\nhelpers doing anything as complex as oauth to just implement their own\nstorage layer. But I get that your goal here is trying to keep things as\ncomposable as possible. I just wonder how successful it will be if there\nis only one helper that you can actually use for storage.\n\n> Add support for the new attribute to credential-cache. Eventually, I\n> hope to see support in other popular storage helpers.\n\nAt least your optimism is documented. :)\n\n> Alternatives considered: ask helpers to store all unrecognised\n> attributes. This seems excessively complex for no obvious gain.\n> Helpers would also need extra information to distinguish between\n> confidential and non-confidential attributes.\n\nI've written the \"store arbitrary attributes\" patch before, and it\nactually is quite simple (if you neglect the \"confidential\" concept,\nwhich is largely what the current protocol and helpers do). But I don't\nknow that it buys that much anyway, if helpers need to decide whether\nand how to store these items anyway. We get support in credential-cache\nfor \"free\" because it happens to store the same in-memory struct that\nGit itself does, but most other helpers would need special patches. And\nbecause they're often interfacing with system storage, they can't\nnecessarily just store arbitrary items.\n\n> diff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c\n> index 62c09a271d6..9db5f00184d 100644\n> --- a/builtin/credential-cache--daemon.c\n> +++ b/builtin/credential-cache--daemon.c\n> @@ -133,6 +133,9 @@ static void serve_one_client(FILE *in, FILE *out)\n>  \t\t\tif (e->item.password_expiry_utc != TIME_MAX)\n>  \t\t\t\tfprintf(out, \"password_expiry_utc=%\"PRItime\"\\n\",\n>  \t\t\t\t\te->item.password_expiry_utc);\n> +\t\t\tif (e->item.oauth_refresh_token)\n> +\t\t\t\tfprintf(out, \"oauth_refresh_token=%s\\n\",\n> +\t\t\t\t\te->item.oauth_refresh_token);\n>  \t\t}\n>  \t}\n>  \telse if (!strcmp(action.buf, \"exit\")) {\n\nThis hunk makes me wonder if the cache daemon should just be using\ncredential_write(), which would then support this automatically (along\nwith any other fields, like the expiry, or the new wwwauth stuff).\n\nIt would mean that it spits back parts of the query (like host, etc) for\na \"get\", rather than just username/password. That's not wrong, but is\nmaybe a little weird.\n\nI can live with it as-is, certainly.\n\n> +helper_test_oauth_refresh_token() {\n> +\tHELPER=$1\n> +\n> +\ttest_expect_success \"helper ($HELPER) stores oauth_refresh_token\" '\n> +\t\tcheck approve $HELPER <<-\\EOF\n> +\t\tprotocol=https\n> +\t\thost=example.com\n> +\t\tusername=user4\n> +\t\tpassword=pass\n> +\t\toauth_refresh_token=xyzzy\n> +\t\tEOF\n> +\t'\n> +\n> +\ttest_expect_success \"helper ($HELPER) gets oauth_refresh_token\" '\n> +\t\tcheck fill $HELPER <<-\\EOF\n> +\t\tprotocol=https\n> +\t\thost=example.com\n> +\t\tusername=user4\n> +\t\t--\n> +\t\tprotocol=https\n> +\t\thost=example.com\n> +\t\tusername=user4\n> +\t\tpassword=pass\n> +\t\toauth_refresh_token=xyzzy\n> +\t\t--\n> +\t\tEOF\n> +\t'\n> +}\n\nThis is in a separate function, so normal t0303 tests won't run it (and\nconfuse users when they fail because they don't support this field).\nGood.\n\nPossibly t0303 should learn to optionally trigger this the way it does\nwith helper_test_timeout. But I'm also content to leave it until\nsomebody has a helper they want to test with it.\n\n> diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\n> index c66d91e82d8..b49fc14a2bd 100755\n> --- a/t/t0300-credentials.sh\n> +++ b/t/t0300-credentials.sh\n> @@ -214,6 +214,24 @@ test_expect_success 'credential_approve stores password expiry' '\n>  \tEOF\n>  '\n>  \n> +test_expect_success 'credential_approve stores oauth refresh token' '\n> +\tcheck approve useless <<-\\EOF\n> +\tprotocol=http\n> +\thost=example.com\n> +\tusername=foo\n> +\tpassword=bar\n> +\toauth_refresh_token=xyzzy\n> +\t--\n> +\t--\n> +\tuseless: store\n> +\tuseless: protocol=http\n> +\tuseless: host=example.com\n> +\tuseless: username=foo\n> +\tuseless: password=bar\n> +\tuseless: oauth_refresh_token=xyzzy\n> +\tEOF\n> +'\n\nMakes sense. This file is just checking how Git passes the data around\nbetween helpers.\n\n> diff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh\n> index 698b7159f03..c02a3b5969c 100755\n> --- a/t/t0301-credential-cache.sh\n> +++ b/t/t0301-credential-cache.sh\n> @@ -29,6 +29,7 @@ test_atexit 'git credential-cache exit'\n>  \n>  # test that the daemon works with no special setup\n>  helper_test cache\n> +helper_test_oauth_refresh_token cache\n>  \n>  test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\n>  \ttest_when_finished \"\n\nAnd this one is just testing credential-cache, which we know supports\nthe feature. Good.\n\nSo the patch looks like it correctly implements the intent. I don't feel\nstrongly about what the patch is trying to do, but it's not a problem\narea I know a lot about or use myself. So if it's doing something useful\nfor you, that sounds reasonable to me. :)\n\n-Peff\n"},{"id":"476065","messageId":"CAGJzqsmAESKYd5AYoZ17pg0F9Oe2sK_Odwpk4niQK9njtpCkTg@mail.gmail.com","threadId":"59395","inReplyTo":"20230425064758.GD4061254@coredump.intra.peff.net","subject":"Re: [PATCH v2] credential: new attribute oauth_refresh_token","fromName":"M Hickford","fromEmail":"mirth.hickford@gmail.com","sentAt":"2023-04-25T19:19:40Z","receivedAt":"2023-04-25T19:20:27Z","isPatch":true,"sender":{"key":"mirth.hickford@gmail.com","avatar":"https://avatars.githubusercontent.com/u/105314?v=4"},"body":"On Tue, 25 Apr 2023 at 07:48, Jeff King <peff@peff.net> wrote:\n>\n> On Fri, Apr 21, 2023 at 09:47:59AM +0000, M Hickford via GitGitGadget wrote:\n>\n> > Git authentication with OAuth access token is supported by every popular\n> > Git host including GitHub, GitLab and BitBucket [1][2][3]. Credential\n> > helpers Git Credential Manager (GCM) and git-credential-oauth generate\n> > OAuth credentials [4][5]. Following RFC 6749, the application prints a\n> > link for the user to authorize access in browser. A loopback redirect\n> > communicates the response including access token to the application.\n> >\n> > For security, RFC 6749 recommends that OAuth response also includes\n> > expiry date and refresh token [6]. After expiry, applications can use\n> > the refresh token to generate a new access token without user\n> > reauthorization in browser. GitLab and BitBucket set the expiry at two\n> > hours [2][3]. (GitHub doesn't populate expiry or refresh token.)\n> >\n> > However the Git credential protocol has no attribute to store the OAuth\n> > refresh token (unrecognised attributes are silently discarded). This\n> > means that the user has to regularly reauthorize the helper in browser.\n> > On a browserless system, this is particularly intrusive, requiring a\n> > second device.\n> >\n> > Introduce a new attribute oauth_refresh_token. This is especially\n> > useful when a storage helper and a read-only OAuth helper are configured\n> > together. Recall that `credential fill` calls each helper until it has a\n> > non-expired password.\n> >\n> > ```\n> > [credential]\n> >       helper = storage  # eg. cache or osxkeychain\n> >       helper = oauth\n> > ```\n>\n> OK. I don't have much knowledge of OAuth, but taking the notion of \"this\n> is a useful thing for oauth clients to store\" as a given, the\n> implementation seems reasonable.\n>\n> It does feel a bit funny, in that nothing _except_ credential-cache is\n> going to understand or store this thing. In general, I'd expect most\n> helpers doing anything as complex as oauth to just implement their own\n> storage layer. But I get that your goal here is trying to keep things as\n> composable as possible. I just wonder how successful it will be if there\n> is only one helper that you can actually use for storage.\n>\n> > Add support for the new attribute to credential-cache. Eventually, I\n> > hope to see support in other popular storage helpers.\n>\n> At least your optimism is documented. :)\n\nI have a draft patch for credential-libsecret\nhttps://github.com/gitgitgadget/git/pull/1524 which I could add to\nthis patch series if you like. Helpers credential-wincred and\ncredential-osxkeychain would be easy to update following the same\napproach.\n\n>\n> > Alternatives considered: ask helpers to store all unrecognised\n> > attributes. This seems excessively complex for no obvious gain.\n> > Helpers would also need extra information to distinguish between\n> > confidential and non-confidential attributes.\n>\n> I've written the \"store arbitrary attributes\" patch before, and it\n> actually is quite simple (if you neglect the \"confidential\" concept,\n> which is largely what the current protocol and helpers do). But I don't\n> know that it buys that much anyway, if helpers need to decide whether\n> and how to store these items anyway. We get support in credential-cache\n> for \"free\" because it happens to store the same in-memory struct that\n> Git itself does, but most other helpers would need special patches. And\n> because they're often interfacing with system storage, they can't\n> necessarily just store arbitrary items.\n>\n> > diff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c\n> > index 62c09a271d6..9db5f00184d 100644\n> > --- a/builtin/credential-cache--daemon.c\n> > +++ b/builtin/credential-cache--daemon.c\n> > @@ -133,6 +133,9 @@ static void serve_one_client(FILE *in, FILE *out)\n> >                       if (e->item.password_expiry_utc != TIME_MAX)\n> >                               fprintf(out, \"password_expiry_utc=%\"PRItime\"\\n\",\n> >                                       e->item.password_expiry_utc);\n> > +                     if (e->item.oauth_refresh_token)\n> > +                             fprintf(out, \"oauth_refresh_token=%s\\n\",\n> > +                                     e->item.oauth_refresh_token);\n> >               }\n> >       }\n> >       else if (!strcmp(action.buf, \"exit\")) {\n>\n> This hunk makes me wonder if the cache daemon should just be using\n> credential_write(), which would then support this automatically (along\n> with any other fields, like the expiry, or the new wwwauth stuff).\n>\n> It would mean that it spits back parts of the query (like host, etc) for\n> a \"get\", rather than just username/password. That's not wrong, but is\n> maybe a little weird.\n>\n> I can live with it as-is, certainly.\n>\n> > +helper_test_oauth_refresh_token() {\n> > +     HELPER=$1\n> > +\n> > +     test_expect_success \"helper ($HELPER) stores oauth_refresh_token\" '\n> > +             check approve $HELPER <<-\\EOF\n> > +             protocol=https\n> > +             host=example.com\n> > +             username=user4\n> > +             password=pass\n> > +             oauth_refresh_token=xyzzy\n> > +             EOF\n> > +     '\n> > +\n> > +     test_expect_success \"helper ($HELPER) gets oauth_refresh_token\" '\n> > +             check fill $HELPER <<-\\EOF\n> > +             protocol=https\n> > +             host=example.com\n> > +             username=user4\n> > +             --\n> > +             protocol=https\n> > +             host=example.com\n> > +             username=user4\n> > +             password=pass\n> > +             oauth_refresh_token=xyzzy\n> > +             --\n> > +             EOF\n> > +     '\n> > +}\n>\n> This is in a separate function, so normal t0303 tests won't run it (and\n> confuse users when they fail because they don't support this field).\n> Good.\n>\n> Possibly t0303 should learn to optionally trigger this the way it does\n> with helper_test_timeout. But I'm also content to leave it until\n> somebody has a helper they want to test with it.\n>\n> > diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\n> > index c66d91e82d8..b49fc14a2bd 100755\n> > --- a/t/t0300-credentials.sh\n> > +++ b/t/t0300-credentials.sh\n> > @@ -214,6 +214,24 @@ test_expect_success 'credential_approve stores password expiry' '\n> >       EOF\n> >  '\n> >\n> > +test_expect_success 'credential_approve stores oauth refresh token' '\n> > +     check approve useless <<-\\EOF\n> > +     protocol=http\n> > +     host=example.com\n> > +     username=foo\n> > +     password=bar\n> > +     oauth_refresh_token=xyzzy\n> > +     --\n> > +     --\n> > +     useless: store\n> > +     useless: protocol=http\n> > +     useless: host=example.com\n> > +     useless: username=foo\n> > +     useless: password=bar\n> > +     useless: oauth_refresh_token=xyzzy\n> > +     EOF\n> > +'\n>\n> Makes sense. This file is just checking how Git passes the data around\n> between helpers.\n>\n> > diff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh\n> > index 698b7159f03..c02a3b5969c 100755\n> > --- a/t/t0301-credential-cache.sh\n> > +++ b/t/t0301-credential-cache.sh\n> > @@ -29,6 +29,7 @@ test_atexit 'git credential-cache exit'\n> >\n> >  # test that the daemon works with no special setup\n> >  helper_test cache\n> > +helper_test_oauth_refresh_token cache\n> >\n> >  test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\n> >       test_when_finished \"\n>\n> And this one is just testing credential-cache, which we know supports\n> the feature. Good.\n>\n> So the patch looks like it correctly implements the intent. I don't feel\n> strongly about what the patch is trying to do, but it's not a problem\n> area I know a lot about or use myself. So if it's doing something useful\n> for you, that sounds reasonable to me. :)\n\nThanks Jeff for the review\n\n>\n> -Peff\n"},{"id":"476416","messageId":"20230502102512.GB1180695@coredump.intra.peff.net","threadId":"59395","inReplyTo":"CAGJzqsmAESKYd5AYoZ17pg0F9Oe2sK_Odwpk4niQK9njtpCkTg@mail.gmail.com","subject":"Re: [PATCH v2] credential: new attribute oauth_refresh_token","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2023-05-02T10:25:12Z","receivedAt":"2023-05-02T10:28:10Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Apr 25, 2023 at 08:19:40PM +0100, M Hickford wrote:\n\n> > > Add support for the new attribute to credential-cache. Eventually, I\n> > > hope to see support in other popular storage helpers.\n> >\n> > At least your optimism is documented. :)\n> \n> I have a draft patch for credential-libsecret\n> https://github.com/gitgitgadget/git/pull/1524 which I could add to\n> this patch series if you like. Helpers credential-wincred and\n> credential-osxkeychain would be easy to update following the same\n> approach.\n\nGood, thanks for doing that work. I don't think it needs to be part of\nthis series. Knowing that it's being worked on gives a sense that this\npatch is going in a useful direction.\n\nI do find the \"shove extra data into the password field\" strategy a\nlittle gross, but that may be the best that can be done when interfacing\nwith existing stores. And at least the details are all contained inside\nthe helper itself (though I guess a user looking at it with other wallet\nmanagement tools would see the weird field).\n\n-Peff\n"},{"id":"476432","messageId":"6451382f7aacf_1ba2d29490@chronos.notmuch","threadId":"59395","inReplyTo":"20230425064758.GD4061254@coredump.intra.peff.net","subject":"Re: [PATCH v2] credential: new attribute oauth_refresh_token","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2023-05-02T16:19:59Z","receivedAt":"2023-05-02T16:20:14Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Jeff King wrote:\n> On Fri, Apr 21, 2023 at 09:47:59AM +0000, M Hickford via GitGitGadget wrote:\n> \n> > Git authentication with OAuth access token is supported by every popular\n> > Git host including GitHub, GitLab and BitBucket [1][2][3]. Credential\n> > helpers Git Credential Manager (GCM) and git-credential-oauth generate\n> > OAuth credentials [4][5]. Following RFC 6749, the application prints a\n> > link for the user to authorize access in browser. A loopback redirect\n> > communicates the response including access token to the application.\n> > \n> > For security, RFC 6749 recommends that OAuth response also includes\n> > expiry date and refresh token [6]. After expiry, applications can use\n> > the refresh token to generate a new access token without user\n> > reauthorization in browser. GitLab and BitBucket set the expiry at two\n> > hours [2][3]. (GitHub doesn't populate expiry or refresh token.)\n> > \n> > However the Git credential protocol has no attribute to store the OAuth\n> > refresh token (unrecognised attributes are silently discarded). This\n> > means that the user has to regularly reauthorize the helper in browser.\n> > On a browserless system, this is particularly intrusive, requiring a\n> > second device.\n> > \n> > Introduce a new attribute oauth_refresh_token. This is especially\n> > useful when a storage helper and a read-only OAuth helper are configured\n> > together. Recall that `credential fill` calls each helper until it has a\n> > non-expired password.\n> > \n> > ```\n> > [credential]\n> > \thelper = storage  # eg. cache or osxkeychain\n> > \thelper = oauth\n> > ```\n> \n> OK. I don't have much knowledge of OAuth, but taking the notion of \"this\n> is a useful thing for oauth clients to store\" as a given, the\n> implementation seems reasonable.\n\nI don't think this is specific to OAuth, I've seen different authorization\nmethods use something like that.\n\nIn general you just need two variables: the refresh token, and the expiration\ntime of the refresh token. The logic is very simple: if the refresh token has\nexpired, you ask for a new one. This way you don't have to go through the\nauthorization process again.\n\n-- \nFelipe Contreras\n"}]}