16 KiB
🏗 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,ScopeServiceoauth2/repositories.py:OAuthClientRepository,OAuthTokenRepository,OAuthUserRepositoryoauth2/schemas.py:OAuthClientCreate,OAuthClientResponse,OAuthTokenCreate,OAuthTokenResponse,OAuthUserCreate,OAuthUserResponseoauth2/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:
- 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. - User consent UI: Need a simple HTML page for authorization approval.
- Password grant: Not required; can be omitted or implemented later.
- 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:
GET /oauth/authorize– Authorization endpoint (RFC 6749 §4.1)POST /oauth/authorize– Authorization submission (user consent)POST /oauth/token– Token endpoint (RFC 6749 §4.1.3, 4.3, 4.4, 6)GET /oauth/userinfo– UserInfo endpoint (OpenID Connect Core §5.3)POST /oauth/introspect– Token introspection (RFC 7662)POST /oauth/revoke– Token revocation (RFC 7009)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:
GET /admin/oauth/clients– List OAuth clients with paginationGET /admin/oauth/clients/new– Form to create new clientPOST /admin/oauth/clients– Create new clientGET /admin/oauth/clients/{client_id}/edit– Edit client formPOST /admin/oauth/clients/{client_id}– Update clientPOST /admin/oauth/clients/{client_id}/delete– Delete client (soft delete via is_active=False)GET /admin/oauth/tokens– List OAuth tokens with filtering (client, user, active/expired)POST /admin/oauth/tokens/{token_id}/revoke– Revoke token (delete)GET /admin/oauth/users– List OAuth users (optional)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:
- Validate client_id, redirect_uri, scopes (via OAuthService).
- 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/loginwith return URL. - Render
templates/oauth/authorize_consent.htmlwith client details and requested scopes. - 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:
- Validate same parameters again.
- If action=allow, generate authorization code (store with expiration, client_id, redirect_uri, scopes, user_id if authenticated).
- Redirect to
redirect_uriwithcodeandstate(if provided). - 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:
- Validate client credentials (if required) via
ClientService. - Route to appropriate method in
OAuthService:authorization_code: validate code, redirect_uri, issue access/refresh tokens.client_credentials: callclient_credentials_flow.refresh_token: callrefresh_token_flow.password: (optional) validate user credentials, issue tokens.
- 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:
- Extract token payload (contains
sub,client_id,scopes). - If token has
user_id, fetch user details fromOAuthUserRepository. - 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:
- Validate client credentials (must be confidential client).
- Look up token in database via
OAuthTokenRepository. - 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:
- Validate client credentials.
- Revoke token (delete from database) via
TokenService.revoke_token. - 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
OAuthClientCreateschema. - 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.htmlpattern (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 in‑memory 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
- Redirect URI validation: Exact match (including query parameters?) – follow RFC 6749 (exact match of entire URI).
- Client secret hashing: Already implemented via bcrypt in repository.
- Token revocation: Immediate deletion from database.
- Scope validation: Ensure requested scopes are subset of client's allowed scopes.
- CSRF protection: Use
stateparameter; for authorization POST, check session token (optional). - PKCE: Future enhancement for public clients (SPA).
- HTTPS: Require in production (configurable).
Performance
- Token validation: Each protected endpoint validates token via database lookup. Ensure indexes on
access_tokenandexpires_at. - Authorization code storage: In‑memory 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
AuthorizationCodeStoreclass with async methods usingdictandasyncio.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-urlencodeddata (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
templatesdirectory andJinja2Templates. - Ensure session authentication works (already covered by AuthMiddleware).
Step 4: Create HTML Templates
- Copy existing
admin/endpoints.htmlpattern 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
HTTPExceptionwith appropriate status codes (400 for client errors, 401/403 for authentication/authorization). - Log errors with
logger. - Return RFC‑compliant 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),
):
# ...
📈 Future‑Proofing
- PKCE support: Add
code_challengevalidation in authorization and token endpoints. - JWT access tokens: Already implemented; consider adding signature algorithm configuration.
- Multiple token stores: Could replace in‑memory code store with Redis.
- OpenID Connect: Extend userinfo with standard claims, add
id_tokenissuance.
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).