Skip to content

fix(auth): derive identity from sm:id(), not the persistent-login attribute#813

Open
joewiz wants to merge 1 commit into
eXist-db:developfrom
joewiz:fix/auth-identity-from-request-user
Open

fix(auth): derive identity from sm:id(), not the persistent-login attribute#813
joewiz wants to merge 1 commit into
eXist-db:developfrom
joewiz:fix/auth-identity-from-request-user

Conversation

@joewiz

@joewiz joewiz commented Jun 6, 2026

Copy link
Copy Markdown
Member

[This PR was co-authored with Claude Code. -Joe]

Summary

GET /api/auth/whoami and POST /api/auth/session now derive identity from the actual eXist subject (sm:id()) instead of the persistent-login request attribute (org.exist.login.user).

The bug

The attribute is populated only by the persistent-login flow (login-form params or the remember-me cookie). A request authenticated with the HTTP Basic header therefore reported as guest even though it executed as the real user:

# before
curl -u admin:'' …/eXide/api/auth/whoami  →  { "user": "guest",  "isLoggedIn": false, "isAdmin": false }
# after
curl -u admin:'' …/eXide/api/auth/whoami  →  { "user": "admin",  "isLoggedIn": true,  "isAdmin": true  }

This also made eXide's notion of "who is logged in" diverge from existdb-openapi's and from Roaster's own rutil:getDBUser() — both of which read sm:id(). Aligning on sm:id() puts eXide on the same identity basis as the rest of the stack.

Why sm:id() is correct here

eXide's controller.xq calls login:set-user on every request before forwarding to the Roaster handlers, so by the time a handler runs the eXist subject already reflects whatever authenticated the request — Basic header, the shared org.exist.login cookie (which is Path=/exist, so it reaches every app under /exist), or a freshly minted login session. sm:id() reads that subject uniformly; the attribute does not. sm:effective is preferred over sm:real (cookie/token logins set the effective user — same reasoning as Roaster's getDBUser).

Changes

  • modules/api/auth.xqm — replaced the attribute-based auth:get-user() with auth:current-user() (reads sm:id(), effective-preferred); whoami/login now use it. Removed the now-unused login module import. Response shapes unchanged.
  • cypress/e2e/auth_spec.cy.js — new whoami identity source block: asserts the real user is reported under Basic auth (the regression), guest when unauthenticated, and the real user via the persistent-login cookie.

