git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add header Injector
commitfc2ff19a1908e43f368792e2a57518beb2add7f5 authorJoel Speed <joel.speed@hotmail.co.uk> dateSun 26 Jul 2020, 04:50 (6 years ago) parenteec7565
7 files changed+725
pkg/apis/options/util/util.go+26 pkg/apis/options/util/util_suite_test.go+16 pkg/apis/options/util/util_test.go+88 pkg/apis/sessions/session_state.go+29 pkg/header/header_suite_test.go+37 pkg/header/injector.go+112 pkg/header/injector_test.go+417
pkg/apis/options/util/util.gonew file+26
@@ -0,0 +1,26 @@
1+package util
2+
3+import (
4+ "encoding/base64"
5+ "errors"
6+ "io/ioutil"
7+ "os"
8+
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
10+)
11+
12+// GetSecretValue returns the value of the Secret from its source
13+func GetSecretValue(source *options.SecretSource) ([]byte, error) {
14+ switch {
15+ case len(source.Value) > 0 && source.FromEnv == "" && source.FromFile == "":
16+ value := make([]byte, base64.StdEncoding.DecodedLen(len(source.Value)))
17+ decoded, err := base64.StdEncoding.Decode(value, source.Value)
18+ return value[:decoded], err
19+ case len(source.Value) == 0 && source.FromEnv != "" && source.FromFile == "":
20+ return []byte(os.Getenv(source.FromEnv)), nil
21+ case len(source.Value) == 0 && source.FromEnv == "" && source.FromFile != "":
22+ return ioutil.ReadFile(source.FromFile)
23+ default:
24+ return nil, errors.New("secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile")
25+ }
26+}
pkg/apis/options/util/util_suite_test.gonew file+16
@@ -0,0 +1,16 @@
1+package util
2+
3+import (
4+ "testing"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+func TestUtilSuite(t *testing.T) {
12+ logger.SetOutput(GinkgoWriter)
13+
14+ RegisterFailHandler(Fail)
15+ RunSpecs(t, "Options Util Suite")
16+}
pkg/apis/options/util/util_test.gonew file+88
@@ -0,0 +1,88 @@
1+package util
2+
3+import (
4+ "encoding/base64"
5+ "io/ioutil"
6+ "os"
7+ "path"
8+
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
10+ . "github.com/onsi/ginkgo"
11+ . "github.com/onsi/gomega"
12+)
13+
14+var _ = Describe("GetSecretValue", func() {
15+ var fileDir string
16+ const secretEnvKey = "SECRET_ENV_KEY"
17+ const secretEnvValue = "secret-env-value"
18+ var secretFileValue = []byte("secret-file-value")
19+
20+ BeforeEach(func() {
21+ os.Setenv(secretEnvKey, secretEnvValue)
22+
23+ var err error
24+ fileDir, err = ioutil.TempDir("", "oauth2-proxy-util-get-secret-value")
25+ Expect(err).ToNot(HaveOccurred())
26+ Expect(ioutil.WriteFile(path.Join(fileDir, "secret-file"), secretFileValue, 0600)).To(Succeed())
27+ })
28+
29+ AfterEach(func() {
30+ os.Unsetenv(secretEnvKey)
31+ os.RemoveAll(fileDir)
32+ })
33+
34+ It("returns the correct value from base64", func() {
35+ originalValue := []byte("secret-value-1")
36+ b64Value := base64.StdEncoding.EncodeToString((originalValue))
37+
38+ // Once encoded, the originalValue could have a decoded length longer than
39+ // its actual length, ensure we trim this.
40+ // This assertion ensures we are testing the triming
41+ Expect(len(originalValue)).To(BeNumerically("<", base64.StdEncoding.DecodedLen(len(b64Value))))
42+
43+ value, err := GetSecretValue(&options.SecretSource{
44+ Value: []byte(b64Value),
45+ })
46+ Expect(err).ToNot(HaveOccurred())
47+ Expect(value).To(Equal(originalValue))
48+ })
49+
50+ It("returns the correct value from the environment", func() {
51+ value, err := GetSecretValue(&options.SecretSource{
52+ FromEnv: secretEnvKey,
53+ })
54+ Expect(err).ToNot(HaveOccurred())
55+ Expect(value).To(BeEquivalentTo(secretEnvValue))
56+ })
57+
58+ It("returns the correct value from a file", func() {
59+ value, err := GetSecretValue(&options.SecretSource{
60+ FromFile: path.Join(fileDir, "secret-file"),
61+ })
62+ Expect(err).ToNot(HaveOccurred())
63+ Expect(value).To(Equal(secretFileValue))
64+ })
65+
66+ It("when the file does not exist", func() {
67+ value, err := GetSecretValue(&options.SecretSource{
68+ FromFile: path.Join(fileDir, "not-exist"),
69+ })
70+ Expect(err).To(HaveOccurred())
71+ Expect(value).To(BeEmpty())
72+ })
73+
74+ It("with no source set", func() {
75+ value, err := GetSecretValue(&options.SecretSource{})
76+ Expect(err).To(MatchError("secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile"))
77+ Expect(value).To(BeEmpty())
78+ })
79+
80+ It("with multiple sources set", func() {
81+ value, err := GetSecretValue(&options.SecretSource{
82+ FromEnv: secretEnvKey,
83+ FromFile: path.Join(fileDir, "secret-file"),
84+ })
85+ Expect(err).To(MatchError("secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile"))
86+ Expect(value).To(BeEmpty())
87+ })
88+})
pkg/apis/sessions/session_state.go+29
@@ -8,6 +8,7 @@ import (
88 "io"
99 "io/ioutil"
1010 "reflect"
11+ "strings"
1112 "time"
1213 "unicode/utf8"
1314
@@ -69,6 +70,34 @@ func (s *SessionState) String() string {
6970 return o + "}"
7071}
7172
73+func (s *SessionState) GetClaim(claim string) string {
74+ if s == nil {
75+ return ""
76+ }
77+ switch claim {
78+ case "access_token":
79+ return s.AccessToken
80+ case "id_token":
81+ return s.IDToken
82+ case "created_at":
83+ return s.CreatedAt.String()
84+ case "expires_on":
85+ return s.ExpiresOn.String()
86+ case "refresh_token":
87+ return s.RefreshToken
88+ case "email":
89+ return s.Email
90+ case "user":
91+ return s.User
92+ case "groups":
93+ return strings.Join(s.Groups, ",")
94+ case "preferred_username":
95+ return s.PreferredUsername
96+ default:
97+ return ""
98+ }
99+}
100+
72101// EncodeSessionState returns an encrypted, lz4 compressed, MessagePack encoded session
73102func (s *SessionState) EncodeSessionState(c encryption.Cipher, compress bool) ([]byte, error) {
74103 packed, err := msgpack.Marshal(s)
pkg/header/header_suite_test.gonew file+37
@@ -0,0 +1,37 @@
1+package header
2+
3+import (
4+ "io/ioutil"
5+ "os"
6+ "path"
7+ "testing"
8+
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
10+ . "github.com/onsi/ginkgo"
11+ . "github.com/onsi/gomega"
12+)
13+
14+var (
15+ filesDir string
16+)
17+
18+func TestHeaderSuite(t *testing.T) {
19+ logger.SetOutput(GinkgoWriter)
20+
21+ RegisterFailHandler(Fail)
22+ RunSpecs(t, "Header")
23+}
24+
25+var _ = BeforeSuite(func() {
26+ os.Setenv("SECRET_ENV", "super-secret-env")
27+
28+ dir, err := ioutil.TempDir("", "oauth2-proxy-header-suite")
29+ Expect(err).ToNot(HaveOccurred())
30+ Expect(ioutil.WriteFile(path.Join(dir, "secret-file"), []byte("super-secret-file"), 0644)).To(Succeed())
31+ filesDir = dir
32+})
33+
34+var _ = AfterSuite(func() {
35+ os.Unsetenv("SECRET_ENV")
36+ Expect(os.RemoveAll(filesDir)).To(Succeed())
37+})
pkg/header/injector.gonew file+112
@@ -0,0 +1,112 @@
1+package header
2+
3+import (
4+ "encoding/base64"
5+ "fmt"
6+ "net/http"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options/util"
10+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11+)
12+
13+type Injector interface {
14+ Inject(http.Header, *sessionsapi.SessionState)
15+}
16+
17+type injector struct {
18+ valueInjectors []valueInjector
19+}
20+
21+func (i injector) Inject(header http.Header, session *sessionsapi.SessionState) {
22+ for _, injector := range i.valueInjectors {
23+ injector.inject(header, session)
24+ }
25+}
26+
27+func NewInjector(headers []options.Header) (Injector, error) {
28+ injectors := []valueInjector{}
29+ for _, header := range headers {
30+ for _, value := range header.Values {
31+ injector, err := newValueinjector(header.Name, value)
32+ if err != nil {
33+ return nil, fmt.Errorf("error building injector for header %q: %v", header.Name, err)
34+ }
35+ injectors = append(injectors, injector)
36+ }
37+ }
38+
39+ return &injector{valueInjectors: injectors}, nil
40+}
41+
42+type valueInjector interface {
43+ inject(http.Header, *sessionsapi.SessionState)
44+}
45+
46+func newValueinjector(name string, value options.HeaderValue) (valueInjector, error) {
47+ switch {
48+ case value.SecretSource != nil && value.ClaimSource == nil:
49+ return newSecretInjector(name, value.SecretSource)
50+ case value.SecretSource == nil && value.ClaimSource != nil:
51+ return newClaimInjector(name, value.ClaimSource)
52+ default:
53+ return nil, fmt.Errorf("header %q value has multiple entries: only one entry per value is allowed", name)
54+ }
55+}
56+
57+type injectorFunc struct {
58+ injectFunc func(http.Header, *sessionsapi.SessionState)
59+}
60+
61+func (i *injectorFunc) inject(header http.Header, session *sessionsapi.SessionState) {
62+ i.injectFunc(header, session)
63+}
64+
65+func newInjectorFunc(injectFunc func(header http.Header, session *sessionsapi.SessionState)) valueInjector {
66+ return &injectorFunc{injectFunc: injectFunc}
67+}
68+
69+func newSecretInjector(name string, source *options.SecretSource) (valueInjector, error) {
70+ value, err := util.GetSecretValue(source)
71+ if err != nil {
72+ return nil, fmt.Errorf("error getting secret value: %v", err)
73+ }
74+
75+ return newInjectorFunc(func(header http.Header, session *sessionsapi.SessionState) {
76+ header.Add(name, string(value))
77+ }), nil
78+}
79+
80+func newClaimInjector(name string, source *options.ClaimSource) (valueInjector, error) {
81+ switch {
82+ case source.BasicAuthPassword != nil:
83+ password, err := util.GetSecretValue(source.BasicAuthPassword)
84+ if err != nil {
85+ return nil, fmt.Errorf("error loading basicAuthPassword: %v", err)
86+ }
87+ return newInjectorFunc(func(header http.Header, session *sessionsapi.SessionState) {
88+ claim := session.GetClaim(source.Claim)
89+ if claim == "" {
90+ return
91+ }
92+ auth := claim + ":" + string(password)
93+ header.Add(name, "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
94+ }), nil
95+ case source.Prefix != "":
96+ return newInjectorFunc(func(header http.Header, session *sessionsapi.SessionState) {
97+ claim := session.GetClaim(source.Claim)
98+ if claim == "" {
99+ return
100+ }
101+ header.Add(name, source.Prefix+claim)
102+ }), nil
103+ default:
104+ return newInjectorFunc(func(header http.Header, session *sessionsapi.SessionState) {
105+ claim := session.GetClaim(source.Claim)
106+ if claim == "" {
107+ return
108+ }
109+ header.Add(name, claim)
110+ }), nil
111+ }
112+}
pkg/header/injector_test.gonew file+417
@@ -0,0 +1,417 @@
1+package header
2+
3+import (
4+ "encoding/base64"
5+ "errors"
6+ "net/http"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
9+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
10+ . "github.com/onsi/ginkgo"
11+ . "github.com/onsi/ginkgo/extensions/table"
12+ . "github.com/onsi/gomega"
13+)
14+
15+var _ = Describe("Injector Suite", func() {
16+ Context("NewInjector", func() {
17+ type newInjectorTableInput struct {
18+ headers []options.Header
19+ initialHeaders http.Header
20+ session *sessionsapi.SessionState
21+ expectedHeaders http.Header
22+ expectedErr error
23+ }
24+
25+ DescribeTable("creates an injector",
26+ func(in newInjectorTableInput) {
27+ injector, err := NewInjector(in.headers)
28+ if in.expectedErr != nil {
29+ Expect(err).To(MatchError(in.expectedErr))
30+ Expect(injector).To(BeNil())
31+ return
32+ }
33+
34+ Expect(err).ToNot(HaveOccurred())
35+ Expect(injector).ToNot(BeNil())
36+
37+ headers := in.initialHeaders.Clone()
38+ injector.Inject(headers, in.session)
39+ Expect(headers).To(Equal(in.expectedHeaders))
40+ },
41+ Entry("with no configured headers", newInjectorTableInput{
42+ headers: []options.Header{},
43+ initialHeaders: http.Header{
44+ "foo": []string{"bar", "baz"},
45+ },
46+ session: &sessionsapi.SessionState{},
47+ expectedHeaders: http.Header{
48+ "foo": []string{"bar", "baz"},
49+ },
50+ expectedErr: nil,
51+ }),
52+ Entry("with a static valued header from base64", newInjectorTableInput{
53+ headers: []options.Header{
54+ {
55+ Name: "Secret",
56+ Values: []options.HeaderValue{
57+ {
58+ SecretSource: &options.SecretSource{
59+ Value: []byte(base64.StdEncoding.EncodeToString([]byte("super-secret"))),
60+ },
61+ },
62+ },
63+ },
64+ },
65+ initialHeaders: http.Header{
66+ "foo": []string{"bar", "baz"},
67+ },
68+ session: &sessionsapi.SessionState{},
69+ expectedHeaders: http.Header{
70+ "foo": []string{"bar", "baz"},
71+ "Secret": []string{"super-secret"},
72+ },
73+ expectedErr: nil,
74+ }),
75+ Entry("with a static valued header from env", newInjectorTableInput{
76+ headers: []options.Header{
77+ {
78+ Name: "Secret",
79+ Values: []options.HeaderValue{
80+ {
81+ SecretSource: &options.SecretSource{
82+ FromEnv: "SECRET_ENV",
83+ },
84+ },
85+ },
86+ },
87+ },
88+ initialHeaders: http.Header{
89+ "foo": []string{"bar", "baz"},
90+ },
91+ session: &sessionsapi.SessionState{},
92+ expectedHeaders: http.Header{
93+ "foo": []string{"bar", "baz"},
94+ "Secret": []string{"super-secret-env"},
95+ },
96+ expectedErr: nil,
97+ }),
98+ Entry("with a claim valued header", newInjectorTableInput{
99+ headers: []options.Header{
100+ {
101+ Name: "Claim",
102+ Values: []options.HeaderValue{
103+ {
104+ ClaimSource: &options.ClaimSource{
105+ Claim: "id_token",
106+ },
107+ },
108+ },
109+ },
110+ },
111+ initialHeaders: http.Header{
112+ "foo": []string{"bar", "baz"},
113+ },
114+ session: &sessionsapi.SessionState{
115+ IDToken: "IDToken-1234",
116+ },
117+ expectedHeaders: http.Header{
118+ "foo": []string{"bar", "baz"},
119+ "Claim": []string{"IDToken-1234"},
120+ },
121+ expectedErr: nil,
122+ }),
123+ Entry("with a claim valued header and a nil session", newInjectorTableInput{
124+ headers: []options.Header{
125+ {
126+ Name: "Claim",
127+ Values: []options.HeaderValue{
128+ {
129+ ClaimSource: &options.ClaimSource{
130+ Claim: "id_token",
131+ },
132+ },
133+ },
134+ },
135+ },
136+ initialHeaders: http.Header{
137+ "foo": []string{"bar", "baz"},
138+ },
139+ session: nil,
140+ expectedHeaders: http.Header{
141+ "foo": []string{"bar", "baz"},
142+ },
143+ expectedErr: nil,
144+ }),
145+ Entry("with a prefixed claim valued header", newInjectorTableInput{
146+ headers: []options.Header{
147+ {
148+ Name: "Claim",
149+ Values: []options.HeaderValue{
150+ {
151+ ClaimSource: &options.ClaimSource{
152+ Claim: "id_token",
153+ Prefix: "Bearer ",
154+ },
155+ },
156+ },
157+ },
158+ },
159+ initialHeaders: http.Header{
160+ "foo": []string{"bar", "baz"},
161+ },
162+ session: &sessionsapi.SessionState{
163+ IDToken: "IDToken-1234",
164+ },
165+ expectedHeaders: http.Header{
166+ "foo": []string{"bar", "baz"},
167+ "Claim": []string{"Bearer IDToken-1234"},
168+ },
169+ expectedErr: nil,
170+ }),
171+ Entry("with a prefixed claim valued header missing the claim", newInjectorTableInput{
172+ headers: []options.Header{
173+ {
174+ Name: "Claim",
175+ Values: []options.HeaderValue{
176+ {
177+ ClaimSource: &options.ClaimSource{
178+ Claim: "idToken",
179+ Prefix: "Bearer ",
180+ },
181+ },
182+ },
183+ },
184+ },
185+ initialHeaders: http.Header{
186+ "foo": []string{"bar", "baz"},
187+ },
188+ session: &sessionsapi.SessionState{},
189+ expectedHeaders: http.Header{
190+ "foo": []string{"bar", "baz"},
191+ },
192+ expectedErr: nil,
193+ }),
194+ Entry("with a basicAuthPassword and claim valued header", newInjectorTableInput{
195+ headers: []options.Header{
196+ {
197+ Name: "X-Auth-Request-Authorization",
198+ Values: []options.HeaderValue{
199+ {
200+ ClaimSource: &options.ClaimSource{
201+ Claim: "user",
202+ BasicAuthPassword: &options.SecretSource{
203+ Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))),
204+ },
205+ },
206+ },
207+ },
208+ },
209+ },
210+ initialHeaders: http.Header{
211+ "foo": []string{"bar", "baz"},
212+ },
213+ session: &sessionsapi.SessionState{
214+ User: "user-123",
215+ },
216+ expectedHeaders: http.Header{
217+ "foo": []string{"bar", "baz"},
218+ "X-Auth-Request-Authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("user-123:basic-password"))},
219+ },
220+ expectedErr: nil,
221+ }),
222+ Entry("with a basicAuthPassword and claim valued header missing the claim", newInjectorTableInput{
223+ headers: []options.Header{
224+ {
225+ Name: "X-Auth-Request-Authorization",
226+ Values: []options.HeaderValue{
227+ {
228+ ClaimSource: &options.ClaimSource{
229+ Claim: "user",
230+ BasicAuthPassword: &options.SecretSource{
231+ Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))),
232+ },
233+ },
234+ },
235+ },
236+ },
237+ },
238+ initialHeaders: http.Header{
239+ "foo": []string{"bar", "baz"},
240+ },
241+ session: &sessionsapi.SessionState{},
242+ expectedHeaders: http.Header{
243+ "foo": []string{"bar", "baz"},
244+ },
245+ expectedErr: nil,
246+ }),
247+ Entry("with a header that already exists", newInjectorTableInput{
248+ headers: []options.Header{
249+ {
250+ Name: "X-Auth-Request-User",
251+ Values: []options.HeaderValue{
252+ {
253+ ClaimSource: &options.ClaimSource{
254+ Claim: "user",
255+ },
256+ },
257+ },
258+ },
259+ },
260+ initialHeaders: http.Header{
261+ "X-Auth-Request-User": []string{"user"},
262+ },
263+ session: &sessionsapi.SessionState{
264+ User: "user-123",
265+ },
266+ expectedHeaders: http.Header{
267+ "X-Auth-Request-User": []string{"user", "user-123"},
268+ },
269+ expectedErr: nil,
270+ }),
271+ Entry("with a claim and secret valued header value", newInjectorTableInput{
272+ headers: []options.Header{
273+ {
274+ Name: "Claim",
275+ Values: []options.HeaderValue{
276+ {
277+ ClaimSource: &options.ClaimSource{
278+ Claim: "id_token",
279+ },
280+ SecretSource: &options.SecretSource{
281+ FromEnv: "SECRET_ENV",
282+ },
283+ },
284+ },
285+ },
286+ },
287+ initialHeaders: http.Header{
288+ "foo": []string{"bar", "baz"},
289+ },
290+ session: &sessionsapi.SessionState{
291+ IDToken: "IDToken-1234",
292+ },
293+ expectedHeaders: nil,
294+ expectedErr: errors.New("error building injector for header \"Claim\": header \"Claim\" value has multiple entries: only one entry per value is allowed"),
295+ }),
296+ Entry("with an invalid static valued header", newInjectorTableInput{
297+ headers: []options.Header{
298+ {
299+ Name: "Secret",
300+ Values: []options.HeaderValue{
301+ {
302+ SecretSource: &options.SecretSource{
303+ FromEnv: "SECRET_ENV",
304+ FromFile: "secret-file",
305+ },
306+ },
307+ },
308+ },
309+ },
310+ initialHeaders: http.Header{
311+ "foo": []string{"bar", "baz"},
312+ },
313+ session: &sessionsapi.SessionState{},
314+ expectedHeaders: nil,
315+ expectedErr: errors.New("error building injector for header \"Secret\": error getting secret value: secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile"),
316+ }),
317+ Entry("with an invalid basicAuthPassword claim valued header", newInjectorTableInput{
318+ headers: []options.Header{
319+ {
320+ Name: "X-Auth-Request-Authorization",
321+ Values: []options.HeaderValue{
322+ {
323+ ClaimSource: &options.ClaimSource{
324+ Claim: "user",
325+ BasicAuthPassword: &options.SecretSource{
326+ Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))),
327+ FromEnv: "SECRET_ENV",
328+ },
329+ },
330+ },
331+ },
332+ },
333+ },
334+ initialHeaders: http.Header{
335+ "foo": []string{"bar", "baz"},
336+ },
337+ session: &sessionsapi.SessionState{
338+ User: "user-123",
339+ },
340+ expectedHeaders: nil,
341+ expectedErr: errors.New("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"),
342+ }),
343+ Entry("with a mix of configured headers", newInjectorTableInput{
344+ headers: []options.Header{
345+ {
346+ Name: "X-Auth-Request-Authorization",
347+ Values: []options.HeaderValue{
348+ {
349+ ClaimSource: &options.ClaimSource{
350+ Claim: "user",
351+ BasicAuthPassword: &options.SecretSource{
352+ Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))),
353+ },
354+ },
355+ },
356+ },
357+ },
358+ {
359+ Name: "X-Auth-Request-User",
360+ Values: []options.HeaderValue{
361+ {
362+ ClaimSource: &options.ClaimSource{
363+ Claim: "user",
364+ },
365+ },
366+ },
367+ },
368+ {
369+ Name: "X-Auth-Request-Email",
370+ Values: []options.HeaderValue{
371+ {
372+ ClaimSource: &options.ClaimSource{
373+ Claim: "email",
374+ },
375+ },
376+ },
377+ },
378+ {
379+ Name: "X-Auth-Request-Version-Info",
380+ Values: []options.HeaderValue{
381+ {
382+ SecretSource: &options.SecretSource{
383+ Value: []byte(base64.StdEncoding.EncodeToString([]byte("major=1"))),
384+ },
385+ },
386+ {
387+ SecretSource: &options.SecretSource{
388+ Value: []byte(base64.StdEncoding.EncodeToString([]byte("minor=2"))),
389+ },
390+ },
391+ {
392+ SecretSource: &options.SecretSource{
393+ Value: []byte(base64.StdEncoding.EncodeToString([]byte("patch=3"))),
394+ },
395+ },
396+ },
397+ },
398+ },
399+ initialHeaders: http.Header{
400+ "foo": []string{"bar", "baz"},
401+ },
402+ session: &sessionsapi.SessionState{
403+ User: "user-123",
404+ Email: "user@example.com",
405+ },
406+ expectedHeaders: http.Header{
407+ "foo": []string{"bar", "baz"},
408+ "X-Auth-Request-Authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("user-123:basic-password"))},
409+ "X-Auth-Request-User": []string{"user-123"},
410+ "X-Auth-Request-Email": []string{"user@example.com"},
411+ "X-Auth-Request-Version-Info": []string{"major=1", "minor=2", "patch=3"},
412+ },
413+ expectedErr: nil,
414+ }),
415+ )
416+ })
417+})