Add detailed Phase 2 (Authentication) project plan

Defines 15 tasks covering OAuth login (Google/Discord), JWT session
management, user services, and API endpoints for player authentication
at play.mantimon.com.

Key components:
- JWT utilities with access/refresh token pattern
- Redis-backed refresh token storage for revocation
- Google and Discord OAuth services
- FastAPI auth dependencies (get_current_user, etc.)
- Account linking support (multiple OAuth providers per user)
- Premium subscription tracking

Estimated: 24 hours across 1-2 weeks
This commit is contained in:
Cal Corum 2026-01-27 16:25:57 -06:00
parent b78236ac49
commit 4ddc9b8c30

View File

@ -0,0 +1,454 @@
{
"meta": {
"version": "1.0.0",
"created": "2026-01-27",
"lastUpdated": "2026-01-27",
"planType": "phase",
"phaseId": "PHASE_2",
"phaseName": "Authentication",
"description": "OAuth login (Google, Discord), JWT session management, user management, premium tier tracking",
"totalEstimatedHours": 24,
"totalTasks": 15,
"completedTasks": 0,
"masterPlan": "../PROJECT_PLAN_MASTER.json"
},
"goals": [
"Implement OAuth 2.0 authentication with Google and Discord providers",
"Create JWT-based session management with access/refresh token pattern",
"Build user management service with create, read, update operations",
"Implement FastAPI dependencies for protected endpoints",
"Support account linking (multiple OAuth providers per user)",
"Track premium subscription status with expiration dates"
],
"architectureNotes": {
"tokenStrategy": {
"accessToken": "Short-lived JWT (30 min default), contains user_id",
"refreshToken": "Longer-lived JWT (7 days default), stored in Redis for revocation",
"storage": "Refresh tokens tracked in Redis for logout/revocation support"
},
"oauthFlow": {
"pattern": "Authorization Code Flow with PKCE",
"callback": "Backend receives code, exchanges for tokens, creates/updates user",
"security": "Never store OAuth provider tokens, only OAuth ID"
},
"accountLinking": {
"strategy": "Email-based matching",
"flow": "If user exists with same email, add OAuth provider to existing account"
},
"existingInfrastructure": {
"config": "JWT and OAuth settings already in app/config.py Settings class",
"dependencies": "python-jose, passlib, bcrypt already installed",
"userModel": "User model with OAuth fields already in app/db/models/user.py"
}
},
"directoryStructure": {
"schemas": "backend/app/schemas/",
"services": "backend/app/services/",
"api": "backend/app/api/",
"tests": "backend/tests/api/, backend/tests/services/"
},
"tasks": [
{
"id": "AUTH-001",
"name": "Create Pydantic schemas for auth",
"description": "Define request/response models for authentication flows",
"category": "critical",
"priority": 1,
"completed": false,
"dependencies": [],
"files": [
{"path": "app/schemas/__init__.py", "status": "pending"},
{"path": "app/schemas/auth.py", "status": "pending"},
{"path": "app/schemas/user.py", "status": "pending"}
],
"details": [
"TokenPayload: sub (user_id), exp, iat, type (access/refresh)",
"TokenResponse: access_token, refresh_token, token_type, expires_in",
"UserResponse: id, email, display_name, avatar_url, is_premium, premium_until",
"UserCreate: internal model for user creation from OAuth",
"OAuthUserInfo: normalized structure for OAuth provider data",
"AccountLinkRequest: for linking additional OAuth providers"
],
"estimatedHours": 1.5
},
{
"id": "AUTH-002",
"name": "Create JWT utilities service",
"description": "Functions for creating and verifying JWT tokens",
"category": "critical",
"priority": 2,
"completed": false,
"dependencies": ["AUTH-001"],
"files": [
{"path": "app/services/jwt_service.py", "status": "pending"}
],
"details": [
"create_access_token(user_id: UUID) -> str - Uses settings.jwt_expire_minutes",
"create_refresh_token(user_id: UUID) -> str - Uses settings.jwt_refresh_expire_days",
"decode_token(token: str) -> TokenPayload - Validates and decodes",
"verify_token(token: str) -> UUID | None - Returns user_id or None if invalid",
"Uses python-jose with HS256 algorithm",
"All timing uses datetime.now(UTC) per project standards"
],
"estimatedHours": 1.5
},
{
"id": "AUTH-003",
"name": "Create refresh token Redis storage",
"description": "Redis-based storage for refresh token tracking and revocation",
"category": "high",
"priority": 3,
"completed": false,
"dependencies": ["AUTH-002"],
"files": [
{"path": "app/services/token_store.py", "status": "pending"}
],
"details": [
"Key format: refresh_token:{user_id}:{jti} -> expiration timestamp",
"store_refresh_token(user_id, jti, expires_at) - Store with TTL",
"is_token_valid(user_id, jti) -> bool - Check if not revoked",
"revoke_token(user_id, jti) - Delete specific token",
"revoke_all_user_tokens(user_id) - Logout from all devices",
"Uses existing Redis connection from app/db/redis.py"
],
"estimatedHours": 1.5
},
{
"id": "AUTH-004",
"name": "Create UserService",
"description": "Service layer for user CRUD operations",
"category": "critical",
"priority": 4,
"completed": false,
"dependencies": ["AUTH-001"],
"files": [
{"path": "app/services/user_service.py", "status": "pending"}
],
"details": [
"get_user_by_id(db, user_id: UUID) -> User | None",
"get_user_by_email(db, email: str) -> User | None",
"get_user_by_oauth(db, provider: str, oauth_id: str) -> User | None",
"create_user(db, user_data: UserCreate) -> User",
"update_last_login(db, user_id: UUID) -> None",
"link_oauth_provider(db, user_id: UUID, provider: str, oauth_id: str) -> User",
"update_premium_status(db, user_id: UUID, premium_until: datetime | None) -> User",
"All operations are async using SQLAlchemy async session"
],
"estimatedHours": 2
},
{
"id": "AUTH-005",
"name": "Create OAuthLinkedAccount model",
"description": "Database model for multiple OAuth providers per user (account linking)",
"category": "high",
"priority": 5,
"completed": false,
"dependencies": [],
"files": [
{"path": "app/db/models/oauth_account.py", "status": "pending"},
{"path": "app/db/migrations/versions/xxx_add_oauth_accounts.py", "status": "pending"}
],
"details": [
"Fields: id, user_id (FK), provider, oauth_id, linked_at",
"Unique constraint on (provider, oauth_id)",
"Migrate existing User.oauth_provider/oauth_id to this table",
"Keep User.oauth_provider/oauth_id as 'primary' for backward compat",
"Relationship: User.linked_accounts -> list[OAuthLinkedAccount]"
],
"estimatedHours": 2
},
{
"id": "AUTH-006",
"name": "Create Google OAuth service",
"description": "Handle Google OAuth authorization code flow",
"category": "critical",
"priority": 6,
"completed": false,
"dependencies": ["AUTH-004"],
"files": [
{"path": "app/services/oauth/google.py", "status": "pending"},
{"path": "app/services/oauth/__init__.py", "status": "pending"}
],
"details": [
"get_authorization_url(redirect_uri, state) -> str",
"exchange_code_for_tokens(code, redirect_uri) -> GoogleTokens",
"get_user_info(access_token) -> OAuthUserInfo",
"Uses httpx for async HTTP requests",
"Validates state parameter to prevent CSRF",
"Google OAuth endpoints: accounts.google.com/o/oauth2/v2/auth, oauth2.googleapis.com/token",
"User info endpoint: www.googleapis.com/oauth2/v2/userinfo"
],
"estimatedHours": 2
},
{
"id": "AUTH-007",
"name": "Create Discord OAuth service",
"description": "Handle Discord OAuth authorization code flow",
"category": "critical",
"priority": 7,
"completed": false,
"dependencies": ["AUTH-004"],
"files": [
{"path": "app/services/oauth/discord.py", "status": "pending"}
],
"details": [
"get_authorization_url(redirect_uri, state) -> str",
"exchange_code_for_tokens(code, redirect_uri) -> DiscordTokens",
"get_user_info(access_token) -> OAuthUserInfo",
"Uses httpx for async HTTP requests",
"Discord OAuth endpoints: discord.com/api/oauth2/authorize, discord.com/api/oauth2/token",
"User info endpoint: discord.com/api/users/@me",
"Avatar URL construction from user ID and avatar hash"
],
"estimatedHours": 2
},
{
"id": "AUTH-008",
"name": "Create FastAPI auth dependencies",
"description": "Dependency injection for protected endpoints",
"category": "critical",
"priority": 8,
"completed": false,
"dependencies": ["AUTH-002", "AUTH-003", "AUTH-004"],
"files": [
{"path": "app/api/__init__.py", "status": "pending"},
{"path": "app/api/deps.py", "status": "pending"}
],
"details": [
"OAuth2PasswordBearer scheme for token extraction",
"get_current_user(token) -> User - Validates token, fetches user",
"get_current_active_user() -> User - Ensures user exists and is active",
"get_current_premium_user() -> User - Requires active premium subscription",
"get_optional_user() -> User | None - For endpoints that work with/without auth",
"Proper error responses: 401 Unauthorized, 403 Forbidden"
],
"estimatedHours": 1.5
},
{
"id": "AUTH-009",
"name": "Create auth API router",
"description": "REST endpoints for OAuth login, token refresh, logout",
"category": "critical",
"priority": 9,
"completed": false,
"dependencies": ["AUTH-006", "AUTH-007", "AUTH-008"],
"files": [
{"path": "app/api/auth.py", "status": "pending"}
],
"details": [
"GET /auth/google - Redirects to Google OAuth consent screen",
"GET /auth/google/callback - Handles OAuth callback, returns tokens",
"GET /auth/discord - Redirects to Discord OAuth consent screen",
"GET /auth/discord/callback - Handles OAuth callback, returns tokens",
"POST /auth/refresh - Exchange refresh token for new access token",
"POST /auth/logout - Revoke refresh token",
"State parameter stored in Redis with short TTL for CSRF protection"
],
"estimatedHours": 3
},
{
"id": "AUTH-010",
"name": "Create user API router",
"description": "REST endpoints for user profile and account management",
"category": "high",
"priority": 10,
"completed": false,
"dependencies": ["AUTH-008", "AUTH-004"],
"files": [
{"path": "app/api/users.py", "status": "pending"}
],
"details": [
"GET /users/me - Get current user profile",
"PATCH /users/me - Update display_name, avatar_url",
"GET /users/me/linked-accounts - List linked OAuth providers",
"POST /users/me/link/{provider} - Start account linking flow",
"DELETE /users/me/link/{provider} - Unlink OAuth provider (if not last)",
"All endpoints require authentication via get_current_user dependency"
],
"estimatedHours": 2
},
{
"id": "AUTH-011",
"name": "Integrate routers in main.py",
"description": "Mount auth and user routers, add any required middleware",
"category": "high",
"priority": 11,
"completed": false,
"dependencies": ["AUTH-009", "AUTH-010"],
"files": [
{"path": "app/main.py", "status": "pending"}
],
"details": [
"Include auth router: prefix='/api/auth', tags=['auth']",
"Include users router: prefix='/api/users', tags=['users']",
"Remove TODO comments for router integration",
"Verify CORS allows credentials for token cookies (if used)"
],
"estimatedHours": 0.5
},
{
"id": "AUTH-012",
"name": "Create JWT service tests",
"description": "Unit tests for token creation and verification",
"category": "high",
"priority": 12,
"completed": false,
"dependencies": ["AUTH-002"],
"files": [
{"path": "tests/services/test_jwt_service.py", "status": "pending"}
],
"details": [
"Test create_access_token returns valid JWT",
"Test create_refresh_token returns valid JWT with correct type",
"Test decode_token extracts correct payload",
"Test verify_token returns None for expired tokens",
"Test verify_token returns None for invalid signatures",
"Test token expiration times are correct"
],
"estimatedHours": 1.5
},
{
"id": "AUTH-013",
"name": "Create UserService tests",
"description": "Integration tests for user CRUD operations",
"category": "high",
"priority": 13,
"completed": false,
"dependencies": ["AUTH-004"],
"files": [
{"path": "tests/services/test_user_service.py", "status": "pending"}
],
"details": [
"Test get_user_by_id returns user or None",
"Test get_user_by_oauth finds by provider+oauth_id",
"Test create_user creates with correct fields",
"Test update_last_login updates timestamp",
"Test link_oauth_provider adds linked account",
"Test premium status update",
"Uses real Postgres via testcontainers pattern"
],
"estimatedHours": 2
},
{
"id": "AUTH-014",
"name": "Create OAuth service tests",
"description": "Unit tests for OAuth flows with mocked HTTP",
"category": "high",
"priority": 14,
"completed": false,
"dependencies": ["AUTH-006", "AUTH-007"],
"files": [
{"path": "tests/services/oauth/test_google.py", "status": "pending"},
{"path": "tests/services/oauth/test_discord.py", "status": "pending"}
],
"details": [
"Mock httpx responses for token exchange",
"Mock httpx responses for user info",
"Test authorization URL construction",
"Test error handling for invalid codes",
"Test OAuthUserInfo normalization",
"Uses respx or pytest-httpx for mocking"
],
"estimatedHours": 2
},
{
"id": "AUTH-015",
"name": "Create auth API endpoint tests",
"description": "Integration tests for auth endpoints",
"category": "high",
"priority": 15,
"completed": false,
"dependencies": ["AUTH-009", "AUTH-010"],
"files": [
{"path": "tests/api/__init__.py", "status": "pending"},
{"path": "tests/api/conftest.py", "status": "pending"},
{"path": "tests/api/test_auth.py", "status": "pending"},
{"path": "tests/api/test_users.py", "status": "pending"}
],
"details": [
"Test OAuth redirect returns correct URL",
"Test callback with mocked OAuth creates user and returns tokens",
"Test refresh endpoint returns new access token",
"Test logout revokes refresh token",
"Test /users/me returns current user",
"Test /users/me update works",
"Uses TestClient with dependency overrides"
],
"estimatedHours": 3
}
],
"testingStrategy": {
"approach": "Unit tests for services, integration tests for API endpoints",
"mocking": "httpx responses mocked for OAuth providers, fakeredis for token store",
"database": "Real Postgres via testcontainers for UserService tests",
"coverage": "Target 90%+ coverage on new auth code"
},
"weeklyRoadmap": {
"week1": {
"theme": "Core Services",
"tasks": ["AUTH-001", "AUTH-002", "AUTH-003", "AUTH-004", "AUTH-005"],
"goals": ["Schemas defined", "JWT working", "UserService complete"]
},
"week2": {
"theme": "OAuth + API",
"tasks": ["AUTH-006", "AUTH-007", "AUTH-008", "AUTH-009", "AUTH-010", "AUTH-011"],
"goals": ["OAuth flows working", "API endpoints complete", "Integration done"]
},
"week3": {
"theme": "Testing",
"tasks": ["AUTH-012", "AUTH-013", "AUTH-014", "AUTH-015"],
"goals": ["Full test coverage", "All tests passing"]
}
},
"acceptanceCriteria": [
{"criterion": "User can login with Google OAuth and receive JWT tokens", "met": false},
{"criterion": "User can login with Discord OAuth and receive JWT tokens", "met": false},
{"criterion": "Access tokens expire after configured time", "met": false},
{"criterion": "Refresh tokens can be used to get new access tokens", "met": false},
{"criterion": "Logout revokes refresh token (cannot be reused)", "met": false},
{"criterion": "Protected endpoints return 401 without valid token", "met": false},
{"criterion": "User can link multiple OAuth providers to one account", "met": false},
{"criterion": "Premium status is tracked with expiration date", "met": false},
{"criterion": "All tests pass with high coverage", "met": false}
],
"securityConsiderations": [
"OAuth state parameter validated to prevent CSRF attacks",
"OAuth provider tokens never stored (only OAuth ID)",
"JWT secret key loaded from environment, never hardcoded",
"Refresh tokens stored in Redis with TTL for revocation support",
"Access tokens short-lived (30 min) to limit exposure",
"Rate limiting on auth endpoints (future enhancement)",
"HTTPS required in production for all auth endpoints"
],
"dependencies": {
"existing": [
"python-jose>=3.5.0 (already installed)",
"passlib>=1.7.4 (already installed)",
"bcrypt>=5.0.0 (already installed)"
],
"toAdd": [
"httpx>=0.25.0 (async HTTP client for OAuth)",
"respx>=0.20.0 (httpx mocking for tests)"
]
},
"phase1Prerequisites": {
"met": [
"JWT configuration in Settings class",
"OAuth configuration in Settings class",
"User model with OAuth fields",
"python-jose dependency installed",
"Database session management",
"Redis connection utilities"
]
}
}