[object Object]

← back to Designer Wallcoverings

Verify local session cookie with HMAC (close TK-11776 auth-bypass twin)

387873882b796c6f5db1d7be6f4ecb6a61fbb872 · 2026-09-22 11:13:20 -0700 · Steve Abrams

The local `session` cookie was accepted on existence alone in middleware and
minted unsigned as base64(user:timestamp) at login — the same forgeable class
TK-11776 #4 closed for dw_central_session, so `session=<anything>` still reached
the product-minting routes. Login now signs via signDWCentralSession; middleware
and /api/auth/check now HMAC-verify instead of trusting presence. Fails closed on
an unset DW_SESSION_SECRET. No other code reads/decodes the session cookie.

TK-11786 review finding #1 (block-on-merge).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwQJGSbGmK5rDzRsCqvVEc

Files touched

Diff

commit 387873882b796c6f5db1d7be6f4ecb6a61fbb872
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 22 11:13:20 2026 -0700

    Verify local session cookie with HMAC (close TK-11776 auth-bypass twin)
    
    The local `session` cookie was accepted on existence alone in middleware and
    minted unsigned as base64(user:timestamp) at login — the same forgeable class
    TK-11776 #4 closed for dw_central_session, so `session=<anything>` still reached
    the product-minting routes. Login now signs via signDWCentralSession; middleware
    and /api/auth/check now HMAC-verify instead of trusting presence. Fails closed on
    an unset DW_SESSION_SECRET. No other code reads/decodes the session cookie.
    
    TK-11786 review finding #1 (block-on-merge).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01DwQJGSbGmK5rDzRsCqvVEc
---
 .../app/api/auth/check/route.ts                    |  9 +++--
 .../app/api/auth/login/route.ts                    |  8 +++--
 DW-Programming/ImportNewSkufromURL/middleware.ts   | 39 +++++++++-------------
 3 files changed, 27 insertions(+), 29 deletions(-)

diff --git a/DW-Programming/ImportNewSkufromURL/app/api/auth/check/route.ts b/DW-Programming/ImportNewSkufromURL/app/api/auth/check/route.ts
index 6430651f..79ba46fb 100644
--- a/DW-Programming/ImportNewSkufromURL/app/api/auth/check/route.ts
+++ b/DW-Programming/ImportNewSkufromURL/app/api/auth/check/route.ts
@@ -1,11 +1,14 @@
 import { NextResponse } from 'next/server';
 import { cookies } from 'next/headers';
+import { verifyDWCentralSession } from '@/lib/dw-central-session';
 
 export async function GET() {
   const cookieStore = await cookies();
   const session = cookieStore.get('session');
 
-  return NextResponse.json({
-    authenticated: !!session?.value
-  });
+  // Verify the HMAC signature, not mere presence (TK-11776 follow-up): otherwise the
+  // frontend would treat a forged base64 cookie as authenticated.
+  const authenticated = await verifyDWCentralSession(session?.value, process.env.DW_SESSION_SECRET);
+
+  return NextResponse.json({ authenticated });
 }
diff --git a/DW-Programming/ImportNewSkufromURL/app/api/auth/login/route.ts b/DW-Programming/ImportNewSkufromURL/app/api/auth/login/route.ts
index b248a6fb..42a4155b 100644
--- a/DW-Programming/ImportNewSkufromURL/app/api/auth/login/route.ts
+++ b/DW-Programming/ImportNewSkufromURL/app/api/auth/login/route.ts
@@ -1,4 +1,5 @@
 import { NextRequest, NextResponse } from 'next/server';
+import { signDWCentralSession } from '@/lib/dw-central-session';
 
 // Credentials must be supplied via DW_BASIC_AUTH in "user:password" form.
 // Fail-closed at module load — never default to a hardcoded literal.
