Migrate authentication to SSR service for EnvelopeReceiver
Migrated the `EnvelopeReceiverPage.razor` component from using a WASM client-side authentication service to a server-side rendering (SSR) authentication service. This resolves issues caused by self-referencing HTTP requests in SSR contexts. - Added `IEnvelopeAuthService` interface and `EnvelopeAuthService` implementation to validate user authentication and envelope key claims directly via `HttpContext.User`. - Registered `EnvelopeAuthService` in DI container with a scoped lifetime. - Updated `EnvelopeReceiverPage.razor` to use `IEnvelopeAuthService` for authentication checks and `IHttpClientFactory` for logout functionality (changes reverted due to merge conflict). - Improved authentication flow by eliminating HTTP overhead and ensuring compatibility with SSR. - Remaining tasks include re-applying page changes, testing, and updating documentation. This migration ensures a cleaner, more reliable authentication mechanism for SSR pages.
This commit is contained in:
@@ -23,7 +23,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B29
|
|||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
|
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
|
||||||
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
|
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
|
||||||
OPEN_SSR_TASK.md = OPEN_SSR_TASK.md
|
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"
|
||||||
|
|||||||
553
OPEN_SSR_TASK.md
553
OPEN_SSR_TASK.md
@@ -1,553 +0,0 @@
|
|||||||
# 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
|
|
||||||
{
|
|
||||||
/// <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:**
|
|
||||||
```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<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
|
|
||||||
```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 `<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)
|
|
||||||
```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
|
|
||||||
Reference in New Issue
Block a user