Skip to content

Commit

Permalink
DEVREL-990 Rework auth handling in middleware to allow unauthenticate…
Browse files Browse the repository at this point in the history
…d requests outside preview mode
  • Loading branch information
JiriLojda committed Sep 29, 2023
1 parent 5d433ac commit 001994b
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 48 deletions.
2 changes: 0 additions & 2 deletions lib/constants/cookies.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export const envIdCookieName = "currentEnvId";

export const previewApiKeyCookieName = "currentPreviewApiKey";

export const ignoreMissingApiKeyCookieName = "ignoreMissingApiKey";
66 changes: 36 additions & 30 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'

import { envIdCookieName, ignoreMissingApiKeyCookieName, previewApiKeyCookieName } from './lib/constants/cookies';
import { envIdCookieName, previewApiKeyCookieName } from './lib/constants/cookies';
import { createQueryString } from './lib/routing';
import { defaultEnvId } from './lib/utils/env';

Expand All @@ -20,11 +20,16 @@ export const middleware = (request: NextRequest) => {
handleArticlesCategoryRoute,
handleArticlesCategoryWithNoPaginationRoute(currentEnvId),
handleExplicitProjectRoute(currentEnvId),
handleEmptyApiKeyCookie(currentEnvId),
handleEmptyCookies
];

return handlers.reduce((prevResponse, handler) => handler(prevResponse, request),
NextResponse.rewrite(new URL(`/${currentEnvId}${request.nextUrl.pathname ? `${request.nextUrl.pathname}` : ''}`, request.url)))
const initialResponse = request.nextUrl.pathname.startsWith("/api/")
? NextResponse.next()
: NextResponse.rewrite(new URL(`/${currentEnvId}${request.nextUrl.pathname ? `${request.nextUrl.pathname}` : ''}`, request.url));


return handlers.reduce((prevResponse, handler) => handler(prevResponse, request), initialResponse);
};

