===================================================== OAuth Login Flow with Postponed Actor Creation ===================================================== This document describes how to implement "Login with Google" / "Login with GitHub" buttons in your ActingWeb application, where actor creation is postponed until after the OAuth2 flow completes and email is obtained. Important: Two Different OAuth2 Flows ======================================== ActingWeb supports **TWO distinct OAuth2 flows**. This document covers the **Google/GitHub OAuth flow for web UI login**. For MCP OAuth2 where ActingWeb acts as an authorization server, see :doc:`mcp-applications`. Flow 1: MCP OAuth2 (ActingWeb as Authorization Server) ---------------------------------------------------------- - MCP clients dynamically register via ``/oauth/register`` - User authorizes via ``/oauth/authorize`` with trust type selection - ActingWeb proxies to Google/GitHub for user authentication - ActingWeb issues its own tokens for ``/mcp`` endpoint access - Uses **encrypted state** - routed to ``OAuth2EndpointsHandler`` Flow 2: Google/GitHub OAuth (This Document) ---------------------------------------------- - Direct OAuth2 with Google/GitHub providers - Used for web UI login at ``/www`` endpoints - Can also establish MCP trust relationships (with ``trust_type`` parameter) - Uses **JSON state** - routed to ``OAuth2CallbackHandler`` - **This is what this document covers** Overview ========== .. note:: **Token Architecture** ActingWeb generates its own session tokens after validating OAuth provider tokens. The Google/GitHub token is validated once during callback, then ActingWeb creates its own token stored in the session manager. This provides: - Fast validation (no network calls to Google/GitHub on each request) - Security (OAuth tokens never exposed to frontend) - Reliability (no dependency on OAuth provider availability) See :doc:`spa-authentication` for details on token architecture. The ActingWeb library now supports Google/GitHub OAuth2 login flows where: 1. **User clicks "Login with Google/GitHub"** on the factory page 2. **OAuth2 flow completes** and email is extracted from the provider 3. **Actor is created** with the obtained email 4. **User is redirected** to their actor's page Additionally, if the OAuth provider cannot provide an email (e.g., GitHub with private email), the library will: 1. Store OAuth tokens temporarily in a session 2. Redirect user to an email input form 3. Complete actor creation once email is provided Library Components ===================== The ActingWeb library provides all the backend functionality. Your application only needs to provide templates and configure OAuth2. 1. Factory Handler Updates ------------------------------ The factory handler (``actingweb.handlers.factory``) now exposes OAuth URLs in ``template_values``: .. code-block:: python # In GET / request, template_values will contain: { '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://accounts.google.com/o/oauth2/v2/auth?...' } ], 'oauth_enabled': True } 2. OAuth2 Callback Handler Updates -------------------------------------- The OAuth2 callback handler (``actingweb.handlers.oauth2_callback``) now handles missing email gracefully: - If email is successfully extracted: Creates actor and redirects to ``/{actor_id}/www`` (existing behavior) - If email cannot be extracted (HTML template flow): Redirects to ``/oauth/email`` with a session token - If email cannot be extracted (SPA flow): Redirects back to SPA with ``?email_required=true&session=`` 3. OAuth Session Management -------------------------------- New module ``actingweb.oauth_session`` provides: - ``OAuth2SessionManager``: Manages temporary OAuth2 sessions - ``store_session()``: Store OAuth tokens when email is missing - ``get_session()``: Retrieve session data - ``complete_session()``: Complete actor creation with provided email Sessions are stored in the database using ActingWeb's attribute bucket system with a 10-minute TTL. This provides persistence across multiple containers in distributed deployments. 4. OAuth Email Input Handler -------------------------------- New handler ``actingweb.handlers.oauth_email.OAuth2EmailHandler``: - **GET /oauth/email?session=...**: Shows email input form (sets ``template_values`` for app to render, or returns JSON for SPAs) - **GET /oauth/email?verify=...**: Validates email verification token and marks email as verified - **POST /oauth/email**: Processes email input and completes actor creation Both Flask and FastAPI integrations automatically route ``/oauth/email`` to this handler. Application Implementation ============================= Your application needs to provide templates to create the UI. Step 1: Create Factory Template with OAuth Buttons ------------------------------------------------------- Create ``templates/aw-root-factory.html``: .. code-block:: html Login - My ActingWeb App

Welcome to My ActingWeb App

{% if oauth_enabled %}

