Skip to content

Commit d129400

Browse files
committed
docs: audit and update authentication-guide.md
1 parent 6a11a78 commit d129400

1 file changed

Lines changed: 44 additions & 9 deletions

File tree

docs/guides/authentication-guide.md

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,14 @@ graph TB
3131

3232
#### Frontend (Blazor Server)
3333
- **`AuthenticationService`**: High-level service for Login, Register, and Logout operations.
34-
- **`TokenService`**: Stores Access and Refresh tokens in **Scoped Memory** (per user session).
34+
- **`TokenService`**: Stores Access and Refresh tokens in **Scoped Memory** (per user session), keyed by tenant ID.
35+
- Tokens are stored as `Dictionary<string, (AccessToken, RefreshToken)>` — one entry per tenant — supporting multi-tenant sessions within the same Blazor circuit.
3536
- *Security Note*: Tokens are **NOT** stored in LocalStorage or Cookies to prevent XSS attacks.
3637
- Tokens persist only for the lifetime of the user's session (browser tab).
3738
- **`JwtAuthenticationStateProvider`**: Custom provider that:
38-
- Reads tokens from `TokenService`.
39+
- Reads tokens from `TokenService` for the **current tenant** (subscribes to `TenantService.OnChange` to re-evaluate state on tenant switch).
3940
- Parses JWT claims to set the user's `AuthenticationState`.
40-
- Automatically handles **Silent Refresh** when the access token is close to expiry.
41+
- Automatically handles **Silent Refresh** 5 minutes before token expiry (background) or immediately on request if the token has already expired.
4142

4243
#### Backend (API)
4344
- **`JwtTokenService`**: Central service for generating access tokens, rotating refresh tokens, and building standardized user claims. Signing algorithm resolution is shared with API validation: explicit `Jwt:Algorithm` wins, otherwise `RS256` is auto-selected when both RS256 keys are configured, else it falls back to `HS256`.
@@ -285,9 +286,16 @@ This keeps validation strict while avoiding false 401 responses from minor clien
285286

286287
### Cross-Tenant Protection
287288

288-
`TenantSecurityMiddleware` blocks requests where the JWT's `tenant_id` differs from the `X-Tenant-ID` header:
289-
- **Mismatch detected**: Returns `403 Forbidden`
290-
- **Refresh endpoint**: Validates stored token's tenant matches request tenant
289+
`TenantSecurityMiddleware` enforces tenant isolation for every request:
290+
291+
| Scenario | Result |
292+
|---|---|
293+
| Authenticated: JWT `tenant_id` missing | `403 Forbidden` |
294+
| Authenticated: JWT `tenant_id` ≠ request tenant | `403 Forbidden` (cross-tenant access denied) |
295+
| Anonymous: request targets a non-default tenant | `403 Forbidden` (anonymous access to tenant data denied) |
296+
| Authenticated or anonymous: tenant matches | Pass-through |
297+
298+
Endpoints can opt out of this check by declaring `[AllowAnonymousTenant]` metadata (used by public endpoints such as `/account/login`, `/api/configuration`, and SSE notifications).
291299

292300
## Authentication Methods
293301

@@ -363,16 +371,27 @@ We store tokens in a Scoped service (`TokenService`). This means:
363371
This is the intentional, final design for Blazor Server. Because all code (including `TokenService`) executes server-side, tokens are never serialised to the browser — not even as HttpOnly cookies. Refreshing the page requires re-login, which is an acceptable trade-off for high-security applications.
364372

365373
### Authorization Headers
366-
All outgoing HTTP requests to the API are intercepted by `AuthorizationMessageHandler`, which attaches the Bearer token:
374+
All outgoing HTTP requests to the API are intercepted by `AuthorizationMessageHandler`, which attaches the Bearer token for the **current tenant** and forwards additional request metadata:
367375

368376
```csharp
369-
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
377+
protected override async Task<HttpResponseMessage> SendAsync(
378+
HttpRequestMessage request, CancellationToken cancellationToken)
370379
{
371-
var token = await _tokenService.GetAccessTokenAsync();
380+
// Propagate distributed tracing identifiers
381+
request.Headers.Add("X-Correlation-ID", _clientContextService.CorrelationId);
382+
request.Headers.Add("X-Causation-ID", _clientContextService.CausationId);
383+
384+
// Attach the access token for the active tenant
385+
var token = _tokenService.GetAccessToken(_tenantService.CurrentTenantId);
372386
if (!string.IsNullOrEmpty(token))
373387
{
374388
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
375389
}
390+
391+
// Forward the original browser User-Agent and client IP
392+
// (used for passkey device naming and rate-limit partitioning)
393+
...
394+
376395
return await base.SendAsync(request, cancellationToken);
377396
}
378397
```
@@ -385,6 +404,22 @@ Role-based authorization is enforced via standard ASP.NET Core policies.
385404
- **Admin**: Full access to management endpoints.
386405
- **User**: Standard access (can manage own profile/orders).
387406

407+
### Policies
408+
409+
#### API (`BookStore.ApiService`)
410+
411+
| Policy | Requirement |
412+
|---|---|
413+
| `Admin` | Role `Admin` **or** `ADMIN` (case-insensitive alias) |
414+
415+
#### Web (`BookStore.Web`)
416+
417+
| Policy | Requirement |
418+
|---|---|
419+
| `SystemAdmin` | Role `Admin` **and** `tenant_id` == default tenant |
420+
421+
The `SystemAdmin` policy is used in the Blazor frontend to gate cross-tenant administration UI that is only available to admins of the default tenant.
422+
388423
### Backend Enforcement
389424
Endpoints are protected using the `[Authorize]` attribute or `.RequireAuthorization()` extension method.
390425

0 commit comments

Comments
 (0)