Metadata-Version: 2.4
Name: hawcx-oauth-client
Version: 1.0.0
Summary: Lightweight OAuth client for exchanging authorization codes and verifying JWT claims
Author: Hawcx Team
License: MIT
Project-URL: Homepage, https://github.com/hawcx/hawcx-oauth-client
Project-URL: Documentation, https://github.com/hawcx/hawcx-oauth-client#readme
Project-URL: Repository, https://github.com/hawcx/hawcx-oauth-client
Project-URL: Issues, https://github.com/hawcx/hawcx-oauth-client/issues
Keywords: oauth,jwt,authentication,oauth2,verification
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28.0
Requires-Dist: PyJWT[crypto]>=2.8.0
Requires-Dist: cryptography>=41.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: pytest-mock>=3.10; extra == "dev"
Requires-Dist: responses>=0.23; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: ruff>=0.1; extra == "dev"
Requires-Dist: cryptography>=41.0; extra == "dev"
Provides-Extra: flask
Requires-Dist: flask>=2.0; extra == "flask"
Dynamic: license-file

# 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

```bash
# Basic installation
pip install hawcx-oauth-client

# With Flask support
pip install 'hawcx-oauth-client[flask]'
```

Or with [uv](https://github.com/astral-sh/uv):

```bash
uv add hawcx-oauth-client
```

## Quick Start

### OAuth Code Exchange

```python
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:

```python
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:**
```bash
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:**
```bash
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:**
```python
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:**
```python
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:

```python
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()`

```python
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

```bash
cd hawcx_oauth_client
pip install -e '.[dev]'
pytest
```

### Type Checking

```bash
mypy hawcx_oauth_client
```

### Linting

```bash
ruff check hawcx_oauth_client
```

## License

MIT

## Support

For issues, questions, or contributions, please visit:
- GitHub Issues: [hawcx/hawcx-oauth-client/issues](https://github.com/hawcx/hawcx-oauth-client/issues)
- Documentation: [README](https://github.com/hawcx/hawcx-oauth-client#readme)