Login with OAuth

{% for provider in oauth_providers %} Login with {{ provider.display_name }} {% endfor %} {% endif %}

Or create account with email

Step 2: Create Email Input Template (Optional) ---------------------------------------------------- Create ``templates/aw-oauth-email.html``: .. code-block:: html Enter Email - My ActingWeb App

Email Required

{{ message }}

{% if error %}

{{ error }}

{% endif %}
.. note:: If you don't provide this template, the Flask/FastAPI integration will use a basic fallback HTML form. Step 3: Configure OAuth2 Provider -------------------------------------- In your application initialization: .. code-block:: python from actingweb.interface import ActingWebApp # Single provider (backward compatible) app = ( ActingWebApp( aw_type="urn:actingweb:myapp", database="dynamodb", fqdn="myapp.example.com", proto="https://" ) .with_oauth( provider="google", client_id="your-google-client-id", client_secret="your-google-client-secret", scope="openid email profile", ) .with_web_ui(enable=True) # Required for template rendering ) # Multiple providers (Google + GitHub simultaneously) app = ( ActingWebApp( aw_type="urn:actingweb:myapp", database="dynamodb", fqdn="myapp.example.com", proto="https://" ) .with_oauth( provider="google", client_id="your-google-client-id", client_secret="your-google-client-secret", scope="openid email profile", ) .with_oauth( provider="github", client_id="your-github-client-id", client_secret="your-github-client-secret", scope="read:user user:email", ) .with_web_ui(enable=True) ) Complete Flow Examples ========================= Flow 2.1: Web UI Login with Email Successfully Retrieved -------------------------------------------------------------- .. code-block:: text 1. User visits GET / ↓ Factory handler returns template_values with oauth_urls (no trust_type) ↓ App renders aw-root-factory.html with "Login with Google" button 2. User clicks "Login with Google" ↓ Browser redirects to Google OAuth2 3. Google redirects to /oauth/callback with code (JSON state, no trust_type) ↓ Flask/FastAPI routes to OAuth2CallbackHandler ↓ Exchange code for access token ↓ Extract email from user info: user@gmail.com ↓ Create actor with email (or lookup existing) ↓ Generate ActingWeb session token (not Google token) ↓ Store in session manager for fast validation ↓ Set HttpOnly cookie with ActingWeb token ↓ Redirect to /{actor_id}/www Flow 2.2: Web UI Login without Email (GitHub Private Email) -------------------------------------------------------------- .. code-block:: text 1. User visits GET / ↓ Factory handler returns template_values with oauth_urls (no trust_type) ↓ App renders aw-root-factory.html with "Login with GitHub" button 2. User clicks "Login with GitHub" (email is private) ↓ Browser redirects to GitHub OAuth2 3. GitHub redirects to /oauth/callback with code (JSON state, no trust_type) ↓ Flask/FastAPI routes to OAuth2CallbackHandler ↓ Exchange code for access token ↓ Email extraction fails (private email) ↓ Check: trust_type is NOT set → This is web UI login ↓ Store OAuth tokens in temporary session ↓ HTML template flow: Redirect to /oauth/email?session= SPA flow: Redirect to {spa_redirect_url}?email_required=true&session= 4a. HTML template flow: OAuth email handler returns template_values ↓ App renders aw-oauth-email.html with email input form ↓ User enters their email address and submits 4b. SPA flow: SPA detects email_required=true parameter ↓ SPA shows its own email input form ↓ SPA POSTs to /oauth/email with Accept: application/json 5. POST /oauth/email ↓ Retrieve OAuth tokens from session ↓ Create actor with provided email ↓ If email needs verification: - Store verification token and reverse index - Fire email_verification_required lifecycle hook - App backend sends verification email ↓ Generate ActingWeb session token ↓ HTML template flow: Set HttpOnly cookie, redirect to /{actor_id}/www SPA flow: Return JSON with actor_id, access_token, email_requires_verification 6. Email verification (if required): ↓ User clicks link in email: GET /oauth/email?verify= ↓ Token validated, email marked as verified ↓ email_verified lifecycle hook fired Flow 2.3: MCP Authorization with trust_type (No Email Available) ----------------------------------------------------------------------- .. code-block:: text 1. OAuth flow initiated with trust_type='mcp_client' in state ↓ Redirect to Google/GitHub OAuth2 2. Google redirects to /oauth/callback with code (JSON state with trust_type) ↓ Flask/FastAPI routes to OAuth2CallbackHandler ↓ Exchange code for access token ↓ Email extraction fails ↓ Check: trust_type IS set → This is MCP authorization ↓ Return error (cannot redirect to web form for MCP clients) ↓ HTTP 502: "Email extraction failed. OAuth provider did not provide email address required for mcp_client authorization." .. note:: Flow 2.3 demonstrates that MCP authorization flows with ``trust_type`` cannot fall back to email input forms because MCP clients are programmatic and cannot interact with web UIs. Users should ensure their OAuth provider (Google/GitHub) is configured to provide email addresses publicly. Flow 2.4: Mobile App OAuth (Authorization Code Exchange) -------------------------------------------------------------- .. code-block:: text 1. Mobile app opens system browser to OAuth provider ↓ User authenticates with Google/GitHub in system browser 2. OAuth provider redirects to custom URL scheme ↓ e.g. io.actingweb.myapp://callback?code=AUTH_CODE&state=... ↓ Mobile OS routes deep link back to the app ↓ App extracts authorization code from the URL 3. App sends POST /oauth/spa/token ↓ Body: { "grant_type": "authorization_code", "code": "AUTH_CODE", "provider": "google-mobile", "redirect_uri": "io.actingweb.myapp://callback", "token_delivery": "json" } ↓ ActingWeb exchanges code with OAuth provider ↓ Extract email from user info ↓ Create actor (or lookup existing) ↓ Generate ActingWeb session tokens ↓ Return JSON: { "success": true, "actor_id": "abc123", "email": "user@example.com", "access_token": "...", "refresh_token": "...", "token_type": "Bearer", "expires_in": 3600, "expires_at": ... } 4. App stores tokens in secure storage ↓ iOS: Keychain Services Android: Android Keystore / EncryptedSharedPreferences ↓ App uses access_token for subsequent API requests **Google Mobile Flow:** - Provider variant: ``google-mobile`` - Custom URL scheme: configured in Google Cloud Console as an authorized redirect URI - Supports PKCE (recommended) - Refresh tokens available **GitHub Mobile Flow:** - Provider variant: ``github-mobile`` - Custom URL scheme: configured in GitHub OAuth App settings - No refresh tokens (GitHub limitation) - Token renewal requires re-authentication **Key Differences from Web/SPA Flows:** - No ``/oauth/callback`` round-trip -- the app receives the code directly via deep link - Uses ``grant_type=authorization_code`` on ``/oauth/spa/token`` instead of the callback endpoint - ``redirect_uri`` must match the custom URL scheme registered with the OAuth provider - Tokens stored in platform-secure storage, not browser memory or cookies Template Variables Reference ================================ Factory Template (aw-root-factory.html) -------------------------------------------- Variables available in ``template_values``: .. list-table:: :header-rows: 1 * - Variable - Type - Description * - ``oauth_urls`` - Dict - Dictionary mapping provider names to OAuth URLs * - ``oauth_providers`` - List - List of dicts with ``name``, ``display_name``, ``url`` * - ``oauth_enabled`` - Boolean - True if OAuth is configured Example usage: .. code-block:: html {% if oauth_enabled %} {% for provider in oauth_providers %} Login with {{ provider.display_name }} {% endfor %} {% endif %} Email Input Template (aw-oauth-email.html) ------------------------------------------------ Variables available in ``template_values``: .. list-table:: :header-rows: 1 * - Variable - Type - Description * - ``session_id`` - String - Session ID for completing OAuth flow * - ``action`` - String - Form action URL (always "/oauth/email") * - ``method`` - String - Form method (always "POST") * - ``provider`` - String - Provider name ("google", "github", etc.) * - ``provider_display`` - String - Provider display name ("Google", "GitHub") * - ``message`` - String - User-friendly message explaining why email is needed * - ``error`` - String or None - Error message if email validation failed Example usage: .. code-block:: html

