https://supertokens.com/ logo
Title

Dani

02/18/2023, 10:28 PM
If I use
UserRoles.init({
    skipAddingRolesToAccessToken: true,
    skipAddingPermissionsToAccessToken: true,
})
does that mean that the permission claim validator will fetch the current values from supertokens core every time? I want that behavior because I would like to set permissions for offline/online users all the time, and these changes in permissions need to be available in real time in the front-end route guards/ back-end api guards. So for example, if I as an admin, remove a permission from an online user and after a second they try to access a protected api route, I would like to deny that.
r

rp

02/19/2023, 5:00 AM
hey @Dani

Dani

02/19/2023, 5:00 AM
Hi! im experimenting with a lot of things 😄
r

rp

02/19/2023, 5:01 AM
The
skipAddingRolesToAccessToken
and
skipAddingPermissionsToAccessToken
basically make it so that the session claims don't have any info about roles / permission during session creation - this is not what you want.
What you want to do is that when you are using the claim validators in your APIs, you want to set the
maxAgeInSeconds
to
0
So for example, instead of using
UserRoles.UserRoleClaim.validators.includes("admin")
, you want to use
UserRoles.UserRoleClaim.validators.includes("admin", 0)
The
maxAgeInSeconds
is a time value which will cause supertokens to refresh the value of the claims (in this case the roles claims) if
maxAgeInSeconds
has been passed since the last refetch time.
So setting it to 0 basically forces supertokens to check the roles / permissions in the db all the time

Dani

02/19/2023, 5:03 AM
yes I tried that, it's almost good, but I want a more instant reaction on the front-end, so I'm trying to refresh the session more frequently right now
r

rp

02/19/2023, 5:04 AM
from the frontend point of view, you could make your own claim validator on the frontend and use that instead of using the roles claim validator that we have. In your claim validator, in the refresh function, you can call an API (which you make) that would update the session with the latest roles / permissions by using
session.fetchAndSetClaim
our default claim roles claim validator on the frontend doesn't do that (yet) cause we don't expose an API for refreshing the roles (yet)

Dani

02/19/2023, 5:07 AM
thank you, I think that will work
This is what I came up with:
ts
import { useEffect, useMemo } from "react";
import { refreshPermissionsClaim } from "shared/session.telefunc";
import Session, {
  SessionAuth,
  useSessionContext,
} from "supertokens-auth-react/recipe/session";
import { PermissionClaim } from "supertokens-auth-react/recipe/userroles";

export type ProtectedProps = {
  permission?: string;
  fallback?: React.ReactNode;
  disableRedirect?: boolean;
};

//Avoids fetching multiple times when multiple Protected components mount
let refreshingPromise: Promise<void> | null = null;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
PermissionClaim.refresh = async () => {
  if (refreshingPromise) {
    return refreshingPromise;
  }
  console.log("refreshing permissions claim");
  refreshingPromise = refreshPermissionsClaim();
  const result = await refreshingPromise;
  refreshingPromise = null;
  return result;
};

export const Protected = ({
  children,
  permission,
  fallback = null,
  disableRedirect = false,
}: {
  children: React.ReactNode;
} & ProtectedProps): JSX.Element => {
  const extraValidators: Session.SessionClaimValidator[] = useMemo(() => {
    const v = [];
    if (permission) {
      v.push(PermissionClaim.validators.includes(permission, 0.2));
    }
    return v;
  }, [permission]);

  const sessionContext = useSessionContext();

  useEffect(() => {
    const listener = () => {
      for (const validator of extraValidators) {
        if (
          !sessionContext.loading &&
          validator.shouldRefresh(sessionContext.accessTokenPayload, {})
        ) {
          validator.refresh({});
        }
      }
    };
    window.addEventListener("focus", listener);
    return () => {
      window.removeEventListener("focus", listener);
    };
  }, [extraValidators, sessionContext]);

  return (
    <SessionAuth
      doRedirection={!disableRedirect}
      overrideGlobalClaimValidators={(globalValidators) => [
        ...globalValidators,
        ...extraValidators,
      ]}
    >
      <InvalidClaimHandler fallback={fallback}>{children}</InvalidClaimHandler>
    </SessionAuth>
  );
};

const InvalidClaimHandler = (
  props: React.PropsWithChildren<{
    fallback?: React.ReactNode;
  }>
) => {
  const sessionContext = useSessionContext();
  if (sessionContext.loading) {
    return <></>;
  }

  if (sessionContext.invalidClaims.length) {
    return <>{props.fallback}</>;
  }

  return <div>{props.children}</div>;
};
Server:
ts
import { getSession } from "server/auth/session";
import UserRoles from "supertokens-node/recipe/userroles";

export const refreshPermissionsClaim = async () => {
  const session = getSession();
  await session.fetchAndSetClaim(UserRoles.PermissionClaim);
};
I'm not sure about the window focus validator refresh logic
Does it look correct?
r

rp

02/19/2023, 7:07 AM
It's in the right direction, but you don't need to manually call
validator.shouldRefresh
or
validator.refresh
. Just use the
PermissionClaim
validators and set the maxAgeInSeconds to
0
in there too (this is on the frontend). Also, when calling
getSession
on the backend, you should await it, and it takes a request and response object as well.

Dani

02/19/2023, 7:14 AM
My server is magic, the code just works like that 😄 I'm using https://telefunc.com/
r

rp

02/19/2023, 7:15 AM
So did you get it to work correctly?
And how is the
getSession();
function working if you aren't awaiting it? Im confused.

Dani

02/19/2023, 7:26 AM
Yes, it works. It doesn't refresh the claim on window focus but that's ok. About getSession: Every exported function in this file is importable on the front-end, vite replaces the import with a fetch call at build time.
ts
import { getContext, Abort } from "telefunc";
import { TelefuncCtx } from "shared/types";
import { getSession } from "server/auth/session";
import UserRoles from "supertokens-node/recipe/userroles";

const getSession = () => {
  const { req } = getContext<TelefuncCtx>();
  const session = req.session;
  if (!session) {
    throw Abort("Session not found");
  }
  return session;
};
export const refreshPermissionsClaim = async () => {
  const session = getSession();
  await session.fetchAndSetClaim(UserRoles.PermissionClaim);
};
And in my main.ts server entry point:
ts
  app.use(
    "/_telefunc",
    verifySession({ sessionRequired: false }), // sets req.session
    express.text({ limit: "10mb" }),
    async (req, res) => {
      provideTelefuncContext({ req, res }); // async_hooks
      const httpResponse = await telefunc({ //this line calls the actual function in the file above
        url: req.originalUrl,
        method: req.method,
        body: req.body,
      });

      const { body, statusCode, contentType } = httpResponse;
      res.status(statusCode).type(contentType).send(body);
    }
  );
r

rp

02/19/2023, 7:33 AM
Righttt. I see. Okay!