Connecting local program to Twitch dev-portal credentials
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
# This is an example file. Copy it to .env and fill in your actual secrets.
|
# 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.
|
# 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
|
||||||
|
|||||||
91
auth.py
Normal file
91
auth.py
Normal file
@@ -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}
|
||||||
18
config.py
Normal file
18
config.py
Normal file
@@ -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()
|
||||||
10
main.py
10
main.py
@@ -1,7 +1,10 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
|
||||||
import models
|
import models
|
||||||
from database import engine
|
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
|
# 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
|
# 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()
|
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("/")
|
@app.get("/")
|
||||||
async def read_root():
|
async def read_root():
|
||||||
return {"message": "MultiChatOverlay API"}
|
return {"message": "MultiChatOverlay API"}
|
||||||
Reference in New Issue
Block a user