diff --git a/docs/totp_usage.md b/docs/totp_usage.md new file mode 100644 index 00000000..9f083fef --- /dev/null +++ b/docs/totp_usage.md @@ -0,0 +1,85 @@ +# TOTP (Two-Factor Authentication) Support + +This module provides TOTP (Time-based One-Time Password) functionality for use with video editing workflows, particularly for YouTube authentication when using playwright automation. + +## Usage + +### Basic TOTP Operations + +```python +from ac_training_lab.video_editing import ( + generate_totp_code, + verify_totp_code, + create_totp_provisioning_uri +) + +# Generate a TOTP code from a secret +secret = "JBSWY3DPEHPK3PXP" # Base32-encoded secret +code = generate_totp_code(secret) +print(f"Current TOTP code: {code}") + +# Verify a TOTP code +is_valid = verify_totp_code(secret, code) +print(f"Code is valid: {is_valid}") + +# Create a provisioning URI for authenticator apps +uri = create_totp_provisioning_uri(secret, "user@example.com", "AC Training Lab") +print(f"Provisioning URI: {uri}") +``` + +### YouTube Integration with TOTP + +```python +from ac_training_lab.video_editing import ( + get_current_totp_for_youtube, + download_youtube_with_totp +) + +# Set environment variable with your YouTube TOTP secret +# export YOUTUBE_TOTP_SECRET="your_base32_secret_here" + +# Get current TOTP code for YouTube +totp_code = get_current_totp_for_youtube() +if totp_code: + print(f"YouTube TOTP code: {totp_code}") + +# Download video with TOTP support +video_id = "your_video_id" +download_youtube_with_totp(video_id) # Uses TOTP from environment +# or +download_youtube_with_totp(video_id, totp_code="123456") # Explicit TOTP code +``` + +### Environment Variables + +- `YOUTUBE_TOTP_SECRET`: Base32-encoded secret for YouTube 2FA +- `TOTP_SECRET`: Default environment variable for TOTP secret + +### Installation + +Install with video editing support: + +```bash +pip install ac-training-lab[video-editing] +``` + +Or install pyotp directly: + +```bash +pip install pyotp +``` + +## Use Cases + +This functionality is designed for: + +1. **Playwright Automation**: When automating YouTube interactions that require 2FA +2. **Scheduled Downloads**: Automated video downloads with 2FA authentication +3. **CI/CD Pipelines**: Automated workflows that need TOTP authentication + +## Security Notes + +- Store TOTP secrets securely as environment variables +- Never commit TOTP secrets to version control +- Use separate secrets for different services +- Consider using secret management services for production deployments \ No newline at end of file diff --git a/examples/totp_example.py b/examples/totp_example.py new file mode 100644 index 00000000..bed6c49e --- /dev/null +++ b/examples/totp_example.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +""" +Example usage of pyotp functionality in AC Training Lab. + +This script demonstrates how to use the TOTP functionality +for YouTube video downloading with 2FA authentication. +""" + +import os +import sys + +# Example of how to use the TOTP functionality +def example_basic_totp(): + """Example of basic TOTP operations.""" + print("=== Basic TOTP Example ===") + + try: + from ac_training_lab.video_editing import ( + generate_totp_code, + verify_totp_code, + create_totp_provisioning_uri + ) + + # Generate a random secret for demonstration + import pyotp + secret = pyotp.random_base32() + print(f"Generated secret: {secret}") + + # Generate TOTP code + code = generate_totp_code(secret) + print(f"Generated TOTP code: {code}") + + # Verify the code + is_valid = verify_totp_code(secret, code) + print(f"Code verification: {is_valid}") + + # Create provisioning URI + uri = create_totp_provisioning_uri(secret, "user@example.com") + print(f"Provisioning URI: {uri}") + + except ImportError as e: + print(f"Import error: {e}") + print("Make sure pyotp is installed: pip install pyotp") + + +def example_youtube_integration(): + """Example of YouTube integration with TOTP.""" + print("\n=== YouTube TOTP Integration Example ===") + + try: + from ac_training_lab.video_editing import ( + get_current_totp_for_youtube, + download_youtube_with_totp + ) + + # Get TOTP for YouTube (returns None if not configured) + totp_code = get_current_totp_for_youtube() + if totp_code: + print(f"YouTube TOTP code: {totp_code}") + else: + print("No YouTube TOTP secret configured") + print("Set YOUTUBE_TOTP_SECRET environment variable to enable") + + # Example of how download would work + # Note: This won't actually download anything without proper setup + print("Example download with TOTP support:") + print("download_youtube_with_totp('video_id', totp_code)") + + except ImportError as e: + print(f"Import error: {e}") + print("Make sure the video_editing module is properly installed") + + +def example_environment_setup(): + """Example of setting up environment variables.""" + print("\n=== Environment Setup Example ===") + + print("To use TOTP with YouTube authentication:") + print("1. Get your TOTP secret from your authenticator app") + print("2. Set environment variable:") + print(" export YOUTUBE_TOTP_SECRET='your_base32_secret_here'") + print("3. Use the functions:") + print(" from ac_training_lab.video_editing import get_current_totp_for_youtube") + print(" code = get_current_totp_for_youtube()") + print(" # Use code with playwright for 2FA authentication") + + +if __name__ == "__main__": + print("AC Training Lab TOTP Functionality Examples") + print("=" * 50) + + example_basic_totp() + example_youtube_integration() + example_environment_setup() + + print("\n" + "=" * 50) + print("For more information, see docs/totp_usage.md") \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 1e2902a2..10af5def 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,7 @@ package_dir = # For more information, check out https://semver.org/. install_requires = importlib-metadata; python_version<"3.8" + pyotp [options.packages.find] @@ -74,6 +75,10 @@ a1-cam = boto3 wget +video-editing = + pyotp + requests + [options.entry_points] # Add here console scripts like: # console_scripts = diff --git a/src/ac_training_lab/video_editing/__init__.py b/src/ac_training_lab/video_editing/__init__.py new file mode 100644 index 00000000..5b0aa411 --- /dev/null +++ b/src/ac_training_lab/video_editing/__init__.py @@ -0,0 +1,25 @@ +"""Video editing utilities for AC Training Lab.""" + +from .yt_utils import ( + download_youtube_live, + get_latest_video_id, + get_current_totp_for_youtube, + download_youtube_with_totp, +) +from .totp_utils import ( + generate_totp_code, + verify_totp_code, + get_totp_code_from_env, + create_totp_provisioning_uri, +) + +__all__ = [ + "download_youtube_live", + "get_latest_video_id", + "get_current_totp_for_youtube", + "download_youtube_with_totp", + "generate_totp_code", + "verify_totp_code", + "get_totp_code_from_env", + "create_totp_provisioning_uri", +] \ No newline at end of file diff --git a/src/ac_training_lab/video_editing/totp_utils.py b/src/ac_training_lab/video_editing/totp_utils.py new file mode 100644 index 00000000..24f56323 --- /dev/null +++ b/src/ac_training_lab/video_editing/totp_utils.py @@ -0,0 +1,90 @@ +""" +TOTP (Time-based One-Time Password) utilities for video editing workflows. + +This module provides minimal, lean TOTP functionality for use with playwright +and YouTube video downloading where 2FA authentication may be required. +""" + +import os +from typing import Optional + +import pyotp + + +def generate_totp_code(secret: str) -> str: + """ + Generate a TOTP code from a secret. + + Args: + secret: Base32-encoded secret string + + Returns: + 6-digit TOTP code as string + + Raises: + ValueError: If secret is invalid + """ + try: + totp = pyotp.TOTP(secret) + return totp.now() + except Exception as e: + raise ValueError(f"Failed to generate TOTP code: {e}") + + +def verify_totp_code(secret: str, token: str, window: int = 1) -> bool: + """ + Verify a TOTP code against a secret. + + Args: + secret: Base32-encoded secret string + token: 6-digit TOTP code to verify + window: Time window for verification (default: 1) + + Returns: + True if code is valid, False otherwise + """ + try: + totp = pyotp.TOTP(secret) + return totp.verify(token, window=window) + except Exception: + return False + + +def get_totp_code_from_env(env_var: str = "TOTP_SECRET") -> Optional[str]: + """ + Generate TOTP code from secret stored in environment variable. + + Args: + env_var: Environment variable name containing the secret + + Returns: + 6-digit TOTP code as string, or None if env var not set + + Raises: + ValueError: If secret is invalid + """ + secret = os.getenv(env_var) + if not secret: + return None + + return generate_totp_code(secret) + + +def create_totp_provisioning_uri( + secret: str, + name: str, + issuer: str = "AC Training Lab" +) -> str: + """ + Create a provisioning URI for setting up TOTP in authenticator apps. + + Args: + secret: Base32-encoded secret string + name: Account name (e.g., email or username) + issuer: Service name + + Returns: + Provisioning URI string + """ + totp = pyotp.TOTP(secret) + return totp.provisioning_uri(name=name, issuer_name=issuer) \ No newline at end of file diff --git a/src/ac_training_lab/video_editing/yt_utils.py b/src/ac_training_lab/video_editing/yt_utils.py index a00e3e85..31374e9d 100644 --- a/src/ac_training_lab/video_editing/yt_utils.py +++ b/src/ac_training_lab/video_editing/yt_utils.py @@ -3,6 +3,8 @@ import requests +from .totp_utils import get_totp_code_from_env + YT_API_KEY = os.getenv("YT_API_KEY") @@ -104,6 +106,43 @@ def download_youtube_live(video_id): print(e.stderr) +def get_current_totp_for_youtube(): + """ + Get current TOTP code for YouTube authentication if configured. + + This function provides TOTP support for YouTube workflows that may + require 2FA authentication when used with playwright automation. + + Returns: + TOTP code as string if secret is configured, None otherwise + """ + return get_totp_code_from_env("YOUTUBE_TOTP_SECRET") + + +def download_youtube_with_totp(video_id, totp_code=None): + """ + Enhanced YouTube download function with TOTP support. + + Args: + video_id: YouTube video ID to download + totp_code: Optional TOTP code for 2FA authentication + + Note: + This function demonstrates how TOTP could be integrated into + YouTube download workflows. The actual implementation would + depend on the specific playwright automation needs. + """ + if totp_code is None: + totp_code = get_current_totp_for_youtube() + + if totp_code: + print(f"TOTP code available for authentication: {totp_code}") + + # For now, fall back to standard download + # In a real implementation, this would pass the TOTP code to playwright + download_youtube_live(video_id) + + if __name__ == "__main__": video_id = get_latest_video_id( diff --git a/tests/test_totp_utils.py b/tests/test_totp_utils.py new file mode 100644 index 00000000..86c5d787 --- /dev/null +++ b/tests/test_totp_utils.py @@ -0,0 +1,106 @@ +""" +Tests for TOTP utilities in video editing module. +""" + +import os +import pytest +import pyotp + +from ac_training_lab.video_editing.totp_utils import ( + generate_totp_code, + verify_totp_code, + get_totp_code_from_env, + create_totp_provisioning_uri, +) + + +def test_generate_totp_code(): + """Test TOTP code generation with a known secret.""" + # Use a test secret + secret = pyotp.random_base32() + + # Generate code + code = generate_totp_code(secret) + + # Verify it's a 6-digit string + assert len(code) == 6 + assert code.isdigit() + + +def test_generate_totp_code_invalid_secret(): + """Test TOTP code generation with invalid secret.""" + with pytest.raises(ValueError): + generate_totp_code("invalid_secret") + + +def test_verify_totp_code(): + """Test TOTP code verification.""" + # Use a test secret + secret = pyotp.random_base32() + + # Generate a code + code = generate_totp_code(secret) + + # Verify the code + assert verify_totp_code(secret, code) is True + + # Verify an invalid code + assert verify_totp_code(secret, "000000") is False + + +def test_verify_totp_code_invalid_secret(): + """Test TOTP code verification with invalid secret.""" + result = verify_totp_code("invalid_secret", "123456") + assert result is False + + +def test_get_totp_code_from_env(): + """Test getting TOTP code from environment variable.""" + test_secret = pyotp.random_base32() + test_env_var = "TEST_TOTP_SECRET" + + # Test with no env var set + result = get_totp_code_from_env(test_env_var) + assert result is None + + # Test with env var set + os.environ[test_env_var] = test_secret + try: + result = get_totp_code_from_env(test_env_var) + assert result is not None + assert len(result) == 6 + assert result.isdigit() + finally: + del os.environ[test_env_var] + + +def test_create_totp_provisioning_uri(): + """Test creating TOTP provisioning URI.""" + secret = pyotp.random_base32() + name = "test@example.com" + issuer = "Test Service" + + uri = create_totp_provisioning_uri(secret, name, issuer) + + # Check that URI contains expected components + assert uri.startswith("otpauth://totp/") + assert name in uri + assert issuer in uri + assert secret in uri + + +def test_totp_integration(): + """Test full TOTP workflow integration.""" + # Generate a random secret + secret = pyotp.random_base32() + + # Generate a code + code = generate_totp_code(secret) + + # Verify the code + is_valid = verify_totp_code(secret, code) + assert is_valid is True + + # Create provisioning URI + uri = create_totp_provisioning_uri(secret, "test@example.com") + assert "otpauth://totp/" in uri \ No newline at end of file diff --git a/tests/test_youtube_totp.py b/tests/test_youtube_totp.py new file mode 100644 index 00000000..b456dffe --- /dev/null +++ b/tests/test_youtube_totp.py @@ -0,0 +1,84 @@ +""" +Tests for YouTube utilities with TOTP integration. +""" + +import os +import pytest +import pyotp + +from ac_training_lab.video_editing.yt_utils import ( + get_current_totp_for_youtube, + download_youtube_with_totp, +) + + +def test_get_current_totp_for_youtube(): + """Test getting TOTP code for YouTube authentication.""" + test_secret = pyotp.random_base32() + env_var = "YOUTUBE_TOTP_SECRET" + + # Test with no env var set + result = get_current_totp_for_youtube() + assert result is None + + # Test with env var set + os.environ[env_var] = test_secret + try: + result = get_current_totp_for_youtube() + assert result is not None + assert len(result) == 6 + assert result.isdigit() + finally: + if env_var in os.environ: + del os.environ[env_var] + + +def test_download_youtube_with_totp_mock(monkeypatch): + """Test YouTube download with TOTP (mocked subprocess to avoid actual download).""" + # Mock subprocess.run to avoid actual download + def mock_run(*args, **kwargs): + class MockResult: + stdout = "Mock download successful" + return MockResult() + + monkeypatch.setattr("ac_training_lab.video_editing.yt_utils.subprocess.run", mock_run) + + # Test download with explicit TOTP code + # This should not raise an exception + download_youtube_with_totp("test_video_id", "123456") + + # Test download with TOTP from environment + test_secret = pyotp.random_base32() + env_var = "YOUTUBE_TOTP_SECRET" + + os.environ[env_var] = test_secret + try: + download_youtube_with_totp("test_video_id") + finally: + if env_var in os.environ: + del os.environ[env_var] + + +def test_youtube_totp_integration(): + """Test integration between YouTube utils and TOTP functionality.""" + # This test verifies that the TOTP functionality is properly integrated + # with the YouTube utilities without requiring actual YouTube API calls + + test_secret = pyotp.random_base32() + env_var = "YOUTUBE_TOTP_SECRET" + + # Set up environment + os.environ[env_var] = test_secret + + try: + # Get TOTP code + totp_code = get_current_totp_for_youtube() + + # Verify we got a valid TOTP code + assert totp_code is not None + assert len(totp_code) == 6 + assert totp_code.isdigit() + + finally: + if env_var in os.environ: + del os.environ[env_var] \ No newline at end of file