Beyond the Login Button: Tackling RBAC in NextJS with Auth0
- David Ishmael
- Feb 22, 2024
- 3 min read
It's been a while since my last post, but I've not been sitting idle. I decided to continue brushing up on my frontend knowledge using NextJS, Prisma, and Auth0. Initially, I thought I would just use Prisma for both authentication and authorization (RBAC). But I know the better option to allow for scaling is to utilize an existing platform like Auth0. On the surface, it looks fairly straightforward and there are a lot of examples. Getting the authentication working wasn't difficult; however, getting the authorization to work properly caused me so much headache I wanted to capture my steps here for future me (or readers) having to deal with this same scenario.
I am going to skip the initial setup of an application in Auth0. That's so simple there's no need to go into detail. The dashboard is relatively intuitive for creating the application, creating users, and creating/assigning roles. The trick is getting a default role assigned to a user and populating the user token with roles when you login.
Assigning a Default Role
Unfortunately, there's no OOTB way to assign a default role (from what I can tell). To accomplish this magical feat, you have to create a custom action (Actions -> Library and click Create Action > Build from scratch). Good news, there's a video by Carla Stabile on how to properly create and apply this custom action. Once I completed the steps outlined in the video, my default role was applied to a user without a role assigned. Great - but that did not populate the user token on a successful login. Annoying.
Adding Role to the Token
Now that our user is getting populated with a default role, we want to utilize that role in our web app. Seems straight forward, we can look for the role in the token...
# app/profile/page.tsx (Auth0 provides the full code as a downloadable sample)
import { useUser } from '@auth/nextjs-auth0/client';
export default Page = () => {
const { user } = useUser();
return (
<div>
{user && (
<p>{JSON.stringify(user, null, 2)}</p>
)};
</div>
)
};
While this snippet of code provided the fields in 'user', the role assigned was not present. I spent entirely too long trying to figure out why this wasn't working. I was just about to scrap Auth0 and roll with a different option when I realized what needed to be done. I created another custom Login action to set the role in the token.
# Add Role to Token
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'http://my.custom.namespace';
const defaultRoles = ['User'];
if (event.authorization) {
// Set the default role to 'User' if not defined
if (event.authorization.roles.length == 0) {
event.authorization.roles = defaultRoles;
}
api.idToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
}
};
Boom. This did exactly what I needed. Here, the code checks first to see if the role is in the event.authorization object. If it is, then we use that to set the user token. If it isn't present, which happens after a user first registers, then we set it to a default value of 'User'.
Conclusion
As with any coding effort, there's likely many ways to accomplish the same outcome. Maybe there's a better way. Who knows. What I can tell you is that this worked for my needs and after several days of banging my head against the table, I'll roll with this until I hear of a better way.
Comentários