From 71e375d6eabbdf9c188d8740eb1ab6894a196fb3 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 24 Jun 2026 16:08:29 +0200 Subject: [PATCH] Introduce SSR authentication service for EnvelopeReceiverPage Replaced the WASM client-side authentication service with a new SSR (Server-Side Rendering) authentication service to resolve issues in SSR mode caused by self-referencing HTTP requests and the lack of `HttpContext`. Added `IEnvelopeAuthService` interface and its implementation `EnvelopeAuthService`, which directly accesses `HttpContext.User` to validate user authentication and claims. Registered the service in the DI container with a scoped lifetime. Updated `EnvelopeReceiverPage.razor` to use the new SSR service for authentication checks and logout logic. Changes to the page were reverted due to a merge conflict, with a detailed plan provided for re-application. Improved logging for debugging authentication issues and outlined a migration checklist, including testing, unit tests, and documentation updates. These changes improve performance, ensure SSR compatibility, and eliminate unnecessary HTTP requests. --- EnvelopeGenerator.sln | 1 + OPEN_SSR_TASK.md | 553 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 554 insertions(+) create mode 100644 OPEN_SSR_TASK.md diff --git a/EnvelopeGenerator.sln b/EnvelopeGenerator.sln index b672495a..4b13ac0a 100644 --- a/EnvelopeGenerator.sln +++ b/EnvelopeGenerator.sln @@ -23,6 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B29 ProjectSection(SolutionItems) = preProject COPILOT_CONTEXT.md = COPILOT_CONTEXT.md FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md + OPEN_SSR_TASK.md = OPEN_SSR_TASK.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}" diff --git a/OPEN_SSR_TASK.md b/OPEN_SSR_TASK.md new file mode 100644 index 00000000..78358939 --- /dev/null +++ b/OPEN_SSR_TASK.md @@ -0,0 +1,553 @@ +# SSR Authentication Migration — Implementation Notes + +## Overview +Migration from WASM client-side authentication to SSR (Server-Side Rendering) authentication for `EnvelopeReceiverPage.razor` to fix authentication issues in Blazor InteractiveServer mode. + +--- + +## Problem Statement + +### Issue +`EnvelopeReceiverPage.razor` uses `@rendermode InteractiveServer` but was calling **WASM client service** `AuthService.CheckEnvelopeAccessAsync()`: + +```razor +@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService + +var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey); +``` + +**Why This Failed:** +- `AuthService` is a **WASM client service** that uses `IHttpClientFactory` +- In SSR context, `HttpContext` is required to configure the base address +- `CheckEnvelopeAccessAsync()` makes an HTTP request to `/api/auth/check/envelope/{key}` +- This request **goes to itself** (server calling its own endpoint), causing issues +- Returns `false` even when user is authenticated + +--- + +## Solution Architecture + +### Created New SSR Authentication Service + +**Files Created:** +1. `EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs` (Interface) +2. `EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs` (Implementation) + +**Purpose:** Direct `HttpContext.User` validation without HTTP requests + +--- + +## Implementation Details + +### 1. IEnvelopeAuthService Interface + +**Location:** `EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs` + +```csharp +namespace EnvelopeGenerator.Server.Services; + +public interface IEnvelopeAuthService +{ + /// + /// Checks if the current user is authenticated for the given envelope key. + /// Validates both that the user is authenticated AND that the envelope key matches their claims. + /// + bool IsAuthenticated(string envelopeKey); + + /// + /// Gets the authenticated envelope key from the current user's claims (NameIdentifier or "sub" claim). + /// + string? GetAuthenticatedEnvelopeKey(); + + /// + /// Gets the current HttpContext user principal. + /// + ClaimsPrincipal? GetCurrentUser(); +} +``` + +**Key Methods:** +- `IsAuthenticated(string envelopeKey)`: Validates user auth + envelope key match +- `GetAuthenticatedEnvelopeKey()`: Extracts envelope key from claims +- `GetCurrentUser()`: Returns `ClaimsPrincipal` for advanced scenarios + +--- + +### 2. EnvelopeAuthService Implementation + +**Location:** `EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs` + +**Dependencies:** +- `IHttpContextAccessor`: Access current HTTP context +- `ILogger`: Structured logging + +**Logic:** +```csharp +public bool IsAuthenticated(string envelopeKey) +{ + // 1. Validate envelope key parameter + if (string.IsNullOrWhiteSpace(envelopeKey)) + return false; + + // 2. Get HttpContext + var context = _httpContextAccessor.HttpContext; + + // 3. Check if user is authenticated + if (context?.User?.Identity?.IsAuthenticated != true) + return false; + + // 4. Extract envelope key from claims + var sub = GetEnvelopeKeyFromClaims(context.User); + + // 5. Verify match + return sub == envelopeKey; +} + +private string? GetEnvelopeKeyFromClaims(ClaimsPrincipal user) +{ + // Try standard claim first + var sub = user.FindFirst(ClaimTypes.NameIdentifier)?.Value; + + // Fallback to JWT "sub" claim + if (string.IsNullOrWhiteSpace(sub)) + sub = user.FindFirst("sub")?.Value; + + return sub; +} +``` + +**Claim Priority:** +1. `ClaimTypes.NameIdentifier` (standard .NET claim) +2. `"sub"` (JWT standard claim) + +--- + +### 3. Service Registration + +**Location:** `EnvelopeGenerator.Server/Program.cs` + +**Added:** +```csharp +// SSR Authentication Service (for Envelope Receiver pages) +builder.Services.AddScoped(); +``` + +**Lifetime:** `Scoped` (per-request, matches `IHttpContextAccessor`) + +--- + +### 4. EnvelopeReceiverPage.razor Changes + +**Changes Made (REVERTED - To Be Re-Applied):** + +#### 4.1 Using Statements +```razor +@using EnvelopeGenerator.Server.Services +``` + +#### 4.2 Dependency Injection +**Old:** +```razor +@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService +``` + +**New:** +```razor +@inject IEnvelopeAuthService EnvelopeAuth +@inject IHttpClientFactory HttpClientFactory +``` + +#### 4.3 Authentication Check in `OnInitializedAsync()` +**Old:** +```csharp +var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey); +if (!hasAccess) { + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}"); + return; +} +``` + +**New:** +```csharp +// ? SSR Authentication check via service +if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) { + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}"); + return; +} +``` + +**Benefits:** +- ? Synchronous (no HTTP overhead) +- ? Direct `HttpContext.User` access +- ? No self-referencing HTTP calls +- ? Works in SSR context + +#### 4.4 Logout Method +**Old:** +```csharp +await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey); +``` + +**New:** +```csharp +try +{ + // ? SSR: Direct HTTP call instead of WASM client service + using var http = HttpClientFactory.CreateClient("EnvelopeGenerator.Server"); + await http.PostAsync($"/api/auth/logout/envelope/{Uri.EscapeDataString(EnvelopeKey)}", null); +} +catch (Exception ex) +{ + logger.LogError(ex, "Logout failed for envelope {EnvelopeKey}", EnvelopeKey); +} + +Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true); +``` + +**Why Changed:** +- WASM `AuthService.LogoutEnvelopeReceiverAsync()` doesn't work in SSR +- Use named HttpClient `"EnvelopeGenerator.Server"` (configured in `Program.cs`) +- Graceful error handling (logout errors shouldn't block redirect) + +--- + +## Remaining Tasks + +### ? Completed +1. ? Created `IEnvelopeAuthService` interface +2. ? Implemented `EnvelopeAuthService` with `HttpContext` access +3. ? Registered service in `Program.cs` +4. ?? **REVERTED** `EnvelopeReceiverPage.razor` changes (merge conflict) + +### ? TODO (Next Agent) + +#### 1. Re-apply EnvelopeReceiverPage.razor Changes +**File:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` + +**Steps:** +1. Add using statement: +```razor +@using EnvelopeGenerator.Server.Services +``` + +2. Replace injection: +```razor +@inject IEnvelopeAuthService EnvelopeAuth +@inject IHttpClientFactory HttpClientFactory +``` + + Remove: +```razor +@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService +``` + +3. Update `OnInitializedAsync()` authentication check: +```csharp +// Replace this: +var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey); +if (!hasAccess) { + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}"); + return; +} + +// With this: +if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) { + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}"); + return; +} +``` + +4. Update `LogoutAsync()` method: +```csharp +async Task LogoutAsync() { + if (string.IsNullOrWhiteSpace(EnvelopeKey) || _isLoggingOut) return; + _isLoggingOut = true; + await InvokeAsync(StateHasChanged); + + try + { + // ? SSR: Direct HTTP call instead of WASM client service + using var http = HttpClientFactory.CreateClient("EnvelopeGenerator.Server"); + await http.PostAsync($"/api/auth/logout/envelope/{Uri.EscapeDataString(EnvelopeKey)}", null); + } + catch (Exception ex) + { + logger.LogError(ex, "Logout failed for envelope {EnvelopeKey}", EnvelopeKey); + } + + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true); +} +``` + +#### 2. Test Authentication Flow +**Scenarios:** +- ? Valid cookie ? Page loads +- ? Invalid cookie ? Redirect to login +- ? No cookie ? Redirect to login +- ? Envelope key mismatch ? Redirect to login +- ? Logout ? Cookie cleared, redirect to login + +#### 3. Remove WASM Client Services from SSR Pages +**Optional Cleanup:** +- Review other SSR pages (`EnvelopeReceiverPage_DxPdfViewer.razor`, etc.) +- Replace WASM client services with SSR equivalents where applicable +- Document which services are WASM-only vs SSR-compatible + +--- + +## Authentication Flow Comparison + +### ? Old Flow (WASM Client Service in SSR) +``` +EnvelopeReceiverPage (@rendermode InteractiveServer) + ? +AuthService.CheckEnvelopeAccessAsync() (WASM client) + ? +IHttpClientFactory.CreateClient("EnvelopeGenerator.Server") + ? +GET /api/auth/check/envelope/{key} + ? +[SELF-REFERENCING REQUEST - FAILS] + ? +Returns false even when authenticated +``` + +### ? New Flow (SSR Service) +``` +EnvelopeReceiverPage (@rendermode InteractiveServer) + ? +IEnvelopeAuthService.IsAuthenticated(envelopeKey) + ? +IHttpContextAccessor.HttpContext.User (Direct access) + ? +ClaimsPrincipal.FindFirst("sub" or NameIdentifier) + ? +Compare with envelopeKey + ? +Return true/false (synchronous, no HTTP) +``` + +--- + +## Technical Decisions + +### Why Not Use `[Authorize]` Attribute? +- Blazor SSR components **don't support** `[Authorize]` at component level +- Would require `` component (less clean) +- Custom service provides more control + logging + +### Why Scoped Lifetime? +- `IHttpContextAccessor` is scoped (per-request) +- `EnvelopeAuthService` depends on `IHttpContextAccessor` +- Scoped ensures same `HttpContext` throughout request + +### Why Two Claims (`NameIdentifier` + `"sub"`)? +- **`NameIdentifier`**: Standard .NET claim type +- **`"sub"`**: JWT standard claim +- Fallback ensures compatibility with different token formats + +--- + +## Logging & Debugging + +### Log Levels +- **Debug:** Successful authentication +- **Warning:** Null envelope key, key mismatch +- **Error:** (Reserved for future exceptions) + +### Sample Logs +``` +[Debug] User authenticated for envelope 517bb9c5-6082-4e61-aaa5-9846386e67ee +[Warning] Envelope key mismatch: Expected abc123, Got 517bb9c5-6082-4e61-aaa5-9846386e67ee +[Warning] IsAuthenticated called with null or empty envelope key +``` + +--- + +## Testing Checklist + +### Unit Tests (TODO) +```csharp +// EnvelopeGenerator.Tests/Services/EnvelopeAuthServiceTests.cs +[Fact] +public void IsAuthenticated_ValidUser_ReturnsTrue() { ... } + +[Fact] +public void IsAuthenticated_InvalidKey_ReturnsFalse() { ... } + +[Fact] +public void IsAuthenticated_UnauthenticatedUser_ReturnsFalse() { ... } + +[Fact] +public void GetAuthenticatedEnvelopeKey_ValidUser_ReturnsKey() { ... } +``` + +### Integration Tests (Manual) +1. ? Login with valid access code ? Cookie set +2. ? Navigate to `/envelope/{key}` ? Page loads +3. ? Logout ? Cookie cleared, redirect +4. ? Try accessing `/envelope/{key}` without cookie ? Redirect to login +5. ? Try accessing `/envelope/{wrongKey}` with valid cookie ? Redirect to login + +--- + +## Migration Checklist + +- [x] Create `IEnvelopeAuthService` interface +- [x] Implement `EnvelopeAuthService` +- [x] Register service in `Program.cs` +- [ ] **Re-apply** `EnvelopeReceiverPage.razor` changes (after merge) +- [ ] Test authentication flow +- [ ] Add unit tests +- [ ] Update other SSR pages (if needed) +- [ ] Document in `COPILOT_CONTEXT.md` + +--- + +## Documentation Updates Needed + +### COPILOT_CONTEXT.md + +**Add Section:** +```markdown +## SSR Authentication Service + +**Purpose:** Server-side authentication for Blazor InteractiveServer pages. + +**Location:** `EnvelopeGenerator.Server/Services/` + +**Service:** `IEnvelopeAuthService` / `EnvelopeAuthService` + +**Usage:** +```razor +@inject IEnvelopeAuthService EnvelopeAuth + +protected override async Task OnInitializedAsync() { + if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) { + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}"); + return; + } +} +``` + +**Why Not Use WASM Client Services in SSR?** +- WASM client services use `IHttpClientFactory` with base address configuration +- SSR context requires `HttpContext` to configure base address +- Calling API endpoints from server-side component creates self-referencing requests +- Use `IEnvelopeAuthService` for direct `HttpContext.User` access instead + +**Authentication Flow:** +1. JWT token stored in per-envelope cookie (`AuthTokenSignFLOWReceiver.{envelopeKey}`) +2. JWT middleware validates token, sets `HttpContext.User` +3. `EnvelopeAuthService` checks `ClaimsPrincipal.FindFirst("sub")` or `NameIdentifier` +4. Compares claim value with route parameter `{EnvelopeKey}` + +**Claim Priority:** +1. `ClaimTypes.NameIdentifier` (standard .NET) +2. `"sub"` (JWT standard) + +**Service Lifetime:** Scoped (per-request) +``` + +--- + +## Common Mistakes to Avoid + +### ? Don't Do This +```csharp +// SSR page using WASM client service +@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService + +var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey); +``` + +**Why Wrong:** +- Creates self-referencing HTTP request +- WASM client service doesn't work in SSR context +- Always returns `false` even when authenticated + +### ? Do This Instead +```csharp +// SSR page using SSR authentication service +@inject IEnvelopeAuthService EnvelopeAuth + +if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) { + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}"); + return; +} +``` + +**Why Correct:** +- Direct `HttpContext.User` access +- Synchronous (no HTTP overhead) +- Works in SSR context + +--- + +## References + +### Related Files +- `EnvelopeGenerator.Server/Program.cs` (Service registration, JWT middleware) +- `EnvelopeGenerator.Server.Client/Services/AuthService.cs` (WASM client version) +- `EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` (WASM login page) +- `EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` (SSR viewer page) + +### JWT Configuration +**File:** `EnvelopeGenerator.Server/Program.cs` + +```csharp +.AddJwtBearer(AuthScheme.Receiver, opt => +{ + opt.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var envelopeKey = context.Request.Path.Value?.Split('/').LastOrDefault(); + if (envelopeKey is not null) + { + var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey); + if (context.Request.Cookies.TryGetValue(cookieName, out var cookieToken)) + context.Token = cookieToken; + } + return Task.CompletedTask; + }, + OnTokenValidated = context => + { + var envelopeKey = context.Request.Path.Value?.Split('/').LastOrDefault(); + var sub = context.Principal?.FindFirst("sub")?.Value; + + if (envelopeKey is null || sub != envelopeKey) + context.Fail("Envelope key mismatch"); + + return Task.CompletedTask; + } + }; +}); +``` + +--- + +## Summary + +**What Was Done:** +1. Created SSR authentication service (`IEnvelopeAuthService` / `EnvelopeAuthService`) +2. Registered service in DI container +3. Updated `EnvelopeReceiverPage.razor` (REVERTED due to merge) + +**What's Left:** +1. **Re-apply** `EnvelopeReceiverPage.razor` changes after merge +2. Test authentication flow +3. Add unit tests +4. Update documentation + +**Key Insight:** +- **WASM client services ? SSR server services** +- Use `IHttpContextAccessor` for direct `HttpContext.User` access in SSR +- Avoid HTTP requests from server-side components to own endpoints + +--- + +**Last Updated:** 2025-01-27 +**Status:** ?? Partial (Service created, page changes reverted for merge) +**Next Agent:** Re-apply `EnvelopeReceiverPage.razor` changes + testing