Skip to main content

Hawcx OAuth Client SDK for Python

Add passwordless authentication to your Python backend. Exchange OAuth codes for verified user claims and manage MFA.

Installation

pip install hawcx-oauth-client

Quick Start

OAuth Code Exchange

The most common flow: exchange a Hawcx authorization code for verified user claims.
from hawcx_oauth_client import exchange_code_for_claims
import os

@app.route('/auth/callback', methods=['POST'])
def auth_callback():
    try:
        # Exchange the 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']
        email = claims['email']
        
        # Create user session or JWT
        session['user_id'] = user_id
        session['email'] = email

        return jsonify({'success': True, 'userId': user_id})
    except Exception as error:
        app.logger.error(f'Authentication failed: {error}')
        return jsonify({'error': 'Authentication failed'}), 401

Hawcx Delegation Client (MFA Setup)

For advanced use cases like setting up MFA for users programmatically:
from hawcx_oauth_client.delegation import HawcxDelegationClient, MfaMethod
import os

# Initialize the delegation client with your signing/encryption keys
client = HawcxDelegationClient.from_keys(
    sp_signing_key=os.getenv('SP_ED25519_PRIVATE_KEY_PEM'),
    sp_encryption_key=os.getenv('SP_X25519_PRIVATE_KEY_PEM'),
    idp_verify_key=os.getenv('IDP_ED25519_PUBLIC_KEY_PEM'),
    idp_encryption_key=os.getenv('IDP_X25519_PUBLIC_KEY_PEM'),
    base_url='https://ceasar-api.hawcx.com',
    sp_id=os.getenv('OAUTH_CLIENT_ID')
)

# Initiate MFA setup (Email, SMS, or TOTP)
result = client.initiate_mfa_change(
    userid="[email protected]",
    mfa_method=MfaMethod.SMS,  # Type-safe enum!
    phone_number="+15551234567"
)

# Verify OTP and complete MFA setup
client.verify_mfa_change(
    userid="[email protected]",
    session_id=result['session_id'],
    otp="123456"
)

# Get user credentials
creds = client.get_user_credentials("[email protected]")
print(f"MFA method: {creds.get('mfa_method')}")

Configuration

Environment Variables

For OAuth Code Exchange:
OAUTH_TOKEN_ENDPOINT="https://oauth.hawcx.com/token"
OAUTH_CLIENT_ID="your-client-id"
OAUTH_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
OAUTH_ISSUER="https://oauth.hawcx.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-----..."
OAUTH_CLIENT_ID="your-client-id"

Public Key Formats

The SDK accepts public keys in multiple formats: From environment variable (recommended):
from os import getenv

claims = exchange_code_for_claims(
    code=code,
    oauth_token_url=getenv('OAUTH_TOKEN_ENDPOINT'),
    client_id=getenv('OAUTH_CLIENT_ID'),
    public_key=getenv('OAUTH_PUBLIC_KEY')  # PEM string
)
From file path:
from pathlib import Path

claims = exchange_code_for_claims(
    code=code,
    oauth_token_url=token_endpoint,
    client_id=client_id,
    public_key=Path('keys/public.pem')  # Path object or 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=auth_code,
        oauth_token_url=token_endpoint,
        client_id=client_id,
        public_key=public_key
    )
except OAuthExchangeError as e:
    # The authorization code may be invalid or expired
    print(f'OAuth exchange failed: {e}')
except JWTVerificationError as e:
    # The JWT signature or claims validation failed
    print(f'JWT verification failed: {e}')
except InvalidPublicKeyError as e:
    # The provided public key is malformed
    print(f'Invalid public key: {e}')

Integration Examples

Flask

from flask import Flask, request, jsonify, session
from hawcx_oauth_client import exchange_code_for_claims
import os
import logging

app = Flask(__name__)
app.secret_key = os.getenv('FLASK_SECRET_KEY')
logging.basicConfig(level=logging.INFO)

