Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* failure mode is a silently dead ANALYTICS tab — pin its behavior.
*/
import { NextRequest } from "next/server";
import { handleProductAnalyticsTab } from "./middleware";
import { handleProductAnalyticsTab, restoreOryCookies } from "./middleware";

const rewriteTarget = (url: string): string | null =>
handleProductAnalyticsTab(new NextRequest(url))?.headers.get(
Expand Down Expand Up @@ -39,3 +39,19 @@ it("ignores non-matching requests", () => {
rewriteTarget("https://source.coop/products/new?tab=analytics"),
).toBeNull();
});

describe("restoreOryCookies", () => {
it("decodes the padding Next.js encodes in a server action redirect", () => {
expect(
restoreOryCookies("theme=dark; ory_session_abc=MTcx_a-b%3D%3D; x=a%3Db"),
).toBe("theme=dark; ory_session_abc=MTcx_a-b==; x=a%3Db");
expect(restoreOryCookies("ory_kratos_session=YQ%3D")).toBe(
"ory_kratos_session=YQ=",
);
});

it("leaves an unencoded header alone", () => {
expect(restoreOryCookies("ory_session_abc=MTcx==; x=a%3Db")).toBeNull();
expect(restoreOryCookies(null)).toBeNull();
});
});
27 changes: 27 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,26 @@ export const handleProductAnalyticsTab = (
return null;
};

/**
* Undoes Next.js's percent-encoding of Ory's session cookie.
*
* A server action that calls `redirect()` renders its target with an internal
* fetch whose cookie header Next rebuilds with `encodeURIComponent`, turning
* the base64 padding in Ory's session cookie into `%3D`. Ory rejects that
* cookie, so the page it lands on renders signed out. Ory's cookie values are
* base64url, which never contains `%`, so decoding them restores the original.
*
* Exported for tests; returns the repaired header, or null when nothing changed.
*/
export const restoreOryCookies = (cookie: string | null): string | null => {
if (!cookie?.includes("%")) return null;
const restored = cookie.replace(
/(\bory_\w*session\w*=)([^;]*)/gi,
(_, name: string, value: string) => name + decodeURIComponent(value),
);
return restored === cookie ? null : restored;
};

const ory = createOryMiddleware({});

// Paths the Ory middleware proxies (self-service flows, session checks). For
Expand Down Expand Up @@ -138,6 +158,13 @@ export const middleware = async (request: NextRequest) => {
const analyticsRewrite = handleProductAnalyticsTab(request);
if (analyticsRewrite) return analyticsRewrite;

const cookie = restoreOryCookies(request.headers.get("cookie"));
if (cookie) {
const headers = new Headers(request.headers);
headers.set("cookie", cookie);
return NextResponse.next({ request: { headers } });
}

return NextResponse.next();
};

Expand Down
Loading