Skip to content

Commit 46f93b1

Browse files
committed
Cleanup implementation
1 parent d366c32 commit 46f93b1

File tree

8 files changed

+257
-200
lines changed

8 files changed

+257
-200
lines changed

examples/with-next-ssr-app-directory/app/components/home.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import { CallAPIButton } from "./callApiButton";
77
import { LinksComponent } from "./linksComponent";
88
import { SessionAuthForNextJS } from "./sessionAuthForNextJS";
99

10-
import { getSSRSession } from "supertokens-auth-react/nextjs/ssr";
10+
import { getSSRSession, init } from "supertokens-auth-react/nextjs/ssr";
1111
import { ssrConfig } from "../config/ssr";
1212

13+
init(ssrConfig());
14+
1315
export async function HomePage() {
1416
const cookiesStore = await cookies();
1517
const headersStore = await headers();
16-
const session = await getSSRSession(ssrConfig(), headersStore, cookiesStore, redirect);
18+
const session = await getSSRSession(cookiesStore, redirect);
1719
console.log(session);
1820

1921
/**

examples/with-next-ssr-app-directory/middleware.ts

Lines changed: 8 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export async function middleware(request: NextRequest & { session?: SessionConta
1818
}
1919

2020
if (request.nextUrl.pathname.startsWith("/api/auth/session/refresh") && request.method === "GET") {
21-
return refreshSession(ssrConfig(), request, NextResponse);
21+
return refreshSession(ssrConfig(), request);
2222
}
2323

2424
if (request.nextUrl.pathname.startsWith("/api/auth")) {
@@ -46,96 +46,18 @@ export async function middleware(request: NextRequest & { session?: SessionConta
4646
// https://github.com/vercel/next.js/issues/43704#issuecomment-2090798307
4747
// TL;DR: You can not access pathname in SSR and requests that originate from redirect()
4848
// do not have an origin header
49-
return NextResponse.next({
50-
headers: {
51-
"x-current-path": request.nextUrl.pathname,
52-
},
49+
const response = new Response(null, {
50+
status: 200,
5351
});
52+
response.headers.append(
53+
"set-cookie",
54+
`sCurrentPath=${request.nextUrl.pathname}; Path=/; HttpOnly; SameSite=Strict`
55+
);
56+
return response;
5457
}
5558

56-
const refreshTokenCookieName = "sRefreshToken";
57-
const refreshTokenHeaderName = "st-refresh-token";
58-
59-
// async function refreshSession(request: NextRequest) {
60-
// console.log("Attempting session refresh");
61-
// const refreshToken =
62-
// request.cookies.get(refreshTokenCookieName)?.value || request.headers.get(refreshTokenHeaderName);
63-
// if (!refreshToken) {
64-
// return NextResponse.redirect(new URL("/auth", request.url));
65-
// }
66-
//
67-
// const redirectTo = request.nextUrl.pathname;
68-
// console.log(`Should redirect to ${redirectTo}`);
69-
// try {
70-
// const refreshResponse = await fetch(`http://localhost:3000/api/auth/session/refresh`, {
71-
// method: "POST",
72-
// headers: {
73-
// "Content-Type": "application/json",
74-
// Cookie: `sRefreshToken=${refreshToken}`,
75-
// },
76-
// credentials: "include",
77-
// });
78-
// console.log("Performed session refresh request");
79-
//
80-
// const setCookieHeaders = refreshResponse.headers.getSetCookie();
81-
// console.log(refreshResponse);
82-
// console.log(refreshResponse.headers);
83-
// console.log("Cookies", setCookieHeaders);
84-
//
85-
// if (!setCookieHeaders.length) {
86-
// return NextResponse.redirect(new URL("/auth", request.url));
87-
// }
88-
//
89-
// const frontToken = refreshResponse.headers.get("front-token");
90-
// if (!frontToken) {
91-
// return NextResponse.redirect(new URL("/auth", request.url));
92-
// }
93-
//
94-
// let sAccessToken: string | null = null;
95-
// let sRefreshToken: string | null = null;
96-
//
97-
// const redirectTo = new URL("/", request.url);
98-
// const response = NextResponse.redirect(redirectTo);
99-
// for (const header of setCookieHeaders) {
100-
// if (header.includes("sAccessToken")) {
101-
// const match = header.match(/sAccessToken=([^;]+)/);
102-
// sAccessToken = match ? match[1] : null;
103-
// }
104-
// if (header.includes("sRefreshToken")) {
105-
// const match = header.match(/sRefreshToken=([^;]+)/);
106-
// sRefreshToken = match ? match[1] : null;
107-
// }
108-
// response.headers.append("set-cookie", header);
109-
// }
110-
//
111-
// response.headers.append("set-cookie", `sFrontToken=${frontToken}`);
112-
// response.headers.append("front-token", frontToken);
113-
// response.headers.append("frontToken", frontToken);
114-
// if (sAccessToken) {
115-
// response.headers.append("sAccessToken", sAccessToken);
116-
// response.cookies.set("sAccessToken", sAccessToken);
117-
// }
118-
// if (sRefreshToken) {
119-
// response.headers.append("sRefreshToken", sRefreshToken);
120-
//
121-
// response.cookies.set("sRefreshToken", sRefreshToken);
122-
// }
123-
//
124-
// response.cookies.set("sFrontToken", frontToken);
125-
// return response;
126-
// } catch (err) {
127-
// console.error("Error refreshing session");
128-
// console.error(err);
129-
// return NextResponse.redirect(new URL("/auth", request.url));
130-
// }
131-
// }
132-
13359
export const config = {
13460
matcher: [
13561
"/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|manifest.json|sw.js|workbox-|public/).*)",
13662
],
13763
};
138-
139-
// export const config = {
140-
// matcher: "/api/:path*",
141-
// };
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { appInfo } from "./appInfo";
2+
import { useRouter } from "next/navigation";
3+
import { SuperTokensConfig } from "supertokens-auth-react/lib/build/types";
4+
import { init } from "supertokens-auth-react/nextjs/middleware";
5+
6+
const routerInfo: { router?: ReturnType<typeof useRouter>; pathName?: string } = {};
7+
8+
export function setRouter(router: ReturnType<typeof useRouter>, pathName: string) {
9+
routerInfo.router = router;
10+
routerInfo.pathName = pathName;
11+
}
12+
13+
export const ssrConfig = (): SuperTokensConfig => {
14+
return {
15+
appInfo,
16+
enableDebugLogs: true,
17+
recipeList: [],
18+
};
19+
};
20+
21+
export const ensureSuperTokensSSRInit = () => {
22+
init(ssrConfig());
23+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { NextResponse } from "next/server";
2+
import type { NextRequest } from "next/server";
3+
import { SessionContainer } from "supertokens-node/recipe/session";
4+
import { ssrConfig } from "./config/ssr";
5+
import { refreshSession } from "supertokens-auth-react/nextjs/middleware";
6+
7+
export async function middleware(request: NextRequest & { session?: SessionContainer }) {
8+
if (request.nextUrl.pathname.startsWith("/api")) {
9+
if (request.nextUrl.pathname.startsWith("/api/auth/session/refresh") && request.method === "GET") {
10+
return refreshSession(ssrConfig(), request);
11+
}
12+
return NextResponse.next();
13+
}
14+
// Save the current path so that we can use it during SSR
15+
// Used to redirect the user to the correct path after login/refresh
16+
// https://github.com/vercel/next.js/issues/43704#issuecomment-2090798307
17+
// TL;DR: You can not access pathname in SSR and requests that originate from redirect()
18+
// do not have an origin header
19+
return NextResponse.next({
20+
headers: {
21+
"x-current-path": request.nextUrl.pathname,
22+
},
23+
});
24+
}
25+
26+
export const config = {
27+
matcher: [
28+
"/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|manifest.json|sw.js|workbox-|public/).*)",
29+
],
30+
};

examples/with-next-ssr-pages-directory/pages/index.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import React from "react";
22
import Head from "next/head";
33
import styles from "../styles/ProtectedHome.module.css";
4+
import type { GetServerSideProps, GetServerSidePropsContext } from "next";
45
import SessionReact from "supertokens-auth-react/recipe/session";
56
import SuperTokensReact from "supertokens-auth-react";
67
import { useSessionContext } from "supertokens-auth-react/recipe/session";
78
import { BlogsIcon, CelebrateIcon, GuideIcon, SeparatorLine, SignOutIcon } from "../assets/images";
89
import Image from "next/image";
910
import { recipeDetails } from "../config/frontendConfig";
1011

12+
import { getSSRSession } from "supertokens-auth-react/nextjs/ssr";
13+
import { ssrConfig } from "../config/ssr";
14+
1115
interface ILink {
1216
name: string;
1317
onClick: () => void;
@@ -89,6 +93,12 @@ function ProtectedPage() {
8993
);
9094
}
9195

96+
export const getServerSideProps = (async (context: GetServerSidePropsContext) => {
97+
const props = await getSSRSession(ssrConfig(), context.req.headers, context.req.cookies);
98+
99+
return props;
100+
}) satisfies GetServerSideProps<>;
101+
92102
export default function Home(props) {
93103
return (
94104
<SessionReact.SessionAuth>

lib/ts/nextjs/middleware.ts

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,83 @@ const REFRESH_TOKEN_HEADER_NAME = "st-refresh-token";
1111
const ANTI_CSRF_TOKEN_COOKIE_NAME = "sAntiCsrf";
1212
const ANTI_CSRF_TOKEN_HEADER_NAME = "anti-csrf";
1313

14+
const REDIRECT_ATTEMPT_MAX_COUNT = 5;
15+
const REDIRECT_PATH_PARAM_NAME = "stRedirectTo";
16+
const REDIRECT_ATTEMPT_COUNT_COOKIE_NAME = "sSsrSessionRefreshAttempt";
17+
1418
let AppInfo: SuperTokensNextjsConfig["appInfo"];
1519

16-
// export function superTokensMiddleware(config: SuperTokensNextjsConfig, request: NextRequest, response: NextResponse) {
20+
// export async function superTokensMiddeware(config: SuperTokensNextjsConfig): () => Promise<Response | void> {
1721
// AppInfo = config.appInfo;
1822
// if (config.enableDebugLogs) {
1923
// enableLogging();
2024
// }
21-
// const url = new URL(request.url);
22-
// if (url.pathname === "/auth/session/refresh") {
23-
// return refreshSession(request, response);
24-
// }
2525
//
26-
// // Save the current path so that we can use it during SSR
27-
// // Used to redirect the user to the correct path after login/refresh
28-
// return response.next({
29-
// headers: {
30-
// "x-current-path": url.pathname,
31-
// },
32-
// });
26+
// return (request: Request) => {
27+
// const url = new URL(request.url);
28+
// if (url.pathname.startsWith("/api")) {
29+
// if (request.headers.has("x-user-id")) {
30+
// console.warn(
31+
// "The FE tried to pass x-user-id, which is only supposed to be a backend internal header. Ignoring."
32+
// );
33+
// request.headers.delete("x-user-id");
34+
// }
35+
//
36+
// if (url.pathname.startsWith("/api/auth/session/refresh") && request.method === "GET") {
37+
// return refreshSession(ssrConfig(), request);
38+
// }
39+
//
40+
// if (url.pathname.startsWith("/api/auth")) {
41+
// // this hits our pages/api/auth/* endpoints
42+
// return;
43+
// }
44+
//
45+
// return withSession(request, async (err, session) => {
46+
// if (err) {
47+
// return NextResponse.json(err, { status: 500 });
48+
// }
49+
//
50+
// if (session === undefined) {
51+
// return NextResponse.next();
52+
// }
53+
// return NextResponse.next({
54+
// headers: {
55+
// "x-user-id": session.getUserId(),
56+
// },
57+
// });
58+
// });
59+
// }
60+
// // Save the current path so that we can use it during SSR
61+
// // Used to redirect the user to the correct path after login/refresh
62+
// // https://github.com/vercel/next.js/issues/43704#issuecomment-2090798307
63+
// // TL;DR: You can not access pathname in SSR and requests that originate from redirect()
64+
// // do not have an origin header
65+
// const response = NextResponse.next();
66+
// response.cookies.set("sCurrentPath", request.nextUrl.pathname);
67+
// };
3368
// }
3469

3570
export async function refreshSession(config: SuperTokensNextjsConfig, request: Request): Promise<Response> {
3671
AppInfo = config.appInfo;
3772
if (config.enableDebugLogs) {
3873
enableLogging();
3974
}
40-
console.log(request);
75+
76+
// Cancel the refresh cycle if an unforseen state is encountered
77+
const redirectAttemptNumber = getRedirectAttemptNumber(request);
78+
if (redirectAttemptNumber > REDIRECT_ATTEMPT_MAX_COUNT) {
79+
return redirectToAuthPage(request);
80+
}
81+
4182
const refreshToken =
4283
getCookie(request, REFRESH_TOKEN_COOKIE_NAME) || request.headers.get(REFRESH_TOKEN_HEADER_NAME);
4384
if (!refreshToken) {
4485
logDebugMessage("Refresh token not found");
4586
return redirectToAuthPage(request);
4687
}
4788

89+
const requestUrl = new URL(request.url);
90+
const redirectTo = requestUrl.searchParams.get(REDIRECT_PATH_PARAM_NAME) || "/";
4891
try {
4992
const tokens = await fetchNewTokens(refreshToken);
5093
const hasRequiredCookies = tokens.accessToken.cookie && tokens.refreshToken.cookie && tokens.frontToken.cookie;
@@ -53,35 +96,35 @@ export async function refreshSession(config: SuperTokensNextjsConfig, request: R
5396
logDebugMessage("Missing tokens from refresh response");
5497
return redirectToAuthPage(request);
5598
}
56-
const currentUrl = new URL(request.url);
57-
const redirectUrl = new URL(currentUrl.searchParams.get("redirectTo") || "/", request.url);
99+
const redirectUrl = new URL(redirectTo, request.url);
58100
const finalResponse = new Response(null, {
59101
status: 307,
60102
headers: {
61103
Location: redirectUrl.toString(),
62-
"x-current-path": redirectUrl.pathname,
63-
"x-st-ssr-session-refresh-attempt": "0",
64104
},
65105
});
106+
finalResponse.headers.append(FRONT_TOKEN_HEADER_NAME, tokens.frontToken.header as string);
66107
if (hasRequiredCookies) {
67-
finalResponse.headers.append(
68-
"set-cookie",
69-
`${tokens.accessToken.cookie as string},${tokens.refreshToken.cookie as string},${
70-
tokens.frontToken.cookie as string
71-
}`
72-
);
73-
finalResponse.headers.append("x-middleware-set-cookie", tokens.accessToken.cookie as string);
74-
finalResponse.headers.append("x-middleware-set-cookie", tokens.refreshToken.cookie as string);
75-
finalResponse.headers.append("x-middleware-set-cookie", tokens.frontToken.cookie as string);
108+
finalResponse.headers.append("set-cookie", `${tokens.accessToken.cookie as string}`);
109+
finalResponse.headers.append("set-cookie", `${tokens.refreshToken.cookie as string}`);
110+
finalResponse.headers.append("set-cookie", `${tokens.frontToken.cookie as string}`);
76111
}
77112
if (hasRequiredHeaders) {
78113
finalResponse.headers.append(REFRESH_TOKEN_HEADER_NAME, tokens.refreshToken.header as string);
79114
finalResponse.headers.append(ACCESS_TOKEN_HEADER_NAME, tokens.accessToken.header as string);
80-
finalResponse.headers.append(FRONT_TOKEN_HEADER_NAME, tokens.frontToken.header as string);
81-
if (tokens.antiCsrfToken.header) {
82-
finalResponse.headers.append(ANTI_CSRF_TOKEN_HEADER_NAME, tokens.antiCsrfToken.header);
83-
}
84115
}
116+
117+
if (tokens.antiCsrfToken.cookie) {
118+
finalResponse.headers.append("set-cookie", `${tokens.antiCsrfToken.cookie as string}`);
119+
} else if (tokens.antiCsrfToken.header) {
120+
finalResponse.headers.append(ANTI_CSRF_TOKEN_HEADER_NAME, tokens.antiCsrfToken.header);
121+
}
122+
finalResponse.headers.append(
123+
"set-cookie",
124+
`${REDIRECT_ATTEMPT_COUNT_COOKIE_NAME}=${
125+
redirectAttemptNumber + 1
126+
}; Path=/; Max-Age=10; HttpOnly; SameSite=Strict`
127+
);
85128
logDebugMessage("Attached new tokens to response");
86129
return finalResponse;
87130
} catch (err) {
@@ -162,3 +205,13 @@ function getCookie(request: Request, name: string) {
162205
.find((row) => row.startsWith(`${name}=`))
163206
?.split("=")[1];
164207
}
208+
209+
function getRedirectAttemptNumber(request: Request): number {
210+
const cookieValue = getCookie(request, REDIRECT_ATTEMPT_COUNT_COOKIE_NAME) || "1";
211+
212+
try {
213+
return parseInt(cookieValue);
214+
} catch (err) {
215+
return 1;
216+
}
217+
}

0 commit comments

Comments
 (0)