Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
af89cf8
feat: Add DPoP authentication support
kishore7snehil Jul 28, 2025
a7a1b81
docs: add early access note for DPoP authentication feature
kishore7snehil Jul 28, 2025
8f37411
ci: add GitHub Actions workflow for testing auth0-api-python package
kishore7snehil Jul 28, 2025
7d154e0
fix: update import paths to use package namespace instead of src dire…
kishore7snehil Jul 28, 2025
51e8987
chore: add ruff linting and apply code style fixes
kishore7snehil Jul 29, 2025
cfa18cf
docs: add examples for bearer and DPoP token authentication
kishore7snehil Jul 29, 2025
be536e1
docs: remove DPoP documentation link from README
kishore7snehil Jul 29, 2025
2452937
feat: implement URL normalization using ada-url library and add test …
kishore7snehil Jul 30, 2025
8bff8fc
chore: remove unused URL normalization test script
kishore7snehil Jul 30, 2025
d8ef382
test: add validation tests for edge case
kishore7snehil Jul 30, 2025
c9014aa
test: verify error message for htu mismatch in dpop proof validation
kishore7snehil Jul 30, 2025
940c735
refactor: improve URL normalization and DPoP verification
kishore7snehil Jul 31, 2025
d5a1606
refactor: simplified JWK handling and iat error messages
kishore7snehil Jul 31, 2025
e6afc56
refactor: reorganize test cases
kishore7snehil Jul 31, 2025
41fb87a
test: update error message assertions for DPoP validation failures
kishore7snehil Jul 31, 2025
b6c901e
feat: add include_jti flag to control jti claim inclusion in DPoP pro…
kishore7snehil Jul 31, 2025
503a6a7
fix: improve error message formatting for DPoP scheme validation
kishore7snehil Aug 1, 2025
f6bd02c
Merge branch 'main' into feature/auth0-api-python/dpop-support
kishore7snehil Aug 6, 2025
87916fd
Update packages/auth0_api_python/EXAMPLES.md
kishore7snehil Aug 7, 2025
8cbbe9d
fix: preserve trailing slashes in DPoP proof URL normalization and ad…
kishore7snehil Aug 12, 2025
5247857
fix: remove trailing whitespace in test_api_client.py URL parameter
kishore7snehil Aug 12, 2025
94bae8b
fix: update error handling for authorization and DPoP validation to r…
kishore7snehil Aug 20, 2025
9249fa2
fix: improve error message for unsupported algorithm in DPoP proof va…
kishore7snehil Aug 20, 2025
692e45a
fix: improve error handling for invalid authorization scheme and upda…
kishore7snehil Aug 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ jobs:
working-directory: packages/${{ github.event.inputs.sdk }}
run: poetry install --no-root

- name: Run tests with pytest
working-directory: packages/${{ github.event.inputs.sdk }}
run: |
poetry run pytest -v --cov=src --cov-report=term-missing --cov-report=xml

- name: Build package
working-directory: packages/${{ github.event.inputs.sdk }}
run: poetry build
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/test-auth0-api-python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Test auth0-api-python

on:
push:
branches:
- feature/auth0-api-python
paths:
- 'packages/auth0_api_python/**'
pull_request:
branches:
- main
paths:
- 'packages/auth0_api_python/**'

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, "3.10", "3.11", "3.12"]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true

- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: packages/auth0_api_python/.venv
key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}

- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
working-directory: ./packages/auth0_api_python
run: poetry install --no-interaction --no-root

- name: Install package
working-directory: ./packages/auth0_api_python
run: poetry install --no-interaction

- name: Run tests with pytest
working-directory: ./packages/auth0_api_python
run: |
poetry run pytest -v --cov=src --cov-report=term-missing --cov-report=xml

- name: Run ruff linting
working-directory: ./packages/auth0_api_python
run: |
poetry run ruff check .
16 changes: 16 additions & 0 deletions packages/auth0_api_python/.ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
line-length = 100
target-version = "py39"
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"S", # bandit (security)
]
ignore = ["E501", "B904"] # Line too long (handled by black), Exception handling without from

[per-file-ignores]
"tests/*" = ["S101", "S105", "S106"] # Allow assert and ignore hardcoded password warnings in test files
160 changes: 160 additions & 0 deletions packages/auth0_api_python/EXAMPLES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Auth0 API Python Examples

This document provides examples for using the `auth0-api-python` package to validate Auth0 tokens in your API.

## Bearer Authentication

Bearer authentication is the standard OAuth 2.0 token authentication method.

### Using verify_access_token

```python
import asyncio
from auth0_api_python import ApiClient, ApiClientOptions

async def validate_bearer_token(headers):
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com"
))

try:
# Extract the token from the Authorization header
auth_header = headers.get("authorization", "")
if not auth_header.startswith("Bearer "):
return {"error": "Missing or invalid authorization header"}, 401

token = auth_header.split(" ")[1]

# Verify the access token
claims = await api_client.verify_access_token(token)
return {"success": True, "user": claims["sub"]}
except Exception as e:
return {"error": str(e)}, getattr(e, "get_status_code", lambda: 401)()

# Example usage
headers = {"authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."}
result = asyncio.run(validate_bearer_token(headers))
```

### Using verify_request

```python
import asyncio
from auth0_api_python import ApiClient, ApiClientOptions
from auth0_api_python.errors import BaseAuthError

async def validate_request(headers):
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com"
))

try:
# Verify the request with Bearer token
claims = await api_client.verify_request(
headers=headers
)
return {"success": True, "user": claims["sub"]}
except BaseAuthError as e:
return {"error": str(e)}, e.get_status_code(), e.get_headers()

# Example usage
headers = {"authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."}
result = asyncio.run(validate_request(headers))
```


## DPoP Authentication

[DPoP](https://www.rfc-editor.org/rfc/rfc9449.html) (Demonstrating Proof of Posession) is an application-level mechanism for sender-constraining OAuth 2.0 access and refresh tokens by proving that the client application is in possession of a certain private key.

This guide covers the DPoP implementation in `auth0-api-python` with complete examples for both operational modes.

For more information about DPoP specification, see [RFC 9449](https://tools.ietf.org/html/rfc9449).

## Configuration Modes

### 1. Allowed Mode (Default)
```python
from auth0_api_python import ApiClient, ApiClientOptions

api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com",
dpop_enabled=True, # Default: enables DPoP support
dpop_required=False # Default: allows both Bearer and DPoP
))
```

### 2. Required Mode
```python
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com",
dpop_required=True # Enforces DPoP-only authentication
))
```

## Getting Started

### Basic Usage with verify_request()

The `verify_request()` method automatically detects the authentication scheme:

```python
import asyncio
from auth0_api_python import ApiClient, ApiClientOptions

async def handle_api_request(headers, http_method, http_url):
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com"
))

try:
# Automatically handles both Bearer and DPoP schemes
claims = await api_client.verify_request(
headers=headers,
http_method=http_method,
http_url=http_url
)
return {"success": True, "user": claims["sub"]}
except Exception as e:
return {"error": str(e)}, e.get_status_code()

# Example usage
headers = {
"authorization": "DPoP eyJ0eXAiOiJKV1Q...",
"dpop": "eyJ0eXAiOiJkcG9wK2p3dC..."
}
result = asyncio.run(handle_api_request(headers, "GET", "https://api.example.com/data"))
```

### Direct DPoP Proof Verification

For more control, use `verify_dpop_proof()` directly:

```python
async def verify_dpop_token(access_token, dpop_proof, http_method, http_url):
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com"
))

# First verify the access token
token_claims = await api_client.verify_access_token(access_token)

# Then verify the DPoP proof
proof_claims = await api_client.verify_dpop_proof(
access_token=access_token,
proof=dpop_proof,
http_method=http_method,
http_url=http_url
)

return {
"token_claims": token_claims,
"proof_claims": proof_claims
}
```
73 changes: 73 additions & 0 deletions packages/auth0_api_python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ It’s intended as a foundation for building more framework-specific integration

πŸ“š [Documentation](#documentation) - πŸš€ [Getting Started](#getting-started) - πŸ’¬ [Feedback](#feedback)

## Features & Authentication Schemes

This SDK provides comprehensive support for securing APIs with Auth0-issued access tokens:

### **Authentication Schemes**
- **Bearer Token Authentication** - Traditional OAuth 2.0 Bearer tokens (RS256)
- **DPoP Authentication** - Enhanced security with Demonstrating Proof-of-Possession (ES256)
- **Mixed Mode Support** - Seamlessly handles both Bearer and DPoP in the same API

### **Core Features**
- **Unified Entry Point**: `verify_request()` - automatically detects and validates Bearer or DPoP schemes
- **OIDC Discovery** - Automatic fetching of Auth0 metadata and JWKS
- **JWT Validation** - Complete RS256 signature verification with claim validation
- **DPoP Proof Verification** - Full RFC 9449 compliance with ES256 signature validation
- **Flexible Configuration** - Support for both "Allowed" and "Required" DPoP modes
- **Comprehensive Error Handling** - Detailed errors with proper HTTP status codes and WWW-Authenticate headers
- **Framework Agnostic** - Works with FastAPI, Django, Flask, or any Python web framework

## Documentation

- [Docs Site](https://auth0.com/docs) - explore our docs site and learn more about Auth0.
Expand Down Expand Up @@ -80,6 +98,61 @@ decoded_and_verified_token = await api_client.verify_access_token(

If the token lacks `my_custom_claim` or fails any standard check (issuer mismatch, expired token, invalid signature), the method raises a `VerifyAccessTokenError`.

### 4. DPoP Authentication

> [!NOTE]
> This feature is currently available in [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). Please reach out to Auth0 support to get it enabled for your tenant.

This library supports **DPoP (Demonstrating Proof-of-Possession)** for enhanced security, allowing clients to prove possession of private keys bound to access tokens.

#### Allowed Mode (Default)

Accepts both Bearer and DPoP tokens - ideal for gradual migration:

```python
api_client = ApiClient(ApiClientOptions(
domain="<AUTH0_DOMAIN>",
audience="<AUTH0_AUDIENCE>",
dpop_enabled=True, # Default - enables DPoP support
dpop_required=False # Default - allows both Bearer and DPoP
))

# Use verify_request() for automatic scheme detection
result = await api_client.verify_request(
headers={
"authorization": "DPoP eyJ0eXAiOiJKV1Q...", # DPoP scheme
"dpop": "eyJ0eXAiOiJkcG9wK2p3dC...", # DPoP proof
},
http_method="GET",
http_url="https://api.example.com/resource"
)
```

#### Required Mode

Enforces DPoP-only authentication, rejecting Bearer tokens:

```python
api_client = ApiClient(ApiClientOptions(
domain="<AUTH0_DOMAIN>",
audience="<AUTH0_AUDIENCE>",
dpop_required=True # Rejects Bearer tokens
))
```

#### Configuration Options

```python
api_client = ApiClient(ApiClientOptions(
domain="<AUTH0_DOMAIN>",
audience="<AUTH0_AUDIENCE>",
dpop_enabled=True, # Enable/disable DPoP support
dpop_required=False, # Require DPoP (reject Bearer)
dpop_iat_leeway=30, # Clock skew tolerance (seconds)
dpop_iat_offset=300, # Maximum proof age (seconds)
))
```

## Feedback

### Contributing
Expand Down
Loading