feat(auth): standalone username/password login when no bunyip is configured (MAPPS-368) #426
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/MAPPS-368-standalone-login"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
When a deployment has no bunyip/OIDC issuer configured, the SPA now presents a standalone username/password login form (against mokosh-server's existing
POST /api/v1/auth/login) instead of redirecting to a dead/oauth2/authorize. Per MAPPS-368. Deployments that DO configure an issuer are unchanged. mokosh-server needs no code changes.Root cause
start_login(src/modules/oidc/flow.rs:57) buildsformat!("{issuer}/oauth2/authorize"), and bothLogin(lib.rs) andAuthGuardnavigate there. With no bunyip,OidcConfigfell back to the compile-time default issuerhttp://localhost:8080, so login hit a relative-ish/oauth2/authorizethat nothing serves, rendering the SPA's own 404.Change (SPA-only)
OidcConfig::from_env's issuer default is now empty;OidcConfig::has_issuer()reports whether a real issuer resolved (runtime-injectedMOKOSH_OIDC_ISSUER, or anmsp.<tld>host that derives one);oidc::is_standalone()wraps it. Only a truly-unconfigured deploy flips to standalone, so the compile default no longer masks "no bunyip".pages/login.rs, mirrors the onboarding form): posts via a new unauthedapi::post_typed; maps 401 to "invalid email or password", 429 to a rate-limit message, andmfa_requiredto "sign in through your identity provider". On success it seeds the access token +AuthContext(like the OIDC callback) and persists aStandaloneSession.AuthGuardroutes an unauthenticated user to/loginwhen standalone;Loginrenders the form when standalone, else firesstart_loginunchanged.StandaloneSessionis stored under its own key, kept separate from the id-token-keyedStoredTokens, so the two rehydrate paths never collide.initial_auth_contexttries the OIDC rehydrate first, then standalone (additive).clear_authnow also drops the standalone session so logout is complete.Deferred (follow-ups)
/api/v1/auth/refresh- standalone sessions currently last the access-token TTL (~1h). Deliberately not touching the shared OIDC refresh hook.First-user note
A fresh self-hosted DB has no users until
ADMIN_EMAIL/ADMIN_PASSWORDseed a super_admin at first boot (mokosh-server'smaybe_bootstrap_admin). Set those for the login form to be usable out of the box.Verified
just pre-commitgreen: fmt + clippy (-D warnings) +cargo check --target wasm32-unknown-unknown+cargo test --lib(214 passed). Configured-OIDC deployments are unaffected (the OIDC rehydrate + trigger paths are unchanged; standalone only activates when no issuer resolves).#MAPPS-368
🤖 Generated with Claude Code
The SPA authenticated only through bunyip's OIDC OP: `start_login` built `{issuer}/oauth2/authorize` and both the `Login` component and the `AuthGuard` route guard navigated there. A self-hosted deployment with no bunyip fell back to the compile-time default issuer `http://localhost:8080`, so login went to a dead `/oauth2/authorize` and rendered the SPA's own 404. mokosh-server's legacy `POST /api/v1/auth/login` (username/password, JSON token response) was still live but had no form. When no OIDC issuer resolves, the SPA now presents a standalone username/password form that authenticates against mokosh-server's `/api/v1/auth/login`. Deployments that configure an issuer (runtime-injected `MOKOSH_OIDC_ISSUER`, or an `msp.<tld>` host that derives one) are unchanged and still use the bunyip OIDC redirect. mokosh-server needs no changes. Detection: `OidcConfig::from_env`'s issuer default becomes empty, and `OidcConfig::has_issuer()` reports whether a real issuer resolved; `oidc::is_standalone()` wraps it for the two trigger sites. Only a truly-unconfigured deploy (no injection, non-msp host) flips to standalone, so the compile default no longer masks the "no bunyip" case. The form (`pages/login.rs`, mirroring the onboarding form) posts via a new unauthed `api::post_typed`, mapping 401 to "invalid email or password", 429 to a rate-limit message, and `mfa_required` to "sign in through your identity provider". On success it seeds the access token + `AuthContext` (like the OIDC callback) and persists a standalone session (a new `StandaloneSession` under its own storage key, kept separate from the id-token-keyed `StoredTokens` so the two rehydrate paths never collide). Reload rehydration is additive: `initial_auth_context` tries the OIDC rehydrate first, then the standalone one. `clear_auth` now also drops the standalone session so logout is complete. Deferred (follow-ups): silent token refresh via `/api/v1/auth/refresh` (standalone sessions currently last the access-token TTL, ~1h), and the MFA second factor (two-factor accounts are directed to their IdP). Note: a fresh self-hosted DB has no users until `ADMIN_EMAIL` / `ADMIN_PASSWORD` seed a super_admin at first boot (mokosh-server's `maybe_bootstrap_admin`); set those for the login form to be usable out of the box. #MAPPS-368 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>