Scopes provide fine-grained access control by defining what actions a token is authorized to perform. AuthX supports simple scopes, hierarchical scopes with wildcard matching, and flexible AND/OR logic for permission checks.
Understanding scopes
Scopes are strings that represent permissions:
Simple scopes : read, write, admin
Hierarchical scopes : users:read, posts:write, admin:settings
Wildcard scopes : admin:* matches any scope under the admin namespace
Scopes are stored in the JWT token, making them fast to check without database queries. However, they cannot be updated without issuing a new token.
Adding scopes to tokens
Include scopes when creating tokens:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from authx import AuthX, AuthXConfig
app = FastAPI()
auth_config = AuthXConfig(
JWT_ALGORITHM = "HS256" ,
JWT_SECRET_KEY = "your-secret-key" ,
JWT_TOKEN_LOCATION = [ "headers" ],
)
auth = AuthX( config = auth_config)
auth.handle_errors(app)
class LoginRequest ( BaseModel ):
username: str
password: str
# Define user roles and their scopes
USERS = {
"admin" : {
"password" : "admin123" ,
"scopes" : [ "admin:*" ], # Full admin access
},
"editor" : {
"password" : "editor123" ,
"scopes" : [ "posts:read" , "posts:write" , "posts:delete" ],
},
"viewer" : {
"password" : "viewer123" ,
"scopes" : [ "posts:read" , "users:read" ],
},
}
@app.post ( "/login" )
def login ( data : LoginRequest):
"""Login and receive a token with user's scopes."""
if data.username not in USERS :
raise HTTPException( status_code = 401 , detail = "Invalid credentials" )
user = USERS [data.username]
if user[ "password" ] != data.password:
raise HTTPException( status_code = 401 , detail = "Invalid credentials" )
# Create token with user's scopes
access_token = auth.create_access_token(
uid = data.username,
scopes = user[ "scopes" ]
)
return {
"access_token" : access_token,
"token_type" : "bearer" ,
"scopes" : user[ "scopes" ]
}
Protecting routes with scopes
Single scope requirement
Require a specific scope to access an endpoint:
from typing import Annotated
from fastapi import Depends
from authx.schema import TokenPayload
@app.get ( "/posts" )
async def list_posts (
payload : Annotated[TokenPayload, Depends(auth.scopes_required( "posts:read" ))]
):
"""List posts - requires 'posts:read' scope."""
return {
"message" : "Posts retrieved successfully" ,
"user" : payload.sub,
"scopes" : payload.scopes,
}
@app.post ( "/posts" )
async def create_post (
payload : Annotated[TokenPayload, Depends(auth.scopes_required( "posts:write" ))]
):
"""Create a post - requires 'posts:write' scope."""
return {
"message" : "Post created successfully" ,
"user" : payload.sub,
}
Multiple scopes (AND logic)
Require ALL specified scopes:
@app.delete ( "/posts/ {post_id} " )
async def delete_post (
post_id : int ,
payload : Annotated[
TokenPayload,
Depends(auth.scopes_required( "posts:read" , "posts:delete" ))
],
):
"""Delete a post - requires BOTH 'posts:read' AND 'posts:delete'."""
return {
"message" : f "Post { post_id } deleted" ,
"user" : payload.sub,
}
Multiple scopes (OR logic)
Require ANY of the specified scopes:
@app.get ( "/moderate" )
async def moderate_content (
payload : Annotated[
TokenPayload,
Depends(
auth.scopes_required(
"admin:*" ,
"moderator" ,
all_required = False # OR logic
)
),
],
):
"""Moderate content - requires 'admin:*' OR 'moderator' scope."""
return {
"message" : "Moderation access granted" ,
"user" : payload.sub,
}
Wildcard scopes
Use wildcards for namespace-level permissions:
# User with "admin:*" scope can access all admin resources
@app.get ( "/admin/dashboard" )
async def admin_dashboard (
payload : Annotated[TokenPayload, Depends(auth.scopes_required( "admin:*" ))]
):
"""Admin dashboard - requires 'admin:*' scope."""
return { "message" : "Welcome to admin dashboard" }
@app.get ( "/admin/users" )
async def admin_users (
payload : Annotated[TokenPayload, Depends(auth.scopes_required( "admin:users" ))]
):
"""Admin users - requires 'admin:users' scope.
Note: A user with 'admin:*' will also have access due to wildcard matching.
"""
return { "message" : "Admin users page" }
@app.get ( "/admin/settings" )
async def admin_settings (
payload : Annotated[TokenPayload, Depends(auth.scopes_required( "admin:settings" ))]
):
"""Admin settings - requires 'admin:settings' scope.
Note: A user with 'admin:*' will also have access due to wildcard matching.
"""
return { "message" : "Admin settings page" }
Wildcard scopes like admin:* match any scope under that namespace:
admin:* matches admin:users, admin:settings, admin:reports, etc.
Wildcards only work at the end: admin:* is valid, *:admin is not
Manual scope checking
Check scopes programmatically within your route:
@app.get ( "/profile" )
async def get_profile ( payload : TokenPayload = auth. ACCESS_REQUIRED ):
"""Profile with conditional features based on scopes."""
profile = {
"username" : payload.sub,
"scopes" : payload.scopes,
}
# Manually check for additional features
if payload.has_scopes( "users:write" ):
profile[ "can_edit_profile" ] = True
if payload.has_scopes( "admin:*" ):
profile[ "is_admin" ] = True
# OR logic: check if user has any of the scopes
if payload.has_scopes( "admin" , "moderator" , all_required = False ):
profile[ "can_moderate" ] = True
return profile
Type-safe scope dependencies
Create reusable typed dependencies:
from typing import Annotated
from fastapi import Depends
from authx.schema import TokenPayload
# Define type aliases for common scope requirements
AdminRequired = Annotated[
TokenPayload,
Depends(auth.scopes_required( "admin:*" ))
]
PostsReadRequired = Annotated[
TokenPayload,
Depends(auth.scopes_required( "posts:read" ))
]
PostsWriteRequired = Annotated[
TokenPayload,
Depends(auth.scopes_required( "posts:write" ))
]
PostsDeleteRequired = Annotated[
TokenPayload,
Depends(auth.scopes_required( "posts:read" , "posts:delete" ))
]
# Use in routes
@app.get ( "/posts" )
async def list_posts ( payload : PostsReadRequired):
"""List posts."""
return { "message" : "Posts retrieved" }
@app.post ( "/posts" )
async def create_post ( payload : PostsWriteRequired):
"""Create a post."""
return { "message" : "Post created" }
@app.delete ( "/posts/ {id} " )
async def delete_post ( id : int , payload : PostsDeleteRequired):
"""Delete a post."""
return { "message" : f "Post { id } deleted" }
@app.get ( "/admin/dashboard" )
async def admin_dashboard ( payload : AdminRequired):
"""Admin dashboard."""
return { "message" : "Admin dashboard" }
Error handling
Handle insufficient scope errors:
from authx.exceptions import InsufficientScopeError
from fastapi import Request
from fastapi.responses import JSONResponse
@app.exception_handler (InsufficientScopeError)
async def insufficient_scope_handler (
request : Request,
exc : InsufficientScopeError
):
"""Custom handler for insufficient scope errors."""
return JSONResponse(
status_code = 403 ,
content = {
"error" : "insufficient_scope" ,
"message" : str (exc),
"required_scopes" : exc.required,
"provided_scopes" : exc.provided,
},
)
When a user lacks required scopes:
{
"error" : "insufficient_scope" ,
"message" : "Missing required scopes: ['posts:delete']. Provided: ['posts:read', 'posts:write']" ,
"required_scopes" : [ "posts:read" , "posts:delete" ],
"provided_scopes" : [ "posts:read" , "posts:write" ]
}
Combining with fresh tokens
Combine scope and freshness requirements:
@app.post ( "/admin/delete-user" )
async def delete_user (
user_id : int ,
payload : Annotated[
TokenPayload,
Depends(
auth.scopes_required(
"admin:*" ,
verify_fresh = True # Require fresh token AND admin scope
)
),
],
):
"""Delete user - requires fresh token with admin:* scope."""
return { "message" : f "User { user_id } deleted" }
Dynamic scopes
Load scopes from a database:
async def get_user_scopes ( username : str ) -> list[ str ]:
"""Fetch user scopes from database."""
# Query database for user permissions
user = await db.users.find_one({ "username" : username})
return user.get( "scopes" , [])
@app.post ( "/login" )
async def login ( data : LoginRequest):
"""Login with dynamic scopes from database."""
if data.username not in USERS :
raise HTTPException( status_code = 401 , detail = "Invalid credentials" )
# Fetch scopes from database
scopes = await get_user_scopes(data.username)
# Create token with dynamic scopes
access_token = auth.create_access_token(
uid = data.username,
scopes = scopes
)
return {
"access_token" : access_token,
"token_type" : "bearer" ,
"scopes" : scopes
}
Complete example
from typing import Annotated
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
from authx import AuthX, AuthXConfig, InsufficientScopeError
from authx.schema import TokenPayload
app = FastAPI( title = "Scopes Example" )
auth_config = AuthXConfig(
JWT_ALGORITHM = "HS256" ,
JWT_SECRET_KEY = "your-secret-key" ,
JWT_TOKEN_LOCATION = [ "headers" ],
)
auth = AuthX( config = auth_config)
auth.handle_errors(app)
class LoginRequest ( BaseModel ):
username: str
password: str
# Users with different permission levels
USERS = {
"admin" : {
"password" : "admin123" ,
"scopes" : [ "admin:*" ],
},
"editor" : {
"password" : "editor123" ,
"scopes" : [ "posts:read" , "posts:write" , "posts:delete" ],
},
"viewer" : {
"password" : "viewer123" ,
"scopes" : [ "posts:read" , "users:read" ],
},
}
# Type aliases
PostsReadRequired = Annotated[
TokenPayload,
Depends(auth.scopes_required( "posts:read" ))
]
PostsWriteRequired = Annotated[
TokenPayload,
Depends(auth.scopes_required( "posts:write" ))
]
AdminRequired = Annotated[
TokenPayload,
Depends(auth.scopes_required( "admin:*" ))
]
@app.post ( "/login" )
def login ( data : LoginRequest):
"""Login and receive token with scopes."""
if data.username not in USERS :
raise HTTPException( status_code = 401 , detail = "Invalid credentials" )
user = USERS [data.username]
if user[ "password" ] != data.password:
raise HTTPException( status_code = 401 , detail = "Invalid credentials" )
access_token = auth.create_access_token(
uid = data.username,
scopes = user[ "scopes" ]
)
return {
"access_token" : access_token,
"token_type" : "bearer" ,
"scopes" : user[ "scopes" ],
}
@app.get ( "/posts" )
async def list_posts ( payload : PostsReadRequired):
"""List posts - requires 'posts:read'."""
return { "message" : "Posts retrieved" , "user" : payload.sub}
@app.post ( "/posts" )
async def create_post ( payload : PostsWriteRequired):
"""Create post - requires 'posts:write'."""
return { "message" : "Post created" , "user" : payload.sub}
@app.delete ( "/posts/ {id} " )
async def delete_post (
id : int ,
payload : Annotated[
TokenPayload,
Depends(auth.scopes_required( "posts:read" , "posts:delete" ))
],
):
"""Delete post - requires 'posts:read' AND 'posts:delete'."""
return { "message" : f "Post { id } deleted" }
@app.get ( "/admin/dashboard" )
async def admin_dashboard ( payload : AdminRequired):
"""Admin dashboard - requires 'admin:*'."""
return { "message" : "Admin dashboard" , "user" : payload.sub}
@app.get ( "/profile" )
async def get_profile ( payload : TokenPayload = auth. ACCESS_REQUIRED ):
"""Profile with conditional features."""
profile = {
"username" : payload.sub,
"scopes" : payload.scopes,
}
if payload.has_scopes( "admin:*" ):
profile[ "is_admin" ] = True
return profile
Best practices
Recommendations for scopes:
Use hierarchical scopes with colons: resource:action (e.g., posts:write)
Grant minimal necessary scopes (principle of least privilege)
Use wildcards for administrative roles: admin:*
Store scopes in the token for fast checks without database queries
Remember scopes cannot be updated without issuing a new token
Combine scopes with fresh tokens for sensitive operations
Use type aliases for commonly used scope requirements
Document your scope hierarchy and what each scope grants
Next steps
Fresh tokens Combine scopes with freshness requirements
Token revocation Revoke tokens when permissions change
Error handling Handle insufficient scope errors
Custom callbacks Implement custom permission logic