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.
15 KiB
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():
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
Why This Failed:
AuthServiceis a WASM client service that usesIHttpClientFactory- In SSR context,
HttpContextis 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
falseeven when user is authenticated
Solution Architecture
Created New SSR Authentication Service
Files Created:
EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs(Interface)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
namespace EnvelopeGenerator.Server.Services;
public interface IEnvelopeAuthService
{
/// <summary>
/// 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.
/// </summary>
bool IsAuthenticated(string envelopeKey);
/// <summary>
/// Gets the authenticated envelope key from the current user's claims (NameIdentifier or "sub" claim).
/// </summary>
string? GetAuthenticatedEnvelopeKey();
/// <summary>
/// Gets the current HttpContext user principal.
/// </summary>
ClaimsPrincipal? GetCurrentUser();
}
Key Methods:
IsAuthenticated(string envelopeKey): Validates user auth + envelope key matchGetAuthenticatedEnvelopeKey(): Extracts envelope key from claimsGetCurrentUser(): ReturnsClaimsPrincipalfor advanced scenarios
2. EnvelopeAuthService Implementation
Location: EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs
Dependencies:
IHttpContextAccessor: Access current HTTP contextILogger<EnvelopeAuthService>: Structured logging
Logic:
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:
ClaimTypes.NameIdentifier(standard .NET claim)"sub"(JWT standard claim)
3. Service Registration
Location: EnvelopeGenerator.Server/Program.cs
Added:
// SSR Authentication Service (for Envelope Receiver pages)
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.IEnvelopeAuthService,
EnvelopeGenerator.Server.Services.EnvelopeAuthService>();
Lifetime: Scoped (per-request, matches IHttpContextAccessor)
4. EnvelopeReceiverPage.razor Changes
Changes Made (REVERTED - To Be Re-Applied):
4.1 Using Statements
@using EnvelopeGenerator.Server.Services
4.2 Dependency Injection
Old:
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
New:
@inject IEnvelopeAuthService EnvelopeAuth
@inject IHttpClientFactory HttpClientFactory
4.3 Authentication Check in OnInitializedAsync()
Old:
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
if (!hasAccess) {
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
return;
}
New:
// ? SSR Authentication check via service
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
return;
}
Benefits:
- ? Synchronous (no HTTP overhead)
- ? Direct
HttpContext.Useraccess - ? No self-referencing HTTP calls
- ? Works in SSR context
4.4 Logout Method
Old:
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
New:
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 inProgram.cs) - Graceful error handling (logout errors shouldn't block redirect)
Remaining Tasks
? Completed
- ? Created
IEnvelopeAuthServiceinterface - ? Implemented
EnvelopeAuthServicewithHttpContextaccess - ? Registered service in
Program.cs - ?? REVERTED
EnvelopeReceiverPage.razorchanges (merge conflict)
? TODO (Next Agent)
1. Re-apply EnvelopeReceiverPage.razor Changes
File: EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor
Steps:
- Add using statement:
@using EnvelopeGenerator.Server.Services
- Replace injection:
@inject IEnvelopeAuthService EnvelopeAuth
@inject IHttpClientFactory HttpClientFactory
Remove:
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
- Update
OnInitializedAsync()authentication check:
// 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;
}
- Update
LogoutAsync()method:
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
<AuthorizeView>component (less clean) - Custom service provides more control + logging
Why Scoped Lifetime?
IHttpContextAccessoris scoped (per-request)EnvelopeAuthServicedepends onIHttpContextAccessor- Scoped ensures same
HttpContextthroughout 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)
// 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)
- ? Login with valid access code ? Cookie set
- ? Navigate to
/envelope/{key}? Page loads - ? Logout ? Cookie cleared, redirect
- ? Try accessing
/envelope/{key}without cookie ? Redirect to login - ? Try accessing
/envelope/{wrongKey}with valid cookie ? Redirect to login
Migration Checklist
- Create
IEnvelopeAuthServiceinterface - Implement
EnvelopeAuthService - Register service in
Program.cs - Re-apply
EnvelopeReceiverPage.razorchanges (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:
## 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
IHttpClientFactorywith base address configuration - SSR context requires
HttpContextto configure base address - Calling API endpoints from server-side component creates self-referencing requests
- Use
IEnvelopeAuthServicefor directHttpContext.Useraccess instead
Authentication Flow:
- JWT token stored in per-envelope cookie (
AuthTokenSignFLOWReceiver.{envelopeKey}) - JWT middleware validates token, sets
HttpContext.User EnvelopeAuthServicechecksClaimsPrincipal.FindFirst("sub")orNameIdentifier- Compares claim value with route parameter
{EnvelopeKey}
Claim Priority:
ClaimTypes.NameIdentifier(standard .NET)"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
falseeven when authenticated
? Do This Instead
// 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.Useraccess - 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
.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:
- Created SSR authentication service (
IEnvelopeAuthService/EnvelopeAuthService) - Registered service in DI container
- Updated
EnvelopeReceiverPage.razor(REVERTED due to merge)
What's Left:
- Re-apply
EnvelopeReceiverPage.razorchanges after merge - Test authentication flow
- Add unit tests
- Update documentation
Key Insight:
- WASM client services ? SSR server services
- Use
IHttpContextAccessorfor directHttpContext.Useraccess 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