@@ -19,8 +20,11 @@ export async function POST(request: NextRequest) {
     const { username, password } = body;
 
     if (username === USERNAME && password === PASSWORD) {
-      // Create a session token (simple implementation)
-      const sessionToken = Buffer.from(`${username}:${Date.now()}`).toString('base64');
+      // HMAC-sign the local session token with DW_SESSION_SECRET (TK-11776 follow-up):
+      // the middleware now VERIFIES this cookie, so an unsigned base64("user:ts") value is
+      // no longer accepted anywhere. signDWCentralSession throws if the secret is unset →
+      // the catch below returns 500 (fail-closed: never mint an unsigned/forgeable session).
+      const sessionToken = await signDWCentralSession(username, process.env.DW_SESSION_SECRET);
 
       // Set cookie using response headers
       const response = NextResponse.json({ success: true });
diff --git a/DW-Programming/ImportNewSkufromURL/middleware.ts b/DW-Programming/ImportNewSkufromURL/middleware.ts
index f9489ad8..0ec4d30a 100644
--- a/DW-Programming/ImportNewSkufromURL/middleware.ts
+++ b/DW-Programming/ImportNewSkufromURL/middleware.ts
@@ -1,25 +1,14 @@
 import { NextResponse } from 'next/server';
 import type { NextRequest } from 'next/server';
+import { verifyDWCentralSession } from '@/lib/dw-central-session';
 
-// DW Central SSO: Check if user has a valid DW Central session
-function verifyDWCentralSession(token: string): boolean {
-  try {
-    // DW Central uses base64 encoded "username:timestamp" format
-    const decoded = Buffer.from(token, 'base64').toString('utf-8');
-    const [username, timestamp] = decoded.split(':');
-    if (username && timestamp) {
-      const sessionTime = parseInt(timestamp, 10);
-      const now = Date.now();
-      const maxAge = 86400000; // 24 hours
-      return (now - sessionTime) < maxAge;
-    }
-    return false;
-  } catch {
-    return false;
-  }
-}
+// DW Central SSO cookies are now HMAC-signed (TK-11776 finding #4). The verifier lives
+// in @/lib/dw-central-session and REQUIRES DW_SESSION_SECRET — an unsigned/forged cookie
+// (incl. every legacy base64('user:'+Date.now()) token) is rejected. The issuer at
+// DW Central must sign with the SAME secret using that module's signDWCentralSession, or
+// logins break. Fails closed when the secret is unset.
 
-export function middleware(request: NextRequest) {
+export async function middleware(request: NextRequest) {
   const isLoginPage = request.nextUrl.pathname === '/login';
   const isAuthApi = request.nextUrl.pathname.startsWith('/api/auth');
   const isDashboard = request.nextUrl.pathname === '/apps-dashboard' || request.nextUrl.pathname.startsWith('/api/pm2');
@@ -32,17 +21,19 @@ export function middleware(request: NextRequest) {
 
   // Check for DW Central SSO session first (from dw.greendomainbrokers.com)
   const dwCentralToken = request.cookies.get('dw_central_session')?.value;
-  if (dwCentralToken && verifyDWCentralSession(dwCentralToken)) {
+  if (dwCentralToken && (await verifyDWCentralSession(dwCentralToken, process.env.DW_SESSION_SECRET))) {
     return NextResponse.next();
   }
 
-  // Check for local session
-  const session = request.cookies.get('session');
-  if (!session) {
-    return NextResponse.redirect(new URL('/login', request.url));
+  // Check for local session — now HMAC-VERIFIED (TK-11776 follow-up), not existence-only.
+  // Previously ANY non-empty `session` cookie passed; the login route mints an HMAC-signed
+  // token via signDWCentralSession, so verification here rejects every unsigned/forged value.
+  const session = request.cookies.get('session')?.value;
+  if (session && (await verifyDWCentralSession(session, process.env.DW_SESSION_SECRET))) {
+    return NextResponse.next();
   }
 
-  return NextResponse.next();
+  return NextResponse.redirect(new URL('/login', request.url));
 }
 
 export const config = {

← b8ec6402 auto-data-snapshot: 2026-09-22T11:11:00 (3 data files) — sho  ·  back to Designer Wallcoverings  ·  Apply TK-11786 review fixes #2/#3/#6/#7 (gate 4th writer, fa e3301879 →