Skip to content

Commit 001994b

Browse files
committed
DEVREL-990 Rework auth handling in middleware to allow unauthenticated requests outside preview mode
1 parent 5d433ac commit 001994b

File tree

5 files changed

+41
-48
lines changed

5 files changed

+41
-48
lines changed

lib/constants/cookies.ts

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
export const envIdCookieName = "currentEnvId";
22

33
export const previewApiKeyCookieName = "currentPreviewApiKey";
4-
5-
export const ignoreMissingApiKeyCookieName = "ignoreMissingApiKey";

middleware.ts

+36-30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NextRequest, NextResponse } from 'next/server'
22

3-
import { envIdCookieName, ignoreMissingApiKeyCookieName, previewApiKeyCookieName } from './lib/constants/cookies';
3+
import { envIdCookieName, previewApiKeyCookieName } from './lib/constants/cookies';
44
import { createQueryString } from './lib/routing';
55
import { defaultEnvId } from './lib/utils/env';
66

@@ -20,11 +20,16 @@ export const middleware = (request: NextRequest) => {
2020
handleArticlesCategoryRoute,
2121
handleArticlesCategoryWithNoPaginationRoute(currentEnvId),
2222
handleExplicitProjectRoute(currentEnvId),
23+
handleEmptyApiKeyCookie(currentEnvId),
2324
handleEmptyCookies
2425
];
2526

26-
return handlers.reduce((prevResponse, handler) => handler(prevResponse, request),
27-
NextResponse.rewrite(new URL(`/${currentEnvId}${request.nextUrl.pathname ? `${request.nextUrl.pathname}` : ''}`, request.url)))
27+
const initialResponse = request.nextUrl.pathname.startsWith("/api/")
28+
? NextResponse.next()
29+
: NextResponse.rewrite(new URL(`/${currentEnvId}${request.nextUrl.pathname ? `${request.nextUrl.pathname}` : ''}`, request.url));
30+
31+
32+
return handlers.reduce((prevResponse, handler) => handler(prevResponse, request), initialResponse);
2833
};
2934

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

39-
if (request.nextUrl.pathname.includes("/api/exit-preview") && request.cookies.get(ignoreMissingApiKeyCookieName)) {
40-
return prevResponse;
41-
}
42-
4344
if (routeEnvId === defaultEnvId) {
44-
const res = NextResponse.redirect(new URL(createUrlWithQueryString(remainingUrl, request.nextUrl.searchParams), request.nextUrl.origin));
45-
res.cookies.set(envIdCookieName, routeEnvId, cookieOptions);
46-
res.cookies.set(previewApiKeyCookieName, '', cookieOptions);
45+
const res = NextResponse.redirect(new URL(createUrlWithQueryString(remainingUrl, request.nextUrl.searchParams.entries()), request.nextUrl.origin));
46+
res.cookies.set(envIdCookieName, defaultEnvId, cookieOptions);
47+
res.cookies.set(previewApiKeyCookieName, "", cookieDeleteOptions);
4748

4849
return res
4950
}
5051

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

5657
res.cookies.set(envIdCookieName, routeEnvId, cookieOptions);
57-
res.cookies.set(previewApiKeyCookieName, '', cookieOptions);
58-
res.cookies.set(ignoreMissingApiKeyCookieName, "true", cookieOptions);
58+
res.cookies.set(previewApiKeyCookieName, "", cookieDeleteOptions);
5959

6060
return res;
6161
}
6262

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

