Lightweight OAuth client for exchanging authorization codes and verifying JWT claims
Project description
Hawcx OAuth Client SDK
A lightweight, production-ready Python library for exchanging OAuth authorization codes and verifying JWT claims. Built with security, reliability, and ease of use in mind.
Features
- 🔐 Secure JWT Verification: RS256 signature verification with configurable validation
- 🆕 PKCE Support: Native PKCE (RFC 7636) support for enhanced OAuth security
- 🚀 Simple API: One function call to go from code to verified claims
- 🎯 High-Level Hawcx Client: One-line MFA setup with automatic encryption/signing
- 🛡️ Comprehensive Error Handling: Clear, actionable error messages for all failure scenarios
- 📝 Type Hints: Full type annotations for better IDE support and type checking
- 🧪 Well Tested: Extensive test coverage including edge cases
- 🔧 Flask Integration: Optional decorator for seamless Flask integration
- 🔍 Detailed Logging: Structured logging for debugging (no sensitive data logged)
Installation
# Basic installation
pip install hawcx-oauth-client
# With Flask support
pip install 'hawcx-oauth-client[flask]'
Or with uv:
uv add hawcx-oauth-client
Quick Start
OAuth Code Exchange
from hawcx_oauth_client import exchange_code_for_claims
# Exchange authorization code for verified claims
claims = exchange_code_for_claims(
code=request.form['code'],
oauth_token_url=os.getenv('OAUTH_TOKEN_ENDPOINT'),
client_id=os.getenv('OAUTH_CLIENT_ID'),
public_key=os.getenv('OAUTH_PUBLIC_KEY'),
# Optional (recommended for production):
code_verifier=session.get('pkce_verifier'), # PKCE support
audience='my-app', # Validate 'aud' claim
issuer='https://oauth.example.com', # Validate 'iss' claim
leeway=10 # Clock skew tolerance
)
# Use the verified claims
user_id = claims['sub']
# Mint your own access token (SDK only verifies, doesn't mint)
Hawcx Delegation (MFA Setup)
For Hawcx MFA setup and user management, use the delegation client:
from hawcx_oauth_client.delegation import HawcxDelegationClient, MfaMethod
# 🎉 One-line initialization from environment variables!
client = HawcxDelegationClient.from_env()
# Initiate MFA setup (Email, SMS, or TOTP)
result = client.initiate_mfa_change(
userid="user@example.com",
mfa_method=MfaMethod.SMS, # Type-safe enum!
phone_number="+15551234567"
)
# Verify OTP and complete MFA setup
client.verify_mfa_change(
userid="user@example.com",
session_id=result['session_id'],
otp="123456"
)
# Get user credentials
creds = client.get_user_credentials("user@example.com")
print(f"MFA method: {creds.get('mfa_method')}")
What it does automatically: ECIES encryption, Ed25519 signatures, request/response crypto, Hawcx payload formatting, type-safe MfaMethod enum
Configuration
Environment Variables
For OAuth Code Exchange:
OAUTH_TOKEN_ENDPOINT="https://oauth.example.com/token"
OAUTH_CLIENT_ID="your-client-id"
OAUTH_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
OAUTH_ISSUER="https://oauth.example.com" # Optional but recommended
OAUTH_AUDIENCE="your-client-id" # Optional but recommended
For Hawcx Delegation:
SP_ED25519_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----..."
SP_X25519_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----..."
IDP_ED25519_PUBLIC_KEY_PEM="-----BEGIN PUBLIC KEY-----..."
IDP_X25519_PUBLIC_KEY_PEM="-----BEGIN PUBLIC KEY-----..."
Public Key Formats
The SDK accepts public keys in multiple formats:
From file:
claims = exchange_code_for_claims(
# ...
public_key='/path/to/public.pem' # Absolute path
# or
public_key=Path('keys/public.pem') # Path object
)
From environment variable:
claims = exchange_code_for_claims(
# ...
public_key=os.getenv('OAUTH_PUBLIC_KEY') # PEM string
)
Expected PEM format:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
Error Handling
The SDK provides specific exceptions for different failure scenarios:
from hawcx_oauth_client import (
exchange_code_for_claims,
OAuthExchangeError,
JWTVerificationError,
InvalidPublicKeyError
)
try:
claims = exchange_code_for_claims(
code=code,
oauth_token_url=oauth_url,
client_id=client_id,
public_key=public_key
)
except OAuthExchangeError as e:
# Code exchange failed (invalid code, network error, etc.)
print(f"Exchange failed: {e}")
print(f"HTTP Status: {e.status_code}")
print(f"Response: {e.response_body}")
except JWTVerificationError as e:
# JWT verification failed (invalid signature, expired, etc.)
print(f"Verification failed: {e}")
print(f"Original error: {e.original_error}")
except InvalidPublicKeyError as e:
# Public key is invalid or unreadable
print(f"Key error: {e}")
Common Error Scenarios
| Exception | Common Causes | Recommended Action |
|---|---|---|
OAuthExchangeError |
Invalid/expired code, network issues | Ask user to re-authenticate |
JWTVerificationError |
Token tampering, expired token | Log incident, ask user to re-authenticate |
InvalidPublicKeyError |
Wrong key, file not found | Check configuration, verify key format |
Security Best Practices
✅ DO
- Always validate audience and issuer in production environments
- Use HTTPS for all OAuth endpoints
- Store keys securely (environment variables, secrets manager)
- Set appropriate leeway for clock skew (5-10 seconds typical)
- Log authentication failures for security monitoring
- Rotate keys regularly following your security policy
❌ DON'T
- Never log JWT tokens or claims containing sensitive data
- Don't disable signature verification in production
- Don't use the id_token as your application's access token (mint your own)
- Don't commit keys to version control
- Don't ignore verification errors or catch them silently
API Reference
exchange_code_for_claims()
def exchange_code_for_claims(
code: str,
oauth_token_url: str,
client_id: str,
public_key: Union[str, Path],
code_verifier: Optional[str] = None, # PKCE support
redirect_uri: Optional[str] = None,
timeout: int = 20,
audience: Optional[str] = None,
issuer: Optional[str] = None,
verify_exp: bool = True,
leeway: int = 0,
) -> Dict[str, Any]
Parameters:
code(str): Authorization code from OAuth flowoauth_token_url(str): Token endpoint URLclient_id(str): OAuth client identifierpublic_key(str | Path): RS256 public key (PEM string or file path)code_verifier(str | None): PKCE code verifier (RFC 7636, optional)redirect_uri(str | None): OAuth redirect URI (optional)timeout(int): Request timeout in seconds (default: 20)audience(str | None): Expected 'aud' claim (optional but recommended)issuer(str | None): Expected 'iss' claim (optional but recommended)verify_exp(bool): Verify token expiration (default: True)leeway(int): Clock skew tolerance in seconds (default: 0)
Returns:
Dict[str, Any]: Verified JWT claims
Raises:
OAuthExchangeError: Code exchange failedJWTVerificationError: JWT verification failedInvalidPublicKeyError: Public key invalid
Development
Running Tests
cd hawcx_oauth_client
pip install -e '.[dev]'
pytest
Type Checking
mypy hawcx_oauth_client
Linting
ruff check hawcx_oauth_client
License
MIT
Support
For issues, questions, or contributions, please visit:
- GitHub Issues: hawcx/hawcx-oauth-client/issues
- Documentation: README
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hawcx_oauth_client-1.0.0.tar.gz.
File metadata
- Download URL: hawcx_oauth_client-1.0.0.tar.gz
- Upload date:
- Size: 43.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4a1b98a55ebf5852ddc9789164d0a3027305a836f0a94b06a813928aa9f9d530
|
|
| MD5 |
752ea4cf4b1d3f826c86af3aa767f667
|
|
| BLAKE2b-256 |
a0a32c22aae1f0be59274cd1a06d43bb6783ae46fbf8977513d34869f9ced3f9
|
File details
Details for the file hawcx_oauth_client-1.0.0-py3-none-any.whl.
File metadata
- Download URL: hawcx_oauth_client-1.0.0-py3-none-any.whl
- Upload date:
- Size: 40.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d514ccbbae8b874e1ac67b09984cc2c4b7a0900bed21d960beb8216c023d716
|
|
| MD5 |
0a6516de02724a60a24301656e5eb2c5
|
|
| BLAKE2b-256 |
744ecd308853603dac421343fa6b23f3c0d795eb4c4f5bc0051594ce97c66b42
|