Next.js feature flags with auth0
In the last weeks I've been working on integrating Plaid into Maffin so your data is automatically synced. When I first looked at it, it felt too much because there were many missing parts in the app in order to support it properly. I transitioned backend functionality from express to server actions and protected them using jwt verification.
After that the codebase felt ready to get right to implementing a POC integration with Plaid behind a beta flag (auth0 user role).
🔒 Auth0
Auth0 is key in the implementation as it's what we use to authenticate our users and check that they have the needed role to access our features. On the frontend side, there's a getAccessTokenSilently
function that you can use to retrieve the auth0 access token. The first problem you find though is that it returns an opaque token unless you set a custom audience in the provider:
<Auth0Provider
domain={process.env.NEXT_PUBLIC_AUTH0_DOMAIN as string}
clientId={process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID as string}
authorizationParams={{
redirect_uri: (typeof window !== 'undefined' && window.location.origin) || '',
scope: process.env.NEXT_PUBLIC_AUTH0_SCOPES,
connection: 'maffin-gcp',
audience: 'https://maffin',
}}
>
<AccessTokenActionProvider>{children}</AccessTokenActionProvider>
</Auth0Provider>
The audience tells Auth0 that you are requesting access to a specific API, in our case, the set of server actions we will code later. For this to work, you need to create the API on Auth0's (under Applications/APIs):
The next step is to make sure that auth0 includes the roles for your API in the accessToken it return. To do so we need to create a Post Login action:
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'https://maffin';
if (event.authorization) {
api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
api.idToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles); // We will talk about this later
}
};
And that's it on the Auth0 side! Now you can retrieve the access token once the user has authenticated with getAccessTokenSilently
call.
🚪 Storing the access token
The access token is something that you'll need to be using quite a bit in your app to check if the user has permissions to do something or if you want to enable different UI depending on the user. With the current version of auth0-react
you need to use the useAuth0
hook in order to have access to the function that returns the token:
import { useAuth0 } from '@auth0/auth0-react';
export default function useSession(): SessionReturn {
const { getAccessTokenSilently, isAuthenticated } = useAuth0();
const [accessToken, setAccessToken] = React.useState();
React.useEffect(() => {
async function load() {
const token = await getAccessTokenSilently();
setAccessToken(token);
}
if (isAuthenticated) {
load();
}
}, [isAuthenticated, getAccessTokenSilently]);
return {
accessToken,
};
🍿 Role based UI
Until here, we've been talking about access token but Auth0 works with two tokens:
- idToken: provides information about the user itself and it's what
auth0.user
returns. This one is mostly used in the frontend - accessToken: contains authorization information to verify the access is granted to APIs.
When I initially wrote this blogpost, I was using access token both for UI and API but updated my code (and the post) to use the idToken in the UI instead after this very constructive discussion where I learned a lot.
Thanks to the line added in our Auth0 action: api.idToken.setCustomClaim(
${namespace}/roles, event.authorization.roles);
we now have the roles in the idToken returned by Auth0.
{
email: <user_email>
// beta is a role I created in the auth0 dashboard and assigned to my user
'https://maffin/roles': ['beta'],
...
}
we can now take decisions in the UI to show one component or the other:
import useSession from '@/hooks/useSession';
export default function PlaidImportButton({
className = '',
...props
}: Readonly<ImportButtonProps>): JSX.Element {
const { user, roles } = useSession();
if (!roles.isBeta) {
return <span />;
}
return (
<button
id="plaid-import-button"
...
);
}
We've modified the
useSession
hook we defined above to attach the roles to the returned object.
With above, if the user doesn't have the beta role they won't see anything while if they have it, they'll see the Plaid import button:
Note that this only changes the UI behavior but doesn't protect your backend resources. A bad agent could always modify the source and inject the roles which would show the "hidden" UI which then can trigger calls to your protected backend. That's what we focus on on the next section.
🛡️ Protected server actions
auth0-react provides a getAccessTokenSilently
that allows us to retrieve the access token in React components, hooks, etc. The problem with this is that you can't retrieve the access token in normal functions like for example the ones defined in server actions. To work around this some people suggest to store it in local storage and retrieve it whenever you need. Auth0 recommends to keep access tokens in memory to reduce risk surface. In our case, we just created a class with a static accessToken variable so it can be set and retrieved whenever is needed:
export class AccessTokenHolder {
static _accessToken: string;
static get accessToken() {
return Actions._accessToken;
}
static set accessToken(token: string) {
Actions._accessToken = token;
}
}
With this, you can do AccessTokenHolder.accessToken
anywhere in your code. In our case we wrap our server actions with this so the access token is injected whenever they are called:
import { createLinkToken } from './plaid';
const wrapperCreateLinkToken = ({
userId,
}: {
userId: string,
}): ReturnType<typeof createLinkToken> => (
createLinkToken({
accessToken: AccessTokenHolder.accessToken, // Inject the access token
userId,
})
);
export { wrapperCreateLinkToken as createLinkToken };
Then, within our server action, we need to verify the JWT. If it's valid, check for the right permissions and then execute:
'use server'
export async function createLinkToken({
userId,
accessToken,
}: {
userId: string,
accessToken: string,
}): Promise<string> {
await verify(accessToken);
if (!(await getRoles(accessToken)).isBeta) {
return '';
}
const response = await client.linkTokenCreate({
user: {
client_user_id: userId,
},
client_name: 'Maffin',
products: [Products.Transactions],
country_codes: Object.values(CountryCode),
language: 'en',
});
return response.data.link_token;
}
The verify
function looks like this:
import jwt from 'jsonwebtoken';
import { JwksClient } from 'jwks-rsa';
import type { JwtHeader, JwtPayload, SigningKeyCallback } from 'jsonwebtoken';
function getKey(header: JwtHeader, callback: SigningKeyCallback) {
const client = new JwksClient({
jwksUri: `https://${process.env.NEXT_PUBLIC_AUTH0_DOMAIN}/.well-known/jwks.json`,
});
client.getSigningKey(header.kid, (_, key) => {
const signingKey = key?.getPublicKey();
callback(null, signingKey);
});
}
export async function verify(token: string): Promise<JwtPayload> {
const verified: JwtPayload = await new Promise((resolve, reject) => {
jwt.verify(token, getKey, {}, (err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded as JwtPayload);
}
});
});
return verified;
}
And that's it! if someone tries to pass a wrong jwt token, the verification will fail so it will not execute. Also, if a user with the wrong roles tries to execute, it will also fail.
💚 Conclusion
With this change, I introduced a way for Maffin to protect features and modify behavior based on the roles assigned to the user in Auth0. Before that, I did it based on the environment I was deploying (i.e. production, staging, etc.) which meant I couldn't have premium and standard users in the same environment and less, have a way to segment functionality.
As always, you can find the full PR in Github. If you want to talk more about this or need help, feel free to join our discord!
If you enjoyed the post, feel free to react to it directly in Github :).