Hey @jayasanka @ibacher,
Following up on my earlier post — I’ve since done a deeper dive into the auth module codebase and @twiine’s thread. Rather than repeat the backend analysis, I want to focus on the frontend side that hasn’t been discussed yet.
Quick background: I’m a final-year CSE student at IIITDM Kancheepuram, currently interning at Attack Capital (YC W-22) where I’m building HealOS, a healthcare EHR platform. Day job involves Stripe billing infrastructure, AI document pipelines, and yes, I migrated our entire auth system from Clerk to Firebase, handling MFA flows, token management, and session state machines in a React SPA. That experience is directly relevant here. Been contributing to O3 since October 2025 across esm-patient-chart and esm-patient-management — @jayasanka reviewed my O3-5272 work and pushed me to rewrite it from hardcoded to template-driven, which honestly made the solution much better.
Where O3 breaks today
The login flow in openmrs-esm-core sends a Basic header to /ws/rest/v1/session and treats any 401 as “wrong credentials.” But with the auth module active, a 401 + Location header means “primary auth passed, go complete this challenge.” openmrsFetch doesn’t check for the Location header — so the user just sees a login error with no idea they need to enter a TOTP code.
How the frontend flow should work
Following @ibacher’s guidance about not exposing auth state:
openmrsFetchdetects401 + Location→ navigates SPA to that route (e.g./spa/login/totp)- Challenge component renders, user submits, POSTs JSON to REST endpoint
- Another
401 + Location? → follow it.200with session? → done, enter the app.
The frontend stays completely stateless — it just follows the backend’s Location headers. UserLogin and TwoFactorAuthenticationScheme already manage the state machine server-side.
I built something very similar at my current job — migrated auth from Clerk to Firebase in a React SPA, handling MFA intermediate states without storing sensitive auth state on the client. Same pattern, different codebase.
Forced password change
Since @ibacher noted a 401 doesn’t fit here (user IS authenticated), what about a requirePasswordChange: true flag in the session response? The login app checks it after successful auth and routes to /spa/login/change-password. No filter changes, no redirect — just a flag in a response we already read.
Backward compatibility
If there’s no Location header on a 401, everything works exactly as today. MFA routes stay dormant. Zero impact on deployments not using this module.
Questions
-
REST endpoints for TOTP/secret question JSON submission — in this module (extending PR #18’s
TotpController), or inopenmrs-module-webservices.rest? -
requirePasswordChangeflag in session response — viable approach? -
Should
401 + Locationinterception be global inopenmrsFetchor scoped to login only?