OAuth2 with Password (and Bearer) Tokens
Implementing OAuth2 with Password (and Bearer) tokens in FastAPI is a common approach to secure APIs by requiring clients to authenticate and authorize using their credentials. This method involves clients exchanging their username and password for an access token, which they then use to access protected resources.
Here's a step-by-step guide to setting up OAuth2 with Password (and Bearer) tokens in FastAPI:
Step-by-Step Implementation
1. Install Dependencies
Ensure you have FastAPI and a library for managing password hashing and authentication:
pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt] sqlalchemy
python-jose
: For JWT token creation and verification.passlib
: For secure password hashing.sqlalchemy
: For database interaction.
2. Set Up Project Structure
Create a project structure as follows:
fastapi_oauth2/
│
├── main.py
├── models.py
├── schemas.py
├── database.py
├── auth.py
├── utils.py
└── requirements.txt
3. Define Your Database Models
Create models.py
for defining SQLAlchemy models.
# models.py
from sqlalchemy import Column, Integer, String, Boolean
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
4. Database Connection Setup
Create database.py
to configure the database connection.
# database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" # Use SQLite for simplicity
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
5. Define Pydantic Schemas
Create schemas.py
for request and response schemas.
# schemas.py
from pydantic import BaseModel
class UserCreate(BaseModel):
username: str
password: str
class User(BaseModel):
id: int
username: str
is_active: bool
class Config:
orm_mode = True
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str | None = None
6. Utility Functions for Authentication
Create utils.py
to handle password hashing and JWT creation.
# utils.py
from passlib.context import CryptContext
from jose import JWTError, jwt
from datetime import datetime, timedelta
SECRET_KEY = "your_secret_key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
7. OAuth2 Password Flow
Set up OAuth2 password flow and user authentication in auth.py
.
# auth.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from datetime import datetime, timedelta
from . import models, schemas, utils, database
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_user(db, username: str):
return db.query(models.User).filter(models.User.username == username).first()
def authenticate_user(db, username: str, password: str):
user = get_user(db, username)
if not user:
return False
if not utils.verify_password(password, user.hashed_password):
return False
return user
def create_access_token(data: dict, expires_delta: timedelta | None = None):
return utils.create_access_token(data, expires_delta)
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(database.get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, utils.SECRET_KEY, algorithms=[utils.ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = schemas.TokenData(username=username)
except JWTError:
raise credentials_exception
user = get_user(db, username=token_data.username)
if user is None:
raise credentials_exception
return user
8. Main Application with Endpoints
Put it all together in main.py
.
# main.py
from fastapi import FastAPI, Depends, HTTPException, status
from sqlalchemy.orm import Session
from fastapi.security import OAuth2PasswordRequestForm
from . import models, schemas, utils, auth, database
models.Base.metadata.create_all(bind=database.engine)
app = FastAPI()
@app.post("/token", response_model=schemas.Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(database.get_db)):
user = auth.authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=utils.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = auth.create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.post("/users/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(database.get_db)):
hashed_password = utils.get_password_hash(user.password)
db_user = models.User(username=user.username, hashed_password=hashed_password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
@app.get("/users/me/", response_model=schemas.User)
async def read_users_me(current_user: models.User = Depends(auth.get_current_user)):
return current_user
/token
: Accepts user credentials and returns an access token./users/
: Endpoint to create a new user./users/me/
: Retrieves the currently authenticated user's details.
Running the Application
Start the FastAPI application:
uvicorn main:app --reload
- Navigate to
http://localhost:8000/docs
to access the interactive Swagger UI. - Use the
/users/
endpoint to create a new user. - Obtain an access token by sending a request to
/token
with the user's credentials. - Access the
/users/me/
endpoint with the token to retrieve user details.
Explanation
- Token-Based Authentication:
- Users exchange their credentials (username and password) for a JWT access token.
- This token is then used to authenticate subsequent requests.
- Password Hashing:
- Passwords are securely hashed using
bcrypt
to ensure they are not stored in plaintext. - The
passlib
library simplifies this process.
- Passwords are securely hashed using
- JWT Tokens:
- JSON Web Tokens (JWT) are used to encode user information and provide a stateless authentication mechanism.
- Tokens have an expiration time, after which they are no longer valid.
- Database Interaction:
- SQLAlchemy ORM is used to interact with the database, managing user data.
Security Considerations
- Secret Key: Ensure
SECRET_KEY
is kept secure and not hard-coded in production. Use environment variables or a secrets manager. - Token Expiration: Set appropriate expiration times for tokens to balance between security and user convenience.
- Secure Storage: Store hashed passwords securely and ensure they are never stored or transmitted in plaintext.
- HTTPS: Use HTTPS to protect data in transit, especially for login credentials and token exchange.
Conclusion
With this setup, you have a secure authentication system using OAuth2 with Password (and Bearer) tokens in FastAPI. This method is suitable for securing API endpoints and managing user sessions in modern web applications.
Further Reading
- FastAPI Security: FastAPI Security Documentation
- OAuth2 and OpenID Connect: OAuth2 Overview
- JWT (JSON Web Tokens): JWT Introduction
- Passlib: Passlib Documentation