ActingWeb Authentication System
This document describes the comprehensive OAuth2 authentication system in ActingWeb, including support for multiple providers like Google and GitHub.
Overview
ActingWeb’s authentication system provides a unified, provider-agnostic OAuth2 implementation that supports multiple authentication providers. The system uses the oauthlib library as the standard OAuth2 implementation and consolidates all OAuth2 functionality into core ActingWeb modules.
Key Features:
Multi-Provider Support: Google OAuth2, GitHub OAuth2, and extensible to other providers
Multiple Authentication Methods: Web sessions (cookies), Bearer tokens (API), and Basic auth (legacy)
Framework Agnostic: Consistent behavior across FastAPI and Flask integrations
MCP Integration: Full OAuth2 support for Model Context Protocol endpoints
Security: CSRF protection, secure token storage, and privacy-respecting email handling
Architecture
Core Components
The authentication system is built around several key components:
- OAuth2Provider Base Class (
actingweb/oauth2.py) Base class for all OAuth2 provider implementations. Handles common OAuth2 configuration and validation.
- OAuth2Authenticator Class (
actingweb/oauth2.py) Main authenticator class that handles the complete OAuth2 flow using oauthlib. Supports:
Authorization URL generation
Authorization code exchange for tokens
Token validation and refresh
User information retrieval
Actor lookup/creation based on OAuth2 identity
- Provider-Specific Classes
GoogleOAuth2Provider: Google-specific OAuth2 configurationGitHubOAuth2Provider: GitHub-specific OAuth2 configuration and special handling
Authentication Methods
Web Session Authentication
For interactive web users, ActingWeb uses session cookies:
User visits a protected endpoint (e.g.,
/,/<actor_id>/www,/mcp)If not authenticated, redirected to OAuth2 provider
After successful authentication, a secure session cookie is set
Subsequent requests use the cookie for authentication
- Cookie Configuration:
Name:
oauth_tokenMax Age: 1 hour (3600 seconds) - matches ActingWeb token TTL
Secure: HTTPS only
HttpOnly: Yes (protected from JavaScript access)
SameSite: Lax
Path:
/(site-wide)
Note
As of ActingWeb 3.x, session cookies contain ActingWeb-generated tokens, not OAuth provider tokens. The cookie TTL matches the token TTL (1 hour). For longer sessions, use the refresh token mechanism (see SPA Authentication Guide).
Bearer Token Authentication
For API clients and actor-to-actor communication:
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
https://yourdomain.com/mcp
- Supported Endpoints:
/mcp- Model Context ProtocolActor-specific API endpoints
Any endpoint requiring authentication
Basic Authentication (Legacy)
Legacy Basic authentication is maintained for backward compatibility:
curl -u username:password https://yourdomain.com/endpoint
Provider Implementations
Google OAuth2 Provider
Configuration (fluent API):
app = ActingWebApp(...).with_oauth(
provider="google",
client_id="your_google_client_id",
client_secret="your_google_client_secret",
scope="openid email profile",
)
- Endpoints:
Auth URI:
https://accounts.google.com/o/oauth2/v2/authToken URI:
https://oauth2.googleapis.com/tokenUserInfo URI:
https://www.googleapis.com/oauth2/v2/userinfoScope:
openid email profile
- Features:
Refresh token support
Standard OpenID Connect flow
Public email addresses
GitHub OAuth2 Provider
Configuration (fluent API):
app = ActingWebApp(...).with_oauth(
provider="github",
client_id="your_github_client_id",
client_secret="your_github_client_secret",
scope="read:user user:email",
)
- Endpoints:
Auth URI:
https://github.com/login/oauth/authorizeToken URI:
https://github.com/login/oauth/access_tokenUserInfo URI:
https://api.github.com/userScope:
user:email
- GitHub-Specific Features:
User-Agent Header: Required for all GitHub API requests
JSON Accept Header: GitHub OAuth2 endpoints require
Accept: application/jsonNo Refresh Tokens: GitHub doesn’t support OAuth2 refresh tokens
Private Email Handling: Special logic for users with private email addresses
Verified Email Required: Only verified primary emails are accepted for actor linking. Unverified primary emails are skipped to prevent account-linking attacks.
- Email Handling:
GitHub users may have private email addresses. The system provides multiple strategies:
Using public email if available (immediate login)
Fetching verified emails via GitHub’s emails API (dropdown selection)
Manual email input with verification link (see Email Verification below)
Provider ID mode: using stable GitHub user ID as identifier (no email required)
Note
Only verified emails from the GitHub
/user/emailsAPI are accepted for actor creation and linking. If the user’s primary email is not verified, the first verified email is used instead. If no verified emails are found, the email-based flow fails gracefully.
GitHub App Setup
Create a GitHub OAuth App at https://github.com/settings/applications/new
Set Authorization callback URL to:
https://yourdomain.com/oauth/callbackCopy the Client ID and Client Secret to your configuration
Ensure your app requests the
user:emailscope
Factory Functions
The system provides several factory functions for creating authenticators:
Provider-Specific Factories:
from actingweb.oauth2 import create_google_authenticator, create_github_authenticator
# Create Google OAuth2 authenticator
google_auth = create_google_authenticator(config)
# Create GitHub OAuth2 authenticator
github_auth = create_github_authenticator(config)
Generic Factory with Auto-Detection:
from actingweb.oauth2 import create_oauth2_authenticator
# Auto-detect provider from config.oauth2_provider
auth = create_oauth2_authenticator(config)
# Explicitly specify provider
github_auth = create_oauth2_authenticator(config, provider_name="github")
When config.oauth_providers is populated (via the multi-provider with_oauth() API), the factory functions automatically extract per-provider credentials from that dict.
Custom Provider:
from actingweb.oauth2 import create_generic_authenticator
custom_config = {
"client_id": "custom_client_id",
"client_secret": "custom_secret",
"auth_uri": "https://example.com/oauth/authorize",
"token_uri": "https://example.com/oauth/token",
"userinfo_uri": "https://example.com/userinfo",
"scope": "read write",
"redirect_uri": "https://yourdomain.com/oauth/callback"
}
custom_auth = create_generic_authenticator(config, custom_config)
OAuth2 Flow
Token Exchange
ActingWeb exchanges the code for an access token:
GitHub Example:
POST https://github.com/login/oauth/access_token
Content-Type: application/x-www-form-urlencoded
Accept: application/json
User-Agent: ActingWeb-OAuth2-Client
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
code=AUTH_CODE&
redirect_uri=https://yourdomain.com/oauth/callback
User Info Retrieval
ActingWeb fetches user information:
GitHub Example:
GET https://api.github.com/user
Authorization: Bearer ACCESS_TOKEN
Accept: application/json
User-Agent: ActingWeb-OAuth2-Client
Actor Lookup/Creation
ActingWeb looks up or creates an actor based on the user’s email address or unique identifier.
ActingWeb Token Generation
After successful OAuth validation, ActingWeb generates its own session tokens rather than using the OAuth provider tokens directly. This provides several benefits:
Performance: Token validation is a fast database lookup, no network calls to OAuth provider
Reliability: No dependency on OAuth provider availability after initial authentication
Security: OAuth provider tokens never exposed to frontend JavaScript
Control: Custom token expiry (1 hour access, 2 weeks refresh) and rotation policies
The OAuth provider token is validated once during the callback, then ActingWeb generates and stores its own token in the session manager. All subsequent authentication uses ActingWeb tokens exclusively.
OAuth Provider Token → Validate once → Generate ActingWeb Token → Store in Session Manager
↓
Return to client (SPA: JSON, /www: Cookie)
OAuth Login Flow with Postponed Actor Creation
ActingWeb supports an OAuth login flow where actor creation is postponed until after email is obtained from the OAuth provider. This enables applications to implement “Login with Google” or “Login with GitHub” buttons on the factory page.
Key Features:
Deferred Actor Creation: Actors are created only after successful OAuth authentication and email retrieval
Email Fallback: If the OAuth provider doesn’t provide an email (e.g., GitHub private email), users are redirected to an email input form
Trust Type Detection: Distinguishes between web UI login flows and MCP authorization flows
Implementation:
The library exposes OAuth authorization URLs through the factory handler’s template_values:
# Factory handler GET / provides:
{
'oauth_urls': {
'google': 'https://accounts.google.com/o/oauth2/v2/auth?...',
'github': 'https://github.com/login/oauth/authorize?...'
},
'oauth_providers': [
{
'name': 'google',
'display_name': 'Google',
'url': 'https://...'
}
],
'oauth_enabled': True
}
Applications render “Login with Google/GitHub” buttons using these URLs:
{% if oauth_enabled %}
{% for provider in oauth_providers %}
<a href="{{ provider.url }}">
Login with {{ provider.display_name }}
</a>
{% endfor %}
{% endif %}
Flow Diagram:
User clicks “Login with Google” → OAuth2 redirect
Google returns to
/oauth/callbackwith authorization codeLibrary exchanges code for access token and retrieves user info
If email is available: Create actor and redirect to
/{actor_id}/wwwIf email is missing: Redirect to
/oauth/emailfor manual inputAfter email input: Create actor and complete login
Email Fallback:
When OAuth providers don’t provide email addresses (e.g., GitHub with private email), the library:
Stores OAuth tokens temporarily in a session (10-minute TTL)
Redirects to
/oauth/emailwith a session tokenPresents email input form to the user
Completes actor creation after email is provided
Applications should provide an aw-oauth-email.html template for email input. If not provided, a basic fallback form is used.
MCP Authorization Protection:
The email fallback flow is disabled for MCP authorization requests (when trust_type parameter is present in OAuth state). MCP clients are programmatic and cannot interact with web forms, so these flows return an error if email cannot be extracted.
MCP Integration
The authentication system integrates seamlessly with ActingWeb’s Model Context Protocol (MCP) implementation:
Bearer Token Authentication:
curl -H "Authorization: Bearer YOUR_GITHUB_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"method": "tools/list", "id": 1}' \
https://yourdomain.com/mcp
- Session Cookie Authentication:
Users authenticated via web session can access MCP endpoints directly.
- 401 Responses:
Unauthenticated requests receive proper
WWW-Authenticateheaders with OAuth2 authorization URLs.
Custom Route Authentication
For custom application routes that don’t go through the standard ActingWeb handler system, use the check_and_verify_auth() function to provide proper authentication verification.
The check_and_verify_auth() Function
Location: actingweb.auth.check_and_verify_auth()
This function performs authentication checks and is designed for use in custom application routes. It supports:
Bearer Token Authentication: OAuth2 tokens, ActingWeb trust secret tokens
Basic Authentication: Username/password authentication
OAuth2 Cookie Sessions: Web UI session authentication
OAuth2 Redirects: Proper redirect handling for unauthenticated users
Function Signature
def check_and_verify_auth(appreq=None, actor_id=None, config=None):
"""Check and verify authentication for non-ActingWeb routes.
Args:
appreq: Request object (same format as used by ActingWeb handlers)
actor_id: Actor ID to verify authentication against
config: ActingWeb config object
Returns:
dict with:
- 'authenticated': bool - True if authentication successful
- 'actor': Actor object if authentication successful, None otherwise
- 'auth': Auth object with authentication details
- 'response': dict with response details {'code': int, 'text': str, 'headers': dict}
- 'redirect': str - redirect URL if authentication requires redirect
"""
Usage Example
Here’s how to use check_and_verify_auth() in a FastAPI custom route:
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import RedirectResponse
from actingweb import auth
from actingweb.aw_web_request import AWWebObj
@fastapi_app.get("/{actor_id}/dashboard/memory")
async def dashboard_memory(actor_id: str, request: Request):
"""Custom dashboard route with proper authentication."""
# Convert FastAPI request to ActingWeb format
req_data = await normalize_fastapi_request(request)
webobj = AWWebObj(
url=req_data["url"],
params=req_data["values"],
body=req_data["data"],
headers=req_data["headers"],
cookies=req_data["cookies"],
)
# Use ActingWeb's proper authentication system
auth_result = auth.check_and_verify_auth(
appreq=webobj,
actor_id=actor_id,
config=app.get_config()
)
if not auth_result['authenticated']:
# Handle different authentication failure scenarios
response_code = auth_result['response']['code']
if response_code == 404:
raise HTTPException(status_code=404, detail="Actor not found")
elif response_code == 302 and auth_result['redirect']:
# OAuth redirect required
raise HTTPException(
status_code=302,
detail="OAuth redirect required",
headers={"Location": auth_result['redirect']}
)
elif response_code == 401:
# Authentication required
headers = auth_result['response']['headers']
raise HTTPException(
status_code=401,
detail="Authentication required",
headers=headers
)
else:
# Other authentication failures
raise HTTPException(
status_code=response_code,
detail=auth_result['response']['text']
)
# Authentication successful - use the actor
actor_interface = ActorInterface(auth_result['actor'])
# Your custom route logic here...
return {"message": f"Dashboard for actor {actor_interface.id}"}
Request Normalization Helper
For FastAPI applications, you’ll need a helper function to convert FastAPI requests to ActingWeb format:
async def normalize_fastapi_request(request: Request) -> dict:
"""Convert FastAPI request to ActingWeb format."""
# Read body asynchronously
body = await request.body()
# Parse cookies
cookies = {}
raw_cookies = request.headers.get("cookie")
if raw_cookies:
for cookie in raw_cookies.split("; "):
if "=" in cookie:
name, value = cookie.split("=", 1)
cookies[name] = value
# Convert headers (preserve case-sensitive header names)
headers = {}
for k, v in request.headers.items():
if k.lower() == "authorization":
headers["Authorization"] = v
elif k.lower() == "content-type":
headers["Content-Type"] = v
else:
headers[k] = v
# If no Authorization header but we have an oauth_token cookie,
# provide it as a Bearer token for web UI requests
if "Authorization" not in headers and "oauth_token" in cookies:
headers["Authorization"] = f"Bearer {cookies['oauth_token']}"
# Get query parameters and form data
params = {}
for k, v in request.query_params.items():
params[k] = v
return {
"method": request.method,
"path": str(request.url.path),
"data": body,
"headers": headers,
"cookies": cookies,
"values": params,
"url": str(request.url),
}
Authentication Flow
The check_and_verify_auth() function follows this authentication flow:
Bearer Token Check: Validates Authorization header for Bearer tokens (OAuth2 or ActingWeb trust tokens)
Basic Authentication: For API clients using username/password
OAuth2 Cookie: For web UI sessions with oauth_token cookie
OAuth2 Redirect: If all methods fail and OAuth2 is configured, creates redirect to OAuth2 provider
Response Codes
The function returns different response codes based on authentication results:
200: Authentication successful
401: Authentication required (with proper WWW-Authenticate headers)
302: OAuth2 redirect required (with Location header)
403: Forbidden (authentication failed)
404: Actor not found
Security Considerations
When implementing custom routes with authentication:
Always validate actor_id: Ensure users can only access their own actor data
Use HTTPS: OAuth2 tokens and cookies should only be transmitted over secure connections
Handle errors gracefully: Don’t expose sensitive information in error messages
Log authentication attempts: For security monitoring and debugging
Framework Integration
The check_and_verify_auth() function works with any Python web framework:
Flask Example:
from flask import Flask, request
from actingweb import auth
from actingweb.aw_web_request import AWWebObj
@app.route("/<actor_id>/dashboard/memory")
def dashboard_memory(actor_id):
webobj = AWWebObj(
url=request.url,
params=request.values,
body=request.get_data(),
headers=dict(request.headers),
cookies=request.cookies,
)
auth_result = auth.check_and_verify_auth(
appreq=webobj,
actor_id=actor_id,
config=app.get_config()
)
if not auth_result['authenticated']:
# Handle authentication failure
return handle_auth_failure(auth_result)
# Use authenticated actor
actor = ActorInterface(auth_result['actor'])
return render_dashboard(actor)
Django Example:
from django.http import JsonResponse, HttpResponseRedirect
from actingweb import auth
from actingweb.aw_web_request import AWWebObj
def dashboard_memory(request, actor_id):
webobj = AWWebObj(
url=request.build_absolute_uri(),
params=request.GET.dict(),
body=request.body,
headers=dict(request.META),
cookies=request.COOKIES,
)
auth_result = auth.check_and_verify_auth(
appreq=webobj,
actor_id=actor_id,
config=get_actingweb_config()
)
if not auth_result['authenticated']:
if auth_result['response']['code'] == 302:
return HttpResponseRedirect(auth_result['redirect'])
else:
return JsonResponse(
{"error": auth_result['response']['text']},
status=auth_result['response']['code']
)
# Use authenticated actor
actor = ActorInterface(auth_result['actor'])
return render_dashboard(request, actor)
Async Authentication for FastAPI
For async FastAPI endpoints, use the check_and_verify_auth_async() function to avoid blocking
the event loop during OAuth2 token validation. This is particularly important when your endpoint
might receive OAuth2 tokens that need validation against the provider.
Why Use Async Authentication?
OAuth2 token validation requires network calls to the provider (e.g., Google, GitHub)
Sync calls block the event loop, reducing throughput in async applications
The async version uses
httpxfor non-blocking HTTP requests
Function Signature:
async def check_and_verify_auth_async(appreq=None, actor_id=None, config=None):
"""Async version: Check and verify authentication for non-ActingWeb routes.
Args:
appreq: Request object (same format as used by ActingWeb handlers)
actor_id: Actor ID to verify authentication against
config: ActingWeb config object
Returns:
dict with:
- 'authenticated': bool - True if authentication successful
- 'actor': Actor object if authentication successful, None otherwise
- 'auth': Auth object with authentication details
- 'response': dict with response details {'code': int, 'text': str, 'headers': dict}
- 'redirect': str - redirect URL if authentication requires redirect
"""
FastAPI Async Example:
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import RedirectResponse, JSONResponse
from actingweb import auth
from actingweb.aw_web_request import AWWebObj
@fastapi_app.get("/{actor_id}/dashboard/memory")
async def dashboard_memory(actor_id: str, request: Request):
"""Custom dashboard route with async authentication."""
# Convert FastAPI request to ActingWeb format
req_data = await normalize_fastapi_request(request)
webobj = AWWebObj(
url=req_data["url"],
params=req_data["values"],
body=req_data["data"],
headers=req_data["headers"],
cookies=req_data["cookies"],
)
# Use async authentication to avoid blocking
auth_result = await auth.check_and_verify_auth_async(
appreq=webobj,
actor_id=actor_id,
config=app.get_config()
)
if not auth_result['authenticated']:
response_code = auth_result['response']['code']
if response_code == 404:
raise HTTPException(status_code=404, detail="Actor not found")
elif response_code == 302 and auth_result['redirect']:
return RedirectResponse(auth_result['redirect'])
else:
return JSONResponse(
status_code=response_code,
content={"error": auth_result['response']['text']}
)
# Authentication successful
actor = ActorInterface(auth_result['actor'])
return await render_dashboard(actor)
When to Use Async vs Sync:
Use
check_and_verify_auth_async()in async FastAPI routes that may receive OAuth2 tokensUse
check_and_verify_auth()(sync) in Flask routes or when you need synchronous behaviorTrust-based authentication (using ActingWeb trust secrets) doesn’t require network calls and works efficiently with either version
Implementation Files
Core Files
- actingweb/oauth2.py
Comprehensive OAuth2 module containing:
Base
OAuth2ProviderclassGoogleOAuth2ProviderandGitHubOAuth2ProviderimplementationsOAuth2Authenticatorclass with full OAuth2 flow handlingOAuth2Authenticator.validate_token_and_get_user_info_async()- Async token validation using httpxFactory functions for creating authenticators
Utility functions for token and state handling
- actingweb/auth.py
Updated authentication module that integrates the new OAuth2 system with legacy authentication methods. Contains:
check_and_verify_auth()- Sync authentication helper for custom routescheck_and_verify_auth_async()- Async authentication helper for FastAPI routesAuth.check_token_auth_async()- Async bearer token validationAuth._looks_like_oauth2_token()- Quick heuristic to avoid unnecessary OAuth2 network calls
- actingweb/handlers/oauth2_callback.py
Unified OAuth2 callback handler that processes callbacks from any OAuth2 provider.
Handler Updates
- actingweb/handlers/mcp.py
Updated to use provider-agnostic OAuth2 authentication with proper
WWW-Authenticateheaders.
Integration Updates
- actingweb/interface/integrations/fastapi_integration.py
Updated FastAPI integration with: - OAuth2 authentication checks for protected endpoints - Session cookie validation - Bearer token validation - Consistent OAuth2 redirect handling
- actingweb/interface/integrations/flask_integration.py
Updated Flask integration with identical OAuth2 behavior to FastAPI.
Removed Legacy Files
actingweb/google_oauth.py- Replaced by consolidatedoauth2.pyactingweb/handlers/google_oauth_callback.py- Replaced byoauth2_callback.py
Security Considerations
CSRF Protection
State parameter used for CSRF protection in OAuth2 flow
State can encode redirect URL for post-authentication routing
State validation prevents replay attacks
Token Security
Access tokens stored securely in actor properties
Session cookies are
httpOnlyandsecure(HTTPS only)No sensitive information logged in debug output
Token validation against provider APIs
Email Privacy
Respects provider-specific email privacy settings
Uses username-based fallback for private emails (GitHub)
Optional enhanced email retrieval via provider APIs
Unique identifier generation for users without public emails
Error Handling
Common Provider-Specific Errors
- GitHub:
403 Forbidden: Check User-Agent header is set
422 Unprocessable Entity: Check Accept header is set to application/json
Email Not Found: User has private email - using username fallback
- Google:
Invalid Grant: Authorization code expired or already used
Invalid Client: Check client_id and client_secret configuration
Scope Error: Requested scopes not available or not consented
Fallback Behavior
If provider email is private/unavailable, uses provider-specific unique identifiers
If refresh token is requested but not supported, logs warning and continues
If API calls fail, gracefully degrades to using available user information
Authentication errors result in proper HTTP status codes and redirect to re-authentication
Testing
The authentication system is designed to be testable:
- Unit Testing:
Each provider class can be instantiated with mock configurations for isolated testing.
- Integration Testing:
OAuth2 flows can be tested against real provider endpoints or mock servers.
- Provider Switching:
Easy configuration changes allow testing different providers in the same application.
- Mock Authentication:
Development and testing environments can use mock tokens and user information.
Backward Compatibility
The implementation maintains full backward compatibility:
Existing Google OAuth2 configurations continue to work unchanged
Legacy Basic authentication still supported for older integrations
API contracts unchanged - only internal implementation updated
Existing actor data and properties remain intact
No breaking changes to ActingWeb public APIs
Migration from Legacy
From Legacy Google OAuth2:
Update imports from
google_oauthtooauth2Replace
create_google_authenticator()calls withcreate_oauth2_authenticator()Update configuration if using custom OAuth2 settings
Test authentication flows to ensure proper functionality
Configuration Changes:
Old configuration:
# Legacy Google-specific config
config.oauth = {
"client_id": "google_client_id",
"client_secret": "google_client_secret"
}
New configuration (fluent API):
# Single provider (backward compatible)
app = ActingWebApp(...).with_oauth(
provider="google",
client_id="google_client_id",
client_secret="google_client_secret",
)
# Multiple providers simultaneously
app = ActingWebApp(...).with_oauth(
provider="google",
client_id="google_client_id",
client_secret="google_client_secret",
).with_oauth(
provider="github",
client_id="github_client_id",
client_secret="github_client_secret",
)
Email Verification System
ActingWeb includes a comprehensive email verification system to prevent account hijacking when OAuth providers cannot verify email ownership.
Security Problem
When OAuth2 providers don’t return a verified email address (e.g., GitHub users with private emails), the system needs to ensure the email entered by the user actually belongs to them. Without verification, an attacker could:
Authenticate with GitHub (private email)
Enter a victim’s email address in the form
Gain access to the victim’s actor
The email verification system prevents this by requiring proof of email ownership.
How It Works
- Scenario 1: Provider Has Verified Emails
OAuth provider returns verified emails → User selects from dropdown → Actor created (email_verified=true)
- Scenario 2: No Verified Emails Available
No verified emails → User enters email → Verification email sent → User clicks link → Email marked verified
- Scenario 3: MCP Flows (Cannot use web forms)
No email from provider → Return error 502 → User must configure OAuth provider to make email public
Configuration
Email verification behavior is controlled by the force_email_prop_as_creator configuration:
app = (
ActingWebApp(...)
.with_email_as_creator(enable=True) # Requires email addresses (default)
.with_unique_creator(enable=True)
)
- When enabled (default):
Actor
creatorfield must be a valid email addressSystem validates email ownership via OAuth provider OR verification link
Suitable for applications requiring email for notifications, billing, etc.
- When disabled:
Actor
creatorfield can be provider-specific IDNo email verification needed (see Provider ID Support below)
Suitable for privacy-focused applications
Verification Flow
Token Generation: 32-byte URL-safe random token, 24-hour expiry
Storage: Token stored in
actor.store.email_verification_token; reverse index stored for token lookupEmail Sent: Lifecycle hook
email_verification_requiredtriggered withverification_urlUser Clicks Link:
GET /oauth/email?verify=<token>Validation: Token looked up via reverse index, checked against stored value and expiry
Mark Verified:
actor.store.email_verifiedset to"true"Hook Triggered: Lifecycle hook
email_verifiedexecuted
The verification flow is the same for both SPA and HTML template applications — the
email_verification_required lifecycle hook is responsible for sending the verification
email in both cases. See SPA Authentication Guide for the SPA-specific email collection flow.
Implementing Email Verification
Required: Implement the verification email hook:
@app.lifecycle_hook("email_verification_required")
def send_verification_email(
actor: ActorInterface,
email: str,
verification_url: str,
token: str
) -> None:
"""Send verification email when OAuth provider cannot verify."""
import boto3 # or your email service
ses = boto3.client('ses', region_name='us-east-1')
ses.send_email(
Source="noreply@yourdomain.com",
Destination={"ToAddresses": [email]},
Message={
"Subject": {"Data": "Verify your email address"},
"Body": {
"Html": {
"Data": f'''
<h2>Verify Your Email</h2>
<p>Click the link below to verify your email:</p>
<p><a href="{verification_url}">Verify Email</a></p>
<p>This link expires in 24 hours.</p>
'''
}
}
}
)
Optional: Handle successful verification:
@app.lifecycle_hook("email_verified")
def handle_email_verified(actor: ActorInterface, email: str) -> None:
"""Called after successful email verification."""
logger.info(f"Email verified for actor {actor.id}: {email}")
# Optional: Send welcome email, grant permissions, etc.
Verification Endpoints
GET /oauth/email?verify=<token>Primary verification endpoint. Validates the verification token via reverse index lookup and marks the email as verified. No actor ID needed in the URL.
- Responses:
Success: Shows “Email Verified!” page (HTML) or JSON success response
Invalid/expired token: Shows error with explanation
Already verified: Shows confirmation message
GET /<actor_id>/www/verify_email?token=<token>Legacy verification endpoint. Still functional for backward compatibility. Validates the token directly against the actor’s stored token.
POST /<actor_id>/www/verify_emailResends the verification email with a new token. Generates a new token, stores the reverse index, and fires the
email_verification_requiredhook.Use case: User didn’t receive the original email or token expired
Verification State
The verification state is stored in actor properties:
# Check if email is verified
if actor.store.email_verified == "true":
# Email has been verified
pass
# Access verification metadata
token = actor.store.email_verification_token # Current token (if pending)
created_at = actor.store.email_verification_created_at # Token creation time
verified_at = actor.store.email_verified_at # When verification completed
Templates
ActingWeb provides default templates for email verification:
- aw-verify-email.html
Verification result page (success, error, expired)
- aw-oauth-email.html
Email input form with dropdown support for verified emails
Applications can override these templates by placing them in their templates/ directory.
Security Considerations
- Token Security:
32 bytes (256 bits) of cryptographically secure random data
URL-safe encoding (no special characters)
Single-use tokens (cleared after verification)
24-hour expiration
- Email Validation:
Verified emails from OAuth providers are validated via API
User input restricted to verified emails when available (dropdown)
Manual email input requires clicking verification link
No way to bypass verification requirement
- Attack Prevention:
Token brute-forcing: 256-bit entropy makes this impractical
Token replay: Tokens are single-use and cleared after verification
Token expiry: Forces re-verification after 24 hours
Email spoofing: Verification link sent to claimed email address
Provider ID Support
ActingWeb supports using stable provider-specific identifiers instead of email addresses as the actor creator. This provides enhanced privacy and works with users who don’t want to share their email.
What Are Provider IDs?
Instead of using email addresses (which can change or be private), ActingWeb can use stable identifiers from OAuth providers:
- Google Provider IDs:
Format:
google:<sub>Example:
google:105123456789012345678The
subclaim is a unique, stable identifier that never changes for a user.- GitHub Provider IDs:
Format:
github:<user_id>Example:
github:12345678The GitHub user ID is stable even if the username changes. Falls back to
github:<username>if user ID unavailable.- Other Providers:
Format:
{provider_name}:{unique_id}Example:
microsoft:550e8400-e29b-41d4-a716-446655440000
Configuration
Enable provider ID mode by disabling email-as-creator:
app = (
ActingWebApp(...)
.with_email_as_creator(enable=False) # Use provider IDs
.with_unique_creator(enable=True)
)
- Effect:
Actor
creatorfield contains provider ID (e.g.,google:105...)Email address stored separately in
actor.store.email(if provided by OAuth)No email verification required
Works with private emails and email-less OAuth flows
Accessing Actor Information
# Get provider and ID from creator
if actor.creator.startswith("google:"):
google_sub = actor.creator.split(":", 1)[1]
print(f"Google user: {google_sub}")
elif actor.creator.startswith("github:"):
github_id = actor.creator.split(":", 1)[1]
print(f"GitHub user: {github_id}")
# Get display email (may be None in provider ID mode)
email = actor.store.email or "No email provided"
# Get OAuth provider
provider = actor.store.oauth_provider # "google", "github", etc.
Benefits of Provider IDs
- Privacy:
Users can authenticate without sharing their email address
- Stability:
Provider IDs never change, even if the user changes their email or username
- Compatibility:
Works with OAuth providers that don’t expose email addresses
- Security:
No user input - identifiers come directly from OAuth provider
- Simplicity:
No email verification flow needed
When to Use Provider IDs
- Use Provider IDs when:
Privacy is a priority
Email addresses are not required for your application
You want stable, unchanging identifiers
Supporting users with private GitHub emails
Building MCP-only applications (no email notifications)
- Use Email Mode when:
You need to send email notifications
Billing requires email addresses
Users expect email-based identification
Compatibility with existing email-based systems
Migration Between Modes
- Switching from Email to Provider ID:
Existing email-based actors continue to work. New actors use provider IDs.
- Switching from Provider ID to Email:
Not recommended. Existing actors use provider IDs. Consider implementing email linking separately.
- Hybrid Approach:
Store provider ID as creator, link email separately:
# In provider ID mode, email is stored separately actor.creator # "google:105123456789012345678" actor.store.email # "user@gmail.com" (if provided by OAuth) actor.store.email_verified # "true" if verified by OAuth provider
Comparison: Email vs Provider ID Modes
Feature |
Email Mode |
Provider ID Mode |
|---|---|---|
Creator field |
Email address |
Provider-specific ID |
Email verification |
Required (if not from verified OAuth source) |
Not required |
Private GitHub emails |
Requires verification |
Works seamlessly |
Stable identifier |
No (email can change) |
Yes (ID never changes) |
Email notifications |
Yes |
Optional (separate field) |
User privacy |
Email exposed |
Email optional |
MCP flows |
Email required |
Works without email |
Configuration |
|
|
Mobile App OAuth2
ActingWeb supports OAuth2 authentication from native mobile apps (iOS and Android) following RFC 8252 best practices. Mobile apps open the system browser for authentication, then receive the authorization code via a custom URL scheme deep link.
Provider Variant Naming
Mobile apps use provider name variants with a -mobile suffix to distinguish their
configuration from web-based providers. ActingWeb uses prefix matching, so google-mobile
resolves to the google provider type:
google-mobile– Google OAuth2 for mobile appsgithub-mobile– GitHub OAuth2 for mobile apps
This allows mobile-specific configuration (such as a different redirect_uri) while
reusing the same provider logic.
Configuration
Configure mobile provider variants with redirect_uri set to a custom URL scheme:
app = (
ActingWebApp(...)
# Web provider
.with_oauth(
provider="google",
client_id="your-web-client-id",
client_secret="your-web-client-secret",
scope="openid email profile",
)
# Mobile provider variant
.with_oauth(
provider="google-mobile",
client_id="your-mobile-client-id",
client_secret="your-mobile-client-secret",
scope="openid email profile",
redirect_uri="io.actingweb.myapp://callback",
)
)
The redirect_uri in provider config is used when exchanging authorization codes,
overriding the default server callback URL.
Mobile Authentication Flow
Mobile app opens system browser to the OAuth provider’s authorization URL
User authenticates in the browser
Provider redirects to the custom URL scheme (e.g.,
io.actingweb.myapp://callback?code=...)Mobile OS routes the deep link back to the app
App extracts the authorization code and calls
POST /oauth/spa/tokenwithgrant_type=authorization_codeActingWeb exchanges the code, creates/looks up the actor, and returns tokens as JSON
Mobile App System Browser OAuth Provider ActingWeb
─────────┐ ──────────────┐ ──────────────┐ ──────────┐
│─open──────────────────│ │ │
│ │──auth request────────│ │
│ │◄─login page──────────│ │
│ │──user credentials───▷│ │
│◄─deep link (code)─────│◄─redirect to app─────│ │
│ │ │ │
│──POST /oauth/spa/token (grant_type=authorization_code)──────▷│
│◄──JSON { access_token, refresh_token, actor_id }────────────│
See SPA Authentication Guide for the POST /oauth/spa/token request and
response format for authorization code exchange.
Future Enhancements
The authentication system is designed for extensibility:
- Additional Providers:
Microsoft Azure AD / Office 365
Auth0 and other identity providers
Custom enterprise OAuth2 providers
OpenID Connect providers
- Enhanced Features:
Organization/team membership validation (GitHub, Google Workspace)
Customizable OAuth2 scopes per application
Advanced token refresh patterns
Webhook integration for account changes
Multi-factor authentication support
- Performance Improvements:
Token caching and validation optimization
Async OAuth2 flows for better performance
Connection pooling for provider API calls
- Developer Experience:
Configuration validation and helpful error messages
OAuth2 flow debugging tools
Provider-specific setup documentation
Integration testing utilities
This implementation provides a solid foundation for multi-provider OAuth2 support in ActingWeb while maintaining backward compatibility and enabling future authentication enhancements. Roles —–
ActingWeb commonly plays two roles with OAuth2:
OAuth2 Client (login): Used for interactive login to the web UI (
/<actor_id>/www) and app flows. The app redirects users to a provider (Google/GitHub) and receives a callback.OAuth2‑Protected Resource (MCP): The
/mcpendpoint requires OAuth2 access. Unauthenticated requests receive 401 with a properWWW-Authenticateheader and discovery endpoints are exposed under/.well-known/.
Discovery Endpoints (served by integrations):
/.well-known/oauth-authorization-server/.well-known/oauth-protected-resource/.well-known/oauth-protected-resource/mcp
Provider Differences (Cheat Sheet)
Google: refresh tokens supported (use
access_type=offline+prompt=consent), OpenID scopes (openid email profile).GitHub: set
User-AgentandAccept: application/jsonheaders, no refresh tokens (short‑lived tokens), email may be private.