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