Welcome to the sixth module of the FastAPI tutorial!
This module focuses on security aspects of FastAPI, including authentication and authorization. This time we will also set up Alembic for database migrations. We will add OAuth2 authentication using GitHub as the provider.
-
Clone the repository
git clone https://github.com/margitantal68/FASTAPI/tree/main/module6_security_oauth2
-
Go to the cloned app folder
cd module6_security_oauth2 -
Create a virtual environment:
python -m venv .venv
-
Activate the virtual environment:
- On macOS/Linux:
source .venv/bin/activate- On Windows use
.venv\Scripts\activate
-
Install dependencies
- For Python 3.14:
pip install -r requirements_py314.txt
```bash
alembic init alembic
```
This creates:
bash alembic/ env.py script.py.mako versions/ alembic.ini
module06_security_oauth2/
│
├── .venv/
├── main.py
├── database.py
├── config.py
├── models/
│ └── user.py
│
├── alembic.ini
└── alembic/
├── env.py
└── versions/-
Edit
alembic.inito set the SQLAlchemy URL: Find the line starting withsqlalchemy.urland set it to your database URL, for example:sqlalchemy.url = postgresql://postgres:postgres123!@localhost:5432/fastapi_week6 -
Edit
alembic/env.pyto include your models' metadata:from logging.config import fileConfig from sqlalchemy import engine_from_config, pool from alembic import context from database import Base from models import user # ensures User model is registered config = context.config if config.config_file_name is not None: fileConfig(config.config_file_name) target_metadata = Base.metadata
-
**Migration flow"
Since your DB already has users table + data:
alembic revision --autogenerate -m "baseline" alembic stamp headThen:
alembic revision --autogenerate -m "add github oauth fields" alembic upgrade head
To enable OAuth2 authentication with GitHub, you need to register your application on GitHub to obtain a Client ID and Client Secret.
✅ Step-by-Step: Register an OAuth App on GitHub
-
Go to GitHub Developer Settings Open your browser and go to: https://github.com/settings/developers
-
Choose "OAuth Apps" On the left-hand sidebar, under "Developer settings", click on "OAuth Apps".
-
Click "New OAuth App" You'll see a list (if any exist) and a button to "New OAuth App". Click it.
-
Fill Out the OAuth Application Form Here’s what each field means:
- Field Description
Application name Name of your app (e.g., MyCoolApp)
Homepage URL
http://localhost:5173 - Authorization callback URL
http://localhost:8000/auth/github/callback - Application description (optional) Short description of your app
- Click "Register application" 🎉 After Registration Once registered, GitHub will give you:
Client ID– Public identifier of your appClient Secret– Keep this secret! Used to authenticate your app
You’ll use these values when implementing OAuth in your app.
- Copy the example environment file:
cp .env.example .env
- Edit the
.envfile:- Set your client ID and secret in the
.envfile:
GITHUB_CLIENT_ID=your_client_id GITHUB_CLIENT_SECRET=your_client_secret - Set your client ID and secret in the
Implement the following exercises in routers/auth.py
- Goal: Add
/auth/github/loginand redirect to GitHub. - Tasks:
- Redirect users to GitHub OAuth consent screen.
- Use environment variables for
GITHUB_CLIENT_IDandSECRET. - Use your GitHub OAuth App credentials.
- Goal: Handle GitHub OAuth callback.
- Tasks:
- Exchange code for access_token.
- Fetch user profile and verified email.
- Create new user or link to an existing one.
- Add GitHub fields to the User model:
github_id,avatar_url,auth_provider. - Log the user in automatically with a JWT.
- Goal: Issue JWT and redirect to frontend.
- Tasks:
- On successful OAuth, create a JWT.
- Redirect to frontend with ?token=... in the URL.
- Allow frontend to decode and store the token.
- Verify the decoded token in the frontend.
-
Create a copy of the module06_security; add
auth.pyto theroutersdirectory:module6_security/ ├── main.py ├── database.py ├── utils.py ├── config.py ├── models/ │ ├── user.py └── routers/ ├── users.py |── auth.py -
.envfile:Set the
CLIENT_IDandCLIENT_SECRETfor OAuth2 authentication. -
Modify
config.pyin order to read GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET:# OAuth Configuration GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID") GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET") GITHUB_REDIRECT_URI = "http://localhost:8000/auth/github/callback" FRONTEND_REDIRECT_URL = "http://localhost:5173/oauth/callback"
-
Modify the model for users (
models/user.py):from sqlalchemy import Column, Integer, String from sqlalchemy.orm import declarative_base from database import Base from pydantic import BaseModel class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=True, index=True) fullname = Column(String) email = Column(String, unique=True, index=True) hashed_password = Column(String) # New fields for GitHub authentication github_id = Column(String, unique=True, index=True, nullable=True) avatar_url = Column(String, nullable=True) auth_provider = Column(String, default="local") # e.g., 'local' or 'github'
-
Create
routers/auth.py: implement GitHub OAuth2 routes:import httpx from fastapi import APIRouter, HTTPException, Request, Depends from starlette.responses import RedirectResponse from jose import jwt from sqlalchemy.orm import Session from database import get_db # Your DB session dependency from models.user import User from config import ( GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_REDIRECT_URI, JWT_SECRET_KEY, JWT_ALGORITHM, FRONTEND_REDIRECT_URL, ) router = APIRouter() @router.get("/github/login") def login_with_github(): print(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET) if not GITHUB_CLIENT_ID or not GITHUB_CLIENT_SECRET: raise HTTPException(status_code=500, detail="GitHub OAuth credentials are not set") else: print("GitHub OAuth credentials are set") github_auth_url = ( f"https://github.com/login/oauth/authorize" f"?client_id={GITHUB_CLIENT_ID}" f"&redirect_uri={GITHUB_REDIRECT_URI}" f"&scope=read:user user:email" ) return RedirectResponse(github_auth_url) @router.get("/github/callback") async def github_callback(request: Request, db: Session = Depends(get_db)): print("GitHub callback received") code = request.query_params.get("code") print(f"Received code: {code}") if not code: raise HTTPException(status_code=400, detail="Missing GitHub code") # Step 1: Exchange code for access token async with httpx.AsyncClient() as client: token_response = await client.post( "https://github.com/login/oauth/access_token", headers={"Accept": "application/json"}, data={ "client_id": GITHUB_CLIENT_ID, "client_secret": GITHUB_CLIENT_SECRET, "code": code, "redirect_uri": GITHUB_REDIRECT_URI, }, ) token_data = token_response.json() access_token = token_data.get("access_token") print(f"Access token: {access_token}") if not access_token: raise HTTPException(status_code=400, detail="GitHub token exchange failed") # Step 2: Fetch GitHub user profile user_response = await client.get( "https://api.github.com/user", headers={"Authorization": f"Bearer {access_token}"} ) user_data = user_response.json() print(f"User data: {user_data}") # Step 3: Get primary email email_response = await client.get( "https://api.github.com/user/emails", headers={"Authorization": f"Bearer {access_token}"} ) email_data = email_response.json() primary_email = next((e["email"] for e in email_data if e.get("primary") and e.get("verified")), None) if not primary_email: raise HTTPException(status_code=400, detail="No verified primary email found") # Step 4: Create or get user user = get_or_create_user( db=db, github_id=str(user_data["id"]), email=primary_email, fullname=user_data.get("name"), avatar_url=user_data.get("avatar_url"), ) print(f"Store user's data in a local db: {user}") # Step 5: Generate JWT jwt_payload = {"sub": user.username, "email": user.email} token = jwt.encode(jwt_payload, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) # Step 6: Redirect to frontend with token return RedirectResponse(f"{FRONTEND_REDIRECT_URL}?token={token}") def get_or_create_user( db: Session, github_id: str, email: str, fullname: str = None, avatar_url: str = None, ): # 1. Try to find user by GitHub ID user = db.query(User).filter(User.github_id == github_id).first() # 2. If not found, try to find user by email (account linking) if not user and email: user = db.query(User).filter(User.email == email).first() if user: # Link GitHub to existing user user.github_id = github_id user.avatar_url = avatar_url user.auth_provider = "github" db.commit() db.refresh(user) # 3. If still not found, create new user if not user: user = User( username=email.split("@")[0], # You can refine this logic fullname=fullname, email=email, github_id=github_id, avatar_url=avatar_url, auth_provider="github", hashed_password=None # GitHub users don’t have local passwords ) db.add(user) db.commit() db.refresh(user) return user
-
Modify
main.py: include auth router:app.include_router(users.router, prefix="/users", tags=["Users"]) app.include_router(auth.router, prefix="/auth", tags=["Auth"])
-
Run the FastAPI app:
uvicorn main:app --reload