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, }