Seeking guidance: "Integrate O3 with the Authentication Module"

Hi everyone,

I’m exploring the GSoC project to integrate O3 with the authentication module mentored by @Jayasanka. I’ve been going through the codebase and wanted to share what I’m thinking and get some guidance before I go further.

What I understand so far

The module works well for 2.x — TOTP, 2FA, secret question, force-password-change all work through JSP pages served by openmrs-module-authenticationui. The problem with O3 is that when authentication is incomplete, AuthenticationFilter redirects to those JSP challengeUrl pages (e.g. /loginTotp.page), which O3 simply can’t render.

The approach I’m thinking about

I noticed that AuthenticationFilter already has this special handling for the session endpoint:

if (WebUtil.matchesPath(request, "/ws/rest/*/session")) {
    response.setHeader("Location", challengeUrl);
}

This is essentially what the project description calls a “fake redirect” — instead of a 3xx redirect, it’s a 401 with a Location header that this section of openmrsFetch reads and uses to navigate the SPA. So the trigger mechanism is already there, the challengeUrl just needs to point to O3 SPA routes.

Backend changes I think are needed

  1. Make challengeUrl values configurable to point to O3 SPA routes. Since loginPage is already a config property on each scheme (e.g. authentication.scheme.totp.config.loginPage), this could be as simple as letting implementers set it to /spa/login instead of /loginTotp.page. Existing 2.x setups like PIH’s would be unaffected since defaults stay the same.

  2. Extend the AuthenticationFilter so that the 401 + Location behavior applies not just to /ws/rest/*/session but also to other REST calls that happen while auth is incomplete — so the SPA always gets redirected correctly regardless of which endpoint triggered the auth check.

  3. Add new REST endpoints to accept TOTP codes and secret question answers. Right now credentials are submitted via JSP form POST, which O3 can’t do. The frontend needs a REST endpoint to POST the TOTP code to and get back a completed session.

  4. Possibly expose the current auth state (e.g. “primary factor done, TOTP pending”) in the session endpoint response so the O3 frontend knows which step to render.

On backward compatibility

Since all the challengeUrl values are driven by runtime properties and defaults stay unchanged, I believe existing 2.x setups would be completely unaffected. O3 behavior would be opt-in via config. Does that hold up in practice?

What I’m still thinking through

  1. The TwoFactorAuthenticationScheme orchestrates primary + secondary auth across multiple requests, storing intermediate state in the HttpSession. When O3 submits credentials via REST, the session state needs to persist between the primary auth call and the secondary (TOTP/secret question) call. Is the current HttpSession-based state management in UserLogin sufficient for this, or does it need changes to work reliably with a stateless REST flow?

  2. For the multi-step auth flow (e.g. username/password → TOTP), the frontend needs to know after the first step that primary auth succeeded but a second factor is still required, and specifically which factor (TOTP vs secret question — since that’s per-user via authentication.secondaryType user property). Should this state be communicated through the Location header URL (e.g. /spa/login?step=totp), or should the session endpoint response body carry this information explicitly?

  3. The ForcePasswordChangeFilter runs as a separate filter after authentication succeeds. Its redirect currently goes to a JSP page. Should the O3-compatible redirect from this filter follow the same 401 + Location pattern as AuthenticationFilter, or is a different approach more appropriate here since the user is already authenticated at that point?

Would really appreciate any guidance here. Thanks a lot!

CC @mseaton @jayasanka @ruhanga @mogoodrich @dkayiwa @ibacher @raff

1 Like

Don’t do this one. It’s bad security hygiene.

Yep

Instead of thinking about it as “communicating which step”, I think it’s cleaner if the backend simply tells the frontend “send the user to this page”. If it’s the same page, then we can interpret it as an authentication failure.

the same 401 + Location pattern as AuthenticationFilter, or is a different approach more appropriate here since the user is already authenticated at that point?

I’d still send the redirect, but probably not the 401 if it’s avoidable.

Thanks @ibacher

Thanks @ibacher ! Are these points a solid foundation for this work as far as this authentication module is concerned?

Hey! I’m Shubhangi, applying for GSoC 2026 (Integrate O3 with the Authentication Module). I just opened PR #29 on openmrs-module-authentication, it’s a small focused chamge: extending the Location header injection in AuthenticationFilter from just /ws/rest/*/session to all /ws/rest/** calls, so O3 doesn’t get a JSP redirect when auth is incomplete on any REST endpoint. Follows the approach @ibacher and @mseaton discussed in PR #18. Would really appreciate a look when you get a chance! Feat(filter): inject Location header for all unauthenticated REST calls, not just/session by shubhangiisinghh · Pull Request #29 · openmrs/openmrs-module-authentication · GitHub

(REPOSTING as I had to make a new talk account ) Hey! I’m Shubhangi, applying for GSoC 2026 (Integrate O3 with the Authentication Module). I just opened PR #29 on openmrs-module-authentication, it’s a small focused chamge: extending the Location header injection in AuthenticationFilter from just /ws/rest/*/session to all /ws/rest/** calls, so O3 doesn’t get a JSP redirect when auth is incomplete on any REST endpoint. Follows the approach @ibacher and @mseaton discussed in PR #18. Would really appreciate a look when you get a chance! https://github.com/openmrs/openmrs-module-authentication/pull/29