Skip to content

Commit fd15e0f

Browse files
andigseankhliao
authored andcommitted
x/oauth2: populate RetrieveError from DeviceAuth
Endpoints may return errors when attempting to request device authorization. Currently, these error codes are ignored and an otherwise empty RetrieveError returned. This change populates the RetrieveError similar to the oauth2 token exchange. Fixes golang/go#75759 Change-Id: Ic00fecce290d3d3b4a40697b54ce74cc8cacab4d GitHub-Last-Rev: 32c6ab5 GitHub-Pull-Request: #794 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/709215 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: David Chase <drchase@google.com> Reviewed-by: Sean Liao <sean@liao.dev> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
1 parent 792c877 commit fd15e0f

File tree

3 files changed

+84
-3
lines changed

3 files changed

+84
-3
lines changed

deviceauth.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"io"
9+
"mime"
910
"net/http"
1011
"net/url"
1112
"strings"
@@ -116,10 +117,38 @@ func retrieveDeviceAuth(ctx context.Context, c *Config, v url.Values) (*DeviceAu
116117
return nil, fmt.Errorf("oauth2: cannot auth device: %v", err)
117118
}
118119
if code := r.StatusCode; code < 200 || code > 299 {
119-
return nil, &RetrieveError{
120+
retrieveError := &RetrieveError{
120121
Response: r,
121122
Body: body,
122123
}
124+
125+
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
126+
switch content {
127+
case "application/x-www-form-urlencoded", "text/plain":
128+
// some endpoints return a query string
129+
vals, err := url.ParseQuery(string(body))
130+
if err != nil {
131+
return nil, retrieveError
132+
}
133+
retrieveError.ErrorCode = vals.Get("error")
134+
retrieveError.ErrorDescription = vals.Get("error_description")
135+
retrieveError.ErrorURI = vals.Get("error_uri")
136+
default:
137+
var tj struct {
138+
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
139+
ErrorCode string `json:"error"`
140+
ErrorDescription string `json:"error_description"`
141+
ErrorURI string `json:"error_uri"`
142+
}
143+
if json.Unmarshal(body, &tj) != nil {
144+
return nil, retrieveError
145+
}
146+
retrieveError.ErrorCode = tj.ErrorCode
147+
retrieveError.ErrorDescription = tj.ErrorDescription
148+
retrieveError.ErrorURI = tj.ErrorURI
149+
}
150+
151+
return nil, retrieveError
123152
}
124153

125154
da := &DeviceAuthResponse{}

deviceauth_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"net/http"
8+
"net/http/httptest"
79
"strings"
810
"testing"
911
"time"
@@ -101,3 +103,52 @@ func ExampleConfig_DeviceAuth() {
101103
}
102104
fmt.Println(token)
103105
}
106+
107+
func TestDeviceAuthTokenRetrieveError(t *testing.T) {
108+
runner := func(responseFun func(w http.ResponseWriter)) func(t *testing.T) {
109+
return func(t *testing.T) {
110+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
111+
if r.URL.String() != "/device" {
112+
t.Errorf("Unexpected device auth request URL, %v is found.", r.URL)
113+
}
114+
responseFun(w)
115+
}))
116+
defer ts.Close()
117+
conf := newConf(ts.URL)
118+
_, err := conf.DeviceAuth(context.Background())
119+
if err == nil {
120+
t.Fatalf("got no error, expected one")
121+
}
122+
re, ok := err.(*RetrieveError)
123+
if !ok {
124+
t.Fatalf("got %T error, expected *RetrieveError; error was: %v", err, err)
125+
}
126+
expected := `oauth2: "invalid_grant" "sometext"`
127+
if errStr := err.Error(); errStr != expected {
128+
t.Fatalf("got %#v, expected %#v", errStr, expected)
129+
}
130+
expected = "invalid_grant"
131+
if re.ErrorCode != expected {
132+
t.Fatalf("got %#v, expected %#v", re.ErrorCode, expected)
133+
}
134+
expected = "sometext"
135+
if re.ErrorDescription != expected {
136+
t.Fatalf("got %#v, expected %#v", re.ErrorDescription, expected)
137+
}
138+
}
139+
}
140+
141+
t.Run("UrlEncoding", runner(func(w http.ResponseWriter) {
142+
w.Header().Set("Content-type", "application/x-www-form-urlencoded")
143+
// "The authorization server responds with an HTTP 400 (Bad Request)" https://www.rfc-editor.org/rfc/rfc6749#section-5.2
144+
w.WriteHeader(http.StatusBadRequest)
145+
w.Write([]byte(`error=invalid_grant&error_description=sometext`))
146+
}))
147+
148+
t.Run("JSON", runner(func(w http.ResponseWriter) {
149+
w.Header().Set("Content-type", "application/json")
150+
// "The authorization server responds with an HTTP 400 (Bad Request)" https://www.rfc-editor.org/rfc/rfc6749#section-5.2
151+
w.WriteHeader(http.StatusBadRequest)
152+
w.Write([]byte(`{"error": "invalid_grant", "error_description": "sometext"}`))
153+
}))
154+
}

oauth2_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ func newConf(url string) *Config {
3131
RedirectURL: "REDIRECT_URL",
3232
Scopes: []string{"scope1", "scope2"},
3333
Endpoint: Endpoint{
34-
AuthURL: url + "/auth",
35-
TokenURL: url + "/token",
34+
AuthURL: url + "/auth",
35+
DeviceAuthURL: url + "/device",
36+
TokenURL: url + "/token",
3637
},
3738
}
3839
}

0 commit comments

Comments
 (0)