Skip to content

Instantly share code, notes, and snippets.

@Thinkscape
Last active June 19, 2024 01:11
Show Gist options
  • Save Thinkscape/c391eeb833b01f9fb259998debaf046b to your computer and use it in GitHub Desktop.
Save Thinkscape/c391eeb833b01f9fb259998debaf046b to your computer and use it in GitHub Desktop.
Oversized JWT early detection for Next.js + kinde SDK
// /src/api/auth/[kindeAuth]/route.ts
import { handleAuth } from "@kinde-oss/kinde-auth-nextjs/server";
import jwt_decode from "jwt-decode";
import { type RedirectError } from "next/dist/client/components/redirect";
import { type ResponseCookies } from "next/dist/compiled/@edge-runtime/cookies";
import { NextResponse, type NextRequest } from "next/server";
import { hostName } from "~/env.mjs";
import { logger } from "~/server/logger";
export const runtime = "edge";
export const dynamic = "force-dynamic";
/**
* @see https://www.ietf.org/rfc/rfc2109.txt
* @see https://superuser.com/questions/97625/what-is-the-maximum-size-of-a-cookie-and-how-many-can-be-stored-in-a-browser-fo
*/
const MAX_SINGLE_COOKIE_SIZE = 4093; // iOS/iPadOS Safari limit
const MAX_COOKIES_SIZE_PER_DOMAIN = 4093; // iOS/iPadOS Safari limit
const kindeHandler = handleAuth();
export const GET = async (
req: NextRequest,
opts: { params: { kindeAuth: string } },
) => {
try {
return await kindeHandler(req, opts);
} catch (e) {
if ((e as Error).message === "NEXT_REDIRECT") {
const cookies = (e as RedirectError<"">).mutableCookies;
// Check all cookies' lengths
if (opts.params.kindeAuth === "kinde_callback") {
for (const cookie of cookies.getAll()) {
if (cookie.value.length >= MAX_SINGLE_COOKIE_SIZE) {
logger.warn(
`kinde_callback emitted oversized cookie which might get dropped by mobile browsers`,
{
user: {
email: decodeJWT(cookies, "id_token")?.email,
kindeId: decodeJWT(cookies, "access_token")?.sub,
},
req: {
ip: req.ip,
forwarded: req.headers.get("x-forwarded-for"),
},
cookie: {
name: cookie.name,
size: cookie.value.length,
},
},
);
}
}
}
// Sum up size of cookie sizes, grouping by domain
const cookieSizes: Record<string, { size: number; names: string[] }> = {};
for (const cookie of cookies.getAll()) {
const domain = cookie.domain || hostName;
cookieSizes[domain] ??= { size: 0, names: [] };
cookieSizes[domain]!.size += cookie.value.length;
cookieSizes[domain]!.names.push(cookie.name);
}
for (const [domain, { size, names }] of Object.entries(cookieSizes)) {
if (size > MAX_COOKIES_SIZE_PER_DOMAIN) {
logger.warn(
`kinde_callback emitted cookies that exceed the maximum cookie size per domain and might get dropped by mobile browsers`,
{
user: {
email: decodeJWT(cookies, "id_token")?.email,
kindeId: decodeJWT(cookies, "access_token")?.sub,
},
req: {
ip: req.ip,
forwarded: req.headers.get("x-forwarded-for"),
},
cookies: {
domain,
size,
names,
},
},
);
}
}
}
throw e;
}
};
// Work-around Kinde SDK lack of proper JWT expiry handling
export const POST = () =>
NextResponse.json(
{
error: {
name: "unauthorized",
message: "Logged out",
status: 401,
},
},
{
status: 401,
},
);
function decodeJWT(cookies: ResponseCookies, cookieName: string | undefined) {
if (!cookieName) {
return undefined;
}
try {
const rawValue = cookies.get(cookieName)?.value;
if (!rawValue) return undefined;
const decoded = jwt_decode(rawValue) as Record<string, string>;
return decoded;
} catch (e) {
return undefined;
}
}
@Thinkscape
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment