git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #660 from oauth2-proxy/request-builder

Use builder pattern to simplify requests to external endpoints

commitd29766609bc652db41bbb45545ce24980ac8c68c authorJoel Speed <Joel.speed@hotmail.co.uk> dateMon 6 Jul 2020, 21:01 (6 years ago) parent9d39816de9e65a
23 files changed+1245−649
CHANGELOG.md+1 pkg/requests/builder.go+118 pkg/requests/builder_test.go+376 pkg/requests/requests.go−74 pkg/requests/requests_suite_test.go+96 pkg/requests/requests_test.go−136 pkg/requests/result.go+98 pkg/requests/result_test.go+326 pkg/validation/options.go+29−29 providers/azure.go+14−37 providers/bitbucket.go+26−28 providers/digitalocean.go+5−6 providers/facebook.go+8−6 providers/github.go+49−128 providers/gitlab.go+7−26 providers/google.go+21−49 providers/internal_util.go+10−9 providers/keycloak.go+5−9 providers/linkedin.go+6−6 providers/logingov.go+26−60 providers/nextcloud.go+7−11 providers/oidc.go+5−7 providers/provider_default.go+12−28
CHANGELOG.md+1
@@ -8,6 +8,7 @@
88
99## Changes since v6.0.0
1010
11+- [#660](https://github.com/oauth2-proxy/oauth2-proxy/pull/660) Use builder pattern to simplify requests to external endpoints (@JoelSpeed)
1112- [#591](https://github.com/oauth2-proxy/oauth2-proxy/pull/591) Introduce upstream package with new reverse proxy implementation (@JoelSpeed)
1213- [#576](https://github.com/oauth2-proxy/oauth2-proxy/pull/576) Separate Cookie validation out of main options validation (@JoelSpeed)
1314- [#656](https://github.com/oauth2-proxy/oauth2-proxy/pull/656) Split long session cookies more precisely (@NickMeves)
pkg/requests/builder.gonew file+118
@@ -0,0 +1,118 @@
1+package requests
2+
3+import (
4+ "context"
5+ "fmt"
6+ "io"
7+ "io/ioutil"
8+ "net/http"
9+)
10+
11+// Builder allows users to construct a request and then execute the
12+// request via Do().
13+// Do returns a Result which allows the user to get the body,
14+// unmarshal the body into an interface, or into a simplejson.Json.
15+type Builder interface {
16+ WithContext(context.Context) Builder
17+ WithBody(io.Reader) Builder
18+ WithMethod(string) Builder
19+ WithHeaders(http.Header) Builder
20+ SetHeader(key, value string) Builder
21+ Do() Result
22+}
23+
24+type builder struct {
25+ context context.Context
26+ method string
27+ endpoint string
28+ body io.Reader
29+ header http.Header
30+ result *result
31+}
32+
33+// New provides a new Builder for the given endpoint.
34+func New(endpoint string) Builder {
35+ return &builder{
36+ endpoint: endpoint,
37+ method: "GET",
38+ }
39+}
40+
41+// WithContext adds a context to the request.
42+// If no context is provided, context.Background() is used instead.
43+func (r *builder) WithContext(ctx context.Context) Builder {
44+ r.context = ctx
45+ return r
46+}
47+
48+// WithBody adds a body to the request.
49+func (r *builder) WithBody(body io.Reader) Builder {
50+ r.body = body
51+ return r
52+}
53+
54+// WithMethod sets the request method. Defaults to "GET".
55+func (r *builder) WithMethod(method string) Builder {
56+ r.method = method
57+ return r
58+}
59+
60+// WithHeaders replaces the request header map with the given header map.
61+func (r *builder) WithHeaders(header http.Header) Builder {
62+ r.header = header
63+ return r
64+}
65+
66+// SetHeader sets a single header to the given value.
67+// May be used to add multiple headers.
68+func (r *builder) SetHeader(key, value string) Builder {
69+ if r.header == nil {
70+ r.header = make(http.Header)
71+ }
72+ r.header.Set(key, value)
73+ return r
74+}
75+
76+// Do performs the request and returns the response in its raw form.
77+// If the request has already been performed, returns the previous result.
78+// This will not allow you to repeat a request.
79+func (r *builder) Do() Result {
80+ if r.result != nil {
81+ // Request has already been done
82+ return r.result
83+ }
84+
85+ // Must provide a non-nil context to NewRequestWithContext
86+ if r.context == nil {
87+ r.context = context.Background()
88+ }
89+
90+ return r.do()
91+}
92+
93+// do creates the request, executes it with the default client and extracts the
94+// the body into the response
95+func (r *builder) do() Result {
96+ req, err := http.NewRequestWithContext(r.context, r.method, r.endpoint, r.body)
97+ if err != nil {
98+ r.result = &result{err: fmt.Errorf("error creating request: %v", err)}
99+ return r.result
100+ }
101+ req.Header = r.header
102+
103+ resp, err := http.DefaultClient.Do(req)
104+ if err != nil {
105+ r.result = &result{err: fmt.Errorf("error performing request: %v", err)}
106+ return r.result
107+ }
108+
109+ defer resp.Body.Close()
110+ body, err := ioutil.ReadAll(resp.Body)
111+ if err != nil {
112+ r.result = &result{err: fmt.Errorf("error reading response body: %v", err)}
113+ return r.result
114+ }
115+
116+ r.result = &result{response: resp, body: body}
117+ return r.result
118+}
pkg/requests/builder_test.gonew file+376
@@ -0,0 +1,376 @@
1+package requests
2+
3+import (
4+ "bytes"
5+ "context"
6+ "encoding/base64"
7+ "encoding/json"
8+ "fmt"
9+ "net/http"
10+
11+ "github.com/bitly/go-simplejson"
12+ . "github.com/onsi/ginkgo"
13+ . "github.com/onsi/gomega"
14+)
15+
16+var _ = Describe("Builder suite", func() {
17+ var b Builder
18+ getBuilder := func() Builder { return b }
19+
20+ baseHeaders := http.Header{
21+ "Accept-Encoding": []string{"gzip"},
22+ "User-Agent": []string{"Go-http-client/1.1"},
23+ }
24+
25+ BeforeEach(func() {
26+ // Most tests will request the server address
27+ b = New(serverAddr + "/json/path")
28+ })
29+
30+ Context("with a basic request", func() {
31+ assertSuccessfulRequest(getBuilder, testHTTPRequest{
32+ Method: "GET",
33+ Header: baseHeaders,
34+ Body: []byte{},
35+ RequestURI: "/json/path",
36+ })
37+ })
38+
39+ Context("with a context", func() {
40+ var ctx context.Context
41+ var cancel context.CancelFunc
42+
43+ BeforeEach(func() {
44+ ctx, cancel = context.WithCancel(context.Background())
45+ b = b.WithContext(ctx)
46+ })
47+
48+ AfterEach(func() {
49+ cancel()
50+ })
51+
52+ assertSuccessfulRequest(getBuilder, testHTTPRequest{
53+ Method: "GET",
54+ Header: baseHeaders,
55+ Body: []byte{},
56+ RequestURI: "/json/path",
57+ })
58+
59+ Context("if the context is cancelled", func() {
60+ BeforeEach(func() {
61+ cancel()
62+ })
63+
64+ assertRequestError(getBuilder, "context canceled")
65+ })
66+ })
67+
68+ Context("with a body", func() {
69+ const body = "{\"some\": \"body\"}"
70+ header := baseHeaders.Clone()
71+ header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
72+
73+ BeforeEach(func() {
74+ buf := bytes.NewBuffer([]byte(body))
75+ b = b.WithBody(buf)
76+ })
77+
78+ assertSuccessfulRequest(getBuilder, testHTTPRequest{
79+ Method: "GET",
80+ Header: header,
81+ Body: []byte(body),
82+ RequestURI: "/json/path",
83+ })
84+ })
85+
86+ Context("with a method", func() {
87+ Context("POST with a body", func() {
88+ const body = "{\"some\": \"body\"}"
89+ header := baseHeaders.Clone()
90+ header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
91+
92+ BeforeEach(func() {
93+ buf := bytes.NewBuffer([]byte(body))
94+ b = b.WithMethod("POST").WithBody(buf)
95+ })
96+
97+ assertSuccessfulRequest(getBuilder, testHTTPRequest{
98+ Method: "POST",
99+ Header: header,
100+ Body: []byte(body),
101+ RequestURI: "/json/path",
102+ })
103+ })
104+
105+ Context("POST without a body", func() {
106+ header := baseHeaders.Clone()
107+ header.Set("Content-Length", "0")
108+
109+ BeforeEach(func() {
110+ b = b.WithMethod("POST")
111+ })
112+
113+ assertSuccessfulRequest(getBuilder, testHTTPRequest{
114+ Method: "POST",
115+ Header: header,
116+ Body: []byte{},
117+ RequestURI: "/json/path",
118+ })
119+ })
120+
121+ Context("OPTIONS", func() {
122+ BeforeEach(func() {
123+ b = b.WithMethod("OPTIONS")
124+ })
125+
126+ assertSuccessfulRequest(getBuilder, testHTTPRequest{
127+ Method: "OPTIONS",
128+ Header: baseHeaders,
129+ Body: []byte{},
130+ RequestURI: "/json/path",
131+ })
132+ })
133+
134+ Context("INVALID-\\t-METHOD", func() {
135+ BeforeEach(func() {
136+ b = b.WithMethod("INVALID-\t-METHOD")
137+ })
138+
139+ assertRequestError(getBuilder, "error creating request: net/http: invalid method \"INVALID-\\t-METHOD\"")
140+ })
141+ })
142+
143+ Context("with headers", func() {
144+ Context("setting a header", func() {
145+ header := baseHeaders.Clone()
146+ header.Set("header", "value")
147+
148+ BeforeEach(func() {
149+ b = b.SetHeader("header", "value")
150+ })
151+
152+ assertSuccessfulRequest(getBuilder, testHTTPRequest{
153+ Method: "GET",
154+ Header: header,
155+ Body: []byte{},
156+ RequestURI: "/json/path",
157+ })
158+
159+ Context("then replacing the headers", func() {
160+ replacementHeaders := http.Header{
161+ "Accept-Encoding": []string{"*"},
162+ "User-Agent": []string{"test-agent"},
163+ "Foo": []string{"bar, baz"},
164+ }
165+
166+ BeforeEach(func() {
167+ b = b.WithHeaders(replacementHeaders)
168+ })
169+
170+ assertSuccessfulRequest(getBuilder, testHTTPRequest{
171+ Method: "GET",
172+ Header: replacementHeaders,
173+ Body: []byte{},
174+ RequestURI: "/json/path",
175+ })
176+ })
177+ })
178+
179+ Context("replacing the header", func() {
180+ replacementHeaders := http.Header{
181+ "Accept-Encoding": []string{"*"},
182+ "User-Agent": []string{"test-agent"},
183+ "Foo": []string{"bar, baz"},
184+ }
185+
186+ BeforeEach(func() {
187+ b = b.WithHeaders(replacementHeaders)
188+ })
189+
190+ assertSuccessfulRequest(getBuilder, testHTTPRequest{
191+ Method: "GET",
192+ Header: replacementHeaders,
193+ Body: []byte{},
194+ RequestURI: "/json/path",
195+ })
196+
197+ Context("then setting a header", func() {
198+ header := replacementHeaders.Clone()
199+ header.Set("User-Agent", "different-agent")
200+
201+ BeforeEach(func() {
202+ b = b.SetHeader("User-Agent", "different-agent")
203+ })
204+
205+ assertSuccessfulRequest(getBuilder, testHTTPRequest{
206+ Method: "GET",
207+ Header: header,
208+ Body: []byte{},
209+ RequestURI: "/json/path",
210+ })
211+ })
212+ })
213+ })
214+
215+ Context("if the request has been completed and then modified", func() {
216+ BeforeEach(func() {
217+ result := b.Do()
218+ Expect(result.Error()).ToNot(HaveOccurred())
219+
220+ b.WithMethod("POST")
221+ })
222+
223+ Context("should not redo the request", func() {
224+ assertSuccessfulRequest(getBuilder, testHTTPRequest{
225+ Method: "GET",
226+ Header: baseHeaders,
227+ Body: []byte{},
228+ RequestURI: "/json/path",
229+ })
230+ })
231+ })
232+
233+ Context("when the requested page is not found", func() {
234+ BeforeEach(func() {
235+ b = New(serverAddr + "/not-found")
236+ })
237+
238+ assertJSONError(getBuilder, "404 page not found")
239+ })
240+
241+ Context("when the requested page is not valid JSON", func() {
242+ BeforeEach(func() {
243+ b = New(serverAddr + "/string/path")
244+ })
245+
246+ assertJSONError(getBuilder, "invalid character 'O' looking for beginning of value")
247+ })
248+})
249+
250+func assertSuccessfulRequest(builder func() Builder, expectedRequest testHTTPRequest) {
251+ Context("Do", func() {
252+ var result Result
253+
254+ BeforeEach(func() {
255+ result = builder().Do()
256+ Expect(result.Error()).ToNot(HaveOccurred())
257+ })
258+
259+ It("returns a successful status", func() {
260+ Expect(result.StatusCode()).To(Equal(http.StatusOK))
261+ })
262+
263+ It("made the expected request", func() {
264+ actualRequest := testHTTPRequest{}
265+ Expect(json.Unmarshal(result.Body(), &actualRequest)).To(Succeed())
266+
267+ Expect(actualRequest).To(Equal(expectedRequest))
268+ })
269+ })
270+
271+ Context("UnmarshalInto", func() {
272+ var actualRequest testHTTPRequest
273+
274+ BeforeEach(func() {
275+ Expect(builder().Do().UnmarshalInto(&actualRequest)).To(Succeed())
276+ })
277+
278+ It("made the expected request", func() {
279+ Expect(actualRequest).To(Equal(expectedRequest))
280+ })
281+ })
282+
283+ Context("UnmarshalJSON", func() {
284+ var response *simplejson.Json
285+
286+ BeforeEach(func() {
287+ var err error
288+ response, err = builder().Do().UnmarshalJSON()
289+ Expect(err).ToNot(HaveOccurred())
290+ })
291+
292+ It("made the expected reqest", func() {
293+ header := http.Header{}
294+ for key, value := range response.Get("Header").MustMap() {
295+ vs, ok := value.([]interface{})
296+ Expect(ok).To(BeTrue())
297+ svs := []string{}
298+ for _, v := range vs {
299+ sv, ok := v.(string)
300+ Expect(ok).To(BeTrue())
301+ svs = append(svs, sv)
302+ }
303+ header[key] = svs
304+ }
305+
306+ // Other json unmarhsallers base64 decode byte slices automatically
307+ body, err := base64.StdEncoding.DecodeString(response.Get("Body").MustString())
308+ Expect(err).ToNot(HaveOccurred())
309+
310+ actualRequest := testHTTPRequest{
311+ Method: response.Get("Method").MustString(),
312+ Header: header,
313+ Body: body,
314+ RequestURI: response.Get("RequestURI").MustString(),
315+ }
316+
317+ Expect(actualRequest).To(Equal(expectedRequest))
318+ })
319+ })
320+}
321+
322+func assertRequestError(builder func() Builder, errorMessage string) {
323+ Context("Do", func() {
324+ It("returns an error", func() {
325+ result := builder().Do()
326+ Expect(result.Error()).To(MatchError(ContainSubstring(errorMessage)))
327+ })
328+ })
329+
330+ Context("UnmarshalInto", func() {
331+ It("returns an error", func() {
332+ var actualRequest testHTTPRequest
333+ err := builder().Do().UnmarshalInto(&actualRequest)
334+ Expect(err).To(MatchError(ContainSubstring(errorMessage)))
335+
336+ // Should be empty
337+ Expect(actualRequest).To(Equal(testHTTPRequest{}))
338+ })
339+ })
340+
341+ Context("UnmarshalJSON", func() {
342+ It("returns an error", func() {
343+ resp, err := builder().Do().UnmarshalJSON()
344+ Expect(err).To(MatchError(ContainSubstring(errorMessage)))
345+ Expect(resp).To(BeNil())
346+ })
347+ })
348+}
349+
350+func assertJSONError(builder func() Builder, errorMessage string) {
351+ Context("Do", func() {
352+ It("does not return an error", func() {
353+ result := builder().Do()
354+ Expect(result.Error()).To(BeNil())
355+ })
356+ })
357+
358+ Context("UnmarshalInto", func() {
359+ It("returns an error", func() {
360+ var actualRequest testHTTPRequest
361+ err := builder().Do().UnmarshalInto(&actualRequest)
362+ Expect(err).To(MatchError(ContainSubstring(errorMessage)))
363+
364+ // Should be empty
365+ Expect(actualRequest).To(Equal(testHTTPRequest{}))
366+ })
367+ })
368+
369+ Context("UnmarshalJSON", func() {
370+ It("returns an error", func() {
371+ resp, err := builder().Do().UnmarshalJSON()
372+ Expect(err).To(MatchError(ContainSubstring(errorMessage)))
373+ Expect(resp).To(BeNil())
374+ })
375+ })
376+}
pkg/requests/requests.godeleted−74
@@ -1,74 +0,0 @@
1−package requests
2−
3−import (
4− "context"
5− "encoding/json"
6− "fmt"
7− "io/ioutil"
8− "net/http"
9−
10− "github.com/bitly/go-simplejson"
11− "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
12−)
13−
14−// Request parses the request body into a simplejson.Json object
15−func Request(req *http.Request) (*simplejson.Json, error) {
16− resp, err := http.DefaultClient.Do(req)
17− if err != nil {
18− logger.Printf("%s %s %s", req.Method, req.URL, err)
19− return nil, err
20− }
21− body, err := ioutil.ReadAll(resp.Body)
22− if body != nil {
23− defer resp.Body.Close()
24− }
25−
26− logger.Printf("%d %s %s %s", resp.StatusCode, req.Method, req.URL, body)
27−
28− if err != nil {
29− return nil, fmt.Errorf("problem reading http request body: %w", err)
30− }
31−
32− if resp.StatusCode != 200 {
33− return nil, fmt.Errorf("got %d %s", resp.StatusCode, body)
34− }
35−
36− data, err := simplejson.NewJson(body)
37− if err != nil {
38− return nil, fmt.Errorf("error unmarshalling json: %w", err)
39− }
40− return data, nil
41−}
42−
43−// RequestJSON parses the request body into the given interface
44−func RequestJSON(req *http.Request, v interface{}) error {
45− resp, err := http.DefaultClient.Do(req)
46− if err != nil {
47− logger.Printf("%s %s %s", req.Method, req.URL, err)
48− return err
49− }
50− body, err := ioutil.ReadAll(resp.Body)
51− if body != nil {
52− defer resp.Body.Close()
53− }
54−
55− logger.Printf("%d %s %s %s", resp.StatusCode, req.Method, req.URL, body)
56− if err != nil {
57− return fmt.Errorf("error reading body from http response: %w", err)
58− }
59− if resp.StatusCode != 200 {
60− return fmt.Errorf("got %d %s", resp.StatusCode, body)
61− }
62− return json.Unmarshal(body, v)
63−}
64−
65−// RequestUnparsedResponse performs a GET and returns the raw response object
66−func RequestUnparsedResponse(ctx context.Context, url string, header http.Header) (resp *http.Response, err error) {
67− req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
68− if err != nil {
69− return nil, fmt.Errorf("error performing get request: %w", err)
70− }
71− req.Header = header
72−
73− return http.DefaultClient.Do(req)
74−}
pkg/requests/requests_suite_test.gonew file+96
@@ -0,0 +1,96 @@
1+package requests
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "io/ioutil"
7+ "log"
8+ "net/http"
9+ "net/http/httptest"
10+ "testing"
11+
12+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
13+ . "github.com/onsi/ginkgo"
14+ . "github.com/onsi/gomega"
15+)
16+
17+var (
18+ server *httptest.Server
19+ serverAddr string
20+)
21+
22+func TestRequetsSuite(t *testing.T) {
23+ logger.SetOutput(GinkgoWriter)
24+ log.SetOutput(GinkgoWriter)
25+
26+ RegisterFailHandler(Fail)
27+ RunSpecs(t, "Requests Suite")
28+}
29+
30+var _ = BeforeSuite(func() {
31+ // Set up a webserver that reflects requests
32+ mux := http.NewServeMux()
33+ mux.Handle("/json/", &testHTTPUpstream{})
34+ mux.HandleFunc("/string/", func(rw http.ResponseWriter, _ *http.Request) {
35+ rw.Write([]byte("OK"))
36+ })
37+ server = httptest.NewServer(mux)
38+ serverAddr = fmt.Sprintf("http://%s", server.Listener.Addr().String())
39+})
40+
41+var _ = AfterSuite(func() {
42+ server.Close()
43+})
44+
45+// testHTTPRequest is a struct used to capture the state of a request made to
46+// the test server
47+type testHTTPRequest struct {
48+ Method string
49+ Header http.Header
50+ Body []byte
51+ RequestURI string
52+}
53+
54+type testHTTPUpstream struct{}
55+
56+func (t *testHTTPUpstream) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
57+ request, err := toTestHTTPRequest(req)
58+ if err != nil {
59+ t.writeError(rw, err)
60+ return
61+ }
62+
63+ data, err := json.Marshal(request)
64+ if err != nil {
65+ t.writeError(rw, err)
66+ return
67+ }
68+
69+ rw.Header().Set("Content-Type", "application/json")
70+ rw.Write(data)
71+}
72+
73+func (t *testHTTPUpstream) writeError(rw http.ResponseWriter, err error) {
74+ rw.WriteHeader(500)
75+ if err != nil {
76+ rw.Write([]byte(err.Error()))
77+ }
78+}
79+
80+func toTestHTTPRequest(req *http.Request) (testHTTPRequest, error) {
81+ requestBody := []byte{}
82+ if req.Body != http.NoBody {
83+ var err error
84+ requestBody, err = ioutil.ReadAll(req.Body)
85+ if err != nil {
86+ return testHTTPRequest{}, err
87+ }
88+ }
89+
90+ return testHTTPRequest{
91+ Method: req.Method,
92+ Header: req.Header,
93+ Body: requestBody,
94+ RequestURI: req.RequestURI,
95+ }, nil
96+}
pkg/requests/requests_test.godeleted−136
@@ -1,136 +0,0 @@
1−package requests
2−
3−import (
4− "context"
5− "io/ioutil"
6− "net/http"
7− "net/http/httptest"
8− "strings"
9− "testing"
10−
11− "github.com/bitly/go-simplejson"
12− "github.com/stretchr/testify/assert"
13− "github.com/stretchr/testify/require"
14−)
15−
16−func testBackend(t *testing.T, responseCode int, payload string) *httptest.Server {
17− return httptest.NewServer(http.HandlerFunc(
18− func(w http.ResponseWriter, r *http.Request) {
19− w.WriteHeader(responseCode)
20− _, err := w.Write([]byte(payload))
21− require.NoError(t, err)
22− }))
23−}
24−
25−func TestRequest(t *testing.T) {
26− backend := testBackend(t, 200, "{\"foo\": \"bar\"}")
27− defer backend.Close()
28−
29− req, _ := http.NewRequest("GET", backend.URL, nil)
30− response, err := Request(req)
31− assert.Equal(t, nil, err)
32− result, err := response.Get("foo").String()
33− assert.Equal(t, nil, err)
34− assert.Equal(t, "bar", result)
35−}
36−
37−func TestRequestFailure(t *testing.T) {
38− // Create a backend to generate a test URL, then close it to cause a
39− // connection error.
40− backend := testBackend(t, 200, "{\"foo\": \"bar\"}")
41− backend.Close()
42−
43− req, err := http.NewRequest("GET", backend.URL, nil)
44− assert.Equal(t, nil, err)
45− resp, err := Request(req)
46− assert.Equal(t, (*simplejson.Json)(nil), resp)
47− assert.NotEqual(t, nil, err)
48− if !strings.Contains(err.Error(), "refused") {
49− t.Error("expected error when a connection fails: ", err)
50− }
51−}
52−
53−func TestHttpErrorCode(t *testing.T) {
54− backend := testBackend(t, 404, "{\"foo\": \"bar\"}")
55− defer backend.Close()
56−
57− req, err := http.NewRequest("GET", backend.URL, nil)
58− assert.Equal(t, nil, err)
59− resp, err := Request(req)
60− assert.Equal(t, (*simplejson.Json)(nil), resp)
61− assert.NotEqual(t, nil, err)
62−}
63−
64−func TestJsonParsingError(t *testing.T) {
65− backend := testBackend(t, 200, "not well-formed JSON")
66− defer backend.Close()
67−
68− req, err := http.NewRequest("GET", backend.URL, nil)
69− assert.Equal(t, nil, err)
70− resp, err := Request(req)
71− assert.Equal(t, (*simplejson.Json)(nil), resp)
72− assert.NotEqual(t, nil, err)
73−}
74−
75−// Parsing a URL practically never fails, so we won't cover that test case.
76−func TestRequestUnparsedResponseUsingAccessTokenParameter(t *testing.T) {
77− backend := httptest.NewServer(http.HandlerFunc(
78− func(w http.ResponseWriter, r *http.Request) {
79− token := r.FormValue("access_token")
80− if r.URL.Path == "/" && token == "my_token" {
81− w.WriteHeader(200)
82− _, err := w.Write([]byte("some payload"))
83− require.NoError(t, err)
84− } else {
85− w.WriteHeader(403)
86− }
87− }))
88− defer backend.Close()
89−
90− response, err := RequestUnparsedResponse(
91− context.Background(), backend.URL+"?access_token=my_token", nil)
92− assert.Equal(t, nil, err)
93− defer response.Body.Close()
94−
95− assert.Equal(t, 200, response.StatusCode)
96− body, err := ioutil.ReadAll(response.Body)
97− assert.Equal(t, nil, err)
98− assert.Equal(t, "some payload", string(body))
99−}
100−
101−func TestRequestUnparsedResponseUsingAccessTokenParameterFailedResponse(t *testing.T) {
102− backend := testBackend(t, 200, "some payload")
103− // Close the backend now to force a request failure.
104− backend.Close()
105−
106− response, err := RequestUnparsedResponse(
107− context.Background(), backend.URL+"?access_token=my_token", nil)
108− assert.NotEqual(t, nil, err)
109− assert.Equal(t, (*http.Response)(nil), response)
110−}
111−
112−func TestRequestUnparsedResponseUsingHeaders(t *testing.T) {
113− backend := httptest.NewServer(http.HandlerFunc(
114− func(w http.ResponseWriter, r *http.Request) {
115− if r.URL.Path == "/" && r.Header["Auth"][0] == "my_token" {
116− w.WriteHeader(200)
117− _, err := w.Write([]byte("some payload"))
118− require.NoError(t, err)
119− } else {
120− w.WriteHeader(403)
121− }
122− }))
123− defer backend.Close()
124−
125− headers := make(http.Header)
126− headers.Set("Auth", "my_token")
127− response, err := RequestUnparsedResponse(context.Background(), backend.URL, headers)
128− assert.Equal(t, nil, err)
129− defer response.Body.Close()
130−
131− assert.Equal(t, 200, response.StatusCode)
132− body, err := ioutil.ReadAll(response.Body)
133− assert.Equal(t, nil, err)
134−
135− assert.Equal(t, "some payload", string(body))
136−}
pkg/requests/result.gonew file+98
@@ -0,0 +1,98 @@
1+package requests
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "net/http"
7+
8+ "github.com/bitly/go-simplejson"
9+)
10+
11+// Result is the result of a request created by a Builder
12+type Result interface {
13+ Error() error
14+ StatusCode() int
15+ Headers() http.Header
16+ Body() []byte
17+ UnmarshalInto(interface{}) error
18+ UnmarshalJSON() (*simplejson.Json, error)
19+}
20+
21+type result struct {
22+ err error
23+ response *http.Response
24+ body []byte
25+}
26+
27+// Error returns an error from the result if present
28+func (r *result) Error() error {
29+ return r.err
30+}
31+
32+// StatusCode returns the response's status code
33+func (r *result) StatusCode() int {
34+ if r.response != nil {
35+ return r.response.StatusCode
36+ }
37+ return 0
38+}
39+
40+// Headers returns the response's headers
41+func (r *result) Headers() http.Header {
42+ if r.response != nil {
43+ return r.response.Header
44+ }
45+ return nil
46+}
47+
48+// Body returns the response's body
49+func (r *result) Body() []byte {
50+ return r.body
51+}
52+
53+// UnmarshalInto attempts to unmarshal the response into the the given interface.
54+// The response body is assumed to be JSON.
55+// The response must have a 200 status otherwise an error will be returned.
56+func (r *result) UnmarshalInto(into interface{}) error {
57+ body, err := r.getBodyForUnmarshal()
58+ if err != nil {
59+ return err
60+ }
61+
62+ if err := json.Unmarshal(body, into); err != nil {
63+ return fmt.Errorf("error unmarshalling body: %v", err)
64+ }
65+
66+ return nil
67+}
68+
69+// UnmarshalJSON performs the request and attempts to unmarshal the response into a
70+// simplejson.Json. The response body is assume to be JSON.
71+// The response must have a 200 status otherwise an error will be returned.
72+func (r *result) UnmarshalJSON() (*simplejson.Json, error) {
73+ body, err := r.getBodyForUnmarshal()
74+ if err != nil {
75+ return nil, err
76+ }
77+
78+ data, err := simplejson.NewJson(body)
79+ if err != nil {
80+ return nil, fmt.Errorf("error reading json: %v", err)
81+ }
82+ return data, nil
83+}
84+
85+// getBodyForUnmarshal returns the body if there wasn't an error and the status
86+// code was 200.
87+func (r *result) getBodyForUnmarshal() ([]byte, error) {
88+ if r.Error() != nil {
89+ return nil, r.Error()
90+ }
91+
92+ // Only unmarshal body if the response was successful
93+ if r.StatusCode() != http.StatusOK {
94+ return nil, fmt.Errorf("unexpected status \"%d\": %s", r.StatusCode(), r.Body())
95+ }
96+
97+ return r.Body(), nil
98+}
pkg/requests/result_test.gonew file+326
@@ -0,0 +1,326 @@
1+package requests
2+
3+import (
4+ "errors"
5+ "net/http"
6+
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/ginkgo/extensions/table"
9+ . "github.com/onsi/gomega"
10+)
11+
12+var _ = Describe("Result suite", func() {
13+ Context("with a result", func() {
14+ type resultTableInput struct {
15+ result Result
16+ expectedError error
17+ expectedStatusCode int
18+ expectedHeaders http.Header
19+ expectedBody []byte
20+ }
21+
22+ DescribeTable("accessors should return expected results",
23+ func(in resultTableInput) {
24+ if in.expectedError != nil {
25+ Expect(in.result.Error()).To(MatchError(in.expectedError))
26+ } else {
27+ Expect(in.result.Error()).To(BeNil())
28+ }
29+
30+ Expect(in.result.StatusCode()).To(Equal(in.expectedStatusCode))
31+ Expect(in.result.Headers()).To(Equal(in.expectedHeaders))
32+ Expect(in.result.Body()).To(Equal(in.expectedBody))
33+ },
34+ Entry("with an empty result", resultTableInput{
35+ result: &result{},
36+ expectedError: nil,
37+ expectedStatusCode: 0,
38+ expectedHeaders: nil,
39+ expectedBody: nil,
40+ }),
41+ Entry("with an error", resultTableInput{
42+ result: &result{
43+ err: errors.New("error"),
44+ },
45+ expectedError: errors.New("error"),
46+ expectedStatusCode: 0,
47+ expectedHeaders: nil,
48+ expectedBody: nil,
49+ }),
50+ Entry("with a response with no headers", resultTableInput{
51+ result: &result{
52+ response: &http.Response{
53+ StatusCode: http.StatusTeapot,
54+ },
55+ },
56+ expectedError: nil,
57+ expectedStatusCode: http.StatusTeapot,
58+ expectedHeaders: nil,
59+ expectedBody: nil,
60+ }),
61+ Entry("with a response with no status code", resultTableInput{
62+ result: &result{
63+ response: &http.Response{
64+ Header: http.Header{
65+ "foo": []string{"bar"},
66+ },
67+ },
68+ },
69+ expectedError: nil,
70+ expectedStatusCode: 0,
71+ expectedHeaders: http.Header{
72+ "foo": []string{"bar"},
73+ },
74+ expectedBody: nil,
75+ }),
76+ Entry("with a response with a body", resultTableInput{
77+ result: &result{
78+ body: []byte("some body"),
79+ },
80+ expectedError: nil,
81+ expectedStatusCode: 0,
82+ expectedHeaders: nil,
83+ expectedBody: []byte("some body"),
84+ }),
85+ Entry("with all fields", resultTableInput{
86+ result: &result{
87+ err: errors.New("some error"),
88+ response: &http.Response{
89+ StatusCode: http.StatusFound,
90+ Header: http.Header{
91+ "header": []string{"value"},
92+ },
93+ },
94+ body: []byte("a body"),
95+ },
96+ expectedError: errors.New("some error"),
97+ expectedStatusCode: http.StatusFound,
98+ expectedHeaders: http.Header{
99+ "header": []string{"value"},
100+ },
101+ expectedBody: []byte("a body"),
102+ }),
103+ )
104+ })
105+
106+ Context("UnmarshalInto", func() {
107+ type testStruct struct {
108+ A string `json:"a"`
109+ B int `json:"b"`
110+ }
111+
112+ type unmarshalIntoTableInput struct {
113+ result Result
114+ expectedErr error
115+ expectedOutput *testStruct
116+ }
117+
118+ DescribeTable("with a result",
119+ func(in unmarshalIntoTableInput) {
120+ input := &testStruct{}
121+ err := in.result.UnmarshalInto(input)
122+ if in.expectedErr != nil {
123+ Expect(err).To(MatchError(in.expectedErr))
124+ } else {
125+ Expect(err).ToNot(HaveOccurred())
126+ }
127+ Expect(input).To(Equal(in.expectedOutput))
128+ },
129+ Entry("with an error", unmarshalIntoTableInput{
130+ result: &result{
131+ err: errors.New("got an error"),
132+ response: &http.Response{
133+ StatusCode: http.StatusOK,
134+ },
135+ body: []byte("{\"a\": \"foo\"}"),
136+ },
137+ expectedErr: errors.New("got an error"),
138+ expectedOutput: &testStruct{},
139+ }),
140+ Entry("with a 409 status code", unmarshalIntoTableInput{
141+ result: &result{
142+ err: nil,
143+ response: &http.Response{
144+ StatusCode: http.StatusConflict,
145+ },
146+ body: []byte("{\"a\": \"foo\"}"),
147+ },
148+ expectedErr: errors.New("unexpected status \"409\": {\"a\": \"foo\"}"),
149+ expectedOutput: &testStruct{},
150+ }),
151+ Entry("when the response has a valid json response", unmarshalIntoTableInput{
152+ result: &result{
153+ err: nil,
154+ response: &http.Response{
155+ StatusCode: http.StatusOK,
156+ },
157+ body: []byte("{\"a\": \"foo\", \"b\": 1}"),
158+ },
159+ expectedErr: nil,
160+ expectedOutput: &testStruct{A: "foo", B: 1},
161+ }),
162+ Entry("when the response body is empty", unmarshalIntoTableInput{
163+ result: &result{
164+ err: nil,
165+ response: &http.Response{
166+ StatusCode: http.StatusOK,
167+ },
168+ body: []byte(""),
169+ },
170+ expectedErr: errors.New("error unmarshalling body: unexpected end of JSON input"),
171+ expectedOutput: &testStruct{},
172+ }),
173+ Entry("when the response body is not json", unmarshalIntoTableInput{
174+ result: &result{
175+ err: nil,
176+ response: &http.Response{
177+ StatusCode: http.StatusOK,
178+ },
179+ body: []byte("not json"),
180+ },
181+ expectedErr: errors.New("error unmarshalling body: invalid character 'o' in literal null (expecting 'u')"),
182+ expectedOutput: &testStruct{},
183+ }),
184+ )
185+ })
186+
187+ Context("UnmarshalJSON", func() {
188+ type testStruct struct {
189+ A string `json:"a"`
190+ B int `json:"b"`
191+ }
192+
193+ type unmarshalJSONTableInput struct {
194+ result Result
195+ expectedErr error
196+ expectedOutput *testStruct
197+ }
198+
199+ DescribeTable("with a result",
200+ func(in unmarshalJSONTableInput) {
201+ j, err := in.result.UnmarshalJSON()
202+ if in.expectedErr != nil {
203+ Expect(err).To(MatchError(in.expectedErr))
204+ Expect(j).To(BeNil())
205+ return
206+ }
207+
208+ // No error so j should not be nil
209+ Expect(err).ToNot(HaveOccurred())
210+
211+ input := &testStruct{
212+ A: j.Get("a").MustString(),
213+ B: j.Get("b").MustInt(),
214+ }
215+ Expect(input).To(Equal(in.expectedOutput))
216+ },
217+ Entry("with an error", unmarshalJSONTableInput{
218+ result: &result{
219+ err: errors.New("got an error"),
220+ response: &http.Response{
221+ StatusCode: http.StatusOK,
222+ },
223+ body: []byte("{\"a\": \"foo\"}"),
224+ },
225+ expectedErr: errors.New("got an error"),
226+ expectedOutput: &testStruct{},
227+ }),
228+ Entry("with a 409 status code", unmarshalJSONTableInput{
229+ result: &result{
230+ err: nil,
231+ response: &http.Response{
232+ StatusCode: http.StatusConflict,
233+ },
234+ body: []byte("{\"a\": \"foo\"}"),
235+ },
236+ expectedErr: errors.New("unexpected status \"409\": {\"a\": \"foo\"}"),
237+ expectedOutput: &testStruct{},
238+ }),
239+ Entry("when the response has a valid json response", unmarshalJSONTableInput{
240+ result: &result{
241+ err: nil,
242+ response: &http.Response{
243+ StatusCode: http.StatusOK,
244+ },
245+ body: []byte("{\"a\": \"foo\", \"b\": 1}"),
246+ },
247+ expectedErr: nil,
248+ expectedOutput: &testStruct{A: "foo", B: 1},
249+ }),
250+ Entry("when the response body is empty", unmarshalJSONTableInput{
251+ result: &result{
252+ err: nil,
253+ response: &http.Response{
254+ StatusCode: http.StatusOK,
255+ },
256+ body: []byte(""),
257+ },
258+ expectedErr: errors.New("error reading json: EOF"),
259+ expectedOutput: &testStruct{},
260+ }),
261+ Entry("when the response body is not json", unmarshalJSONTableInput{
262+ result: &result{
263+ err: nil,
264+ response: &http.Response{
265+ StatusCode: http.StatusOK,
266+ },
267+ body: []byte("not json"),
268+ },
269+ expectedErr: errors.New("error reading json: invalid character 'o' in literal null (expecting 'u')"),
270+ expectedOutput: &testStruct{},
271+ }),
272+ )
273+ })
274+
275+ Context("getBodyForUnmarshal", func() {
276+ type getBodyForUnmarshalTableInput struct {
277+ result *result
278+ expectedErr error
279+ expectedBody []byte
280+ }
281+
282+ DescribeTable("when getting the body", func(in getBodyForUnmarshalTableInput) {
283+ body, err := in.result.getBodyForUnmarshal()
284+ if in.expectedErr != nil {
285+ Expect(err).To(MatchError(in.expectedErr))
286+ } else {
287+ Expect(err).ToNot(HaveOccurred())
288+ }
289+ Expect(body).To(Equal(in.expectedBody))
290+ },
291+ Entry("when the result has an error", getBodyForUnmarshalTableInput{
292+ result: &result{
293+ err: errors.New("got an error"),
294+ response: &http.Response{
295+ StatusCode: http.StatusOK,
296+ },
297+ body: []byte("body"),
298+ },
299+ expectedErr: errors.New("got an error"),
300+ expectedBody: nil,
301+ }),
302+ Entry("when the response has a 409 status code", getBodyForUnmarshalTableInput{
303+ result: &result{
304+ err: nil,
305+ response: &http.Response{
306+ StatusCode: http.StatusConflict,
307+ },
308+ body: []byte("body"),
309+ },
310+ expectedErr: errors.New("unexpected status \"409\": body"),
311+ expectedBody: nil,
312+ }),
313+ Entry("when the response has a 200 status code", getBodyForUnmarshalTableInput{
314+ result: &result{
315+ err: nil,
316+ response: &http.Response{
317+ StatusCode: http.StatusOK,
318+ },
319+ body: []byte("body"),
320+ },
321+ expectedErr: nil,
322+ expectedBody: []byte("body"),
323+ }),
324+ )
325+ })
326+})
pkg/validation/options.go+29−29
@@ -83,34 +83,34 @@ func Validate(o *options.Options) error {
8383
8484 logger.Printf("Performing OIDC Discovery...")
8585
86− if req, err := http.NewRequest("GET", strings.TrimSuffix(o.OIDCIssuerURL, "/")+"/.well-known/openid-configuration", nil); err == nil {
87− if body, err := requests.Request(req); err == nil {
88−
89− // Prefer manually configured URLs. It's a bit unclear
90− // why you'd be doing discovery and also providing the URLs
91− // explicitly though...
92− if o.LoginURL == "" {
93− o.LoginURL = body.Get("authorization_endpoint").MustString()
94− }
95−
96− if o.RedeemURL == "" {
97− o.RedeemURL = body.Get("token_endpoint").MustString()
98− }
99−
100− if o.OIDCJwksURL == "" {
101− o.OIDCJwksURL = body.Get("jwks_uri").MustString()
102− }
103−
104− if o.ProfileURL == "" {
105− o.ProfileURL = body.Get("userinfo_endpoint").MustString()
106− }
107−
108− o.SkipOIDCDiscovery = true
109− } else {
110− logger.Printf("error: failed to discover OIDC configuration: %v", err)
111− }
86+ requestURL := strings.TrimSuffix(o.OIDCIssuerURL, "/") + "/.well-known/openid-configuration"
87+ body, err := requests.New(requestURL).
88+ WithContext(ctx).
89+ Do().
90+ UnmarshalJSON()
91+ if err != nil {
92+ logger.Printf("error: failed to discover OIDC configuration: %v", err)
11293 } else {
113− logger.Printf("error: failed parsing OIDC discovery URL: %v", err)
94+ // Prefer manually configured URLs. It's a bit unclear
95+ // why you'd be doing discovery and also providing the URLs
96+ // explicitly though...
97+ if o.LoginURL == "" {
98+ o.LoginURL = body.Get("authorization_endpoint").MustString()
99+ }
100+
101+ if o.RedeemURL == "" {
102+ o.RedeemURL = body.Get("token_endpoint").MustString()
103+ }
104+
105+ if o.OIDCJwksURL == "" {
106+ o.OIDCJwksURL = body.Get("jwks_uri").MustString()
107+ }
108+
109+ if o.ProfileURL == "" {
110+ o.ProfileURL = body.Get("userinfo_endpoint").MustString()
111+ }
112+
113+ o.SkipOIDCDiscovery = true
114114 }
115115 }
116116
@@ -385,10 +385,10 @@ func newVerifierFromJwtIssuer(jwtIssuer jwtIssuer) (*oidc.IDTokenVerifier, error
385385 if err != nil {
386386 // Try as JWKS URI
387387 jwksURI := strings.TrimSuffix(jwtIssuer.issuerURI, "/") + "/.well-known/jwks.json"
388− _, err := http.NewRequest("GET", jwksURI, nil)
389− if err != nil {
388+ if err := requests.New(jwksURI).Do().Error(); err != nil {
390389 return nil, err
391390 }
391+
392392 verifier = oidc.NewVerifier(jwtIssuer.issuerURI, oidc.NewRemoteKeySet(context.Background(), jwksURI), config)
393393 } else {
394394 verifier = provider.Verifier(config)
providers/azure.go+14−37
@@ -3,10 +3,8 @@ package providers
33import (
44 "bytes"
55 "context"
6− "encoding/json"
76 "errors"
87 "fmt"
9− "io/ioutil"
108 "net/http"
119 "net/url"
1210 "time"
@@ -91,39 +89,22 @@ func (p *AzureProvider) Redeem(ctx context.Context, redirectURL, code string) (s
9189 params.Add("resource", p.ProtectedResource.String())
9290 }
9391
94− var req *http.Request
95− req, err = http.NewRequestWithContext(ctx, "POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))
96− if err != nil {
97− return
98− }
99− req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
100−
101− var resp *http.Response
102− resp, err = http.DefaultClient.Do(req)
103− if err != nil {
104− return nil, err
105− }
106− var body []byte
107− body, err = ioutil.ReadAll(resp.Body)
108− resp.Body.Close()
109− if err != nil {
110− return
111− }
112−
113− if resp.StatusCode != 200 {
114− err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemURL.String(), body)
115− return
116− }
117−
11892 var jsonResponse struct {
11993 AccessToken string `json:"access_token"`
12094 RefreshToken string `json:"refresh_token"`
12195 ExpiresOn int64 `json:"expires_on,string"`
12296 IDToken string `json:"id_token"`
12397 }
124− err = json.Unmarshal(body, &jsonResponse)
98+
99+ err = requests.New(p.RedeemURL.String()).
100+ WithContext(ctx).
101+ WithMethod("POST").
102+ WithBody(bytes.NewBufferString(params.Encode())).
103+ SetHeader("Content-Type", "application/x-www-form-urlencoded").
104+ Do().
105+ UnmarshalInto(&jsonResponse)
125106 if err != nil {
126− return
107+ return nil, err
127108 }
128109
129110 created := time.Now()
@@ -169,26 +150,22 @@ func (p *AzureProvider) GetEmailAddress(ctx context.Context, s *sessions.Session
169150 if s.AccessToken == "" {
170151 return "", errors.New("missing access token")
171152 }
172− req, err := http.NewRequestWithContext(ctx, "GET", p.ProfileURL.String(), nil)
173− if err != nil {
174− return "", err
175− }
176− req.Header = getAzureHeader(s.AccessToken)
177−
178− json, err := requests.Request(req)
179153
154+ json, err := requests.New(p.ProfileURL.String()).
155+ WithContext(ctx).
156+ WithHeaders(getAzureHeader(s.AccessToken)).
157+ Do().
158+ UnmarshalJSON()
180159 if err != nil {
181160 return "", err
182161 }
183162
184163 email, err = getEmailFromJSON(json)
185−
186164 if err == nil && email != "" {
187165 return email, err
188166 }
189167
190168 email, err = json.Get("userPrincipalName").String()
191−
192169 if err != nil {
193170 logger.Printf("failed making request %s", err)
194171 return "", err
providers/bitbucket.go+26−28
@@ -2,7 +2,6 @@ package providers
22
33import (
44 "context"
5− "net/http"
65 "net/url"
76 "strings"
87
@@ -85,15 +84,14 @@ func (p *BitbucketProvider) GetEmailAddress(ctx context.Context, s *sessions.Ses
8584 FullName string `json:"full_name"`
8685 }
8786 }
88− req, err := http.NewRequestWithContext(ctx, "GET",
89− p.ValidateURL.String()+"?access_token="+s.AccessToken, nil)
90− if err != nil {
91− logger.Printf("failed building request %s", err)
92− return "", err
93− }
94− err = requests.RequestJSON(req, &emails)
87+
88+ requestURL := p.ValidateURL.String() + "?access_token=" + s.AccessToken
89+ err := requests.New(requestURL).
90+ WithContext(ctx).
91+ Do().
92+ UnmarshalInto(&emails)
9593 if err != nil {
96− logger.Printf("failed making request %s", err)
94+ logger.Printf("failed making request: %v", err)
9795 return "", err
9896 }
9997
@@ -101,15 +99,15 @@ func (p *BitbucketProvider) GetEmailAddress(ctx context.Context, s *sessions.Ses
10199 teamURL := &url.URL{}
102100 *teamURL = *p.ValidateURL
103101 teamURL.Path = "/2.0/teams"
104− req, err = http.NewRequestWithContext(ctx, "GET",
105− teamURL.String()+"?role=member&access_token="+s.AccessToken, nil)
106− if err != nil {
107− logger.Printf("failed building request %s", err)
108− return "", err
109− }
110− err = requests.RequestJSON(req, &teams)
102+
103+ requestURL := teamURL.String() + "?role=member&access_token=" + s.AccessToken
104+
105+ err := requests.New(requestURL).
106+ WithContext(ctx).
107+ Do().
108+ UnmarshalInto(&teams)
111109 if err != nil {
112− logger.Printf("failed requesting teams membership %s", err)
110+ logger.Printf("failed requesting teams membership: %v", err)
113111 return "", err
114112 }
115113 var found = false
@@ -129,20 +127,20 @@ func (p *BitbucketProvider) GetEmailAddress(ctx context.Context, s *sessions.Ses
129127 repositoriesURL := &url.URL{}
130128 *repositoriesURL = *p.ValidateURL
131129 repositoriesURL.Path = "/2.0/repositories/" + strings.Split(p.Repository, "/")[0]
132− req, err = http.NewRequestWithContext(ctx, "GET",
133− repositoriesURL.String()+"?role=contributor"+
134− "&q=full_name="+url.QueryEscape("\""+p.Repository+"\"")+
135− "&access_token="+s.AccessToken,
136− nil)
137− if err != nil {
138− logger.Printf("failed building request %s", err)
139− return "", err
140− }
141− err = requests.RequestJSON(req, &repositories)
130+
131+ requestURL := repositoriesURL.String() + "?role=contributor" +
132+ "&q=full_name=" + url.QueryEscape("\""+p.Repository+"\"") +
133+ "&access_token=" + s.AccessToken
134+
135+ err := requests.New(requestURL).
136+ WithContext(ctx).
137+ Do().
138+ UnmarshalInto(&repositories)
142139 if err != nil {
143− logger.Printf("failed checking repository access %s", err)
140+ logger.Printf("failed checking repository access: %v", err)
144141 return "", err
145142 }
143+
146144 var found = false
147145 for _, repository := range repositories.Values {
148146 if p.Repository == repository.FullName {
providers/digitalocean.go+5−6
@@ -60,13 +60,12 @@ func (p *DigitalOceanProvider) GetEmailAddress(ctx context.Context, s *sessions.
6060 if s.AccessToken == "" {
6161 return "", errors.New("missing access token")
6262 }
63− req, err := http.NewRequestWithContext(ctx, "GET", p.ProfileURL.String(), nil)
64− if err != nil {
65− return "", err
66− }
67− req.Header = getDigitalOceanHeader(s.AccessToken)
6863
69− json, err := requests.Request(req)
64+ json, err := requests.New(p.ProfileURL.String()).
65+ WithContext(ctx).
66+ WithHeaders(getDigitalOceanHeader(s.AccessToken)).
67+ Do().
68+ UnmarshalJSON()
7069 if err != nil {
7170 return "", err
7271 }
providers/facebook.go+8−6
@@ -62,20 +62,22 @@ func (p *FacebookProvider) GetEmailAddress(ctx context.Context, s *sessions.Sess
6262 if s.AccessToken == "" {
6363 return "", errors.New("missing access token")
6464 }
65− req, err := http.NewRequestWithContext(ctx, "GET", p.ProfileURL.String()+"?fields=name,email", nil)
66− if err != nil {
67− return "", err
68− }
69− req.Header = getFacebookHeader(s.AccessToken)
7065
7166 type result struct {
7267 Email string
7368 }
7469 var r result
75− err = requests.RequestJSON(req, &r)
70+
71+ requestURL := p.ProfileURL.String() + "?fields=name,email"
72+ err := requests.New(requestURL).
73+ WithContext(ctx).
74+ WithHeaders(getFacebookHeader(s.AccessToken)).
75+ Do().
76+ UnmarshalInto(&r)
7677 if err != nil {
7778 return "", err
7879 }
80+
7981 if r.Email == "" {
8082 return "", errors.New("no email")
8183 }
providers/github.go+49−128
@@ -2,10 +2,8 @@ package providers
22
33import (
44 "context"
5− "encoding/json"
65 "errors"
76 "fmt"
8− "io/ioutil"
97 "net/http"
108 "net/url"
119 "path"
@@ -15,6 +13,7 @@ import (
1513
1614 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
1715 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
16+ "github.com/oauth2-proxy/oauth2-proxy/pkg/requests"
1817)
1918
2019// GitHubProvider represents an GitHub based Identity Provider
@@ -111,27 +110,17 @@ func (p *GitHubProvider) hasOrg(ctx context.Context, accessToken string) (bool,
111110 Path: path.Join(p.ValidateURL.Path, "/user/orgs"),
112111 RawQuery: params.Encode(),
113112 }
114− req, _ := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil)
115− req.Header = getGitHubHeader(accessToken)
116− resp, err := http.DefaultClient.Do(req)
117− if err != nil {
118− return false, err
119− }
120113
121− body, err := ioutil.ReadAll(resp.Body)
122− resp.Body.Close()
114+ var op orgsPage
115+ err := requests.New(endpoint.String()).
116+ WithContext(ctx).
117+ WithHeaders(getGitHubHeader(accessToken)).
118+ Do().
119+ UnmarshalInto(&op)
123120 if err != nil {
124121 return false, err
125122 }
126− if resp.StatusCode != 200 {
127− return false, fmt.Errorf(
128− "got %d from %q %s", resp.StatusCode, endpoint.String(), body)
129− }
130123
131− var op orgsPage
132− if err := json.Unmarshal(body, &op); err != nil {
133− return false, err
134− }
135124 if len(op) == 0 {
136125 break
137126 }
@@ -187,11 +176,15 @@ func (p *GitHubProvider) hasOrgAndTeam(ctx context.Context, accessToken string)
187176 RawQuery: params.Encode(),
188177 }
189178
190− req, _ := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil)
191− req.Header = getGitHubHeader(accessToken)
192− resp, err := http.DefaultClient.Do(req)
193− if err != nil {
194− return false, err
179+ // bodyclose cannot detect that the body is being closed later in requests.Into,
180+ // so have to skip the linting for the next line.
181+ // nolint:bodyclose
182+ result := requests.New(endpoint.String()).
183+ WithContext(ctx).
184+ WithHeaders(getGitHubHeader(accessToken)).
185+ Do()
186+ if result.Error() != nil {
187+ return false, result.Error()
195188 }
196189
197190 if last == 0 {
@@ -207,7 +200,7 @@ func (p *GitHubProvider) hasOrgAndTeam(ctx context.Context, accessToken string)
207200 // link header at last page (doesn't exist last info)
208201 // <https://api.github.com/user/teams?page=3&per_page=10>; rel="prev", <https://api.github.com/user/teams?page=1&per_page=10>; rel="first"
209202
210− link := resp.Header.Get("Link")
203+ link := result.Headers().Get("Link")
211204 rep1 := regexp.MustCompile(`(?s).*\<https://api.github.com/user/teams\?page=(.)&per_page=[0-9]+\>; rel="last".*`)
212205 i, converr := strconv.Atoi(rep1.ReplaceAllString(link, "$1"))
213206
@@ -217,21 +210,9 @@ func (p *GitHubProvider) hasOrgAndTeam(ctx context.Context, accessToken string)
217210 }
218211 }
219212
220− body, err := ioutil.ReadAll(resp.Body)
221− if err != nil {
222− resp.Body.Close()
223− return false, err
224− }
225− resp.Body.Close()
226−
227− if resp.StatusCode != 200 {
228− return false, fmt.Errorf(
229− "got %d from %q %s", resp.StatusCode, endpoint.String(), body)
230− }
231−
232213 var tp teamsPage
233− if err := json.Unmarshal(body, &tp); err != nil {
234− return false, fmt.Errorf("%s unmarshaling %s", err, body)
214+ if err := result.UnmarshalInto(&tp); err != nil {
215+ return false, err
235216 }
236217 if len(tp) == 0 {
237218 break
@@ -297,25 +278,13 @@ func (p *GitHubProvider) hasRepo(ctx context.Context, accessToken string) (bool,
297278 Path: path.Join(p.ValidateURL.Path, "/repo/", p.Repo),
298279 }
299280
300− req, _ := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil)
301− req.Header = getGitHubHeader(accessToken)
302− resp, err := http.DefaultClient.Do(req)
303− if err != nil {
304− return false, err
305− }
306−
307− body, err := ioutil.ReadAll(resp.Body)
308− resp.Body.Close()
309− if err != nil {
310− return false, err
311− }
312− if resp.StatusCode != 200 {
313− return false, fmt.Errorf(
314− "got %d from %q %s", resp.StatusCode, endpoint.String(), body)
315− }
316−
317281 var repo repository
318− if err := json.Unmarshal(body, &repo); err != nil {
282+ err := requests.New(endpoint.String()).
283+ WithContext(ctx).
284+ WithHeaders(getGitHubHeader(accessToken)).
285+ Do().
286+ UnmarshalInto(&repo)
287+ if err != nil {
319288 return false, err
320289 }
321290
@@ -337,26 +306,15 @@ func (p *GitHubProvider) hasUser(ctx context.Context, accessToken string) (bool,
337306 Host: p.ValidateURL.Host,
338307 Path: path.Join(p.ValidateURL.Path, "/user"),
339308 }
340− req, _ := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil)
341− req.Header = getGitHubHeader(accessToken)
342− resp, err := http.DefaultClient.Do(req)
343− if err != nil {
344− return false, err
345− }
346− defer resp.Body.Close()
347309
348− body, err := ioutil.ReadAll(resp.Body)
310+ err := requests.New(endpoint.String()).
311+ WithContext(ctx).
312+ WithHeaders(getGitHubHeader(accessToken)).
313+ Do().
314+ UnmarshalInto(&user)
349315 if err != nil {
350316 return false, err
351317 }
352− if resp.StatusCode != 200 {
353− return false, fmt.Errorf("got %d from %q %s",
354− resp.StatusCode, stripToken(endpoint.String()), body)
355− }
356−
357− if err := json.Unmarshal(body, &user); err != nil {
358− return false, err
359− }
360318
361319 if p.isVerifiedUser(user.Login) {
362320 return true, nil
@@ -372,24 +330,20 @@ func (p *GitHubProvider) isCollaborator(ctx context.Context, username, accessTok
372330 Host: p.ValidateURL.Host,
373331 Path: path.Join(p.ValidateURL.Path, "/repos/", p.Repo, "/collaborators/", username),
374332 }
375− req, _ := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil)
376− req.Header = getGitHubHeader(accessToken)
377− resp, err := http.DefaultClient.Do(req)
378− if err != nil {
379− return false, err
380− }
381− body, err := ioutil.ReadAll(resp.Body)
382− resp.Body.Close()
383− if err != nil {
384− return false, err
333+ result := requests.New(endpoint.String()).
334+ WithContext(ctx).
335+ WithHeaders(getGitHubHeader(accessToken)).
336+ Do()
337+ if result.Error() != nil {
338+ return false, result.Error()
385339 }
386340
387− if resp.StatusCode != 204 {
341+ if result.StatusCode() != 204 {
388342 return false, fmt.Errorf("got %d from %q %s",
389− resp.StatusCode, endpoint.String(), body)
343+ result.StatusCode(), endpoint.String(), result.Body())
390344 }
391345
392− logger.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body)
346+ logger.Printf("got %d from %q %s", result.StatusCode(), endpoint.String(), result.Body())
393347
394348 return true, nil
395349}
@@ -440,28 +394,14 @@ func (p *GitHubProvider) GetEmailAddress(ctx context.Context, s *sessions.Sessio
440394 Host: p.ValidateURL.Host,
441395 Path: path.Join(p.ValidateURL.Path, "/user/emails"),
442396 }
443− req, _ := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil)
444− req.Header = getGitHubHeader(s.AccessToken)
445− resp, err := http.DefaultClient.Do(req)
397+ err := requests.New(endpoint.String()).
398+ WithContext(ctx).
399+ WithHeaders(getGitHubHeader(s.AccessToken)).
400+ Do().
401+ UnmarshalInto(&emails)
446402 if err != nil {
447403 return "", err
448404 }
449− body, err := ioutil.ReadAll(resp.Body)
450− resp.Body.Close()
451− if err != nil {
452− return "", err
453− }
454−
455− if resp.StatusCode != 200 {
456− return "", fmt.Errorf("got %d from %q %s",
457− resp.StatusCode, endpoint.String(), body)
458− }
459−
460− logger.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body)
461−
462− if err := json.Unmarshal(body, &emails); err != nil {
463− return "", fmt.Errorf("%s unmarshaling %s", err, body)
464− }
465405
466406 returnEmail := ""
467407 for _, email := range emails {
@@ -489,34 +429,15 @@ func (p *GitHubProvider) GetUserName(ctx context.Context, s *sessions.SessionSta
489429 Path: path.Join(p.ValidateURL.Path, "/user"),
490430 }
491431
492− req, err := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil)
493− if err != nil {
494− return "", fmt.Errorf("could not create new GET request: %v", err)
495− }
496−
497− req.Header = getGitHubHeader(s.AccessToken)
498− resp, err := http.DefaultClient.Do(req)
432+ err := requests.New(endpoint.String()).
433+ WithContext(ctx).
434+ WithHeaders(getGitHubHeader(s.AccessToken)).
435+ Do().
436+ UnmarshalInto(&user)
499437 if err != nil {
500438 return "", err
501439 }
502440
503− body, err := ioutil.ReadAll(resp.Body)
504− defer resp.Body.Close()
505− if err != nil {
506− return "", err
507− }
508−
509− if resp.StatusCode != 200 {
510− return "", fmt.Errorf("got %d from %q %s",
511− resp.StatusCode, endpoint.String(), body)
512− }
513−
514− logger.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body)
515−
516− if err := json.Unmarshal(body, &user); err != nil {
517− return "", fmt.Errorf("%s unmarshaling %s", err, body)
518− }
519−
520441 // Now that we have the username we can check collaborator status
521442 if !p.isVerifiedUser(user.Login) && p.Org == "" && p.Repo != "" && p.Token != "" {
522443 if ok, err := p.isCollaborator(ctx, user.Login, p.Token); err != nil || !ok {
providers/gitlab.go+7−26
@@ -2,15 +2,13 @@ package providers
22
33import (
44 "context"
5− "encoding/json"
65 "fmt"
7− "io/ioutil"
8− "net/http"
96 "strings"
107 "time"
118
129 oidc "github.com/coreos/go-oidc"
1310 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
11+ "github.com/oauth2-proxy/oauth2-proxy/pkg/requests"
1412 "golang.org/x/oauth2"
1513)
1614
@@ -131,31 +129,14 @@ func (p *GitLabProvider) getUserInfo(ctx context.Context, s *sessions.SessionSta
131129 userInfoURL := *p.LoginURL
132130 userInfoURL.Path = "/oauth/userinfo"
133131
134− req, err := http.NewRequestWithContext(ctx, "GET", userInfoURL.String(), nil)
135− if err != nil {
136− return nil, fmt.Errorf("failed to create user info request: %v", err)
137− }
138− req.Header.Set("Authorization", "Bearer "+s.AccessToken)
139−
140− resp, err := http.DefaultClient.Do(req)
141− if err != nil {
142− return nil, fmt.Errorf("failed to perform user info request: %v", err)
143− }
144− var body []byte
145− body, err = ioutil.ReadAll(resp.Body)
146− resp.Body.Close()
147− if err != nil {
148− return nil, fmt.Errorf("failed to read user info response: %v", err)
149− }
150−
151− if resp.StatusCode != 200 {
152− return nil, fmt.Errorf("got %d during user info request: %s", resp.StatusCode, body)
153− }
154−
155132 var userInfo gitlabUserInfo
156− err = json.Unmarshal(body, &userInfo)
133+ err := requests.New(userInfoURL.String()).
134+ WithContext(ctx).
135+ SetHeader("Authorization", "Bearer "+s.AccessToken).
136+ Do().
137+ UnmarshalInto(&userInfo)
157138 if err != nil {
158− return nil, fmt.Errorf("failed to parse user info: %v", err)
139+ return nil, fmt.Errorf("error getting user info: %v", err)
159140 }
160141
161142 return &userInfo, nil
providers/google.go+21−49
@@ -9,13 +9,13 @@ import (
99 "fmt"
1010 "io"
1111 "io/ioutil"
12− "net/http"
1312 "net/url"
1413 "strings"
1514 "time"
1615
1716 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
1817 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
18+ "github.com/oauth2-proxy/oauth2-proxy/pkg/requests"
1919 "golang.org/x/oauth2/google"
2020 admin "google.golang.org/api/admin/directory/v1"
2121 "google.golang.org/api/googleapi"
@@ -116,28 +116,6 @@ func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code string) (
116116 params.Add("client_secret", clientSecret)
117117 params.Add("code", code)
118118 params.Add("grant_type", "authorization_code")
119− var req *http.Request
120− req, err = http.NewRequestWithContext(ctx, "POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))
121− if err != nil {
122− return
123− }
124− req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
125−
126− resp, err := http.DefaultClient.Do(req)
127− if err != nil {
128− return
129− }
130− var body []byte
131− body, err = ioutil.ReadAll(resp.Body)
132− resp.Body.Close()
133− if err != nil {
134− return
135− }
136−
137− if resp.StatusCode != 200 {
138− err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemURL.String(), body)
139− return
140− }
141119
142120 var jsonResponse struct {
143121 AccessToken string `json:"access_token"`
@@ -145,10 +123,18 @@ func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code string) (
145123 ExpiresIn int64 `json:"expires_in"`
146124 IDToken string `json:"id_token"`
147125 }
148− err = json.Unmarshal(body, &jsonResponse)
126+
127+ err = requests.New(p.RedeemURL.String()).
128+ WithContext(ctx).
129+ WithMethod("POST").
130+ WithBody(bytes.NewBufferString(params.Encode())).
131+ SetHeader("Content-Type", "application/x-www-form-urlencoded").
132+ Do().
133+ UnmarshalInto(&jsonResponse)
149134 if err != nil {
150− return
135+ return nil, err
151136 }
137+
152138 c, err := claimsFromIDToken(jsonResponse.IDToken)
153139 if err != nil {
154140 return
@@ -283,38 +269,24 @@ func (p *GoogleProvider) redeemRefreshToken(ctx context.Context, refreshToken st
283269 params.Add("client_secret", clientSecret)
284270 params.Add("refresh_token", refreshToken)
285271 params.Add("grant_type", "refresh_token")
286− var req *http.Request
287− req, err = http.NewRequestWithContext(ctx, "POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))
288− if err != nil {
289− return
290− }
291− req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
292−
293− resp, err := http.DefaultClient.Do(req)
294− if err != nil {
295− return
296− }
297− var body []byte
298− body, err = ioutil.ReadAll(resp.Body)
299− resp.Body.Close()
300− if err != nil {
301− return
302− }
303−
304− if resp.StatusCode != 200 {
305− err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemURL.String(), body)
306− return
307− }
308272
309273 var data struct {
310274 AccessToken string `json:"access_token"`
311275 ExpiresIn int64 `json:"expires_in"`
312276 IDToken string `json:"id_token"`
313277 }
314− err = json.Unmarshal(body, &data)
278+
279+ err = requests.New(p.RedeemURL.String()).
280+ WithContext(ctx).
281+ WithMethod("POST").
282+ WithBody(bytes.NewBufferString(params.Encode())).
283+ SetHeader("Content-Type", "application/x-www-form-urlencoded").
284+ Do().
285+ UnmarshalInto(&data)
315286 if err != nil {
316− return
287+ return "", "", 0, err
317288 }
289+
318290 token = data.AccessToken
319291 idToken = data.IDToken
320292 expires = time.Duration(data.ExpiresIn) * time.Second
providers/internal_util.go+10−9
@@ -2,7 +2,6 @@ package providers
22
33import (
44 "context"
5− "io/ioutil"
65 "net/http"
76 "net/url"
87
@@ -56,20 +55,22 @@ func validateToken(ctx context.Context, p Provider, accessToken string, header h
5655 params := url.Values{"access_token": {accessToken}}
5756 endpoint = endpoint + "?" + params.Encode()
5857 }
59− resp, err := requests.RequestUnparsedResponse(ctx, endpoint, header)
60− if err != nil {
58+
59+ result := requests.New(endpoint).
60+ WithContext(ctx).
61+ WithHeaders(header).
62+ Do()
63+ if result.Error() != nil {
6164 logger.Printf("GET %s", stripToken(endpoint))
62− logger.Printf("token validation request failed: %s", err)
65+ logger.Printf("token validation request failed: %s", result.Error())
6366 return false
6467 }
6568
66− body, _ := ioutil.ReadAll(resp.Body)
67− resp.Body.Close()
68− logger.Printf("%d GET %s %s", resp.StatusCode, stripToken(endpoint), body)
69+ logger.Printf("%d GET %s %s", result.StatusCode(), stripToken(endpoint), result.Body())
6970
70− if resp.StatusCode == 200 {
71+ if result.StatusCode() == 200 {
7172 return true
7273 }
73− logger.Printf("token validation request failed: status %d - %s", resp.StatusCode, body)
74+ logger.Printf("token validation request failed: status %d - %s", result.StatusCode(), result.Body())
7475 return false
7576}
providers/keycloak.go+5−9
@@ -2,7 +2,6 @@ package providers
22
33import (
44 "context"
5− "net/http"
65 "net/url"
76
87 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
@@ -51,14 +50,11 @@ func (p *KeycloakProvider) SetGroup(group string) {
5150}
5251
5352func (p *KeycloakProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
54−
55− req, err := http.NewRequestWithContext(ctx, "GET", p.ValidateURL.String(), nil)
56− req.Header.Set("Authorization", "Bearer "+s.AccessToken)
57− if err != nil {
58− logger.Printf("failed building request %s", err)
59− return "", err
60− }
61− json, err := requests.Request(req)
53+ json, err := requests.New(p.ValidateURL.String()).
54+ WithContext(ctx).
55+ SetHeader("Authorization", "Bearer "+s.AccessToken).
56+ Do().
57+ UnmarshalJSON()
6258 if err != nil {
6359 logger.Printf("failed making request %s", err)
6460 return "", err
providers/linkedin.go+6−6
@@ -58,13 +58,13 @@ func (p *LinkedInProvider) GetEmailAddress(ctx context.Context, s *sessions.Sess
5858 if s.AccessToken == "" {
5959 return "", errors.New("missing access token")
6060 }
61− req, err := http.NewRequestWithContext(ctx, "GET", p.ProfileURL.String()+"?format=json", nil)
62− if err != nil {
63− return "", err
64− }
65− req.Header = getLinkedInHeader(s.AccessToken)
6661
67− json, err := requests.Request(req)
62+ requestURL := p.ProfileURL.String() + "?format=json"
63+ json, err := requests.New(requestURL).
64+ WithContext(ctx).
65+ WithHeaders(getLinkedInHeader(s.AccessToken)).
66+ Do().
67+ UnmarshalJSON()
6868 if err != nil {
6969 return "", err
7070 }
providers/logingov.go+26−60
@@ -15,6 +15,7 @@ import (
1515
1616 "github.com/dgrijalva/jwt-go"
1717 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
18+ "github.com/oauth2-proxy/oauth2-proxy/pkg/requests"
1819 "gopkg.in/square/go-jose.v2"
1920)
2021
@@ -128,51 +129,34 @@ func checkNonce(idToken string, p *LoginGovProvider) (err error) {
128129 return
129130}
130131
131−func emailFromUserInfo(ctx context.Context, accessToken string, userInfoEndpoint string) (email string, err error) {
132− // query the user info endpoint for user attributes
133− var req *http.Request
134− req, err = http.NewRequestWithContext(ctx, "GET", userInfoEndpoint, nil)
135− if err != nil {
136− return
137− }
138− req.Header.Set("Authorization", "Bearer "+accessToken)
139−
140− resp, err := http.DefaultClient.Do(req)
141− if err != nil {
142− return
143− }
144− var body []byte
145− body, err = ioutil.ReadAll(resp.Body)
146− resp.Body.Close()
147− if err != nil {
148− return
149− }
150−
151− if resp.StatusCode != 200 {
152− err = fmt.Errorf("got %d from %q %s", resp.StatusCode, userInfoEndpoint, body)
153− return
154− }
155−
132+func emailFromUserInfo(ctx context.Context, accessToken string, userInfoEndpoint string) (string, error) {
156133 // parse the user attributes from the data we got and make sure that
157134 // the email address has been validated.
158135 var emailData struct {
159136 Email string `json:"email"`
160137 EmailVerified bool `json:"email_verified"`
161138 }
162− err = json.Unmarshal(body, &emailData)
139+
140+ // query the user info endpoint for user attributes
141+ err := requests.New(userInfoEndpoint).
142+ WithContext(ctx).
143+ SetHeader("Authorization", "Bearer "+accessToken).
144+ Do().
145+ UnmarshalInto(&emailData)
163146 if err != nil {
164− return
147+ return "", err
165148 }
166− if emailData.Email == "" {
167− err = fmt.Errorf("missing email")
168− return
149+
150+ email := emailData.Email
151+ if email == "" {
152+ return "", fmt.Errorf("missing email")
169153 }
170− email = emailData.Email
154+
171155 if !emailData.EmailVerified {
172− err = fmt.Errorf("email %s not listed as verified", email)
173− return
156+ return "", fmt.Errorf("email %s not listed as verified", email)
174157 }
175− return
158+
159+ return email, nil
176160}
177161
178162// Redeem exchanges the OAuth2 authentication token for an ID token
@@ -201,30 +185,6 @@ func (p *LoginGovProvider) Redeem(ctx context.Context, redirectURL, code string)
201185 params.Add("code", code)
202186 params.Add("grant_type", "authorization_code")
203187
204− var req *http.Request
205− req, err = http.NewRequestWithContext(ctx, "POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))
206− if err != nil {
207− return
208− }
209− req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
210−
211− var resp *http.Response
212− resp, err = http.DefaultClient.Do(req)
213− if err != nil {
214− return nil, err
215− }
216− var body []byte
217− body, err = ioutil.ReadAll(resp.Body)
218− resp.Body.Close()
219− if err != nil {
220− return
221− }
222−
223− if resp.StatusCode != 200 {
224− err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemURL.String(), body)
225− return
226− }
227−
228188 // Get the token from the body that we got from the token endpoint.
229189 var jsonResponse struct {
230190 AccessToken string `json:"access_token"`
@@ -232,9 +192,15 @@ func (p *LoginGovProvider) Redeem(ctx context.Context, redirectURL, code string)
232192 TokenType string `json:"token_type"`
233193 ExpiresIn int64 `json:"expires_in"`
234194 }
235− err = json.Unmarshal(body, &jsonResponse)
195+ err = requests.New(p.RedeemURL.String()).
196+ WithContext(ctx).
197+ WithMethod("POST").
198+ WithBody(bytes.NewBufferString(params.Encode())).
199+ SetHeader("Content-Type", "application/x-www-form-urlencoded").
200+ Do().
201+ UnmarshalInto(&jsonResponse)
236202 if err != nil {
237− return
203+ return nil, err
238204 }
239205
240206 // check nonce here
providers/nextcloud.go+7−11
@@ -6,7 +6,6 @@ import (
66 "net/http"
77
88 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
9− "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
109 "github.com/oauth2-proxy/oauth2-proxy/pkg/requests"
1110)
1211
@@ -31,18 +30,15 @@ func getNextcloudHeader(accessToken string) http.Header {
3130
3231// GetEmailAddress returns the Account email address
3332func (p *NextcloudProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
34− req, err := http.NewRequestWithContext(ctx, "GET",
35− p.ValidateURL.String(), nil)
33+ json, err := requests.New(p.ValidateURL.String()).
34+ WithContext(ctx).
35+ WithHeaders(getNextcloudHeader(s.AccessToken)).
36+ Do().
37+ UnmarshalJSON()
3638 if err != nil {
37− logger.Printf("failed building request %s", err)
38− return "", err
39− }
40− req.Header = getNextcloudHeader(s.AccessToken)
41− json, err := requests.Request(req)
42− if err != nil {
43− logger.Printf("failed making request %s", err)
44− return "", err
39+ return "", fmt.Errorf("error making request: %v", err)
4540 }
41+
4642 email, err := json.Get("ocs").Get("data").Get("email").String()
4743 return email, err
4844}
providers/oidc.go+5−7
@@ -256,13 +256,11 @@ func (p *OIDCProvider) findClaimsFromIDToken(ctx context.Context, idToken *oidc.
256256 // If the userinfo endpoint profileURL is defined, then there is a chance the userinfo
257257 // contents at the profileURL contains the email.
258258 // Make a query to the userinfo endpoint, and attempt to locate the email from there.
259− req, err := http.NewRequestWithContext(ctx, "GET", profileURL, nil)
260− if err != nil {
261− return nil, err
262− }
263− req.Header = getOIDCHeader(accessToken)
264−
265− respJSON, err := requests.Request(req)
259+ respJSON, err := requests.New(profileURL).
260+ WithContext(ctx).
261+ WithHeaders(getOIDCHeader(accessToken)).
262+ Do().
263+ UnmarshalJSON()
266264 if err != nil {
267265 return nil, err
268266 }
providers/provider_default.go+12−28
@@ -3,17 +3,15 @@ package providers
33import (
44 "bytes"
55 "context"
6− "encoding/json"
76 "errors"
87 "fmt"
9− "io/ioutil"
10− "net/http"
118 "net/url"
129 "time"
1310
1411 "github.com/coreos/go-oidc"
1512
1613 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
14+ "github.com/oauth2-proxy/oauth2-proxy/pkg/requests"
1715)
1816
1917var _ Provider = (*ProviderData)(nil)
@@ -39,35 +37,21 @@ func (p *ProviderData) Redeem(ctx context.Context, redirectURL, code string) (s
3937 params.Add("resource", p.ProtectedResource.String())
4038 }
4139
42− var req *http.Request
43− req, err = http.NewRequestWithContext(ctx, "POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))
44− if err != nil {
45− return
46− }
47− req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
48−
49− var resp *http.Response
50− resp, err = http.DefaultClient.Do(req)
51− if err != nil {
52− return nil, err
53− }
54− var body []byte
55− body, err = ioutil.ReadAll(resp.Body)
56− resp.Body.Close()
57− if err != nil {
58− return
59− }
60−
61− if resp.StatusCode != 200 {
62− err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemURL.String(), body)
63− return
40+ result := requests.New(p.RedeemURL.String()).
41+ WithContext(ctx).
42+ WithMethod("POST").
43+ WithBody(bytes.NewBufferString(params.Encode())).
44+ SetHeader("Content-Type", "application/x-www-form-urlencoded").
45+ Do()
46+ if result.Error() != nil {
47+ return nil, result.Error()
6448 }
6549
6650 // blindly try json and x-www-form-urlencoded
6751 var jsonResponse struct {
6852 AccessToken string `json:"access_token"`
6953 }
70− err = json.Unmarshal(body, &jsonResponse)
54+ err = result.UnmarshalInto(&jsonResponse)
7155 if err == nil {
7256 s = &sessions.SessionState{
7357 AccessToken: jsonResponse.AccessToken,
@@ -76,7 +60,7 @@ func (p *ProviderData) Redeem(ctx context.Context, redirectURL, code string) (s
7660 }
7761
7862 var v url.Values
79− v, err = url.ParseQuery(string(body))
63+ v, err = url.ParseQuery(string(result.Body()))
8064 if err != nil {
8165 return
8266 }
@@ -84,7 +68,7 @@ func (p *ProviderData) Redeem(ctx context.Context, redirectURL, code string) (s
8468 created := time.Now()
8569 s = &sessions.SessionState{AccessToken: a, CreatedAt: &created}
8670 } else {
87− err = fmt.Errorf("no access token found %s", body)
71+ err = fmt.Errorf("no access token found %s", result.Body())
8872 }
8973 return
9074}