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

372 lines
No EOL
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🏗 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:
```python
# 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:
```python
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`**:
```python
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**:
```python
# 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).