feat: implement auth, projects, and frontend foundation
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
# Auth OAuth Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Implement Authentik-backed OIDC login that issues internal JWT access tokens, rotates DB-backed refresh tokens, and supports secure logout.
|
||||
|
||||
**Architecture:** FastAPI route handlers delegate to focused auth services: OIDC provider client, token verifier/minting service, and refresh token store. Session state is carried in httpOnly cookies while refresh-token validity is enforced from PostgreSQL. Token trust boundary is explicit: provider token is JWKS-verified before local token minting.
|
||||
|
||||
**Tech Stack:** FastAPI, SQLAlchemy (async), Alembic, python-jose, httpx, pytest/pytest-asyncio, ruff, mypy
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Config and DB schema
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/api/src/config.py`
|
||||
- Modify: `apps/api/src/models/user.py`
|
||||
- Create: `apps/api/src/models/refresh_token.py`
|
||||
- Modify: `apps/api/src/models/__init__.py`
|
||||
- Create: `apps/api/alembic/versions/0002_refresh_tokens.py`
|
||||
- Test: `apps/api/tests/test_config.py`
|
||||
- Test: `apps/api/tests/test_models.py`
|
||||
|
||||
- [ ] **Step 1: Write failing tests for OIDC/JWT config and refresh-token metadata**
|
||||
- [ ] **Step 2: Run focused tests to verify red state**
|
||||
- [ ] **Step 3: Implement minimal config and model changes**
|
||||
- [ ] **Step 4: Add migration and migration metadata test updates**
|
||||
- [ ] **Step 5: Re-run focused tests to verify green state**
|
||||
|
||||
### Task 2: Auth services
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/api/src/auth/__init__.py`
|
||||
- Create: `apps/api/src/auth/cookies.py`
|
||||
- Create: `apps/api/src/auth/oidc.py`
|
||||
- Create: `apps/api/src/auth/jwt_service.py`
|
||||
- Create: `apps/api/src/auth/refresh_store.py`
|
||||
- Test: `apps/api/tests/test_auth_services.py`
|
||||
|
||||
- [ ] **Step 1: Write failing tests for cookie policy, JWT mint/verify, and refresh lifecycle**
|
||||
- [ ] **Step 2: Run targeted tests to verify failures are expected**
|
||||
- [ ] **Step 3: Implement minimal auth service modules to satisfy tests**
|
||||
- [ ] **Step 4: Re-run tests and iterate until green**
|
||||
|
||||
### Task 3: Auth API routes
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/api/src/main.py`
|
||||
- Create: `apps/api/src/api/__init__.py`
|
||||
- Create: `apps/api/src/api/auth.py`
|
||||
- Test: `apps/api/tests/test_auth_api.py`
|
||||
|
||||
- [ ] **Step 1: Write failing API tests for `/auth/login`, `/auth/callback`, `/auth/refresh`, `/auth/logout`, `/auth/me`**
|
||||
- [ ] **Step 2: Run targeted API tests to confirm red state**
|
||||
- [ ] **Step 3: Implement minimal route handlers and dependency wiring**
|
||||
- [ ] **Step 4: Re-run API tests until green**
|
||||
|
||||
### Task 4: Verification and OpenSpec updates
|
||||
|
||||
**Files:**
|
||||
- Modify: `openspec/changes/auth-oauth/tasks.md`
|
||||
|
||||
- [ ] **Step 1: Run full checks: `pytest`, `ruff check src tests`, `mypy src`**
|
||||
- [ ] **Step 2: Run migration against local Postgres and verify current revision**
|
||||
- [ ] **Step 3: Mark completed checkboxes and capture any blockers in OpenSpec tasks**
|
||||
@@ -0,0 +1,79 @@
|
||||
# Database Models Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build the backend database foundation for Headquarter with SQLAlchemy 2.0 async models, Alembic migrations, and development seed data.
|
||||
|
||||
**Architecture:** Add a minimal FastAPI backend package under `apps/api/src` with one shared declarative base, one async database/session module, and focused model modules for each core entity. Drive the work from tests that assert schema metadata and relationship wiring first, then add Alembic and seeding on top.
|
||||
|
||||
**Tech Stack:** Python 3.11, SQLAlchemy 2.x, asyncpg, Alembic, pytest, pytest-asyncio, Pydantic Settings, PostgreSQL JSONB/UUID types
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Backend package skeleton and settings
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/api/src/__init__.py`
|
||||
- Create: `apps/api/src/config.py`
|
||||
- Create: `apps/api/src/database.py`
|
||||
- Test: `apps/api/tests/test_config.py`
|
||||
|
||||
- [ ] Step 1: Write a failing test for configuration defaults and async engine URL expectations.
|
||||
- [ ] Step 2: Run the focused config test and confirm it fails because the module does not exist.
|
||||
- [ ] Step 3: Add minimal settings and async session factory implementation.
|
||||
- [ ] Step 4: Run the focused config test and confirm it passes.
|
||||
|
||||
### Task 2: Declarative base and shared timestamp/UUID columns
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/api/src/models/__init__.py`
|
||||
- Create: `apps/api/src/models/base.py`
|
||||
- Test: `apps/api/tests/test_models.py`
|
||||
|
||||
- [ ] Step 1: Write a failing metadata test that imports the base and asserts mapped tables can inherit UUID/timestamp columns.
|
||||
- [ ] Step 2: Run the focused model test and confirm it fails.
|
||||
- [ ] Step 3: Implement the declarative base plus reusable UUID/timestamp mixins.
|
||||
- [ ] Step 4: Re-run the focused model test and confirm it passes.
|
||||
|
||||
### Task 3: Core entity models and relationships
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/api/src/models/user.py`
|
||||
- Create: `apps/api/src/models/project.py`
|
||||
- Create: `apps/api/src/models/git_repository.py`
|
||||
- Create: `apps/api/src/models/ssh_key.py`
|
||||
- Create: `apps/api/src/models/user_config.py`
|
||||
- Modify: `apps/api/src/models/__init__.py`
|
||||
- Test: `apps/api/tests/test_models.py`
|
||||
|
||||
- [ ] Step 1: Write failing tests that assert the five tables exist, required columns are present, and the expected relationships are wired.
|
||||
- [ ] Step 2: Run the focused model tests and confirm they fail because the models are missing.
|
||||
- [ ] Step 3: Implement the minimal models to satisfy the spec, including PostgreSQL UUID/JSONB fields and foreign keys.
|
||||
- [ ] Step 4: Re-run the focused model tests and confirm they pass.
|
||||
|
||||
### Task 4: Alembic integration and initial migration
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/api/alembic.ini`
|
||||
- Create: `apps/api/alembic/env.py`
|
||||
- Create: `apps/api/alembic/script.py.mako`
|
||||
- Create: `apps/api/alembic/versions/0001_initial_schema.py`
|
||||
- Test: `apps/api/tests/test_migration_metadata.py`
|
||||
|
||||
- [ ] Step 1: Write a failing test that imports model metadata and asserts the initial migration covers all expected tables.
|
||||
- [ ] Step 2: Run the focused migration test and confirm it fails.
|
||||
- [ ] Step 3: Add minimal Alembic configuration plus an initial migration that creates all core tables.
|
||||
- [ ] Step 4: Re-run the focused migration test and confirm it passes.
|
||||
|
||||
### Task 5: Seed data and verification
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/api/scripts/seed.py`
|
||||
- Modify: `openspec/changes/database-models/tasks.md`
|
||||
- Test: `apps/api/tests/test_seed.py`
|
||||
|
||||
- [ ] Step 1: Write a failing test that verifies the seed module builds a deterministic development user payload.
|
||||
- [ ] Step 2: Run the focused seed test and confirm it fails.
|
||||
- [ ] Step 3: Implement the minimal seed helpers and script entrypoint.
|
||||
- [ ] Step 4: Re-run the focused seed test and confirm it passes.
|
||||
- [ ] Step 5: Mark completed OpenSpec checklist items and run the targeted verification commands.
|
||||
@@ -0,0 +1,170 @@
|
||||
# Auth OAuth Design
|
||||
|
||||
## Goal
|
||||
|
||||
Implement OAuth2/OIDC authentication via Authentik with internal JWT access tokens, DB-backed refresh token rotation, secure cookie handling, and explicit logout revocation.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
- Authentik code flow callback handling
|
||||
- Authentik token verification via JWKS
|
||||
- Internal JWT minting and validation
|
||||
- Refresh token persistence and rotation
|
||||
- Logout revocation and cookie clearing
|
||||
- Cookie policy split for dev vs production
|
||||
|
||||
Out of scope:
|
||||
- RBAC policy engine
|
||||
- Multi-device session management UI
|
||||
- Social providers beyond existing Authentik setup
|
||||
|
||||
## Chosen Approach
|
||||
|
||||
Use full OIDC callback exchange with JWKS verification, then mint an internal short-lived JWT and store opaque refresh tokens server-side.
|
||||
|
||||
Why this approach:
|
||||
- Keeps trust boundary explicit (no blind trust of exchange payload)
|
||||
- Allows immediate refresh-token revocation on logout
|
||||
- Decouples internal auth contract from external provider claim shape
|
||||
|
||||
## Defaults
|
||||
|
||||
- Access token TTL: 15 minutes
|
||||
- Refresh token TTL: 7 days
|
||||
- Cookie mode:
|
||||
- Production: `Secure=true`, `SameSite=strict`, `httpOnly=true`
|
||||
- Local development: `Secure=false`, `SameSite=lax`, `httpOnly=true`
|
||||
|
||||
## Architecture
|
||||
|
||||
1. Frontend calls `GET /auth/login`.
|
||||
2. Backend redirects to Authentik authorize endpoint.
|
||||
3. Authentik redirects to backend callback with `code`.
|
||||
4. Backend exchanges `code` for Authentik tokens.
|
||||
5. Backend validates Authentik access token using Authentik JWKS (`iss`, `aud`, `exp`, signature).
|
||||
6. Backend maps claims to local user record (create/update by `authentik_id`).
|
||||
7. Backend mints internal access JWT and opaque refresh token.
|
||||
8. Backend stores hashed refresh token in database and sets cookies.
|
||||
9. Protected endpoints validate internal access JWT.
|
||||
10. `POST /auth/refresh` rotates refresh token and issues new access JWT.
|
||||
11. `POST /auth/logout` revokes refresh token and clears cookies.
|
||||
|
||||
## Data Model
|
||||
|
||||
Add a `refresh_tokens` table:
|
||||
|
||||
- `id`: UUID primary key
|
||||
- `user_id`: UUID foreign key -> `users.id`
|
||||
- `token_hash`: string (hash of opaque refresh token; never store raw token)
|
||||
- `expires_at`: timestamp with timezone
|
||||
- `created_at`: timestamp with timezone
|
||||
- `revoked_at`: timestamp with timezone, nullable
|
||||
- `user_agent`: string, nullable
|
||||
- `ip_address`: string, nullable
|
||||
|
||||
Indexes:
|
||||
- unique index on `token_hash`
|
||||
- index on `user_id`
|
||||
- index on `expires_at`
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### `GET /auth/login`
|
||||
|
||||
- Redirects to Authentik authorize URL with state and nonce.
|
||||
|
||||
### `GET /auth/callback`
|
||||
|
||||
- Validates state.
|
||||
- Exchanges code at Authentik token endpoint.
|
||||
- Verifies Authentik access token via JWKS.
|
||||
- Upserts local user.
|
||||
- Mints internal access JWT + opaque refresh token.
|
||||
- Persists hashed refresh token record.
|
||||
- Sets `access_token` and `refresh_token` cookies.
|
||||
|
||||
### `POST /auth/refresh`
|
||||
|
||||
- Reads `refresh_token` cookie.
|
||||
- Hashes and finds matching non-revoked, non-expired DB row.
|
||||
- If valid, revokes old row and creates a new row (rotation).
|
||||
- Mints new internal access JWT and new opaque refresh token.
|
||||
- Sets rotated cookies.
|
||||
|
||||
### `POST /auth/logout`
|
||||
|
||||
- Reads refresh cookie if present.
|
||||
- Revokes corresponding DB token row.
|
||||
- Clears access and refresh cookies.
|
||||
|
||||
### `GET /auth/me`
|
||||
|
||||
- Validates internal access JWT.
|
||||
- Returns current user payload.
|
||||
|
||||
## Token Strategy
|
||||
|
||||
### Internal access JWT
|
||||
|
||||
Claims:
|
||||
- `sub`: local user id
|
||||
- `email`
|
||||
- `name`
|
||||
- `roles` (optional, if available)
|
||||
- `iat`, `exp`
|
||||
|
||||
Signing:
|
||||
- Use configured backend signing secret/algorithm.
|
||||
|
||||
### Refresh token
|
||||
|
||||
- Opaque, random, high-entropy value
|
||||
- Hashed before persistence
|
||||
- Rotated on each refresh
|
||||
- Revoked on logout and on detected reuse
|
||||
|
||||
## Security Rules
|
||||
|
||||
- Never expose token contents to frontend JS (httpOnly cookies only).
|
||||
- Validate Authentik token signature and critical claims before minting local JWT.
|
||||
- Enforce strict cookie attributes by environment.
|
||||
- Log security-relevant events with safe redaction.
|
||||
- Return generic auth errors to clients; keep details in server logs.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Invalid code exchange -> `401`
|
||||
- JWKS verification failure -> `401`
|
||||
- Missing/invalid refresh cookie -> `401`
|
||||
- Revoked/expired refresh token -> `401`
|
||||
- Reuse detection (if token already rotated/revoked) -> revoke chain and force login
|
||||
|
||||
Error body shape:
|
||||
- stable machine-readable code
|
||||
- non-sensitive message
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Unit tests:
|
||||
- cookie option builder (dev vs prod)
|
||||
- Authentik token verification helper
|
||||
- internal JWT mint/verify helpers
|
||||
- refresh hash + rotation logic
|
||||
|
||||
Integration tests:
|
||||
- callback creates/updates user and sets cookies
|
||||
- refresh rotates token and invalidates previous token
|
||||
- logout revokes token and clears cookies
|
||||
- protected endpoint rejects invalid/expired JWT
|
||||
|
||||
Quality gates:
|
||||
- `pytest` passes
|
||||
- `mypy .` passes
|
||||
- `ruff check .` passes
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Keep auth logic in focused modules (provider client, jwt service, refresh store, route handlers).
|
||||
- Keep DB writes idempotent where feasible (user upsert path).
|
||||
- Keep changes scoped to auth-oauth and required schema support.
|
||||
Reference in New Issue
Block a user