From 19c6f5da43b4c4f82ed629f1280c23ec259091b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Voron?=
Date: Thu, 2 Jan 2025 14:37:56 +0100
Subject: [PATCH] server: add customer session and user session to GitHub
Secret Scanning
---
server/polar/auth/service.py | 48 ++++++++++++++++---
server/polar/customer_session/service.py | 30 ++++++++++++
server/polar/enums.py | 2 +
.../github/service/secret_scanning.py | 4 ++
4 files changed, 78 insertions(+), 6 deletions(-)
diff --git a/server/polar/auth/service.py b/server/polar/auth/service.py
index adeb4e26e2..680473de8e 100644
--- a/server/polar/auth/service.py
+++ b/server/polar/auth/service.py
@@ -1,17 +1,22 @@
from datetime import datetime
from typing import TypeVar
+import structlog
from fastapi import Request, Response
from fastapi.responses import RedirectResponse
from sqlalchemy import delete, select
from polar.config import settings
+from polar.enums import TokenType
from polar.kit.crypto import generate_token_hash_pair, get_token_hash
from polar.kit.http import get_safe_return_url
from polar.kit.utils import utc_now
+from polar.logging import Logger
from polar.models import User, UserSession
from polar.postgres import AsyncSession
+log: Logger = structlog.get_logger()
+
USER_SESSION_TOKEN_PREFIX = "polar_us_"
R = TypeVar("R", bound=Response)
@@ -55,12 +60,7 @@ async def authenticate(
if token is None:
return None
- token_hash = get_token_hash(token, secret=settings.SECRET)
- statement = select(UserSession).where(
- UserSession.token == token_hash, UserSession.expires_at > utc_now()
- )
- result = await session.execute(statement)
- user_session = result.unique().scalar_one_or_none()
+ user_session = await self._get_user_session_by_token(session, token)
if user_session is None:
return None
@@ -71,6 +71,42 @@ async def delete_expired(self, session: AsyncSession) -> None:
statement = delete(UserSession).where(UserSession.expires_at < utc_now())
await session.execute(statement)
+ async def revoke_leaked(
+ self,
+ session: AsyncSession,
+ token: str,
+ token_type: TokenType,
+ *,
+ notifier: str,
+ url: str | None,
+ ) -> bool:
+ user_session = await self._get_user_session_by_token(session, token)
+
+ if user_session is None:
+ return False
+
+ await session.delete(user_session)
+
+ log.info(
+ "Revoke leaked user session token",
+ id=user_session.id,
+ notifier=notifier,
+ url=url,
+ )
+
+ return True
+
+ async def _get_user_session_by_token(
+ self, session: AsyncSession, token: str, *, expired: bool = False
+ ) -> UserSession | None:
+ token_hash = get_token_hash(token, secret=settings.SECRET)
+ statement = select(UserSession).where(UserSession.token == token_hash)
+ result = await session.execute(statement)
+ if not expired:
+ statement = statement.where(UserSession.expires_at > utc_now())
+ result = await session.execute(statement)
+ return result.unique().scalar_one_or_none()
+
async def _create_user_session(
self, session: AsyncSession, user: User, *, user_agent: str
) -> tuple[str, UserSession]:
diff --git a/server/polar/customer_session/service.py b/server/polar/customer_session/service.py
index e65dbde163..8907e57a1f 100644
--- a/server/polar/customer_session/service.py
+++ b/server/polar/customer_session/service.py
@@ -1,18 +1,23 @@
+import structlog
from sqlalchemy import delete, select
from sqlalchemy.orm import joinedload
from polar.auth.models import AuthSubject, Organization, User
from polar.config import settings
from polar.customer.service import customer as customer_service
+from polar.enums import TokenType
from polar.exceptions import PolarRequestValidationError
from polar.kit.crypto import generate_token_hash_pair, get_token_hash
from polar.kit.services import ResourceServiceReader
from polar.kit.utils import utc_now
+from polar.logging import Logger
from polar.models import Customer, CustomerSession
from polar.postgres import AsyncSession
from .schemas import CustomerSessionCreate
+log: Logger = structlog.get_logger()
+
CUSTOMER_SESSION_TOKEN_PREFIX = "polar_cst_"
@@ -77,5 +82,30 @@ async def delete_expired(self, session: AsyncSession) -> None:
)
await session.execute(statement)
+ async def revoke_leaked(
+ self,
+ session: AsyncSession,
+ token: str,
+ token_type: TokenType,
+ *,
+ notifier: str,
+ url: str | None,
+ ) -> bool:
+ customer_session = await self.get_by_token(session, token)
+
+ if customer_session is None:
+ return False
+
+ await session.delete(customer_session)
+
+ log.info(
+ "Revoke leaked customer session token",
+ id=customer_session.id,
+ notifier=notifier,
+ url=url,
+ )
+
+ return True
+
customer_session = CustomerSessionService(CustomerSession)
diff --git a/server/polar/enums.py b/server/polar/enums.py
index 6812d44f52..7420f6195e 100644
--- a/server/polar/enums.py
+++ b/server/polar/enums.py
@@ -37,3 +37,5 @@ class TokenType(StrEnum):
access_token = "polar_access_token"
refresh_token = "polar_refresh_token"
personal_access_token = "polar_personal_access_token"
+ customer_session_token = "polar_customer_session_token"
+ user_session_token = "polar_user_session_token"
diff --git a/server/polar/integrations/github/service/secret_scanning.py b/server/polar/integrations/github/service/secret_scanning.py
index 764cac728c..587db3121a 100644
--- a/server/polar/integrations/github/service/secret_scanning.py
+++ b/server/polar/integrations/github/service/secret_scanning.py
@@ -9,6 +9,8 @@
from fastapi.exceptions import RequestValidationError
from pydantic import BeforeValidator, TypeAdapter, ValidationError
+from polar.auth.service import auth as auth_service
+from polar.customer_session.service import customer_session as customer_session_service
from polar.enums import TokenType
from polar.exceptions import PolarError
from polar.kit.schemas import Schema
@@ -76,6 +78,8 @@ async def revoke_leaked(
TokenType.access_token: oauth2_token_service,
TokenType.refresh_token: oauth2_token_service,
TokenType.personal_access_token: personal_access_token_service,
+ TokenType.customer_session_token: customer_session_service,
+ TokenType.user_session_token: auth_service,
}