{access_token, refresh_token} pair minted by POST /api/v1/login, stored in localStorage, and attached as Authorization: Bearer <jwt> on every HTTP call. Three small classes own this end-to-end, all under src/app/auth/:
AuthGuard
auth.guard.ts — a CanActivate guard that blocks /home and /billing until AuthService.isAuthenticated() resolves true.AuthInterceptor
auth.interceptor.ts — injects the Bearer header, picks the right token per URL, and transparently refreshes-and-retries on 401.AuthService
auth.service.ts — the auth API client and token store, plus a background 8-minute refresh timer.
This page covers the browser-side JWT machinery only. For the Gateway’s view of the same tokens — HS256 signing, the 10-minute access / 12-hour refresh TTLs, the login/refresh/logout routes, and the in-memory blocklist — see Authentication & JWT Lifecycle and the platform-wide Authentication & Security Model. For how the same token is passed to the other transports (
?token= on Socket.IO, ?access_token= on redispad), see Real-time Telemetry Client and Vehicle Commands & Gamepad.How a protected navigation authenticates
Both eager (/login, /register, /reset-password) and lazy protected (/home, /billing) routes are declared in the app shell; the guard is attached only to the protected pair. See Routing & Lazy Loading for the route tree.
AuthGuard
auth.guard.ts is a providedIn: 'root' guard whose canActivate is async — it awaits a live GET /auth round-trip on every protected navigation, not just a local token check.
src/app/auth/auth.guard.ts
catch branch (auth.guard.ts:23-26) returns false without navigating. isAuthenticated() swallows its own errors internally, so this branch is effectively unreachable — but if you ever make isAuthenticated() throw, the user is silently stranded on a blank outlet (there is no ** wildcard/404 route).
AuthInterceptor
Registered as a multiHTTP_INTERCEPTORS provider in AppModule, auth.interceptor.ts runs on every outbound HttpClient request. It does two jobs: attach the correct token on the way out, and refresh-and-retry on a 401 coming back.
Per-URL token selection (outbound)
src/app/auth/auth.interceptor.ts
401 refresh-and-retry (inbound)
On a401, the interceptor refreshes the token and replays the original request — unless the failing URL contains /auth, /login, or /logout (those failures propagate as-is, preventing an infinite refresh loop on the refresh call itself).
src/app/auth/auth.interceptor.ts
handle401Error first guards against a missing/poisoned refresh token, then calls refreshToken(), stores the new pair, re-attaches the fresh access token, and retries. A refresh failure logs out and routes to /login.
The snake_case / camelCase refresh-token pitfall
This is the single most important gotcha on this page. The Gateway returns refresh responses in snake_case ({access_token, refresh_token}), and login stores them correctly (login.component.ts:55-56). But the interceptor’s refresh handler reads the wrong key:
src/app/auth/auth.interceptor.ts handle401Error, 53-56
tokens.refreshToken is undefined, storeRefreshToken(undefined) writes the literal string "undefined" into localStorage['refreshToken']. The access token is fine, so the retried request succeeds — but the refresh token is now poisoned.
The background timer in AuthService reads the correct key, so the two paths disagree:
src/app/auth/auth.service.ts setInterval body, 36-37
"undefined", so the poisoned value is treated as “no refresh token.” The next interceptor-driven 401 short-circuits to /login (auth.interceptor.ts:45), the 8-minute timer skips its refresh (auth.service.ts:30), and isAuthenticated() treats a "undefined" access token as missing (auth.service.ts:73). In practice: a single interceptor-driven refresh silently ends the session at the next 401, while the timer path keeps working.
AuthService — token lifecycle
auth.service.ts is the providedIn: 'root' client for every auth route and the sole owner of token storage.
localStorage token contract
logout() calls localStorage.clear() (not a targeted removal), so selectedDroneId, showMenu, skyhub_subscription and every other key are wiped on sign-out.
The auth API surface
All calls prefixenvironment.url (default http://localhost:5000/api/v1; see Environments, Build & CI).
The 8-minute background refresh timer
TheAuthService constructor starts a setInterval that fires every environment.httpSessionExpiryTime minutes (default 8) and silently refreshes both the token pair and the billing subscription cache:
src/app/auth/auth.service.ts constructor
Why 8 minutes? The Gateway’s access token expires at 10 minutes (JWT lifecycle), so an 8-minute proactive refresh keeps the session alive with ~2 minutes of headroom, ideally before any request hits a
401. The two mechanisms are complementary: the timer is the happy path; the interceptor’s refresh-and-retry is the fallback when a request races the expiry.Logout side effects
logout() does more than call the API — via Injector (lazily grabbed to break a circular dependency with VehicleCommandService) it tears down client state:
1
DELETE /logout
Server-side session/token invalidation.
2
Clear local state
localStorage.clear() + clearCookies() (expires every cookie on path=/).3
Free memory & sockets
ModelCacheService.clearCache() drops downloaded 3D models; VehicleCommandService.disconnect() closes the redispad WebSocket.4
Redirect
navigate('login') and a PrimeNG toast.Things a future editor must preserve
- The
'1234'fallback header is load-bearing for how public routes behave — verify the backend before removing it. refreshTokenfield name inhandle401ErroriscamelCaseand wrong; the timer path issnake_caseand right. Fix them together or not at all.- Skip-lists are substring matches:
ignoreRefreshUrlsand the outbound/update-password//verify-emailchecks all useurl.includes(...). A new route whose path merely contains/auth(e.g./authors) would silently be excluded from refresh-and-retry. - Both the guard and
isAuthenticated()navigate to/login— don’t add a third redirect. logout()useslocalStorage.clear(), so anything else you stash inlocalStoragedisappears on sign-out.

