Login and Authentication
The sign-in screen is the /login route. Whether anyone ever reaches it is decided by one back-end
setting — the Enabled flag of the Auth configuration section: with the flag off the service is
open and the route sends every visitor straight back out, and with it on a cookie-authentication
scheme plus a global authorization fallback policy lock every hub and every controller but the
authentication one — the whole api/ surface apart from api/auth — until a visitor signs in. The
route hosts no Control-Tree branch and takes no tree query argument.
The Enable Switch, and Its Two Defaults
AuthConfig binds the Auth section of configuration and is registered as one instance, so the
authentication controller and the startup decision read the same object. Two different values answer
the question “is the gate on by default”, and conflating them gets the answer wrong on a real
instance:
- The class default is off.
Enabledinitialises tofalse, so a service whose configuration carries noAuthsection — or an empty one — runs with no login at all. - The shipped configuration turns it on.
appsettings.jsonand the Development overlay both setEnabledtotrueand both supply one credential entry, so a service started from the repository as it ships demands a sign-in.
Three further settings live in the same section. SessionHours defaults to 8 and is raised to 1 at
registration if a smaller number is configured. SlidingExpiration defaults to true.
Users is a list of username and password pairs and defaults to empty.
That list is the whole credential store. Passwords are held in the configuration file in clear text
and compared verbatim — a first-match scan for an exact username and password — so there is no
hashing, no lockout, no rate limit and no user database behind it. The class documents the clear-text
form as deliberate, because the project also ships as sample code. Every entry grants the same
access; there are no roles. Enabled set to true over an empty list therefore admits nobody.
What the Gate Registers
The flag guards exactly two registrations at startup: a cookie authentication scheme, which becomes the application's default and only scheme, and a global authorization fallback policy requiring an authenticated user.
The fallback policy is what does the locking. It applies to every endpoint carrying no authorization
metadata of its own — every controller in the host assembly except the authentication one, and all
eight SignalR hub routes. UseAuthentication and UseAuthorization are added to the request pipeline
unconditionally and are no-ops when neither registration happened. See
Program and Hosting for the pipeline order this sits in.
Three places in the host carry AllowAnonymous, and they are the only ones that do:
- The authentication controller itself, marked at class level, so all three of its endpoints stay reachable under the policy that locks everything else.
- Both SPA fallbacks, so the client shell — which carries no data of its own — can load and show its own login screen.
Two pipeline stages answer before the authorization stage is reached and are unaffected by the policy: static-file serving and the Swagger middleware. The first of those is what lets the login screen render before anyone has signed in — the built bundle and the brand image it draws are physical files under the web root.
With the gate off, none of this is registered: every controller, every hub and both fallbacks are open, the login endpoint answers success without inspecting anything, the client's navigation guard short-circuits, and the login route bounces the visitor away as it mounts.
The Cookie
The scheme issues a cookie named HiNC.Auth. It is HttpOnly, its SameSite mode is Lax, and its
secure policy is same as request — marked Secure when the request itself arrived over HTTPS and
issued plainly otherwise, so a plain-HTTP run on a local network still works. That decision is one of
the reasons forwarded headers are applied first in the pipeline, the HTTPS-redirection stage being
the other: behind a TLS-terminating reverse proxy the scheme must be read from X-Forwarded-Proto
rather than from the plain hop between proxy and service.
The ticket carries exactly one claim, the name of the matched user. Its lifetime is the configured
session length, refreshed on activity when sliding expiration is left on. Sign-in passes no
authentication properties, so the ticket is not marked persistent and the cookie carries no Expires
attribute: the browser holds it for the browser session, and the ticket's own expiry bounds a session
left open.
One event pair is overridden, and it is what makes a single-page client possible at all. A challenge answers 401 and a refusal answers 403, in place of the framework's default redirect to a server-rendered login page. Every protected call therefore fails as data the client can read rather than as a 302 that would arrive at the fetch layer as an HTML page.
The Auth Endpoints
The controller is routed at api/auth and holds three endpoints.
| Endpoint | Behaviour |
|---|---|
GET status |
Reports whether the gate is enabled, whether this caller is authenticated, the caller's name, and a version string. It is anonymous, so it answers before sign-in and whether or not the gate is on. |
POST login |
With the gate off, returns success without inspecting the body. With it on, scans the configured users for an exact match and either answers 401 with an English message plus the stable code InvalidCredentials, or signs the caller in. |
POST logout |
Signs the caller out when the gate is on, and reports “not authenticated” either way. |
The version the status endpoint reports is the HiNc assembly version, ApiVersion(API). Because the endpoint is anonymous, that value is readable before sign-in, which is what lets the login screen and the menu bar show the same version mark.
Nothing else in the host reads the signed-in identity — the status endpoint is its only consumer. Every application service is registered as a process-wide singleton — the host adds no scoped or transient registration of its own — so authentication decides admission rather than identity: two signed-in browsers drive the same project and the same session. See Session State for what that shared state consists of.
The Client Side
The Store
A Pinia store holds the whole client-side picture: whether the gate is enabled, whether this session is
authenticated, the user name, the version string, and a ready flag set once the first status probe has
resolved. refresh reads the status endpoint; login and logout call their endpoints and update the
same fields; a fourth action flips the store to logged-out with no round trip, for the interceptor
below. Every transition re-drives the hub gate.
The Navigation Guard
One global beforeEach guard runs the whole client-side decision, in order:
- If the store is not ready yet, hydrate it from the status endpoint, inside a
try/catchthat swallows the failure. The ready flag is set only on success, so a failed probe is retried on the next navigation. - If the gate reports disabled, allow the navigation.
- If the target is the login route, allow it.
- If the session is not authenticated, redirect to the login route with the blocked target's full
path — query arguments,
treeincluded — parked in aredirectargument. - Otherwise allow it.
What the guard does not do is protect anything. It is a navigation redirect: it runs on router
navigations only, so it has no bearing on a direct api/ request, on a hub negotiate, or on a static
asset, all of which are the fallback policy's business. It reads no per-route metadata, so there is no
list of public routes beyond the login route itself and no notion of a role. And it fails open by
construction — when the status probe throws, the store keeps the gate disabled and the navigation is
allowed, so a back-end hiccup cannot lock a user out of an installation that has no gate.
The 401 Interceptor
A boot file wraps the global fetch once for the whole application. Every API module, and SignalR's
own negotiate, call that global, so one wrapper covers a session that expires mid-use: on any 401
response, and only while the store says the gate is enabled, it marks the store logged out and pushes
the login route with the current full path in redirect, unless the router is already there. The
response is handed back unchanged, so the calling code still sees its own failure and handles it.
Boot order is load-bearing here. The interceptor's boot file is declared ahead of the i18n one, and
boot files are awaited in declaration order, so the locale probe made during startup goes through the
patched fetch.
The Hub Gate
The shared-hub registry carries one tri-state gate that the store drives on every transition: undecided
while the status probe is still resolving, closed while the gate is on and the session is signed out,
and open for a signed-in session or a service with no gate. Undecided holds, so a first-paint race
never fires a negotiate that is certain to fail; closed keeps every hub idle rather than letting the
never-give-up reconnect schedule hammer a /negotiate that answers 401 by design. The store releases
the gate even when the status probe fails, so a probe hiccup never strands the hubs of a service that
has no login. Opening the gate resumes the connections a caller had already asked for, without the
caller asking again.
After a Successful Sign-In
The login screen navigates with a full page load rather than a router navigation. The application's one-shot wiring — the project hub subscription and the first project-status fetch — sits behind a watcher on the auth predicate and runs once per page load, so reloading is what re-runs it with the cookie present.
Signing Out
The logout control sits at the right of the menu bar and renders only while the gate is enabled and
the session is authenticated. It is labelled with the signed-in user name, falling back to Logout
when the status carries no name, and its tooltip reads Log out. It posts the logout endpoint and
then, in a finally, hard-navigates to /login — so a failed request still tears the page down and
rebuilds the client from scratch, rather than leaving the singletons of an ended session wired up.
Layout
The route renders outside the shell layout, in a Quasar layout of its own — the same arrangement the
catch-all not-found route uses. Nothing of the application frame is present: no menu bar, and therefore
no Project, Page or Preference menu and no language submenu; no cached page panel; no
routine-progress footer. The browser tab reads Login - HiNC.
- Login Page — a single card centred on an empty page
- Brand Section
- Brand image, above the literal title
HiNC - Version caption,
vfollowed by the version string — drawn only when the status probe returned one - Prompt caption —
Please sign in to continue
- Brand image, above the literal title
- Separator
- Sign-In Form
UsernameText Field — autofocused on arrival, disabled while a sign-in is in flightPasswordText Field — masked, with a trailing eye icon that toggles the text visible; the icon's accessible label alternates betweenShow passwordandHide password- Error caption — drawn in the negative colour, and only after a failed attempt
Sign InButton — submits the form and shows a spinner while the request is in flight
- Brand Section
Pressing Enter in either field submits the same form. Neither field carries a validation rule, so an
empty pair is submitted and refused by the server like any other wrong pair. The error caption shows
the localized “Incorrect username or password” whenever the server pairs its refusal with the
InvalidCredentials code, and the server's own English message for any other refusal that carries
one.
Two cases send the visitor away again as soon as the screen mounts. The mount hook hydrates the auth
status if the first probe has not resolved yet, and then replaces the route with the parked redirect
target — or with /, which redirects on to the Execution page — when the gate reports disabled or when
the session is already authenticated.
The locale the screen paints in is whatever the boot sequence resolved, because the stored language preference is served by an endpoint the fallback policy locks. With a browser-local cached locale the screen paints in that; without one the awaited probe fails and the locale falls back to the browser's own language, and to English after that. The screen's own strings ship in all three locales.
Source Code Path
See HiNC App Anatomy for git repository links.
Web Application
HiNC-2025-webservice (Quasar CLI SPA):
Common/AuthConfig.cs— the bound shape: the enable flag and its class default, the session length, the sliding-expiration flag, and the clear-text user list.Common/AuthController.cs— the anonymousapi/authcontroller: the status projection, the first-match credential scan with its message and code pair, the single name claim it signs in, and the sign-out.Program.cs— the flag-guarded registration of the cookie scheme and the fallback policy, the cookie name and flags, the 401 / 403 event overrides, the forwarded-headers configuration ahead of them, and the two anonymous SPA fallbacks.wwwroot-src/src/pages/LoginPage.vue— the screen: its own Quasar layout, the card and its fields, the bounce on mount, and the full page load after a successful sign-in.wwwroot-src/src/router/routes.ts— the login route outside the shell layout, and the title key the tab shows.wwwroot-src/src/router/index.ts— the navigation guard: the once-only hydration, the disabled and login-route short circuits, and the redirect carrying the full path; also the retitle that composes the tab text from the route's title key.wwwroot-src/src/stores/auth.ts— the client-side state, the four actions, and the hub gate each of them re-drives.wwwroot-src/src/api/auth.ts— the three typed endpoint wrappers, and the mapping of the server's refusal code onto the localized message.wwwroot-src/src/boot/auth.ts— the globalfetchwrapper that turns a 401 into a logged-out store and a push to the login route.wwwroot-src/quasar.config.ts— the boot-file order that puts the interceptor ahead of the locale probe.wwwroot-src/src/boot/i18n.ts— the locale resolution the login screen paints in, and its fallback chain when the language endpoint answers 401.wwwroot-src/src/composables/useSharedHub.ts— the tri-state hub gate and what each of its states does to a connection.wwwroot-src/src/App.vue— the one-shot wiring held behind the auth predicate, which is what the post-sign-in page load re-runs.wwwroot-src/src/components/AppMenuBar.vue— the logout control and the version badge, both drawn from the auth store.wwwroot-src/src/i18n/en/auth.ts— the screen's English strings.wwwroot-src/src/i18n/en/menu.ts— theLogoutlabel the control falls back to, and itsLog outtooltip.wwwroot-src/src/i18n/en/routes.ts— theLoginroute title the browser tab resolves.
HiAPI Engine
HiNc/MachiningProcs/MachiningProject.cs— the assembly version the status endpoint reports and the screen shows.
See Also
- Program and Hosting — the host that binds the
Authsection and registers the scheme, the fallback policy and the anonymous carve-outs described here - Main Panel — the shell this screen renders outside of, and the menu bar that carries the logout control and the version badge