Lands local password auth with JWT access + refresh tokens, and starts
carrying YPMS-specific claims (`uid`, `roles`, `iam.assurance_level`,
`mfa_verified`) so downstream authorization can gate high-risk actions
above base RBAC without a DB round-trip. Follows the OWC auth pattern
verbatim except where noted.
Added:
- src/ypms/auth/security.py hash_password / verify_password
(pbkdf2_sha256), create_access_token
with YPMS-specific claims,
create_refresh_token, decode_token
- src/ypms/auth/rate_limit.py SlidingWindow (vendored from OWC),
login_limiter tuned to NIST AC-7 /
source RFP C.5.7.10: 5 failed attempts
per IP per 15 minutes → 429 lockout
- src/ypms/auth/schemas.py LoginResponse, RefreshRequest,
RefreshResponse, ChangePasswordRequest,
UserRead
- src/ypms/auth/deps.py get_current_user replaces the milestone
0.2 stub; rejects refresh tokens on
resource endpoints; refuses inactive users
- src/ypms/auth/router.py POST /auth/login (OAuth2 form; audit-logs
successful auth; DOES_SYSTEM_ADMIN role
upgrades assurance_level to `admin` in
the token)
POST /auth/refresh (issues fresh access;
refuses non-refresh tokens)
GET /auth/me
POST /auth/change-password (self-service;
≥12 char floor; audit-logged)
- src/ypms/main.py Registers auth_router
Tests (43 passing, 93.47% coverage — up from 26 / 93.10%):
- tests/auth/test_security.py Token claims shape, refresh-vs-access
separation, invalid-token handling
(5 tests)
- tests/auth/test_router.py End-to-end via TestClient with a
dependency-overridden DB: login happy
path + wrong password + inactive user,
admin assurance upgrade, /me auth-gate,
refresh happy path + reject access token,
change-password full flow + length gate,
login lockout after 5 attempts (12 tests)
Test-infra fixes:
- tests/conftest.py Env defaults moved from a session-scoped
fixture to module-import time so
`ypms.main` (which builds the app at
import) can be transitively imported by
test modules without ValidationError on
Settings.
`db` fixture switched to StaticPool +
check_same_thread=False so TestClient's
worker thread sees the same in-memory
SQLite DB the fixture set up.
Notes:
- OWC includes an /auth/register for external users; that's out of scope
here — Phase 1 lands parental co-consent and youth/host registration,
which needs its own PIA-driven flow. Keeping /auth minimal in Phase 0.
- MFA is captured in `User.mfa_verified` and JWT claim but not enforced
yet; enforcement lands as an assurance-level gate in a later phase.
- Refresh flow returns only a new access token; refresh-token rotation
is a later hardening step.
Advances YPMS-POAM-040 and unblocks milestone 0.4 (RBAC seed) and 0.5
(Registrant vertical slice).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
YPMS — Youth Program Management System
DC Department of Employment Services / Office of Youth Programs — in-house build replacing seven legacy tools (MBSYEP intake, TMS timekeeping, PF3 admin, manual payroll scripts, MicroStrategy, and two supporting apps) with a single Python / FastAPI / Postgres system on Azure Government.
Status: Pre-development. Phase 0 (foundations) in flight.
System owner + ISSO + AO: Chris Tonjes, DOES CIO.
Full context: OVERVIEW.md, ARCHITECTURE.md, PHASE0_PLAN.md.
Quickstart (local)
Requires Docker Desktop (or equivalent). Ports 8000 (app), 5433 (Postgres), 8081 (Adminer) must be free.
cp .env.example .env
docker compose up --build
App at http://localhost:8000. OpenAPI docs at http://localhost:8000/docs.
curl http://localhost:8000/healthz # → {"status":"ok"}
curl http://localhost:8000/readyz # → {"status":"ok"} once db is up
Adminer (DB explorer) at http://localhost:8081. Connect with:
- System: PostgreSQL
- Server:
db - Username / Password / Database:
ypms/ypms/ypms
Stop with Ctrl+C; wipe local DB with docker compose down -v.
Tests
Local (no Docker required for the tests):
python -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'
pytest
Coverage:
pytest --cov=src/ypms --cov-report=term-missing
Tests use SQLite in-memory for speed. Postgres-specific behaviour gets its own Postgres-only fixture when a migration or query needs it (see ARCHITECTURE.md §4.8).
Repo shape
Full tree in ARCHITECTURE.md §5.1. High points:
src/ypms/main.py— FastAPI factorysrc/ypms/settings.py— pydantic-settings configsrc/ypms/db.py— SQLAlchemy engine, session,Basesrc/ypms/logging.py— structlog + sensitive-field sanitizersrc/ypms/middleware.py— request idsrc/ypms/framework/— vendored from OWC, milestone 0.2src/ypms/security/— auth + RBAC + column encryption, milestones 0.3–0.4src/ypms/youth/— Registrant entity (vertical slice), milestone 0.5- Other entity packages — 501 stubs, milestone 0.6
migrations/— Alembic, immutable-once-mergedtests/— pytest, SQLite by default
Adding an entity
Once the framework lands in milestone 0.2, adding a new entity follows the OWC add-entity pattern (model → schemas → router → main.py registration → migration → permissions seed entry → test → coverage doc). Full instructions land here at milestone 0.2 completion.
Contributing
See CONTRIBUTING.md for the hard rules. Highlights: migrations are immutable once merged, every state-mutating route emits an audit row, framework/security modules must not carry domain logic, no --no-verify or # type: ignore without a written reason.
Compliance
SYSTEM_SECURITY_PLAN.mdv0.3 — NIST 800-53 Moderate SSPNIST_800_53_CONTROL_MAPPING.mdv0.2 — full control-family mapPRIVACY_IMPACT_ASSESSMENT.mdv0.1 — PIA covering minors, SSN, banking, FERPA, CFSAPOAM.mdv0.1 — POA&M with 62 items and 2 closures as of 2026-09-30
License / usage
Internal DOES system. Not for external distribution.