Skip to content

feat: Next.js SSR Improvements #877

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open

feat: Next.js SSR Improvements #877

wants to merge 6 commits into from

Conversation

bcbogdan
Copy link
Contributor

@bcbogdan bcbogdan commented Feb 4, 2025

Summary of change

The following PR improves SSR support in Next.js applications.
To get the session data in the context of server side rendering you now have to call getSSRSession either inside a Server Component, or in getServerSideProps (for older next applications).
There's two parts to this change:

Fetching the session during SSR

The logic for this is kept in the ssr.ts file. The function reads the active tokens and has 3 possible outcomes:

  • redirect to the login page if there was an issue (unable to read the tokens, token payload was invalid)
  • redirect to the refresh api if the tokens have expired or if the front token payload is different from the access token payload
  • return the session value

Refreshing a session

The getSSRSession function redirects to a refresh endpoint that we do not expose from our existing API.
Hence, this change adds logic inside the next.js middleware to handle such a request. The custom middleware calls the backend API using a fetch and updates the response with the new tokens.

This part is now kept mostly in the middleware.ts file, in the react library. It's there just to make it easy to review during this stage. It should be moved in the node SDK package as a separate, Nextjs specific API.

Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Bonus points for screenshots and videos!)

Documentation changes

(If relevant, please create a PR in our docs repo, or create a checklist here highlighting the necessary changes)

Checklist for important updates

  • Changelog has been updated
  • frontendDriverInterfaceSupported.json file has been updated (if needed)
  • Changes to the version if needed
    • In package.json
    • In package-lock.json
    • In lib/ts/version.ts
  • Had run npm run build-pretty
  • Had installed and ran the pre-commit hook
  • Issue this PR against the latest non released version branch.
    • To know which one it is, run find the latest released tag (git tag) in the format vX.Y.Z, and then find the latest branch (git branch --all) whose X.Y is greater than the latest released tag.
    • If no such branch exists, then create one from the latest released branch.
  • If added a new recipe interface, then make sure that the implementation of it uses NON arrow functions only (like someFunc: function () {..}).
  • If I added a new recipe, I also added the recipe entry point into the size-limit section of package.json with the size limit set to the current size rounded up.
  • If I added a new recipe, I also added the recipe entry point into the rollup.config.mjs
  • If I added a new login method, I modified the list in lib/ts/types.ts
  • If I added a factor id, I modified the list in lib/ts/recipe/multifactorauth/types.ts

Remaining TODOs for this PR

  • Item1
  • Item2

Copy link

github-actions bot commented Feb 12, 2025

size-limit report 📦

Path Size
lib/build/index.js 24.58 KB (+0.25% 🔺)
recipe/session/index.js 25.29 KB (+0.31% 🔺)
recipe/session/prebuiltui.js 30 KB (+0.2% 🔺)
recipe/thirdparty/index.js 32.37 KB (+0.19% 🔺)
recipe/emailpassword/index.js 11.74 KB (+0.68% 🔺)
recipe/emailverification/index.js 8.03 KB (+0.67% 🔺)
recipe/passwordless/index.js 15.64 KB (+0.55% 🔺)
recipe/emailverification/prebuiltui.js 34.73 KB (+0.37% 🔺)
recipe/thirdparty/prebuiltui.js 53.91 KB (+0.25% 🔺)
recipe/emailpassword/prebuiltui.js 40.91 KB (+0.3% 🔺)
recipe/passwordless/prebuiltui.js 128.89 KB (+0.12% 🔺)
recipe/multitenancy/index.js 6.9 KB (+1.05% 🔺)
recipe/multifactorauth/index.js 11.68 KB (+0.72% 🔺)
recipe/multifactorauth/prebuiltui.js 33.7 KB (+0.36% 🔺)
recipe/oauth2provider/index.js 7.65 KB (+0.74% 🔺)
recipe/oauth2provider/prebuiltui.js 32.07 KB (+0.29% 🔺)

@bcbogdan bcbogdan force-pushed the feat/nextjs-ssr branch 2 times, most recently from b306f85 to ad89bbe Compare February 12, 2025 21:57
@bcbogdan bcbogdan changed the title Next.js SSR Improvements feat: Next.js SSR Improvements Feb 24, 2025
@bcbogdan bcbogdan force-pushed the feat/nextjs-ssr branch 2 times, most recently from 8231792 to 7c23d35 Compare February 24, 2025 14:26
@bcbogdan bcbogdan requested a review from porcellus February 24, 2025 14:42
@bcbogdan bcbogdan force-pushed the feat/nextjs-ssr branch 6 times, most recently from ebadbed to 3f9b519 Compare March 4, 2025 10:49
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
@bcbogdan bcbogdan force-pushed the feat/nextjs-ssr branch from db16f4e to 3b1973c Compare May 5, 2025 07:52
@bcbogdan bcbogdan force-pushed the feat/nextjs-ssr branch from ff038f8 to 0c1ecca Compare May 7, 2025 07:58
@bcbogdan bcbogdan requested a review from porcellus May 7, 2025 08:01
accessTokenPayload,
doesSessionExist: true,
loading: false,
invalidClaims: [],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should somehow communicate that this doesn't do claim validation, but maybe docs are enough.

cookies: CookiesObject | CookiesStore
): Promise<{ state: SSRSessionState; session?: LoadedSessionContext }> {
const frontToken =
getCookieValue(cookies, FRONT_TOKEN_COOKIE_NAME) || getCookieValue(cookies, FRONT_TOKEN_HEADER_NAME);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FRONT_TOKEN_HEADER_NAME doesn't appear as a cookie I think.

Comment on lines +351 to +355
function defaultIsApiRequest(request: Request): boolean {
const requestUrl = new URL(request.url);
const refreshPath = getRefreshAPIPath();
return requestUrl.pathname.startsWith(refreshPath);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this isn't right, or the name is misleading.

* @returns The server action return value
**/
static async authenticateServerAction<T extends (session: LoadedSessionContext) => Promise<K>, K>(action: T) {
let loadedSessionContext: LoadedSessionContext | undefined = undefined;
static async confirmAuthenticationAndCallServerAction<T extends () => Promise<K>, K>(action: T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a shorter name would be better.

Suggested change
static async confirmAuthenticationAndCallServerAction<T extends () => Promise<K>, K>(action: T) {
static async ensureSessionAndCall<T extends () => Promise<K>, K>(action: T) {

Comment on lines 212 to 220
function getRefreshAPIPath(): string {
const apiPath = SuperTokensNextjsSSRAPIWrapper.getConfigOrThrow().appInfo.apiBasePath || DEFAULT_API_PATH;
return `${apiPath}/session/refresh`;
}

function getRefreshLocation(redirectPath: string): string {
return `${SESSION_REFRESH_API_PATH}?${REDIRECT_PATH_PARAM_NAME}=${redirectPath}`;
const refreshAPIPath = getRefreshAPIPath();
return `${refreshAPIPath}?${REDIRECT_PATH_PARAM_NAME}=${redirectPath}`;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the first name is also present in the middleware file, you could indicate in the name that one path is meant for the frontend/as a full page redirect.

throw new Error("JWT header missing alg (Algorithm)");
}

const jwksResponse = await fetch(jwksUrl);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will reload the jwks on every action call, could we cache this somehow? Also, I'm not necessarily opposed to using jose if it doesn't mess with our bundle sizes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants