Skip to content

Commit a7259d2

Browse files
hpsinCopilot
andcommitted
Unify refresh token opt-in
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent b997d28 commit a7259d2

9 files changed

Lines changed: 63 additions & 46 deletions

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ compatible** — existing applications continue to receive non-expiring tokens w
1010

1111
- `Flow.RequestRefreshToken` opts an authorization into expiring tokens by requesting the
1212
`offline_access` scope, in both Device flow and Web application flow. Also available as
13-
`device.WithOfflineAccess()` and `webapp.BrowserParams.RequestRefreshToken` for callers using
14-
those packages directly.
13+
`device.WithRefreshToken()` and `webapp.WithRefreshToken()` for callers using those packages
14+
directly.
1515
- `api.AccessToken` now records `ExpiresIn`, `ExpiresAt`, `RefreshTokenExpiresIn`, and
1616
`RefreshTokenExpiresAt`, with `IsExpired()` and `CanRefresh()` helpers.
1717
- `api.Refresh` exchanges a refresh token for a new token. A rejected refresh token is reported as

device/device_flow.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,9 @@ func WithAudience(audience string) AuthRequestEditorFn {
6666
}
6767
}
6868

69-
// WithOfflineAccess requests the "offline_access" scope, opting this authorization into receiving an
70-
// expiring access token and a refresh token. Servers that do not support expiring tokens ignore it
71-
// and issue a non-expiring token with no refresh token.
72-
func WithOfflineAccess() AuthRequestEditorFn {
69+
// WithRefreshToken requests an expiring access token and a refresh token. Servers that do not
70+
// support expiring tokens ignore this and issue a non-expiring token with no refresh token.
71+
func WithRefreshToken() AuthRequestEditorFn {
7372
return func(values *url.Values) {
7473
scopes := strings.Fields(values.Get("scope"))
7574
values.Set("scope", strings.Join(api.AppendOfflineAccess(scopes), " "))

device/examples_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ func ExampleRequestCode() {
3737
fmt.Printf("Access token: %s\n", accessToken.Token)
3838
}
3939

40-
// Request an expiring access token and a refresh token by adding the "offline_access" scope.
41-
// Servers that do not support expiring tokens ignore it and return a non-expiring token instead.
42-
func ExampleWithOfflineAccess() {
40+
// Request an expiring access token and a refresh token.
41+
// Servers that do not support expiring tokens ignore the request and return a non-expiring token instead.
42+
func ExampleWithRefreshToken() {
4343
clientID := os.Getenv("OAUTH_CLIENT_ID")
4444
scopes := []string{"repo", "read:org"}
4545
httpClient := http.DefaultClient
4646

4747
code, err := device.RequestCode(httpClient, "https://github.com/login/device/code",
48-
clientID, scopes, device.WithOfflineAccess())
48+
clientID, scopes, device.WithRefreshToken())
4949
if err != nil {
5050
panic(err)
5151
}

device/offline_access_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"testing"
66
)
77

8-
func TestWithOfflineAccess(t *testing.T) {
8+
func TestWithRefreshToken(t *testing.T) {
99
tests := []struct {
1010
name string
1111
scope string
@@ -19,15 +19,15 @@ func TestWithOfflineAccess(t *testing.T) {
1919
for _, tt := range tests {
2020
t.Run(tt.name, func(t *testing.T) {
2121
values := url.Values{"scope": {tt.scope}}
22-
WithOfflineAccess()(&values)
22+
WithRefreshToken()(&values)
2323
if got := values.Get("scope"); got != tt.want {
2424
t.Errorf("scope = %q, want %q", got, tt.want)
2525
}
2626
})
2727
}
2828
}
2929

30-
func TestRequestCode_withOfflineAccess(t *testing.T) {
30+
func TestRequestCode_withRefreshToken(t *testing.T) {
3131
client := &apiClient{
3232
stubs: []apiStub{
3333
{
@@ -39,7 +39,7 @@ func TestRequestCode_withOfflineAccess(t *testing.T) {
3939
}
4040

4141
if _, err := RequestCode(client, "https://example.com/device/code", "CLIENTID",
42-
[]string{"repo"}, WithOfflineAccess()); err != nil {
42+
[]string{"repo"}, WithRefreshToken()); err != nil {
4343
t.Fatalf("unexpected error: %v", err)
4444
}
4545

oauth_device.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ func (oa *Flow) DeviceFlow() (*api.AccessToken, error) {
3939
host = parsedHost
4040
}
4141

42-
scopes := oa.Scopes
42+
requestOptions := []device.AuthRequestEditorFn{device.WithAudience(oa.Audience)}
4343
if oa.RequestRefreshToken {
44-
scopes = api.AppendOfflineAccess(scopes)
44+
requestOptions = append(requestOptions, device.WithRefreshToken())
4545
}
4646

4747
code, err := device.RequestCode(httpClient, host.DeviceCodeURL,
48-
oa.ClientID, scopes, device.WithAudience(oa.Audience))
48+
oa.ClientID, oa.Scopes, requestOptions...)
4949
if err != nil {
5050
return nil, err
5151
}

oauth_webapp.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,13 @@ func (oa *Flow) WebAppFlow() (*api.AccessToken, error) {
3434
Scopes: oa.Scopes,
3535
Audience: oa.Audience,
3636
AllowSignup: true,
37-
38-
RequestRefreshToken: oa.RequestRefreshToken,
3937
}
40-
browserURL, err := flow.BrowserURL(host.AuthorizeURL, params)
38+
browserOptions := []webapp.BrowserURLOption{}
39+
if oa.RequestRefreshToken {
40+
browserOptions = append(browserOptions, webapp.WithRefreshToken())
41+
}
42+
43+
browserURL, err := flow.BrowserURL(host.AuthorizeURL, params, browserOptions...)
4144
if err != nil {
4245
return nil, err
4346
}

webapp/examples_test.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,10 @@ func ExampleInitFlow() {
2626
RedirectURI: callbackURL,
2727
Scopes: []string{"repo", "read:org"},
2828
AllowSignup: true,
29-
30-
// Opt in to receiving an expiring access token and a refresh token. Servers without support
31-
// for expiring tokens ignore this and return a non-expiring token with no refresh token.
32-
RequestRefreshToken: true,
3329
}
34-
browserURL, err := flow.BrowserURL("https://github.com/login/oauth/authorize", params)
30+
// WithRefreshToken requests an expiring access token and a refresh token. Servers without
31+
// support for expiring tokens ignore it and return a non-expiring token with no refresh token.
32+
browserURL, err := flow.BrowserURL("https://github.com/login/oauth/authorize", params, webapp.WithRefreshToken())
3533
if err != nil {
3634
panic(err)
3735
}

webapp/offline_access_test.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"testing"
66
)
77

8-
func TestFlow_BrowserURL_offlineAccess(t *testing.T) {
8+
func TestFlow_BrowserURL_withRefreshToken(t *testing.T) {
99
tests := []struct {
1010
name string
1111
scopes []string
@@ -43,12 +43,15 @@ func TestFlow_BrowserURL_offlineAccess(t *testing.T) {
4343
t.Fatalf("InitFlow: %v", err)
4444
}
4545

46+
options := []BrowserURLOption{}
47+
if tt.requestRefreshToken {
48+
options = append(options, WithRefreshToken())
49+
}
4650
browserURL, err := flow.BrowserURL("https://github.com/login/oauth/authorize", BrowserParams{
47-
ClientID: "CLIENTID",
48-
RedirectURI: "http://127.0.0.1/callback",
49-
Scopes: tt.scopes,
50-
RequestRefreshToken: tt.requestRefreshToken,
51-
})
51+
ClientID: "CLIENTID",
52+
RedirectURI: "http://127.0.0.1/callback",
53+
Scopes: tt.scopes,
54+
}, options...)
5255
if err != nil {
5356
t.Fatalf("BrowserURL: %v", err)
5457
}
@@ -72,11 +75,10 @@ func TestFlow_BrowserURL_doesNotMutateCallerScopes(t *testing.T) {
7275

7376
scopes := []string{"repo"}
7477
if _, err := flow.BrowserURL("https://github.com/login/oauth/authorize", BrowserParams{
75-
ClientID: "CLIENTID",
76-
RedirectURI: "http://127.0.0.1/callback",
77-
Scopes: scopes,
78-
RequestRefreshToken: true,
79-
}); err != nil {
78+
ClientID: "CLIENTID",
79+
RedirectURI: "http://127.0.0.1/callback",
80+
Scopes: scopes,
81+
}, WithRefreshToken()); err != nil {
8082
t.Fatalf("BrowserURL: %v", err)
8183
}
8284

webapp/webapp_flow.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,32 +50,47 @@ type BrowserParams struct {
5050
Audience string
5151
LoginHandle string
5252
AllowSignup bool
53-
// RequestRefreshToken opts this authorization into receiving an expiring access token along with
54-
// a refresh token, by requesting the "offline_access" scope. Servers that do not support
55-
// expiring tokens ignore it and issue a non-expiring token with no refresh token.
56-
RequestRefreshToken bool
53+
}
54+
55+
// BrowserURLOption configures an authorization request.
56+
type BrowserURLOption func(*browserURLOptions)
57+
58+
type browserURLOptions struct {
59+
requestRefreshToken bool
60+
}
61+
62+
// WithRefreshToken requests an expiring access token and a refresh token. Servers that do not
63+
// support expiring tokens ignore this and issue a non-expiring token with no refresh token.
64+
func WithRefreshToken() BrowserURLOption {
65+
return func(options *browserURLOptions) {
66+
options.requestRefreshToken = true
67+
}
5768
}
5869

5970
// BrowserURL appends GET query parameters to baseURL and returns the url that the user should
6071
// navigate to in their web browser.
61-
func (flow *Flow) BrowserURL(baseURL string, params BrowserParams) (string, error) {
72+
func (flow *Flow) BrowserURL(baseURL string, params BrowserParams, options ...BrowserURLOption) (string, error) {
6273
ru, err := url.Parse(params.RedirectURI)
6374
if err != nil {
6475
return "", err
6576
}
6677

78+
requestOptions := browserURLOptions{}
79+
for _, option := range options {
80+
option(&requestOptions)
81+
}
82+
6783
ru.Host = fmt.Sprintf("%s:%d", ru.Hostname(), flow.server.Port())
6884
flow.server.CallbackPath = ru.Path
6985
flow.clientID = params.ClientID
7086

71-
scopes := params.Scopes
72-
if params.RequestRefreshToken {
73-
scopes = api.AppendOfflineAccess(scopes)
74-
}
75-
7687
q := url.Values{}
7788
q.Set("client_id", params.ClientID)
7889
q.Set("redirect_uri", ru.String())
90+
scopes := params.Scopes
91+
if requestOptions.requestRefreshToken {
92+
scopes = api.AppendOfflineAccess(scopes)
93+
}
7994
q.Set("scope", strings.Join(scopes, " "))
8095
q.Set("state", flow.state)
8196

0 commit comments

Comments
 (0)