@app.route('/auth/callback', methods=['POST'])
def auth_callback():
    """Handle Hawcx OAuth callback"""
    code = request.form.get('code')
    
    if not code:
        return jsonify({'error': 'Missing authorization code'}), 400
    
    try:
        claims = exchange_code_for_claims(
            code=code,
            oauth_token_url=os.getenv('OAUTH_TOKEN_ENDPOINT'),
            client_id=os.getenv('OAUTH_CLIENT_ID'),
            public_key=os.getenv('OAUTH_PUBLIC_KEY'),
            audience=os.getenv('OAUTH_CLIENT_ID')
        )
        
        # Create user session
        session['user_id'] = claims['sub']
        session['email'] = claims.get('email')
        
        return jsonify({'success': True, 'userId': claims['sub']})
    except Exception as error:
        app.logger.error(f'Authentication failed: {error}')
        return jsonify({'error': 'Authentication failed'}), 401

@app.route('/auth/user', methods=['GET'])
def get_user():
    """Get current user from session"""
    user_id = session.get('user_id')
    if not user_id:
        return jsonify({'error': 'Unauthorized'}), 401
    
    return jsonify({'userId': user_id, 'email': session.get('email')})

if __name__ == '__main__':
    app.run(debug=False, port=5000)

Django

from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from hawcx_oauth_client import exchange_code_for_claims
import os

@require_http_methods(["POST"])
def auth_callback(request):
    """Handle Hawcx OAuth callback"""
    code = request.POST.get('code')
    
    if not code:
        return JsonResponse({'error': 'Missing authorization code'}, status=400)
    
    try:
        claims = exchange_code_for_claims(
            code=code,
            oauth_token_url=os.getenv('OAUTH_TOKEN_ENDPOINT'),
            client_id=os.getenv('OAUTH_CLIENT_ID'),
            public_key=os.getenv('OAUTH_PUBLIC_KEY')
        )
        
        # Create user session
        request.session['user_id'] = claims['sub']
        request.session['email'] = claims.get('email')
        
        return JsonResponse({'success': True, 'userId': claims['sub']})
    except Exception as error:
        return JsonResponse({'error': 'Authentication failed'}, status=401)

@require_http_methods(["GET"])
def get_user(request):
    """Get current user from session"""
    user_id = request.session.get('user_id')
    if not user_id:
        return JsonResponse({'error': 'Unauthorized'}, status=401)
    
    return JsonResponse({
        'userId': user_id,
        'email': request.session.get('email')
    })

FastAPI

from fastapi import FastAPI, Form, HTTPException
from fastapi.responses import JSONResponse
from hawcx_oauth_client import exchange_code_for_claims
import os

app = FastAPI()

@app.post("/auth/callback")
async def auth_callback(code: str = Form(...)):
    """Handle Hawcx OAuth callback"""
    if not code:
        raise HTTPException(status_code=400, detail="Missing authorization code")
    
    try:
        claims = exchange_code_for_claims(
            code=code,
            oauth_token_url=os.getenv('OAUTH_TOKEN_ENDPOINT'),
            client_id=os.getenv('OAUTH_CLIENT_ID'),
            public_key=os.getenv('OAUTH_PUBLIC_KEY')
        )
        
        return JSONResponse({
            'success': True,
            'userId': claims['sub'],
            'email': claims.get('email')
        })
    except Exception as error:
        raise HTTPException(status_code=401, detail="Authentication failed")

PKCE Support

For enhanced security, especially in native or SPA applications, use PKCE:
import hashlib
import base64
import secrets
from hawcx_oauth_client import exchange_code_for_claims

# On client: Generate code verifier
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode('utf-8').rstrip('=')
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode('utf-8')).digest()
).decode('utf-8').rstrip('=')

# On backend: Exchange with verifier
claims = exchange_code_for_claims(
    code=auth_code,
    code_verifier=code_verifier,  # Include the verifier from client session
    oauth_token_url=os.getenv('OAUTH_TOKEN_ENDPOINT'),
    client_id=os.getenv('OAUTH_CLIENT_ID'),
    public_key=os.getenv('OAUTH_PUBLIC_KEY')
)

Next Steps