Skip to content
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

SSR support for SessionAuth #790

Open
1 task
sasha240100 opened this issue Jan 24, 2024 · 3 comments
Open
1 task

SSR support for SessionAuth #790

sasha240100 opened this issue Jan 24, 2024 · 3 comments

Comments

@sasha240100
Copy link

sasha240100 commented Jan 24, 2024

TODOs:

Key takeaways about SSR support for SessionAuth

  1. For support of SSR, SessionAuth needs to handle an initial context (initialSessionAuthContext property from feat: SessionAuth SSR support (initialSessionAuthContext) #789).
  2. There are several ways of SSR implementation and they differ a lot. The most popular (NextJS), [with App Router] separates server components (execute on server only) from client components (execute on server [partially] & client) and there are many restrictions that cause a need in additional component layers. A few examples:
    1. The only data that can be passed has to be valid JSON (primitives), no functions or classes allowed.
      1. This means that the initial context can’t be computed inside a "client component" like SessionAuth
    2. All the component logic has to be in separate files marked with 'use client'; or 'use server'; depending on the components purpose.
    3. This leads to the following structure of nested components (abstraction) required to do what SessionAuth does on both server & client. (see below)
  3. With custom SSR implementation outside NextJS [App router], it could be possible to combine all the logic in a single component, if the same component is rendered exactly the same on both server & client. However with NextJS [App Router] it is not possible, and the following structure is needed to be in place.

Example NextJS Server SessionAuth structure

  • Page component
    • ServerSessionAuth component (server component) - an implementation of the SessionAuth logic on server side (retrieving session data from request, redirection and rendering logic)
      • ClientSessionAuth component [OPTIONAL] (client component) - needed in case you need to provide custom function properties like onSessionExpired or overrideGlobalClaimValidators
        • SessionAuth component (client component), from supertokens-auth-react
      • OR SessionAuth component (client component), from supertokens-auth-react

Restriction: it's only possible to pass JSON data between ServerSessionAuth <> ClientSessionAuth (see above)

Implementation

Example of ServerSessionAuth implementation (NextJS)
app/components/serverSessionAuth.tsx:

'use server'; // We mark NextJS that this is a server component

import { PropsWithChildren } from 'react';
import { SessionContainer } from 'supertokens-node/recipe/session';
import { getSSRSession, getInitialSessionAuthContext } from "supertokens-node/nextjs";
import { redirect } from "next/navigation";
import { TryRefreshComponent } from "@/app/components/tryRefreshClientComponent";
import { cookies, headers } from "next/headers";
import { SessionAuth, SessionAuthProps } from 'supertokens-auth-react/recipe/session';

// A helper function to catch error from
// https://supertokens.com/docs/thirdpartyemailpassword/nextjs/app-directory/server-components-requests 
async function getSSRSessionHelper(): Promise<{
    session: SessionContainer | undefined;
    hasToken: boolean;
    hasInvalidClaims: boolean;
    error: Error | undefined;
}> {
    let session: SessionContainer | undefined;
    let hasToken = false;
    let hasInvalidClaims = false;
    let error: Error | undefined = undefined;

    try {
        ({ session, hasToken, hasInvalidClaims } = await getSSRSession(cookies().getAll(), headers()));
    } catch (err: any) {
        error = err;
    }
    return { session, hasToken, hasInvalidClaims, error };
}

// We don't need initialSessionAuthContext as it is computed internally inside ServerSessionAuth
type ServerSessionAuthProps = PropsWithChildren<Pick<SessionAuthProps, 'requireAuth' | 'doRedirection'>>;

export default async function ServerSessionAuth(props: ServerSessionAuthProps) {
  const { session, hasToken, hasInvalidClaims, error } = await getSSRSessionHelper();

  if (error) {
    return <div>Something went wrong while trying to get the session. Error - {error.message}</div>;
  }

  if (props.requireAuth && !session) {
    if (!hasToken) {
      /**
       * This means that the user is not logged in. If you want to display some other UI in this
       * case, you can do so here.
       */
      if (props.doRedirection) {
        return redirect("/auth")
      } else {
        return null
      }
    }

    if (!hasInvalidClaims) {
      return <TryRefreshComponent />;
    }
  }

  // Get initial Session provider context and pass it to the client component for rendering data based on conditions
  const initialSessionAuthContext = await getInitialSessionAuthContext(session)

  return (
    <SessionAuth {...props} initialSessionAuthContext={initialSessionAuthContext}>
      {props.children}
    </SessionAuth>
  );
}

The logic in the above component is adapted for NextJS App Router, and depending on scenario other customisations to the ServerSessionAuth may be needed. Therefor, it is not a component for supertokens-auth-react library, and is rather a customer-side implementation that could be presented in documentation as example.

@rishabhpoddar
Copy link
Contributor

Can you show how users can use one of the function props provided to SessionAuth? For example, overrideGlobalClaims function.

@sasha240100
Copy link
Author

@rishabhpoddar In that case users will have to implement a ClientSessionAuth in the middle between ServerSessionAuth and SessionAuth:

"use client";

import {PropsWithChildren} from 'react';
import {SessionAuth, SessionAuthProps} from 'supertokens-auth-react/recipe/session';

export function ClientSessionAuth(props: PropsWithChildren<SessionAuthProps>) {
  // Custom implementation of overrideGlobalClaimValidators goes here
  const overrideGlobalClaimValidators = () => []

  return (
    <SessionAuth {...props} overrideGlobalClaimValidators={overrideGlobalClaimValidators}>
      {props.children}
    </SessionAuth>
  )
}

There are several things that prevent from having exactly the same function for overrideGlobalClaimValidators on server side & client side:

  • There are different types for SessionClaimValidator on supertokens-node and supertokens-auth-react, they would have to be unified first, otherwise they will conflict.
  • As functions from server components cannot be executed on client side, the function that will be used on both server & client side has to be inside a client component, which means that it cannot be passed as a prop to the ServerSessionAuth component because it is a server component.

Types difference:

// supertokens-auth-react
export declare type SessionClaimValidator = {
    readonly id: string;
    /**
     * Makes an API call that will refresh the claim in the token.
     */
    refresh(userContext: any): Promise<void>;
    /**
     * Decides if we need to refresh the claim value before checking the payload with `validate`.
     * E.g.: if the information in the payload is expired, or is not sufficient for this validator.
     */
    shouldRefresh(accessTokenPayload: any, userContext: any): Promise<boolean> | boolean;
    /**
     * Decides if the claim is valid based on the accessTokenPayload object (and not checking DB or anything else)
     */
    validate(accessTokenPayload: any, userContext: any): Promise<ClaimValidationResult> | ClaimValidationResult;
};

// supertokens-node
export declare type SessionClaimValidator = (
    | // We split the type like this to express that either both claim and shouldRefetch is defined or neither.
    {
          claim: SessionClaim<any>;
          /**
           * Decides if we need to refetch the claim value before checking the payload with `isValid`.
           * E.g.: if the information in the payload is expired, or is not sufficient for this check.
           */
          shouldRefetch: (payload: any, userContext: any) => Promise<boolean> | boolean;
      }
    | {}
) & {
    id: string;
    /**
     * Decides if the claim is valid based on the payload (and not checking DB or anything else)
     */
    validate: (payload: any, userContext: any) => Promise<ClaimValidationResult>;
};

@rishabhpoddar
Copy link
Contributor

TODO for later (this is out of the scope of the interview trial): We need to do something about SuperTokensWrapper as well.

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

No branches or pull requests

2 participants