Moving from @auth0/auth0-spa-js to @auth0/nextjs-auth0, stuck on RBAC

I have an app that uses @auth0/auth0-spa-js, gets an access token and based on roles in the token, dynamically adjusts the UI to render or skip various components. Note that this is generally at a component level and not at a route level, and I like it that way. This works very nicely, and in fact is based on an RBAC example that I found in the auth0 repo several years ago.

Now I know that @auth0/nextjs-auth0 uses an Authentication Code Grant that goes out of its way to disallow the token from reaching the client, and yes, I get that that’s more secure, fewer features is alway likely to be more secure, but it’s really inconvenient.

My question is, how do people generally architect things so that their client side code can make role based determinations at runtime. Do folk just add another api endpoint to retrieve the roles and manage it independently of auth0? That seems more error prone than just allowing the access token to get to the client. Just go all in on SSR and hydrate the roles up font?

I will say that so far the auth0 code has been much more time consuming to migrate to next.js than the whole rest of the app, not what I was either expecting or hoping for.

As a quick follow up, I’ve got a working solution now. The docs for @auth0/nextjs-auth0 are pretty terse, however the code is full of useful comments (thank you).

What I came up with is:

in src/pages/api/auth/[auth0].ts

import { handleAuth, handleLogin, handleProfile, Session } from '@auth0/nextjs-auth0';
import { NextApiRequest, NextApiResponse } from 'next';
import JwtDecode from 'jwt-decode'

const auth0Audience = process.env.AUTH0_AUDIENCE

export default handleAuth({
  async login(req, res) {
    try {
      await handleLogin(req, res, {
        // you have to set audience otherwise you don't get a decodable token
        // and you have to do it at login otherwise the wrong value gets cached
        authorizationParams: { audience: auth0Audience }
      });
    } catch (error: any) {
      res.status(error.status || 500).end(error.message);
    }
  },
  async profile(req, res) {
    try {
      await handleProfile(req, res, {
        refetch: true,
        afterRefetch: async (req: NextApiRequest, res: NextApiResponse, session: Session) => {
          const decodedToken: Record<string, any> | undefined = session.accessToken ? JwtDecode( session.accessToken) : undefined
          // extend the user profile object with the "interesting" parts of the auth token
          // in this case the roles array set by the auth0 rule
          // this continues to keep tokens out of the client, but exposes the appropriate
          // additional information for the UI to make rbac-like rendering choices.
          return {
            ...session,
            user: {
              ...session.user,
              ...decodedToken?.[auth0Audience!]
            }
          };
        }
      });
    } catch (error: any) {
      res.status(error.status || 500).end(error.message);
    }
  }
});

Hope that this saves someone else some time.

Thank you a lot for sharing it with the rest of community!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.