Archived
deploy.
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Login and Authentication | HiAPI-C# 2025 </title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="title" content="Login and Authentication | HiAPI-C# 2025 ">
|
||||
|
||||
|
||||
<link rel="icon" href="../../img/HiAPI.favicon.ico">
|
||||
<link rel="stylesheet" href="../../public/docfx.min.css">
|
||||
<link rel="stylesheet" href="../../public/main.css">
|
||||
<meta name="docfx:navrel" content="../../toc.html">
|
||||
<meta name="docfx:tocrel" content="toc.html">
|
||||
|
||||
<meta name="docfx:rel" content="../../">
|
||||
|
||||
|
||||
|
||||
<meta name="loc:inThisArticle" content="In this article">
|
||||
<meta name="loc:searchResultsCount" content="{count} results for "{query}"">
|
||||
<meta name="loc:searchNoResults" content="No results for "{query}"">
|
||||
<meta name="loc:tocFilter" content="Filter by title">
|
||||
<meta name="loc:nextArticle" content="Next">
|
||||
<meta name="loc:prevArticle" content="Previous">
|
||||
<meta name="loc:themeLight" content="Light">
|
||||
<meta name="loc:themeDark" content="Dark">
|
||||
<meta name="loc:themeAuto" content="Auto">
|
||||
<meta name="loc:changeTheme" content="Change theme">
|
||||
<meta name="loc:copy" content="Copy">
|
||||
<meta name="loc:downloadPdf" content="Download PDF">
|
||||
|
||||
<script type="module" src="./../../public/docfx.min.js"></script>
|
||||
|
||||
<script>
|
||||
const theme = localStorage.getItem('theme') || 'auto'
|
||||
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
|
||||
<header class="bg-body border-bottom">
|
||||
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
|
||||
<div class="container-xxl flex-nowrap">
|
||||
<a class="navbar-brand" href="../../index.html">
|
||||
<img id="logo" class="svg" src="../../img/HiAPI.logo.png" alt="">
|
||||
|
||||
</a>
|
||||
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<i class="bi bi-three-dots"></i>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navpanel">
|
||||
<div id="navbar">
|
||||
<form class="search" role="search" id="search">
|
||||
<i class="bi bi-search"></i>
|
||||
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container-xxl">
|
||||
<div class="toc-offcanvas">
|
||||
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
<nav class="toc" id="toc"></nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="actionbar">
|
||||
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
|
||||
<nav id="breadcrumb"></nav>
|
||||
</div>
|
||||
|
||||
<article data-uid="Anatomy-LoginAndAuth">
|
||||
<h1 id="login-and-authentication">Login and Authentication</h1>
|
||||
|
||||
<p>The sign-in screen is the <code>/login</code> route. Whether anyone ever reaches it is decided by one back-end
|
||||
setting — the <code>Enabled</code> flag of the <code>Auth</code> 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 <code>api/</code> surface apart from <code>api/auth</code> — until a visitor signs in. The
|
||||
route hosts no Control-Tree branch and takes no <code>tree</code> query argument.</p>
|
||||
<h2 id="the-enable-switch-and-its-two-defaults">The Enable Switch, and Its Two Defaults</h2>
|
||||
<p><code>AuthConfig</code> binds the <code>Auth</code> 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:</p>
|
||||
<ul>
|
||||
<li><strong>The class default is off.</strong> <code>Enabled</code> initialises to <code>false</code>, so a service whose configuration
|
||||
carries no <code>Auth</code> section — or an empty one — runs with no login at all.</li>
|
||||
<li><strong>The shipped configuration turns it on.</strong> <code>appsettings.json</code> and the Development overlay both set
|
||||
<code>Enabled</code> to <code>true</code> and both supply one credential entry, so a service started from the repository
|
||||
as it ships demands a sign-in.</li>
|
||||
</ul>
|
||||
<p>Three further settings live in the same section. <code>SessionHours</code> defaults to 8 and is raised to 1 at
|
||||
registration if a smaller number is configured. <code>SlidingExpiration</code> defaults to <code>true</code>.
|
||||
<code>Users</code> is a list of username and password pairs and defaults to empty.</p>
|
||||
<p>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 <em>and</em> 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. <code>Enabled</code> set to <code>true</code> over an empty list therefore admits nobody.</p>
|
||||
<h2 id="what-the-gate-registers">What the Gate Registers</h2>
|
||||
<p>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.</p>
|
||||
<p>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. <code>UseAuthentication</code> and <code>UseAuthorization</code> are added to the request pipeline
|
||||
unconditionally and are no-ops when neither registration happened. See
|
||||
<a class="xref" href="program-and-hosting.html">Program and Hosting</a> for the pipeline order this sits in.</p>
|
||||
<p>Three places in the host carry <code>AllowAnonymous</code>, and they are the only ones that do:</p>
|
||||
<ul>
|
||||
<li>The authentication controller itself, marked at class level, so all three of its endpoints stay
|
||||
reachable under the policy that locks everything else.</li>
|
||||
<li>Both SPA fallbacks, so the client shell — which carries no data of its own — can load and show its
|
||||
own login screen.</li>
|
||||
</ul>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="the-cookie">The Cookie</h2>
|
||||
<p>The scheme issues a cookie named <code>HiNC.Auth</code>. It is <code>HttpOnly</code>, its <code>SameSite</code> mode is <code>Lax</code>, and its
|
||||
secure policy is <em>same as request</em> — marked <code>Secure</code> 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 <code>X-Forwarded-Proto</code>
|
||||
rather than from the plain hop between proxy and service.</p>
|
||||
<p>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 <code>Expires</code>
|
||||
attribute: the browser holds it for the browser session, and the ticket's own expiry bounds a session
|
||||
left open.</p>
|
||||
<p>One event pair is overridden, and it is what makes a single-page client possible at all. A challenge
|
||||
answers <strong>401</strong> and a refusal answers <strong>403</strong>, 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.</p>
|
||||
<h2 id="the-auth-endpoints">The Auth Endpoints</h2>
|
||||
<p>The controller is routed at <code>api/auth</code> and holds three endpoints.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Endpoint</th>
|
||||
<th>Behaviour</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>GET status</code></td>
|
||||
<td>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.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>POST login</code></td>
|
||||
<td>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 <code>InvalidCredentials</code>, or signs the caller in.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>POST logout</code></td>
|
||||
<td>Signs the caller out when the gate is on, and reports “not authenticated” either way.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>The version the status endpoint reports is the HiNc assembly version,
|
||||
<a class="xref" href="../../api/Hi.MachiningProcs.MachiningProject.html#Hi_MachiningProcs_MachiningProject_ApiVersion_">ApiVersion</a><small>(API)</small>. 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.</p>
|
||||
<p>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 <a class="xref" href="session-state.html">Session State</a> for what that
|
||||
shared state consists of.</p>
|
||||
<h2 id="the-client-side">The Client Side</h2>
|
||||
<h3 id="the-store">The Store</h3>
|
||||
<p>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. <code>refresh</code> reads the status endpoint; <code>login</code> and <code>logout</code> 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.</p>
|
||||
<h3 id="the-navigation-guard">The Navigation Guard</h3>
|
||||
<p>One global <code>beforeEach</code> guard runs the whole client-side decision, in order:</p>
|
||||
<ol>
|
||||
<li>If the store is not ready yet, hydrate it from the status endpoint, inside a <code>try</code>/<code>catch</code> that
|
||||
swallows the failure. The ready flag is set only on success, so a failed probe is retried on the
|
||||
next navigation.</li>
|
||||
<li>If the gate reports disabled, allow the navigation.</li>
|
||||
<li>If the target is the login route, allow it.</li>
|
||||
<li>If the session is not authenticated, redirect to the login route with the blocked target's full
|
||||
path — query arguments, <code>tree</code> included — parked in a <code>redirect</code> argument.</li>
|
||||
<li>Otherwise allow it.</li>
|
||||
</ol>
|
||||
<p><strong>What the guard does not do</strong> is protect anything. It is a navigation redirect: it runs on router
|
||||
navigations only, so it has no bearing on a direct <code>api/</code> 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.</p>
|
||||
<h3 id="the-401-interceptor">The 401 Interceptor</h3>
|
||||
<p>A boot file wraps the global <code>fetch</code> 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 <code>redirect</code>, unless the router is already there. The
|
||||
response is handed back unchanged, so the calling code still sees its own failure and handles it.</p>
|
||||
<p>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 <code>fetch</code>.</p>
|
||||
<h3 id="the-hub-gate">The Hub Gate</h3>
|
||||
<p>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 <code>/negotiate</code> 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.</p>
|
||||
<h3 id="after-a-successful-sign-in">After a Successful Sign-In</h3>
|
||||
<p>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.</p>
|
||||
<h2 id="signing-out">Signing Out</h2>
|
||||
<p>The logout control sits at the right of the menu bar and renders only while the gate is enabled <em>and</em>
|
||||
the session is authenticated. It is labelled with the signed-in user name, falling back to <code>Logout</code>
|
||||
when the status carries no name, and its tooltip reads <code>Log out</code>. It posts the logout endpoint and
|
||||
then, in a <code>finally</code>, hard-navigates to <code>/login</code> — 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.</p>
|
||||
<h2 id="layout">Layout</h2>
|
||||
<p>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 <code>Login - HiNC</code>.</p>
|
||||
<ul>
|
||||
<li>Login Page — a single card centred on an empty page
|
||||
<ul>
|
||||
<li>Brand Section
|
||||
<ul>
|
||||
<li>Brand image, above the literal title <code>HiNC</code></li>
|
||||
<li>Version caption, <code>v</code> followed by the version string — drawn only when the status probe returned
|
||||
one</li>
|
||||
<li>Prompt caption — <code>Please sign in to continue</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Separator</li>
|
||||
<li>Sign-In Form
|
||||
<ul>
|
||||
<li><code>Username</code> Text Field — autofocused on arrival, disabled while a sign-in is in flight</li>
|
||||
<li><code>Password</code> Text Field — masked, with a trailing eye icon that toggles the text visible; the icon's
|
||||
accessible label alternates between <code>Show password</code> and <code>Hide password</code></li>
|
||||
<li>Error caption — drawn in the negative colour, and only after a failed attempt</li>
|
||||
<li><code>Sign In</code> Button — submits the form and shows a spinner while the request is in flight</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p>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
|
||||
<code>InvalidCredentials</code> code, and the server's own English message for any other refusal that carries
|
||||
one.</p>
|
||||
<p>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 <code>redirect</code>
|
||||
target — or with <code>/</code>, which redirects on to the Execution page — when the gate reports disabled or when
|
||||
the session is already authenticated.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="source-code-path">Source Code Path</h2>
|
||||
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
|
||||
<h3 id="web-application">Web Application</h3>
|
||||
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
|
||||
<ul>
|
||||
<li><code>Common/AuthConfig.cs</code> — the bound shape: the enable flag and its class default, the session length,
|
||||
the sliding-expiration flag, and the clear-text user list.</li>
|
||||
<li><code>Common/AuthController.cs</code> — the anonymous <code>api/auth</code> controller: 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.</li>
|
||||
<li><code>Program.cs</code> — 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.</li>
|
||||
<li><code>wwwroot-src/src/pages/LoginPage.vue</code> — 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.</li>
|
||||
<li><code>wwwroot-src/src/router/routes.ts</code> — the login route outside the shell layout, and the title key the
|
||||
tab shows.</li>
|
||||
<li><code>wwwroot-src/src/router/index.ts</code> — 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.</li>
|
||||
<li><code>wwwroot-src/src/stores/auth.ts</code> — the client-side state, the four actions, and the hub gate each of
|
||||
them re-drives.</li>
|
||||
<li><code>wwwroot-src/src/api/auth.ts</code> — the three typed endpoint wrappers, and the mapping of the server's
|
||||
refusal code onto the localized message.</li>
|
||||
<li><code>wwwroot-src/src/boot/auth.ts</code> — the global <code>fetch</code> wrapper that turns a 401 into a logged-out store
|
||||
and a push to the login route.</li>
|
||||
<li><code>wwwroot-src/quasar.config.ts</code> — the boot-file order that puts the interceptor ahead of the locale
|
||||
probe.</li>
|
||||
<li><code>wwwroot-src/src/boot/i18n.ts</code> — the locale resolution the login screen paints in, and its fallback
|
||||
chain when the language endpoint answers 401.</li>
|
||||
<li><code>wwwroot-src/src/composables/useSharedHub.ts</code> — the tri-state hub gate and what each of its states
|
||||
does to a connection.</li>
|
||||
<li><code>wwwroot-src/src/App.vue</code> — the one-shot wiring held behind the auth predicate, which is what the
|
||||
post-sign-in page load re-runs.</li>
|
||||
<li><code>wwwroot-src/src/components/AppMenuBar.vue</code> — the logout control and the version badge, both drawn
|
||||
from the auth store.</li>
|
||||
<li><code>wwwroot-src/src/i18n/en/auth.ts</code> — the screen's English strings.</li>
|
||||
<li><code>wwwroot-src/src/i18n/en/menu.ts</code> — the <code>Logout</code> label the control falls back to, and its <code>Log out</code>
|
||||
tooltip.</li>
|
||||
<li><code>wwwroot-src/src/i18n/en/routes.ts</code> — the <code>Login</code> route title the browser tab resolves.</li>
|
||||
</ul>
|
||||
<h3 id="hiapi-engine">HiAPI Engine</h3>
|
||||
<ul>
|
||||
<li><code>HiNc/MachiningProcs/MachiningProject.cs</code> — the assembly version the status endpoint reports and the
|
||||
screen shows.</li>
|
||||
</ul>
|
||||
<h2 id="see-also">See Also</h2>
|
||||
<ul>
|
||||
<li><a class="xref" href="program-and-hosting.html">Program and Hosting</a> — the host that binds the <code>Auth</code> section and registers the scheme,
|
||||
the fallback policy and the anonymous carve-outs described here</li>
|
||||
<li><a class="xref" href="../shell/main-panel.html">Main Panel</a> — the shell this screen renders outside of, and the menu bar that carries
|
||||
the logout control and the version badge</li>
|
||||
</ul>
|
||||
|
||||
</article>
|
||||
|
||||
<div class="contribution d-print-none">
|
||||
</div>
|
||||
|
||||
<div class="next-article d-print-none border-top" id="nextArticle"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="affix">
|
||||
<nav id="affix"></nav>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="container-xxl search-results" id="search-results"></div>
|
||||
|
||||
<footer class="border-top text-secondary">
|
||||
<div class="container-xxl">
|
||||
<div class="flex-fill">
|
||||
<span> Copyright © 2025 <a href='https://superhightech.com.tw'>Tech Coordinate</a>. All rights reserved. <a href='https://superhightech.com.tw'>超級高科技股份有限公司</a> © 2025 版權所有 </span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user