import logging import os from datetime import datetime from core.base import ( AuthConfig, CryptoProvider, EmailProvider, R2RException, TokenData, ) from ..database import PostgresDatabaseProvider from .jwt import JwtAuthProvider logger = logging.getLogger(__name__) class ClerkAuthProvider(JwtAuthProvider): """ ClerkAuthProvider extends JwtAuthProvider to support token verification with Clerk. It uses Clerk's SDK to verify the JWT token and extract user information. """ def __init__( self, config: AuthConfig, crypto_provider: CryptoProvider, database_provider: PostgresDatabaseProvider, email_provider: EmailProvider, ): super().__init__( config=config, crypto_provider=crypto_provider, database_provider=database_provider, email_provider=email_provider, ) try: from clerk_backend_api.jwks_helpers.verifytoken import ( VerifyTokenOptions, verify_token, ) self.verify_token = verify_token self.VerifyTokenOptions = VerifyTokenOptions except ImportError as e: raise R2RException( status_code=500, message="Clerk SDK is not installed. Run `pip install clerk-backend-api`", ) from e async def decode_token(self, token: str) -> TokenData: """ Decode and verify the JWT token using Clerk's verify_token function. Args: token: The JWT token to decode Returns: TokenData: The decoded token data with user information Raises: R2RException: If the token is invalid or verification fails """ clerk_secret_key = os.getenv("CLERK_SECRET_KEY") if not clerk_secret_key: raise R2RException( status_code=500, message="CLERK_SECRET_KEY environment variable is not set", ) try: # Configure verification options options = self.VerifyTokenOptions( secret_key=clerk_secret_key, # Optional: specify audience if needed # audience="your-audience", # Optional: specify authorized parties if needed # authorized_parties=["https://your-domain.com"] ) # Verify the token using Clerk's SDK payload = self.verify_token(token, options) # Check for the expected claims in the token payload if not payload.get("sub") or not payload.get("email"): raise R2RException( status_code=401, message="Invalid token: missing required claims", ) # Create user in database if not exists try: await self.database_provider.users_handler.get_user_by_email( payload.get("email") ) # TODO do we want to update user info here based on what's in the token? except Exception: # user doesn't exist, create in db logger.debug(f"Creating new user: {payload.get('email')}") try: # Construct name from first_name and last_name if available first_name = payload.get("first_name", "") last_name = payload.get("last_name", "") name = payload.get("name") # If name not directly provided, try to build it from first and last names if not name and (first_name or last_name): name = f"{first_name} {last_name}".strip() await self.database_provider.users_handler.create_user( email=payload.get("email"), account_type="external", name=name, ) except Exception as e: logger.error(f"Error creating user: {e}") raise R2RException( status_code=500, message="Failed to create user" ) from e # Return the token data return TokenData( email=payload.get("email"), token_type="bearer", exp=datetime.fromtimestamp(payload.get("exp")), ) except Exception as e: logger.info(f"Clerk token verification failed: {e}") raise R2RException( status_code=401, message="Invalid token", detail=str(e) ) from e