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 styles from "../page.module.css";
import { redirect } from "next/navigation";
@ -7,37 +7,72 @@ import { CelebrateIcon, SeparatorLine } from "../../assets/images";
import { CallAPIButton } from "./callApiButton";
import { LinksComponent } from "./linksComponent";
import { SessionAuthForNextJS } from "./sessionAuthForNextJS";
import { getSSRSession } from "supertokens-node/nextjs";
import { SessionContainer } from "supertokens-node/recipe/session";
import { ensureSuperTokensInit } from "../config/backend";
import jwksClient from "jwks-rsa";
import JsonWebToken from "jsonwebtoken";
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<{
session: SessionContainer | undefined;
accessTokenPayload: JwtPayload | undefined;
hasToken: boolean;
hasInvalidClaims: boolean;
error: Error | undefined;
}> {
let session: SessionContainer | undefined;
let hasToken = false;
let hasInvalidClaims = false;
let error: Error | undefined = undefined;
const accessToken = getAccessToken();
const hasToken = !!accessToken;
try {
({ session, hasToken, hasInvalidClaims } = await getSSRSession(
cookies().getAll(),
headers(),
));
} catch (err: any) {
error = err;
if (accessToken) {
const decoded = await verifyToken(accessToken);
return { accessTokenPayload: decoded, hasToken, error: undefined };
}
return { accessTokenPayload: undefined, hasToken, error: undefined };
} 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() {
const { session, hasToken, hasInvalidClaims, error } =
await getSSRSessionHelper();
const { accessTokenPayload, hasToken, error } = await getSSRSessionHelper();
if (error) {
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) {
/**
* 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");
}
if (hasInvalidClaims) {
return <SessionAuthForNextJS />;
} else {
// 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()} />;
}
/**
* This means that the session does not exist but we have session tokens for the user. In this case
* 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
*/
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 (
<SessionAuthForNextJS>
<div className={styles.homeContainer}>
@ -82,7 +123,7 @@ export async function HomePage() {
<div className={styles.innerContent}>
<div>Your userID is:</div>
<div className={`${styles.truncate} ${styles.userId}`}>
{session.getUserId()}
{accessTokenPayload.sub}
</div>
<CallAPIButton />
</div>

View file

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