Skip to content

Commit cfe200d

Browse files
hickfordneild
authored andcommitted
oauth2: parse RFC 6749 error response
Parse error response described in https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 Handle unorthodox servers responding 200 in error case. Implements API changes in accepted proposal golang/go#58125 Fixes golang#441 Fixes golang#274 Updates golang#173 Change-Id: If9399c3f952ac0501edbeefeb3a71ed057ca8d37 GitHub-Last-Rev: 0030e27 GitHub-Pull-Request: golang#610 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/451076 Run-TryBot: Matt Hickford <[email protected]> Run-TryBot: Damien Neil <[email protected]> Reviewed-by: Matt Hickford <[email protected]> Reviewed-by: Damien Neil <[email protected]> Reviewed-by: Cody Oss <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 3607514 commit cfe200d

File tree

3 files changed

+104
-14
lines changed

3 files changed

+104
-14
lines changed

internal/token.go

+50-10
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,18 @@ type Token struct {
5555
}
5656

5757
// tokenJSON is the struct representing the HTTP response from OAuth2
58-
// providers returning a token in JSON form.
58+
// providers returning a token or error in JSON form.
59+
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
5960
type tokenJSON struct {
6061
AccessToken string `json:"access_token"`
6162
TokenType string `json:"token_type"`
6263
RefreshToken string `json:"refresh_token"`
6364
ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
65+
// error fields
66+
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
67+
ErrorCode string `json:"error"`
68+
ErrorDescription string `json:"error_description"`
69+
ErrorURI string `json:"error_uri"`
6470
}
6571