const handleExplicitProjectRoute = (currentEnvId: string) => (prevResponse: NextResponse, request: NextRequest) => {
Expand All @@ -36,33 +41,44 @@ const handleExplicitProjectRoute = (currentEnvId: string) => (prevResponse: Next
return prevResponse;
}

if (request.nextUrl.pathname.includes("/api/exit-preview") && request.cookies.get(ignoreMissingApiKeyCookieName)) {
return prevResponse;
}

if (routeEnvId === defaultEnvId) {
const res = NextResponse.redirect(new URL(createUrlWithQueryString(remainingUrl, request.nextUrl.searchParams), request.nextUrl.origin));
res.cookies.set(envIdCookieName, routeEnvId, cookieOptions);
res.cookies.set(previewApiKeyCookieName, '', cookieOptions);
const res = NextResponse.redirect(new URL(createUrlWithQueryString(remainingUrl, request.nextUrl.searchParams.entries()), request.nextUrl.origin));
res.cookies.set(envIdCookieName, defaultEnvId, cookieOptions);
res.cookies.set(previewApiKeyCookieName, "", cookieDeleteOptions);

return res
}

if (routeEnvId !== currentEnvId || !request.cookies.get(previewApiKeyCookieName)) {
if (routeEnvId !== currentEnvId) {
const originalPath = encodeURIComponent(createUrlWithQueryString(remainingUrl, request.nextUrl.searchParams.entries()));
const redirectPath = `/api/exit-preview?callback=${encodeURIComponent(`/getPreviewApiKey?path=${originalPath}`)}`;
const res = NextResponse.redirect(new URL(redirectPath, request.url));
const redirectPath = `/api/exit-preview?callback=${originalPath}`; // We need to exit preview, because the old preview API key is in preview data
const res = NextResponse.redirect(new URL(redirectPath, request.nextUrl.origin));

res.cookies.set(envIdCookieName, routeEnvId, cookieOptions);
res.cookies.set(previewApiKeyCookieName, '', cookieOptions);
res.cookies.set(ignoreMissingApiKeyCookieName, "true", cookieOptions);
res.cookies.set(previewApiKeyCookieName, "", cookieDeleteOptions);

return res;
}

return NextResponse.redirect(new URL(`${remainingUrl ?? ''}?${createQueryString(Object.fromEntries(request.nextUrl.searchParams.entries()))}`, request.nextUrl.origin));
}

const handleEmptyApiKeyCookie = (currentEnvId: string) => (prevResponse: NextResponse, request: NextRequest) => {
if (request.cookies.get(previewApiKeyCookieName)?.value || !request.nextUrl.pathname.startsWith("/api/preview")) {
return prevResponse;
}

if (currentEnvId === defaultEnvId) {
const res = NextResponse.redirect(request.url); // Workaround for this issue https://github.com/vercel/next.js/issues/49442, we cannot set cookies on NextResponse.next()
res.cookies.set(previewApiKeyCookieName, KONTENT_PREVIEW_API_KEY, cookieOptions);
return res;
}

const originalPath = encodeURIComponent(createUrlWithQueryString(request.nextUrl.pathname, request.nextUrl.searchParams.entries()));
const redirectPath = `/getPreviewApiKey?path=${originalPath}`;
return NextResponse.redirect(new URL(redirectPath, request.nextUrl.origin));
};

const handleArticlesRoute = (currentEnvId: string) => (prevResponse: NextResponse, request: NextRequest) => request.nextUrl.pathname === '/articles'
? NextResponse.rewrite(new URL(`/${currentEnvId}/articles/category/all/page/1`, request.url))
: prevResponse;
Expand All @@ -78,35 +94,25 @@ const handleArticlesCategoryWithNoPaginationRoute = (currentEnvId: string) => (p
: prevResponse

const handleEmptyCookies = (prevResponse: NextResponse, request: NextRequest) => {
if (!request.cookies.get(envIdCookieName)?.value) {
prevResponse.cookies.set(envIdCookieName, defaultEnvId, cookieOptions)
if (!request.cookies.get(envIdCookieName)?.value && !prevResponse.cookies.get(envIdCookieName)) {
prevResponse.cookies.set(envIdCookieName, defaultEnvId, cookieOptions);
}
if (!request.cookies.get(envIdCookieName)?.value || request.cookies.get(envIdCookieName)?.value === defaultEnvId) {
prevResponse.cookies.set(previewApiKeyCookieName, KONTENT_PREVIEW_API_KEY, cookieOptions)
}


return prevResponse;
}

const createUrlWithQueryString = (url: string | undefined, searchParams: any) => {
const createUrlWithQueryString = (url: string | undefined, searchParams: IterableIterator<[string, string]>) => {
const entries = Object.fromEntries(searchParams);

return Object.entries(entries).length > 0 ? `${url ?? ''}?${createQueryString(entries)}` : url ?? '';
}

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.png (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.png|getPreviewApiKey|logo.png|callback).*)',
'/((?!_next/static|_next/image|favicon.png|getPreviewApiKey|logo.png|callback).*)',
'/'
],
};

const cookieOptions = { path: '/', sameSite: 'none', secure: true } as const;
const cookieDeleteOptions = { ...cookieOptions, maxAge: -1 } as const; // It seems that res.cookies.delete doesn't propagate provided options (we need sameSite: none) so we use this as a workaround
6 changes: 1 addition & 5 deletions pages/api/exit-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { NextApiHandler } from "next";

import { ignoreMissingApiKeyCookieName } from "../../lib/constants/cookies";

const handler: NextApiHandler = (req, res) => {
// Exit the current user from "Preview Mode". This function accepts no args.
res.clearPreviewData();

res.setHeader("Set-Cookie", `${ignoreMissingApiKeyCookieName}=; Path=/; SameSite=None; Secure; Max-Age=-1`);

// Redirect the user back to the index page.
// Might be implemented return URL by the query string.
res.redirect(typeof req.query.callback === "string" ? req.query.callback : "/");
res.redirect(req.query.callback && typeof req.query.callback === "string" ? req.query.callback : "/");
}

export default handler;
10 changes: 2 additions & 8 deletions pages/api/preview.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { NextApiHandler, NextApiResponse } from "next";

import { envIdCookieName, previewApiKeyCookieName } from "../../lib/constants/cookies";
import { previewApiKeyCookieName } from "../../lib/constants/cookies";
import { ResolutionContext, resolveUrlPath } from "../../lib/routing";
import { defaultEnvId } from "../../lib/utils/env";

const handler: NextApiHandler = async (req, res) => {
// TODO move secret to env variables
Expand All @@ -11,13 +10,8 @@ const handler: NextApiHandler = async (req, res) => {
return;
}

const currentEnvId = req.cookies[envIdCookieName];
const currentPreviewApiKey = req.cookies[previewApiKeyCookieName];

if (!currentPreviewApiKey && currentEnvId !== defaultEnvId) {
res.redirect(`/api/exit-preview?callback=${`/getPreviewApiKey?path=${encodeURIComponent(req.url ?? '')}`}`);
return;
}
// Enable Preview Mode by setting the cookies
res.setPreviewData({ currentPreviewApiKey });
const newCookieHeader = makeCookiesCrossOrigin(res);
Expand All @@ -27,7 +21,7 @@ const handler: NextApiHandler = async (req, res) => {

const path = resolveUrlPath({
type: req.query.type.toString(),
slug: req.query.slug.toString()
slug: req.query.slug.toString(),
} as ResolutionContext);

// Redirect to the path from the fetched post
Expand Down
5 changes: 2 additions & 3 deletions pages/callback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { internalApiDomain } from "../lib/utils/env";
const CallbackPage: React.FC = () => {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
const { replace } = router;

useEffect(() => {
if (!router.isReady) {
Expand Down Expand Up @@ -97,9 +96,9 @@ const CallbackPage: React.FC = () => {
setError(api_key.error);
}

replace(authResult?.appState ?? '/');
window.location.replace(authResult?.appState ?? '/'); // router.replace changes the "slug" query parameter so we can't use it here, because this parameter is used when calling the /api/preview endpoint
});
}, [router.isReady, replace]);
}, [router.isReady]);

if (error) {
return <BuildError>{error}</BuildError>;
Expand Down

5 comments on commit 001994b

@vercel
Copy link

@vercel vercel bot commented on 001994b Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 001994b Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 001994b Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 001994b Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 001994b Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.