Yes, you can update user metadata with Next.js + Auth0

It’s just not documented that well.

Here’s how I got it to work:

Install both both @auth0/nextjs-auth0 and auth0 (aka node-auth0). @auth0/nextjs-auth0 works beautifully out of the box for authentication, but you’ll want to configure it to actually work with current_user scopes.

yarn add @auth0/nextjs-auth0 auth0

Follow the README guide to set up @auth0/nextjs-auth0. You'll want to customize just the [...auth0].js file:

import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';

export default handleAuth({
  async login(req, res) {
    try {
      await handleLogin(req, res, {
        authorizationParams: {
          audience: `${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/`,
          scope: 'openid read:current_user update:current_user_metadata',
        },
      });
    } catch (error) {
      res.status(error.status || 400).end(error.message);
    }
  },
});

The key here is specifying the audience and scope when logging in. For whatever reason, the nextjs package won't read AUTH0_SCOPE from your .env.local file. You can verify this out by inspecting session in your API routes without setting the scope in handleAuth:

import { getSession } from '@auth0/nextjs-auth0';

const handler = async (req, res) => {
  const session = await getSession(req, res);

  console.log(session.accessTokenScope) // doesn't have 'current_user'
}

After you explicitly set it, session.accessTokenScope should return the correct scope. (Another caveat, not including openid in the scope with throw an error.)

The final step is to use the access token from your session to init your management client and specify the scope again.

// at the top:
import { ManagementClient } from 'auth0';

// in your handler
  const session = await getSession(req, res);

  const id = session.user.sub;
  const accessToken = session.accessToken;

  const currentUserManagementClient = new ManagementClient({
    token: accessToken,
    domain: process.env.AUTH0_ISSUER_BASE_URL.replace('https://', ''),
    scope: 'openid read:current_user update:current_user_metadata'
  });

  const user = await currentUserManagementClient.updateUserMetadata(
    { id },
    params
  );

Try it out—if you're already logged in, you'll need to log out and back in to see the scope change applied.

Here's a full example. I'm pretty sure the scope thing is a bug (does someone want to investigate to confirm and report it?), but drop in any suggestions in the gist comments.