6672
func (e *tokenJSON) expiry() (t time.Time) {
@@ -236,21 +242,29 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
236242
if err != nil {
237243
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
238244
}
239-
if code := r.StatusCode; code < 200 || code > 299 {
240-
return nil, &RetrieveError{
241-
Response: r,
242-
Body: body,
243-
}
245+
246+
failureStatus := r.StatusCode < 200 || r.StatusCode > 299
247+
retrieveError := &RetrieveError{
248+
Response: r,
249+
Body: body,
250+
// attempt to populate error detail below
244251
}
245252

246253
var token *Token
247254
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
248255
switch content {
249256
case "application/x-www-form-urlencoded", "text/plain":
257+
// some endpoints return a query string
250258
vals, err := url.ParseQuery(string(body))
251259
if err != nil {
252-
return nil, err
260+
if failureStatus {
261+
return nil, retrieveError
262+
}
263+
return nil, fmt.Errorf("oauth2: cannot parse response: %v", err)
253264
}
265+
retrieveError.ErrorCode = vals.Get("error")
266+
retrieveError.ErrorDescription = vals.Get("error_description")
267+
retrieveError.ErrorURI = vals.Get("error_uri")
254268
token = &Token{
255269
AccessToken: vals.Get("access_token"),
256270
TokenType: vals.Get("token_type"),
@@ -265,8 +279,14 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
265279
default:
266280
var tj tokenJSON
267281
if err = json.Unmarshal(body, &tj); err != nil {
268-
return nil, err
282+
if failureStatus {
283+
return nil, retrieveError
284+
}
285+
return nil, fmt.Errorf("oauth2: cannot parse json: %v", err)
269286
}
287+
retrieveError.ErrorCode = tj.ErrorCode
288+
retrieveError.ErrorDescription = tj.ErrorDescription
289+
retrieveError.ErrorURI = tj.ErrorURI
270290
token = &Token{
271291
AccessToken: tj.AccessToken,
272292
TokenType: tj.TokenType,
@@ -276,17 +296,37 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
276296
}
277297
json.Unmarshal(body, &token.Raw) // no error checks for optional fields
278298
}
299+
// according to spec, servers should respond status 400 in error case
300+
// https://www.rfc-editor.org/rfc/rfc6749#section-5.2
301+
// but some unorthodox servers respond 200 in error case
302+
if failureStatus || retrieveError.ErrorCode != "" {
303+
return nil, retrieveError
304+
}
279305
if token.AccessToken == "" {
280306
return nil, errors.New("oauth2: server response missing access_token")
281307
}
282308
return token, nil
283309
}
284310

311+
// mirrors oauth2.RetrieveError
285312
type RetrieveError struct {
286-
Response *http.Response
287-
Body []byte
313+
Response *http.Response
314+
Body []byte
315+
ErrorCode string
316+
ErrorDescription string
317+
ErrorURI string
288318
}
289319

290320
func (r *RetrieveError) Error() string {
321+
if r.ErrorCode != "" {
322+
s := fmt.Sprintf("oauth2: %q", r.ErrorCode)
323+
if r.ErrorDescription != "" {
324+
s += fmt.Sprintf(" %q", r.ErrorDescription)
325+
}
326+
if r.ErrorURI != "" {
327+
s += fmt.Sprintf(" %q", r.ErrorURI)
328+
}
329+
return s
330+
}
291331
return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
292332
}

oauth2_test.go

+36-3
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ func TestTokenRetrieveError(t *testing.T) {
484484
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
485485
}
486486
w.Header().Set("Content-type", "application/json")
487+
// "The authorization server responds with an HTTP 400 (Bad Request)" https://www.rfc-editor.org/rfc/rfc6749#section-5.2
487488
w.WriteHeader(http.StatusBadRequest)
488489
w.Write([]byte(`{"error": "invalid_grant"}`))
489490
}))
@@ -493,15 +494,47 @@ func TestTokenRetrieveError(t *testing.T) {
493494
if err == nil {
494495
t.Fatalf("got no error, expected one")
495496
}
496-
_, ok := err.(*RetrieveError)
497+
re, ok := err.(*RetrieveError)
497498
if !ok {
498499
t.Fatalf("got %T error, expected *RetrieveError; error was: %v", err, err)
499500
}
500-
// Test error string for backwards compatibility
501-
expected := fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", "400 Bad Request", `{"error": "invalid_grant"}`)
501+
expected := `oauth2: "invalid_grant"`
502502
if errStr := err.Error(); errStr != expected {
503503
t.Fatalf("got %#v, expected %#v", errStr, expected)
504504
}
505+
expected = "invalid_grant"
506+
if re.ErrorCode != expected {
507+
t.Fatalf("got %#v, expected %#v", re.ErrorCode, expected)
508+
}
509+
}
510+
511+
// TestTokenRetrieveError200 tests handling of unorthodox server that returns 200 in error case
512+
func TestTokenRetrieveError200(t *testing.T) {
513+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
514+
if r.URL.String() != "/token" {
515+
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
516+
}
517+
w.Header().Set("Content-type", "application/json")
518+
w.Write([]byte(`{"error": "invalid_grant"}`))
519+
}))
520+
defer ts.Close()
521+
conf := newConf(ts.URL)
522+
_, err := conf.Exchange(context.Background(), "exchange-code")
523+
if err == nil {
524+
t.Fatalf("got no error, expected one")
525+
}
526+
re, ok := err.(*RetrieveError)
527+
if !ok {
528+
t.Fatalf("got %T error, expected *RetrieveError; error was: %v", err, err)
529+
}
530+
expected := `oauth2: "invalid_grant"`
531+
if errStr := err.Error(); errStr != expected {
532+
t.Fatalf("got %#v, expected %#v", errStr, expected)
533+
}
534+
expected = "invalid_grant"
535+
if re.ErrorCode != expected {
536+
t.Fatalf("got %#v, expected %#v", re.ErrorCode, expected)
537+
}
505538
}
506539

507540
func TestRefreshToken_RefreshTokenReplacement(t *testing.T) {

token.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,31 @@ func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error)
175175
}
176176

177177
// RetrieveError is the error returned when the token endpoint returns a
178-
// non-2XX HTTP status code.
178+
// non-2XX HTTP status code or populates RFC 6749's 'error' parameter.
179+
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
179180
type RetrieveError struct {
180181
Response *http.Response
181182
// Body is the body that was consumed by reading Response.Body.
182183
// It may be truncated.
183184
Body []byte
185+
// ErrorCode is RFC 6749's 'error' parameter.
186+
ErrorCode string
187+
// ErrorDescription is RFC 6749's 'error_description' parameter.
188+
ErrorDescription string
189+
// ErrorURI is RFC 6749's 'error_uri' parameter.
190+
ErrorURI string
184191
}
185192

186193
func (r *RetrieveError) Error() string {
194+
if r.ErrorCode != "" {
195+
s := fmt.Sprintf("oauth2: %q", r.ErrorCode)
196+
if r.ErrorDescription != "" {
197+
s += fmt.Sprintf(" %q", r.ErrorDescription)
198+
}
199+
if r.ErrorURI != "" {
200+
s += fmt.Sprintf(" %q", r.ErrorURI)
201+
}
202+
return s
203+
}
187204
return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
188205
}

0 commit comments

Comments
 (0)