@@ -16,6 +16,7 @@ import (
16
16
"net/url"
17
17
"strconv"
18
18
"strings"
19
+ "sync"
19
20
"time"
20
21
21
22
"golang.org/x/net/context/ctxhttp"
@@ -90,102 +91,71 @@ func (e *expirationTime) UnmarshalJSON(b []byte) error {
90
91
return nil
91
92
}
92
93
93
- var brokenAuthHeaderProviders = []string {
94
- "https://accounts.google.com/" ,
95
- "https://api.codeswholesale.com/oauth/token" ,
96
- "https://api.dropbox.com/" ,
97
- "https://api.dropboxapi.com/" ,
98
- "https://api.instagram.com/" ,
99
- "https://api.netatmo.net/" ,
100
- "https://api.odnoklassniki.ru/" ,
101
- "https://api.pushbullet.com/" ,
102
- "https://api.soundcloud.com/" ,
103
- "https://api.twitch.tv/" ,
104
- "https://id.twitch.tv/" ,
105
- "https://app.box.com/" ,
106
- "https://api.box.com/" ,
107
- "https://connect.stripe.com/" ,
108
- "https://login.mailchimp.com/" ,
109
- "https://login.microsoftonline.com/" ,
110
- "https://login.salesforce.com/" ,
111
- "https://login.windows.net" ,
112
- "https://login.live.com/" ,
113
- "https://login.live-int.com/" ,
114
- "https://oauth.sandbox.trainingpeaks.com/" ,
115
- "https://oauth.trainingpeaks.com/" ,
116
- "https://oauth.vk.com/" ,
117
- "https://openapi.baidu.com/" ,
118
- "https://slack.com/" ,
119
- "https://test-sandbox.auth.corp.google.com" ,
120
- "https://test.salesforce.com/" ,
121
- "https://user.gini.net/" ,
122
- "https://www.douban.com/" ,
123
- "https://www.googleapis.com/" ,
124
- "https://www.linkedin.com/" ,
125
- "https://www.strava.com/oauth/" ,
126
- "https://www.wunderlist.com/oauth/" ,
127
- "https://api.patreon.com/" ,
128
- "https://sandbox.codeswholesale.com/oauth/token" ,
129
- "https://api.sipgate.com/v1/authorization/oauth" ,
130
- "https://api.medium.com/v1/tokens" ,
131
- "https://log.finalsurge.com/oauth/token" ,
132
- "https://multisport.todaysplan.com.au/rest/oauth/access_token" ,
133
- "https://whats.todaysplan.com.au/rest/oauth/access_token" ,
134
- "https://stackoverflow.com/oauth/access_token" ,
135
- "https://account.health.nokia.com" ,
136
- "https://accounts.zoho.com" ,
137
- "https://gitter.im/login/oauth/token" ,
138
- "https://openid-connect.onelogin.com/oidc" ,
139
- "https://api.dailymotion.com/oauth/token" ,
94
+ // RegisterBrokenAuthHeaderProvider previously did something. It is now a no-op.
95
+ //
96
+ // Deprecated: this function no longer does anything. Caller code that
97
+ // wants to avoid potential extra HTTP requests made during
98
+ // auto-probing of the provider's auth style should set
99
+ // Endpoint.AuthStyle.
100
+ func RegisterBrokenAuthHeaderProvider (tokenURL string ) {}
101
+
102
+ // AuthStyle is a copy of the golang.org/x/oauth2 package's AuthStyle type.
103
+ type AuthStyle int
104
+
105
+ const (
106
+ AuthStyleUnknown AuthStyle = 0
107
+ AuthStyleInParams AuthStyle = 1
108
+ AuthStyleInHeader AuthStyle = 2
109
+ )
110
+
111
+ // authStyleCache is the set of tokenURLs we've successfully used via
112
+ // RetrieveToken and which style auth we ended up using.
113
+ // It's called a cache, but it doesn't (yet?) shrink. It's expected that
114
+ // the set of OAuth2 servers a program contacts over time is fixed and
115
+ // small.
116
+ var authStyleCache struct {
117
+ sync.Mutex
118
+ m map [string ]AuthStyle // keyed by tokenURL
140
119
}
141
120
142
- // brokenAuthHeaderDomains lists broken providers that issue dynamic endpoints.
143
- var brokenAuthHeaderDomains = []string {
144
- ".auth0.com" ,
145
- ".force.com" ,
146
- ".myshopify.com" ,
147
- ".okta.com" ,
148
- ".oktapreview.com" ,
121
+ // ResetAuthCache resets the global authentication style cache used
122
+ // for AuthStyleUnknown token requests.
123
+ func ResetAuthCache () {
124
+ authStyleCache .Lock ()
125
+ defer authStyleCache .Unlock ()
126
+ authStyleCache .m = nil
149
127
}
150
128
151
- func RegisterBrokenAuthHeaderProvider (tokenURL string ) {
152
- brokenAuthHeaderProviders = append (brokenAuthHeaderProviders , tokenURL )
129
+ // lookupAuthStyle reports which auth style we last used with tokenURL
130
+ // when calling RetrieveToken and whether we have ever done so.
131
+ func lookupAuthStyle (tokenURL string ) (style AuthStyle , ok bool ) {
132
+ authStyleCache .Lock ()
133
+ defer authStyleCache .Unlock ()
134
+ style , ok = authStyleCache .m [tokenURL ]
135
+ return
153
136
}
154
137
155
- // providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
156
- // implements the OAuth2 spec correctly
157
- // See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
158
- // In summary:
159
- // - Reddit only accepts client secret in the Authorization header
160
- // - Dropbox accepts either it in URL param or Auth header, but not both.
161
- // - Google only accepts URL param (not spec compliant?), not Auth header
162
- // - Stripe only accepts client secret in Auth header with Bearer method, not Basic
163
- func providerAuthHeaderWorks (tokenURL string ) bool {
164
- for _ , s := range brokenAuthHeaderProviders {
165
- if strings .HasPrefix (tokenURL , s ) {
166
- // Some sites fail to implement the OAuth2 spec fully.
167
- return false
168
- }
138
+ // setAuthStyle adds an entry to authStyleCache, documented above.
139
+ func setAuthStyle (tokenURL string , v AuthStyle ) {
140
+ authStyleCache .Lock ()
141
+ defer authStyleCache .Unlock ()
142
+ if authStyleCache .m == nil {
143
+ authStyleCache .m = make (map [string ]AuthStyle )
169
144
}
170
-
171
- if u , err := url .Parse (tokenURL ); err == nil {
172
- for _ , s := range brokenAuthHeaderDomains {
173
- if strings .HasSuffix (u .Host , s ) {
174
- return false
175
- }
176
- }
177
- }
178
-
179
- // Assume the provider implements the spec properly
180
- // otherwise. We can add more exceptions as they're
181
- // discovered. We will _not_ be adding configurable hooks
182
- // to this package to let users select server bugs.
183
- return true
145
+ authStyleCache .m [tokenURL ] = v
184
146
}
185
147
186
- func RetrieveToken (ctx context.Context , clientID , clientSecret , tokenURL string , v url.Values ) (* Token , error ) {
187
- bustedAuth := ! providerAuthHeaderWorks (tokenURL )
188
- if bustedAuth {
148
+ // newTokenRequest returns a new *http.Request to retrieve a new token
149
+ // from tokenURL using the provided clientID, clientSecret, and POST
150
+ // body parameters.
151
+ //
152
+ // inParams is whether the clientID & clientSecret should be encoded
153
+ // as the POST body. An 'inParams' value of true means to send it in
154
+ // the POST body (along with any values in v); false means to send it
155
+ // in the Authorization header.
156
+ func newTokenRequest (tokenURL , clientID , clientSecret string , v url.Values , authStyle AuthStyle ) (* http.Request , error ) {
157
+ if authStyle == AuthStyleInParams {
158
+ v = cloneURLValues (v )
189
159
if clientID != "" {
190
160
v .Set ("client_id" , clientID )
191
161
}
@@ -198,15 +168,70 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
198
168
return nil , err
199
169
}
200
170
req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
201
- if ! bustedAuth {
171
+ if authStyle == AuthStyleInHeader {
202
172
req .SetBasicAuth (url .QueryEscape (clientID ), url .QueryEscape (clientSecret ))
203
173
}
174
+ return req , nil
175
+ }
176
+
177
+ func cloneURLValues (v url.Values ) url.Values {
178
+ v2 := make (url.Values , len (v ))
179
+ for k , vv := range v {
180
+ v2 [k ] = append ([]string (nil ), vv ... )
181
+ }
182
+ return v2
183
+ }
184
+
185
+ func RetrieveToken (ctx context.Context , clientID , clientSecret , tokenURL string , v url.Values , authStyle AuthStyle ) (* Token , error ) {
186
+ needsAuthStyleProbe := authStyle == 0
187
+ if needsAuthStyleProbe {
188
+ if style , ok := lookupAuthStyle (tokenURL ); ok {
189
+ authStyle = style
190
+ needsAuthStyleProbe = false
191
+ } else {
192
+ authStyle = AuthStyleInHeader // the first way we'll try
193
+ }
194
+ }
195
+ req , err := newTokenRequest (tokenURL , clientID , clientSecret , v , authStyle )
196
+ if err != nil {
197
+ return nil , err
198
+ }
199
+ token , err := doTokenRoundTrip (ctx , req )
200
+ if err != nil && needsAuthStyleProbe {
201
+ // If we get an error, assume the server wants the
202
+ // clientID & clientSecret in a different form.
203
+ // See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
204
+ // In summary:
205
+ // - Reddit only accepts client secret in the Authorization header
206
+ // - Dropbox accepts either it in URL param or Auth header, but not both.
207
+ // - Google only accepts URL param (not spec compliant?), not Auth header
208
+ // - Stripe only accepts client secret in Auth header with Bearer method, not Basic
209
+ //
210
+ // We used to maintain a big table in this code of all the sites and which way
211
+ // they went, but maintaining it didn't scale & got annoying.
212
+ // So just try both ways.
213
+ authStyle = AuthStyleInParams // the second way we'll try
214
+ req , _ = newTokenRequest (tokenURL , clientID , clientSecret , v , authStyle )
215
+ token , err = doTokenRoundTrip (ctx , req )
216
+ }
217
+ if needsAuthStyleProbe && err == nil {
218
+ setAuthStyle (tokenURL , authStyle )
219
+ }
220
+ // Don't overwrite `RefreshToken` with an empty value
221
+ // if this was a token refreshing request.
222
+ if token != nil && token .RefreshToken == "" {
223
+ token .RefreshToken = v .Get ("refresh_token" )
224
+ }
225
+ return token , err
226
+ }
227
+
228
+ func doTokenRoundTrip (ctx context.Context , req * http.Request ) (* Token , error ) {
204
229
r , err := ctxhttp .Do (ctx , ContextClient (ctx ), req )
205
230
if err != nil {
206
231
return nil , err
207
232
}
208
- defer r .Body .Close ()
209
233
body , err := ioutil .ReadAll (io .LimitReader (r .Body , 1 << 20 ))
234
+ r .Body .Close ()
210
235
if err != nil {
211
236
return nil , fmt .Errorf ("oauth2: cannot fetch token: %v" , err )
212
237
}
@@ -256,13 +281,8 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
256
281
}
257
282
json .Unmarshal (body , & token .Raw ) // no error checks for optional fields
258
283
}
259
- // Don't overwrite `RefreshToken` with an empty value
260
- // if this was a token refreshing request.
261
- if token .RefreshToken == "" {
262
- token .RefreshToken = v .Get ("refresh_token" )
263
- }
264
284
if token .AccessToken == "" {
265
- return token , errors .New ("oauth2: server response missing access_token" )
285
+ return nil , errors .New ("oauth2: server response missing access_token" )
266
286
}
267
287
return token , nil
268
288
}
0 commit comments