Skip to main content
Prerequisites: Before you begin, ensure you have the following installed:
  • Python 3.9 or higher
  • pip or Poetry package manager
  • jq - Required for Auth0 CLI setup
  • Your preferred code editor
Flask Version Compatibility: This quickstart requires Flask 3.0 or higher for native async support.

Get Started

This guide demonstrates how to integrate Auth0 with any new or existing Python API built with Flask.
1

Create a new Flask project

Create a new directory for your Flask API:
mkdir flask-auth0-api
cd flask-auth0-api
Create a virtual environment and activate it:
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
2

Install dependencies

Create a requirements.txt file with the following dependencies:
requirements.txt
flask>=3.0
auth0-api-python
python-dotenv
Install the dependencies:
pip install -r requirements.txt
3

Setup your Auth0 API

Next up, you need to create a new API on your Auth0 tenant and configure your application.You can choose to do this automatically by running a CLI command or do it manually via the Dashboard:
  1. Go to the Auth0 DashboardApplicationsAPIs
  2. Click Create API
  3. Enter your API details:
    • Name: My Flask API
    • Identifier: https://my-flask-api (this will be your audience)
    • Signing Algorithm: RS256
  4. Click Create
  5. Copy your Domain from the Dashboard (found under ApplicationsApplications[Your App]Settings)
  6. Copy the Identifier you just created (this is your audience)
Your Domain should not include https:// - use only the domain name (e.g., your-tenant.auth0.com).The Audience (API Identifier) is a unique identifier for your API and can be any valid URI.
4

Define API permissions

Configure permissions (scopes) for your API to control access to specific resources:
  1. In the Auth0 Dashboard, navigate to ApplicationsAPIs
  2. Select your API (My Flask API)
  3. Go to the Permissions tab
  4. Click Add Permission
  5. Add the following permission:
    • Permission (Scope): read:messages
    • Description: Read messages
  6. Click Add
Permissions define what actions can be performed on your API. You can add multiple permissions like write:messages, delete:messages, etc. The /api/private-scoped endpoint in this quickstart requires the read:messages permission.
5

Configure the Auth0 client

If you used the CLI method in Step 3, your .env file was automatically created. Skip to creating the app.py file below.
If you used the Dashboard method, create a .env file in your project root to store your Auth0 configuration:
.env
AUTH0_DOMAIN=your-tenant.us.auth0.com
AUTH0_AUDIENCE=https://my-flask-api
Replace your-tenant.us.auth0.com with your actual Auth0 domain and update the API_IDENTIFIER to match your API identifier from the dashboard.
Create an app.py file and configure the Auth0 API client:
app.py
import os
import asyncio
from flask import Flask, request, jsonify, g
from functools import wraps
from dotenv import load_dotenv
from auth0_api_python import ApiClient, ApiClientOptions
from auth0_api_python.errors import BaseAuthError

# Load environment variables
load_dotenv()

app = Flask(__name__)

# Initialize Auth0 API client (singleton - created once)
api_client = ApiClient(ApiClientOptions(
    domain=os.getenv("AUTH0_DOMAIN"),
    audience=os.getenv("AUTH0_AUDIENCE")
))
5

Create protected routes

