Files
EnvelopeGenerator/OPEN_SSR_TASK.md
TekH 71e375d6ea 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.
2026-06-24 16:08:29 +02:00

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:

  • 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

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 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<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:

  1. ClaimTypes.NameIdentifier (standard .NET claim)
  2. "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.User access
  • ? 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 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:
@using EnvelopeGenerator.Server.Services
  1. Replace injection:
@inject IEnvelopeAuthService EnvelopeAuth
@inject IHttpClientFactory HttpClientFactory

Remove:

@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
  1. 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;
}
  1. 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?

  • 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)

// 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

  • Create IEnvelopeAuthService interface
  • Implement EnvelopeAuthService
  • 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:

## 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

// 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

  • 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:

  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