{{ message }}

{% if error %}

{{ error }}

{% endif %}
Advanced Configuration ========================= Multiple OAuth Providers ------------------------------ Multiple OAuth providers can be configured simultaneously using the ``provider`` parameter: .. code-block:: python app = ( ActingWebApp( aw_type="urn:actingweb:myapp", database="dynamodb", fqdn="myapp.example.com", proto="https://" ) .with_oauth( provider="google", client_id="your-google-client-id", client_secret="your-google-client-secret", scope="openid email profile", ) .with_oauth( provider="github", client_id="your-github-client-id", client_secret="your-github-client-secret", scope="read:user user:email", ) .with_web_ui(enable=True) ) When multiple providers are configured: - The factory page (``/``) shows login buttons for all configured providers - The ``/oauth/config`` endpoint returns all providers in its ``oauth_providers`` array - Each OAuth callback identifies the correct provider via the ``provider`` field in the state parameter - ``config.oauth`` still points to the first provider for backward compatibility - Users typically log in via one provider; email-based linking allows the same user to use both providers Session Storage Architecture ---------------------------------- OAuth sessions are automatically stored in the database using ActingWeb's attribute bucket system: - **Storage Location**: ``OAUTH2_SYSTEM_ACTOR`` actor with bucket ``oauth_sessions`` - **Persistence**: Database-backed for multi-container deployments - **TTL**: 10 minutes (configurable via ``_SESSION_TTL`` constant) - **Automatic Cleanup**: Expired sessions are automatically removed on access No custom session storage implementation is needed - the default implementation handles distributed deployments automatically. Custom Session TTL ------------------------ The default session TTL is 10 minutes. To change it: .. code-block:: python # In actingweb/oauth_session.py _SESSION_TTL = 600 # Default: 10 minutes # Override by modifying the module constant before use: import actingweb.oauth_session actingweb.oauth_session._SESSION_TTL = 1800 # 30 minutes Security Considerations =========================== 1. **Session Storage**: Sessions are automatically stored in the database using ActingWeb's attribute bucket system, providing persistence across multiple containers. 2. **Session TTL**: Sessions expire after 10 minutes to prevent token leakage. Adjust TTL based on your security requirements. 3. **HTTPS Required**: OAuth2 flows must use HTTPS in production (``proto="https://"``). 4. **State Parameter**: The library automatically includes CSRF protection via the OAuth2 state parameter. 5. **Email Validation**: The library validates that authenticated email matches the expected email from forms (when applicable). Testing ========== To test the OAuth login flow: 1. Configure test OAuth2 credentials (Google/GitHub OAuth app) 2. Set redirect_uri to your local development URL 3. Visit ``http://localhost:5000/`` (or your dev server) 4. Click "Login with Google/GitHub" 5. Complete OAuth flow 6. Verify actor creation and redirection For testing email input flow with GitHub (HTML template): 1. Configure GitHub OAuth app 2. Set your GitHub email to private in settings 3. Complete OAuth flow 4. Verify redirect to ``/oauth/email`` 5. Enter email and verify actor creation 6. Check that ``email_verification_required`` hook fires 7. Click verification link (``/oauth/email?verify=``) to complete For testing email input flow with GitHub (SPA): 1. Configure GitHub OAuth app 2. Set your GitHub email to private in settings 3. Complete SPA OAuth flow 4. Verify redirect back to SPA with ``?email_required=true&session=...`` 5. POST email to ``/oauth/email`` with ``Accept: application/json`` 6. Check that ``email_verification_required`` hook fires 7. Verify ``email_requires_verification: true`` in JSON response Troubleshooting ================== OAuth URLs Not Showing -------------------------- - Check that ``config.oauth`` is properly configured - Verify ``config.ui = True`` for template rendering - Check that ``client_id`` and ``client_secret`` are set - Inspect ``template_values`` in factory handler logs Email Form Not Appearing ------------------------------ - Check that ``/oauth/email`` route is registered (Flask/FastAPI integration does this automatically) - Verify session has not expired (10-minute TTL) - Check browser console for JavaScript errors - Inspect template rendering errors in logs Actor Not Created After Email Input ------------------------------------------ - Check session storage logs for session retrieval - Verify email format validation - Check actor creation logs for errors - Verify database connectivity Session Expired Error -------------------------- - User took longer than 10 minutes to enter email - Increase ``_SESSION_TTL`` or implement database-backed sessions - Add user-friendly error message in template Migration from Traditional Flow =================================== If your app currently uses traditional actor creation (email input only), you can add OAuth login alongside: 1. Keep existing ``aw-root-factory.html`` template 2. Add OAuth button section with conditional rendering: .. code-block:: html {% if oauth_enabled %} {% endif %}
...
3. No changes needed to backend code 4. Users can choose between OAuth login or traditional email Both flows will coexist and work independently.