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:
parent
642e93c6f2
commit
14b7b37a84
2 changed files with 72 additions and 29 deletions
|
@ -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
|
*
|
||||||
return <TryRefreshComponent key={Date.now()} />;
|
* 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 (
|
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>
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue