From 0374098ee986a16c4ce1c7717bd7b7286df37c9b Mon Sep 17 00:00:00 2001 From: cybermaggedon Date: Tue, 7 Jul 2026 16:00:34 +0100 Subject: [PATCH] fix: lazily retry signing key fetch on JWT authentication (#1032) If the gateway started before IAM was ready and exhausted its startup retries, _signing_public_pem stayed None and all JWT authentication permanently 401'd with no recovery path short of a restart. Now _verify_jwt retries the fetch once on demand, so the gateway self-heals as soon as IAM becomes available. --- trustgraph-flow/trustgraph/gateway/auth.py | 56 +++++++++++++--------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/trustgraph-flow/trustgraph/gateway/auth.py b/trustgraph-flow/trustgraph/gateway/auth.py index e81b6908..f0e06911 100644 --- a/trustgraph-flow/trustgraph/gateway/auth.py +++ b/trustgraph-flow/trustgraph/gateway/auth.py @@ -189,36 +189,44 @@ class IamAuth: # Lifecycle # ------------------------------------------------------------------ + async def _fetch_signing_key(self): + """Single attempt to fetch the IAM signing public key. + Returns True on success.""" + try: + async def _fetch(client): + return await client.get_signing_key_public() + + pem = await self._with_client(_fetch) + if pem: + self._signing_public_pem = pem + logger.info( + "IamAuth: fetched IAM signing public key " + f"({len(pem)} bytes)" + ) + return True + except Exception as e: + logger.debug( + f"IamAuth: signing key fetch failed: " + f"{type(e).__name__}: {e}" + ) + return False + async def start(self, max_retries=30, retry_delay=2.0): """Fetch the signing public key from IAM. Retries on failure — the gateway may be starting before IAM is ready.""" - async def _fetch(client): - return await client.get_signing_key_public() - for attempt in range(max_retries): - try: - pem = await self._with_client(_fetch) - if pem: - self._signing_public_pem = pem - logger.info( - "IamAuth: fetched IAM signing public key " - f"({len(pem)} bytes)" - ) - return - except Exception as e: - logger.info( - f"IamAuth: waiting for IAM signing key " - f"({type(e).__name__}: {e}); " - f"retry {attempt + 1}/{max_retries}" - ) + if await self._fetch_signing_key(): + return + logger.info( + f"IamAuth: waiting for IAM signing key; " + f"retry {attempt + 1}/{max_retries}" + ) await asyncio.sleep(retry_delay) - # Don't prevent startup forever. A later authenticate() call - # will try again via the JWT path. logger.warning( "IamAuth: could not fetch IAM signing key at startup; " - "JWT validation will fail until it's available" + "JWT validation will retry on first request" ) # ------------------------------------------------------------------ @@ -259,7 +267,7 @@ class IamAuth: self._annotate_request(request, identity) return identity if token.count(".") == 2: - identity = self._verify_jwt(token) + identity = await self._verify_jwt(token) self._annotate_request(request, identity) return identity raise _auth_failure() @@ -281,7 +289,9 @@ class IamAuth: except Exception: pass - def _verify_jwt(self, token): + async def _verify_jwt(self, token): + if not self._signing_public_pem: + await self._fetch_signing_key() if not self._signing_public_pem: raise _auth_failure() try: