git.iko.soy/typeable/oauth2-proxy
on master help sign in
Reduce SessionState size better with MessagePack + LZ4 (#632)

* Encode sessions with MsgPack + LZ4 Assumes ciphers are now mandatory per #414. Cookie & Redis sessions can fallback to V5 style JSON in error cases. TODO: session_state.go unit tests & new unit tests for Legacy fallback scenarios. * Only compress encoded sessions with Cookie Store * Cleanup msgpack + lz4 error handling * Change NewBase64Cipher to take in an existing Cipher * Add msgpack & lz4 session state tests * Add required options for oauthproxy tests More aggressively assert.NoError on all validation.Validate(opts) calls to enforce legal options in all our tests. Add additional NoError checks wherever error return values were ignored. * Remove support for uncompressed session state fields * Improve error verbosity & add session state tests * Ensure all marshalled sessions are valid Invalid CFB decryptions can result in garbage data that 1/100 times might cause message pack unmarshal to not fail and instead return an empty session. This adds more rigor to make sure legacy sessions cause appropriate errors. * Add tests for legacy V5 session decoding Refactor common legacy JSON test cases to a legacy helpers area under session store tests. * Make ValidateSession a struct method & add CHANGELOG entry * Improve SessionState error & comments verbosity * Move legacy session test helpers to sessions pkg Placing these helpers under the sessions pkg removed all the circular import uses in housing it under the session store area. * Improve SignatureAuthenticator test helper formatting * Make redis.legacyV5DecodeSession internal * Make LegacyV5TestCase test table public for linter

commita09eecc6a261ce6b5fe62dee64b6b1a402cfd705 authorNick Meves <nick.meves@greenhouse.io> dateMon 13 Jul 2020, 12:56 (6 years ago) parentdd36138
12 files changed+995−659
CHANGELOG.md+4 go.mod+5−4 go.sum+17−6 oauthproxy_test.go+395−232 pkg/apis/sessions/legacy_v5_tester.go+87 pkg/apis/sessions/session_state.go+156−67 pkg/apis/sessions/session_state_test.go+162−270 pkg/encryption/cipher.go+2−6 pkg/encryption/cipher_test.go+7−6 pkg/sessions/cookie/session_store.go+21−12 pkg/sessions/redis/redis_store.go+75−56 pkg/sessions/redis/session_store_test.go → pkg/sessions/redis/redis_store_test.go+64
CHANGELOG.md+4
@@ -4,10 +4,14 @@
44
55## Important Notes
66
7+- [#632](https://github.com/oauth2-proxy/oauth2-proxy/pull/632) There is backwards compatibility to sessions from v5
8+ - Any unencrypted sessions from before v5 that only contained a Username & Email will trigger a reauthentication
9+
710## Breaking Changes
811
912## Changes since v6.0.0
1013
14+- [#632](https://github.com/oauth2-proxy/oauth2-proxy/pull/632) Reduce session size by encoding with MessagePack and using LZ4 compression (@NickMeves)
1115- [#675](https://github.com/oauth2-proxy/oauth2-proxy/pull/675) Fix required ruby version and deprecated option for building docs (@mkontani)
1216- [#669](https://github.com/oauth2-proxy/oauth2-proxy/pull/669) Reduce docker context to improve build times (@JoelSpeed)
1317- [#668](https://github.com/oauth2-proxy/oauth2-proxy/pull/668) Use req.Host in --force-https when req.URL.Host is empty (@zucaritask)
go.mod+5−4
@@ -9,23 +9,24 @@ require (
99 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
1010 github.com/coreos/go-oidc v2.2.1+incompatible
1111 github.com/dgrijalva/jwt-go v3.2.0+incompatible
12+ github.com/frankban/quicktest v1.10.0 // indirect
1213 github.com/fsnotify/fsnotify v1.4.9
1314 github.com/go-redis/redis/v7 v7.2.0
1415 github.com/justinas/alice v1.2.0
15− github.com/kr/pretty v0.2.0 // indirect
1616 github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa
1717 github.com/mitchellh/mapstructure v1.1.2
1818 github.com/onsi/ginkgo v1.12.0
1919 github.com/onsi/gomega v1.9.0
20+ github.com/pierrec/lz4 v2.5.2+incompatible
2021 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
2122 github.com/spf13/pflag v1.0.3
2223 github.com/spf13/viper v1.6.3
2324 github.com/stretchr/testify v1.5.1
25+ github.com/vmihailenco/msgpack/v4 v4.3.11
2426 github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997
25− golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
26− golang.org/x/net v0.0.0-20200226121028-0de0cce0169b
27+ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
28+ golang.org/x/net v0.0.0-20200301022130-244492dfa37a
2729 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
28− golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
2930 google.golang.org/api v0.20.0
3031 gopkg.in/natefinch/lumberjack.v2 v2.0.0
3132 gopkg.in/square/go-jose.v2 v2.4.1
go.sum+17−6
@@ -47,6 +47,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
4747github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
4848github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
4949github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
50+github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE=
51+github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
5052github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
5153github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
5254github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
@@ -69,6 +71,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
6971github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
7072github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
7173github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
74+github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=
75+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
7276github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3 h1:6amM4HsNPOvMLVc2ZnyqrjeQ92YAVWn7T4WBKK87inY=
7377github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
7478github.com/gomodule/redigo v1.8.1 h1:Abmo0bI7Xf0IhdIPc7HZQzZcShdnmxeoVuDDtIQp8N8=
@@ -78,6 +82,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
7882github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
7983github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
8084github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
85+github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
86+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
8187github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
8288github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
8389github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -142,6 +148,8 @@ github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
142148github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
143149github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
144150github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
151+github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
152+github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
145153github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
146154github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
147155github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -184,6 +192,10 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
184192github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
185193github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
186194github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
195+github.com/vmihailenco/msgpack/v4 v4.3.11 h1:Q47CePddpNGNhk4GCnAx9DDtASi2rasatE0cd26cZoE=
196+github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
197+github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY=
198+github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
187199github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
188200github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
189201github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997 h1:1+FQ4Ns+UZtUiQ4lP0sTCyKSQ0EXoiwAdHZB0Pd5t9Q=
@@ -201,8 +213,6 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
201213golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
202214golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
203215golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
204−golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
205−golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
206216golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
207217golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
208218golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -217,14 +227,14 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r
217227golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
218228golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
219229golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
220−golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
221230golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
222231golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
223232golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
233+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
224234golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
225235golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
226−golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
227−golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
236+golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
237+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
228238golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
229239golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
230240golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@ -243,7 +253,6 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
243253golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
244254golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
245255golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
246−golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
247256golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA=
248257golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
249258golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -276,6 +285,8 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
276285google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
277286google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
278287google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
288+google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
289+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
279290google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
280291google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
281292google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
oauthproxy_test.go+395−232
@@ -23,7 +23,7 @@ import (
2323 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
2424 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
2525 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
26− "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/cookie"
26+ sessionscookie "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/cookie"
2727 "github.com/oauth2-proxy/oauth2-proxy/pkg/validation"
2828 "github.com/oauth2-proxy/oauth2-proxy/providers"
2929 "github.com/stretchr/testify/assert"
@@ -36,11 +36,12 @@ const (
3636 // encoded version of this.
3737 rawCookieSecret = "secretthirtytwobytes+abcdefghijk"
3838 base64CookieSecret = "c2VjcmV0dGhpcnR5dHdvYnl0ZXMrYWJjZGVmZ2hpams"
39+ clientID = "3984n253984d7348dm8234yf982t"
40+ clientSecret = "gv3498mfc9t23y23974dm2394dm9"
3941)
4042
4143func init() {
4244 logger.SetFlags(logger.Lshortfile)
43−
4445}
4546
4647type WebSocketOrRestHandler struct {
@@ -61,24 +62,30 @@ func TestWebSocketProxy(t *testing.T) {
6162 restHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6263 w.WriteHeader(200)
6364 hostname, _, _ := net.SplitHostPort(r.Host)
64− w.Write([]byte(hostname))
65+ _, err := w.Write([]byte(hostname))
66+ if err != nil {
67+ t.Fatal(err)
68+ }
6569 }),
6670 wsHandler: websocket.Handler(func(ws *websocket.Conn) {
67− defer ws.Close()
71+ defer func(t *testing.T) {
72+ if err := ws.Close(); err != nil {
73+ t.Fatal(err)
74+ }
75+ }(t)
6876 var data []byte
6977 err := websocket.Message.Receive(ws, &data)
7078 if err != nil {
71− t.Fatalf("err %s", err)
72− return
79+ t.Fatal(err)
7380 }
7481 err = websocket.Message.Send(ws, data)
7582 if err != nil {
76− t.Fatalf("err %s", err)
83+ t.Fatal(err)
7784 }
7885 }),
7986 }
8087 backend := httptest.NewServer(&handler)
81− defer backend.Close()
88+ t.Cleanup(backend.Close)
8289
8390 backendURL, _ := url.Parse(backend.URL)
8491
@@ -87,24 +94,24 @@ func TestWebSocketProxy(t *testing.T) {
8794 opts.PassHostHeader = true
8895 proxyHandler := NewWebSocketOrRestReverseProxy(backendURL, opts, auth)
8996 frontend := httptest.NewServer(proxyHandler)
90− defer frontend.Close()
97+ t.Cleanup(frontend.Close)
9198
9299 frontendURL, _ := url.Parse(frontend.URL)
93100 frontendWSURL := "ws://" + frontendURL.Host + "/"
94101
95102 ws, err := websocket.Dial(frontendWSURL, "", "http://localhost/")
96103 if err != nil {
97− t.Fatalf("err %s", err)
104+ t.Fatal(err)
98105 }
99106 request := []byte("hello, world!")
100107 err = websocket.Message.Send(ws, request)
101108 if err != nil {
102− t.Fatalf("err %s", err)
109+ t.Fatal(err)
103110 }
104111 var response = make([]byte, 1024)
105− websocket.Message.Receive(ws, &response)
112+ err = websocket.Message.Receive(ws, &response)
106113 if err != nil {
107− t.Fatalf("err %s", err)
114+ t.Fatal(err)
108115 }
109116 if g, e := string(request), string(response); g != e {
110117 t.Errorf("got body %q; expected %q", g, e)
@@ -123,9 +130,12 @@ func TestNewReverseProxy(t *testing.T) {
123130 backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
124131 w.WriteHeader(200)
125132 hostname, _, _ := net.SplitHostPort(r.Host)
126− w.Write([]byte(hostname))
133+ _, err := w.Write([]byte(hostname))
134+ if err != nil {
135+ t.Fatal(err)
136+ }
127137 }))
128− defer backend.Close()
138+ t.Cleanup(backend.Close)
129139
130140 backendURL, _ := url.Parse(backend.URL)
131141 backendHostname, backendPort, _ := net.SplitHostPort(backendURL.Host)
@@ -135,7 +145,7 @@ func TestNewReverseProxy(t *testing.T) {
135145 proxyHandler := NewReverseProxy(proxyURL, &options.Options{FlushInterval: time.Second})
136146 setProxyUpstreamHostHeader(proxyHandler, proxyURL)
137147 frontend := httptest.NewServer(proxyHandler)
138− defer frontend.Close()
148+ t.Cleanup(frontend.Close)
139149
140150 getReq, _ := http.NewRequest("GET", frontend.URL, nil)
141151 res, _ := http.DefaultClient.Do(getReq)
@@ -151,20 +161,20 @@ func TestEncodedSlashes(t *testing.T) {
151161 w.WriteHeader(200)
152162 seen = r.RequestURI
153163 }))
154− defer backend.Close()
164+ t.Cleanup(backend.Close)
155165
156166 b, _ := url.Parse(backend.URL)
157167 proxyHandler := NewReverseProxy(b, &options.Options{FlushInterval: time.Second})
158168 setProxyDirector(proxyHandler)
159169 frontend := httptest.NewServer(proxyHandler)
160− defer frontend.Close()
170+ t.Cleanup(frontend.Close)
161171
162172 f, _ := url.Parse(frontend.URL)
163173 encodedPath := "/a%2Fb/?c=1"
164174 getReq := &http.Request{URL: &url.URL{Scheme: "http", Host: f.Host, Opaque: encodedPath}}
165175 _, err := http.DefaultClient.Do(getReq)
166176 if err != nil {
167− t.Fatalf("err %s", err)
177+ t.Fatal(err)
168178 }
169179 if seen != encodedPath {
170180 t.Errorf("got bad request %q expected %q", seen, encodedPath)
@@ -173,13 +183,13 @@ func TestEncodedSlashes(t *testing.T) {
173183
174184func TestRobotsTxt(t *testing.T) {
175185 opts := baseTestOptions()
176− opts.ClientID = "asdlkjx"
177− opts.ClientSecret = "alkgks"
178− opts.Cookie.Secret = rawCookieSecret
179− validation.Validate(opts)
186+ err := validation.Validate(opts)
187+ assert.NoError(t, err)
180188
181189 proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
182− assert.NoError(t, err)
190+ if err != nil {
191+ t.Fatal(err)
192+ }
183193 rw := httptest.NewRecorder()
184194 req, _ := http.NewRequest("GET", "/robots.txt", nil)
185195 proxy.ServeHTTP(rw, req)
@@ -189,9 +199,6 @@ func TestRobotsTxt(t *testing.T) {
189199
190200func TestIsValidRedirect(t *testing.T) {
191201 opts := baseTestOptions()
192− opts.ClientID = "skdlfj"
193− opts.ClientSecret = "fgkdsgj"
194− opts.Cookie.Secret = base64CookieSecret
195202 // Should match domains that are exactly foo.bar and any subdomain of bar.foo
196203 opts.WhitelistDomains = []string{
197204 "foo.bar",
@@ -201,10 +208,13 @@ func TestIsValidRedirect(t *testing.T) {
201208 "anyport.bar:*",
202209 ".sub.anyport.bar:*",
203210 }
204− validation.Validate(opts)
211+ err := validation.Validate(opts)
212+ assert.NoError(t, err)
205213
206214 proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
207− assert.NoError(t, err)
215+ if err != nil {
216+ t.Fatal(err)
217+ }
208218
209219 testCases := []struct {
210220 Desc, Redirect string
@@ -439,11 +449,7 @@ func TestIsValidRedirect(t *testing.T) {
439449}
440450
441451func TestOpenRedirects(t *testing.T) {
442− opts := options.NewOptions()
443− opts.ClientID = "skdlfj"
444− opts.ClientSecret = "fgkdsgj"
445− opts.Cookie.Secret = rawCookieSecret
446− opts.EmailDomains = []string{"*"}
452+ opts := baseTestOptions()
447453 // Should match domains that are exactly foo.bar and any subdomain of bar.foo
448454 opts.WhitelistDomains = []string{
449455 "foo.bar",
@@ -458,13 +464,19 @@ func TestOpenRedirects(t *testing.T) {
458464 assert.NoError(t, err)
459465
460466 proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
461− assert.NoError(t, err)
467+ if err != nil {
468+ t.Fatal(err)
469+ }
462470
463471 file, err := os.Open("./test/openredirects.txt")
464472 if err != nil {
465473 t.Fatal(err)
466474 }
467− defer file.Close()
475+ defer func(t *testing.T) {
476+ if err := file.Close(); err != nil {
477+ t.Fatal(err)
478+ }
479+ }(t)
468480
469481 scanner := bufio.NewScanner(file)
470482 for scanner.Scan() {
@@ -544,22 +556,21 @@ func TestBasicAuthPassword(t *testing.T) {
544556 }
545557 }
546558 w.WriteHeader(200)
547− w.Write([]byte(payload))
559+ _, err := w.Write([]byte(payload))
560+ if err != nil {
561+ t.Fatal(err)
562+ }
548563 }))
549564 opts := baseTestOptions()
550565 opts.Upstreams = append(opts.Upstreams, providerServer.URL)
551− // The CookieSecret must be 32 bytes in order to create the AES
552− // cipher.
553− opts.Cookie.Secret = "xyzzyplughxyzzyplughxyzzyplughxp"
554− opts.ClientID = "dlgkj"
555− opts.ClientSecret = "alkgret"
556566 opts.Cookie.Secure = false
557567 opts.PassBasicAuth = true
558568 opts.SetBasicAuth = true
559569 opts.PassUserHeaders = true
560570 opts.PreferEmailToUser = true
561571 opts.BasicAuthPassword = "This is a secure password"
562− validation.Validate(opts)
572+ err := validation.Validate(opts)
573+ assert.NoError(t, err)
563574
564575 providerURL, _ := url.Parse(providerServer.URL)
565576 const emailAddress = "john.doe@example.com"
@@ -568,7 +579,9 @@ func TestBasicAuthPassword(t *testing.T) {
568579 proxy, err := NewOAuthProxy(opts, func(email string) bool {
569580 return email == emailAddress
570581 })
571− assert.NoError(t, err)
582+ if err != nil {
583+ t.Fatal(err)
584+ }
572585
573586 rw := httptest.NewRecorder()
574587 req, _ := http.NewRequest("GET", "/oauth2/callback?code=callback_code&state=nonce:", strings.NewReader(""))
@@ -618,8 +631,8 @@ func TestBasicAuthWithEmail(t *testing.T) {
618631 opts.PassUserHeaders = false
619632 opts.PreferEmailToUser = false
620633 opts.BasicAuthPassword = "This is a secure password"
621− opts.Cookie.Secret = rawCookieSecret
622− validation.Validate(opts)
634+ err := validation.Validate(opts)
635+ assert.NoError(t, err)
623636
624637 const emailAddress = "john.doe@example.com"
625638 const userName = "9fcab5c9b889a557"
@@ -641,7 +654,9 @@ func TestBasicAuthWithEmail(t *testing.T) {
641654 proxy, err := NewOAuthProxy(opts, func(email string) bool {
642655 return email == emailAddress
643656 })
644− assert.NoError(t, err)
657+ if err != nil {
658+ t.Fatal(err)
659+ }
645660 proxy.addHeadersForProxying(rw, req, session)
646661 assert.Equal(t, expectedUserHeader, req.Header["Authorization"][0])
647662 assert.Equal(t, userName, req.Header["X-Forwarded-User"][0])
@@ -655,7 +670,9 @@ func TestBasicAuthWithEmail(t *testing.T) {
655670 proxy, err := NewOAuthProxy(opts, func(email string) bool {
656671 return email == emailAddress
657672 })
658− assert.NoError(t, err)
673+ if err != nil {
674+ t.Fatal(err)
675+ }
659676 proxy.addHeadersForProxying(rw, req, session)
660677 assert.Equal(t, expectedEmailHeader, req.Header["Authorization"][0])
661678 assert.Equal(t, emailAddress, req.Header["X-Forwarded-User"][0])
@@ -664,11 +681,8 @@ func TestBasicAuthWithEmail(t *testing.T) {
664681
665682func TestPassUserHeadersWithEmail(t *testing.T) {
666683 opts := baseTestOptions()
667− opts.PassBasicAuth = false
668− opts.PassUserHeaders = true
669− opts.PreferEmailToUser = false
670− opts.Cookie.Secret = base64CookieSecret
671− validation.Validate(opts)
684+ err := validation.Validate(opts)
685+ assert.NoError(t, err)
672686
673687 const emailAddress = "john.doe@example.com"
674688 const userName = "9fcab5c9b889a557"
@@ -686,7 +700,9 @@ func TestPassUserHeadersWithEmail(t *testing.T) {
686700 proxy, err := NewOAuthProxy(opts, func(email string) bool {
687701 return email == emailAddress
688702 })
689− assert.NoError(t, err)
703+ if err != nil {
704+ t.Fatal(err)
705+ }
690706 proxy.addHeadersForProxying(rw, req, session)
691707 assert.Equal(t, userName, req.Header["X-Forwarded-User"][0])
692708 }
@@ -699,7 +715,9 @@ func TestPassUserHeadersWithEmail(t *testing.T) {
699715 proxy, err := NewOAuthProxy(opts, func(email string) bool {
700716 return email == emailAddress
701717 })
702− assert.NoError(t, err)
718+ if err != nil {
719+ t.Fatal(err)
720+ }
703721 proxy.addHeadersForProxying(rw, req, session)
704722 assert.Equal(t, emailAddress, req.Header["X-Forwarded-User"][0])
705723 }
@@ -716,10 +734,10 @@ type PassAccessTokenTestOptions struct {
716734 ProxyUpstream string
717735}
718736
719−func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTest {
720− t := &PassAccessTokenTest{}
737+func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) (*PassAccessTokenTest, error) {
738+ patt := &PassAccessTokenTest{}
721739
722− t.providerServer = httptest.NewServer(
740+ patt.providerServer = httptest.NewServer(
723741 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
724742 var payload string
725743 switch r.URL.Path {
@@ -732,35 +750,35 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes
732750 }
733751 }
734752 w.WriteHeader(200)
735− w.Write([]byte(payload))
753+ _, err := w.Write([]byte(payload))
754+ if err != nil {
755+ panic(err)
756+ }
736757 }))
737758
738− t.opts = baseTestOptions()
739− t.opts.Upstreams = append(t.opts.Upstreams, t.providerServer.URL)
759+ patt.opts = baseTestOptions()
760+ patt.opts.Upstreams = append(patt.opts.Upstreams, patt.providerServer.URL)
740761 if opts.ProxyUpstream != "" {
741− t.opts.Upstreams = append(t.opts.Upstreams, opts.ProxyUpstream)
742− }
743− // The CookieSecret must be 32 bytes in order to create the AES
744− // cipher.
745− t.opts.Cookie.Secret = "xyzzyplughxyzzyplughxyzzyplughxp"
746− t.opts.ClientID = "slgkj"
747− t.opts.ClientSecret = "gfjgojl"
748− t.opts.Cookie.Secure = false
749− t.opts.PassAccessToken = opts.PassAccessToken
750− validation.Validate(t.opts)
751−
752− providerURL, _ := url.Parse(t.providerServer.URL)
762+ patt.opts.Upstreams = append(patt.opts.Upstreams, opts.ProxyUpstream)
763+ }
764+ patt.opts.Cookie.Secure = false
765+ patt.opts.PassAccessToken = opts.PassAccessToken
766+ err := validation.Validate(patt.opts)
767+ if err != nil {
768+ return nil, err
769+ }
770+
771+ providerURL, _ := url.Parse(patt.providerServer.URL)
753772 const emailAddress = "michael.bland@gsa.gov"
754773
755− t.opts.SetProvider(NewTestProvider(providerURL, emailAddress))
756− var err error
757− t.proxy, err = NewOAuthProxy(t.opts, func(email string) bool {
774+ patt.opts.SetProvider(NewTestProvider(providerURL, emailAddress))
775+ patt.proxy, err = NewOAuthProxy(patt.opts, func(email string) bool {
758776 return email == emailAddress
759777 })
760778 if err != nil {
761− panic(err)
779+ return nil, err
762780 }
763− return t
781+ return patt, nil
764782}
765783
766784func (patTest *PassAccessTokenTest) Close() {
@@ -817,17 +835,20 @@ func (patTest *PassAccessTokenTest) getEndpointWithCookie(cookie string, endpoin
817835}
818836
819837func TestForwardAccessTokenUpstream(t *testing.T) {
820− patTest := NewPassAccessTokenTest(PassAccessTokenTestOptions{
838+ patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{
821839 PassAccessToken: true,
822840 })
823− defer patTest.Close()
841+ if err != nil {
842+ t.Fatal(err)
843+ }
844+ t.Cleanup(patTest.Close)
824845
825846 // A successful validation will redirect and set the auth cookie.
826847 code, cookie := patTest.getCallbackEndpoint()
827848 if code != 302 {
828849 t.Fatalf("expected 302; got %d", code)
829850 }
830− assert.NotEqual(t, nil, cookie)
851+ assert.NotNil(t, cookie)
831852
832853 // Now we make a regular request; the access_token from the cookie is
833854 // forwarded as the "X-Forwarded-Access-Token" header. The token is
@@ -840,12 +861,14 @@ func TestForwardAccessTokenUpstream(t *testing.T) {
840861}
841862
842863func TestStaticProxyUpstream(t *testing.T) {
843− patTest := NewPassAccessTokenTest(PassAccessTokenTestOptions{
864+ patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{
844865 PassAccessToken: true,
845866 ProxyUpstream: "static://200/static-proxy",
846867 })
847−
848− defer patTest.Close()
868+ if err != nil {
869+ t.Fatal(err)
870+ }
871+ t.Cleanup(patTest.Close)
849872
850873 // A successful validation will redirect and set the auth cookie.
851874 code, cookie := patTest.getCallbackEndpoint()
@@ -864,10 +887,13 @@ func TestStaticProxyUpstream(t *testing.T) {
864887}
865888
866889func TestDoNotForwardAccessTokenUpstream(t *testing.T) {
867− patTest := NewPassAccessTokenTest(PassAccessTokenTestOptions{
890+ patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{
868891 PassAccessToken: false,
869892 })
870− defer patTest.Close()
893+ if err != nil {
894+ t.Fatal(err)
895+ }
896+ t.Cleanup(patTest.Close)
871897
872898 // A successful validation will redirect and set the auth cookie.
873899 code, cookie := patTest.getCallbackEndpoint()
@@ -895,27 +921,26 @@ type SignInPageTest struct {
895921const signInRedirectPattern = `<input type="hidden" name="rd" value="(.*)">`
896922const signInSkipProvider = `>Found<`
897923
898−func NewSignInPageTest(skipProvider bool) *SignInPageTest {
924+func NewSignInPageTest(skipProvider bool) (*SignInPageTest, error) {
899925 var sipTest SignInPageTest
900926
901927 sipTest.opts = baseTestOptions()
902− sipTest.opts.Cookie.Secret = rawCookieSecret
903− sipTest.opts.ClientID = "lkdgj"
904− sipTest.opts.ClientSecret = "sgiufgoi"
905928 sipTest.opts.SkipProviderButton = skipProvider
906− validation.Validate(sipTest.opts)
929+ err := validation.Validate(sipTest.opts)
930+ if err != nil {
931+ return nil, err
932+ }
907933
908− var err error
909934 sipTest.proxy, err = NewOAuthProxy(sipTest.opts, func(email string) bool {
910935 return true
911936 })
912937 if err != nil {
913− panic(err)
938+ return nil, err
914939 }
915940 sipTest.signInRegexp = regexp.MustCompile(signInRedirectPattern)
916941 sipTest.signInProviderRegexp = regexp.MustCompile(signInSkipProvider)
917942
918− return &sipTest
943+ return &sipTest, nil
919944}
920945
921946func (sipTest *SignInPageTest) GetEndpoint(endpoint string) (int, string) {
@@ -926,7 +951,10 @@ func (sipTest *SignInPageTest) GetEndpoint(endpoint string) (int, string) {
926951}
927952
928953func TestSignInPageIncludesTargetRedirect(t *testing.T) {
929− sipTest := NewSignInPageTest(false)
954+ sipTest, err := NewSignInPageTest(false)
955+ if err != nil {
956+ t.Fatal(err)
957+ }
930958 const endpoint = "/some/random/endpoint"
931959
932960 code, body := sipTest.GetEndpoint(endpoint)
@@ -944,7 +972,10 @@ func TestSignInPageIncludesTargetRedirect(t *testing.T) {
944972}
945973
946974func TestSignInPageDirectAccessRedirectsToRoot(t *testing.T) {
947− sipTest := NewSignInPageTest(false)
975+ sipTest, err := NewSignInPageTest(false)
976+ if err != nil {
977+ t.Fatal(err)
978+ }
948979 code, body := sipTest.GetEndpoint("/oauth2/sign_in")
949980 assert.Equal(t, 200, code)
950981
@@ -959,8 +990,12 @@ func TestSignInPageDirectAccessRedirectsToRoot(t *testing.T) {
959990}
960991
961992func TestSignInPageSkipProvider(t *testing.T) {
962− sipTest := NewSignInPageTest(true)
963− const endpoint = "/some/random/endpoint"
993+ sipTest, err := NewSignInPageTest(true)
994+ if err != nil {
995+ t.Fatal(err)
996+ }
997+
998+ endpoint := "/some/random/endpoint"
964999
9651000 code, body := sipTest.GetEndpoint(endpoint)
9661001 assert.Equal(t, 302, code)
@@ -973,8 +1008,12 @@ func TestSignInPageSkipProvider(t *testing.T) {
9731008}
9741009
9751010func TestSignInPageSkipProviderDirect(t *testing.T) {
976− sipTest := NewSignInPageTest(true)
977− const endpoint = "/sign_in"
1011+ sipTest, err := NewSignInPageTest(true)
1012+ if err != nil {
1013+ t.Fatal(err)
1014+ }
1015+
1016+ endpoint := "/sign_in"
9781017
9791018 code, body := sipTest.GetEndpoint(endpoint)
9801019 assert.Equal(t, 302, code)
@@ -1000,27 +1039,26 @@ type ProcessCookieTestOpts struct {
10001039
10011040type OptionsModifier func(*options.Options)
10021041
1003−func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifier) *ProcessCookieTest {
1042+func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
10041043 var pcTest ProcessCookieTest
10051044
10061045 pcTest.opts = baseTestOptions()
10071046 for _, modifier := range modifiers {
10081047 modifier(pcTest.opts)
10091048 }
1010− pcTest.opts.ClientID = "asdfljk"
1011− pcTest.opts.ClientSecret = "lkjfdsig"
1012− pcTest.opts.Cookie.Secret = "0123456789abcdef0123456789abcdef"
10131049 // First, set the CookieRefresh option so proxy.AesCipher is created,
10141050 // needed to encrypt the access_token.
10151051 pcTest.opts.Cookie.Refresh = time.Hour
1016− validation.Validate(pcTest.opts)
1052+ err := validation.Validate(pcTest.opts)
1053+ if err != nil {
1054+ return nil, err
1055+ }
10171056
1018− var err error
10191057 pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool {
10201058 return pcTest.validateUser
10211059 })
10221060 if err != nil {
1023− panic(err)
1061+ return nil, err
10241062 }
10251063 pcTest.proxy.provider = &TestProvider{
10261064 ValidToken: opts.providerValidateCookieResponse,
@@ -1032,16 +1070,16 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi
10321070 pcTest.rw = httptest.NewRecorder()
10331071 pcTest.req, _ = http.NewRequest("GET", "/", strings.NewReader(""))
10341072 pcTest.validateUser = true
1035− return &pcTest
1073+ return &pcTest, nil
10361074}
10371075
1038−func NewProcessCookieTestWithDefaults() *ProcessCookieTest {
1076+func NewProcessCookieTestWithDefaults() (*ProcessCookieTest, error) {
10391077 return NewProcessCookieTest(ProcessCookieTestOpts{
10401078 providerValidateCookieResponse: true,
10411079 })
10421080}
10431081
1044−func NewProcessCookieTestWithOptionsModifiers(modifiers ...OptionsModifier) *ProcessCookieTest {
1082+func NewProcessCookieTestWithOptionsModifiers(modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
10451083 return NewProcessCookieTest(ProcessCookieTestOpts{
10461084 providerValidateCookieResponse: true,
10471085 }, modifiers...)
@@ -1063,37 +1101,51 @@ func (p *ProcessCookieTest) LoadCookiedSession() (*sessions.SessionState, error)
10631101}
10641102
10651103func TestLoadCookiedSession(t *testing.T) {
1066− pcTest := NewProcessCookieTestWithDefaults()
1104+ pcTest, err := NewProcessCookieTestWithDefaults()
1105+ if err != nil {
1106+ t.Fatal(err)
1107+ }
10671108
10681109 created := time.Now()
10691110 startSession := &sessions.SessionState{Email: "john.doe@example.com", AccessToken: "my_access_token", CreatedAt: &created}
1070− pcTest.SaveSession(startSession)
1111+ err = pcTest.SaveSession(startSession)
1112+ assert.NoError(t, err)
10711113
10721114 session, err := pcTest.LoadCookiedSession()
1073− assert.Equal(t, nil, err)
1115+ if err != nil {
1116+ t.Fatal(err)
1117+ }
10741118 assert.Equal(t, startSession.Email, session.Email)
10751119 assert.Equal(t, "", session.User)
10761120 assert.Equal(t, startSession.AccessToken, session.AccessToken)
10771121}
10781122
10791123func TestProcessCookieNoCookieError(t *testing.T) {
1080− pcTest := NewProcessCookieTestWithDefaults()
1124+ pcTest, err := NewProcessCookieTestWithDefaults()
1125+ if err != nil {
1126+ t.Fatal(err)
1127+ }
10811128
10821129 session, err := pcTest.LoadCookiedSession()
1083− assert.Equal(t, "cookie \"_oauth2_proxy\" not present", err.Error())
1130+ assert.Error(t, err, "cookie \"_oauth2_proxy\" not present")
10841131 if session != nil {
10851132 t.Errorf("expected nil session. got %#v", session)
10861133 }
10871134}
10881135
10891136func TestProcessCookieRefreshNotSet(t *testing.T) {
1090− pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
1137+ pcTest, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
10911138 opts.Cookie.Expire = time.Duration(23) * time.Hour
10921139 })
1140+ if err != nil {
1141+ t.Fatal(err)
1142+ }
1143+
10931144 reference := time.Now().Add(time.Duration(-2) * time.Hour)
10941145
10951146 startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference}
1096− pcTest.SaveSession(startSession)
1147+ err = pcTest.SaveSession(startSession)
1148+ assert.NoError(t, err)
10971149
10981150 session, err := pcTest.LoadCookiedSession()
10991151 assert.Equal(t, nil, err)
@@ -1104,12 +1156,17 @@ func TestProcessCookieRefreshNotSet(t *testing.T) {
11041156}
11051157
11061158func TestProcessCookieFailIfCookieExpired(t *testing.T) {
1107− pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
1159+ pcTest, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
11081160 opts.Cookie.Expire = time.Duration(24) * time.Hour
11091161 })
1162+ if err != nil {
1163+ t.Fatal(err)
1164+ }
1165+
11101166 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
11111167 startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference}
1112− pcTest.SaveSession(startSession)
1168+ err = pcTest.SaveSession(startSession)
1169+ assert.NoError(t, err)
11131170
11141171 session, err := pcTest.LoadCookiedSession()
11151172 assert.NotEqual(t, nil, err)
@@ -1119,12 +1176,17 @@ func TestProcessCookieFailIfCookieExpired(t *testing.T) {
11191176}
11201177
11211178func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
1122− pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
1179+ pcTest, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
11231180 opts.Cookie.Expire = time.Duration(24) * time.Hour
11241181 })
1182+ if err != nil {
1183+ t.Fatal(err)
1184+ }
1185+
11251186 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
11261187 startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference}
1127− pcTest.SaveSession(startSession)
1188+ err = pcTest.SaveSession(startSession)
1189+ assert.NoError(t, err)
11281190
11291191 pcTest.proxy.CookieRefresh = time.Hour
11301192 session, err := pcTest.LoadCookiedSession()
@@ -1134,18 +1196,26 @@ func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
11341196 }
11351197}
11361198
1137−func NewUserInfoEndpointTest() *ProcessCookieTest {
1138− pcTest := NewProcessCookieTestWithDefaults()
1199+func NewUserInfoEndpointTest() (*ProcessCookieTest, error) {
1200+ pcTest, err := NewProcessCookieTestWithDefaults()
1201+ if err != nil {
1202+ return nil, err
1203+ }
11391204 pcTest.req, _ = http.NewRequest("GET",
11401205 pcTest.opts.ProxyPrefix+"/userinfo", nil)
1141− return pcTest
1206+ return pcTest, nil
11421207}
11431208
11441209func TestUserInfoEndpointAccepted(t *testing.T) {
1145− test := NewUserInfoEndpointTest()
1210+ test, err := NewUserInfoEndpointTest()
1211+ if err != nil {
1212+ t.Fatal(err)
1213+ }
1214+
11461215 startSession := &sessions.SessionState{
11471216 Email: "john.doe@example.com", AccessToken: "my_access_token"}
1148− test.SaveSession(startSession)
1217+ err = test.SaveSession(startSession)
1218+ assert.NoError(t, err)
11491219
11501220 test.proxy.ServeHTTP(test.rw, test.req)
11511221 assert.Equal(t, http.StatusOK, test.rw.Code)
@@ -1154,25 +1224,36 @@ func TestUserInfoEndpointAccepted(t *testing.T) {
11541224}
11551225
11561226func TestUserInfoEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
1157− test := NewUserInfoEndpointTest()
1227+ test, err := NewUserInfoEndpointTest()
1228+ if err != nil {
1229+ t.Fatal(err)
1230+ }
11581231
11591232 test.proxy.ServeHTTP(test.rw, test.req)
11601233 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
11611234}
11621235
1163−func NewAuthOnlyEndpointTest(modifiers ...OptionsModifier) *ProcessCookieTest {
1164− pcTest := NewProcessCookieTestWithOptionsModifiers(modifiers...)
1236+func NewAuthOnlyEndpointTest(modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
1237+ pcTest, err := NewProcessCookieTestWithOptionsModifiers(modifiers...)
1238+ if err != nil {
1239+ return nil, err
1240+ }
11651241 pcTest.req, _ = http.NewRequest("GET",
11661242 pcTest.opts.ProxyPrefix+"/auth", nil)
1167− return pcTest
1243+ return pcTest, nil
11681244}
11691245
11701246func TestAuthOnlyEndpointAccepted(t *testing.T) {
1171− test := NewAuthOnlyEndpointTest()
1247+ test, err := NewAuthOnlyEndpointTest()
1248+ if err != nil {
1249+ t.Fatal(err)
1250+ }
1251+
11721252 created := time.Now()
11731253 startSession := &sessions.SessionState{
11741254 Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &created}
1175− test.SaveSession(startSession)
1255+ err = test.SaveSession(startSession)
1256+ assert.NoError(t, err)
11761257
11771258 test.proxy.ServeHTTP(test.rw, test.req)
11781259 assert.Equal(t, http.StatusAccepted, test.rw.Code)
@@ -1181,7 +1262,10 @@ func TestAuthOnlyEndpointAccepted(t *testing.T) {
11811262}
11821263
11831264func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
1184− test := NewAuthOnlyEndpointTest()
1265+ test, err := NewAuthOnlyEndpointTest()
1266+ if err != nil {
1267+ t.Fatal(err)
1268+ }
11851269
11861270 test.proxy.ServeHTTP(test.rw, test.req)
11871271 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
@@ -1190,13 +1274,18 @@ func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
11901274}
11911275
11921276func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
1193− test := NewAuthOnlyEndpointTest(func(opts *options.Options) {
1277+ test, err := NewAuthOnlyEndpointTest(func(opts *options.Options) {
11941278 opts.Cookie.Expire = time.Duration(24) * time.Hour
11951279 })
1280+ if err != nil {
1281+ t.Fatal(err)
1282+ }
1283+
11961284 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
11971285 startSession := &sessions.SessionState{
11981286 Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference}
1199− test.SaveSession(startSession)
1287+ err = test.SaveSession(startSession)
1288+ assert.NoError(t, err)
12001289
12011290 test.proxy.ServeHTTP(test.rw, test.req)
12021291 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
@@ -1205,11 +1294,16 @@ func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
12051294}
12061295
12071296func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
1208− test := NewAuthOnlyEndpointTest()
1297+ test, err := NewAuthOnlyEndpointTest()
1298+ if err != nil {
1299+ t.Fatal(err)
1300+ }
1301+
12091302 created := time.Now()
12101303 startSession := &sessions.SessionState{
12111304 Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &created}
1212− test.SaveSession(startSession)
1305+ err = test.SaveSession(startSession)
1306+ assert.NoError(t, err)
12131307 test.validateUser = false
12141308
12151309 test.proxy.ServeHTTP(test.rw, test.req)
@@ -1224,15 +1318,13 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
12241318 pcTest.opts = baseTestOptions()
12251319 pcTest.opts.SetXAuthRequest = true
12261320 err := validation.Validate(pcTest.opts)
1227− if err != nil {
1228− panic(err)
1229− }
1321+ assert.NoError(t, err)
12301322
12311323 pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool {
12321324 return pcTest.validateUser
12331325 })
12341326 if err != nil {
1235− panic(err)
1327+ t.Fatal(err)
12361328 }
12371329 pcTest.proxy.provider = &TestProvider{
12381330 ValidToken: true,
@@ -1247,7 +1339,8 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
12471339 created := time.Now()
12481340 startSession := &sessions.SessionState{
12491341 User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created}
1250− pcTest.SaveSession(startSession)
1342+ err = pcTest.SaveSession(startSession)
1343+ assert.NoError(t, err)
12511344
12521345 pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req)
12531346 assert.Equal(t, http.StatusAccepted, pcTest.rw.Code)
@@ -1261,14 +1354,14 @@ func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) {
12611354 pcTest.opts = baseTestOptions()
12621355 pcTest.opts.SetXAuthRequest = true
12631356 pcTest.opts.SetBasicAuth = true
1264− validation.Validate(pcTest.opts)
1357+ err := validation.Validate(pcTest.opts)
1358+ assert.NoError(t, err)
12651359
1266− var err error
12671360 pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool {
12681361 return pcTest.validateUser
12691362 })
12701363 if err != nil {
1271− panic(err)
1364+ t.Fatal(err)
12721365 }
12731366 pcTest.proxy.provider = &TestProvider{
12741367 ValidToken: true,
@@ -1283,7 +1376,8 @@ func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) {
12831376 created := time.Now()
12841377 startSession := &sessions.SessionState{
12851378 User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created}
1286− pcTest.SaveSession(startSession)
1379+ err = pcTest.SaveSession(startSession)
1380+ assert.NoError(t, err)
12871381
12881382 pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req)
12891383 assert.Equal(t, http.StatusAccepted, pcTest.rw.Code)
@@ -1299,14 +1393,14 @@ func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) {
12991393 pcTest.opts = baseTestOptions()
13001394 pcTest.opts.SetXAuthRequest = true
13011395 pcTest.opts.SetBasicAuth = false
1302− validation.Validate(pcTest.opts)
1396+ err := validation.Validate(pcTest.opts)
1397+ assert.NoError(t, err)
13031398
1304− var err error
13051399 pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool {
13061400 return pcTest.validateUser
13071401 })
13081402 if err != nil {
1309− panic(err)
1403+ t.Fatal(err)
13101404 }
13111405 pcTest.proxy.provider = &TestProvider{
13121406 ValidToken: true,
@@ -1321,7 +1415,8 @@ func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) {
13211415 created := time.Now()
13221416 startSession := &sessions.SessionState{
13231417 User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created}
1324− pcTest.SaveSession(startSession)
1418+ err = pcTest.SaveSession(startSession)
1419+ assert.NoError(t, err)
13251420
13261421 pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req)
13271422 assert.Equal(t, http.StatusAccepted, pcTest.rw.Code)
@@ -1333,20 +1428,26 @@ func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) {
13331428func TestAuthSkippedForPreflightRequests(t *testing.T) {
13341429 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13351430 w.WriteHeader(200)
1336− w.Write([]byte("response"))
1431+ _, err := w.Write([]byte("response"))
1432+ if err != nil {
1433+ t.Fatal(err)
1434+ }
13371435 }))
1338− defer upstream.Close()
1436+ t.Cleanup(upstream.Close)
13391437
13401438 opts := baseTestOptions()
13411439 opts.Upstreams = append(opts.Upstreams, upstream.URL)
13421440 opts.SkipAuthPreflight = true
1343− validation.Validate(opts)
1441+ err := validation.Validate(opts)
1442+ assert.NoError(t, err)
13441443
13451444 upstreamURL, _ := url.Parse(upstream.URL)
13461445 opts.SetProvider(NewTestProvider(upstreamURL, ""))
13471446
13481447 proxy, err := NewOAuthProxy(opts, func(string) bool { return false })
1349− assert.NoError(t, err)
1448+ if err != nil {
1449+ t.Fatal(err)
1450+ }
13501451 rw := httptest.NewRecorder()
13511452 req, _ := http.NewRequest("OPTIONS", "/preflight-request", nil)
13521453 proxy.ServeHTTP(rw, req)
@@ -1361,16 +1462,25 @@ type SignatureAuthenticator struct {
13611462
13621463func (v *SignatureAuthenticator) Authenticate(w http.ResponseWriter, r *http.Request) {
13631464 result, headerSig, computedSig := v.auth.AuthenticateRequest(r)
1364− if result == hmacauth.ResultNoSignature {
1365− w.Write([]byte("no signature received"))
1366− } else if result == hmacauth.ResultMatch {
1367− w.Write([]byte("signatures match"))
1368− } else if result == hmacauth.ResultMismatch {
1369− w.Write([]byte("signatures do not match:" +
1370− "\n received: " + headerSig +
1371− "\n computed: " + computedSig))
1372− } else {
1373− panic("Unknown result value: " + result.String())
1465+
1466+ var msg string
1467+ switch result {
1468+ case hmacauth.ResultNoSignature:
1469+ msg = "no signature received"
1470+ case hmacauth.ResultMatch:
1471+ msg = "signatures match"
1472+ case hmacauth.ResultMismatch:
1473+ msg = fmt.Sprintf(
1474+ "signatures do not match:\n received: %s\n computed: %s",
1475+ headerSig,
1476+ computedSig)
1477+ default:
1478+ panic("unknown result value: " + result.String())
1479+ }
1480+
1481+ _, err := w.Write([]byte(msg))
1482+ if err != nil {
1483+ panic(err)
13741484 }
13751485}
13761486
@@ -1384,24 +1494,30 @@ type SignatureTest struct {
13841494 authenticator *SignatureAuthenticator
13851495}
13861496
1387−func NewSignatureTest() *SignatureTest {
1497+func NewSignatureTest() (*SignatureTest, error) {
13881498 opts := baseTestOptions()
1389− opts.Cookie.Secret = rawCookieSecret
1390− opts.ClientID = "client ID"
1391− opts.ClientSecret = "client secret"
13921499 opts.EmailDomains = []string{"acm.org"}
13931500
13941501 authenticator := &SignatureAuthenticator{}
13951502 upstream := httptest.NewServer(
13961503 http.HandlerFunc(authenticator.Authenticate))
1397− upstreamURL, _ := url.Parse(upstream.URL)
1504+ upstreamURL, err := url.Parse(upstream.URL)
1505+ if err != nil {
1506+ return nil, err
1507+ }
13981508 opts.Upstreams = append(opts.Upstreams, upstream.URL)
13991509
14001510 providerHandler := func(w http.ResponseWriter, r *http.Request) {
1401− w.Write([]byte(`{"access_token": "my_auth_token"}`))
1511+ _, err := w.Write([]byte(`{"access_token": "my_auth_token"}`))
1512+ if err != nil {
1513+ panic(err)
1514+ }
14021515 }
14031516 provider := httptest.NewServer(http.HandlerFunc(providerHandler))
1404− providerURL, _ := url.Parse(provider.URL)
1517+ providerURL, err := url.Parse(provider.URL)
1518+ if err != nil {
1519+ return nil, err
1520+ }
14051521 opts.SetProvider(NewTestProvider(providerURL, "mbland@acm.org"))
14061522
14071523 return &SignatureTest{
@@ -1412,7 +1528,7 @@ func NewSignatureTest() *SignatureTest {
14121528 make(http.Header),
14131529 httptest.NewRecorder(),
14141530 authenticator,
1415− }
1531+ }, nil
14161532}
14171533
14181534func (st *SignatureTest) Close() {
@@ -1436,14 +1552,14 @@ func (fnc *fakeNetConn) Read(p []byte) (n int, err error) {
14361552 return 0, io.EOF
14371553}
14381554
1439−func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) {
1555+func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) error {
14401556 err := validation.Validate(st.opts)
14411557 if err != nil {
1442− panic(err)
1558+ return err
14431559 }
14441560 proxy, err := NewOAuthProxy(st.opts, func(email string) bool { return true })
14451561 if err != nil {
1446− panic(err)
1562+ return err
14471563 }
14481564
14491565 var bodyBuf io.ReadCloser
@@ -1457,7 +1573,7 @@ func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) {
14571573 Email: "mbland@acm.org", AccessToken: "my_access_token"}
14581574 err = proxy.SaveSession(st.rw, req, state)
14591575 if err != nil {
1460− panic(err)
1576+ return err
14611577 }
14621578 for _, c := range st.rw.Result().Cookies() {
14631579 req.AddCookie(c)
@@ -1466,33 +1582,52 @@ func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) {
14661582 st.authenticator.auth = hmacauth.NewHmacAuth(
14671583 crypto.SHA1, []byte(key), SignatureHeader, SignatureHeaders)
14681584 proxy.ServeHTTP(st.rw, req)
1469−}
1470−
1471−func TestNoRequestSignature(t *testing.T) {
1472− st := NewSignatureTest()
1473− defer st.Close()
1474− st.MakeRequestWithExpectedKey("GET", "", "")
1475− assert.Equal(t, 200, st.rw.Code)
1476− assert.Equal(t, st.rw.Body.String(), "no signature received")
1477−}
14781585
1479−func TestRequestSignatureGetRequest(t *testing.T) {
1480− st := NewSignatureTest()
1481− defer st.Close()
1482− st.opts.SignatureKey = "sha1:7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d"
1483− st.MakeRequestWithExpectedKey("GET", "", "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d")
1484− assert.Equal(t, 200, st.rw.Code)
1485− assert.Equal(t, st.rw.Body.String(), "signatures match")
1586+ return nil
14861587}
14871588
1488−func TestRequestSignaturePostRequest(t *testing.T) {
1489− st := NewSignatureTest()
1490− defer st.Close()
1491− st.opts.SignatureKey = "sha1:d90df39e2d19282840252612dd7c81421a372f61"
1492− payload := `{ "hello": "world!" }`
1493− st.MakeRequestWithExpectedKey("POST", payload, "d90df39e2d19282840252612dd7c81421a372f61")
1494− assert.Equal(t, 200, st.rw.Code)
1495− assert.Equal(t, st.rw.Body.String(), "signatures match")
1589+func TestRequestSignature(t *testing.T) {
1590+ testCases := map[string]struct {
1591+ method string
1592+ body string
1593+ key string
1594+ resp string
1595+ }{
1596+ "No request signature": {
1597+ method: "GET",
1598+ body: "",
1599+ key: "",
1600+ resp: "no signature received",
1601+ },
1602+ "Get request": {
1603+ method: "GET",
1604+ body: "",
1605+ key: "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d",
1606+ resp: "signatures match",
1607+ },
1608+ "Post request": {
1609+ method: "POST",
1610+ body: `{ "hello": "world!" }`,
1611+ key: "d90df39e2d19282840252612dd7c81421a372f61",
1612+ resp: "signatures match",
1613+ },
1614+ }
1615+ for name, tc := range testCases {
1616+ t.Run(name, func(t *testing.T) {
1617+ st, err := NewSignatureTest()
1618+ if err != nil {
1619+ t.Fatal(err)
1620+ }
1621+ t.Cleanup(st.Close)
1622+ if tc.key != "" {
1623+ st.opts.SignatureKey = fmt.Sprintf("sha1:%s", tc.key)
1624+ }
1625+ err = st.MakeRequestWithExpectedKey(tc.method, tc.body, tc.key)
1626+ assert.NoError(t, err)
1627+ assert.Equal(t, 200, st.rw.Code)
1628+ assert.Equal(t, tc.resp, st.rw.Body.String())
1629+ })
1630+ }
14961631}
14971632
14981633func TestGetRedirect(t *testing.T) {
@@ -1501,7 +1636,9 @@ func TestGetRedirect(t *testing.T) {
15011636 assert.NoError(t, err)
15021637 require.NotEmpty(t, opts.ProxyPrefix)
15031638 proxy, err := NewOAuthProxy(opts, func(s string) bool { return false })
1504− assert.NoError(t, err)
1639+ if err != nil {
1640+ t.Fatal(err)
1641+ }
15051642
15061643 tests := []struct {
15071644 name string
@@ -1535,22 +1672,21 @@ type ajaxRequestTest struct {
15351672 proxy *OAuthProxy
15361673}
15371674
1538−func newAjaxRequestTest() *ajaxRequestTest {
1675+func newAjaxRequestTest() (*ajaxRequestTest, error) {
15391676 test := &ajaxRequestTest{}
15401677 test.opts = baseTestOptions()
1541− test.opts.Cookie.Secret = base64CookieSecret
1542− test.opts.ClientID = "gkljfdl"
1543− test.opts.ClientSecret = "sdflkjs"
1544− validation.Validate(test.opts)
1678+ err := validation.Validate(test.opts)
1679+ if err != nil {
1680+ return nil, err
1681+ }
15451682
1546− var err error
15471683 test.proxy, err = NewOAuthProxy(test.opts, func(email string) bool {
15481684 return true
15491685 })
15501686 if err != nil {
1551− panic(err)
1687+ return nil, err
15521688 }
1553− return test
1689+ return test, nil
15541690}
15551691
15561692func (test *ajaxRequestTest) getEndpoint(endpoint string, header http.Header) (int, http.Header, error) {
@@ -1565,7 +1701,10 @@ func (test *ajaxRequestTest) getEndpoint(endpoint string, header http.Header) (i
15651701}
15661702
15671703func testAjaxUnauthorizedRequest(t *testing.T, header http.Header) {
1568− test := newAjaxRequestTest()
1704+ test, err := newAjaxRequestTest()
1705+ if err != nil {
1706+ t.Fatal(err)
1707+ }
15691708 endpoint := "/test"
15701709
15711710 code, rh, err := test.getEndpoint(endpoint, header)
@@ -1589,7 +1728,10 @@ func TestAjaxUnauthorizedRequest2(t *testing.T) {
15891728}
15901729
15911730func TestAjaxForbiddendRequest(t *testing.T) {
1592− test := newAjaxRequestTest()
1731+ test, err := newAjaxRequestTest()
1732+ if err != nil {
1733+ t.Fatal(err)
1734+ }
15931735 endpoint := "/test"
15941736 header := make(http.Header)
15951737 code, rh, err := test.getEndpoint(endpoint, header)
@@ -1604,8 +1746,14 @@ func TestClearSplitCookie(t *testing.T) {
16041746 opts.Cookie.Secret = base64CookieSecret
16051747 opts.Cookie.Name = "oauth2"
16061748 opts.Cookie.Domains = []string{"abc"}
1607− store, err := cookie.NewCookieSessionStore(&opts.Session, &opts.Cookie)
1608− assert.Equal(t, nil, err)
1749+ err := validation.Validate(opts)
1750+ assert.NoError(t, err)
1751+
1752+ store, err := sessionscookie.NewCookieSessionStore(&opts.Session, &opts.Cookie)
1753+ if err != nil {
1754+ t.Fatal(err)
1755+ }
1756+
16091757 p := OAuthProxy{CookieName: opts.Cookie.Name, CookieDomains: opts.Cookie.Domains, sessionStore: store}
16101758 var rw = httptest.NewRecorder()
16111759 req := httptest.NewRequest("get", "/", nil)
@@ -1623,7 +1771,8 @@ func TestClearSplitCookie(t *testing.T) {
16231771 Value: "oauth2_1",
16241772 })
16251773
1626− p.ClearSessionCookie(rw, req)
1774+ err = p.ClearSessionCookie(rw, req)
1775+ assert.NoError(t, err)
16271776 header := rw.Header()
16281777
16291778 assert.Equal(t, 2, len(header["Set-Cookie"]), "should have 3 set-cookie header entries")
@@ -1633,8 +1782,11 @@ func TestClearSingleCookie(t *testing.T) {
16331782 opts := baseTestOptions()
16341783 opts.Cookie.Name = "oauth2"
16351784 opts.Cookie.Domains = []string{"abc"}
1636− store, err := cookie.NewCookieSessionStore(&opts.Session, &opts.Cookie)
1637− assert.Equal(t, nil, err)
1785+ store, err := sessionscookie.NewCookieSessionStore(&opts.Session, &opts.Cookie)
1786+ if err != nil {
1787+ t.Fatal(err)
1788+ }
1789+
16381790 p := OAuthProxy{CookieName: opts.Cookie.Name, CookieDomains: opts.Cookie.Domains, sessionStore: store}
16391791 var rw = httptest.NewRecorder()
16401792 req := httptest.NewRequest("get", "/", nil)
@@ -1648,7 +1800,8 @@ func TestClearSingleCookie(t *testing.T) {
16481800 Value: "oauth2",
16491801 })
16501802
1651− p.ClearSessionCookie(rw, req)
1803+ err = p.ClearSessionCookie(rw, req)
1804+ assert.NoError(t, err)
16521805 header := rw.Header()
16531806
16541807 assert.Equal(t, 1, len(header["Set-Cookie"]), "should have 1 set-cookie header entries")
@@ -1686,13 +1839,16 @@ func TestGetJwtSession(t *testing.T) {
16861839 verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
16871840 &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
16881841
1689− test := NewAuthOnlyEndpointTest(func(opts *options.Options) {
1842+ test, err := NewAuthOnlyEndpointTest(func(opts *options.Options) {
16901843 opts.PassAuthorization = true
16911844 opts.SetAuthorization = true
16921845 opts.SetXAuthRequest = true
16931846 opts.SkipJwtBearerTokens = true
16941847 opts.SetJWTBearerVerifiers(append(opts.GetJWTBearerVerifiers(), verifier))
16951848 })
1849+ if err != nil {
1850+ t.Fatal(err)
1851+ }
16961852 tp, _ := test.proxy.provider.(*TestProvider)
16971853 tp.GroupValidator = func(s string) bool {
16981854 return true
@@ -1705,7 +1861,8 @@ func TestGetJwtSession(t *testing.T) {
17051861
17061862 // Bearer
17071863 expires := time.Unix(1912151821, 0)
1708− session, _ := test.proxy.GetJwtSession(test.req)
1864+ session, err := test.proxy.GetJwtSession(test.req)
1865+ assert.NoError(t, err)
17091866 assert.Equal(t, session.User, "1234567890")
17101867 assert.Equal(t, session.Email, "john@example.com")
17111868 assert.Equal(t, session.ExpiresOn, &expires)
@@ -1739,22 +1896,26 @@ func TestFindJwtBearerToken(t *testing.T) {
17391896 "Authorization": {fmt.Sprintf("Bearer %s", validToken)},
17401897 }
17411898
1742− token, _ = p.findBearerToken(getReq)
1899+ token, err := p.findBearerToken(getReq)
1900+ assert.NoError(t, err)
17431901 assert.Equal(t, validToken, token)
17441902
17451903 // Basic - no password
17461904 getReq.SetBasicAuth(token, "")
1747− token, _ = p.findBearerToken(getReq)
1905+ token, err = p.findBearerToken(getReq)
1906+ assert.NoError(t, err)
17481907 assert.Equal(t, validToken, token)
17491908
17501909 // Basic - sentinel password
17511910 getReq.SetBasicAuth(token, "x-oauth-basic")
1752− token, _ = p.findBearerToken(getReq)
1911+ token, err = p.findBearerToken(getReq)
1912+ assert.NoError(t, err)
17531913 assert.Equal(t, validToken, token)
17541914
17551915 // Basic - any username, password matching jwt pattern
17561916 getReq.SetBasicAuth("any-username-you-could-wish-for", token)
1757− token, _ = p.findBearerToken(getReq)
1917+ token, err = p.findBearerToken(getReq)
1918+ assert.NoError(t, err)
17581919 assert.Equal(t, validToken, token)
17591920
17601921 failures := []string{
@@ -1785,8 +1946,6 @@ func TestFindJwtBearerToken(t *testing.T) {
17851946 _, err := p.findBearerToken(getReq)
17861947 assert.Error(t, err)
17871948 }
1788−
1789− fmt.Printf("%s", token)
17901949}
17911950
17921951func Test_prepareNoCache(t *testing.T) {
@@ -1807,18 +1966,22 @@ func Test_prepareNoCache(t *testing.T) {
18071966
18081967func Test_noCacheHeaders(t *testing.T) {
18091968 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1810− w.Write([]byte("upstream"))
1969+ _, err := w.Write([]byte("upstream"))
1970+ if err != nil {
1971+ t.Error(err)
1972+ }
18111973 }))
18121974 t.Cleanup(upstream.Close)
18131975
18141976 opts := baseTestOptions()
18151977 opts.Upstreams = []string{upstream.URL}
18161978 opts.SkipAuthRegex = []string{".*"}
1817− _ = validation.Validate(opts)
1818− proxy, err := NewOAuthProxy(opts, func(email string) bool {
1819− return true
1820− })
1979+ err := validation.Validate(opts)
18211980 assert.NoError(t, err)
1981+ proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true })
1982+ if err != nil {
1983+ t.Fatal(err)
1984+ }
18221985
18231986 t.Run("not exist in response from upstream", func(t *testing.T) {
18241987 rec := httptest.NewRecorder()
@@ -1887,8 +2050,8 @@ func Test_noCacheHeaders(t *testing.T) {
18872050func baseTestOptions() *options.Options {
18882051 opts := options.NewOptions()
18892052 opts.Cookie.Secret = rawCookieSecret
1890− opts.ClientID = "cliend-id"
1891− opts.ClientSecret = "client-secret"
2053+ opts.ClientID = clientID
2054+ opts.ClientSecret = clientSecret
18922055 opts.EmailDomains = []string{"*"}
18932056 return opts
18942057}
pkg/apis/sessions/legacy_v5_tester.gonew file+87
@@ -0,0 +1,87 @@
1+package sessions
2+
3+import (
4+ "fmt"
5+ "testing"
6+ "time"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
9+ "github.com/stretchr/testify/assert"
10+)
11+
12+// LegacyV5TestCase provides V5 JSON based test cases for legacy fallback code
13+type LegacyV5TestCase struct {
14+ Input string
15+ Error bool
16+ Output *SessionState
17+}
18+
19+// CreateLegacyV5TestCases makes various V5 JSON sessions as test cases
20+//
21+// Used for `apis/sessions/session_state_test.go` & `sessions/redis/redis_store_test.go`
22+//
23+// TODO: Remove when this is deprecated (likely V7)
24+func CreateLegacyV5TestCases(t *testing.T) (map[string]LegacyV5TestCase, encryption.Cipher, encryption.Cipher) {
25+ const secret = "0123456789abcdefghijklmnopqrstuv"
26+
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(secret))
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+156−67
@@ -1,25 +1,30 @@
11package sessions
22
33import (
4+ "bytes"
45 "encoding/json"
56 "errors"
67 "fmt"
8+ "io"
9+ "io/ioutil"
710 "time"
811 "unicode/utf8"
912
1013 "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
14+ "github.com/pierrec/lz4"
15+ "github.com/vmihailenco/msgpack/v4"
1116)
1217
1318// SessionState is used to store information about the currently authenticated user session
1419type SessionState struct {
15− AccessToken string `json:",omitempty"`
16− IDToken string `json:",omitempty"`
17− CreatedAt *time.Time `json:",omitempty"`
18− ExpiresOn *time.Time `json:",omitempty"`
19− RefreshToken string `json:",omitempty"`
20− Email string `json:",omitempty"`
21− User string `json:",omitempty"`
22− PreferredUsername string `json:",omitempty"`
20+ AccessToken string `json:",omitempty" msgpack:"at,omitempty"`
21+ IDToken string `json:",omitempty" msgpack:"it,omitempty"`
22+ CreatedAt *time.Time `json:",omitempty" msgpack:"ca,omitempty"`
23+ ExpiresOn *time.Time `json:",omitempty" msgpack:"eo,omitempty"`
24+ RefreshToken string `json:",omitempty" msgpack:"rt,omitempty"`
25+ Email string `json:",omitempty" msgpack:"e,omitempty"`
26+ User string `json:",omitempty" msgpack:"u,omitempty"`
27+ PreferredUsername string `json:",omitempty" msgpack:"pu,omitempty"`
2328}
2429
2530// IsExpired checks whether the session has expired
@@ -59,78 +64,79 @@ func (s *SessionState) String() string {
5964 return o + "}"
6065}
6166
62−// EncodeSessionState returns string representation of the current session
63−func (s *SessionState) EncodeSessionState(c encryption.Cipher) (string, error) {
64− var ss SessionState
65− if c == nil {
66− // Store only Email and User when cipher is unavailable
67− ss.Email = s.Email
68− ss.User = s.User
69− ss.PreferredUsername = s.PreferredUsername
70− } else {
71− ss = *s
72− for _, s := range []*string{
73− &ss.Email,
74− &ss.User,
75− &ss.PreferredUsername,
76− &ss.AccessToken,
77− &ss.IDToken,
78− &ss.RefreshToken,
79− } {
80− err := into(s, c.Encrypt)
81− if err != nil {
82− return "", err
83− }
67+// EncodeSessionState returns an encrypted, lz4 compressed, MessagePack encoded session
68+func (s *SessionState) EncodeSessionState(c encryption.Cipher, compress bool) ([]byte, error) {
69+ packed, err := msgpack.Marshal(s)
70+ if err != nil {
71+ return nil, fmt.Errorf("error marshalling session state to msgpack: %w", err)
72+ }
73+
74+ if !compress {
75+ return c.Encrypt(packed)
76+ }
77+
78+ compressed, err := lz4Compress(packed)
79+ if err != nil {
80+ return nil, err
81+ }
82+ return c.Encrypt(compressed)
83+}
84+
85+// DecodeSessionState decodes a LZ4 compressed MessagePack into a Session State
86+func DecodeSessionState(data []byte, c encryption.Cipher, compressed bool) (*SessionState, error) {
87+ decrypted, err := c.Decrypt(data)
88+ if err != nil {
89+ return nil, fmt.Errorf("error decrypting the session state: %w", err)
90+ }
91+
92+ packed := decrypted
93+ if compressed {
94+ packed, err = lz4Decompress(decrypted)
95+ if err != nil {
96+ return nil, err
8497 }
8598 }
8699
87− b, err := json.Marshal(ss)
88− return string(b), err
100+ var ss SessionState
101+ err = msgpack.Unmarshal(packed, &ss)
102+ if err != nil {
103+ return nil, fmt.Errorf("error unmarshalling data to session state: %w", err)
104+ }
105+
106+ err = ss.validate()
107+ if err != nil {
108+ return nil, err
109+ }
110+
111+ return &ss, nil
89112}
90113
91−// DecodeSessionState decodes the session cookie string into a SessionState
92−func DecodeSessionState(v string, c encryption.Cipher) (*SessionState, error) {
114+// LegacyV5DecodeSessionState decodes a legacy JSON session cookie string into a SessionState
115+func LegacyV5DecodeSessionState(v string, c encryption.Cipher) (*SessionState, error) {
93116 var ss SessionState
94117 err := json.Unmarshal([]byte(v), &ss)
95118 if err != nil {
96119 return nil, fmt.Errorf("error unmarshalling session: %w", err)
97120 }
98121
99− if c == nil {
100− // Load only Email and User when cipher is unavailable
101− ss = SessionState{
102− Email: ss.Email,
103− User: ss.User,
104− PreferredUsername: ss.PreferredUsername,
105− }
106− } else {
107− // Backward compatibility with using unencrypted Email or User
108− // Decryption errors will leave original string
109− err = into(&ss.Email, c.Decrypt)
110− if err == nil {
111− if !utf8.ValidString(ss.Email) {
112− return nil, errors.New("invalid value for decrypted email")
113− }
114− }
115− err = into(&ss.User, c.Decrypt)
116− if err == nil {
117− if !utf8.ValidString(ss.User) {
118− return nil, errors.New("invalid value for decrypted user")
119− }
120− }
121−
122− for _, s := range []*string{
123− &ss.PreferredUsername,
124− &ss.AccessToken,
125− &ss.IDToken,
126− &ss.RefreshToken,
127− } {
128− err := into(s, c.Decrypt)
129− if err != nil {
130− return nil, err
131− }
122+ for _, s := range []*string{
123+ &ss.User,
124+ &ss.Email,
125+ &ss.PreferredUsername,
126+ &ss.AccessToken,
127+ &ss.IDToken,
128+ &ss.RefreshToken,
129+ } {
130+ err := into(s, c.Decrypt)
131+ if err != nil {
132+ return nil, err
132133 }
133134 }
135+ err = ss.validate()
136+ if err != nil {
137+ return nil, err
138+ }
139+
134140 return &ss, nil
135141}
136142
@@ -150,3 +156,86 @@ func into(s *string, f codecFunc) error {
150156 *s = string(d)
151157 return nil
152158}
159+
160+// lz4Compress compresses with LZ4
161+//
162+// The Compress:Decompress ratio is 1:Many. LZ4 gives fastest decompress speeds
163+// at the expense of greater compression compared to other compression
164+// algorithms.
165+func lz4Compress(payload []byte) ([]byte, error) {
166+ buf := new(bytes.Buffer)
167+ zw := lz4.NewWriter(nil)
168+ zw.Header = lz4.Header{
169+ BlockMaxSize: 65536,
170+ CompressionLevel: 0,
171+ }
172+ zw.Reset(buf)
173+
174+ reader := bytes.NewReader(payload)
175+ _, err := io.Copy(zw, reader)
176+ if err != nil {
177+ return nil, fmt.Errorf("error copying lz4 stream to buffer: %w", err)
178+ }
179+ err = zw.Close()
180+ if err != nil {
181+ return nil, fmt.Errorf("error closing lz4 writer: %w", err)
182+ }
183+
184+ compressed, err := ioutil.ReadAll(buf)
185+ if err != nil {
186+ return nil, fmt.Errorf("error reading lz4 buffer: %w", err)
187+ }
188+
189+ return compressed, nil
190+}
191+
192+// lz4Decompress decompresses with LZ4
193+func lz4Decompress(compressed []byte) ([]byte, error) {
194+ reader := bytes.NewReader(compressed)
195+ buf := new(bytes.Buffer)
196+ zr := lz4.NewReader(nil)
197+ zr.Reset(reader)
198+ _, err := io.Copy(buf, zr)
199+ if err != nil {
200+ return nil, fmt.Errorf("error copying lz4 stream to buffer: %w", err)
201+ }
202+
203+ payload, err := ioutil.ReadAll(buf)
204+ if err != nil {
205+ return nil, fmt.Errorf("error reading lz4 buffer: %w", err)
206+ }
207+
208+ return payload, nil
209+}
210+
211+// validate ensures the decoded session is non-empty and contains valid data
212+//
213+// Non-empty check is needed due to ensure the non-authenticated AES-CFB
214+// decryption doesn't result in garbage data that collides with a valid
215+// MessagePack header bytes (which MessagePack will unmarshal to an empty
216+// default SessionState). <1% chance, but observed with random test data.
217+//
218+// UTF-8 check ensures the strings are valid and not raw bytes overloaded
219+// into Latin-1 encoding. The occurs when legacy unencrypted fields are
220+// decrypted with AES-CFB which results in random bytes.
221+func (s *SessionState) validate() error {
222+ for _, field := range []string{
223+ s.User,
224+ s.Email,
225+ s.PreferredUsername,
226+ s.AccessToken,
227+ s.IDToken,
228+ s.RefreshToken,
229+ } {
230+ if !utf8.ValidString(field) {
231+ return errors.New("invalid non-UTF8 field in session")
232+ }
233+ }
234+
235+ empty := new(SessionState)
236+ if *s == *empty {
237+ return errors.New("invalid empty session unmarshalled")
238+ }
239+
240+ return nil
241+}
pkg/apis/sessions/session_state_test.go+162−270
@@ -12,132 +12,11 @@ import (
1212 "github.com/stretchr/testify/assert"
1313)
1414
15−const secret = "0123456789abcdefghijklmnopqrstuv"
16−const altSecret = "0000000000abcdefghijklmnopqrstuv"
17−
1815func timePtr(t time.Time) *time.Time {
1916 return &t
2017}
2118
22−func newTestCipher(secret []byte) (encryption.Cipher, error) {
23− return encryption.NewBase64Cipher(encryption.NewCFBCipher, secret)
24−}
25−
26−func TestSessionStateSerialization(t *testing.T) {
27− c, err := newTestCipher([]byte(secret))
28− assert.Equal(t, nil, err)
29− c2, err := newTestCipher([]byte(altSecret))
30− assert.Equal(t, nil, err)
31− s := &SessionState{
32− Email: "user@domain.com",
33− PreferredUsername: "user",
34− AccessToken: "token1234",
35− IDToken: "rawtoken1234",
36− CreatedAt: timePtr(time.Now()),
37− ExpiresOn: timePtr(time.Now().Add(time.Duration(1) * time.Hour)),
38− RefreshToken: "refresh4321",
39− }
40− encoded, err := s.EncodeSessionState(c)
41− assert.Equal(t, nil, err)
42−
43− ss, err := DecodeSessionState(encoded, c)
44− t.Logf("%#v", ss)
45− assert.Equal(t, nil, err)
46− assert.Equal(t, "", ss.User)
47− assert.Equal(t, s.Email, ss.Email)
48− assert.Equal(t, s.PreferredUsername, ss.PreferredUsername)
49− assert.Equal(t, s.AccessToken, ss.AccessToken)
50− assert.Equal(t, s.IDToken, ss.IDToken)
51− assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
52− assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
53− assert.Equal(t, s.RefreshToken, ss.RefreshToken)
54−
55− // ensure a different cipher can't decode properly (ie: it gets gibberish)
56− ss, err = DecodeSessionState(encoded, c2)
57− t.Logf("%#v", ss)
58− assert.NotEqual(t, nil, err)
59−}
60−
61−func TestSessionStateSerializationWithUser(t *testing.T) {
62− c, err := newTestCipher([]byte(secret))
63− assert.Equal(t, nil, err)
64− c2, err := newTestCipher([]byte(altSecret))
65− assert.Equal(t, nil, err)
66− s := &SessionState{
67− User: "just-user",
68− PreferredUsername: "ju",
69− Email: "user@domain.com",
70− AccessToken: "token1234",
71− CreatedAt: timePtr(time.Now()),
72− ExpiresOn: timePtr(time.Now().Add(time.Duration(1) * time.Hour)),
73− RefreshToken: "refresh4321",
74− }
75− encoded, err := s.EncodeSessionState(c)
76− assert.Equal(t, nil, err)
77−
78− ss, err := DecodeSessionState(encoded, c)
79− t.Logf("%#v", ss)
80− assert.Equal(t, nil, err)
81− assert.Equal(t, s.User, ss.User)
82− assert.Equal(t, s.Email, ss.Email)
83− assert.Equal(t, s.PreferredUsername, ss.PreferredUsername)
84− assert.Equal(t, s.AccessToken, ss.AccessToken)
85− assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
86− assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
87− assert.Equal(t, s.RefreshToken, ss.RefreshToken)
88−
89− // ensure a different cipher can't decode properly (ie: it gets gibberish)
90− ss, err = DecodeSessionState(encoded, c2)
91− t.Logf("%#v", ss)
92− assert.NotEqual(t, nil, err)
93−}
94−
95−func TestSessionStateSerializationNoCipher(t *testing.T) {
96− s := &SessionState{
97− Email: "user@domain.com",
98− PreferredUsername: "user",
99− AccessToken: "token1234",
100− CreatedAt: timePtr(time.Now()),
101− ExpiresOn: timePtr(time.Now().Add(time.Duration(1) * time.Hour)),
102− RefreshToken: "refresh4321",
103− }
104− encoded, err := s.EncodeSessionState(nil)
105− assert.Equal(t, nil, err)
106−
107− // only email should have been serialized
108− ss, err := DecodeSessionState(encoded, nil)
109− assert.Equal(t, nil, err)
110− assert.Equal(t, "", ss.User)
111− assert.Equal(t, s.Email, ss.Email)
112− assert.Equal(t, s.PreferredUsername, ss.PreferredUsername)
113− assert.Equal(t, "", ss.AccessToken)
114− assert.Equal(t, "", ss.RefreshToken)
115−}
116−
117−func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
118− s := &SessionState{
119− User: "just-user",
120− Email: "user@domain.com",
121− PreferredUsername: "user",
122− AccessToken: "token1234",
123− CreatedAt: timePtr(time.Now()),
124− ExpiresOn: timePtr(time.Now().Add(time.Duration(1) * time.Hour)),
125− RefreshToken: "refresh4321",
126− }
127− encoded, err := s.EncodeSessionState(nil)
128− assert.Equal(t, nil, err)
129−
130− // only email should have been serialized
131− ss, err := DecodeSessionState(encoded, nil)
132− assert.Equal(t, nil, err)
133− assert.Equal(t, s.User, ss.User)
134− assert.Equal(t, s.Email, ss.Email)
135− assert.Equal(t, s.PreferredUsername, ss.PreferredUsername)
136− assert.Equal(t, "", ss.AccessToken)
137− assert.Equal(t, "", ss.RefreshToken)
138−}
139−
140−func TestExpired(t *testing.T) {
19+func TestIsExpired(t *testing.T) {
14120 s := &SessionState{ExpiresOn: timePtr(time.Now().Add(time.Duration(-1) * time.Minute))}
14221 assert.Equal(t, true, s.IsExpired())
14322
@@ -148,172 +27,160 @@ func TestExpired(t *testing.T) {
14827 assert.Equal(t, false, s.IsExpired())
14928}
15029
151−type testCase struct {
152− SessionState
153− Encoded string
154− Cipher encryption.Cipher
155− Error bool
156−}
157−
158−// TestEncodeSessionState tests EncodeSessionState with the test vector
159−//
160−// Currently only tests without cipher here because we have no way to mock
161−// the random generator used in EncodeSessionState.
162−func TestEncodeSessionState(t *testing.T) {
163− c := time.Now()
164− e := time.Now().Add(time.Duration(1) * time.Hour)
30+func TestAge(t *testing.T) {
31+ ss := &SessionState{}
16532
166− testCases := []testCase{
167− {
168− SessionState: SessionState{
169− Email: "user@domain.com",
170− User: "just-user",
171− },
172− Encoded: `{"Email":"user@domain.com","User":"just-user"}`,
173− },
174− {
175− SessionState: SessionState{
176− Email: "user@domain.com",
177− User: "just-user",
178− AccessToken: "token1234",
179− IDToken: "rawtoken1234",
180− CreatedAt: &c,
181− ExpiresOn: &e,
182− RefreshToken: "refresh4321",
183− },
184− Encoded: `{"Email":"user@domain.com","User":"just-user"}`,
185− },
186− }
33+ // Created at unset so should be 0
34+ assert.Equal(t, time.Duration(0), ss.Age())
18735
188− for i, tc := range testCases {
189− encoded, err := tc.EncodeSessionState(tc.Cipher)
190− t.Logf("i:%d Encoded:%#vSessionState:%#v Error:%#v", i, encoded, tc.SessionState, err)
191− if tc.Error {
192− assert.Error(t, err)
193− assert.Empty(t, encoded)
194− continue
195− }
196− assert.NoError(t, err)
197− assert.JSONEq(t, tc.Encoded, encoded)
198− }
36+ // Set CreatedAt to 1 hour ago
37+ ss.CreatedAt = timePtr(time.Now().Add(-1 * time.Hour))
38+ assert.Equal(t, time.Hour, ss.Age().Round(time.Minute))
19939}
20040
201−// TestDecodeSessionState testssessions.DecodeSessionState with the test vector
202−func TestDecodeSessionState(t *testing.T) {
41+// TestEncodeAndDecodeSessionState encodes & decodes various session states
42+// and confirms the operation is 1:1
43+func TestEncodeAndDecodeSessionState(t *testing.T) {
20344 created := time.Now()
204− createdJSON, _ := created.MarshalJSON()
205− createdString := string(createdJSON)
206− e := time.Now().Add(time.Duration(1) * time.Hour)
207− eJSON, _ := e.MarshalJSON()
208− eString := string(eJSON)
209−
210− c, err := newTestCipher([]byte(secret))
211− assert.NoError(t, err)
212−
213− testCases := []testCase{
214− {
215− SessionState: SessionState{
216− Email: "user@domain.com",
217− User: "just-user",
218− },
219− Encoded: `{"Email":"user@domain.com","User":"just-user"}`,
220− },
221− {
222− SessionState: SessionState{
223− Email: "user@domain.com",
224− User: "",
225− },
226− Encoded: `{"Email":"user@domain.com"}`,
45+ expires := time.Now().Add(time.Duration(1) * time.Hour)
46+
47+ // Tokens in the test table are purposefully redundant
48+ // Otherwise compressing small payloads could result in a compressed value
49+ // that is larger (compression dictionary + limited like strings to compress)
50+ // which breaks the len(compressed) < len(uncompressed) assertion.
51+ testCases := map[string]SessionState{
52+ "Full session": {
53+ Email: "username@example.com",
54+ User: "username",
55+ PreferredUsername: "preferred.username",
56+ AccessToken: "AccessToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
57+ IDToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
58+ CreatedAt: &created,
59+ ExpiresOn: &expires,
60+ RefreshToken: "RefreshToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
22761 },
228− {
229− SessionState: SessionState{
230− User: "just-user",
231− },
232− Encoded: `{"User":"just-user"}`,
62+ "No ExpiresOn": {
63+ Email: "username@example.com",
64+ User: "username",
65+ PreferredUsername: "preferred.username",
66+ AccessToken: "AccessToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
67+ IDToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
68+ CreatedAt: &created,
69+ RefreshToken: "RefreshToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
23370 },
234− {
235− SessionState: SessionState{
236− Email: "user@domain.com",
237− User: "just-user",
238− },
239− Encoded: 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),
71+ "No PreferredUsername": {
72+ Email: "username@example.com",
73+ User: "username",
74+ AccessToken: "AccessToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
75+ IDToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
76+ CreatedAt: &created,
77+ ExpiresOn: &expires,
78+ RefreshToken: "RefreshToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
24079 },
241− {
242− SessionState: SessionState{
243− Email: "user@domain.com",
244− User: "just-user",
245− AccessToken: "token1234",
246− IDToken: "rawtoken1234",
247− CreatedAt: &created,
248− ExpiresOn: &e,
249− RefreshToken: "refresh4321",
250− },
251− Encoded: fmt.Sprintf(`{"Email":"FsKKYrTWZWrxSOAqA/fTNAUZS5QWCqOBjuAbBlbVOw==","User":"rT6JP3dxQhxUhkWrrd7yt6c1mDVyQCVVxw==","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","CreatedAt":%s,"ExpiresOn":%s}`, createdString, eString),
252− Cipher: c,
80+ "Minimal session": {
81+ User: "username",
82+ IDToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
83+ CreatedAt: &created,
84+ RefreshToken: "RefreshToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
25385 },
254− {
255− SessionState: SessionState{
256− Email: "user@domain.com",
257− User: "just-user",
258− },
259− Encoded: `{"Email":"EGTllJcOFC16b7LBYzLekaHAC5SMMSPdyUrg8hd25g==","User":"rT6JP3dxQhxUhkWrrd7yt6c1mDVyQCVVxw=="}`,
260− Cipher: c,
261− },
262− {
263− Encoded: `{"Email":"user@domain.com","User":"just-user","AccessToken":"X"}`,
264− Cipher: c,
265− Error: true,
266− },
267− {
268− Encoded: `{"Email":"user@domain.com","User":"just-user","IDToken":"XXXX"}`,
269− Cipher: c,
270− Error: true,
271− },
272− {
273− SessionState: SessionState{
274− Email: "user@domain.com",
275− User: "YmFzZTY0LWVuY29kZWQtdXNlcgo=", // Base64 encoding of base64-encoded-user
276− },
277− Error: true,
278− Cipher: c,
86+ "Bearer authorization header created session": {
87+ Email: "username",
88+ User: "username",
89+ AccessToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
90+ IDToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
91+ ExpiresOn: &expires,
27992 },
28093 }
28194
282− for i, tc := range testCases {
283− ss, err := DecodeSessionState(tc.Encoded, tc.Cipher)
284− t.Logf("i:%d Encoded:%#vSessionState:%#v Error:%#v", i, tc.Encoded, ss, err)
285− if tc.Error {
286− assert.Error(t, err)
287− assert.Nil(t, ss)
288− continue
289− }
290− assert.NoError(t, err)
291− if assert.NotNil(t, ss) {
292− assert.Equal(t, tc.User, ss.User)
293− assert.Equal(t, tc.Email, ss.Email)
294− assert.Equal(t, tc.AccessToken, ss.AccessToken)
295− assert.Equal(t, tc.RefreshToken, ss.RefreshToken)
296− assert.Equal(t, tc.IDToken, ss.IDToken)
297− if tc.ExpiresOn != nil {
298− assert.NotEqual(t, nil, ss.ExpiresOn)
299− assert.Equal(t, tc.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
95+ for _, secretSize := range []int{16, 24, 32} {
96+ t.Run(fmt.Sprintf("%d byte secret", secretSize), func(t *testing.T) {
97+ secret := make([]byte, secretSize)
98+ _, err := io.ReadFull(rand.Reader, secret)
99+ assert.NoError(t, err)
100+
101+ cfb, err := encryption.NewCFBCipher([]byte(secret))
102+ assert.NoError(t, err)
103+ gcm, err := encryption.NewGCMCipher([]byte(secret))
104+ assert.NoError(t, err)
105+
106+ ciphers := map[string]encryption.Cipher{
107+ "CFB cipher": cfb,
108+ "GCM cipher": gcm,
300109 }
301− }
110+
111+ for cipherName, c := range ciphers {
112+ t.Run(cipherName, func(t *testing.T) {
113+ for testName, ss := range testCases {
114+ t.Run(testName, func(t *testing.T) {
115+ encoded, err := ss.EncodeSessionState(c, false)
116+ assert.NoError(t, err)
117+ encodedCompressed, err := ss.EncodeSessionState(c, true)
118+ assert.NoError(t, err)
119+ // Make sure compressed version is smaller than if not compressed
120+ assert.Greater(t, len(encoded), len(encodedCompressed))
121+
122+ decoded, err := DecodeSessionState(encoded, c, false)
123+ assert.NoError(t, err)
124+ decodedCompressed, err := DecodeSessionState(encodedCompressed, c, true)
125+ assert.NoError(t, err)
126+
127+ compareSessionStates(t, decoded, decodedCompressed)
128+ compareSessionStates(t, decoded, &ss)
129+ })
130+ }
131+ })
132+ }
133+
134+ t.Run("Mixed cipher types cause errors", func(t *testing.T) {
135+ for testName, ss := range testCases {
136+ t.Run(testName, func(t *testing.T) {
137+ cfbEncoded, err := ss.EncodeSessionState(cfb, false)
138+ assert.NoError(t, err)
139+ _, err = DecodeSessionState(cfbEncoded, gcm, false)
140+ assert.Error(t, err)
141+
142+ gcmEncoded, err := ss.EncodeSessionState(gcm, false)
143+ assert.NoError(t, err)
144+ _, err = DecodeSessionState(gcmEncoded, cfb, false)
145+ assert.Error(t, err)
146+ })
147+ }
148+ })
149+ })
302150 }
303151}
304152
305−func TestSessionStateAge(t *testing.T) {
306− ss := &SessionState{}
307−
308− // Created at unset so should be 0
309− assert.Equal(t, time.Duration(0), ss.Age())
153+// TestLegacyV5DecodeSessionState confirms V5 JSON sessions decode
154+//
155+// TODO: Remove when this is deprecated (likely V7)
156+func TestLegacyV5DecodeSessionState(t *testing.T) {
157+ testCases, cipher, legacyCipher := CreateLegacyV5TestCases(t)
158+
159+ for testName, tc := range testCases {
160+ t.Run(testName, func(t *testing.T) {
161+ // Legacy sessions fail in DecodeSessionState which results in
162+ // the fallback to LegacyV5DecodeSessionState
163+ _, err := DecodeSessionState([]byte(tc.Input), cipher, false)
164+ assert.Error(t, err)
165+ _, err = DecodeSessionState([]byte(tc.Input), cipher, true)
166+ assert.Error(t, err)
310167
311− // Set CreatedAt to 1 hour ago
312− ss.CreatedAt = timePtr(time.Now().Add(-1 * time.Hour))
313− assert.Equal(t, time.Hour, ss.Age().Round(time.Minute))
168+ ss, err := LegacyV5DecodeSessionState(tc.Input, legacyCipher)
169+ if tc.Error {
170+ assert.Error(t, err)
171+ assert.Nil(t, ss)
172+ return
173+ }
174+ assert.NoError(t, err)
175+ compareSessionStates(t, tc.Output, ss)
176+ })
177+ }
314178}
315179
316−func TestIntoEncryptAndIntoDecrypt(t *testing.T) {
180+// Test_into tests the into helper function used in LegacyV5DecodeSessionState
181+//
182+// TODO: Remove when this is deprecated (likely V7)
183+func Test_into(t *testing.T) {
317184 const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
318185
319186 // Test all 3 valid AES sizes
@@ -323,8 +190,9 @@ func TestIntoEncryptAndIntoDecrypt(t *testing.T) {
323190 _, err := io.ReadFull(rand.Reader, secret)
324191 assert.Equal(t, nil, err)
325192
326− c, err := newTestCipher(secret)
193+ cfb, err := encryption.NewCFBCipher(secret)
327194 assert.NoError(t, err)
195+ c := encryption.NewBase64Cipher(cfb)
328196
329197 // Check no errors with empty or nil strings
330198 empty := ""
@@ -353,3 +221,27 @@ func TestIntoEncryptAndIntoDecrypt(t *testing.T) {
353221 })
354222 }
355223}
224+
225+func compareSessionStates(t *testing.T, expected *SessionState, actual *SessionState) {
226+ if expected.CreatedAt != nil {
227+ assert.NotNil(t, actual.CreatedAt)
228+ assert.Equal(t, true, expected.CreatedAt.Equal(*actual.CreatedAt))
229+ } else {
230+ assert.Nil(t, actual.CreatedAt)
231+ }
232+ if expected.ExpiresOn != nil {
233+ assert.NotNil(t, actual.ExpiresOn)
234+ assert.Equal(t, true, expected.ExpiresOn.Equal(*actual.ExpiresOn))
235+ } else {
236+ assert.Nil(t, actual.ExpiresOn)
237+ }
238+
239+ // Compare sessions without *time.Time fields
240+ exp := *expected
241+ exp.CreatedAt = nil
242+ exp.ExpiresOn = nil
243+ act := *actual
244+ act.CreatedAt = nil
245+ act.ExpiresOn = nil
246+ assert.Equal(t, exp, act)
247+}
pkg/encryption/cipher.go+2−6
@@ -21,12 +21,8 @@ type base64Cipher struct {
2121
2222// NewBase64Cipher returns a new AES Cipher for encrypting cookie values
2323// and wrapping them in Base64 -- Supports Legacy encryption scheme
24−func NewBase64Cipher(initCipher func([]byte) (Cipher, error), secret []byte) (Cipher, error) {
25− c, err := initCipher(secret)
26− if err != nil {
27− return nil, err
28− }
29− return &base64Cipher{Cipher: c}, nil
24+func NewBase64Cipher(c Cipher) Cipher {
25+ return &base64Cipher{Cipher: c}
3026}
3127
3228// Encrypt encrypts a value with the embedded Cipher & Base64 encodes it
pkg/encryption/cipher_test.go+7−6
@@ -13,8 +13,9 @@ import (
1313func TestEncodeAndDecodeAccessToken(t *testing.T) {
1414 const secret = "0123456789abcdefghijklmnopqrstuv"
1515 const token = "my access token"
16− c, err := NewBase64Cipher(NewCFBCipher, []byte(secret))
17− assert.Equal(t, nil, err)
16+ cfb, err := NewCFBCipher([]byte(secret))
17+ assert.NoError(t, err)
18+ c := NewBase64Cipher(cfb)
1819
1920 encoded, err := c.Encrypt([]byte(token))
2021 assert.Equal(t, nil, err)
@@ -32,8 +33,9 @@ func TestEncodeAndDecodeAccessTokenB64(t *testing.T) {
3233
3334 secret, err := base64.URLEncoding.DecodeString(secretBase64)
3435 assert.Equal(t, nil, err)
35− c, err := NewBase64Cipher(NewCFBCipher, []byte(secret))
36− assert.Equal(t, nil, err)
36+ cfb, err := NewCFBCipher([]byte(secret))
37+ assert.NoError(t, err)
38+ c := NewBase64Cipher(cfb)
3739
3840 encoded, err := c.Encrypt([]byte(token))
3941 assert.Equal(t, nil, err)
@@ -64,8 +66,7 @@ func TestEncryptAndDecrypt(t *testing.T) {
6466 cstd, err := initCipher(secret)
6567 assert.Equal(t, nil, err)
6668
67− cb64, err := NewBase64Cipher(initCipher, secret)
68− assert.Equal(t, nil, err)
69+ cb64 := NewBase64Cipher(cstd)
6970
7071 ciphers := map[string]Cipher{
7172 "Standard": cstd,
pkg/sessions/cookie/session_store.go+21−12
@@ -60,7 +60,7 @@ func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
6060 return nil, errors.New("cookie signature not valid")
6161 }
6262
63− session, err := sessionFromCookie(string(val), s.CookieCipher)
63+ session, err := sessionFromCookie(val, s.CookieCipher)
6464 if err != nil {
6565 return nil, err
6666 }
@@ -85,17 +85,26 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
8585}
8686
8787// cookieForSession serializes a session state for storage in a cookie
88−func cookieForSession(s *sessions.SessionState, c encryption.Cipher) (string, error) {
89− return s.EncodeSessionState(c)
88+func cookieForSession(s *sessions.SessionState, c encryption.Cipher) ([]byte, error) {
89+ return s.EncodeSessionState(c, true)
9090}
9191
9292// sessionFromCookie deserializes a session from a cookie value
93−func sessionFromCookie(v string, c encryption.Cipher) (s *sessions.SessionState, err error) {
94− return sessions.DecodeSessionState(v, c)
93+func sessionFromCookie(v []byte, c encryption.Cipher) (s *sessions.SessionState, err error) {
94+ ss, err := sessions.DecodeSessionState(v, c, true)
95+ // If anything fails (Decrypt, LZ4, MessagePack), try legacy JSON decode
96+ // LZ4 will likely fail for wrong header after AES-CFB spits out garbage
97+ // data from trying to decrypt JSON it things is ciphertext
98+ if err != nil {
99+ // Legacy used Base64 + AES CFB
100+ legacyCipher := encryption.NewBase64Cipher(c)
101+ return sessions.LegacyV5DecodeSessionState(string(v), legacyCipher)
102+ }
103+ return ss, nil
95104}
96105
97106// setSessionCookie adds the user's session cookie to the response
98−func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val string, created time.Time) {
107+func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val []byte, created time.Time) {
99108 for _, c := range s.makeSessionCookie(req, val, created) {
100109 http.SetCookie(rw, c)
101110 }
@@ -103,12 +112,12 @@ func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Reques
103112
104113// makeSessionCookie creates an http.Cookie containing the authenticated user's
105114// authentication details
106−func (s *SessionStore) makeSessionCookie(req *http.Request, value string, now time.Time) []*http.Cookie {
107− if value != "" {
108− value = encryption.SignedValue(s.Cookie.Secret, s.Cookie.Name, []byte(value), now)
115+func (s *SessionStore) makeSessionCookie(req *http.Request, value []byte, now time.Time) []*http.Cookie {
116+ strValue := string(value)
117+ if strValue != "" {
118+ strValue = encryption.SignedValue(s.Cookie.Secret, s.Cookie.Name, value, now)
109119 }
110− c := s.makeCookie(req, s.Cookie.Name, value, s.Cookie.Expire, now)
111−
120+ c := s.makeCookie(req, s.Cookie.Name, strValue, s.Cookie.Expire, now)
112121 if len(c.String()) > maxCookieLength {
113122 return splitCookie(c)
114123 }
@@ -129,7 +138,7 @@ func (s *SessionStore) makeCookie(req *http.Request, name string, value string,
129138// NewCookieSessionStore initialises a new instance of the SessionStore from
130139// the configuration given
131140func NewCookieSessionStore(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessions.SessionStore, error) {
132− cipher, err := encryption.NewBase64Cipher(encryption.NewCFBCipher, encryption.SecretBytes(cookieOpts.Secret))
141+ cipher, err := encryption.NewCFBCipher(encryption.SecretBytes(cookieOpts.Secret))
133142 if err != nil {
134143 return nil, fmt.Errorf("error initialising cipher: %v", err)
135144 }
pkg/sessions/redis/redis_store.go+75−56
@@ -40,7 +40,7 @@ type SessionStore struct {
4040// NewRedisSessionStore initialises a new instance of the SessionStore from
4141// the configuration given
4242func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessions.SessionStore, error) {
43− cipher, err := encryption.NewBase64Cipher(encryption.NewCFBCipher, encryption.SecretBytes(cookieOpts.Secret))
43+ cfbCipher, err := encryption.NewCFBCipher(encryption.SecretBytes(cookieOpts.Secret))
4444 if err != nil {
4545 return nil, fmt.Errorf("error initialising cipher: %v", err)
4646 }
@@ -52,7 +52,7 @@ func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.Cook
5252
5353 rs := &SessionStore{
5454 Client: client,
55− CookieCipher: cipher,
55+ CookieCipher: cfbCipher,
5656 Cookie: cookieOpts,
5757 }
5858 return rs, nil
@@ -146,12 +146,8 @@ func (store *SessionStore) Save(rw http.ResponseWriter, req *http.Request, s *se
146146 // Old sessions that we are refreshing would have a request cookie
147147 // New sessions don't, so we ignore the error. storeValue will check requestCookie
148148 requestCookie, _ := req.Cookie(store.Cookie.Name)
149− value, err := s.EncodeSessionState(store.CookieCipher)
150− if err != nil {
151− return err
152− }
153149 ctx := req.Context()
154− ticketString, err := store.storeValue(ctx, value, store.Cookie.Expire, requestCookie)
150+ ticketString, err := store.saveSession(ctx, s, store.Cookie.Expire, requestCookie)
155151 if err != nil {
156152 return err
157153 }
@@ -180,40 +176,13 @@ func (store *SessionStore) Load(req *http.Request) (*sessions.SessionState, erro
180176 return nil, fmt.Errorf("cookie signature not valid")
181177 }
182178 ctx := req.Context()
183− session, err := store.loadSessionFromString(ctx, string(val))
179+ session, err := store.loadSessionFromTicket(ctx, string(val))
184180 if err != nil {
185181 return nil, fmt.Errorf("error loading session: %s", err)
186182 }
187183 return session, nil
188184}
189185
190−// loadSessionFromString loads the session based on the ticket value
191−func (store *SessionStore) loadSessionFromString(ctx context.Context, value string) (*sessions.SessionState, error) {
192− ticket, err := decodeTicket(store.Cookie.Name, value)
193− if err != nil {
194− return nil, err
195− }
196−
197− resultBytes, err := store.Client.Get(ctx, ticket.asHandle(store.Cookie.Name))
198− if err != nil {
199− return nil, err
200− }
201−
202− block, err := aes.NewCipher(ticket.Secret)
203− if err != nil {
204− return nil, err
205− }
206− // Use secret as the IV too, because each entry has it's own key
207− stream := cipher.NewCFBDecrypter(block, ticket.Secret)
208− stream.XORKeyStream(resultBytes, resultBytes)
209−
210− session, err := sessions.DecodeSessionState(string(resultBytes), store.CookieCipher)
211− if err != nil {
212− return nil, err
213− }
214− return session, nil
215−}
216−
217186// Clear clears any saved session information for a given ticket cookie
218187// from redis, and then clears the session
219188func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
@@ -253,36 +222,25 @@ func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro
253222 return nil
254223}
255224
256−// makeCookie makes a cookie, signing the value if present
257−func (store *SessionStore) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) *http.Cookie {
258− if value != "" {
259− value = encryption.SignedValue(store.Cookie.Secret, store.Cookie.Name, []byte(value), now)
260− }
261− return cookies.MakeCookieFromOptions(
262− req,
263− store.Cookie.Name,
264− value,
265− store.Cookie,
266− expires,
267− now,
268− )
269−}
270−
271−func (store *SessionStore) storeValue(ctx context.Context, value string, expiration time.Duration, requestCookie *http.Cookie) (string, error) {
225+// saveSession encodes a session with a GCM cipher & saves the data into Redis
226+func (store *SessionStore) saveSession(ctx context.Context, s *sessions.SessionState, expiration time.Duration, requestCookie *http.Cookie) (string, error) {
272227 ticket, err := store.getTicket(requestCookie)
273228 if err != nil {
274229 return "", fmt.Errorf("error getting ticket: %v", err)
275230 }
276231
277− ciphertext := make([]byte, len(value))
278− block, err := aes.NewCipher(ticket.Secret)
232+ c, err := encryption.NewGCMCipher(ticket.Secret)
279233 if err != nil {
280234 return "", fmt.Errorf("error initiating cipher block %s", err)
281235 }
282236
283− // Use secret as the Initialization Vector too, because each entry has it's own key
284− stream := cipher.NewCFBEncrypter(block, ticket.Secret)
285− stream.XORKeyStream(ciphertext, []byte(value))
237+ // Use AES-GCM since it provides authenticated encryption
238+ // AES-CFB used in cookies has the cookie signing SHA to get around the lack of
239+ // authentication in AES-CFB
240+ ciphertext, err := s.EncodeSessionState(c, false)
241+ if err != nil {
242+ return "", err
243+ }
286244
287245 handle := ticket.asHandle(store.Cookie.Name)
288246 err = store.Client.Set(ctx, handle, ciphertext, expiration)
@@ -292,6 +250,67 @@ func (store *SessionStore) storeValue(ctx context.Context, value string, expirat
292250 return ticket.encodeTicket(store.Cookie.Name), nil
293251}
294252
253+// loadSessionFromTicket loads the session based on the ticket value
254+func (store *SessionStore) loadSessionFromTicket(ctx context.Context, value string) (*sessions.SessionState, error) {
255+ ticket, err := decodeTicket(store.Cookie.Name, value)
256+ if err != nil {
257+ return nil, err
258+ }
259+
260+ resultBytes, err := store.Client.Get(ctx, ticket.asHandle(store.Cookie.Name))
261+ if err != nil {
262+ return nil, err
263+ }
264+
265+ c, err := encryption.NewGCMCipher(ticket.Secret)
266+ if err != nil {
267+ return nil, err
268+ }
269+
270+ session, err := sessions.DecodeSessionState(resultBytes, c, false)
271+ if err != nil {
272+ // The GCM cipher will error due to a legacy JSON payload not passing
273+ // the authentication check part of AES GCM encryption.
274+ // In that case, we can attempt to fallback to try a legacy load
275+ legacyCipher := encryption.NewBase64Cipher(store.CookieCipher)
276+ return legacyV5DecodeSession(resultBytes, ticket, legacyCipher)
277+ }
278+ return session, nil
279+}
280+
281+// legacyV5DecodeSession loads the session based on the ticket value
282+// This fallback uses V5 style encryption of Base64 + AES CFB
283+func legacyV5DecodeSession(resultBytes []byte, ticket *TicketData, c encryption.Cipher) (*sessions.SessionState, error) {
284+ block, err := aes.NewCipher(ticket.Secret)
285+ if err != nil {
286+ return nil, err
287+ }
288+ // Use secret as the IV too, because each entry has it's own key
289+ stream := cipher.NewCFBDecrypter(block, ticket.Secret)
290+ stream.XORKeyStream(resultBytes, resultBytes)
291+
292+ session, err := sessions.LegacyV5DecodeSessionState(string(resultBytes), c)
293+ if err != nil {
294+ return nil, err
295+ }
296+ return session, nil
297+}
298+
299+// makeCookie makes a cookie, signing the value if present
300+func (store *SessionStore) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) *http.Cookie {
301+ if value != "" {
302+ value = encryption.SignedValue(store.Cookie.Secret, store.Cookie.Name, []byte(value), now)
303+ }
304+ return cookies.MakeCookieFromOptions(
305+ req,
306+ store.Cookie.Name,
307+ value,
308+ store.Cookie,
309+ expires,
310+ now,
311+ )
312+}
313+
295314// getTicket retrieves an existing ticket from the cookie if present,
296315// or creates a new ticket
297316func (store *SessionStore) getTicket(requestCookie *http.Cookie) (*TicketData, error) {
pkg/sessions/redis/session_store_test.go → pkg/sessions/redis/redis_store_test.gorenamed+64
@@ -1,6 +1,11 @@
11package redis
22
33import (
4+ "crypto/aes"
5+ "crypto/cipher"
6+ "crypto/rand"
7+ "fmt"
8+ "io"
49 "log"
510 "os"
611 "testing"
@@ -17,6 +22,65 @@ import (
1722 . "github.com/onsi/gomega"
1823)
1924
25+// TestLegacyV5DecodeSession tests the fallback to LegacyV5DecodeSession
26+// when a V5 encoded session is in Redis
27+//
28+// TODO: Remove when this is deprecated (likely V7)
29+func Test_legacyV5DecodeSession(t *testing.T) {
30+ testCases, _, legacyCipher := sessionsapi.CreateLegacyV5TestCases(t)
31+
32+ for testName, tc := range testCases {
33+ t.Run(testName, func(t *testing.T) {
34+ g := NewWithT(t)
35+
36+ secret := make([]byte, aes.BlockSize)
37+ _, err := io.ReadFull(rand.Reader, secret)
38+ g.Expect(err).ToNot(HaveOccurred())
39+ ticket := &TicketData{
40+ TicketID: "",
41+ Secret: secret,
42+ }
43+
44+ encrypted, err := legacyStoreValue(tc.Input, ticket.Secret)
45+ g.Expect(err).ToNot(HaveOccurred())
46+
47+ ss, err := legacyV5DecodeSession(encrypted, ticket, legacyCipher)
48+ if tc.Error {
49+ g.Expect(err).To(HaveOccurred())
50+ g.Expect(ss).To(BeNil())
51+ return
52+ }
53+ g.Expect(err).ToNot(HaveOccurred())
54+
55+ // Compare sessions without *time.Time fields
56+ exp := *tc.Output
57+ exp.CreatedAt = nil
58+ exp.ExpiresOn = nil
59+ act := *ss
60+ act.CreatedAt = nil
61+ act.ExpiresOn = nil
62+ g.Expect(exp).To(Equal(act))
63+ })
64+ }
65+}
66+
67+// legacyStoreValue implements the legacy V5 Redis store AES-CFB value encryption
68+//
69+// TODO: Remove when this is deprecated (likely V7)
70+func legacyStoreValue(value string, ticketSecret []byte) ([]byte, error) {
71+ ciphertext := make([]byte, len(value))
72+ block, err := aes.NewCipher(ticketSecret)
73+ if err != nil {
74+ return nil, fmt.Errorf("error initiating cipher block: %v", err)
75+ }
76+
77+ // Use secret as the Initialization Vector too, because each entry has it's own key
78+ stream := cipher.NewCFBEncrypter(block, ticketSecret)
79+ stream.XORKeyStream(ciphertext, []byte(value))
80+
81+ return ciphertext, nil
82+}
83+
2084func TestSessionStore(t *testing.T) {
2185 logger.SetOutput(GinkgoWriter)
2286