Skip to content

Commit ebadbed

Browse files
committed
improv: add an improved way of refreshing the session during SSR
Update the function and add types Add more info about the proposal Account for pages directory in the function implementation Update types and clean things up Add comments Add nextjs init function Add the jose library Fix refreshing and add example Fix build and token config Fix payload processing Load config fron env variables Add a redirect location during refresh Refresh token from middleware Move everything in the library Rename files and update function singatures Use the correct refresh path Use normal responses instead of nextresponse Use normal request instead of nextrequest Cleanup implementation Revoke session inside the middleware Code review fixes Add tests for getSSRSession Add tests for middleware
1 parent bfe9f61 commit ebadbed

File tree

390 files changed

+27935
-47883
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

390 files changed

+27935
-47883
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
NEXT_PUBLIC_SUPERTOKENS_APP_NAME=test
2+
NEXT_PUBLIC_SUPERTOKENS_API_DOMAIN=http://localhost:3000
3+
NEXT_PUBLIC_SUPERTOKENS_WEBSITE_DOMAIN=http://localhost:3000
4+
NEXT_PUBLIC_SUPERTOKENS_API_BASE_PATH=/api/auth
5+
NEXT_PUBLIC_SUPERTOKENS_WEBSITE_BASE_PATH=/auth
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
27+
# local env files
28+
.env*.local
29+
30+
# vercel
31+
.vercel
32+
33+
# typescript
34+
*.tsbuildinfo
35+
next-env.d.ts
36+
37+
# VSCode
38+
.vscode
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# SuperTokens App with Next.js app directory
2+
3+
This is a simple application that is protected by SuperTokens. This app uses the Next.js app directory.
4+
5+
## How to use
6+
7+
### Using `create-next-app`
8+
9+
- Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
10+
11+
```bash
12+
npx create-next-app --example with-supertokens with-supertokens-app
13+
```
14+
15+
```bash
16+
yarn create next-app --example with-supertokens with-supertokens-app
17+
```
18+
19+
```bash
20+
pnpm create next-app --example with-supertokens with-supertokens-app
21+
```
22+
23+
- Run `yarn install`
24+
25+
- Run `npm run dev` to start the application on `http://localhost:3000`.
26+
27+
### Using `create-supertokens-app`
28+
29+
- Run the following command
30+
31+
```bash
32+
npx create-supertokens-app@latest --frontend=next
33+
```
34+
35+
- Select the option to use the app directory
36+
37+
Follow the instructions after `create-supertokens-app` has finished
38+
39+
## Notes
40+
41+
- To know more about how this app works and to learn how to customise it based on your use cases refer to the [SuperTokens Documentation](https://supertokens.com/docs/guides)
42+
- We have provided development OAuth keys for the various built-in third party providers in the `/app/config/backend.ts` file. Feel free to use them for development purposes, but **please create your own keys for production use**.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { getAppDirRequestHandler } from "supertokens-node/nextjs";
2+
import Session, { refreshSessionWithoutRequestResponse } from "supertokens-node/recipe/session";
3+
import { NextRequest, NextResponse } from "next/server";
4+
import { ensureSuperTokensInit } from "../../../config/backend";
5+
import { cookies } from "next/headers";
6+
7+
ensureSuperTokensInit();
8+
9+
const handleCall = getAppDirRequestHandler();
10+
11+
// input
12+
// { refreshSessionWithoutRequestResponse }
13+
// async function
14+
//
15+
16+
export async function GET(request: NextRequest) {
17+
if (request.method === "GET" && request.url.includes("/session/refresh")) {
18+
return refreshSession(request);
19+
}
20+
const res = await handleCall(request);
21+
if (!res.headers.has("Cache-Control")) {
22+
// This is needed for production deployments with Vercel
23+
res.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
24+
}
25+
return res;
26+
}
27+
28+
export async function POST(request: NextRequest) {
29+
return handleCall(request);
30+
}
31+
32+
export async function DELETE(request: NextRequest) {
33+
return handleCall(request);
34+
}
35+
36+
export async function PUT(request: NextRequest) {
37+
return handleCall(request);
38+
}
39+
40+
export async function PATCH(request: NextRequest) {
41+
return handleCall(request);
42+
}
43+
44+
export async function HEAD(request: NextRequest) {
45+
return handleCall(request);
46+
}
47+
48+
const refreshTokenCookieName = "sRefreshToken";
49+
const refreshTokenHeaderName = "st-refresh-token";
50+
async function refreshSession(request: NextRequest) {
51+
console.log("Attempting session refresh");
52+
const cookiesFromReq = await cookies();
53+
54+
const refreshToken =
55+
request.cookies.get(refreshTokenCookieName)?.value || request.headers.get(refreshTokenHeaderName);
56+
if (!refreshToken) {
57+
return NextResponse.redirect(new URL("/auth", request.url));
58+
}
59+
60+
const redirectTo = new URL("/", request.url);
61+
62+
try {
63+
const refreshResponse = await fetch(`http://localhost:3000/api/auth/session/refresh`, {
64+
method: "POST",
65+
headers: {
66+
"Content-Type": "application/json",
67+
Cookie: `sRefreshToken=${refreshToken}`,
68+
},
69+
credentials: "include",
70+
});
71+
// console.log("Performed session refresh request");
72+
// console.log(refreshResponse);
73+
// console.log(refreshResponse.headers);
74+
// console.log(await refreshResponse.text());
75+
76+
const setCookieHeaders = refreshResponse.headers.getSetCookie();
77+
const frontToken = refreshResponse.headers.get("front-token");
78+
if (!frontToken) {
79+
return NextResponse.redirect(new URL("/auth", request.url));
80+
}
81+
82+
// TODO: Check for csrf token
83+
if (!setCookieHeaders.length) {
84+
return NextResponse.redirect(new URL("/auth", request.url));
85+
}
86+
87+
const response = NextResponse.redirect(redirectTo);
88+
let sAccessToken: string | null = null;
89+
let sRefreshToken: string | null = null;
90+
for (const header of setCookieHeaders) {
91+
if (header.includes("sAccessToken")) {
92+
const match = header.match(/sAccessToken=([^;]+)/);
93+
sAccessToken = match ? match[1] : null;
94+
}
95+
if (header.includes("sRefreshToken")) {
96+
const match = header.match(/sRefreshToken=([^;]+)/);
97+
sRefreshToken = match ? match[1] : null;
98+
}
99+
response.headers.append("set-cookie", header);
100+
}
101+
102+
response.headers.append("set-cookie", `sFrontToken=${frontToken}`);
103+
response.headers.append("front-token", frontToken);
104+
response.headers.append("frontToken", frontToken);
105+
if (sAccessToken) {
106+
response.headers.append("sAccessToken", sAccessToken);
107+
108+
cookiesFromReq.set("sAccessToken", sAccessToken);
109+
}
110+
if (sRefreshToken) {
111+
response.headers.append("sRefreshToken", sRefreshToken);
112+
113+
cookiesFromReq.set("sRefreshToken", sRefreshToken);
114+
}
115+
116+
cookiesFromReq.set("sFrontToken", frontToken);
117+
118+
// console.log(sAccessToken, sRefreshToken);
119+
120+
return response;
121+
} catch (err) {
122+
console.error("Error refreshing session");
123+
console.error(err);
124+
return NextResponse.redirect(new URL("/auth", request.url));
125+
}
126+
}
127+
128+
// async function saveTokensFromHeaders(response: Response) {
129+
// logDebugMessage("saveTokensFromHeaders: Saving updated tokens from the response headers");
130+
//
131+
// const refreshToken = response.headers.get("st-refresh-token");
132+
// if (refreshToken !== null) {
133+
// logDebugMessage("saveTokensFromHeaders: saving new refresh token");
134+
// await setToken("refresh", refreshToken);
135+
// }
136+
//
137+
// const accessToken = response.headers.get("st-access-token");
138+
// if (accessToken !== null) {
139+
// logDebugMessage("saveTokensFromHeaders: saving new access token");
140+
// await setToken("access", accessToken);
141+
// }
142+
//
143+
// const frontToken = response.headers.get("front-token");
144+
// if (frontToken !== null) {
145+
// logDebugMessage("saveTokensFromHeaders: Setting sFrontToken: " + frontToken);
146+
// await FrontToken.setItem(frontToken);
147+
// updateClockSkewUsingFrontToken({ frontToken, responseHeaders: response.headers });
148+
// }
149+
// const antiCsrfToken = response.headers.get("anti-csrf");
150+
// if (antiCsrfToken !== null) {
151+
// // At this point, the session has either been newly created or refreshed.
152+
// // Thus, there's no need to call getLocalSessionState with tryRefresh: true.
153+
// // Calling getLocalSessionState with tryRefresh: true will cause a refresh loop
154+
// // if cookie writes are disabled.
155+
// const tok = await getLocalSessionState(false);
156+
// if (tok.status === "EXISTS") {
157+
// logDebugMessage("saveTokensFromHeaders: Setting anti-csrf token");
158+
// await AntiCsrfToken.setItem(tok.lastAccessTokenUpdate, antiCsrfToken);
159+
// }
160+
// }
161+
// }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ensureSuperTokensInit } from "@/app/config/backend";
2+
import { NextResponse, NextRequest } from "next/server";
3+
import { withSession } from "supertokens-node/nextjs";
4+
5+
ensureSuperTokensInit();
6+
7+
export function GET(request: NextRequest) {
8+
return withSession(request, async (err, session) => {
9+
if (err) {
10+
return NextResponse.json(err, { status: 500 });
11+
}
12+
if (!session) {
13+
return new NextResponse("Authentication required", { status: 401 });
14+
}
15+
16+
return NextResponse.json({
17+
note: "Fetch any data from your application for authenticated user after using verifySession middleware",
18+
userId: session.getUserId(),
19+
sessionHandle: session.getHandle(),
20+
accessTokenPayload: session.getAccessTokenPayload(),
21+
});
22+
});
23+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { redirectToAuth } from "supertokens-auth-react";
5+
import SuperTokens from "supertokens-auth-react/ui";
6+
import { PreBuiltUIList } from "../../config/frontend";
7+
8+
export default function Auth() {
9+
// if the user visits a page that is not handled by us (like /auth/random), then we redirect them back to the auth page.
10+
const [loaded, setLoaded] = useState(false);
11+
12+
console.log("running this");
13+
14+
useEffect(() => {
15+
if (SuperTokens.canHandleRoute(PreBuiltUIList) === false) {
16+
redirectToAuth({ redirectBack: false });
17+
} else {
18+
setLoaded(true);
19+
}
20+
}, []);
21+
22+
if (loaded) {
23+
return SuperTokens.getRoutingComponent(PreBuiltUIList);
24+
}
25+
26+
return null;
27+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"use client";
2+
3+
import styles from "../page.module.css";
4+
5+
export const CallAPIButton = () => {
6+
const fetchUserData = async () => {
7+
const userInfoResponse = await fetch("http://localhost:3000/api/user");
8+
9+
alert(JSON.stringify(await userInfoResponse.json()));
10+
};
11+
12+
return (
13+
<div onClick={fetchUserData} className={styles.sessionButton}>
14+
Call API
15+
</div>
16+
);
17+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { cookies, headers } from "next/headers";
2+
import styles from "../page.module.css";
3+
import { redirect } from "next/navigation";
4+
import Image from "next/image";
5+
import { CelebrateIcon, SeparatorLine } from "../../assets/images";
6+
import { CallAPIButton } from "./callApiButton";
7+
import { LinksComponent } from "./linksComponent";
8+
import { SessionAuthForNextJS } from "./sessionAuthForNextJS";
9+
10+
import { getSSRSession, init } from "supertokens-auth-react/nextjs/ssr";
11+
import { ssrConfig } from "../config/ssr";
12+
13+
init(ssrConfig());
14+
15+
export async function HomePage() {
16+
const cookiesStore = await cookies();
17+
const session = await getSSRSession(cookiesStore, redirect);
18+
console.log(session);
19+
20+
/**
21+
* SessionAuthForNextJS will handle proper redirection for the user based on the different session states.
22+
* It will redirect to the login page if the session does not exist etc.
23+
*/
24+
return (
25+
<SessionAuthForNextJS>
26+
<div className={styles.homeContainer}>
27+
<div className={styles.mainContainer}>
28+
<div className={`${styles.topBand} ${styles.successTitle} ${styles.bold500}`}>
29+
<Image src={CelebrateIcon} alt="Login successful" className={styles.successIcon} /> Login
30+
successful
31+
</div>
32+
<div className={styles.innerContent}>
33+
<div>Your userID is:</div>
34+
<div className={`${styles.truncate} ${styles.userId}`}>{session.userId}</div>
35+
<CallAPIButton />
36+
</div>
37+
</div>
38+
<LinksComponent />
39+
<Image className={styles.separatorLine} src={SeparatorLine} alt="separator" />
40+
</div>
41+
</SessionAuthForNextJS>
42+
);
43+
}

0 commit comments

Comments
 (0)