Skip to content

Commit 5444935

Browse files
committed
fix: jwt cache is not purged
Fixed the JWT cache not purging expired tokens in cache. This is tested by adding a new metric pgrst_jwt_cache_size.
1 parent d78877c commit 5444935

File tree

5 files changed

+102
-11
lines changed

5 files changed

+102
-11
lines changed

docs/references/observability.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,20 @@ pgrst_db_pool_max
169169

170170
Max pool connections.
171171

172+
JWT Cache Metric
173+
----------------
174+
175+
Related to the :ref:`jwt_caching`.
176+
177+
pgrst_jwt_cache_size
178+
~~~~~~~~~~~~~~~~~~~~
179+
180+
======== =======
181+
**Type** Gauge
182+
======== =======
183+
184+
JWT Cache Size.
185+
172186
Traces
173187
======
174188

src/PostgREST/Auth.hs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ import System.Clock (TimeSpec (..))
4444
import System.IO.Unsafe (unsafePerformIO)
4545
import System.TimeIt (timeItT)
4646

47-
import PostgREST.AppState (AppState, AuthResult (..), getConfig,
48-
getJwtCache, getTime)
49-
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
50-
import PostgREST.Error (Error (..))
47+
import PostgREST.AppState (AppState, AuthResult (..), getConfig,
48+
getJwtCache, getObserver, getTime)
49+
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
50+
import PostgREST.Error (Error (..))
51+
import PostgREST.Observation (Observation (..))
5152

5253
import Protolude
5354

@@ -163,14 +164,25 @@ middleware appState app req respond = do
163164
-- | Used to retrieve and insert JWT to JWT Cache
164165
getJWTFromCache :: AppState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
165166
getJWTFromCache appState token maxLifetime parseJwt utc = do
166-
checkCache <- C.lookup (getJwtCache appState) token
167+
168+
-- Call first to delete the expired JWTs
169+
C.purgeExpired jwtCache
170+
171+
checkCache <- C.lookup jwtCache token
167172
authResult <- maybe parseJwt (pure . Right) checkCache
168173

174+
169175
case (authResult,checkCache) of
170-
(Right res, Nothing) -> C.insert' (getJwtCache appState) (getTimeSpec res maxLifetime utc) token res
176+
(Right res, Nothing) -> C.insert' jwtCache (getTimeSpec res maxLifetime utc) token res
171177
_ -> pure ()
172178

179+
jwtCacheSize <- C.size jwtCache
180+
observer $ JWTCache jwtCacheSize
181+
173182
return authResult
183+
where
184+
observer = getObserver appState
185+
jwtCache = getJwtCache appState
174186

175187
-- Used to extract JWT exp claim and add to JWT Cache
176188
getTimeSpec :: AuthResult -> Int -> UTCTime -> Maybe TimeSpec

src/PostgREST/Metrics.hs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{-|
2-
Module : PostgREST.Logger
2+
Module : PostgREST.Metrics
33
Description : Metrics based on the Observation module. See Observation.hs.
44
-}
55
module PostgREST.Metrics
@@ -19,7 +19,7 @@ import PostgREST.Observation
1919
import Protolude
2020

2121
data MetricsState =
22-
MetricsState Counter Gauge Gauge Gauge (Vector Label1 Counter) Gauge
22+
MetricsState Counter Gauge Gauge Gauge (Vector Label1 Counter) Gauge Gauge
2323

2424
init :: Int -> IO MetricsState
2525
init configDbPoolSize = do
@@ -29,12 +29,13 @@ init configDbPoolSize = do
2929
poolMaxSize <- register $ gauge (Info "pgrst_db_pool_max" "Max pool connections")
3030
schemaCacheLoads <- register $ vector "status" $ counter (Info "pgrst_schema_cache_loads_total" "The total number of times the schema cache was loaded")
3131
schemaCacheQueryTime <- register $ gauge (Info "pgrst_schema_cache_query_time_seconds" "The query time in seconds of the last schema cache load")
32+
jwtCacheSize <- register $ gauge (Info "pgrst_jwt_cache_size" "The number of cached JWTs")
3233
setGauge poolMaxSize (fromIntegral configDbPoolSize)
33-
pure $ MetricsState poolTimeouts poolAvailable poolWaiting poolMaxSize schemaCacheLoads schemaCacheQueryTime
34+
pure $ MetricsState poolTimeouts poolAvailable poolWaiting poolMaxSize schemaCacheLoads schemaCacheQueryTime jwtCacheSize
3435

