Update with-supertokens example (#66827)

Updates the with-supertokens example to replace `getSSRSession` usage
with manual JWT parsing in SSR as shown in
https://github.com/supertokens/create-supertokens-app/pull/107

Co-authored-by: Sam Ko <sam@vercel.com>
This commit is contained in:
Ankit Tiwari 2024-06-14 23:36:20 +05:30 committed by GitHub
parent 642e93c6f2
commit 14b7b37a84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 72 additions and 29 deletions

View file

@ -1,4 +1,4 @@
import { cookies, headers } from "next/headers"; import { cookies } from "next/headers";
import { TryRefreshComponent } from "./tryRefreshClientComponent"; import { TryRefreshComponent } from "./tryRefreshClientComponent";
import styles from "../page.module.css"; import styles from "../page.module.css";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@ -7,37 +7,72 @@ import { CelebrateIcon, SeparatorLine } from "../../assets/images";
import { CallAPIButton } from "./callApiButton"; import { CallAPIButton } from "./callApiButton";
import { LinksComponent } from "./linksComponent"; import { LinksComponent } from "./linksComponent";
import { SessionAuthForNextJS } from "./sessionAuthForNextJS"; import { SessionAuthForNextJS } from "./sessionAuthForNextJS";
import { getSSRSession } from "supertokens-node/nextjs"; import jwksClient from "jwks-rsa";
import { SessionContainer } from "supertokens-node/recipe/session"; import JsonWebToken from "jsonwebtoken";
import { ensureSuperTokensInit } from "../config/backend"; import type { JwtHeader, JwtPayload, SigningKeyCallback } from "jsonwebtoken";
import { appInfo } from "../config/appInfo";
ensureSuperTokensInit(); const client = jwksClient({
jwksUri: `${appInfo.apiDomain}${appInfo.apiBasePath}/jwt/jwks.json`,
});
function getAccessToken(): string | undefined {
return cookies().get("sAccessToken")?.value;
}
function getPublicKey(header: JwtHeader, callback: SigningKeyCallback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
callback(err);
} else {
const signingKey = key?.getPublicKey();
callback(null, signingKey);
}
});
}
async function verifyToken(token: string): Promise<JwtPayload> {
return new Promise((resolve, reject) => {
JsonWebToken.verify(token, getPublicKey, {}, (err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded as JwtPayload);
}
});
});
}
/**
* A helper function to retrieve session details on the server side.
*
* NOTE: This function does not use the getSSRSession function from the supertokens-node SDK
* because getSession can update the access token. These updated tokens would not be
* propagated to the client side, as request interceptors do not run on the server side.
*/
async function getSSRSessionHelper(): Promise<{ async function getSSRSessionHelper(): Promise<{
session: SessionContainer | undefined; accessTokenPayload: JwtPayload | undefined;
hasToken: boolean; hasToken: boolean;
hasInvalidClaims: boolean;
error: Error | undefined; error: Error | undefined;
}> { }> {
let session: SessionContainer | undefined; const accessToken = getAccessToken();
let hasToken = false; const hasToken = !!accessToken;
let hasInvalidClaims = false;
let error: Error | undefined = undefined;
try { try {
({ session, hasToken, hasInvalidClaims } = await getSSRSession( if (accessToken) {
cookies().getAll(), const decoded = await verifyToken(accessToken);
headers(), return { accessTokenPayload: decoded, hasToken, error: undefined };
)); }
} catch (err: any) { return { accessTokenPayload: undefined, hasToken, error: undefined };
error = err; } catch (error) {
if (error instanceof JsonWebToken.TokenExpiredError) {
return { accessTokenPayload: undefined, hasToken, error: undefined };
}
return { accessTokenPayload: undefined, hasToken, error: error as Error };
} }
return { session, hasToken, hasInvalidClaims, error };
} }
export async function HomePage() { export async function HomePage() {
const { session, hasToken, hasInvalidClaims, error } = const { accessTokenPayload, hasToken, error } = await getSSRSessionHelper();
await getSSRSessionHelper();
if (error) { if (error) {
return ( return (
@ -48,7 +83,8 @@ export async function HomePage() {
); );
} }
if (!session) { // `accessTokenPayload` will be undefined if it the session does not exist or has expired
if (accessTokenPayload === undefined) {
if (!hasToken) { if (!hasToken) {
/** /**
* This means that the user is not logged in. If you want to display some other UI in this * This means that the user is not logged in. If you want to display some other UI in this
@ -57,14 +93,19 @@ export async function HomePage() {
return redirect("/auth"); return redirect("/auth");
} }
if (hasInvalidClaims) { /**
return <SessionAuthForNextJS />; * This means that the session does not exist but we have session tokens for the user. In this case
} else { * the `TryRefreshComponent` will try to refresh the session.
// To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 *
* To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048
*/
return <TryRefreshComponent key={Date.now()} />; return <TryRefreshComponent key={Date.now()} />;
} }
}
/**
* SessionAuthForNextJS will handle proper redirection for the user based on the different session states.
* It will redirect to the login page if the session does not exist etc.
*/
return ( return (
<SessionAuthForNextJS> <SessionAuthForNextJS>
<div className={styles.homeContainer}> <div className={styles.homeContainer}>
@ -82,7 +123,7 @@ export async function HomePage() {
<div className={styles.innerContent}> <div className={styles.innerContent}>
<div>Your userID is:</div> <div>Your userID is:</div>
<div className={`${styles.truncate} ${styles.userId}`}> <div className={`${styles.truncate} ${styles.userId}`}>
{session.getUserId()} {accessTokenPayload.sub}
</div> </div>
<CallAPIButton /> <CallAPIButton />
</div> </div>

View file

@ -7,6 +7,8 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0",
"next": "latest", "next": "latest",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",