Skip to content

fix(auth): address security issues and bugs in authentication flow#70

Merged
chrisjwalk merged 1 commit into
mainfrom
feat/auth-security-fixes-69
Mar 16, 2026
Merged

fix(auth): address security issues and bugs in authentication flow#70
chrisjwalk merged 1 commit into
mainfrom
feat/auth-security-fixes-69

Conversation

@chrisjwalk-bot
Copy link
Copy Markdown
Collaborator

Closes #69

What changed

Bug fixes

loginStart() was removing the refresh token prematurely
The old code called removeRefreshToken() inside loginStart(), which is invoked at the start of both fresh logins and silent refreshes. If a refresh request failed due to a network error, the refresh token was already gone and the user would be silently logged out on their next visit. Now removeRefreshToken() is called only at the explicit start of a fresh login (inside the login() rxMethod tap), never during a background refresh.

Token swap order in loginSuccessful()
The new refresh token is now persisted to localStorage before the state is patched. This ensures a page reload that lands between the two operations always recovers the latest token.

Concurrent 401 race condition in the HTTP interceptor
Multiple in-flight requests failing with 401 simultaneously would each call store.refresh() independently, firing multiple refresh round-trips and causing unpredictable state transitions. The interceptor now checks store.loginLoading() before calling store.refresh(); subsequent 401 handlers just subscribe to loginStatus$ and wait for the first refresh to complete.

Spurious refresh triggered by non-401 errors
The old catchError condition was error.status === 401 || store.expired(). A 400, 500, or any other error would trigger a refresh attempt whenever the client-side clock said the access token was expired. The condition is now error.status === 401 only.

Security improvements

Rate limiting on /api/account/*
Added AddRateLimiter with a per-IP fixed-window policy (10 requests per minute) applied to the entire /api/account endpoint group. This covers login, register, and password-reset against brute force.

Explicit bearer token lifetimes
BearerTokenOptions is now configured explicitly: access token 1 h (unchanged from default), refresh token 7 d (down from the 14 d default). The shorter refresh window reduces the blast radius of a stolen token and makes the security posture auditable at a glance.

localhost added to CORS allowlist in development
The CORS policy now also permits localhost origins when running in the Development environment.

Hardcoded credentials removed from Api.http

Pre-filled test credentials removed from the login form
defaultLoginRequest in login.store.ts was initialising the login form with the same test credentials, shipping them to every end user in the compiled bundle. Replaced with empty strings.

Tests

  • Renamed and updated the interceptor test that previously asserted refresh was triggered by a 400 error + expired token (now correctly expects a 401 to trigger refresh).
  • Added test: should not trigger refresh for non-401 errors even when the token is expired
  • Added test: should not start a duplicate refresh when one is already in progress

…loses #69)

- Remove removeRefreshToken() from loginStart() to prevent losing the
  session when a network error occurs during token refresh; call it
  explicitly only at the start of a fresh login (not during refresh)
- Swap token storage order in loginSuccessful(): persist the new refresh
  token before updating state so a page reload mid-swap never loses it
- Guard store.refresh() in the interceptor with a loginLoading() check
  so concurrent 401 responses only trigger one refresh round-trip
- Replace '|| store.expired()' with only status === 401 in the
  interceptor catchError; non-401 errors no longer spuriously kick off
  a token refresh when the client-side clock says the token is expired
- Add per-IP fixed-window rate limiting (10 req/min) to all
  /api/account/* endpoints via AddRateLimiter to mitigate brute force
- Configure BearerTokenOptions explicitly: access 1 h, refresh 7 d
- Allow localhost origins in CORS during development
- Remove hardcoded test access token and credentials from Api.http
- Replace pre-filled test@test.com/SuperSecret1! defaults in the login
  form with empty strings
- Add/update tests covering the corrected interceptor and store behaviour

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://green-water-08792290f-70.eastus2.2.azurestaticapps.net

@github-actions
Copy link
Copy Markdown

Note

Coverage shown for affected projects only. Per-file thresholds (80%) are enforced by vitest.

Code Coverage

Package Line Rate Branch Rate Complexity Health
src 100% 100% 0
src.lib.services 100% 100% 0
src.lib.state 95% 97% 0
src 100% 100% 0
src.lib.components.forecast-table 100% 100% 0
src.lib.components.weather-forecast 100% 100% 0
src.lib.models 100% 100% 0
src.lib.services 100% 100% 0
src.lib.state 100% 100% 0
src 100% 100% 0
src.lib.login 100% 100% 0
src.lib.state 100% 100% 0
app 100% 100% 0
app.debug 83% 100% 0
Summary 97% (184 / 191) 99% (47 / 48) 0

@chrisjwalk chrisjwalk merged commit 5b42591 into main Mar 16, 2026
7 checks passed
@chrisjwalk chrisjwalk deleted the feat/auth-security-fixes-69 branch March 16, 2026 19:17
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.

fix(auth): address security issues and bugs in authentication flow

2 participants