Linking accounts (/identities) nets a "Unauthorized: Invalid Token"

So, I’m using Next.js for Auth0 and I’m trying to link accounts using the @auth/auth0-spa-js library to instantiate a new Auth0Client to get the linking and then I call the identities url via POST to link but I get this error:

{
    "statusCode": 401,
    "error": "Unauthorized",
    "message": "Invalid token",
    "attributes": {
        "error": "Invalid token"
    }
}

and here’s the URL/payload:

https://MY_WEBSITE.us.auth0.com/api/v2/users/auth0%7C62825f0192ed030070eb920d/identities

    "link_with": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllISmV2eEF1T1I4SzJaUmNmLThqNiJ9.eyJnaXZlbl9uYW1lIjoiRGFuIiwiZmFtaWx5X25hbWUiOiJKYXNub3dza2kiLCJuaWNrbmFtZSI6ImQuamFzbm93c2tpIiwibmFtZSI6IkRhbiBKYXNub3dza2kiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2prcGhpTTdlX1hpYlh4aTJHRk0wWEVyVE0tWDFSZUlCZldqbC1QU0E9czk2LWMiLCJsb2NhbGUiOiJlbiIsInVwZGF0ZWRfYXQiOiIyMDIyLTA1LTE2VDE3OjQzOjEzLjg2NFoiLCJlbWFpbCI6ImQuamFzbm93c2tpQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczovL3ZpbmRleC1kZXYudXMuYXV0aDAuY29tLyIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTEwMzEzMTU1NzQ3MTQ1MDUwNjM4IiwiYXVkIjoia1plNnM0SVJXMjhXa1dWdk5ibmNvQlhiSmVtd0tNcUMiLCJpYXQiOjE2NTI3MjI5OTQsImV4cCI6MTY1Mjc1ODk5NCwiYXV0aF90aW1lIjoxNjUyNzIyOTkzLCJub25jZSI6ImFHbDNWMDlwVWs5TmRWVmpZVTExU201K2FITmxRbkZUYmpoRE1pNDVaVXR6Y0VGdmNuWm5RekZsVWc9PSJ9.qc6EU3zCPApDt56fqG0wbEoqh6UlcFOvbYvjIVWPPD3zG-t31aia9gVzqp8RT8oSE_LFE51duPo65QJvB9Z1Ea98A9GH7vUzRYY2nnNBsJ8A-nKCubJjTDYBWQc9Ds3r_Vl3eo0N9gSBqm_ZG2M1VBfoRwP9L5j73xwVtpiLnFZM3-Tl4s495Mzs5mS2x-Q04w6zIFCcoqh_pOv7odYKvV6IrJa96665X5a2UKdbM0XfJYGj5jKsi-Eo4GxHfxi63T571cWH6O9Ky4c49s0xmzHQvibQfpvx3W0e33x8zx5aK9XpYPeeNMOLG7PKDMKikOe_PO21VAIVRVBJFFRO5w"
}

And I know that’s right because I used JWT.IO to verify and it looks good.

And just for good measure, during a0.loginWithPopup I hit the https://MY_WEBSITE.us.auth0.com/oauth/token with this payload:

{
    "client_id": "CLIENT_ID",
    "code_verifier": "1UfVE9x9h................9.oKIUYqfVDi3dI-Hr-pjl",
    "code": "j6GyP8uGyfic...................FD-bzOvSjvGD",
    "grant_type": "authorization_code",
    "redirect_uri": "http://localhost:3000"
}

(removed some code/verifier for brevity…) and this the response:

{
    "access_token": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiaXNzIjoiaHR0cHM6Ly92aW5kZXgtZGV2LnVzLmF1dGgwLmNvbS8ifQ..7Jl1AmIQ5h_1pB_j.tiUeBqpGPTDtxPyL6Qo9_8GpMcAMnowRujX0S5kfLzyaxd1ZqLOBgoHeIZaZ-YTz0_Mh0RMiPJVpggv9E4S3ibuoST-5R1dVtjtr2WolM6XeWkvRMzXo9BrDutDKem2CFkDI8yXzoPxxxE7590LHqLSKd4yyf_CpwS1h_vzL283dxJlWzrMbPhHoWtAoSDw3MLn3JDNrt2mCVYN7c93Cs-XyGz5Xz8aToj9-dF6ybRnoLrJk2NdLPythhkJh1cJKqV0BrdwJf7mByVUA1aA2VAD7FJ24B6mjagX8L_zpuW2WsJ0gD-y5za9BWjjnr5N4048a.QY1Z2dq1suLO5wb4x1zbkQ",
    "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllISmV2eEF1T1I4SzJaUmNmLThqNiJ9.eyJnaXZlbl9uYW1lIjoiRGFuIiwiZmFtaWx5X25hbWUiOiJKYXNub3dza2kiLCJuaWNrbmFtZSI6ImQuamFzbm93c2tpIiwibmFtZSI6IkRhbiBKYXNub3dza2kiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2prcGhpTTdlX1hpYlh4aTJHRk0wWEVyVE0tWDFSZUlCZldqbC1QU0E9czk2LWMiLCJsb2NhbGUiOiJlbiIsInVwZGF0ZWRfYXQiOiIyMDIyLTA1LTE2VDE3OjQzOjEzLjg2NFoiLCJlbWFpbCI6ImQuamFzbm93c2tpQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczovL3ZpbmRleC1kZXYudXMuYXV0aDAuY29tLyIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTEwMzEzMTU1NzQ3MTQ1MDUwNjM4IiwiYXVkIjoia1plNnM0SVJXMjhXa1dWdk5ibmNvQlhiSmVtd0tNcUMiLCJpYXQiOjE2NTI3MjI5OTQsImV4cCI6MTY1Mjc1ODk5NCwiYXV0aF90aW1lIjoxNjUyNzIyOTkzLCJub25jZSI6ImFHbDNWMDlwVWs5TmRWVmpZVTExU201K2FITmxRbkZUYmpoRE1pNDVaVXR6Y0VGdmNuWm5RekZsVWc9PSJ9.qc6EU3zCPApDt56fqG0wbEoqh6UlcFOvbYvjIVWPPD3zG-t31aia9gVzqp8RT8oSE_LFE51duPo65QJvB9Z1Ea98A9GH7vUzRYY2nnNBsJ8A-nKCubJjTDYBWQc9Ds3r_Vl3eo0N9gSBqm_ZG2M1VBfoRwP9L5j73xwVtpiLnFZM3-Tl4s495Mzs5mS2x-Q04w6zIFCcoqh_pOv7odYKvV6IrJa96665X5a2UKdbM0XfJYGj5jKsi-Eo4GxHfxi63T571cWH6O9Ky4c49s0xmzHQvibQfpvx3W0e33x8zx5aK9XpYPeeNMOLG7PKDMKikOe_PO21VAIVRVBJFFRO5w",
    "scope": "openid profile email",
    "expires_in": 86400,
    "token_type": "Bearer"
}

Lastly, this worked on a stricly SPA-js app but now that I’m integrating it into a Next.js app, perhaps I need to do things different? The documentation on this is a bit out of date hence I am asking here.

Here’s my code:

import { withPageAuthRequired, getAccessToken } from "@auth0/nextjs-auth0";
import { Auth0Client } from "@auth0/auth0-spa-js";

import { useState } from "react";
import { Alert, Button, Container } from "@mui/material";
import Layout from "../components/Layout";

import axios from "axios";

export default withPageAuthRequired(function Connections(
  props
): React.ReactElement {
  const [identities, setIdentities] = useState(null);

  async function linkAccount() {
    const a0 = new Auth0Client({
      domain: "MY_WEBSITE.us.auth0.com",
      client_id: "CLIENT_ID",
    });

    const scope =
      "openid profile email update:current_user_identities read:users read:current_user read:user_idp_tokens";

    await a0.loginWithPopup({
      max_age: 0,
      scope,
    });

    // User to link account with
    const authenticatedUser = await a0.getIdTokenClaims();
    const email = authenticatedUser?.email;
    const email_verified = authenticatedUser?.email_verified;
    const targetUserIdToken = authenticatedUser?.__raw;

    const { id_token } = await a0.getTokenSilently({
      detailedResponse: true,
      scope,
    });

    let responseData = null;

    if (!email_verified) {
      responseData = "Please verify your email: " + email;
    } else if (props.user.sub) {
      // Link account with new identity
      const response = await fetch(
        `https://${"MY_WEBSITE.us.auth0.com"}/api/v2/users/${encodeURIComponent(
          props.user.sub
        )}/identities`,
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${id_token}`,
          },
          body: JSON.stringify({
            link_with: targetUserIdToken,
          }),
        }
      );

      responseData = await response.json();
      setIdentities(responseData.identities);
    }
  }

  return (
    <Layout>
      <Alert sx={{ mt: 3 }} severity="info">
        Here are your connections.
      </Alert>
      <Button
        onClick={linkAccount}
        sx={{ my: 2 }}
        variant="contained"
        color="success"
      >
        Link Accounts
      </Button>
      <Container sx={{ mb: 2 }}>{JSON.stringify(identities)}</Container>
    </Layout>
  );
});