Skip to content

Commit cffa1bd

Browse files
committed
Add error fields to RetrieveError
Parse error parameters described in OAuth RFC https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 In error case, server should respond 400 according to spec, but also handle unorthodox servers responding 200. Try to be backwards compatible, avoid changing error string where possible
1 parent 68a41d6 commit cffa1bd

File tree

3 files changed

+73
-10
lines changed

3 files changed

+73
-10
lines changed

internal/token.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,18 @@ type Token struct {
5757
}
5858

5959
// tokenJSON is the struct representing the HTTP response from OAuth2
60-
// providers returning a token in JSON form.
60+
// providers returning a token or error in JSON form.
61+
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
6162
type tokenJSON struct {
6263
AccessToken string `json:"access_token"`
6364
TokenType string `json:"token_type"`
6465
RefreshToken string `json:"refresh_token"`
6566
ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
67+
// error fields
68+
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
69+
Error string `json:"error"`
70+
ErrorDescription string `json:"error_description"`
71+
ErrorUri string `json:"error_uri"`
6672
}
6773

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

248255
var token *Token
249256
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
250257
switch content {
251258
case "application/x-www-form-urlencoded", "text/plain":
259+
// some endpoints such as GitHub return a query string https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#response-1
252260
vals, err := url.ParseQuery(string(body))
253261
if err != nil {
254-
return nil, err
262+
if failureStatus {
263+
return nil, retrieveError
264+
}
265+
return nil, fmt.Errorf("oauth2: cannot parse response: %v", err)
255266
}
267+
retrieveError.ErrorCode = vals.Get("error")
268+
retrieveError.ErrorDescription = vals.Get("error_description")
269+
retrieveError.ErrorCode = vals.Get("error")
256270
token = &Token{
257271
AccessToken: vals.Get("access_token"),
258272
TokenType: vals.Get("token_type"),
@@ -265,10 +279,17 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
265279
token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
266280
}
267281
default:
282+
// spec says to return JSON https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
268283
var tj tokenJSON
269284
if err = json.Unmarshal(body, &tj); err != nil {
270-
return nil, err
285+
if failureStatus {
286+
return nil, retrieveError
287+
}
288+
return nil, fmt.Errorf("oauth2: cannot parse json: %v", err)
271289
}
290+
retrieveError.ErrorCode = tj.Error
291+
retrieveError.ErrorDescription = tj.ErrorDescription
292+
retrieveError.ErrorUri = tj.ErrorUri
272293
token = &Token{
273294
AccessToken: tj.AccessToken,
274295
TokenType: tj.TokenType,
@@ -278,15 +299,25 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
278299
}
279300
json.Unmarshal(body, &token.Raw) // no error checks for optional fields
280301
}
302+
// according to spec, servers should respond status 400 in error case
303+
// https://www.rfc-editor.org/rfc/rfc6749#section-5.2
304+
// but some servers such as GitHub respond 200 in error case, so look at response
305+
if failureStatus || retrieveError.ErrorCode != "" {
306+
return nil, retrieveError
307+
}
281308
if token.AccessToken == "" {
282309
return nil, errors.New("oauth2: server response missing access_token")
283310
}
284311
return token, nil
285312
}
286313

314+
// mirrors oauth2.RetrieveError
287315
type RetrieveError struct {
288-
Response *http.Response
289-
Body []byte
316+
Response *http.Response
317+
Body []byte
318+
ErrorCode string
319+
ErrorDescription string
320+
ErrorUri string
290321
}
291322

292323
func (r *RetrieveError) Error() string {

oauth2_test.go

Lines changed: 26 additions & 0 deletions
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
}))
@@ -504,6 +505,31 @@ func TestTokenRetrieveError(t *testing.T) {
504505
}
505506
}
506507

508+
// TestTokenRetrieveError200 tests handling of unorthodox server that returns 200 in error case
509+
func TestTokenRetrieveError200(t *testing.T) {
510+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
511+
if r.URL.String() != "/token" {
512+
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
513+
}
514+
w.Header().Set("Content-type", "application/json")
515+
w.Write([]byte(`{"error": "invalid_grant"}`))
516+
}))
517+
defer ts.Close()
518+
conf := newConf(ts.URL)
519+
_, err := conf.Exchange(context.Background(), "exchange-code")
520+
if err == nil {
521+
t.Fatalf("got no error, expected one")
522+
}
523+
_, ok := err.(*RetrieveError)
524+
if !ok {
525+
t.Fatalf("got %T error, expected *RetrieveError; error was: %v", err, err)
526+
}
527+
expected := fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", "200 OK", `{"error": "invalid_grant"}`)
528+
if errStr := err.Error(); errStr != expected {
529+
t.Fatalf("got %#v, expected %#v", errStr, expected)
530+
}
531+
}
532+
507533
func TestRefreshToken_RefreshTokenReplacement(t *testing.T) {
508534
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
509535
w.Header().Set("Content-Type", "application/json")

token.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@ type RetrieveError struct {
171171
// Body is the body that was consumed by reading Response.Body.
172172
// It may be truncated.
173173
Body []byte
174+
// rfc6749 error parameter https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
175+
ErrorCode string
176+
// rfc6749 error_description parameter https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
177+
ErrorDescription string
178+
// rfc6749 error_uri parameter https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
179+
ErrorUri string
174180
}
175181

176182
func (r *RetrieveError) Error() string {

0 commit comments

Comments
 (0)