@@ -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+})