From 19d267587b80e15430c63e4cec527c1ff1b1755a Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Thu, 12 Mar 2026 22:31:02 +0100 Subject: [PATCH] fix(loyalty): use private key PEM for JWT signing instead of RSASigner.key RSASigner doesn't expose a .key attribute. Load the private key string directly from the service account JSON file for PyJWT encoding. Also adds fat JWT fallback for demo mode where DRAFT classes reject object creation via REST API. Co-Authored-By: Claude Opus 4.6 --- .../loyalty/services/google_wallet_service.py | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/app/modules/loyalty/services/google_wallet_service.py b/app/modules/loyalty/services/google_wallet_service.py index 5c0d104a..63936fe9 100644 --- a/app/modules/loyalty/services/google_wallet_service.py +++ b/app/modules/loyalty/services/google_wallet_service.py @@ -472,34 +472,60 @@ class GoogleWalletService: if not self.is_configured: raise GoogleWalletNotConfiguredException() + # Try to create the object via API. If it fails (e.g. demo mode + # where DRAFT classes reject object creation), fall back to + # embedding the full object data in the JWT ("fat JWT"). if not card.google_object_id: - self.create_object(db, card) + try: + self.create_object(db, card) + except WalletIntegrationException: + logger.info( + "Object creation failed for card %s, using fat JWT", + card.id, + ) try: import jwt credentials = self._get_credentials() - signer = self._get_signer() now = datetime.now(tz=UTC) origins = settings.loyalty_google_wallet_origins or [] + issuer_id = settings.loyalty_google_issuer_id + object_id = card.google_object_id or f"{issuer_id}.loyalty_card_{card.id}" + + if card.google_object_id: + # Object exists in Google — reference by ID only + payload = { + "loyaltyObjects": [{"id": card.google_object_id}], + } + else: + # Object not created — embed full object data in JWT + object_data = self._build_object_data(card, object_id) + payload = { + "loyaltyObjects": [object_data], + } + claims = { "iss": credentials.service_account_email, "aud": "google", "origins": origins, "typ": "savetowallet", - "payload": { - "loyaltyObjects": [{"id": card.google_object_id}], - }, + "payload": payload, "iat": now, "exp": now + timedelta(hours=1), } - # Sign using the RSASigner's key_id and key bytes (public API) + # Load the private key directly from the service account file + # (RSASigner doesn't expose .key; PyJWT needs the PEM string) + with open(settings.loyalty_google_service_account_json) as f: + sa_data = json.load(f) + private_key = sa_data["private_key"] + token = jwt.encode( claims, - signer.key, + private_key, algorithm="RS256", ) @@ -507,7 +533,7 @@ class GoogleWalletService: db.commit() return f"https://pay.google.com/gp/v/save/{token}" - except (AttributeError, ValueError, KeyError) as exc: + except (AttributeError, ValueError, KeyError, OSError) as exc: logger.error( "Failed to generate Google Wallet save URL: %s", exc )