Add a decorator for protecting routes and create public and private endpoints:
app.py
# Authentication decorator
def require_auth(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        auth_header = request.headers.get("Authorization", "")
        
        if not auth_header.startswith("Bearer "):
            return jsonify({"error": "Missing or invalid authorization header"}), 401
        
        token = auth_header.split(" ")[1]
        
        try:
            claims = asyncio.run(api_client.verify_access_token(token))
            g.user_claims = claims
            return f(*args, **kwargs)
        except BaseAuthError as e:
            return (
                jsonify({"error": str(e)}),
                e.get_status_code(),
                e.get_headers()
            )
    
    return decorated_function


# Public endpoint - no authentication required
@app.route("/api/public", methods=["GET"])
def public():
    return jsonify({"message": "This endpoint is public"})


# Protected endpoint - requires authentication
@app.route("/api/private", methods=["GET"])
@require_auth
def private():
    return jsonify({
        "message": "This endpoint requires authentication",
        "user": g.user_claims.get("sub")
    })


# Protected endpoint with permission validation
@app.route("/api/private-scoped", methods=["GET"])
@require_auth
def private_scoped():
    scopes = g.user_claims.get("scope", "").split()
    
    if "read:messages" not in scopes:
        return jsonify({"error": "Insufficient permissions"}), 403
    
    return jsonify({
        "message": "Private scoped endpoint - read:messages permission granted",
        "user": g.user_claims.get("sub")
    })


if __name__ == "__main__":
    app.run(debug=True, port=5000)
6

Run your API

Start your Flask application:
python app.py
Your API is now running on http://localhost:5000.
CheckpointYou should now have a fully functional Auth0-protected Flask API running on your localhost with three endpoints:
  • /api/public - Accessible without authentication
  • /api/private - Requires a valid Auth0 access token
  • /api/private-scoped - Requires authentication and the read:messages permission

Test Your API

To test your protected endpoints, you need an access token.

Get a test token

  1. Go to the Auth0 Dashboard
  2. Navigate to Applications → APIs
  3. Select your API
  4. Go to the Test tab
  5. Copy the access token

Make a request

Test the public endpoint (no token required):
curl http://localhost:5000/api/public
Test the protected endpoint (token required):
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  http://localhost:5000/api/private
Replace YOUR_ACCESS_TOKEN with the token you copied from the Auth0 Dashboard.

Advanced Usage

Require specific claims to be present in the access token:
try:
    claims = await api_client.verify_access_token(
        access_token=token,
        required_claims=["email_verified", "org_id"]
    )
except BaseAuthError as e:
    return jsonify({"error": "Missing required claims"}), 401
For enhanced security, enable DPoP (Demonstrating Proof-of-Possession). DPoP enhances OAuth 2.0 by binding access tokens to cryptographic keys.
import asyncio


# Configure API client with DPoP enabled (Allowed mode)
api_client = ApiClient(ApiClientOptions(
    domain=os.getenv("AUTH0_DOMAIN"),
    audience=os.getenv("AUTH0_AUDIENCE"),
    dpop_enabled=True,   # Default - enables DPoP support
    dpop_required=False  # Default - allows both Bearer and DPoP tokens
))

# For Required mode (DPoP-only, rejects Bearer tokens)
# dpop_required=True

# DPoP-aware decorator
def require_auth_dpop(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        headers = {
            "authorization": request.headers.get("Authorization", ""),
            "dpop": request.headers.get("DPoP", "")  # DPoP proof header
        }
        
        try:
            # verify_request() automatically detects Bearer or DPoP scheme
            claims = asyncio.run(api_client.verify_request(
                headers=headers,
                http_method=request.method,
                http_url=request.url
            ))
            g.user_claims = claims
            return f(*args, **kwargs)
        except BaseAuthError as e:
            return jsonify({"error": str(e)}), e.get_status_code(), e.get_headers()
    
    return decorated_function

@app.route("/api/dpop-protected")
@require_auth_dpop
def dpop_protected():
    return jsonify({
        "message": "Successfully authenticated with DPoP or Bearer",
        "user": g.user_claims.get("sub")
    })
The verify_request() method automatically detects whether the request uses Bearer or DPoP authentication. When DPoP is used, it validates both the access token and the DPoP proof according to RFC 9449.
Create a decorator to check for specific scopes:
def require_scope(required_scope):
    def decorator(f):
        @wraps(f)
        async def wrapper(*args, **kwargs):
            if not hasattr(g, "user_claims"):
                return jsonify({"error": "Unauthorized"}), 401
            
            scopes = g.user_claims.get("scope", "").split()
            
            if required_scope not in scopes:
                return jsonify({"error": "Insufficient permissions"}), 403
            
            return await f(*args, **kwargs)
        return wrapper
    return decorator


@app.route("/api/admin")
@require_auth
@require_scope("admin:write")
async def admin_endpoint():
    return jsonify({"message": "Admin access granted"})
Implement comprehensive error handling with specific error types:
from auth0_api_python.errors import (
    BaseAuthError,
    VerifyAccessTokenError,
    GetTokenByExchangeProfileError,
    ApiError
)

@app.errorhandler(BaseAuthError)
def handle_auth_error(error):
    """Handle all Auth0 authentication errors"""
    return jsonify({
        "error": error.get_error_code(),
        "error_description": str(error)
    }), error.get_status_code(), error.get_headers()


@app.errorhandler(VerifyAccessTokenError)
def handle_token_verification_error(error):
    """Handle token verification failures specifically"""
    app.logger.warning(f"Token verification failed: {str(error)}")
    return jsonify({
        "error": "invalid_token",
        "error_description": str(error)
    }), 401, error.get_headers()


@app.errorhandler(ApiError)
def handle_api_error(error):
    """Handle Auth0 API errors (network, rate limits, etc.)"""
    app.logger.error(f"Auth0 API error: {error.code} - {error.message}")
    return jsonify({
        "error": error.code,
        "error_description": error.message
    }), error.status_code or 500


@app.errorhandler(Exception)
def handle_generic_error(error):
    """Catch-all for unexpected errors"""
    app.logger.error(f"Unexpected error: {str(error)}", exc_info=True)
    return jsonify({"error": "Internal server error"}), 500
All authentication errors extend from BaseAuthError, which provides methods like get_status_code(), get_headers(), and get_error_code() for proper HTTP responses with WWW-Authenticate headers.
For applications where most endpoints require authentication, use Flask’s before_request to validate tokens globally:
from flask import g

PUBLIC_ROUTES = ["/api/public", "/health"]

@app.before_request
async def verify_token():
    # Skip auth for public routes
    if request.path in PUBLIC_ROUTES:
        return
    
    auth_header = request.headers.get("Authorization", "")
    
    if not auth_header.startswith("Bearer "):
        return jsonify({"error": "Missing authorization"}), 401
    
    token = auth_header.split(" ")[1]
    
    try:
        claims = await api_client.verify_access_token(token)
        g.user_claims = claims
    except BaseAuthError as e:
        return jsonify({"error": str(e)}), e.get_status_code(), e.get_headers()


@app.route("/api/protected-data")
async def protected_data():
    # Claims automatically available via g.user_claims
    return jsonify({"user_id": g.user_claims["sub"]})

Common Issues

Symptom: Getting 401 errors even with valid-looking tokensCause: The audience in your token doesn’t match the audience configured in your API clientSolution:
  1. Verify the AUTH0_AUDIENCE in your .env file matches your Auth0 API Identifier exactly
  2. The audience is case-sensitive
  3. Ensure the audience is a URL or URN format (e.g., https://my-api not my-api)
Symptom: Token validation fails with issuer mismatchCause: The domain configuration doesn’t match the token issuerSolution:
  1. Verify AUTH0_DOMAIN is correct (e.g., tenant.us.auth0.com)
  2. Don’t include https:// in the domain
  3. Don’t include a trailing slash
Symptom: None values or environment variable errorsCause: Environment variables not loaded or .env file not foundSolution:
  1. Ensure .env file exists in your project root
  2. Verify load_dotenv() is called before accessing os.getenv()
  3. Check that variable names match exactly (case-sensitive)
Symptom: RuntimeError: This event loop is already running or similar async errorsCause: Using async routes without Flask 3.0+ or mixing sync/async incorrectlySolution:
  1. Upgrade to Flask 3.0 or higher: pip install --upgrade flask
  2. Ensure all route handlers using api_client are declared as async def
  3. Don’t use asyncio.run() within route handlers
Symptom: VerifyAccessTokenError: Token is expiredCause: The access token has passed its expiration timeSolution:
  1. Request a new token from the Auth0 Dashboard Test tab
  2. Implement token refresh in your client application
  3. Tokens from the Dashboard are typically valid for 24 hours
Symptom: Missing or invalid authorization header errorCause: Request doesn’t include the Authorization header or uses incorrect formatSolution:
  1. Ensure the header is named Authorization (capital A)
  2. Use format: Authorization: Bearer YOUR_TOKEN
  3. Don’t include quotes around the token

Additional Resources

Next Steps

Check out the Auth0 Python API samples repository for complete working examples with Flask.