Verification

  • auth_spec.cy.js: 16/16 (was 13).
  • Full eXide suite: 263 passing, 0 failing, 2 intentional skips — all 36 specs green (with fix(query): don't swallow cursor:eval errors as HTTP 200 'Invalid context-item' (regression in 0.9.7) existdb-openapi#46 deployed; see dependency note below).
  • Manual: whoami correct under Basic, cookie, unauthenticated, and login-POST. Additionally verified with a temporary non-empty-password user (betatester): correct identity under Basic and cookie, correctly reported isAdmin:false for the non-dba account, and a wrong password did not authenticate — confirming the fix is not an artifact of the default empty admin password.

Cross-repo dependency note

Two eXide specs (query_error_display, query_error_structured) assert that a failed query surfaces a real error. They pass only when the deployed existdb-openapi includes the fix in eXist-db/existdb-openapi#46 (the 0.9.7 /api/query regression that returned HTTP 200 + "Invalid context-item" for genuine query errors). Against a stock 0.9.7 those two specs fail. So eXide's green suite currently depends on eXist-db/existdb-openapi#46 landing and a 0.9.8 release — worth prioritizing.

Scope note (deliberate, not in this PR)

This is the beachhead of a larger consolidation (existdb-openapi as the single backend; stock apps as thin clients on a common sm:id() identity baseline). Natural follow-ups, each its own verified step:

  1. Remove the security: [] overrides on the two /api/auth/* routes so they participate in Roaster's standard-authorization middleware and read $request?user directly (full convergence; deferred because it brings the login POST under CSRF and wants its own validation).
  2. Retire eXide's legacy controller.xq /login branch in favour of /api/auth/session.
  3. Apply the same sm:id() identity fix to documentation-next (also reads the attribute); dashboard-next already reads $request?user.
  4. Parametrize the cypress suites onto a non-empty admin password so credential-handling bugs can't hide behind the empty-string default (touches test config in both eXide and existdb-openapi — worth doing deliberately).

…ribute

GET /api/auth/whoami and POST /api/auth/session resolved the current user
by reading request:get-attribute("org.exist.login.user"). That attribute
is populated only by the persistent-login flow (login-form params or the
remember-me cookie), so a request authenticated with the HTTP Basic
header reported as "guest" even though it executed as the real user:

  before:  curl -u admin: .../api/auth/whoami  ->  user=guest, isAdmin=false
  after:   curl -u admin: .../api/auth/whoami  ->  user=admin, isAdmin=true

Replace the attribute-based auth:get-user() with auth:current-user(),
which reads the actual eXist subject via sm:id() (sm:effective preferred,
matching the cookie/token case). This works because controller.xq calls
login:set-user before forwarding to the Roaster handlers, so by handler
time the subject already reflects whatever authenticated the request --
Basic header, the shared org.exist.login cookie (Path=/exist), or a
freshly minted login session.

This puts eXide's notion of "who is logged in" on the same sm:id() basis
as Roaster's rutil:getDBUser() and existdb-openapi -- the first step of
converging the stock apps onto a single identity model.

Verified: auth_spec 16/16; identity correct under Basic, cookie, and
unauthenticated, including with a temporary non-empty-password user
(confirming the fix is not an artifact of the default empty admin
password) and correctly distinguishing dba from non-dba accounts.

Beachhead only -- deliberate follow-ups (each its own verified step):
removing the security:[] overrides on the two /api/auth routes so they
read $request?user via Roaster's middleware; retiring the legacy
controller /login branch; the same sm:id() fix in documentation-next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@line-o

line-o commented Jun 8, 2026

Copy link
Copy Markdown
Member

@joewiz I would like to take a completely different route forward:

  1. Roaster always populates each $request map with the information of the user that authenticated regardless of the method or strategy this happened with. This is done within the authentication middleware.
    So, if the current map lacks important information this should be added there instead.
  2. use roaster's new authentication functions in eXide that were added in version 1.12.1
    This way a lot of the quirks of persistent login can be alleviated and eXide gains more control over the ways to login, authenticate and settings.

@joewiz

joewiz commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

[This response was co-authored with Claude Code. -Joe]

Thanks — I like this direction, and I think it's the right one. Pushing identity resolution into Roaster's middleware (and letting eXide just consume $request?user) rather than working around it per-app is exactly the convergence we want, and adopting Roaster's native cookie auth would let eXide retire its bespoke controller.xq login:set-user persistent-login flow — which #814 already lists as a follow-up. It also lines up with the v1.12.1 fix ("change auth:login to fall back to cookie auth"), which looks like it targets the same persistent-login resolution issue that prompted #813. So I'm glad to take this route.

To turn it into something actionable, a few specifics would help:

  1. Which Roaster functions, and how to wire them? I want to use the right ones — the cookie login/logout work landed across 1.12.0 ("improve cookie-login and -logout") and 1.12.1 (the auth:login cookie-auth fallback). Could you point me at the specific functions to call for the login POST and logout (a short usage example or the docs would be perfect)? Especially how they'd replace controller.xq's login:set-user, which currently mints the org.exist.login cookie before the handler runs.

  2. Is anything actually missing from $request?user today, or should eXide just consume it as-is? fix(auth): fold /api/auth routes into Roaster authorization ($request?user) #814 already reads {name, fullName, groups, dba} from it, which covers whoami/login. If there's a concrete field eXide needs that the map lacks, I'm happy to help add it in the Roaster middleware — just let me know which.

  3. Scope: should this supersede fix(auth): derive identity from sm:id(), not the persistent-login attribute #813's approach, rework fix(auth): fold /api/auth routes into Roaster authorization ($request?user) #814 to call Roaster's cookie-login in the /api/auth/session handler, and retire the controller.xq /login branch — landed together? Happy to drive the eXide side; if any Roaster-side change is better in your hands, say so and I'll build on it.

Thanks for steering this toward the more durable design.

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