diff --git a/app/cabinet/auth/__init__.py b/app/cabinet/auth/__init__.py index b5773149..b20a4637 100644 --- a/app/cabinet/auth/__init__.py +++ b/app/cabinet/auth/__init__.py @@ -8,7 +8,7 @@ from .jwt_handler import ( get_token_payload, ) from .password_utils import hash_password, verify_password -from .telegram_auth import validate_telegram_init_data, validate_telegram_login_widget +from .telegram_auth import validate_telegram_init_data, validate_telegram_login_widget, validate_telegram_oidc_token __all__ = [ @@ -20,5 +20,6 @@ __all__ = [ 'hash_password', 'validate_telegram_init_data', 'validate_telegram_login_widget', + 'validate_telegram_oidc_token', 'verify_password', ] diff --git a/app/cabinet/routes/auth.py b/app/cabinet/routes/auth.py index b5ab69a3..79b2b978 100644 --- a/app/cabinet/routes/auth.py +++ b/app/cabinet/routes/auth.py @@ -41,6 +41,7 @@ from ..auth import ( hash_password, validate_telegram_init_data, validate_telegram_login_widget, + validate_telegram_oidc_token, verify_password, ) from ..auth.email_verification import ( @@ -71,6 +72,7 @@ from ..schemas.auth import ( RefreshTokenRequest, RegisterResponse, TelegramAuthRequest, + TelegramOIDCAuthRequest, TelegramWidgetAuthRequest, TokenResponse, UserResponse, @@ -544,6 +546,95 @@ async def auth_telegram_widget( return response +@router.post('/telegram/oidc', response_model=AuthResponse) +async def auth_telegram_oidc( + request: TelegramOIDCAuthRequest, + db: AsyncSession = Depends(get_cabinet_db), +): + """ + Authenticate using Telegram OIDC id_token (popup flow). + + The frontend uses Telegram.Login.init() popup which returns an id_token. + We validate it via JWKS and create/login the user. + """ + if not settings.TELEGRAM_OIDC_ENABLED or not settings.TELEGRAM_OIDC_CLIENT_ID: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Telegram OIDC is not configured', + ) + + claims = await validate_telegram_oidc_token( + request.id_token, + settings.TELEGRAM_OIDC_CLIENT_ID, + ) + if not claims: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Invalid or expired Telegram OIDC token', + ) + + # Extract user info from OIDC claims + telegram_id = int(claims.get('id', claims.get('sub', 0))) + if not telegram_id: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Missing user ID in OIDC claims', + ) + + first_name = claims.get('name', claims.get('given_name', '')) + username = claims.get('preferred_username') + + user = await get_user_by_telegram_id(db, telegram_id) + + # Resolve referral code for new users + referrer_id = None + if request.referral_code and not user: + try: + referrer = await get_user_by_referral_code(db, request.referral_code) + if referrer: + referrer_id = referrer.id + except Exception as e: + logger.warning('Failed to resolve referral code', referral_code=request.referral_code, error=e) + + if not user: + logger.info('Creating new user from cabinet OIDC', telegram_id=telegram_id, username=username) + user = await create_user( + db=db, + telegram_id=telegram_id, + username=username, + first_name=first_name, + language='ru', + referred_by_id=referrer_id, + ) + logger.info('User created successfully', user_id=user.id, telegram_id=user.telegram_id) + + if user.status != 'active': + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail='User account is not active', + ) + + # Update user info from OIDC claims + if username and username != user.username: + user.username = username + if first_name and first_name != user.first_name: + user.first_name = first_name + + user.cabinet_last_login = datetime.now(UTC) + await db.commit() + + response = await _create_auth_response(user, db) + await _store_refresh_token(db, user.id, response.refresh_token) + + await _process_referral_code(db, user, request.referral_code) + + response.campaign_bonus = await _process_campaign_bonus(db, user, request.campaign_slug) + if response.campaign_bonus: + response.user = _user_to_response(user) + + return response + + @router.post('/email/register') async def register_email( request: EmailRegisterRequest, diff --git a/app/cabinet/schemas/auth.py b/app/cabinet/schemas/auth.py index afe667e0..7fc27000 100644 --- a/app/cabinet/schemas/auth.py +++ b/app/cabinet/schemas/auth.py @@ -31,6 +31,16 @@ class TelegramWidgetAuthRequest(BaseModel): referral_code: str | None = Field(None, max_length=32, description='Referral code of inviter') +class TelegramOIDCAuthRequest(BaseModel): + """Request for Telegram OIDC authentication (popup flow).""" + + id_token: str = Field(..., description='JWT id_token from Telegram OIDC popup') + campaign_slug: str | None = Field( + None, min_length=1, max_length=64, pattern=r'^[a-zA-Z0-9_-]+$', description='Campaign slug from web link' + ) + referral_code: str | None = Field(None, max_length=32, description='Referral code of inviter') + + class EmailRegisterRequest(BaseModel): """Request to register/link email to existing Telegram account."""