Keycard OAuth SDK
A comprehensive Python SDK for OAuth 2.0 functionality implementing multiple OAuth 2.0 standards for enterprise-grade token management.
Requirements
- Python 3.10 or greater
- Virtual environment (recommended)
Setup Guide
Option 1: Using uv (Recommended)
If you have uv installed:
# Create a new project with uv
uv init my-oauth-project
cd my-oauth-project
# Create and activate virtual environment
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
Option 2: Using Standard Python
# Create project directory
mkdir my-oauth-project
cd my-oauth-project
# Create and activate virtual environment
python3 -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Upgrade pip (recommended)
pip install --upgrade pip
Installation
uv add keycardai-oauth
Or with pip:
pip install keycardai-oauth
Quick Start
Synchronous Client
For traditional applications that don't use async/await:
from keycardai.oauth import Client, BasicAuth, TokenType
with Client(
"https://oauth.example.com",
auth=BasicAuth("your_client_id", "your_client_secret")
) as client:
response = client.exchange_token(
subject_token="original_access_token",
subject_token_type=TokenType.ACCESS_TOKEN,
audience="https://api.example.com"
)
print(f"New token: {response.access_token}")
print(f"Expires in: {response.expires_in} seconds")
Asynchronous Client
For async applications (FastAPI, aiohttp, etc.):
import asyncio
from keycardai.oauth import AsyncClient, BasicAuth, TokenType
async def main():
async with AsyncClient(
"https://oauth.example.com",
auth=BasicAuth("your_client_id", "your_client_secret")
) as client:
response = await client.exchange_token(
subject_token="original_access_token",
subject_token_type=TokenType.ACCESS_TOKEN,
audience="https://api.example.com"
)
print(f"New token: {response.access_token}")
asyncio.run(main())
Web-App Authorization Code Flow
For applications that own their registered redirect route, use the stateless
web-app flow and store the returned state and code_verifier in session
state between requests:
from keycardai.oauth.pkce import begin_authorization, complete_authorization
async def login_route(session):
redirect = await begin_authorization(
client_id="my-web-app",
issuer="https://oauth.example.com",
redirect_uri="https://app.example.com/oauth/callback",
scopes=["openid"],
)
session["oauth_flow"] = {
"state": redirect.state,
"code_verifier": redirect.code_verifier,
}
return redirect.url # Redirect the browser to this URL.
async def callback_route(request, session):
flow = session.pop("oauth_flow")
return await complete_authorization(
callback_params=request.query_params,
state=flow["state"],
code_verifier=flow["code_verifier"],
client_id="my-web-app",
redirect_uri="https://app.example.com/oauth/callback",
issuer="https://oauth.example.com",
)
If the application caches authorization server metadata, it can replace
issuer=... with metadata=cached_metadata in both handlers to skip discovery
on each sign-in:
from keycardai.oauth import AuthorizationServerMetadata
cached_metadata = AuthorizationServerMetadata(
issuer="https://oauth.example.com",
authorization_endpoint="https://oauth.example.com/authorize",
token_endpoint="https://oauth.example.com/token",
)
# login_route: issuer="https://oauth.example.com"
# -> metadata=cached_metadata
# callback_route: issuer="https://oauth.example.com"
# -> metadata=cached_metadata
Features
- Token Exchange (RFC 8693) - Exchange tokens for different audiences, scopes, or token types
- Dynamic Client Registration (RFC 7591) - Register OAuth clients programmatically
- Authorization Server Metadata (RFC 8414) - Auto-discover server endpoints and capabilities
- UserInfo (OIDC Core 1.0 Section 5.3) - Fetch the signed-in user's identity claims
- Bearer Token Support (RFC 6750) - Standard bearer token handling and utilities
- PKCE Support (RFC 7636) - Proof Key for Code Exchange for public clients
- Web-App Authorization Code Flow - Stateless begin and complete helpers for applications with their own callback route
- Multiple Auth Strategies - BasicAuth, BearerAuth, and multi-zone authentication
- Comprehensive Error Handling - Structured exceptions with retry guidance
- Sync and Async Clients - Choose the right client for your application
OAuth Standards Supported
The SDK implements the following OAuth 2.0 specifications:
| RFC | Standard | Description |
|---|---|---|
| RFC 8693 | Token Exchange | Exchange tokens for different audiences, scopes, or impersonation |
| RFC 7591 | Dynamic Client Registration | Register clients programmatically with authorization servers |
| RFC 8414 | Authorization Server Metadata | Discover server endpoints and capabilities automatically |
| RFC 6750 | Bearer Token Usage | Standard format for OAuth 2.0 access tokens |
| RFC 7636 | PKCE | Security extension for public clients |
| RFC 7662 | Token Introspection | Validate and inspect token metadata |
| RFC 7009 | Token Revocation | Invalidate access and refresh tokens |
| RFC 9126 | Pushed Authorization Requests | Enhanced authorization request security |
| OIDC Core 1.0 Section 5.3 | UserInfo | Fetch identity claims for the subject of an access token |
Configuration
Client Initialization
Both Client and AsyncClient accept the same initialization parameters:
from keycardai.oauth import Client, AsyncClient, BasicAuth, Endpoints, ClientConfig
# Minimal initialization
client = Client("https://oauth.example.com")
# Full initialization with all options
client = Client(
base_url="https://oauth.example.com",
auth=BasicAuth("client_id", "client_secret"),
endpoints=Endpoints(
token="/oauth2/token",
register="/oauth2/register"
),
config=ClientConfig(
timeout=60.0,
max_retries=5
)
)
ClientConfig Options
Configure client behavior with ClientConfig:
| Parameter | Type | Default | Description |
|---|---|---|---|
timeout |
float |
30.0 |
HTTP request timeout in seconds |
max_retries |
int |
3 |
Maximum retry attempts for failed requests |
verify_ssl |
bool |
True |
Verify SSL/TLS certificates |
user_agent |
str |
"Keycard-OAuth/0.0.1" |
HTTP User-Agent header |
custom_headers |
dict[str, str] | None |
None |
Additional HTTP headers for all requests |
enable_metadata_discovery |
bool |
True |
Auto-discover server endpoints via RFC 8414 |
auto_register_client |
bool |
False |
Automatically register client on context entry |
client_id |
str | None |
None |
Pre-existing client ID (skip registration) |
client_name |
str |
"Keycard OAuth Client" |
Client name for dynamic registration |
client_redirect_uris |
list[str] |
["http://localhost:8080/callback"] |
Redirect URIs for registration |
client_grant_types |
list[GrantType] |
[AUTHORIZATION_CODE, REFRESH_TOKEN, TOKEN_EXCHANGE] |
Grant types for registration |
client_token_endpoint_auth_method |
TokenEndpointAuthMethod |
NONE |
Token endpoint auth method |
client_jwks_url |
str | None |
None |
JWKS URL for private_key_jwt auth |
Example with custom configuration:
from keycardai.oauth import Client, ClientConfig, GrantType
config = ClientConfig(
timeout=60.0,
max_retries=5,
enable_metadata_discovery=True,
auto_register_client=True,
client_name="My Application",
client_grant_types=[GrantType.TOKEN_EXCHANGE, GrantType.CLIENT_CREDENTIALS]
)
with Client("https://oauth.example.com", config=config) as client:
# Client automatically discovers endpoints and registers if needed
response = client.exchange_token(...)
Endpoints Configuration
Override discovered or default endpoints with Endpoints:
| Endpoint | RFC | Description |
|---|---|---|
token |
RFC 6749 | Token endpoint for exchanges and grants |
introspect |
RFC 7662 | Token introspection endpoint |
revoke |
RFC 7009 | Token revocation endpoint |
register |
RFC 7591 | Dynamic client registration endpoint |
par |
RFC 9126 | Pushed authorization request endpoint |
authorize |
RFC 6749 | Authorization endpoint |
from keycardai.oauth import Client, Endpoints
endpoints = Endpoints(
token="/custom/token",
register="/custom/register"
)
with Client("https://oauth.example.com", endpoints=endpoints) as client:
# Uses custom endpoints instead of discovered ones
pass
Configuration Precedence
Endpoint resolution follows this priority (highest to lowest):
- Explicit
Endpointsoverrides - Always used if provided - Discovered server metadata - From RFC 8414 discovery (if
enable_metadata_discovery=True) - Default endpoints - Standard OAuth 2.0 paths (e.g.,
/oauth2/token)
Authentication Strategies
The SDK provides four authentication strategies for different use cases.
NoneAuth
No authentication. Use for public endpoints or dynamic client registration:
from keycardai.oauth import Client, NoneAuth
# For server metadata discovery (no auth required)
with Client("https://oauth.example.com", auth=NoneAuth()) as client:
metadata = client.discover_server_metadata()
print(f"Token endpoint: {metadata.token_endpoint}")
BasicAuth (RFC 7617)
HTTP Basic authentication using client credentials:
from keycardai.oauth import Client, BasicAuth
auth = BasicAuth(
client_id="your_client_id",
client_secret="your_client_secret"
)
with Client("https://oauth.example.com", auth=auth) as client:
response = client.exchange_token(
subject_token="user_token",
subject_token_type=TokenType.ACCESS_TOKEN,
audience="https://api.example.com"
)
BearerAuth (RFC 6750)
Bearer token authentication for API access:
from keycardai.oauth import Client, BearerAuth
# Use an existing access token for authentication
auth = BearerAuth(access_token="your_access_token")
with Client("https://oauth.example.com", auth=auth) as client:
response = client.exchange_token(
subject_token="another_token",
subject_token_type=TokenType.ACCESS_TOKEN,
resource="https://api.example.com"
)
MultiZoneBasicAuth
For multi-zone deployments with different credentials per zone, keyed by each zone's issuer URL:
from keycardai.oauth import Client, MultiZoneBasicAuth
# Configure credentials per zone issuer
auth = MultiZoneBasicAuth({
"https://prod.keycard.cloud": ("prod_client_id", "prod_client_secret"),
"https://staging.keycard.cloud": ("staging_client_id", "staging_client_secret"),
})
# Check configured issuers
print(auth.get_configured_issuers())
# Check if an issuer is configured
if auth.has_issuer("https://prod.keycard.cloud"):
# Get headers for a specific issuer
headers = auth.apply_headers("https://prod.keycard.cloud")
# Or get the BasicAuth instance for an issuer
prod_auth = auth.get_auth_for_issuer("https://prod.keycard.cloud")
# Token operations select credentials per call with issuer=...
with Client("https://prod.keycard.cloud", auth=auth) as client:
response = client.exchange_token(
subject_token="token",
issuer="https://prod.keycard.cloud",
)
Operations
Token Exchange (RFC 8693)
Exchange tokens for different audiences, scopes, or perform delegation/impersonation:
from keycardai.oauth import Client, BasicAuth, TokenType, TokenExchangeRequest
with Client("https://oauth.example.com", auth=BasicAuth(...)) as client:
# Simple delegation - exchange for a different audience
response = client.exchange_token(
subject_token="user_access_token",
subject_token_type=TokenType.ACCESS_TOKEN,
audience="https://api.example.com"
)
print(f"Delegated token: {response.access_token}")
# Exchange with scope restriction
response = client.exchange_token(
subject_token="user_access_token",
subject_token_type=TokenType.ACCESS_TOKEN,
audience="https://api.example.com",
scope="read:users"
)
# Advanced: Impersonation with actor token
request = TokenExchangeRequest(
subject_token="user_token",
subject_token_type=TokenType.ACCESS_TOKEN,
actor_token="service_account_token",
actor_token_type=TokenType.ACCESS_TOKEN,
audience="https://backend-api.example.com"
)
response = client.exchange_token(request)
Dynamic Client Registration (RFC 7591)
Register OAuth clients programmatically:
from keycardai.oauth import Client, ClientRegistrationRequest, GrantType, TokenEndpointAuthMethod
with Client("https://oauth.example.com") as client:
# Simple registration with defaults
response = client.register_client(client_name="My Application")
print(f"Client ID: {response.client_id}")
print(f"Client Secret: {response.client_secret}")
# Full control over registration
request = ClientRegistrationRequest(
client_name="Production Web App",
redirect_uris=[
"https://app.example.com/callback",
"https://app.example.com/silent-refresh"
],
grant_types=[
GrantType.AUTHORIZATION_CODE,
GrantType.REFRESH_TOKEN,
GrantType.TOKEN_EXCHANGE
],
token_endpoint_auth_method=TokenEndpointAuthMethod.CLIENT_SECRET_BASIC,
scope="openid profile email"
)
response = client.register_client(request)
Server Metadata Discovery (RFC 8414)
Discover authorization server capabilities:
from keycardai.oauth import Client
with Client("https://oauth.example.com") as client:
metadata = client.discover_server_metadata()
print(f"Issuer: {metadata.issuer}")
print(f"Token endpoint: {metadata.token_endpoint}")
print(f"Registration endpoint: {metadata.registration_endpoint}")
print(f"Supported grants: {metadata.grant_types_supported}")
print(f"Supported scopes: {metadata.scopes_supported}")
print(f"PKCE methods: {metadata.code_challenge_methods_supported}")
print(f"UserInfo endpoint: {metadata.userinfo_endpoint}")
print(f"End session endpoint: {metadata.end_session_endpoint}")
UserInfo (OIDC Core 1.0 Section 5.3)
Fetch the identity claims for the user an access token was issued to. The
endpoint comes from discovery (userinfo_endpoint), and the access token is
sent as a Bearer credential instead of the client's own credentials:
from keycardai.oauth import Client
with Client("https://oauth.example.com") as client:
user = client.userinfo(access_token)
print(f"Subject: {user.sub}")
print(f"Email: {user.claims.get('email')}")
# Every claim the provider returned is preserved in user.claims
If the server's metadata has no userinfo_endpoint, userinfo() raises
ConfigError without making a request. An expired or revoked token raises
InvalidTokenError.
Error Handling
The SDK provides a structured exception hierarchy with retry guidance.
Exception Hierarchy
OAuthError (base)
├── OAuthHttpError # HTTP 4xx/5xx responses
├── OAuthProtocolError # RFC 6749 OAuth error responses
│ └── TokenExchangeError # RFC 8693 specific errors
├── NetworkError # Connection/transport failures
├── ConfigError # Client misconfiguration
└── AuthenticationError # Authentication failures
Retriable vs Non-Retriable Errors
| Exception | Retriable | Condition |
|---|---|---|
OAuthHttpError |
Yes | HTTP 429 (rate limit) or 5xx (server error) |
OAuthHttpError |
No | HTTP 4xx (client error, except 429) |
OAuthProtocolError |
No | OAuth protocol violations |
TokenExchangeError |
No | Token exchange failures |
NetworkError |
Yes | Connection timeouts, DNS failures |
ConfigError |
No | Invalid configuration (requires code fix) |
AuthenticationError |
No | Invalid credentials |
Error Handling Patterns
from keycardai.oauth import (
Client,
BasicAuth,
OAuthError,
OAuthHttpError,
OAuthProtocolError,
NetworkError,
ConfigError,
AuthenticationError,
)
with Client("https://oauth.example.com", auth=BasicAuth(...)) as client:
try:
response = client.exchange_token(
subject_token="token",
subject_token_type=TokenType.ACCESS_TOKEN,
audience="https://api.example.com"
)
except OAuthHttpError as e:
if e.retriable:
# HTTP 429 or 5xx - implement backoff and retry
print(f"Retriable HTTP error (status {e.status_code}): {e}")
else:
# HTTP 4xx - fix the request
print(f"Client error: {e.response_body}")
except OAuthProtocolError as e:
# OAuth error response from server
print(f"OAuth error: {e.error}")
print(f"Description: {e.error_description}")
if e.error_uri:
print(f"More info: {e.error_uri}")
except NetworkError as e:
# Connection issues - usually retriable
print(f"Network error (retriable: {e.retriable}): {e.cause}")
except ConfigError as e:
# Configuration issue - fix code
print(f"Configuration error: {e}")
except AuthenticationError as e:
# Credentials invalid
print(f"Authentication failed: {e}")
Implementing Retry Logic
import time
from keycardai.oauth import Client, BasicAuth, OAuthHttpError, NetworkError
def exchange_with_retry(client, max_attempts=3, base_delay=1.0):
"""Exchange token with exponential backoff for retriable errors."""
for attempt in range(max_attempts):
try:
return client.exchange_token(
subject_token="token",
subject_token_type=TokenType.ACCESS_TOKEN,
audience="https://api.example.com"
)
except (OAuthHttpError, NetworkError) as e:
if not e.retriable or attempt == max_attempts - 1:
raise
delay = base_delay * (2 ** attempt)
print(f"Attempt {attempt + 1} failed, retrying in {delay}s...")
time.sleep(delay)
Utility Functions
Bearer Token Utilities
Extract and validate bearer tokens from HTTP headers:
from keycardai.oauth import extract_bearer_token, validate_bearer_format
# Extract token from Authorization header
header = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
token = extract_bearer_token(header)
print(token) # "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
# Validate token format
is_valid = validate_bearer_format(token)
print(f"Token format valid: {is_valid}")
Examples
Working examples are available in the examples/ directory:
- discover_server_metadata - RFC 8414 server metadata discovery
- dynamic_client_registration - RFC 7591 client registration
Run examples:
cd examples/discover_server_metadata
ZONE_URL="https://your-zone.keycard.cloud" uv run python main.py
API Reference
Note: Auto-generated API documentation is planned for a future release. For now, refer to the inline docstrings in the source code and the examples in this README. The SDK includes comprehensive docstrings with RFC references.
Development
This package is part of the Keycard Python SDK workspace.
To develop:
# From workspace root
uv sync
uv run --package keycardai-oauth pytest
Run tests with coverage:
uv run --package keycardai-oauth pytest --cov=keycardai.oauth --cov-report=term-missing
License
MIT License - see LICENSE file for details.
Support
- Documentation: Keycard Docs
- Issues: GitHub Issues
- Email: support@keycard.ai
Release files for keycardai-oauth 0.23.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| keycardai_oauth-0.23.0.tar.gz | 213.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| keycardai_oauth-0.23.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 305.6 kB
Release files / keycardai_oauth-0.23.0.tar.gz
| Download URL | keycardai_oauth-0.23.0.tar.gz |
|---|---|
| Size | 213.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
79ad3cb57c05276f5138388eeb095a52f97d1495bba412d0a9b283a50025a56e
|
|
BLAKE2b-256 checksum How to use checksums |
403a5fa5f4bddf3fbbfe326d07d67b608d8096e15a06c8e274ae276b6ab058a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / keycardai_oauth-0.23.0-py3-none-any.whl
| Download URL | keycardai_oauth-0.23.0-py3-none-any.whl |
|---|---|
| Size | 92.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8c655348cfd016db9bc1e44141d4e2a8c3dc1ce0832255fadd58536d757cc482
|
|
BLAKE2b-256 checksum How to use checksums |
9c65c507739d1fa68c8a57397c0b7dc4f487ee90030cdc61c179e2351214412f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|