git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add redis lock feature (#1063)

* Add sensible logging flag to default setup for logger * Add Redis lock * Fix default value flag for sensitive logging * Split RefreshSessionIfNeeded in two methods and use Redis lock * Small adjustments to doc and code * Remove sensible logging * Fix method names in ticket.go * Revert "Fix method names in ticket.go" This reverts commit 408ba1a1a5c55a3cad507a0be8634af1977769cb. * Fix methods name in ticket.go * Remove block in Redis client get * Increase lock time to 1 second * Perform retries, if session store is locked * Reverse if condition, because it should return if session does not have to be refreshed * Update go.sum * Update MockStore * Return error if loading session fails * Fix and update tests * Change validSession to session in docs and strings * Change validSession to session in docs and strings * Fix docs * Fix wrong field name * Fix linting * Fix imports for linting * Revert changes except from locking functionality * Add lock feature on session state * Update from master * Remove errors package, because it is not used * Only pass context instead of request to lock * Use lock key * By default use NoOpLock * Remove debug output * Update ticket_test.go * Map internal error to sessions error * Add ErrLockNotObtained * Enable lock peek for all redis clients * Use lock key prefix consistent * Fix imports * Use exists method for peek lock * Fix imports * Fix imports * Fix imports * Remove own Dockerfile * Fix imports * Fix tests for ticket and session store * Fix session store test * Update pkg/apis/sessions/interfaces.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Do not wrap lock method Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Use errors package for lock constants * Use better naming for initLock function * Add comments * Add session store lock test * Fix tests * Fix tests * Fix tests * Fix tests * Add cookies after saving session * Add mock lock * Fix imports for mock_lock.go * Store mock lock for key * Apply elapsed time on mock lock * Check if lock is initially applied * Reuse existing lock * Test all lock methods * Update CHANGELOG.md * Use redis client methods in redis.lock for release an refresh * Use lock key suffix instead of prefix for lock key * Add comments for Lock interface * Update comment for Lock interface * Update CHANGELOG.md * Change LockSuffix to const * Check lock on already loaded session * Use global var for loadedSession in lock tests * Use lock instance for refreshing and releasing of lock * Update possible error type for Refresh Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>

commitf648c54d87c0422cbb4a58e76874ade5ace438ce authorKevin Kreitner <kevin.kreitner@real-digital.de> dateWed 2 Jun 2021, 20:08 (5 years ago) parent67bfa4b
17 files changed+389−21
CHANGELOG.md+2 go.mod+1 go.sum+17 pkg/apis/sessions/interfaces.go+24 pkg/apis/sessions/lock.go+24 pkg/apis/sessions/session_state.go+31 pkg/middleware/stored_session_test.go+1−1 pkg/sessions/persistence/interfaces.go+3 pkg/sessions/persistence/manager.go+6−3 pkg/sessions/persistence/ticket.go+13−2 pkg/sessions/persistence/ticket_test.go+18−7 pkg/sessions/redis/client.go+16−2 pkg/sessions/redis/lock.go+84 pkg/sessions/redis/redis_store.go+7−2 pkg/sessions/tests/mock_lock.go+48 pkg/sessions/tests/mock_store.go+20−4 pkg/sessions/tests/session_store_tests.go+74
CHANGELOG.md+2
@@ -8,11 +8,13 @@
88
99## Changes since v7.1.3
1010
11+- [#1063](https://github.com/oauth2-proxy/oauth2-proxy/pull/1063) Add Redis lock feature to lock persistent sessions (@Bibob7)
1112- [#1108](https://github.com/oauth2-proxy/oauth2-proxy/pull/1108) Add alternative ways to generate cookie secrets to docs (@JoelSpeed)
1213- [#1142](https://github.com/oauth2-proxy/oauth2-proxy/pull/1142) Add pagewriter to upstream proxy (@JoelSpeed)
1314- [#1181](https://github.com/oauth2-proxy/oauth2-proxy/pull/1181) Fix incorrect `cfg` name in show-debug-on-error flag (@iTaybb)
1415- [#1207](https://github.com/oauth2-proxy/oauth2-proxy/pull/1207) Fix URI fragment handling on sign-in page, regression introduced in 7.1.0 (@tarvip)
1516
17+
1618# V7.1.3
1719
1820## Release Highlights
go.mod+1
@@ -8,6 +8,7 @@ require (
88 github.com/benbjohnson/clock v1.1.1-0.20210213131748-c97fc7b6bee0
99 github.com/bitly/go-simplejson v0.5.0
1010 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
11+ github.com/bsm/redislock v0.7.0
1112 github.com/coreos/go-oidc v2.2.1+incompatible
1213 github.com/dgrijalva/jwt-go v3.2.0+incompatible
1314 github.com/frankban/quicktest v1.10.0 // indirect
go.sum+17
@@ -2,10 +2,12 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
22cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
33cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
44cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
5+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
56github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb h1:ZVN4Iat3runWOFLaBCDVU5a9X/XikSRBosye++6gojw=
67github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss=
78github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
89github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
10+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
911github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc=
1012github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI=
1113github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@@ -49,6 +51,8 @@ github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkN
4951github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
5052github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
5153github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
54+github.com/bsm/redislock v0.7.0 h1:RL7aZJhCKkuBjQbnSTKCeedTRifBWxd/ffP+GZ599Mo=
55+github.com/bsm/redislock v0.7.0/go.mod h1:3Kgu+cXw0JrkZ5pmY/JbcFpixGZ5M9v9G2PGWYqku+k=
5256github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
5357github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
5458github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -107,6 +111,7 @@ github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2H
107111github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
108112github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
109113github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
114+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
110115github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
111116github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
112117github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
@@ -120,6 +125,7 @@ github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+
120125github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
121126github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
122127github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
128+github.com/go-redis/redis/v8 v8.1.0/go.mod h1:isLoQT/NFSP7V67lyvM9GmdvLdyZ7pEhsXvvyQtnQTo=
123129github.com/go-redis/redis/v8 v8.2.3 h1:eNesND+DWt/sjQOtPFxAbQkTIXaXX00qNLxjVWkZ70k=
124130github.com/go-redis/redis/v8 v8.2.3/go.mod h1:ysgGY09J/QeDYbu3HikWEIPCwaeOkuNoTgKayTEaEOw=
125131github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@@ -325,6 +331,7 @@ github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUM
325331github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
326332github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
327333github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
334+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
328335github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
329336github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
330337github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -454,16 +461,23 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
454461golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
455462golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
456463golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
464+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
465+golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw=
466+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
467+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
457468golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
458469golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
459470golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
460471golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
461472golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
462473golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
474+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
463475golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
464476golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
465477golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
466478golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
479+golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ=
480+golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
467481golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
468482golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
469483golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -512,6 +526,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
512526golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
513527golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
514528golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
529+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
515530golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
516531golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
517532golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -519,6 +534,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
519534golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
520535golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
521536golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
537+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
522538golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
523539golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
524540golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -555,6 +571,7 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn
555571golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
556572golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
557573golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
574+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
558575golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8 h1:BMFHd4OFnFtWX46Xj4DN6vvT1btiBxyq+s0orYBqcQY=
559576golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
560577golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
pkg/apis/sessions/interfaces.go+24
@@ -1,7 +1,10 @@
11package sessions
22
33import (
4+ "context"
5+ "errors"
46 "net/http"
7+ "time"
58)
69
710// SessionStore is an interface to storing user sessions in the proxy
@@ -10,3 +13,24 @@ type SessionStore interface {
1013 Load(req *http.Request) (*SessionState, error)
1114 Clear(rw http.ResponseWriter, req *http.Request) error
1215}
16+
17+var ErrLockNotObtained = errors.New("lock: not obtained")
18+var ErrNotLocked = errors.New("tried to release not existing lock")
19+
20+// Lock is an interface for controlling session locks
21+type Lock interface {
22+ // Obtain obtains the lock on the distributed
23+ // lock resource if no lock exists yet.
24+ // Otherwise it will return ErrLockNotObtained
25+ Obtain(ctx context.Context, expiration time.Duration) error
26+ // Peek returns true if the lock currently exists
27+ // Otherwise it returns false.
28+ Peek(ctx context.Context) (bool, error)
29+ // Refresh refreshes the expiration time of the lock,
30+ // if is still applied.
31+ // Otherwise it will return ErrNotLocked
32+ Refresh(ctx context.Context, expiration time.Duration) error
33+ // Release removes the existing lock,
34+ // Otherwise it will return ErrNotLocked
35+ Release(ctx context.Context) error
36+}
pkg/apis/sessions/lock.gonew file+24
@@ -0,0 +1,24 @@
1+package sessions
2+
3+import (
4+ "context"
5+ "time"
6+)
7+
8+type NoOpLock struct{}
9+
10+func (l *NoOpLock) Obtain(ctx context.Context, expiration time.Duration) error {
11+ return nil
12+}
13+
14+func (l *NoOpLock) Peek(ctx context.Context) (bool, error) {
15+ return false, nil
16+}
17+
18+func (l *NoOpLock) Refresh(ctx context.Context, expiration time.Duration) error {
19+ return nil
20+}
21+
22+func (l *NoOpLock) Release(ctx context.Context) error {
23+ return nil
24+}
pkg/apis/sessions/session_state.go+31
@@ -2,6 +2,7 @@ package sessions
22
33import (
44 "bytes"
5+ "context"
56 "errors"
67 "fmt"
78 "io"
@@ -30,6 +31,36 @@ type SessionState struct {
3031 User string `msgpack:"u,omitempty"`
3132 Groups []string `msgpack:"g,omitempty"`
3233 PreferredUsername string `msgpack:"pu,omitempty"`
34+
35+ Lock Lock `msgpack:"-"`
36+}
37+
38+func (s *SessionState) ObtainLock(ctx context.Context, expiration time.Duration) error {
39+ if s.Lock == nil {
40+ s.Lock = &NoOpLock{}
41+ }
42+ return s.Lock.Obtain(ctx, expiration)
43+}
44+
45+func (s *SessionState) RefreshLock(ctx context.Context, expiration time.Duration) error {
46+ if s.Lock == nil {
47+ s.Lock = &NoOpLock{}
48+ }
49+ return s.Lock.Refresh(ctx, expiration)
50+}
51+
52+func (s *SessionState) ReleaseLock(ctx context.Context) error {
53+ if s.Lock == nil {
54+ s.Lock = &NoOpLock{}
55+ }
56+ return s.Lock.Release(ctx)
57+}
58+
59+func (s *SessionState) PeekLock(ctx context.Context) (bool, error) {
60+ if s.Lock == nil {
61+ s.Lock = &NoOpLock{}
62+ }
63+ return s.Lock.Peek(ctx)
3364}
3465
3566// IsExpired checks whether the session has expired
pkg/middleware/stored_session_test.go+1−1
@@ -101,7 +101,7 @@ var _ = Describe("Stored Session Suite", func() {
101101 Session: in.existingSession,
102102 }
103103
104− // Set up the request with the request headesr and a request scope
104+ // Set up the request with the request header and a request scope
105105 req := httptest.NewRequest("", "/", nil)
106106 req.Header = in.requestHeaders
107107 req = middlewareapi.AddRequestScope(req, scope)
pkg/sessions/persistence/interfaces.go+3
@@ -3,6 +3,8 @@ package persistence
33import (
44 "context"
55 "time"
6+
7+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
68)
79
810// Store is used for persistent session stores (IE not Cookie)
@@ -12,4 +14,5 @@ type Store interface {
1214 Save(context.Context, string, []byte, time.Duration) error
1315 Load(context.Context, string) ([]byte, error)
1416 Clear(context.Context, string) error
17+ Lock(key string) sessions.Lock
1518}
pkg/sessions/persistence/manager.go+6−3
@@ -60,9 +60,12 @@ func (m *Manager) Load(req *http.Request) (*sessions.SessionState, error) {
6060 return nil, err
6161 }
6262
63− return tckt.loadSession(func(key string) ([]byte, error) {
64− return m.Store.Load(req.Context(), key)
65− })
63+ return tckt.loadSession(
64+ func(key string) ([]byte, error) {
65+ return m.Store.Load(req.Context(), key)
66+ },
67+ m.Store.Lock,
68+ )
6669}
6770
6871// Clear clears any saved session information for a given ticket cookie.
pkg/sessions/persistence/ticket.go+13−2
@@ -30,6 +30,10 @@ type loadFunc func(string) ([]byte, error)
3030// a string key for the target of the deletion.
3131type clearFunc func(string) error
3232
33+// initLockFunc returns a lock object for a persistent store using a
34+// string key
35+type initLockFunc func(string) sessions.Lock
36+
3337// ticket is a structure representing the ticket used in server based
3438// session storage. It provides a unique per session decryption secret giving
3539// more security than the shared CookieSecret.
@@ -122,7 +126,8 @@ func (t *ticket) saveSession(s *sessions.SessionState, saver saveFunc) error {
122126// loadSession loads a session from the disk store via the passed loadFunc
123127// using the ticket.id as the key. It then decodes the SessionState using
124128// ticket.secret to make the AES-GCM cipher.
125−func (t *ticket) loadSession(loader loadFunc) (*sessions.SessionState, error) {
129+// finally it appends a lock implementation
130+func (t *ticket) loadSession(loader loadFunc, initLock initLockFunc) (*sessions.SessionState, error) {
126131 ciphertext, err := loader(t.id)
127132 if err != nil {
128133 return nil, fmt.Errorf("failed to load the session state with the ticket: %v", err)
@@ -132,7 +137,13 @@ func (t *ticket) loadSession(loader loadFunc) (*sessions.SessionState, error) {
132137 return nil, err
133138 }
134139
135− return sessions.DecodeSessionState(ciphertext, c, false)
140+ sessionState, err := sessions.DecodeSessionState(ciphertext, c, false)
141+ if err != nil {
142+ return nil, err
143+ }
144+ lock := initLock(t.id)
145+ sessionState.Lock = lock
146+ return sessionState, nil
136147}
137148
138149// clearSession uses the passed clearFunc to delete a session stored with a
pkg/sessions/persistence/ticket_test.go+18−7
@@ -103,10 +103,17 @@ var _ = Describe("Session Ticket Tests", func() {
103103 c, err := t.makeCipher()
104104 Expect(err).ToNot(HaveOccurred())
105105
106− ss := &sessions.SessionState{User: "foobar"}
107− loadedSession, err := t.loadSession(func(k string) ([]byte, error) {
108− return ss.EncodeSessionState(c, false)
109− })
106+ ss := &sessions.SessionState{
107+ User: "foobar",
108+ Lock: &sessions.NoOpLock{},
109+ }
110+ loadedSession, err := t.loadSession(
111+ func(k string) ([]byte, error) {
112+ return ss.EncodeSessionState(c, false)
113+ },
114+ func(k string) sessions.Lock {
115+ return &sessions.NoOpLock{}
116+ })
110117 Expect(err).ToNot(HaveOccurred())
111118 Expect(loadedSession).To(Equal(ss))
112119 })
@@ -115,9 +122,13 @@ var _ = Describe("Session Ticket Tests", func() {
115122 t, err := newTicket(&options.Cookie{Name: "dummy"})
116123 Expect(err).ToNot(HaveOccurred())
117124
118− data, err := t.loadSession(func(k string) ([]byte, error) {
119− return nil, errors.New("load error")
120− })
125+ data, err := t.loadSession(
126+ func(k string) ([]byte, error) {
127+ return nil, errors.New("load error")
128+ },
129+ func(k string) sessions.Lock {
130+ return &sessions.NoOpLock{}
131+ })
121132 Expect(data).To(BeNil())
122133 Expect(err).To(MatchError(errors.New("failed to load the session state with the ticket: load error")))
123134 })
pkg/sessions/redis/client.go+16−2
@@ -5,11 +5,13 @@ import (
55 "time"
66
77 "github.com/go-redis/redis/v8"
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
89)
910
1011// Client is wrapper interface for redis.Client and redis.ClusterClient.
1112type Client interface {
1213 Get(ctx context.Context, key string) ([]byte, error)
14+ Lock(key string) sessions.Lock
1315 Set(ctx context.Context, key string, value []byte, expiration time.Duration) error
1416 Del(ctx context.Context, key string) error
1517}
@@ -21,7 +23,9 @@ type client struct {
2123}
2224
2325func newClient(c *redis.Client) Client {
24− return &client{Client: c}
26+ return &client{
27+ Client: c,
28+ }
2529}
2630
2731func (c *client) Get(ctx context.Context, key string) ([]byte, error) {
@@ -36,6 +40,10 @@ func (c *client) Del(ctx context.Context, key string) error {
3640 return c.Client.Del(ctx, key).Err()
3741}
3842
43+func (c *client) Lock(key string) sessions.Lock {
44+ return NewLock(c.Client, key)
45+}
46+
3947var _ Client = (*clusterClient)(nil)
4048
4149type clusterClient struct {
@@ -43,7 +51,9 @@ type clusterClient struct {
4351}
4452
4553func newClusterClient(c *redis.ClusterClient) Client {
46− return &clusterClient{ClusterClient: c}
54+ return &clusterClient{
55+ ClusterClient: c,
56+ }
4757}
4858
4959func (c *clusterClient) Get(ctx context.Context, key string) ([]byte, error) {
@@ -57,3 +67,7 @@ func (c *clusterClient) Set(ctx context.Context, key string, value []byte, expir
5767func (c *clusterClient) Del(ctx context.Context, key string) error {
5868 return c.ClusterClient.Del(ctx, key).Err()
5969}
70+
71+func (c *clusterClient) Lock(key string) sessions.Lock {
72+ return NewLock(c.ClusterClient, key)
73+}
pkg/sessions/redis/lock.gonew file+84
@@ -0,0 +1,84 @@
1+package redis
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "time"
8+
9+ "github.com/bsm/redislock"
10+ "github.com/go-redis/redis/v8"
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12+)
13+
14+const LockSuffix = "lock"
15+
16+type Lock struct {
17+ client redis.Cmdable
18+ locker *redislock.Client
19+ lock *redislock.Lock
20+ key string
21+}
22+
23+// NewLock instantiate a new lock instance. This will not yet apply a lock on Redis side.
24+// For that you have to call Obtain(ctx context.Context, expiration time.Duration)
25+func NewLock(client redis.Cmdable, key string) sessions.Lock {
26+ return &Lock{
27+ client: client,
28+ locker: redislock.New(client),
29+ key: key,
30+ }
31+}
32+
33+// Obtain obtains a distributed lock on Redis for the configured key.
34+func (l *Lock) Obtain(ctx context.Context, expiration time.Duration) error {
35+ lock, err := l.locker.Obtain(ctx, l.lockKey(), expiration, nil)
36+ if errors.Is(err, redislock.ErrNotObtained) {
37+ return sessions.ErrLockNotObtained
38+ }
39+ if err != nil {
40+ return err
41+ }
42+ l.lock = lock
43+ return nil
44+}
45+
46+// Refresh refreshes an already existing lock.
47+func (l *Lock) Refresh(ctx context.Context, expiration time.Duration) error {
48+ if l.lock == nil {
49+ return sessions.ErrNotLocked
50+ }
51+ err := l.lock.Refresh(ctx, expiration, nil)
52+ if errors.Is(err, redislock.ErrNotObtained) {
53+ return sessions.ErrNotLocked
54+ }
55+ return err
56+}
57+
58+// Peek returns true, if the lock is still applied.
59+func (l *Lock) Peek(ctx context.Context) (bool, error) {
60+ v, err := l.client.Exists(ctx, l.lockKey()).Result()
61+ if err != nil {
62+ return false, err
63+ }
64+ if v == 0 {
65+ return false, nil
66+ }
67+ return true, nil
68+}
69+
70+// Release releases the lock on Redis side.
71+func (l *Lock) Release(ctx context.Context) error {
72+ if l.lock == nil {
73+ return sessions.ErrNotLocked
74+ }
75+ err := l.lock.Release(ctx)
76+ if errors.Is(err, redislock.ErrLockNotHeld) {
77+ return sessions.ErrNotLocked
78+ }
79+ return err
80+}
81+
82+func (l *Lock) lockKey() string {
83+ return fmt.Sprintf("%s.%s", l.key, LockSuffix)
84+}
pkg/sessions/redis/redis_store.go+7−2
@@ -35,7 +35,7 @@ func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.Cook
3535}
3636
3737// Save takes a sessions.SessionState and stores the information from it
38−// to redies, and adds a new persistence cookie on the HTTP response writer
38+// to redis, and adds a new persistence cookie on the HTTP response writer
3939func (store *SessionStore) Save(ctx context.Context, key string, value []byte, exp time.Duration) error {
4040 err := store.Client.Set(ctx, key, value, exp)
4141 if err != nil {
@@ -64,6 +64,11 @@ func (store *SessionStore) Clear(ctx context.Context, key string) error {
6464 return nil
6565}
6666
67+// Lock creates a lock object for sessions.SessionState
68+func (store *SessionStore) Lock(key string) sessions.Lock {
69+ return store.Client.Lock(key)
70+}
71+
6772// NewRedisClient makes a redis.Client (either standalone, sentinel aware, or
6873// redis cluster)
6974func NewRedisClient(opts options.RedisStoreOptions) (Client, error) {
@@ -151,7 +156,7 @@ func buildStandaloneClient(opts options.RedisStoreOptions) (Client, error) {
151156}
152157
153158// parseRedisURLs parses a list of redis urls and returns a list
154−// of addresses in the form of host:port that can be used to connnect to Redis
159+// of addresses in the form of host:port that can be used to connect to Redis
155160func parseRedisURLs(urls []string) ([]string, error) {
156161 addrs := []string{}
157162 for _, u := range urls {
pkg/sessions/tests/mock_lock.gonew file+48
@@ -0,0 +1,48 @@
1+package tests
2+
3+import (
4+ "context"
5+ "time"
6+
7+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
8+)
9+
10+type MockLock struct {
11+ expiration time.Duration
12+ elapsed time.Duration
13+}
14+
15+func (l *MockLock) Obtain(ctx context.Context, expiration time.Duration) error {
16+ l.expiration = expiration
17+ return nil
18+}
19+
20+func (l *MockLock) Peek(ctx context.Context) (bool, error) {
21+ if l.elapsed < l.expiration {
22+ return true, nil
23+ }
24+ return false, nil
25+}
26+
27+func (l *MockLock) Refresh(ctx context.Context, expiration time.Duration) error {
28+ if l.expiration <= l.elapsed {
29+ return sessions.ErrNotLocked
30+ }
31+ l.expiration = expiration
32+ l.elapsed = time.Duration(0)
33+ return nil
34+}
35+
36+func (l *MockLock) Release(ctx context.Context) error {
37+ if l.expiration <= l.elapsed {
38+ return sessions.ErrNotLocked
39+ }
40+ l.expiration = time.Duration(0)
41+ l.elapsed = time.Duration(0)
42+ return nil
43+}
44+
45+// FastForward simulates the flow of time to test expirations
46+func (l *MockLock) FastForward(duration time.Duration) {
47+ l.elapsed += duration
48+}
pkg/sessions/tests/mock_store.go+20−4
@@ -4,6 +4,8 @@ import (
44 "context"
55 "fmt"
66 "time"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
79)
810
911// entry is a MockStore cache entry with an expiration
@@ -15,15 +17,17 @@ type entry struct {
1517// MockStore is a generic in-memory implementation of persistence.Store
1618// for mocking in tests
1719type MockStore struct {
18− cache map[string]entry
19− elapsed time.Duration
20+ cache map[string]entry
21+ lockCache map[string]*MockLock
22+ elapsed time.Duration
2023}
2124
2225// NewMockStore creates a MockStore
2326func NewMockStore() *MockStore {
2427 return &MockStore{
25− cache: map[string]entry{},
26− elapsed: 0 * time.Second,
28+ cache: map[string]entry{},
29+ lockCache: map[string]*MockLock{},
30+ elapsed: 0 * time.Second,
2731 }
2832}
2933
@@ -52,7 +56,19 @@ func (s *MockStore) Clear(_ context.Context, key string) error {
5256 return nil
5357}
5458
59+func (s *MockStore) Lock(key string) sessions.Lock {
60+ if s.lockCache[key] != nil {
61+ return s.lockCache[key]
62+ }
63+ lock := &MockLock{}
64+ s.lockCache[key] = lock
65+ return lock
66+}
67+
5568// FastForward simulates the flow of time to test expirations
5669func (s *MockStore) FastForward(duration time.Duration) {
70+ for _, mockLock := range s.lockCache {
71+ mockLock.FastForward(duration)
72+ }
5773 s.elapsed += duration
5874}
pkg/sessions/tests/session_store_tests.go+74
@@ -286,6 +286,78 @@ func PersistentSessionStoreInterfaceTests(in *testInput) {
286286 })
287287 })
288288 })
289+
290+ Context("when lock is applied", func() {
291+ var loadedSession *sessionsapi.SessionState
292+ BeforeEach(func() {
293+ resp := httptest.NewRecorder()
294+ err := in.ss().Save(resp, in.request, in.session)
295+ Expect(err).ToNot(HaveOccurred())
296+
297+ for _, cookie := range resp.Result().Cookies() {
298+ in.request.AddCookie(cookie)
299+ }
300+
301+ loadedSession, err = in.ss().Load(in.request)
302+ Expect(err).ToNot(HaveOccurred())
303+ err = loadedSession.ObtainLock(in.request.Context(), 2*time.Minute)
304+ Expect(err).ToNot(HaveOccurred())
305+ isLocked, err := loadedSession.PeekLock(in.request.Context())
306+ Expect(err).ToNot(HaveOccurred())
307+ Expect(isLocked).To(BeTrue())
308+ })
309+
310+ Context("before lock expired", func() {
311+ BeforeEach(func() {
312+ Expect(in.persistentFastForward(time.Minute)).To(Succeed())
313+ })
314+
315+ It("peek returns true on loaded session lock", func() {
316+ l := *loadedSession
317+ isLocked, err := l.PeekLock(in.request.Context())
318+
319+ Expect(err).NotTo(HaveOccurred())
320+ Expect(isLocked).To(BeTrue())
321+ })
322+
323+ It("lock can be released", func() {
324+ l := *loadedSession
325+
326+ err := l.ReleaseLock(in.request.Context())
327+ Expect(err).NotTo(HaveOccurred())
328+
329+ isLocked, err := l.PeekLock(in.request.Context())
330+ Expect(err).NotTo(HaveOccurred())
331+ Expect(isLocked).To(BeFalse())
332+ })
333+
334+ It("lock is refreshed", func() {
335+ l := *loadedSession
336+ err := l.RefreshLock(in.request.Context(), 3*time.Minute)
337+ Expect(err).NotTo(HaveOccurred())
338+
339+ Expect(in.persistentFastForward(2 * time.Minute)).To(Succeed())
340+
341+ isLocked, err := l.PeekLock(in.request.Context())
342+ Expect(err).NotTo(HaveOccurred())
343+ Expect(isLocked).To(BeTrue())
344+ })
345+ })
346+
347+ Context("after lock expired", func() {
348+ BeforeEach(func() {
349+ Expect(in.persistentFastForward(3 * time.Minute)).To(Succeed())
350+ })
351+
352+ It("peek returns false on loaded session lock", func() {
353+ l := *loadedSession
354+ isLocked, err := l.PeekLock(in.request.Context())
355+
356+ Expect(err).NotTo(HaveOccurred())
357+ Expect(isLocked).To(BeFalse())
358+ })
359+ })
360+ })
289361}
290362
291363func SessionStoreInterfaceTests(in *testInput) {
@@ -411,9 +483,11 @@ func LoadSessionTests(in *testInput) {
411483 l := *loadedSession
412484 l.CreatedAt = nil
413485 l.ExpiresOn = nil
486+ l.Lock = &sessionsapi.NoOpLock{}
414487 s := *in.session
415488 s.CreatedAt = nil
416489 s.ExpiresOn = nil
490+ s.Lock = &sessionsapi.NoOpLock{}
417491 Expect(l).To(Equal(s))
418492
419493 // Compare time.Time separately