mockapi/docs/ARCHITECTURE_OAUTH2_CONTROLLERS.md
2026-03-16 10:49:01 +00:00

16 KiB
Raw Permalink Blame History

🏗 Architectural Specification: OAuth2 Controllers (Phase 6.4)

🎯 Design Philosophy

"We are implementing a Strategy pattern for OAuth2 grant types (already established in OAuthService) and Repository-Service-Controller pattern for clean separation of concerns. The OAuth2 endpoints follow RFC 6749, 7662, 7009, and OpenID Connect Core 1.0 (userinfo). Admin management routes extend the existing admin interface with consistent session-based authentication."

🔍 Discovery & Analysis

Current State:

  • OAuth2 models, repositories, schemas, and services are already implemented.
  • RouteManager already validates OAuth2 tokens for endpoints with requires_oauth=True.
  • Admin interface uses session middleware (AuthMiddleware) protecting /admin/* routes.
  • Existing pattern: controllers define routers, use dependencies for DB sessions, and Jinja2 templates for HTML responses.

Dependencies:

  • oauth2/services.py: OAuthService, TokenService, ClientService, ScopeService
  • oauth2/repositories.py: OAuthClientRepository, OAuthTokenRepository, OAuthUserRepository
  • oauth2/schemas.py: OAuthClientCreate, OAuthClientResponse, OAuthTokenCreate, OAuthTokenResponse, OAuthUserCreate, OAuthUserResponse
  • oauth2/dependencies.py: get_current_token_payload, require_scope, etc.
  • controllers/admin_controller.py: pattern for admin routes, session handling, pagination.
  • templates/base.html: Bootstrap 5 layout with sidebar.

Bottlenecks & Risks:

  1. Authorization code storage: Currently not implemented (TODO in OAuthService.authorize_code_flow). Need a simple in-memory or database store for authorization codes with expiration.
  2. User consent UI: Need a simple HTML page for authorization approval.
  3. Password grant: Not required; can be omitted or implemented later.
  4. Security: Must validate redirect_uri, client credentials, scopes, and PKCE (optional future enhancement).

🛠 Blueprint

1. File Structure

mockapi/
├── oauth2/
│   ├── __init__.py
│   ├── repositories.py
│   ├── schemas.py
│   ├── services.py
│   ├── dependencies.py
│   ├── controller.py          # NEW: OAuth2 standard endpoints (API)
│   └── auth_code_store.py    # NEW: Temporary storage for authorization codes
├── controllers/
│   ├── __init__.py
│   ├── admin_controller.py   # EXTEND: Add OAuth2 admin management routes
│   └── (no separate oauth2_controller.py)
└── templates/
    ├── admin/
    │   ├── oauth_clients.html      # NEW: List OAuth clients
    │   ├── oauth_client_form.html  # NEW: Create/edit client form
    │   ├── oauth_tokens.html       # NEW: List OAuth tokens
    │   └── oauth_users.html        # NEW: List OAuth users (optional)
    └── oauth/
        └── authorize_consent.html  # NEW: Authorization consent page

2. Router Definitions

2.1 OAuth2 Standard Endpoints (oauth2/controller.py)

  • Prefix: /oauth
  • Tags: ["oauth2"]
  • Dependencies: Depends(get_db) for database session; no session authentication.
  • Endpoints:
    1. GET /oauth/authorize Authorization endpoint (RFC 6749 §4.1)
    2. POST /oauth/authorize Authorization submission (user consent)
    3. POST /oauth/token Token endpoint (RFC 6749 §4.1.3, 4.3, 4.4, 6)
    4. GET /oauth/userinfo UserInfo endpoint (OpenID Connect Core §5.3)
    5. POST /oauth/introspect Token introspection (RFC 7662)
    6. POST /oauth/revoke Token revocation (RFC 7009)
    7. GET /.well-known/openid-configuration OIDC discovery (optional)

2.2 Admin OAuth2 Management (controllers/admin_controller.py)

  • Prefix: /admin/oauth
  • Tags: ["admin-oauth"]
  • Dependencies: Existing session authentication (AuthMiddleware) applies automatically.
  • Endpoints:
    1. GET /admin/oauth/clients List OAuth clients with pagination
    2. GET /admin/oauth/clients/new Form to create new client
    3. POST /admin/oauth/clients Create new client
    4. GET /admin/oauth/clients/{client_id}/edit Edit client form
    5. POST /admin/oauth/clients/{client_id} Update client
    6. POST /admin/oauth/clients/{client_id}/delete Delete client (soft delete via is_active=False)
    7. GET /admin/oauth/tokens List OAuth tokens with filtering (client, user, active/expired)
    8. POST /admin/oauth/tokens/{token_id}/revoke Revoke token (delete)
    9. GET /admin/oauth/users List OAuth users (optional)
    10. POST /admin/oauth/users/{user_id}/toggle Toggle user active status

3. Endpoint Specifications

3.1 Authorization Endpoint (GET /oauth/authorize)

Purpose: Display consent screen to resource owner. Parameters (query string):

  • response_type=code (only authorization code supported)
  • client_id (required)
  • redirect_uri (required, must match registered)
  • scope (optional)
  • state (recommended)
  • code_challenge, code_challenge_method (PKCE optional future)

Flow:

  1. Validate client_id, redirect_uri, scopes (via OAuthService).
  2. If user not authenticated, redirect to login page (reuse admin login? Or separate OAuth user login). For simplicity, we can check if admin session exists; if not, redirect to /admin/login with return URL.
  3. Render templates/oauth/authorize_consent.html with client details and requested scopes.
  4. Include hidden inputs for all query parameters.

Response: HTML consent page.

3.2 Authorization Submission (POST /oauth/authorize)

Purpose: Process user consent. Parameters (form data):

  • client_id, redirect_uri, state, scope (hidden fields)
  • action (allow/deny)

Flow:

  1. Validate same parameters again.
  2. If action=allow, generate authorization code (store with expiration, client_id, redirect_uri, scopes, user_id if authenticated).
  3. Redirect to redirect_uri with code and state (if provided).
  4. If action=deny, redirect with error=access_denied.

Response: 302 Redirect to client's redirect_uri.

3.3 Token Endpoint (POST /oauth/token)

Purpose: Issue tokens for all grant types. Content-Type: application/x-www-form-urlencoded Parameters (depending on grant_type):

  • grant_type (required): authorization_code, client_credentials, refresh_token, password (optional)
  • client_id, client_secret (required for confidential clients, except password grant)
  • code, redirect_uri (for authorization_code)
  • refresh_token (for refresh_token)
  • username, password (for password grant optional)
  • scope (optional)

Flow:

  1. Validate client credentials (if required) via ClientService.
  2. Route to appropriate method in OAuthService:
    • authorization_code: validate code, redirect_uri, issue access/refresh tokens.
    • client_credentials: call client_credentials_flow.
    • refresh_token: call refresh_token_flow.
    • password: (optional) validate user credentials, issue tokens.
  3. Return JSON response per RFC 6749 §5.1.

Response: JSON with access_token, token_type, expires_in, refresh_token (if applicable), scope.

3.4 UserInfo Endpoint (GET /oauth/userinfo)

Purpose: Return claims about authenticated user (OpenID Connect). Authentication: Bearer token with openid scope (or any scope). Use dependency get_current_token_payload. Flow:

  1. Extract token payload (contains sub, client_id, scopes).
  2. If token has user_id, fetch user details from OAuthUserRepository.
  3. Return JSON with standard claims (sub, name, email, etc.) as available.

Response: JSON with user claims.

3.5 Token Introspection (POST /oauth/introspect)

Purpose: Validate token and return its metadata (RFC 7662). Authentication: Client credentials via HTTP Basic (or bearer token). Use ClientService. Parameters: token (required), token_type_hint (optional). Flow:

  1. Validate client credentials (must be confidential client).
  2. Look up token in database via OAuthTokenRepository.
  3. Return active/expired status, scopes, client_id, user_id, etc.

Response: JSON per RFC 7662.

3.6 Token Revocation (POST /oauth/revoke)

Purpose: Revoke a token (RFC 7009). Authentication: Client credentials via HTTP Basic (or bearer token). Parameters: token (required), token_type_hint (optional). Flow:

  1. Validate client credentials.
  2. Revoke token (delete from database) via TokenService.revoke_token.
  3. Return 200 OK regardless of token existence (RFC 7009).

Response: 200 with no body.

3.7 OIDC Discovery (GET /.well-known/openid-configuration)

Purpose: Provide OpenID Connect discovery metadata. Response: JSON with issuer, authorization/token/userinfo endpoints, supported grant types, scopes, etc.

4. Admin Management Endpoints

4.1 OAuth Clients CRUD

  • List: Paginated table with client ID, name, grant types, redirect URIs, active status, actions (edit, delete).
  • Create/Edit Form: Fields: client_id, client_secret (plaintext), name, redirect_uris (newline separated), grant_types (checkboxes), scopes (newline separated), is_active (checkbox).
  • Validation: Use OAuthClientCreate schema.
  • Password Hashing: Hash client_secret with bcrypt before storing (already in repository).

4.2 OAuth Tokens Management

  • List: Table with access token (truncated), client, user, scopes, expires, active (not expired). Filter by client, user, active/expired.
  • Revoke: Delete token from database (immediate invalidation).

4.3 OAuth Users Management (optional)

  • List: Username, email, active status.
  • Toggle active: Prevent user from obtaining new tokens.

5. Template Files Needed

Templates Structure:

templates/admin/
├── oauth_clients.html
├── oauth_client_form.html
├── oauth_tokens.html
└── oauth_users.html
templates/oauth/
└── authorize_consent.html

Design Guidelines:

  • Extend base.html (already includes Bootstrap 5, sidebar).
  • Use same styling as existing admin pages (cards, tables, buttons).
  • For forms, reuse admin/endpoint_form.html pattern (field errors, validation).

6. Configuration Additions (config.py)

Add to Settings class:

# OAuth2 Settings
oauth2_issuer: str = "http://localhost:8000"  # Used for discovery
oauth2_access_token_expire_minutes: int = 30
oauth2_refresh_token_expire_days: int = 7
oauth2_authorization_code_expire_minutes: int = 10
oauth2_supported_grant_types: List[str] = ["authorization_code", "client_credentials", "refresh_token"]
oauth2_supported_scopes: List[str] = ["openid", "profile", "email", "api:read", "api:write"]
oauth2_pkce_required: bool = False  # Future enhancement

7. Updates to app.py

Add after admin router inclusion:

from oauth2.controller import router as oauth_router

# Include OAuth2 router
app.include_router(oauth_router)

Ensure OAuth2 router is added before the dynamic route registration? Order doesn't matter because routes are matched sequentially; OAuth2 routes have specific prefixes.

8. Authorization Code Storage

Create oauth2/auth_code_store.py with a simple inmemory store (dictionary) mapping code → dict (client_id, redirect_uri, scopes, user_id, expires_at). In production, replace with Redis or database table.

Interface:

  • store_code(code, data)
  • get_code(code) -> Optional[dict]
  • delete_code(code)

Integration: Update OAuthService.authorize_code_flow to store code; add authorization_code_flow method to exchange code for tokens.


🔒 Security & Performance

Security Considerations

  1. Redirect URI validation: Exact match (including query parameters?) follow RFC 6749 (exact match of entire URI).
  2. Client secret hashing: Already implemented via bcrypt in repository.
  3. Token revocation: Immediate deletion from database.
  4. Scope validation: Ensure requested scopes are subset of client's allowed scopes.
  5. CSRF protection: Use state parameter; for authorization POST, check session token (optional).
  6. PKCE: Future enhancement for public clients (SPA).
  7. HTTPS: Require in production (configurable).

Performance

  • Token validation: Each protected endpoint validates token via database lookup. Ensure indexes on access_token and expires_at.
  • Authorization code storage: Inmemory store is fast; consider expiration cleanup job (cron or background task).

📋 Instructions for @coder

Step 1: Create Authorization Code Store

  • File: oauth2/auth_code_store.py
  • Implement AuthorizationCodeStore class with async methods using dict and asyncio.Lock.
  • Integrate with OAuthService (add dependency).

Step 2: Implement OAuth2 Controller (oauth2/controller.py)

  • Create router with prefix /oauth.
  • Implement each endpoint as async function, delegating to OAuthService.
  • Use Depends(get_db) to get database session.
  • For token endpoint, parse x-www-form-urlencoded data (fastapi.Form).
  • For introspection/revocation, implement HTTP Basic authentication (or bearer token).
  • Add OIDC discovery endpoint returning static JSON.

Step 3: Extend Admin Controller (controllers/admin_controller.py)

  • Add new router with prefix /admin/oauth.
  • Create route functions similar to existing endpoint CRUD.
  • Use existing templates directory and Jinja2Templates.
  • Ensure session authentication works (already covered by AuthMiddleware).

Step 4: Create HTML Templates

  • Copy existing admin/endpoints.html pattern for listing.
  • Create forms with appropriate fields.
  • Use Bootstrap 5 classes.

Step 5: Update Configuration (config.py)

  • Add OAuth2 settings with sensible defaults.
  • Ensure backward compatibility (existing settings unchanged).

Step 6: Update App (app.py)

  • Import and include OAuth2 router.
  • Optionally add middleware for CORS if needed.

Step 7: Test

  • Use curl or Postman to test grant flows.
  • Verify admin pages load and CRUD works.

🚨 Error Handling & Validation

  • Use HTTPException with appropriate status codes (400 for client errors, 401/403 for authentication/authorization).
  • Log errors with logger.
  • Return RFCcompliant error responses for OAuth2 endpoints (e.g., error, error_description).
  • Validate input with Pydantic schemas (already defined).

📁 Example Imports & Function Signatures

oauth2/controller.py:

import logging
from typing import Optional, List
from fastapi import APIRouter, Depends, Request, Form, HTTPException, status
from fastapi.responses import RedirectResponse, JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession

from database import get_db
from oauth2.services import OAuthService, TokenService, ClientService
from oauth2.dependencies import get_current_token_payload
from config import settings

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/oauth", tags=["oauth2"])

@router.get("/authorize")
async def authorize(
    request: Request,
    response_type: str,
    client_id: str,
    redirect_uri: str,
    scope: Optional[str] = None,
    state: Optional[str] = None,
    db: AsyncSession = Depends(get_db),
):
    # ...

controllers/admin_controller.py additions:

# Add after existing endpoint routes
@router.get("/oauth/clients", response_class=HTMLResponse)
async def list_oauth_clients(
    request: Request,
    page: int = 1,
    db: AsyncSession = Depends(get_db),
):
    # ...

📈 FutureProofing

  • PKCE support: Add code_challenge validation in authorization and token endpoints.
  • JWT access tokens: Already implemented; consider adding signature algorithm configuration.
  • Multiple token stores: Could replace inmemory code store with Redis.
  • OpenID Connect: Extend userinfo with standard claims, add id_token issuance.

FINAL MISSION: Deliver a clean, maintainable OAuth2 provider that integrates seamlessly with the existing mock API admin interface, follows established patterns, and is ready for Phase 6.5 (Configuration & Integration).