MemberJunction / MemberJunction/MJ
A non-Owner contextUserForNewUserCreation fails at a user's first login, not at startup
- Dominant language
- TSQL
- Stars
- 29
- Forks
- 6
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 308
Description
**Found by** the PR gauntlet on #4275 (`fix/4260-new-user-roles-default`), 2026-09-07. The fail-closed behaviour is introduced by that PR and is correct; the *timing* of the failure is what this issue is about.
### Where
`packages/MJServer/src/auth/principals.ts` — `resolvePrincipalFrom`; the `Name` and `Email` rungs match without filtering on `Type`.
Consumers: `packages/MJServer/src/auth/newUsers.ts` (JWT auto-provisioning) and `packages/MJServer/src/auth/magicLink/MagicLinkService.ts`.
### What happens
Provisioning runs as the principal resolved from `contextUserForNewUserCreation` / `contextUserForProvisioning`. That ladder matches the configured string against `User.Name`, then `User.Email`; only its later System-by-ID and lowest-ID-active-Owner rungs guarantee an Owner. So a deployment can point either setting at a **non-Owner** user and the ladder will resolve it happily.
Since the `MJ: Users` guard shipped, creating a user row as a non-Owner is refused. The server starts normally, reports nothing unusual, and then fails the first time a real user tries to sign in — at `Save()`, per request.
### Why it matters
The failure is correct (failing closed beats silently creating rows as a non-Owner) but it surfaces at the worst moment and to the wrong person: an end user hitting a broken login, rather than the operator at deploy time. The configuration that caused it is static and knowable at boot. The changeset tells operators to repoint the setting before upgrading, but nothing checks that they did.
### Repro
```bash
# In the host config, set:
# userHandling.contextUserForNewUserCreation = ''
pnpm run start:api # starts cleanly, no warning
# Then authenticate as a brand-new user against the configured IdP.
# Observed: provisioning fails at Save(); the new user cannot sign in.
```
### Evidence
`resolvePrincipalFrom`'s own structure: the `Name` rung and the `Email` rung both select on the string alone, with ties broken by lowest active ID, and no `Type` predicate. Only `resolveFallback` guarantees an Owner.
### Suggested fix
At startup, resolve the configured principal once and check its `Type`. Log a loud, named warning — or refuse to start, if that matches how this server treats other fatal misconfiguration — quoting the setting name, the resolved user, and the consequence ("provisioning will fail closed"). Startup is the right place because the inputs are static config and the user cache is already loaded there; doing it per-request would repeat the same work and still report to the wrong audience. Reuse `resolvePrincipalFrom` rather than re-implementing the ladder, so the check cannot drift from the resolution it is validating.
### Definition of done
- [ ] A failing test that reproduces it (configured principal is a non-Owner ⇒ the startup check fires), then green
- [ ] Startup surfaces the problem naming the setting and the resolved user; a correctly configured host is unaffected
- [ ] Existing suite and gates green; no changed expectation in an existing test
### Verify by
Running the repro: the server now reports the misconfiguration at startup, before any user attempts to sign in.
Contributor guide
Research direction
Start in packages/MJServer/src/auth/principals.ts at resolvePrincipalFrom, then trace startup initialization and the consumers in auth/newUsers.ts and auth/magicLink/MagicLinkService.ts. Add a test for a configured non-Owner and verify startup names the setting and resolved user before authentication; run the existing suite and gates.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- authentication, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100