Skip to content

flask: Add internal password sync endpoint + strip password hash from API responses (2/3 split of #73) - #75

Merged
jm1021 merged 4 commits into
Open-Coding-Society:mainfrom
CSA-Admin-OCS:split/flask/f2-password-sync-and-hash-leak
Sep 10, 2026
Merged

flask: Add internal password sync endpoint + strip password hash from API responses (2/3 split of #73)#75
jm1021 merged 4 commits into
Open-Coding-Society:mainfrom
CSA-Admin-OCS:split/flask/f2-password-sync-and-hash-leak

Conversation

@RudraBJoshi

Copy link
Copy Markdown
Contributor

Splitting #73 into smaller, independently-reviewable PRs across spring/flask/pages. This one covers password database syncing, security for transfers of data between the two dbs, and a related hash-leak fix (kept together — the sync endpoint's auth check isn't meaningfully separable from the endpoint itself without landing an insecure intermediate state).

POST /api/internal/sync-password: updates a user's password by uid, called server-to-server by Spring after an OAuth-verified reset. Gated by a shared secret (INTERNAL_SYNC_KEY, compared with hmac.compare_digest for timing-safety), fails closed if unset.

Password hash no longer returned by the general user API: GET /api/user, the bulk user list, and the create/update/delete/guest-create responses were all including the PBKDF2 hash in the JSON body, to any logged-in user, not just admins. Admin-only backup/export endpoints are deliberately left alone since a restore needs the hash.

Original PR: #73

RudraBJoshi and others added 2 commits August 24, 2026 11:25
POST /api/internal/sync-password: updates a user's password by uid,
called server-to-server by the Spring backend after it completes an
OAuth + student ID verified password reset, so the same account's Flask
password doesn't drift out of sync with Spring's.

Gated by a shared secret (INTERNAL_SYNC_KEY, compared with
hmac.compare_digest for timing-safety) instead of user auth, since this
is never called from a browser -- there's no existing service-to-service
auth mechanism in this app to reuse, and reusing the admin-only
PUT /api/user route would have meant giving Spring real Flask admin
credentials. This endpoint can only ever change one user's password, and
is a closed no-op if INTERNAL_SYNC_KEY is unset.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GET /api/user (any logged-in user, not just admins) and the other UserAPI
create/update/delete responses were including the PBKDF2 hash from
User.read() in the JSON body. Adds _without_password() and applies it at
every general-purpose response site; the admin-only backup/export endpoints
in data_export_import_api.py are left alone since they need the hash for
restore fidelity.
RudraBJoshi and others added 2 commits August 31, 2026 12:01
_InternalPasswordSync always returned 200 after calling user.update(),
even though update() returns None on IntegrityError (rolled back
internally). That meant Spring could be told a sync succeeded when the
write never actually happened.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…word

jm1021 closed spring #169 (which called this endpoint) as violating the
project's architecture: Spring must never push to Flask directly. This
endpoint was the Flask side of that banned call, gated by a static
shared secret and reachable only from Spring's server.

Replaced _InternalPasswordSync with _ResetPasswordVerified: called
directly by the frontend (pages' support.md), not by Spring. It
verifies the caller's resetToken itself -- the same HMAC-SHA256-signed
token Spring's ResetCode.java issues -- using a RESET_TOKEN_SECRET both
backends are independently configured with, entirely locally (no
network call back to Spring). Flask becomes the source of truth for
this password write; the frontend syncs Spring's copy afterward by
calling Spring's /reset/oauth/complete with the same token.

Also added local single-use enforcement for the token (a small
in-process consumed-token set), since Flask has no visibility into
Spring's own single-use tracking and the old endpoint's stateless
shared-secret auth didn't need this at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@RudraBJoshi

Copy link
Copy Markdown
Contributor Author

Replaced the internal server-to-server sync endpoint with a frontend-facing one, per `jm1021`'s review on spring #169.

Why: `jm1021` closed #169 (the Spring PR that called this endpoint) as violating the project's architecture — Spring must never push a password to Flask directly. This endpoint was the Flask side of that banned call, gated by a static shared secret and reachable only from Spring's own server.

Fix: `_InternalPasswordSync` → `_ResetPasswordVerified` (`POST /api/reset-password`). Called directly by the frontend now, not by Spring. It verifies the caller's `resetToken` itself — the same HMAC-SHA256-signed token Spring's `ResetCode.java` issues — using a `RESET_TOKEN_SECRET` both backends are independently configured with, entirely locally (no network call back to Spring). Flask becomes the source of truth for this password write; the frontend syncs Spring's copy afterward by calling Spring's `/reset/oauth/complete` with the same token. Also added local single-use enforcement for the token, since Flask has no visibility into Spring's own tracking and the old shared-secret auth didn't need it.

Companion changes: spring #170 (removes the call site + `FlaskPasswordSync`), pages #1374 (calls this endpoint before Spring's).

Verified locally end-to-end: cross-language HMAC compatibility with Spring's token issuance, and a real reset through this endpoint (login with new password succeeds, old password rejected, tampered/wrong-uid/expired/replayed tokens all correctly rejected).

@jm1021
jm1021 merged commit ed4047c into Open-Coding-Society:main Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants