The "Strangler Fig" pattern is the de facto standard for modernizing monoliths, but it introduces a distinct architectural fracture: identity.
When you place a Next.js micro-frontend alongside a legacy ASP.NET 4.x application, they live in different runtimes. A browser cookie issued by ASP.NET WebForms (encrypted with MachineKey) is an opaque, undecipherable blob to a Node.js server. Consequently, users navigating from /legacy/dashboard to /next/profile effectively "log out" because the Next.js server cannot validate the session credentials.
This post details how to bridge that gap using YARP (Yet Another Reverse Proxy) as an authentication gateway, ensuring seamless session propagation without rewriting your legacy authentication logic immediately.
The Root Cause: Incompatible Encryption
The failure isn't in the transport; the browser successfully sends the cookie to both paths (assuming correct domain scope). The failure is in decryption.
- Legacy ASP.NET uses
machineKeywithinweb.configto encrypt Forms Authentication tickets or Session IDs. - Modern .NET (YARP) uses the Data Protection API (DPAPI).
- Next.js (Node) uses neither.
Even if you forward the cookie to Next.js, Node.js cannot natively decrypt a proprietary .NET binary format. To fix this, we must offload the decryption responsibility to the one component that understands the network topology: the YARP proxy.
The Solution: The Claims Transformation Pattern
Instead of teaching Next.js how to decrypt .NET cookies, we configure YARP to:
- Intercept the incoming request.
- Decrypt and validate the legacy Auth Cookie.
- Extract relevant User Claims (ID, Name, Roles).
- Inject these claims as HTTP Headers into the downstream request destined for Next.js.
- Next.js trusts these headers (verified via internal network security or HMAC) to hydrate the user session.
Step 1: Configure YARP to Read Legacy Cookies
First, your YARP instance (running on .NET 8) must be able to read the authentication ticket.
If your legacy app uses ASP.NET Core Identity, share the Data Protection Key Ring via Redis or a shared file share.
If your legacy app is ASP.NET 4.8 WebForms, use Microsoft.AspNetCore.SystemWebAdapters. This allows the .NET 8 host to read the FormsAuthentication cookie using the same keys as the legacy app.
YARP Program.cs Setup:
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
using Yarp.ReverseProxy.Transforms;
var builder = WebApplication.CreateBuilder(args);
// 1. Setup Data Protection to match the Legacy App
// If Legacy is .NET Core: Point to same Redis/Keyring
// If Legacy is Framework: Use SystemWebAdapters (omitted for brevity, assuming unified auth)
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"\\share\keys"))
.SetApplicationName("SharedAuthApp");
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = ".AspNetCore.Cookies"; // Or your legacy cookie name
options.Cookie.Domain = ".yourdomain.com";
});
// 2. Register YARP
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddTransforms(builderContext =>
{
// This is where we inject the identity into downstream requests
builderContext.AddRequestTransform(async transformContext =>
{
var user = transformContext.HttpContext.User;
// Only transform if user is authenticated
if (user.Identity?.IsAuthenticated == true)
{
// Serialize essential claims to JSON (base64 encoded to be header-safe)
var userData = new
{
id = user.FindFirst("sub")?.Value ?? user.Identity.Name,
email = user.FindFirst("email")?.Value,
roles = user.FindAll("role").Select(c => c.Value).ToArray()
};
var jsonBytes = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(userData);
var base64User = Convert.ToBase64String(jsonBytes);
// Inject header for Next.js
transformContext.ProxyRequest.Headers.Add("X-MS-CLIENT-PRINCIPAL", base64User);
}
});
});
var app = builder.Build();
app.UseAuthentication(); // Decrypts cookie -> populates HttpContext.User
app.UseAuthorization();
app.MapReverseProxy();
app.Run();
Step 2: Ingest Identity in Next.js Middleware
In Next.js (App Router), we use middleware.ts to intercept the request before it hits your page logic. We read the header injected by YARP and expose it to the application.
Security Note: In a production environment, ensure your Next.js server only accepts traffic from the YARP IP address, or implement a shared HMAC secret signature in the C# transform and verify it here to prevent header spoofing.
middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 1. Read the header injected by YARP
const principalHeader = request.headers.get('X-MS-CLIENT-PRINCIPAL');
const requestHeaders = new Headers(request.headers);
if (principalHeader) {
// 2. Decode the header (Base64 -> JSON)
// We treat this as trusted because it comes from our internal YARP proxy
const buffer = Buffer.from(principalHeader, 'base64');
const userJson = buffer.toString('utf-8');
// 3. Re-inject as a clean header for Server Components to consume easily
// or set a short-lived internal session cookie.
requestHeaders.set('x-user-profile', userJson);
} else {
// Handle unauthenticated state (optional: redirect to legacy login)
// return NextResponse.redirect(new URL('/legacy/login', request.url));
}
// Pass the modified headers to the application
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
Step 3: Consuming User State in Server Components
Now that the middleware has normalized the identity, we can create a reusable helper to access user data in our React Server Components.
lib/auth.ts
import { headers } from 'next/headers';
export interface UserProfile {
id: string;
email: string;
roles: string[];
}
export async function getCurrentUser(): Promise<UserProfile | null> {
const headersList = headers();
const userJson = headersList.get('x-user-profile');
if (!userJson) {
return null;
}
try {
return JSON.parse(userJson) as UserProfile;
} catch (error) {
console.error('Failed to parse user profile header', error);
return null;
}
}
app/page.tsx
import { getCurrentUser } from '@/lib/auth';
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user) {
return <div className="p-4 text-red-500">Access Denied</div>;
}
return (
<main className="p-8">
<h1 className="text-2xl font-bold">Welcome back, {user.email}</h1>
<div className="mt-4 p-4 border rounded bg-gray-50">
<h2 className="text-lg font-semibold">Legacy Session Data:</h2>
<pre className="mt-2 text-sm">{JSON.stringify(user, null, 2)}</pre>
</div>
{/*
This works because YARP handled the decryption.
Next.js simply renders based on trusted headers.
*/}
</main>
);
}
Why This Works
- Decoupling: Next.js doesn't need to know about ASP.NET MachineKeys, Data Protection, or XML configuration files. It simply consumes JSON.
- Performance: Decryption happens once at the edge (YARP). Next.js receives plain text headers, avoiding heavy crypto operations in the Node event loop.
- Security: The sensitive session cookie never touches the Next.js application code. It remains HttpOnly and secure, handled solely by the .NET stack that issued it. The header injection happens entirely within the server-side internal network.
Conclusion
Sharing state between monolithic and micro-frontend architectures is often the hardest part of a migration. By leveraging YARP's request transformation pipeline, we convert a complex cryptographic compatibility problem into a simple data mapping exercise. This allows you to keep your legacy auth system as the "source of truth" while aggressively migrating UI components to Next.js.