Padroneggia l'autenticazione OAuth2 con FastAPI! Questa guida copre il flusso con password, il flusso implicito, il flusso con codice di autorizzazione, l'aggiornamento del token e le migliori pratiche di sicurezza per la creazione di API robuste.
FastAPI OAuth2 Implementation: A Comprehensive Authentication Flow Guide
Nel panorama digitale odierno, proteggere le tue API è fondamentale. OAuth2 (Open Authorization) è diventato lo standard del settore per l'autorizzazione delegata, consentendo agli utenti di concedere un accesso limitato alle proprie risorse senza condividere le proprie credenziali. FastAPI, un framework web Python moderno e ad alte prestazioni, semplifica l'implementazione dell'autenticazione OAuth2. Questa guida completa ti guiderà attraverso i vari flussi OAuth2 e dimostrerà come integrarli nella tua applicazione FastAPI, garantendo che la tua API rimanga sicura e accessibile.
Understanding OAuth2 Concepts
Prima di immergerci nel codice, stabiliamo una chiara comprensione dei concetti fondamentali di OAuth2:
- Resource Owner: L'utente che possiede i dati e concede l'accesso.
- Client: L'applicazione che richiede l'accesso ai dati del proprietario della risorsa. Potrebbe trattarsi di un'applicazione web, un'app mobile o qualsiasi altro servizio.
- Authorization Server: Autentica il proprietario della risorsa e concede l'autorizzazione al client.
- Resource Server: Ospita le risorse protette e verifica il token di accesso prima di concedere l'accesso.
- Access Token: Una credenziale che rappresenta l'autorizzazione concessa dal proprietario della risorsa al client.
- Refresh Token: Una credenziale a lunga durata utilizzata per ottenere nuovi token di accesso senza richiedere al proprietario della risorsa di autorizzare nuovamente.
- Scopes: Definiscono le autorizzazioni specifiche richieste dal client.
OAuth2 Flows: Choosing the Right Approach
OAuth2 definisce diversi flussi di autorizzazione, ognuno adatto a scenari diversi. Ecco una suddivisione dei flussi più comuni e quando usarli:
1. Password (Resource Owner Password Credentials) Flow
Description: Il client ottiene direttamente il token di accesso dal server di autorizzazione fornendo il nome utente e la password del proprietario della risorsa. Use Case: Applicazioni altamente affidabili, come le app mobili di prima parte. Dovrebbe essere utilizzato solo quando altri flussi non sono fattibili. Pros: Semplice da implementare. Cons: Richiede al client di gestire le credenziali del proprietario della risorsa, aumentando il rischio di esposizione se il client viene compromesso. Meno sicuro di altri flussi. Example: L'app mobile di una società che accede alla propria API interna.
Implementation in FastAPI:
First, install the necessary packages:
pip install fastapi uvicorn python-multipart passlib[bcrypt] python-jose[cryptography]
Now, let's create a basic example:
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
app = FastAPI()
# Replace with a strong, randomly generated secret key
SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Password hashing configuration
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Dummy user database (replace with a real database in production)
users = {
"johndoe": {
"username": "johndoe",
"hashed_password": pwd_context.hash("password123"),
"scopes": ["read", "write"]
}
}
# Function to verify password
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
# Function to create access token
def create_access_token(data: dict, expires_delta: timedelta):
to_encode = data.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# OAuth2 endpoint for token generation
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = users.get(form_data.username)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not verify_password(form_data.password, user["hashed_password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user["username"], "scopes": user["scopes"]},
expires_delta=access_token_expires,
)
return {"access_token": access_token, "token_type": "bearer"}
# Dependency to authenticate requests
async def get_current_user(token: str):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = users.get(username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user = Depends(get_current_user)):
return current_user
# Example protected endpoint
@app.get("/users/me")
async def read_users_me(current_user = Depends(get_current_active_user)):
return {"username": current_user["username"], "scopes": current_user["scopes"]}
Explanation:
- Dependencies: We use `fastapi.security.OAuth2PasswordRequestForm` for handling the username and password.
- Password Hashing: `passlib` is used for securely hashing and verifying passwords. Never store passwords in plain text!
- JWT Generation: `python-jose` is used for creating and verifying JSON Web Tokens (JWTs).
- `/token` endpoint: This endpoint handles the login process. It validates the username and password, and if valid, generates an access token.
- `get_current_user` dependency: This function verifies the access token and retrieves the user.
- `/users/me` endpoint: This is a protected endpoint that requires a valid access token to access.
2. Implicit Flow
Description: Il client riceve direttamente il token di accesso dal server di autorizzazione dopo che il proprietario della risorsa si è autenticato. Il token di accesso viene restituito nel frammento URL. Use Case: Applicazioni a pagina singola (SPA) e altre applicazioni basate su browser in cui la memorizzazione dei segreti del client non è fattibile. Pros: Semplice per le applicazioni basate su browser. Cons: Meno sicuro di altri flussi perché il token di accesso è esposto nell'URL. Non viene emesso alcun token di aggiornamento. Example: Un'applicazione JavaScript che accede a un'API di social media.
Implementation Considerations in FastAPI:
While FastAPI doesn't directly handle the frontend aspects of the Implicit Flow (as it's primarily a backend framework), you would use a frontend framework like React, Vue, or Angular to manage the authentication flow. FastAPI would primarily act as the Resource Server.
Simplified Backend (FastAPI - Resource Server) Example:
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2AuthorizationCodeBearer
from jose import JWTError, jwt
app = FastAPI()
# Replace with a strong, randomly generated secret key
SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"
# Dummy user database (replace with a real database in production)
users = {
"johndoe": {
"username": "johndoe",
"scopes": ["read", "write"]
}
}
# OAuth2 scheme - using AuthorizationCodeBearer for token verification
oauth2_scheme = OAuth2AuthorizationCodeBearer(authorizationUrl="/auth", tokenUrl="/token") # These URLs are handled by the Authorization Server (not this FastAPI app).
# Dependency to authenticate requests
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = users.get(username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user = Depends(get_current_user)):
return current_user
# Example protected endpoint
@app.get("/users/me")
async def read_users_me(current_user = Depends(get_current_active_user)):
return {"username": current_user["username"], "scopes": current_user["scopes"]}
Key Points for Implicit Flow with FastAPI:
- Authorization Server's Role: The actual authorization and token issuance happens on a separate Authorization Server. FastAPI acts as the Resource Server, validating the token.
- Frontend Handling: The frontend application (e.g., React, Vue) handles the redirect to the Authorization Server, the user login, and the retrieval of the access token from the URL fragment.
- Security Considerations: Due to the exposure of the access token in the URL, it's crucial to use HTTPS and keep the token lifetime short. The implicit flow should be avoided if possible in favor of the Authorization Code Flow with PKCE.
3. Authorization Code Flow
Description: Il client ottiene prima un codice di autorizzazione dal server di autorizzazione, che poi scambia con un token di accesso. Questo flusso prevede un reindirizzamento dal client al server di autorizzazione e viceversa. Use Case: Applicazioni web e app mobile in cui è possibile archiviare in modo sicuro un segreto del client. Pros: Più sicuro del flusso implicito perché il token di accesso non è direttamente esposto nel browser. Cons: Più complesso da implementare rispetto al flusso implicito. Example: Un'applicazione di terze parti che richiede l'accesso ai dati di Google Drive di un utente.
Authorization Code Flow with PKCE (Proof Key for Code Exchange):
PKCE è un'estensione del flusso con codice di autorizzazione che mitiga il rischio di intercettazione del codice di autorizzazione. È altamente raccomandato per app mobile e SPA, in quanto non richiede al client di archiviare un segreto.
Implementation Considerations in FastAPI: Similar to the Implicit Flow, FastAPI would primarily act as the Resource Server in this flow. A separate Authorization Server is responsible for the authentication and authorization code issuance.
Simplified Backend (FastAPI - Resource Server) Example (Similar to Implicit Flow):
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2AuthorizationCodeBearer
from jose import JWTError, jwt
app = FastAPI()
# Replace with a strong, randomly generated secret key
SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"
# Dummy user database (replace with a real database in production)
users = {
"johndoe": {
"username": "johndoe",
"scopes": ["read", "write"]
}
}
# OAuth2 scheme - using AuthorizationCodeBearer for token verification
oauth2_scheme = OAuth2AuthorizationCodeBearer(authorizationUrl="/auth", tokenUrl="/token") # These URLs are handled by the Authorization Server.
# Dependency to authenticate requests
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = users.get(username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user = Depends(get_current_user)):
return current_user
# Example protected endpoint
@app.get("/users/me")
async def read_users_me(current_user = Depends(get_current_active_user)):
return {"username": current_user["username"], "scopes": current_user["scopes"]}
Key Points for Authorization Code Flow with PKCE with FastAPI:
- Authorization Server's Role: The Authorization Server handles the generation of the authorization code, verification of the PKCE code verifier, and issuance of the access token.
- Frontend Handling: The frontend application generates a code verifier and code challenge, redirects the user to the Authorization Server, receives the authorization code, and exchanges it for an access token.
- Increased Security: PKCE prevents authorization code interception attacks, making it suitable for SPAs and mobile apps.
- Recommended Approach: The Authorization Code Flow with PKCE is generally the most secure and recommended flow for modern web and mobile applications.
4. Client Credentials Flow
Description: Il client si autentica direttamente con il server di autorizzazione utilizzando le proprie credenziali (ID client e segreto del client) per ottenere un token di accesso. Use Case: Comunicazione da macchina a macchina, come servizi backend che accedono l'uno all'altro. Pros: Semplice per i servizi backend. Cons: Non adatto per l'autenticazione dell'utente. Example: Un servizio di elaborazione dati che accede a un servizio di database.
Implementation in FastAPI:
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from jose import JWTError, jwt
from datetime import datetime, timedelta
app = FastAPI()
# Replace with a strong, randomly generated secret key
SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Dummy client database (replace with a real database in production)
clients = {
"client_id": {
"client_secret": "client_secret",
"scopes": ["read", "write"]
}
}
# Function to create access token
def create_access_token(data: dict, expires_delta: timedelta):
to_encode = data.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# HTTP Basic Authentication scheme
security = HTTPBasic()
# Endpoint for token generation
@app.post("/token")
async def login(credentials: HTTPBasicCredentials = Depends(security)):
client = clients.get(credentials.username)
if not client:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect client ID or secret",
headers={"WWW-Authenticate": "Basic"},
)
if credentials.password != client["client_secret"]:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect client ID or secret",
headers={"WWW-Authenticate": "Basic"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": credentials.username, "scopes": client["scopes"]},
expires_delta=access_token_expires,
)
return {"access_token": access_token, "token_type": "bearer"}
# Dependency to authenticate requests
async def get_current_client(token: str):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
client_id: str = payload.get("sub")
if client_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
client = clients.get(client_id)
if client is None:
raise credentials_exception
return client
async def get_current_active_client(current_client = Depends(get_current_client)):
return current_client
# Example protected endpoint
@app.get("/data")
async def read_data(current_client = Depends(get_current_active_client)):
return {"message": "Data accessed by client: " + current_client["client_secret"]}
Explanation:
- HTTP Basic Authentication: We use `fastapi.security.HTTPBasic` for authenticating the client.
- `/token` endpoint: This endpoint handles the client authentication. It validates the client ID and secret, and if valid, generates an access token.
- `get_current_client` dependency: This function verifies the access token and retrieves the client.
- `/data` endpoint: This is a protected endpoint that requires a valid access token to access.
Token Refresh
I token di accesso hanno in genere una breve durata per ridurre al minimo l'impatto dei token compromessi. I token di aggiornamento sono credenziali a lunga durata che possono essere utilizzate per ottenere nuovi token di accesso senza richiedere all'utente di autorizzare nuovamente.
Implementation Considerations:
- Storing Refresh Tokens: I token di aggiornamento devono essere archiviati in modo sicuro, idealmente crittografati in un database.
- Refresh Token Endpoint: Create a dedicated endpoint (e.g., `/refresh_token`) to handle refresh token requests.
- Revoking Refresh Tokens: Implement a mechanism to revoke refresh tokens if they are compromised or no longer needed.
Example (Extending the Password Flow Example):
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
import secrets # For generating secure random strings
app = FastAPI()
# Replace with a strong, randomly generated secret key
SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
REFRESH_TOKEN_EXPIRE_DAYS = 30 # Longer lifetime for refresh tokens
# Password hashing configuration
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Dummy user database (replace with a real database in production)
users = {
"johndoe": {
"username": "johndoe",
"hashed_password": pwd_context.hash("password123"),
"scopes": ["read", "write"],
"refresh_token": None # Store refresh token here
}
}
# Function to verify password (same as before)
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
# Function to create access token (same as before)
def create_access_token(data: dict, expires_delta: timedelta):
to_encode = data.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# Function to create refresh token
def create_refresh_token():
return secrets.token_urlsafe(32) # Generate a secure random string
# OAuth2 endpoint for token generation
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = users.get(form_data.username)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not verify_password(form_data.password, user["hashed_password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Create refresh token and store it (securely in a database in real-world)
refresh_token = create_refresh_token()
user["refresh_token"] = refresh_token # Store it in the user object for now (INSECURE for production)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user["username"], "scopes": user["scopes"]},
expires_delta=access_token_expires,
)
return {"access_token": access_token, "token_type": "bearer", "refresh_token": refresh_token}
# Endpoint for refreshing the access token
@app.post("/refresh_token")
async def refresh_access_token(refresh_token: str):
# Find user by refresh token (securely query the database)
user = next((user for user in users.values() if user["refresh_token"] == refresh_token), None)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token",
headers={"WWW-Authenticate": "Bearer"},
)
# Create a new access token
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user["username"], "scopes": user["scopes"]},
expires_delta=access_token_expires,
)
return {"access_token": access_token, "token_type": "bearer"}
# Dependency to authenticate requests (same as before)
async def get_current_user(token: str):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = next((user for user in users.values() if user["username"] == username), None)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user = Depends(get_current_user)):
return current_user
# Example protected endpoint (same as before)
@app.get("/users/me")
async def read_users_me(current_user = Depends(get_current_active_user)):
return {"username": current_user["username"], "scopes": current_user["scopes"]}
Important Security Notes:
- Storing Refresh Tokens: The example stores the refresh token in memory (insecurely). In a production environment, store refresh tokens securely in a database, preferably encrypted.
- Refresh Token Rotation: Consider implementing refresh token rotation. After a refresh token is used, generate a new refresh token and invalidate the old one. This limits the impact of compromised refresh tokens.
- Auditing: Log refresh token usage to detect suspicious activity.
Security Best Practices
Implementing OAuth2 is only the first step. Adhering to security best practices is crucial to protect your API and user data.
- Use HTTPS: Utilizza sempre HTTPS per crittografare la comunicazione tra il client, il server di autorizzazione e il server di risorse.
- Validate Input: Convalida accuratamente tutti i dati di input per prevenire attacchi di injection.
- Rate Limiting: Implementa la limitazione della frequenza per prevenire attacchi di forza bruta.
- Regularly Update Dependencies: Mantieni aggiornato il tuo framework FastAPI e tutte le dipendenze per correggere le vulnerabilità di sicurezza.
- Use Strong Secrets: Genera segreti casuali e complessi per i segreti del tuo client e le chiavi di firma JWT. Archivia questi segreti in modo sicuro (ad esempio, utilizzando variabili d'ambiente o un sistema di gestione dei segreti).
- Monitor and Log: Monitora la tua API per attività sospette e registra tutti gli eventi di autenticazione e autorizzazione.
- Enforce Least Privilege: Concedi ai client solo le autorizzazioni necessarie (scope).
- Proper Error Handling: Evita di esporre informazioni sensibili nei messaggi di errore.
- Consider using a well-vetted OAuth2 library: Instead of implementing OAuth2 from scratch, consider using a well-vetted library like Authlib. Authlib provides a more robust and secure implementation of OAuth2.
Beyond the Basics: Advanced Considerations
Dopo aver implementato una base OAuth2, considera questi aspetti avanzati:
- Consent Management: Fornisci agli utenti un controllo chiaro e granulare sulle autorizzazioni che concedono ai client.
- Delegated Authorization: Implementa il supporto per l'autorizzazione delegata, consentendo agli utenti di autorizzare i client ad agire per loro conto.
- Multi-Factor Authentication (MFA): Integra l'autenticazione a più fattori (MFA) per migliorare la sicurezza.
- Federated Identity: Supporta l'autenticazione tramite provider di identità di terze parti (ad esempio, Google, Facebook, Twitter).
- Dynamic Client Registration: Consenti ai client di registrarsi dinamicamente con il tuo server di autorizzazione.
Conclusion
L'implementazione dell'autenticazione OAuth2 con FastAPI è un modo efficace per proteggere le tue API e i dati degli utenti. Comprendendo i diversi flussi OAuth2, implementando le migliori pratiche di sicurezza e considerando argomenti avanzati, puoi creare API robuste e sicure che soddisfino le esigenze dei tuoi utenti e delle tue applicazioni. Ricorda di scegliere il flusso appropriato per il tuo caso d'uso specifico, dare la priorità alla sicurezza e monitorare e migliorare continuamente il tuo sistema di autenticazione. Sebbene gli esempi forniti mostrino i principi fondamentali, adattali sempre alle tue esigenze specifiche e consulta esperti di sicurezza per una revisione approfondita.