git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #905 from grnhse/deprecate-legacy-sessions
47710d0Joel Speed
CHANGELOG.md+2
@@ -4,6 +4,7 @@
4
4
5## Important Notes
5## Important Notes
6
6
7+- [#905](https://github.com/oauth2-proxy/oauth2-proxy/pull/905) Existing sessions from v6.0.0 or earlier are no longer valid. They will trigger a reauthentication.
7- [#826](https://github.com/oauth2-proxy/oauth2-proxy/pull/826) `skip-auth-strip-headers` now applies to all requests, not just those where authentication would be skipped.
8- [#826](https://github.com/oauth2-proxy/oauth2-proxy/pull/826) `skip-auth-strip-headers` now applies to all requests, not just those where authentication would be skipped.
8- [#789](https://github.com/oauth2-proxy/oauth2-proxy/pull/789) `--skip-auth-route` is (almost) backwards compatible with `--skip-auth-regex`
9- [#789](https://github.com/oauth2-proxy/oauth2-proxy/pull/789) `--skip-auth-route` is (almost) backwards compatible with `--skip-auth-regex`
9 - We are marking `--skip-auth-regex` as DEPRECATED and will remove it in the next major version.
10 - We are marking `--skip-auth-regex` as DEPRECATED and will remove it in the next major version.
@@ -33,6 +34,7 @@
33
34
34## Changes since v6.1.1
35## Changes since v6.1.1
35
36
37+- [#905](https://github.com/oauth2-proxy/oauth2-proxy/pull/905) Remove v5 legacy sessions support (@NickMeves)
36- [#904](https://github.com/oauth2-proxy/oauth2-proxy/pull/904) Set `skip-auth-strip-headers` to `true` by default (@NickMeves)
38- [#904](https://github.com/oauth2-proxy/oauth2-proxy/pull/904) Set `skip-auth-strip-headers` to `true` by default (@NickMeves)
37- [#826](https://github.com/oauth2-proxy/oauth2-proxy/pull/826) Integrate new header injectors into project (@JoelSpeed)
39- [#826](https://github.com/oauth2-proxy/oauth2-proxy/pull/826) Integrate new header injectors into project (@JoelSpeed)
38- [#898](https://github.com/oauth2-proxy/oauth2-proxy/pull/898) Migrate documentation to Docusaurus (@JoelSpeed)
40- [#898](https://github.com/oauth2-proxy/oauth2-proxy/pull/898) Migrate documentation to Docusaurus (@JoelSpeed)
pkg/apis/sessions/legacy_v5_tester.godeleted−87
@@ -1,87 +0,0 @@
1−package sessions
2−
3−import (
4− "fmt"
5− "testing"
6− "time"
7−
8− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
9− "github.com/stretchr/testify/assert"
10−)
11−
12−const LegacyV5TestSecret = "0123456789abcdefghijklmnopqrstuv"
13−
14−// LegacyV5TestCase provides V5 JSON based test cases for legacy fallback code
15−type LegacyV5TestCase struct {
16− Input string
17− Error bool
18− Output *SessionState
19−}
20−
21−// CreateLegacyV5TestCases makes various V5 JSON sessions as test cases
22−//
23−// Used for `apis/sessions/session_state_test.go` & `sessions/redis/redis_store_test.go`
24−//
25−// TODO: Remove when this is deprecated (likely V7)
26−func CreateLegacyV5TestCases(t *testing.T) (map[string]LegacyV5TestCase, encryption.Cipher, encryption.Cipher) {
27− created := time.Now()
28− createdJSON, err := created.MarshalJSON()
29− assert.NoError(t, err)
30− createdString := string(createdJSON)
31− e := time.Now().Add(time.Duration(1) * time.Hour)
32− eJSON, err := e.MarshalJSON()
33− assert.NoError(t, err)
34− eString := string(eJSON)
35−
36− cfbCipher, err := encryption.NewCFBCipher([]byte(LegacyV5TestSecret))
37− assert.NoError(t, err)
38− legacyCipher := encryption.NewBase64Cipher(cfbCipher)
39−
40− testCases := map[string]LegacyV5TestCase{
41− "User & email unencrypted": {
42− Input: `{"Email":"user@domain.com","User":"just-user"}`,
43− Error: true,
44− },
45− "Only email unencrypted": {
46− Input: `{"Email":"user@domain.com"}`,
47− Error: true,
48− },
49− "Just user unencrypted": {
50− Input: `{"User":"just-user"}`,
51− Error: true,
52− },
53− "User and Email unencrypted while rest is encrypted": {
54− Input: fmt.Sprintf(`{"Email":"user@domain.com","User":"just-user","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","CreatedAt":%s,"ExpiresOn":%s}`, createdString, eString),
55− Error: true,
56− },
57− "Full session with cipher": {
58− Input: fmt.Sprintf(`{"Email":"FsKKYrTWZWrxSOAqA/fTNAUZS5QWCqOBjuAbBlbVOw==","User":"rT6JP3dxQhxUhkWrrd7yt6c1mDVyQCVVxw==","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","CreatedAt":%s,"ExpiresOn":%s}`, createdString, eString),
59− Output: &SessionState{
60− Email: "user@domain.com",
61− User: "just-user",
62− AccessToken: "token1234",
63− IDToken: "rawtoken1234",
64− CreatedAt: &created,
65− ExpiresOn: &e,
66− RefreshToken: "refresh4321",
67− },
68− },
69− "Minimal session encrypted with cipher": {
70− Input: `{"Email":"EGTllJcOFC16b7LBYzLekaHAC5SMMSPdyUrg8hd25g==","User":"rT6JP3dxQhxUhkWrrd7yt6c1mDVyQCVVxw=="}`,
71− Output: &SessionState{
72− Email: "user@domain.com",
73− User: "just-user",
74− },
75− },
76− "Unencrypted User, Email and AccessToken": {
77− Input: `{"Email":"user@domain.com","User":"just-user","AccessToken":"X"}`,
78− Error: true,
79− },
80− "Unencrypted User, Email and IDToken": {
81− Input: `{"Email":"user@domain.com","User":"just-user","IDToken":"XXXX"}`,
82− Error: true,
83− },
84− }
85−
86− return testCases, cfbCipher, legacyCipher
87−}
pkg/apis/sessions/session_state.go+11−56
@@ -2,7 +2,6 @@ package sessions
2
2
3import (
3import (
4 "bytes"
4 "bytes"
5− "encoding/json"
6 "errors"
5 "errors"
7 "fmt"
6 "fmt"
8 "io"
7 "io"
@@ -18,15 +17,17 @@ import (
18
17
19// SessionState is used to store information about the currently authenticated user session
18// SessionState is used to store information about the currently authenticated user session
20type SessionState struct {
19type SessionState struct {
20+ CreatedAt *time.Time `msgpack:"ca,omitempty"`
21+ ExpiresOn *time.Time `msgpack:"eo,omitempty"`
22+
21− AccessToken string `json:",omitempty" msgpack:"at,omitempty"`
23+ AccessToken string `msgpack:"at,omitempty"`
22− IDToken string `json:",omitempty" msgpack:"it,omitempty"`
24+ IDToken string `msgpack:"it,omitempty"`
23− CreatedAt *time.Time `json:",omitempty" msgpack:"ca,omitempty"`
24− ExpiresOn *time.Time `json:",omitempty" msgpack:"eo,omitempty"`
25− RefreshToken string `json:",omitempty" msgpack:"rt,omitempty"`
25+ RefreshToken string `msgpack:"rt,omitempty"`
26+
26− Email string `json:",omitempty" msgpack:"e,omitempty"`
27+ Email string `msgpack:"e,omitempty"`
27− User string `json:",omitempty" msgpack:"u,omitempty"`
28+ User string `msgpack:"u,omitempty"`
28− Groups []string `json:",omitempty" msgpack:"g,omitempty"`
29+ Groups []string `msgpack:"g,omitempty"`
29− PreferredUsername string `json:",omitempty" msgpack:"pu,omitempty"`
30+ PreferredUsername string `msgpack:"pu,omitempty"`
30}
31}
31
32
32// IsExpired checks whether the session has expired
33// IsExpired checks whether the session has expired
@@ -146,52 +147,6 @@ func DecodeSessionState(data []byte, c encryption.Cipher, compressed bool) (*Ses
146 return &ss, nil
147 return &ss, nil
147}
148}
148
149
149−// LegacyV5DecodeSessionState decodes a legacy JSON session cookie string into a SessionState
150−func LegacyV5DecodeSessionState(v string, c encryption.Cipher) (*SessionState, error) {
151− var ss SessionState
152− err := json.Unmarshal([]byte(v), &ss)
153− if err != nil {
154− return nil, fmt.Errorf("error unmarshalling session: %w", err)
155− }
156−
157− for _, s := range []*string{
158− &ss.User,
159− &ss.Email,
160− &ss.PreferredUsername,
161− &ss.AccessToken,
162− &ss.IDToken,
163− &ss.RefreshToken,
164− } {
165− err := into(s, c.Decrypt)
166− if err != nil {
167− return nil, err
168− }
169− }
170− err = ss.validate()
171− if err != nil {
172− return nil, err
173− }
174−
175− return &ss, nil
176−}
177−
178−// codecFunc is a function that takes a []byte and encodes/decodes it
179−type codecFunc func([]byte) ([]byte, error)
180−
181−func into(s *string, f codecFunc) error {
182− // Do not encrypt/decrypt nil or empty strings
183− if s == nil || *s == "" {
184− return nil
185− }
186−
187− d, err := f([]byte(*s))
188− if err != nil {
189− return err
190− }
191− *s = string(d)
192− return nil
193−}
194−
195// lz4Compress compresses with LZ4
150// lz4Compress compresses with LZ4
196//
151//
197// The Compress:Decompress ratio is 1:Many. LZ4 gives fastest decompress speeds
152// The Compress:Decompress ratio is 1:Many. LZ4 gives fastest decompress speeds
pkg/apis/sessions/session_state_test.go−73
@@ -4,7 +4,6 @@ import (
4 "crypto/rand"
4 "crypto/rand"
5 "fmt"
5 "fmt"
6 "io"
6 "io"
7− mathrand "math/rand"
8 "testing"
7 "testing"
9 "time"
8 "time"
10
9
@@ -257,78 +256,6 @@ func TestEncodeAndDecodeSessionState(t *testing.T) {
257 }
256 }
258}
257}
259
258
260−// TestLegacyV5DecodeSessionState confirms V5 JSON sessions decode
261−//
262−// TODO: Remove when this is deprecated (likely V7)
263−func TestLegacyV5DecodeSessionState(t *testing.T) {
264− testCases, cipher, legacyCipher := CreateLegacyV5TestCases(t)
265−
266− for testName, tc := range testCases {
267− t.Run(testName, func(t *testing.T) {
268− // Legacy sessions fail in DecodeSessionState which results in
269− // the fallback to LegacyV5DecodeSessionState
270− _, err := DecodeSessionState([]byte(tc.Input), cipher, false)
271− assert.Error(t, err)
272− _, err = DecodeSessionState([]byte(tc.Input), cipher, true)
273− assert.Error(t, err)
274−
275− ss, err := LegacyV5DecodeSessionState(tc.Input, legacyCipher)
276− if tc.Error {
277− assert.Error(t, err)
278− assert.Nil(t, ss)
279− return
280− }
281− assert.NoError(t, err)
282− compareSessionStates(t, tc.Output, ss)
283− })
284− }
285−}
286−
287−// Test_into tests the into helper function used in LegacyV5DecodeSessionState
288−//
289−// TODO: Remove when this is deprecated (likely V7)
290−func Test_into(t *testing.T) {
291− const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
292−
293− // Test all 3 valid AES sizes
294− for _, secretSize := range []int{16, 24, 32} {
295− t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) {
296− secret := make([]byte, secretSize)
297− _, err := io.ReadFull(rand.Reader, secret)
298− assert.Equal(t, nil, err)
299−
300− cfb, err := encryption.NewCFBCipher(secret)
301− assert.NoError(t, err)
302− c := encryption.NewBase64Cipher(cfb)
303−
304− // Check no errors with empty or nil strings
305− empty := ""
306− assert.Equal(t, nil, into(&empty, c.Encrypt))
307− assert.Equal(t, nil, into(&empty, c.Decrypt))
308− assert.Equal(t, nil, into(nil, c.Encrypt))
309− assert.Equal(t, nil, into(nil, c.Decrypt))
310−
311− // Test various sizes tokens might be
312− for _, dataSize := range []int{10, 100, 1000, 5000, 10000} {
313− t.Run(fmt.Sprintf("%d", dataSize), func(t *testing.T) {
314− b := make([]byte, dataSize)
315− for i := range b {
316− b[i] = charset[mathrand.Intn(len(charset))]
317− }
318− data := string(b)
319− originalData := data
320−
321− assert.Equal(t, nil, into(&data, c.Encrypt))
322− assert.NotEqual(t, originalData, data)
323−
324− assert.Equal(t, nil, into(&data, c.Decrypt))
325− assert.Equal(t, originalData, data)
326− })
327− }
328− })
329− }
330−}
331−
332func compareSessionStates(t *testing.T, expected *SessionState, actual *SessionState) {
259func compareSessionStates(t *testing.T, expected *SessionState, actual *SessionState) {
333 if expected.CreatedAt != nil {
260 if expected.CreatedAt != nil {
334 assert.NotNil(t, actual.CreatedAt)
261 assert.NotNil(t, actual.CreatedAt)
pkg/sessions/cookie/session_store.go+1−15
@@ -60,7 +60,7 @@ func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
60 return nil, errors.New("cookie signature not valid")
60 return nil, errors.New("cookie signature not valid")
61 }
61 }
62
62
63− session, err := sessionFromCookie(val, s.CookieCipher)
63+ session, err := sessions.DecodeSessionState(val, s.CookieCipher, true)
64 if err != nil {
64 if err != nil {
65 return nil, err
65 return nil, err
66 }
66 }
@@ -98,20 +98,6 @@ func (s *SessionStore) cookieForSession(ss *sessions.SessionState) ([]byte, erro
98 return ss.EncodeSessionState(s.CookieCipher, true)
98 return ss.EncodeSessionState(s.CookieCipher, true)
99}
99}
100
100
101−// sessionFromCookie deserializes a session from a cookie value
102−func sessionFromCookie(v []byte, c encryption.Cipher) (s *sessions.SessionState, err error) {
103− ss, err := sessions.DecodeSessionState(v, c, true)
104− // If anything fails (Decrypt, LZ4, MessagePack), try legacy JSON decode
105− // LZ4 will likely fail for wrong header after AES-CFB spits out garbage
106− // data from trying to decrypt JSON it things is ciphertext
107− if err != nil {
108− // Legacy used Base64 + AES CFB
109− legacyCipher := encryption.NewBase64Cipher(c)
110− return sessions.LegacyV5DecodeSessionState(string(v), legacyCipher)
111− }
112− return ss, nil
113−}
114−
115// setSessionCookie adds the user's session cookie to the response
101// setSessionCookie adds the user's session cookie to the response
116func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val []byte, created time.Time) error {
102func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val []byte, created time.Time) error {
117 cookies, err := s.makeSessionCookie(req, val, created)
103 cookies, err := s.makeSessionCookie(req, val, created)
pkg/sessions/persistence/ticket.go+2−33
@@ -2,7 +2,6 @@ package persistence
2
2
3import (
3import (
4 "crypto/aes"
4 "crypto/aes"
5− "crypto/cipher"
6 "crypto/rand"
5 "crypto/rand"
7 "encoding/base64"
6 "encoding/base64"
8 "encoding/hex"
7 "encoding/hex"
@@ -123,8 +122,6 @@ func (t *ticket) saveSession(s *sessions.SessionState, saver saveFunc) error {
123// loadSession loads a session from the disk store via the passed loadFunc
122// loadSession loads a session from the disk store via the passed loadFunc
124// using the ticket.id as the key. It then decodes the SessionState using
123// using the ticket.id as the key. It then decodes the SessionState using
125// ticket.secret to make the AES-GCM cipher.
124// ticket.secret to make the AES-GCM cipher.
126−//
127−// TODO (@NickMeves): Remove legacyV5LoadSession support in V7
128func (t *ticket) loadSession(loader loadFunc) (*sessions.SessionState, error) {
125func (t *ticket) loadSession(loader loadFunc) (*sessions.SessionState, error) {
129 ciphertext, err := loader(t.id)
126 ciphertext, err := loader(t.id)
130 if err != nil {
127 if err != nil {
@@ -134,11 +131,8 @@ func (t *ticket) loadSession(loader loadFunc) (*sessions.SessionState, error) {
134 if err != nil {
131 if err != nil {
135 return nil, err
132 return nil, err
136 }
133 }
134+
137− ss, err := sessions.DecodeSessionState(ciphertext, c, false)
135+ return sessions.DecodeSessionState(ciphertext, c, false)
138− if err != nil {
139− return t.legacyV5LoadSession(ciphertext)
140− }
141− return ss, nil
142}
136}
143
137
144// clearSession uses the passed clearFunc to delete a session stored with a
138// clearSession uses the passed clearFunc to delete a session stored with a
@@ -203,28 +197,3 @@ func (t *ticket) makeCipher() (encryption.Cipher, error) {
203 }
197 }
204 return c, nil
198 return c, nil
205}
199}
206−
207−// legacyV5LoadSession loads a Redis session created in V5 with historical logic
208−//
209−// TODO (@NickMeves): Remove in V7
210−func (t *ticket) legacyV5LoadSession(resultBytes []byte) (*sessions.SessionState, error) {
211− block, err := aes.NewCipher(t.secret)
212− if err != nil {
213− return nil, fmt.Errorf("failed to create a legacy AES-CFB cipher from the ticket secret: %v", err)
214− }
215−
216− stream := cipher.NewCFBDecrypter(block, t.secret)
217− stream.XORKeyStream(resultBytes, resultBytes)
218−
219− cfbCipher, err := encryption.NewCFBCipher(encryption.SecretBytes(t.options.Secret))
220− if err != nil {
221− return nil, err
222− }
223− legacyCipher := encryption.NewBase64Cipher(cfbCipher)
224−
225− session, err := sessions.LegacyV5DecodeSessionState(string(resultBytes), legacyCipher)
226− if err != nil {
227− return nil, err
228− }
229− return session, nil
230−}
pkg/sessions/persistence/ticket_test.go−66
@@ -1,14 +1,9 @@
1package persistence
1package persistence
2
2
3import (
3import (
4− "crypto/aes"
5− "crypto/cipher"
6− "crypto/rand"
7 "encoding/base64"
4 "encoding/base64"
8 "errors"
5 "errors"
9 "fmt"
6 "fmt"
10− "io"
11− "testing"
12 "time"
7 "time"
13
8
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
9 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
@@ -153,64 +148,3 @@ var _ = Describe("Session Ticket Tests", func() {
153 })
148 })
154 })
149 })
155})
150})
156−
157−// TestLegacyV5DecodeSession tests the fallback to LegacyV5DecodeSession
158−// when a V5 encoded session is in Redis
159−//
160−// TODO (@NickMeves): Remove when this is deprecated (likely V7)
161−func Test_legacyV5LoadSession(t *testing.T) {
162− testCases, _, _ := sessions.CreateLegacyV5TestCases(t)
163−
164− for testName, tc := range testCases {
165− t.Run(testName, func(t *testing.T) {
166− g := NewWithT(t)
167−
168− secret := make([]byte, aes.BlockSize)
169− _, err := io.ReadFull(rand.Reader, secret)
170− g.Expect(err).ToNot(HaveOccurred())
171− tckt := &ticket{
172− secret: secret,
173− options: &options.Cookie{
174− Secret: base64.RawURLEncoding.EncodeToString([]byte(sessions.LegacyV5TestSecret)),
175− },
176− }
177−
178− encrypted, err := legacyStoreValue(tc.Input, tckt.secret)
179− g.Expect(err).ToNot(HaveOccurred())
180−
181− ss, err := tckt.legacyV5LoadSession(encrypted)
182− if tc.Error {
183− g.Expect(err).To(HaveOccurred())
184− g.Expect(ss).To(BeNil())
185− return
186− }
187− g.Expect(err).ToNot(HaveOccurred())
188−
189− // Compare sessions without *time.Time fields
190− exp := *tc.Output
191− exp.CreatedAt = nil
192− exp.ExpiresOn = nil
193− act := *ss
194− act.CreatedAt = nil
195− act.ExpiresOn = nil
196− g.Expect(exp).To(Equal(act))
197− })
198− }
199−}
200−
201−// legacyStoreValue implements the legacy V5 Redis persistence AES-CFB value encryption
202−//
203−// TODO (@NickMeves): Remove when this is deprecated (likely V7)
204−func legacyStoreValue(value string, ticketSecret []byte) ([]byte, error) {
205− ciphertext := make([]byte, len(value))
206− block, err := aes.NewCipher(ticketSecret)
207− if err != nil {
208− return nil, fmt.Errorf("error initiating cipher block: %v", err)
209− }
210−
211− // Use secret as the Initialization Vector too, because each entry has it's own key
212− stream := cipher.NewCFBEncrypter(block, ticketSecret)
213− stream.XORKeyStream(ciphertext, []byte(value))
214−
215− return ciphertext, nil
216−}