FastAPI Security Scopes
Introduction
Security scopes provide a powerful way to control access to specific resources or operations in your API based on user permissions. In OAuth2 terminology, scopes define the specific permissions that a token grants to access protected resources.
With FastAPI's security scopes functionality, you can:
- Limit access to endpoints based on user permissions
- Implement role-based access control
- Create a granular permission system for your API
- Handle different permission levels across your application
In this tutorial, we'll explore how to implement and use security scopes in FastAPI to secure your applications more effectively.
Understanding OAuth2 Scopes
Before diving into the implementation, let's understand what scopes are in the OAuth2 context.
Scopes are strings that represent permissions. For example:
read:items
- Permission to read itemswrite:items
- Permission to create or modify itemsadmin
- Administrative accessprofile
- Access to user profile information
When a user authenticates, they receive a token with specific scopes attached. Your API then checks if the token contains the required scopes before allowing access to protected resources.
Setting Up Security Scopes in FastAPI
Step 1: Define Your Scopes
First, let's define the scopes our application will use:
from fastapi import FastAPI, Depends, Security, HTTPException
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
from pydantic import BaseModel, ValidationError
from typing import List, Optional
from jose import jwt, JWTError
from datetime import datetime, timedelta
# Define available scopes
SCOPES = {
"users:read": "Read information about users",
"users:write": "Create or modify users",
"items:read": "Read items",
"items:write": "Create or modify items",
"admin": "Administrative access"
}
app = FastAPI()
Step 2: Create a Security Scheme with Scopes
Now, let's create an OAuth2 password bearer scheme that includes our scopes:
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes=SCOPES
)
Step 3: Define the Token and User Models
We'll need to define models for our token and user data:
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
scopes: List[str] = []
class User(BaseModel):
username: str
email: str
full_name: Optional[str] = None
disabled: bool = False
class UserInDB(User):
hashed_password: str
Step 4: Create Mock Database and Helper Functions
For this tutorial, we'll use a mock database and simple helper functions:
# Mock user database
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
"hashed_password": "fakehashedsecret",
"disabled": False,
},
"alice": {
"username": "alice",
"full_name": "Alice Smith",
"email": "[email protected]",
"hashed_password": "fakehashedsecret2",
"disabled": False,
},
}
# Mock user scopes
user_scopes = {
"johndoe": ["users:read", "items:read"],
"alice": ["users:read", "users:write", "items:read", "items:write", "admin"],
}
# Secret key for JWT token generation
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def verify_password(plain_password, hashed_password):
# In a real app, use proper password hashing
return plain_password + "notreallyhashed" == hashed_password
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
return None
def authenticate_user(fake_db, username: str, password: str):
user = get_user(fake_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
Step 5: Create the Token Endpoint
Let's create a token endpoint that will issue tokens with appropriate scopes:
from fastapi import Form, status
from fastapi.security import OAuth2PasswordRequestForm
@app.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(fake_users_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"},
)
# Check if requested scopes are valid for this user
user_available_scopes = user_scopes.get(user.username, [])
for scope in form_data.scopes:
if scope not in user_available_scopes:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Not enough permissions. User doesn't have the scope: {scope}",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={
"sub": user.username,
"scopes": form_data.scopes
},
expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
Step 6: Create a Dependency for Getting the Current User with Scopes
Next, we'll create a dependency that extracts and validates the user and their scopes from the token:
async def get_current_user(
security_scopes: SecurityScopes,
token: str = Depends(oauth2_scheme)
):
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = "Bearer"
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": authenticate_value},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_scopes = payload.get("scopes", [])
token_data = TokenData(username=username, scopes=token_scopes)
except (JWTError, ValidationError):
raise credentials_exception
user = get_user(fake_users_db, username=token_data.username)
if user is None:
raise credentials_exception
# Check if user has all required scopes
for scope in security_scopes.scopes:
if scope not in token_data.scopes:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Not enough permissions. Required scope: {scope}",
headers={"WWW-Authenticate": authenticate_value},
)
return user
async def get_current_active_user(
current_user: User = Security(get_current_user, scopes=[])
):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
Using Security Scopes in Endpoints
Now that we have set up our security scopes system, let's use it to protect various endpoints with different permission requirements.
Example 1: Read User Profile (Requires users:read
Scope)
@app.get("/users/me/", response_model=User)
async def read_users_me(
current_user: User = Security(get_current_user, scopes=["users:read"])
):
"""
This endpoint requires the users:read scope.
It returns the current user's information.
"""
return current_user
Example 2: Read All Users (Requires admin
Scope)
@app.get("/users/", response_model=List[User])
async def read_all_users(
current_user: User = Security(get_current_user, scopes=["admin"])
):
"""
This endpoint requires the admin scope.
It returns a list of all users.
"""
users = []
for username, data in fake_users_db.items():
user = User(**data)
users.append(user)
return users
Example 3: Create Item (Requires items:write
Scope)
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
@app.post("/items/")
async def create_item(
item: Item,
current_user: User = Security(get_current_user, scopes=["items:write"])
):
"""
This endpoint requires the items:write scope.
It allows creating a new item.
"""
return {
"item_name": item.name,
"created_by": current_user.username
}
Example 4: Multiple Required Scopes
You can also require multiple scopes for a single endpoint:
@app.put("/users/{username}")
async def update_user(
username: str,
user_data: User,
current_user: User = Security(get_current_user, scopes=["users:write", "admin"])
):
"""
This endpoint requires both users:write and admin scopes.
It allows updating user information.
"""
if username != current_user.username and "admin" not in user_scopes.get(current_user.username, []):
raise HTTPException(status_code=403, detail="You can only update your own user")
return {
"username": username,
"updated": True,
"updater": current_user.username
}
Real-World Example: API for a Blog Platform
Here's a more complete example for a blog platform with different user roles:
# Define more specific scopes
BLOG_SCOPES = {
"posts:read": "Read blog posts",
"posts:write": "Create or edit blog posts",
"posts:delete": "Delete blog posts",
"comments:read": "Read comments",
"comments:write": "Create or edit comments",
"comments:delete": "Delete comments",
"users:admin": "Administer users",
"blog:admin": "Full blog administration"
}
# Blog post model
class BlogPost(BaseModel):
title: str
content: str
published: bool = False
# Comment model
class Comment(BaseModel):
post_id: int
content: str
# Example endpoints
@app.get("/blog/posts/")
async def read_blog_posts(
current_user: User = Security(get_current_user, scopes=["posts:read"])
):
"""Endpoint to read all blog posts. Requires posts:read scope."""
return {"posts": [{"title": "FastAPI is awesome", "id": 1}, {"title": "Security Scopes Tutorial", "id": 2}]}
@app.post("/blog/posts/")
async def create_blog_post(
post: BlogPost,
current_user: User = Security(get_current_user, scopes=["posts:write"])
):
"""Endpoint to create a blog post. Requires posts:write scope."""
return {"post_id": 3, "title": post.title, "author": current_user.username}
@app.delete("/blog/posts/{post_id}")
async def delete_blog_post(
post_id: int,
current_user: User = Security(get_current_user, scopes=["posts:delete"])
):
"""Endpoint to delete a blog post. Requires posts:delete scope."""
return {"deleted": True, "post_id": post_id}
@app.get("/blog/admin/stats")
async def blog_admin_stats(
current_user: User = Security(get_current_user, scopes=["blog:admin"])
):
"""Admin endpoint for blog statistics. Requires blog:admin scope."""
return {
"total_posts": 100,
"total_users": 42,
"total_comments": 359,
"active_users": 27
}
Testing Your Scoped API
To test your API with scopes, you can:
- Use the FastAPI interactive docs (Swagger UI)
- Send requests with tools like curl, Postman, or Python's requests library
Example Using curl
Getting a token with specific scopes:
curl -X POST "http://localhost:8000/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=alice&password=secret&scope=users:read items:read"
Using the token to access a protected endpoint:
curl -X GET "http://localhost:8000/users/me" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Best Practices for Security Scopes
-
Be specific with scope names: Use clear, descriptive names that indicate what resources and actions they permit.
-
Follow the resource:action pattern: For example,
users:read
,items:write
. -
Assign minimum required scopes: Always follow the principle of least privilege.
-
Document your scopes: Make sure users and developers understand what each scope allows.
-
Validate scopes on every request: Don't assume that because a user is authenticated, they have all required permissions.
-
Consider scope hierarchies: You might want higher-level scopes to include lower-level permissions.
Summary
FastAPI's security scopes provide a powerful way to implement fine-grained access control in your API. By defining specific permissions and checking them at the endpoint level, you can ensure that users only have access to the resources and actions they're authorized to use.
In this guide, we've learned:
- How to define security scopes in FastAPI
- How to create and issue tokens with specific scopes
- How to protect endpoints with scope requirements
- How to validate user permissions based on token scopes
- How to implement a complete security system with multiple permission levels
Security scopes are an essential part of any production API that needs to support different user roles or permission levels. With FastAPI, implementing this complex security feature becomes straightforward and declarative.
Additional Resources
Exercises
-
Implement a role-based access control system where users have roles (admin, editor, viewer) and each role has a predefined set of scopes.
-
Create an API that manages a library, with scopes for borrowing books, adding new books, and managing users.
-
Add a feature to revoke specific scopes from a user's token without invalidating the entire token.
-
Implement a scope hierarchy where having an "admin" scope automatically grants all other scopes.
-
Create an endpoint that lets users see what scopes their current token has.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)