A registration/re-invite link auto-established a session for any existing
active user, so re-inviting a fully set-up user acted as a passwordless
login. Invite links are for account setup only.
After consuming the token, refuse to establish a session when the target
account already has a password or WebAuthn credential. Credential-less
accounts (e.g. freshly created by initial-admin) can still complete setup.
Account recovery for set-up accounts must use a separate, authenticated flow.
Refs: porchlight-a3a
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Validation and marking-used were two separate steps, so two concurrent
requests for the same registration token could both pass validation before
either marked it used — a replay window.
Add an atomic consume() at the repository (conditional UPDATE ... WHERE
used = 0 AND not expired, gated on rowcount) and service layers, and switch
the /register handler to consume() instead of validate()+mark_used().
Refs: porchlight-ur7
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Magic-link tokens were persisted in plaintext, so a database read disclosed
usable login/invite tokens. The service now hashes tokens (HMAC-SHA256 when a
pepper is configured, else SHA-256 of the high-entropy token) and persists
only the hash; the raw token is exposed solely in the registration URL and is
re-attached to objects returned to callers.
Refs: porchlight-42h
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The /consent POST handler trusted the scope values submitted in the form,
so a forged consent submission could approve (and persist consent for)
scopes that were never part of the originating authorization request —
a scope-escalation vector.
Intersect the submitted scopes with the originally requested set stored in
the session before saving consent and completing the flow.
Refs: porchlight-a03
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
format_validation_errors interpolated Pydantic error messages directly into
HTML. Some messages echo user input (e.g. "Invalid group name '<name>'"), so
a crafted group name was reflected as raw HTML — a stored/reflected XSS.
HTML-escape each formatted message before interpolation.
Refs: porchlight-due
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The admin nav had no way back to the account management area. Add a
Profile link to /manage/profile, mirroring the Admin link already present
in the manage nav.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 6-column user table could not shrink below its content width at the
40rem main column, so the Created column was clipped past the card border.
Tighten cell padding to fit all columns, and add overflow-x: auto on the
table container as a safety net for longer content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The full Playwright suite authenticates ~100 times in a few minutes, far
over the login endpoint's 5/minute limit, so most specs failed at the
beforeEach login with 429s.
Add an OIDC_OP_RATE_LIMIT_ENABLED setting (default True) wired to the
slowapi limiter's enabled flag, and set it to false in tests/e2e/run.sh.
Production behavior is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The password setup/change form used hx-target="#password-section" with
hx-swap="innerHTML", but that div wraps the form itself. On a validation
error the route returns only an alert div, so the swap replaced the entire
form — the password inputs disappeared. Most visible during registration's
"set password" step.
Retarget the form to a dedicated #password-error div outside the form
(mirrors the working login form's #login-error pattern), so the form and
its inputs survive errors while messages still render inside #password-section.
Also fix pre-existing broken e2e tests: they omitted the required
current_password fill and used passwords below the zxcvbn strength
threshold (score 1 < MIN_PASSWORD_STRENGTH=2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Use Annotated[str, Form()] for FastAPI dependencies (FAST002)
- Add missing type annotations across src/ and tests/ (ANN001/003/201/202)
- Reduce function arguments via request.form() reads (PLR0913)
- Combine return paths to reduce return statements (PLR0911)
- Use anyio.Path for async-safe filesystem operations (ASYNC240)
- Extract constants, helpers, and dict comprehensions for clarity
- Move inline imports to top-level (PLC0415)
- Use raw strings for regex match patterns (RUF043)
- Fix redundant get_session_user call in delete_user (not-iterable)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use isinstance check instead of bool flag to help ty resolve
the current_password attribute on the validated model.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use PasswordChange model (requires current password) for users with
existing passwords and PasswordSet for first-time setup. Add zxcvbn
strength validation and current password field to credentials template.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace manual validation error formatting with shared helper in both
admin and manage profile routes. Add UsernameInput validation to invite
route and GroupListInput validation to groups route.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add hidden CSRF token inputs to admin profile, groups, and invite
forms. Add maxlength, pattern, and title attributes to invite input.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add slowapi-based rate limiting: 5/min on password login, 10/min on
WebAuthn login. Includes shared rate limiter reset fixture for tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add active-user checks to password login, WebAuthn login, and magic
link registration to prevent deactivated accounts from authenticating.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add BCP 47 locale validator to ProfileUpdate, UsernameInput model,
GroupListInput model, PasswordSet/PasswordChange with zxcvbn strength
checking, and shared format_validation_errors HTML helper.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 7 new e2e tests verifying profile form validation in both manage
and admin UIs: invalid phone number, phone normalization, E.164 hint
attributes, and admin-side email/phone/picture URL validation errors.
Fix 3 pre-existing test failures:
- Replace invalid seeded phone number (+1234567890) with valid E.164
(+12025551234) that was causing profile update tests to fail
- Update email validation error assertion to match actual pydantic
message (value_error type uses raw message, not label-prefixed)
The CSRF middleware added to main after the admin-pages branch was
created caused all admin test POSTs/DELETEs to be rejected. Add
get_csrf_token() calls and X-CSRF-Token headers to login helpers and
all mutation requests, matching the pattern used by other tests.