Skip to main content

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 flow
  • oauth_token_url (str): Token endpoint URL
  • client_id (str): OAuth client identifier
  • public_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 failed
  • JWTVerificationError: JWT verification failed
  • InvalidPublicKeyError: 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:

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hawcx_oauth_client-1.0.0.tar.gz (43.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hawcx_oauth_client-1.0.0-py3-none-any.whl (40.8 kB view details)

Uploaded Python 3

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

Hashes for hawcx_oauth_client-1.0.0.tar.gz
Algorithm Hash digest
SHA256 4a1b98a55ebf5852ddc9789164d0a3027305a836f0a94b06a813928aa9f9d530
MD5 752ea4cf4b1d3f826c86af3aa767f667
BLAKE2b-256 a0a32c22aae1f0be59274cd1a06d43bb6783ae46fbf8977513d34869f9ced3f9

See more details on using hashes here.

File details

Details for the file hawcx_oauth_client-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for hawcx_oauth_client-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1d514ccbbae8b874e1ac67b09984cc2c4b7a0900bed21d960beb8216c023d716
MD5 0a6516de02724a60a24301656e5eb2c5
BLAKE2b-256 744ecd308853603dac421343fa6b23f3c0d795eb4c4f5bc0051594ce97c66b42

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page