Skip to content

Add AUTH=TOKEN, a server-declared API route marker - #192

Merged
mgrossmann merged 1 commit into
mainfrom
issue-121-auth-token
Aug 17, 2026
Merged

Add AUTH=TOKEN, a server-declared API route marker#192
mgrossmann merged 1 commit into
mainfrom
issue-121-auth-token

Conversation

@mgrossmann

Copy link
Copy Markdown
Contributor

Fixes #121.

The decision the issue left open

The issue offered two spellings — resurrect AUTH=BASIC,TOKEN from #98, or add a mode AUTH=TOKEN — and deferred the choice to design. One observation settles it:

/* src/httppc.c */
resolve_credential(httpc);              /* line 65 */
route = path ? http_find_cgi(httpd, path) : NULL;   /* line 68 */

The credential is resolved before the route is even looked up. Every route already accepts every source — Sec-Token, LtpaToken2, Bearer, Basic — and no route can restrict that. So the source axis does not exist per route, and AUTH= never selected one: it selects whether a login is required and how a missing one is challenged.

That makes AUTH=BASIC,TOKEN the wrong shape twice over. It reads as a list of accepted sources, which is the conflation this issue exists to remove, and TOKEN-as-a-source would be meaningless because every route takes tokens today. A fourth challenge mode is the honest fix. Nothing depends on the #98 spelling — it was documented but never built.

What AUTH=TOKEN does

A bare 401, unconditionally, without consulting the request. It does not change which credentials are accepted: curl -u user:pass against a TOKEN route authenticates exactly as against a BASIC one — the route just never advertises that it would.

#120's X-CSRF-ZOSMF-HEADER suppression stays exactly where the issue put it, as an additional suppressor for BASIC and inherited routes. TOKEN does not consult that header at all: an operator declared this route machine-facing, and that must not depend on what a client happens to send. Two heuristics would be worse than one.

Why an API route must not challenge

A WWW-Authenticate header makes a browser pop its native credential dialog, and the Basic credentials it caches afterwards outlive the token session — every later request carries them, so logging out of the application ends nothing (#119). A bare 401 leaves the client's own "session expired" handling in charge.

Details worth a look

  • TOKEN lands on the authentication-required side of the stage-1 test in auth_gate(), like BASIC — marking a route with it must not make it public.
  • Both route-table decoders learn the name (httpdsrv.c ?target=MOD, httpdbug.c ?debug=cgi). A bare 4 there would cost exactly the person debugging an auth problem.
  • samplib/httpprm0 documents source-vs-challenge and marks the protected-API example AUTH=TOKEN.

Deliberately out of scope

The issue's fourth bullet — should XHR clients hitting a FORM or inherited route get a 401 instead of the HTML form? That changes behaviour for routes nobody marked, which is the opposite of a server-declared marker. Worth its own issue if wanted.

Verification

make modules clean, make test-host 87 assertions. The behaviour is pipeline + route table, so no unit test reaches it; the discriminating live check is a AUTH=TOKEN route answering a bare 401 without the CSRF header present — the case today's heuristic gets wrong. Pending a Parmlib line and a restart on the test system.

An API route wants to accept Basic credentials without advertising them. The
distinction matters because a WWW-Authenticate header makes a browser pop its
native credential dialog, and the Basic credentials it caches afterwards
outlive the token session -- so logging out of the application stops ending
anything (#119).

#120 fixed the immediate symptom by suppressing the challenge when a request
carries X-CSRF-ZOSMF-HEADER. That works because our SPA and Zowe always send
it, but it is a guess made from the request rather than a property the
operator declared, and real z/OSMF does not use the header at all.

AUTH=TOKEN is the declared form: the route answers a bare 401, unconditionally,
without consulting the request. #120's heuristic stays where the issue put it,
as an additional suppressor for BASIC and inherited routes.

Not the AUTH=BASIC,TOKEN comma list that #98 documented but never built. That
spelling reads as a list of accepted credential sources, and the source axis
does not exist per route: resolve_credential() runs in httppc() before the
route is even looked up, so every route already accepts the Sec-Token and
LtpaToken2 cookies, Bearer and Basic. AUTH= only ever selected whether a login
is required and how a missing one is challenged, so the honest fix is a fourth
challenge mode -- and TOKEN as a "source" would have been meaningless anyway,
since every route takes tokens today.

TOKEN lands on the authentication-required side of the stage-1 test, like
BASIC, so marking a route with it does not make it public. Both route-table
decoders learn the name, since a bare 4 in ?target=MOD or ?debug=cgi would
cost exactly the reader who is debugging an auth problem.

Left out deliberately: the issue's fourth bullet, whether XHR clients hitting a
FORM or inherited route should get a 401 instead of the login form. That would
change behaviour for routes nobody marked, which is the opposite of what this
change is for.

Fixes #121
@mgrossmann

Copy link
Copy Markdown
Contributor Author

Verified on a live server

Parmlib under test:

MOD=MVSMF /zosmf/info                    AUTH=NONE
MOD=MVSMF /zosmf/services/authenticate   AUTH=NONE
MOD=MVSMF /zosmf/*                       AUTH=TOKEN

The route table decodes the new mode

/.dsrv?target=MOD — the field a request is actually gated on:

+000C  route->path   "/zosmf/info"                    +0014  auth  1 NONE (public, never challenged)
+000C  route->path   "/zosmf/services/authenticate"   +0014  auth  1 NONE (public, never challenged)
+000C  route->path   "/zosmf/*"                       +0014  auth  4 TOKEN (bare 401, API route)
+000C  route->path   "/.dsrv"                         +0014  auth  0 DEFAULT (inherits the global LOGIN policy)

The discriminating case

/zosmf/restjobs/jobs — a path that matches /zosmf/* and not the two AUTH=NONE routes, so it is genuinely gated by httpd rather than by mvsMF's own track:

Request Status WWW-Authenticate
no credentials, no X-CSRF-ZOSMF-HEADER 401 absent
no credentials, with X-CSRF-ZOSMF-HEADER 401 absent
-u user:pass 200

The first row is the one that matters: without the CSRF marker, #120's heuristic would have emitted the challenge. The declared marker suppresses it regardless of what the client sent, which is the whole point — the two rows being identical is the evidence that TOKEN does not consult the request.

The third row confirms the other half: AUTH=TOKEN does not restrict the credential source. Basic still authenticates; the route just never advertises it.

Also confirmed in the same restart

SESSION_MAXAGE reads 480 with the keyword removed from the Parmlib — the #118 default path, previously only exercised with an explicit value.

Note on the banner

The running server reports 081755A-DIRTY rather than the commit on this branch. make deploy does not rebuild, and the modules were compiled from this exact tree just before it was committed — the version stamp is a snapshot of the git state at compile time, so it under-reports. The behaviour above is from this branch's code; the next build after a commit will stamp cleanly.

@mgrossmann
mgrossmann merged commit 26caea1 into main Aug 17, 2026
1 check passed
@mgrossmann
mgrossmann deleted the issue-121-auth-token branch August 17, 2026 12:15
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.

Auth: per-route API marker — separate credential source from challenge presentation

1 participant