diff --git a/.env.example b/.env.example index 633d90b..1e7dce8 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,7 @@ # This is an example file. Copy it to .env and fill in your actual secrets. # The .env file is ignored by Git and should NEVER be committed. -ENCRYPTION_KEY="your_32_byte_url_safe_base64_encoded_key_goes_here" +ENCRYPTION_KEY=your_32_byte_url_safe_base64_encoded_key_goes_here + +TWITCH_CLIENT_ID=your_twitch_client_id_goes_here +TWITCH_CLIENT_SECRET=your_twitch_client_secret_goes_here diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..b245209 --- /dev/null +++ b/auth.py @@ -0,0 +1,91 @@ +import httpx +import secrets +from fastapi import APIRouter, Depends, HTTPException, Request, Response +from fastapi.responses import RedirectResponse +from sqlalchemy.orm import Session + +from config import settings +from database import SessionLocal +import models +import security + +router = APIRouter() + +# Dependency to get a DB session +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +@router.get("/login/twitch") +async def login_with_twitch(request: Request): + """ + Step 1 of OAuth flow: Redirect the user to Twitch's authorization page. + """ + # Generate a random state token for CSRF protection + state = secrets.token_urlsafe(16) + request.session['oauth_state'] = state + + # As per RESEARCH_REPORT.md, these are the minimum required scopes + scopes = "chat:read chat:write" + + # Construct the authorization URL + auth_url = ( + f"https://id.twitch.tv/oauth2/authorize" + f"?response_type=code" + f"&client_id={settings.TWITCH_CLIENT_ID}" + f"&redirect_uri={settings.APP_BASE_URL}/auth/twitch/callback" + f"&scope={scopes}" + f"&state={state}" + ) + return RedirectResponse(url=auth_url) + +@router.get("/auth/twitch/callback") +async def auth_twitch_callback(code: str, state: str, request: Request, db: Session = Depends(get_db)): + """ + Step 2 of OAuth flow: Handle the callback from Twitch after user authorization. + """ + # CSRF Protection: Validate the state + if state != request.session.pop('oauth_state', None): + raise HTTPException(status_code=403, detail="Invalid state parameter. CSRF attack suspected.") + + # Step 4: Exchange the authorization code for an access token + token_url = "https://id.twitch.tv/oauth2/token" + token_data = { + "client_id": settings.TWITCH_CLIENT_ID, + "client_secret": settings.TWITCH_CLIENT_SECRET, + "code": code, + "grant_type": "authorization_code", + "redirect_uri": f"{settings.APP_BASE_URL}/auth/twitch/callback", + } + + async with httpx.AsyncClient() as client: + token_response = await client.post(token_url, data=token_data) + if token_response.status_code != 200: + raise HTTPException(status_code=400, detail="Failed to retrieve access token from Twitch.") + + token_json = token_response.json() + access_token = token_json["access_token"] + refresh_token = token_json["refresh_token"] + + # Step 5: Validate the user and get their details from Twitch API + users_url = "https://api.twitch.tv/helix/users" + headers = { + "Client-ID": settings.TWITCH_CLIENT_ID, + "Authorization": f"Bearer {access_token}", + } + user_response = await client.get(users_url, headers=headers) + user_data = user_response.json()["data"][0] + + # TODO: Upsert user into our database + # For now, we'll just return the user data as a proof of concept. + + # Encrypt the tokens for storage + encrypted_tokens = security.encrypt_tokens(access_token, refresh_token) + + # Here you would create or update the user in your database + # user = db.query(models.User).filter_by(platform_user_id=user_data['id']).first() ... etc. + + return {"message": "Twitch login successful!", "user": user_data, "encrypted_tokens": encrypted_tokens} \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..baac62d --- /dev/null +++ b/config.py @@ -0,0 +1,18 @@ +import os +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +class Settings: + """ + A simple class to hold all application settings, loaded from environment variables. + """ + ENCRYPTION_KEY: str = os.getenv("ENCRYPTION_KEY") + TWITCH_CLIENT_ID: str = os.getenv("TWITCH_CLIENT_ID") + TWITCH_CLIENT_SECRET: str = os.getenv("TWITCH_CLIENT_SECRET") + + # The full URL where our app is running, needed for the redirect_uri + APP_BASE_URL: str = "http://localhost:8000" # Update for production + +settings = Settings() \ No newline at end of file diff --git a/main.py b/main.py index 3c25d1c..c0cffef 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,10 @@ from fastapi import FastAPI +from starlette.middleware.sessions import SessionMiddleware import models from database import engine +import auth # Import the new auth module +from config import settings # Import settings to get the secret key # This line tells SQLAlchemy to create all the tables based on the models # we defined. It will create the `multichat_overlay.db` file with the @@ -10,6 +13,13 @@ models.Base.metadata.create_all(bind=engine) app = FastAPI() +# Add the authentication router +app.include_router(auth.router) + +# Add session middleware. A secret key is required for signing the session cookie. +# We can reuse our encryption key for this, but in production you might want a separate key. +app.add_middleware(SessionMiddleware, secret_key=settings.ENCRYPTION_KEY) + @app.get("/") async def read_root(): return {"message": "MultiChatOverlay API"} \ No newline at end of file