3536
-- Only some observations are used as metrics
3637
observationMetrics :: MetricsState -> ObservationHandler
37-
observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schemaCacheLoads schemaCacheQueryTime) obs = case obs of
38+
observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schemaCacheLoads schemaCacheQueryTime jwtCacheSize) obs = case obs of
3839
(PoolAcqTimeoutObs _) -> do
3940
incCounter poolTimeouts
4041
(HasqlPoolObs (SQL.ConnectionObservation _ status)) -> case status of
@@ -54,6 +55,8 @@ observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schema
5455
setGauge schemaCacheQueryTime resTime
5556
SchemaCacheErrorObs _ -> do
5657
withLabel schemaCacheLoads "FAIL" incCounter
58+
JWTCache cacheSize -> do
59+
setGauge jwtCacheSize $ fromIntegral cacheSize
5760
_ ->
5861
pure ()
5962

src/PostgREST/Observation.hs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,13 @@ data Observation
5757
| HasqlPoolObs SQL.Observation
5858
| PoolRequest
5959
| PoolRequestFullfilled
60+
| JWTCache Int
6061

61-
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
62+
data ObsFatalError
63+
= ServerAuthError
64+
| ServerPgrstBug
65+
| ServerError42P05
66+
| ServerError08P01
6267

6368
type ObservationHandler = Observation -> IO ()
6469

test/io/test_io.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,8 @@ def test_admin_metrics(defaultenv):
16321632
assert "pgrst_db_pool_available" in response.text
16331633
assert "pgrst_db_pool_timeouts_total" in response.text
16341634

1635+
assert "pgrst_jwt_cache_size" in response.text
1636+
16351637

16361638
def test_schema_cache_startup_load_with_in_db_config(defaultenv, metapostgrest):
16371639
"verify that the Schema Cache loads correctly at startup, using the in-db `pgrst.db_schemas` config"
@@ -1648,3 +1650,58 @@ def test_schema_cache_startup_load_with_in_db_config(defaultenv, metapostgrest):
16481650
response = metapostgrest.session.post("/rpc/reset_db_schemas_config")
16491651
assert response.text == ""
16501652
assert response.status_code == 204
1653+
1654+
1655+
def test_jwt_cache_size_decreases_after_expiry(defaultenv):
1656+
"verify that JWT purges expired JWTs"
1657+
1658+
relativeSeconds = lambda sec: int(
1659+
(datetime.now(timezone.utc) + timedelta(seconds=sec)).timestamp()
1660+
)
1661+
1662+
headers = lambda sec: jwtauthheader(
1663+
{"role": "postgrest_test_author", "exp": relativeSeconds(sec)},
1664+
SECRET,
1665+
)
1666+
1667+
env = {
1668+
**defaultenv,
1669+
"PGRST_JWT_CACHE_MAX_LIFETIME": "86400",
1670+
"PGRST_JWT_SECRET": SECRET,
1671+
"PGRST_DB_CONFIG": "false",
1672+
}
1673+
1674+
with run(env=env, port=freeport()) as postgrest:
1675+
1676+
# Generate three unique JWT tokens
1677+
# The 1 second sleep is needed for it generate a unique token
1678+
hdrs1 = headers(5)
1679+
postgrest.session.get("/authors_only", headers=hdrs1)
1680+
1681+
time.sleep(1)
1682+
1683+
hdrs2 = headers(5)
1684+
postgrest.session.get("/authors_only", headers=hdrs2)
1685+
1686+
time.sleep(1)
1687+
1688+
hdrs3 = headers(5)
1689+
postgrest.session.get("/authors_only", headers=hdrs3)
1690+
1691+
# the cache should now have three tokens
1692+
response = postgrest.admin.get("/metrics")
1693+
assert response.status_code == 200
1694+
assert "pgrst_jwt_cache_size 3.0" in response.text
1695+
1696+
# Wait 5 seconds for the tokens to expire
1697+
time.sleep(5)
1698+
1699+
hdrs4 = headers(5)
1700+
1701+
# Make another request to force call the purgeExpired method
1702+
# This should remove the 3 expired tokens and adds 1 to cache
1703+
postgrest.session.get("/authors_only", headers=hdrs4)
1704+
1705+
response = postgrest.admin.get("/metrics")
1706+
assert response.status_code == 200
1707+
assert "pgrst_jwt_cache_size 1.0" in response.text

0 commit comments

Comments
 (0)