5b. Session verification in getServerSideProps
note
This is applicable for when verifying a session in getServerSideProps
or getInitialProps
.
For this guide, we will assume that we want to pass the logged in user's ID as a prop to a protected route. An easier method to achieve this would be to directly get the user ID from the frontend, but for this example, we will pass it from the server side.
accessToken
JWT in getServerSideProps
#
1) We parse the important
If using getInitialProps
, the method described below applies as well. The only difference is the way the props are returned (see comments in the code).
- Single app setup
- Multi app setup
import jwksClient from "jwks-rsa";
import JsonWebToken from "jsonwebtoken";
import type { JwtHeader, JwtPayload, SigningKeyCallback } from "jsonwebtoken";
import { GetServerSidePropsContext } from 'next';
const client = jwksClient({
jwksUri: "/.well-known/jwks.json",
async fetcher(jwksUri) {
return fetch(jwksUri).then((res) => res.json());
},
});
function getAccessToken(context: GetServerSidePropsContext ): string | undefined {
return context.req.cookies["sAccessToken"];
}
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 getSession or verifySession functions from the supertokens-node SDK
* because they 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(context: GetServerSidePropsContext): Promise<{
accessTokenPayload: JwtPayload | undefined;
hasToken: boolean;
error: Error | undefined;
}> {
const accessToken = getAccessToken(context);
const hasToken = !!accessToken;
try {
if (accessToken) {
const decoded = await verifyToken(accessToken);
return { accessTokenPayload: decoded, hasToken, error: undefined };
}
return { accessTokenPayload: undefined, hasToken, error: undefined };
} catch (error) {
return { accessTokenPayload: undefined, hasToken, error: undefined };
}
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { accessTokenPayload, error } = await getSSRSessionHelper(context);
if (error) {
throw error;
}
if (accessTokenPayload === undefined) {
// This occurs if the token has expired or doesn't exist.
// Either way, sending this response prompts the frontend to attempt a session refresh.
//
// Case 1: Token doesn't exist
// - The refresh will fail, and the user will be redirected to the login page.
//
// Case 2: Token has expired
// - The client will call the refresh API and update the session tokens.
return { props: { fromSupertokens: 'needs-refresh' } }
// or return {fromSupertokens: 'needs-refresh'} in case of getInitialProps
}
return {
props: { userId: accessTokenPayload.sub }
}
// or return { userId: accessTokenPayload.sub } in case of getInitialProps
}
import jwksClient from "jwks-rsa";
import JsonWebToken from "jsonwebtoken";
import type { JwtHeader, JwtPayload, SigningKeyCallback } from "jsonwebtoken";
import { GetServerSidePropsContext } from 'next';
const client = jwksClient({
jwksUri: "/.well-known/jwks.json",
async fetcher(jwksUri) {
return fetch(jwksUri).then((res) => res.json());
},
});
function getAccessToken(context: GetServerSidePropsContext ): string | undefined {
return context.req.cookies["sAccessToken"];
}
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 getSession or verifySession functions from the supertokens-node SDK
* because they 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(context: GetServerSidePropsContext): Promise<{
accessTokenPayload: JwtPayload | undefined;
hasToken: boolean;
error: Error | undefined;
}> {
const accessToken = getAccessToken(context);
const hasToken = !!accessToken;
try {
if (accessToken) {
const decoded = await verifyToken(accessToken);
return { accessTokenPayload: decoded, hasToken, error: undefined };
}
return { accessTokenPayload: undefined, hasToken, error: undefined };
} catch (error) {
return { accessTokenPayload: undefined, hasToken, error: undefined };
}
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { accessTokenPayload, error } = await getSSRSessionHelper(context);
if (error) {
throw error;
}
if (accessTokenPayload === undefined) {
// This occurs if the token has expired or doesn't exist.
// Either way, sending this response prompts the frontend to attempt a session refresh.
//
// Case 1: Token doesn't exist
// - The refresh will fail, and the user will be redirected to the login page.
//
// Case 2: Token has expired
// - The client will call the refresh API and update the session tokens.
return { props: { fromSupertokens: 'needs-refresh' } }
// or return {fromSupertokens: 'needs-refresh'} in case of getInitialProps
}
return {
props: { userId: accessTokenPayload.sub }
}
// or return { userId: accessTokenPayload.sub } in case of getInitialProps
}
caution
Don't use getSession or verifySession here. They might update the session tokens, and since our request interceptors don't run server-side, the updated token won't be propagated to the client side.
#
2) Doing manual refresh on the frontend- The following will refresh a session if needed, for all your website pages
- This goes in the
/pages/_app.tsx
file
import React, { useEffect } from "react";
import Session from 'supertokens-auth-react/recipe/session'
import { redirectToAuth } from 'supertokens-auth-react'
import { AppProps } from "next/app";
function MyApp({ Component, pageProps }: AppProps<{fromSupertokens: string}>) {
useEffect(() => {
async function doRefresh() {
// pageProps.fromSupertokens === 'needs-refresh' will be true
// when in getServerSideProps, getSession throws a TRY_REFRESH_TOKEN
// error.
if (pageProps.fromSupertokens === 'needs-refresh') {
if (await Session.attemptRefreshingSession()) {
// post session refreshing, we reload the page. This will
// send the new access token to the server, and then
// getServerSideProps will succeed
location.reload()
} else {
// the user's session has expired. So we redirect
// them to the login page
redirectToAuth()
}
}
}
doRefresh()
}, [pageProps.fromSupertokens])
if (pageProps.fromSupertokens === 'needs-refresh') {
// in case the frontend needs to refresh, we show nothing.
// Alternatively, you can show a spinner.
return null
}
// the below is already there by default
return <Component {...pageProps} />
}
export default MyApp
userId
returned by getServerSideProps in your component#
3) Consume the On success, getServerSideProps
returns
{
// Refer to Step 1)
props: { userId: accessTokenPayload.sub }
}
Therefore, the associated page can access the userId
like:
export default function Home(props: any) {
let userId = props.userId;
}