git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add header injector middlewares
commit6743e3991d4a0da3b40ad124877fabfa3234b7a5 authorJoel Speed <joel.speed@hotmail.co.uk> dateSun 26 Jul 2020, 04:50 (6 years ago) parentfc2ff19
2 files changed+507
pkg/middleware/headers.go+102 pkg/middleware/headers_test.go+405
pkg/middleware/headers.gonew file+102
@@ -0,0 +1,102 @@
1+package middleware
2+
3+import (
4+ "fmt"
5+ "net/http"
6+
7+ "github.com/justinas/alice"
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/header"
10+)
11+
12+func NewRequestHeaderInjector(headers []options.Header) (alice.Constructor, error) {
13+ headerInjector, err := newRequestHeaderInjector(headers)
14+ if err != nil {
15+ return nil, fmt.Errorf("error building request header injector: %v", err)
16+ }
17+
18+ strip := newStripHeaders(headers)
19+ if strip != nil {
20+ return alice.New(strip, headerInjector).Then, nil
21+ }
22+ return headerInjector, nil
23+}
24+
25+func newStripHeaders(headers []options.Header) alice.Constructor {
26+ headersToStrip := []string{}
27+ for _, header := range headers {
28+ if !header.PreserveRequestValue {
29+ headersToStrip = append(headersToStrip, header.Name)
30+ }
31+ }
32+
33+ if len(headersToStrip) == 0 {
34+ return nil
35+ }
36+
37+ return func(next http.Handler) http.Handler {
38+ return stripHeaders(headersToStrip, next)
39+ }
40+}
41+
42+func stripHeaders(headers []string, next http.Handler) http.Handler {
43+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
44+ for _, header := range headers {
45+ req.Header.Del(header)
46+ }
47+ next.ServeHTTP(rw, req)
48+ })
49+}
50+
51+func newRequestHeaderInjector(headers []options.Header) (alice.Constructor, error) {
52+ injector, err := header.NewInjector(headers)
53+ if err != nil {
54+ return nil, fmt.Errorf("error building request injector: %v", err)
55+ }
56+
57+ return func(next http.Handler) http.Handler {
58+ return injectRequestHeaders(injector, next)
59+ }, nil
60+}
61+
62+func injectRequestHeaders(injector header.Injector, next http.Handler) http.Handler {
63+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
64+ scope := GetRequestScope(req)
65+
66+ // If scope is nil, this will panic.
67+ // A scope should always be injected before this handler is called.
68+ injector.Inject(req.Header, scope.Session)
69+ next.ServeHTTP(rw, req)
70+ })
71+}
72+
73+func NewResponseHeaderInjector(headers []options.Header) (alice.Constructor, error) {
74+ headerInjector, err := newResponseHeaderInjector(headers)
75+ if err != nil {
76+ return nil, fmt.Errorf("error building response header injector: %v", err)
77+ }
78+
79+ return headerInjector, nil
80+}
81+
82+func newResponseHeaderInjector(headers []options.Header) (alice.Constructor, error) {
83+ injector, err := header.NewInjector(headers)
84+ if err != nil {
85+ return nil, fmt.Errorf("error building response injector: %v", err)
86+ }
87+
88+ return func(next http.Handler) http.Handler {
89+ return injectResponseHeaders(injector, next)
90+ }, nil
91+}
92+
93+func injectResponseHeaders(injector header.Injector, next http.Handler) http.Handler {
94+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
95+ scope := GetRequestScope(req)
96+
97+ // If scope is nil, this will panic.
98+ // A scope should always be injected before this handler is called.
99+ injector.Inject(rw.Header(), scope.Session)
100+ next.ServeHTTP(rw, req)
101+ })
102+}
pkg/middleware/headers_test.gonew file+405
@@ -0,0 +1,405 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "encoding/base64"
6+ "net/http"
7+ "net/http/httptest"
8+
9+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
11+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12+ . "github.com/onsi/ginkgo"
13+ . "github.com/onsi/ginkgo/extensions/table"
14+ . "github.com/onsi/gomega"
15+)
16+
17+var _ = Describe("Headers Suite", func() {
18+ type headersTableInput struct {
19+ headers []options.Header
20+ initialHeaders http.Header
21+ session *sessionsapi.SessionState
22+ expectedHeaders http.Header
23+ expectedErr string
24+ }
25+
26+ DescribeTable("the request header injector",
27+ func(in headersTableInput) {
28+ scope := &middlewareapi.RequestScope{
29+ Session: in.session,
30+ }
31+
32+ // Set up the request with a request scope
33+ req := httptest.NewRequest("", "/", nil)
34+ contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
35+ req = req.WithContext(contextWithScope)
36+ req.Header = in.initialHeaders.Clone()
37+
38+ rw := httptest.NewRecorder()
39+
40+ // Create the handler with a next handler that will capture the headers
41+ // from the request
42+ var gotHeaders http.Header
43+ injector, err := NewRequestHeaderInjector(in.headers)
44+ if in.expectedErr != "" {
45+ Expect(err).To(MatchError(in.expectedErr))
46+ return
47+ }
48+ Expect(err).ToNot(HaveOccurred())
49+
50+ handler := injector(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
51+ gotHeaders = r.Header.Clone()
52+ }))
53+ handler.ServeHTTP(rw, req)
54+
55+ Expect(gotHeaders).To(Equal(in.expectedHeaders))
56+ },
57+ Entry("with no configured headers", headersTableInput{
58+ headers: []options.Header{},
59+ initialHeaders: http.Header{
60+ "foo": []string{"bar", "baz"},
61+ },
62+ session: &sessionsapi.SessionState{},
63+ expectedHeaders: http.Header{
64+ "foo": []string{"bar", "baz"},
65+ },
66+ expectedErr: "",
67+ }),
68+ Entry("with a claim valued header", headersTableInput{
69+ headers: []options.Header{
70+ {
71+ Name: "Claim",
72+ Values: []options.HeaderValue{
73+ {
74+ ClaimSource: &options.ClaimSource{
75+ Claim: "id_token",
76+ },
77+ },
78+ },
79+ },
80+ },
81+ initialHeaders: http.Header{
82+ "foo": []string{"bar", "baz"},
83+ },
84+ session: &sessionsapi.SessionState{
85+ IDToken: "IDToken-1234",
86+ },
87+ expectedHeaders: http.Header{
88+ "foo": []string{"bar", "baz"},
89+ "Claim": []string{"IDToken-1234"},
90+ },
91+ expectedErr: "",
92+ }),
93+ Entry("with a claim valued header (without preservation)", headersTableInput{
94+ headers: []options.Header{
95+ {
96+ Name: "Claim",
97+ Values: []options.HeaderValue{
98+ {
99+ ClaimSource: &options.ClaimSource{
100+ Claim: "id_token",
101+ },
102+ },
103+ },
104+ },
105+ },
106+ initialHeaders: http.Header{
107+ "Claim": []string{"bar", "baz"},
108+ },
109+ session: &sessionsapi.SessionState{
110+ IDToken: "IDToken-1234",
111+ },
112+ expectedHeaders: http.Header{
113+ "Claim": []string{"IDToken-1234"},
114+ },
115+ expectedErr: "",
116+ }),
117+ Entry("with a claim valued header (with preservation)", headersTableInput{
118+ headers: []options.Header{
119+ {
120+ Name: "Claim",
121+ PreserveRequestValue: true,
122+ Values: []options.HeaderValue{
123+ {
124+ ClaimSource: &options.ClaimSource{
125+ Claim: "id_token",
126+ },
127+ },
128+ },
129+ },
130+ },
131+ initialHeaders: http.Header{
132+ "Claim": []string{"bar", "baz"},
133+ },
134+ session: &sessionsapi.SessionState{
135+ IDToken: "IDToken-1234",
136+ },
137+ expectedHeaders: http.Header{
138+ "Claim": []string{"bar", "baz", "IDToken-1234"},
139+ },
140+ expectedErr: "",
141+ }),
142+ Entry("with a claim valued header that's not present (without preservation)", headersTableInput{
143+ headers: []options.Header{
144+ {
145+ Name: "Claim",
146+ Values: []options.HeaderValue{
147+ {
148+ ClaimSource: &options.ClaimSource{
149+ Claim: "id_token",
150+ },
151+ },
152+ },
153+ },
154+ },
155+ initialHeaders: http.Header{
156+ "Claim": []string{"bar", "baz"},
157+ },
158+ session: nil,
159+ expectedHeaders: http.Header{},
160+ expectedErr: "",
161+ }),
162+ Entry("with a claim valued header that's not present (with preservation)", headersTableInput{
163+ headers: []options.Header{
164+ {
165+ Name: "Claim",
166+ PreserveRequestValue: true,
167+ Values: []options.HeaderValue{
168+ {
169+ ClaimSource: &options.ClaimSource{
170+ Claim: "id_token",
171+ },
172+ },
173+ },
174+ },
175+ },
176+ initialHeaders: http.Header{
177+ "Claim": []string{"bar", "baz"},
178+ },
179+ session: nil,
180+ expectedHeaders: http.Header{
181+ "Claim": []string{"bar", "baz"},
182+ },
183+ expectedErr: "",
184+ }),
185+ Entry("with an invalid basicAuthPassword claim valued header", headersTableInput{
186+ headers: []options.Header{
187+ {
188+ Name: "X-Auth-Request-Authorization",
189+ Values: []options.HeaderValue{
190+ {
191+ ClaimSource: &options.ClaimSource{
192+ Claim: "user",
193+ BasicAuthPassword: &options.SecretSource{
194+ Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))),
195+ FromEnv: "SECRET_ENV",
196+ },
197+ },
198+ },
199+ },
200+ },
201+ },
202+ initialHeaders: http.Header{
203+ "foo": []string{"bar", "baz"},
204+ },
205+ session: &sessionsapi.SessionState{
206+ User: "user-123",
207+ },
208+ expectedHeaders: nil,
209+ expectedErr: "error building request header injector: error building request injector: error building injector for header \"X-Auth-Request-Authorization\": error loading basicAuthPassword: secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile",
210+ }),
211+ )
212+
213+ DescribeTable("the response header injector",
214+ func(in headersTableInput) {
215+ scope := &middlewareapi.RequestScope{
216+ Session: in.session,
217+ }
218+
219+ // Set up the request with a request scope
220+ req := httptest.NewRequest("", "/", nil)
221+ contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
222+ req = req.WithContext(contextWithScope)
223+
224+ rw := httptest.NewRecorder()
225+ for key, values := range in.initialHeaders {
226+ for _, value := range values {
227+ rw.Header().Add(key, value)
228+ }
229+ }
230+
231+ // Create the handler with a next handler that will capture the headers
232+ // from the request
233+ var gotHeaders http.Header
234+ injector, err := NewResponseHeaderInjector(in.headers)
235+ if in.expectedErr != "" {
236+ Expect(err).To(MatchError(in.expectedErr))
237+ return
238+ }
239+ Expect(err).ToNot(HaveOccurred())
240+
241+ handler := injector(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
242+ gotHeaders = w.Header().Clone()
243+ }))
244+ handler.ServeHTTP(rw, req)
245+
246+ Expect(gotHeaders).To(Equal(in.expectedHeaders))
247+ },
248+ Entry("with no configured headers", headersTableInput{
249+ headers: []options.Header{},
250+ initialHeaders: http.Header{
251+ "Foo": []string{"bar", "baz"},
252+ },
253+ session: &sessionsapi.SessionState{},
254+ expectedHeaders: http.Header{
255+ "Foo": []string{"bar", "baz"},
256+ },
257+ expectedErr: "",
258+ }),
259+ Entry("with a claim valued header", headersTableInput{
260+ headers: []options.Header{
261+ {
262+ Name: "Claim",
263+ Values: []options.HeaderValue{
264+ {
265+ ClaimSource: &options.ClaimSource{
266+ Claim: "id_token",
267+ },
268+ },
269+ },
270+ },
271+ },
272+ initialHeaders: http.Header{
273+ "Foo": []string{"bar", "baz"},
274+ },
275+ session: &sessionsapi.SessionState{
276+ IDToken: "IDToken-1234",
277+ },
278+ expectedHeaders: http.Header{
279+ "Foo": []string{"bar", "baz"},
280+ "Claim": []string{"IDToken-1234"},
281+ },
282+ expectedErr: "",
283+ }),
284+ Entry("with a claim valued header (without preservation)", headersTableInput{
285+ headers: []options.Header{
286+ {
287+ Name: "Claim",
288+ Values: []options.HeaderValue{
289+ {
290+ ClaimSource: &options.ClaimSource{
291+ Claim: "id_token",
292+ },
293+ },
294+ },
295+ },
296+ },
297+ initialHeaders: http.Header{
298+ "Claim": []string{"bar", "baz"},
299+ },
300+ session: &sessionsapi.SessionState{
301+ IDToken: "IDToken-1234",
302+ },
303+ expectedHeaders: http.Header{
304+ "Claim": []string{"bar", "baz", "IDToken-1234"},
305+ },
306+ expectedErr: "",
307+ }),
308+ Entry("with a claim valued header (with preservation)", headersTableInput{
309+ headers: []options.Header{
310+ {
311+ Name: "Claim",
312+ PreserveRequestValue: true,
313+ Values: []options.HeaderValue{
314+ {
315+ ClaimSource: &options.ClaimSource{
316+ Claim: "id_token",
317+ },
318+ },
319+ },
320+ },
321+ },
322+ initialHeaders: http.Header{
323+ "Claim": []string{"bar", "baz"},
324+ },
325+ session: &sessionsapi.SessionState{
326+ IDToken: "IDToken-1234",
327+ },
328+ expectedHeaders: http.Header{
329+ "Claim": []string{"bar", "baz", "IDToken-1234"},
330+ },
331+ expectedErr: "",
332+ }),
333+ Entry("with a claim valued header that's not present (without preservation)", headersTableInput{
334+ headers: []options.Header{
335+ {
336+ Name: "Claim",
337+ Values: []options.HeaderValue{
338+ {
339+ ClaimSource: &options.ClaimSource{
340+ Claim: "id_token",
341+ },
342+ },
343+ },
344+ },
345+ },
346+ initialHeaders: http.Header{
347+ "Claim": []string{"bar", "baz"},
348+ },
349+ session: nil,
350+ expectedHeaders: http.Header{
351+ "Claim": []string{"bar", "baz"},
352+ },
353+ expectedErr: "",
354+ }),
355+ Entry("with a claim valued header that's not present (with preservation)", headersTableInput{
356+ headers: []options.Header{
357+ {
358+ Name: "Claim",
359+ PreserveRequestValue: true,
360+ Values: []options.HeaderValue{
361+ {
362+ ClaimSource: &options.ClaimSource{
363+ Claim: "id_token",
364+ },
365+ },
366+ },
367+ },
368+ },
369+ initialHeaders: http.Header{
370+ "Claim": []string{"bar", "baz"},
371+ },
372+ session: nil,
373+ expectedHeaders: http.Header{
374+ "Claim": []string{"bar", "baz"},
375+ },
376+ expectedErr: "",
377+ }),
378+ Entry("with an invalid basicAuthPassword claim valued header", headersTableInput{
379+ headers: []options.Header{
380+ {
381+ Name: "X-Auth-Request-Authorization",
382+ Values: []options.HeaderValue{
383+ {
384+ ClaimSource: &options.ClaimSource{
385+ Claim: "user",
386+ BasicAuthPassword: &options.SecretSource{
387+ Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))),
388+ FromEnv: "SECRET_ENV",
389+ },
390+ },
391+ },
392+ },
393+ },
394+ },
395+ initialHeaders: http.Header{
396+ "foo": []string{"bar", "baz"},
397+ },
398+ session: &sessionsapi.SessionState{
399+ User: "user-123",
400+ },
401+ expectedHeaders: nil,
402+ expectedErr: "error building response header injector: error building response injector: error building injector for header \"X-Auth-Request-Authorization\": error loading basicAuthPassword: secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile",
403+ }),
404+ )
405+})