66+
const handleEmptyApiKeyCookie = (currentEnvId: string) => (prevResponse: NextResponse, request: NextRequest) => {
67+
if (request.cookies.get(previewApiKeyCookieName)?.value || !request.nextUrl.pathname.startsWith("/api/preview")) {
68+
return prevResponse;
69+
}
70+
71+
if (currentEnvId === defaultEnvId) {
72+
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()
73+
res.cookies.set(previewApiKeyCookieName, KONTENT_PREVIEW_API_KEY, cookieOptions);
74+
return res;
75+
}
76+
77+
const originalPath = encodeURIComponent(createUrlWithQueryString(request.nextUrl.pathname, request.nextUrl.searchParams.entries()));
78+
const redirectPath = `/getPreviewApiKey?path=${originalPath}`;
79+
return NextResponse.redirect(new URL(redirectPath, request.nextUrl.origin));
80+
};
81+
6682
const handleArticlesRoute = (currentEnvId: string) => (prevResponse: NextResponse, request: NextRequest) => request.nextUrl.pathname === '/articles'
6783
? NextResponse.rewrite(new URL(`/${currentEnvId}/articles/category/all/page/1`, request.url))
6884
: prevResponse;
@@ -78,35 +94,25 @@ const handleArticlesCategoryWithNoPaginationRoute = (currentEnvId: string) => (p
7894
: prevResponse
7995

8096
const handleEmptyCookies = (prevResponse: NextResponse, request: NextRequest) => {
81-
if (!request.cookies.get(envIdCookieName)?.value) {
82-
prevResponse.cookies.set(envIdCookieName, defaultEnvId, cookieOptions)
97+
if (!request.cookies.get(envIdCookieName)?.value && !prevResponse.cookies.get(envIdCookieName)) {
98+
prevResponse.cookies.set(envIdCookieName, defaultEnvId, cookieOptions);
8399
}
84-
if (!request.cookies.get(envIdCookieName)?.value || request.cookies.get(envIdCookieName)?.value === defaultEnvId) {
85-
prevResponse.cookies.set(previewApiKeyCookieName, KONTENT_PREVIEW_API_KEY, cookieOptions)
86-
}
87-
88100

89101
return prevResponse;
90102
}
91103

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

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

98110
export const config = {
99111
matcher: [
100-
/*
101-
* Match all request paths except for the ones starting with:
102-
* - api (API routes)
103-
* - _next/static (static files)
104-
* - _next/image (image optimization files)
105-
* - favicon.png (favicon file)
106-
*/
107-
'/((?!api|_next/static|_next/image|favicon.png|getPreviewApiKey|logo.png|callback).*)',
112+
'/((?!_next/static|_next/image|favicon.png|getPreviewApiKey|logo.png|callback).*)',
108113
'/'
109114
],
110115
};
111116

112117
const cookieOptions = { path: '/', sameSite: 'none', secure: true } as const;
118+
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

pages/api/exit-preview.tsx

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import { NextApiHandler } from "next";
22

3-
import { ignoreMissingApiKeyCookieName } from "../../lib/constants/cookies";
4-
53
const handler: NextApiHandler = (req, res) => {
64
// Exit the current user from "Preview Mode". This function accepts no args.
75
res.clearPreviewData();
86

9-
res.setHeader("Set-Cookie", `${ignoreMissingApiKeyCookieName}=; Path=/; SameSite=None; Secure; Max-Age=-1`);
10-
117
// Redirect the user back to the index page.
128
// Might be implemented return URL by the query string.
13-
res.redirect(typeof req.query.callback === "string" ? req.query.callback : "/");
9+
res.redirect(req.query.callback && typeof req.query.callback === "string" ? req.query.callback : "/");
1410
}
1511

1612
export default handler;

pages/api/preview.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { NextApiHandler, NextApiResponse } from "next";
22

3-
import { envIdCookieName, previewApiKeyCookieName } from "../../lib/constants/cookies";
3+
import { previewApiKeyCookieName } from "../../lib/constants/cookies";
44
import { ResolutionContext, resolveUrlPath } from "../../lib/routing";
5-
import { defaultEnvId } from "../../lib/utils/env";
65

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

14-
const currentEnvId = req.cookies[envIdCookieName];
1513
const currentPreviewApiKey = req.cookies[previewApiKeyCookieName];
1614

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

2822
const path = resolveUrlPath({
2923
type: req.query.type.toString(),
30-
slug: req.query.slug.toString()
24+
slug: req.query.slug.toString(),
3125
} as ResolutionContext);
3226

3327
// Redirect to the path from the fetched post

pages/callback.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { internalApiDomain } from "../lib/utils/env";
1111
const CallbackPage: React.FC = () => {
1212
const router = useRouter();
1313
const [error, setError] = useState<string | null>(null);
14-
const { replace } = router;
1514

1615
useEffect(() => {
1716
if (!router.isReady) {
@@ -97,9 +96,9 @@ const CallbackPage: React.FC = () => {
9796
setError(api_key.error);
9897
}
9998

100-
replace(authResult?.appState ?? '/');
99+
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
101100
});
102-
}, [router.isReady, replace]);
101+
}, [router.isReady]);
103102

104103
if (error) {
105104
return <BuildError>{error}</BuildError>;

0 commit comments

Comments
 (0)