Skip to content

Commit 89ad870

Browse files
committed
auth refresh: preserve existing scopes when requesting new ones
When there was a previously valid token that was granted some scopes, ensure all those scopes will be re-requested when doing the authentication flow for the new token.
1 parent 64a19ee commit 89ad870

File tree

3 files changed

+69
-13
lines changed

3 files changed

+69
-13
lines changed

pkg/cmd/auth/refresh/refresh.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package refresh
33
import (
44
"errors"
55
"fmt"
6+
"net/http"
7+
"strings"
68

79
"github.com/AlecAivazis/survey/v2"
810
"github.com/MakeNowJust/heredoc"
@@ -16,8 +18,9 @@ import (
1618
)
1719

1820
type RefreshOptions struct {
19-
IO *iostreams.IOStreams
20-
Config func() (config.Config, error)
21+
IO *iostreams.IOStreams
22+
Config func() (config.Config, error)
23+
httpClient *http.Client
2124

2225
MainExecutable string
2326

@@ -36,6 +39,7 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
3639
_, err := authflow.AuthFlowWithConfig(cfg, io, hostname, "", scopes)
3740
return err
3841
},
42+
httpClient: http.DefaultClient,
3943
}
4044

4145
cmd := &cobra.Command{
@@ -128,6 +132,16 @@ func refreshRun(opts *RefreshOptions) error {
128132
}
129133

130134
var additionalScopes []string
135+
if oldToken, _ := cfg.Get(hostname, "oauth_token"); oldToken != "" {
136+
if oldScopes, err := shared.GetScopes(opts.httpClient, hostname, oldToken); err == nil {
137+
for _, s := range strings.Split(oldScopes, ",") {
138+
s = strings.TrimSpace(s)
139+
if s != "" {
140+
additionalScopes = append(additionalScopes, s)
141+
}
142+
}
143+
}
144+
}
131145

132146
credentialFlow := &shared.GitCredentialFlow{
133147
Executable: opts.MainExecutable,

pkg/cmd/auth/refresh/refresh_test.go

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package refresh
22

33
import (
44
"bytes"
5+
"io/ioutil"
6+
"net/http"
7+
"strings"
58
"testing"
69

710
"github.com/cli/cli/v2/internal/config"
@@ -134,6 +137,7 @@ func Test_refreshRun(t *testing.T) {
134137
opts *RefreshOptions
135138
askStubs func(*prompt.AskStubber)
136139
cfgHosts []string
140+
oldScopes string
137141
wantErr string
138142
nontty bool
139143
wantAuthArgs authArgs
@@ -211,6 +215,20 @@ func Test_refreshRun(t *testing.T) {
211215
scopes: []string{"repo:invite", "public_key:read"},
212216
},
213217
},
218+
{
219+
name: "scopes provided",
220+
cfgHosts: []string{
221+
"github.com",
222+
},
223+
oldScopes: "delete_repo, codespace",
224+
opts: &RefreshOptions{
225+
Scopes: []string{"repo:invite", "public_key:read"},
226+
},
227+
wantAuthArgs: authArgs{
228+
hostname: "github.com",
229+
scopes: []string{"repo:invite", "public_key:read", "delete_repo", "codespace"},
230+
},
231+
},
214232
}
215233
for _, tt := range tests {
216234
t.Run(tt.name, func(t *testing.T) {
@@ -234,10 +252,26 @@ func Test_refreshRun(t *testing.T) {
234252
for _, hostname := range tt.cfgHosts {
235253
_ = cfg.Set(hostname, "oauth_token", "abc123")
236254
}
237-
reg := &httpmock.Registry{}
238-
reg.Register(
239-
httpmock.GraphQL(`query UserCurrent\b`),
240-
httpmock.StringResponse(`{"data":{"viewer":{"login":"cybilb"}}}`))
255+
256+
httpReg := &httpmock.Registry{}
257+
httpReg.Register(
258+
httpmock.REST("GET", ""),
259+
func(req *http.Request) (*http.Response, error) {
260+
statusCode := 200
261+
if req.Header.Get("Authorization") != "token abc123" {
262+
statusCode = 400
263+
}
264+
return &http.Response{
265+
Request: req,
266+
StatusCode: statusCode,
267+
Body: ioutil.NopCloser(strings.NewReader(``)),
268+
Header: http.Header{
269+
"X-Oauth-Scopes": {tt.oldScopes},
270+
},
271+
}, nil
272+
},
273+
)
274+
tt.opts.httpClient = &http.Client{Transport: httpReg}
241275

242276
mainBuf := bytes.Buffer{}
243277
hostsBuf := bytes.Buffer{}
@@ -258,8 +292,8 @@ func Test_refreshRun(t *testing.T) {
258292
assert.NoError(t, err)
259293
}
260294

261-
assert.Equal(t, aa.hostname, tt.wantAuthArgs.hostname)
262-
assert.Equal(t, aa.scopes, tt.wantAuthArgs.scopes)
295+
assert.Equal(t, tt.wantAuthArgs.hostname, aa.hostname)
296+
assert.Equal(t, tt.wantAuthArgs.scopes, aa.scopes)
263297
})
264298
}
265299
}

pkg/cmd/auth/shared/oauth_scopes.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,19 @@ type httpClient interface {
3232
Do(*http.Request) (*http.Response, error)
3333
}
3434

35-
func HasMinimumScopes(httpClient httpClient, hostname, authToken string) error {
35+
func GetScopes(httpClient httpClient, hostname, authToken string) (string, error) {
3636
apiEndpoint := ghinstance.RESTPrefix(hostname)
3737

3838
req, err := http.NewRequest("GET", apiEndpoint, nil)
3939
if err != nil {
40-
return err
40+
return "", err
4141
}
4242

4343
req.Header.Set("Authorization", "token "+authToken)
4444

4545
res, err := httpClient.Do(req)
4646
if err != nil {
47-
return err
47+
return "", err
4848
}
4949

5050
defer func() {
@@ -55,10 +55,18 @@ func HasMinimumScopes(httpClient httpClient, hostname, authToken string) error {
5555
}()
5656

5757
if res.StatusCode != 200 {
58-
return api.HandleHTTPError(res)
58+
return "", api.HandleHTTPError(res)
59+
}
60+
61+
return res.Header.Get("X-Oauth-Scopes"), nil
62+
}
63+
64+
func HasMinimumScopes(httpClient httpClient, hostname, authToken string) error {
65+
scopesHeader, err := GetScopes(httpClient, hostname, authToken)
66+
if err != nil {
67+
return err
5968
}
6069

61-
scopesHeader := res.Header.Get("X-Oauth-Scopes")
6270
if scopesHeader == "" {
6371
// if the token reports no scopes, assume that it's an integration token and give up on
6472
// detecting its capabilities

0 commit comments

Comments
 (0)