User object doesn't exist in functions triggered by Netlify Identity

My Netlify site is named mellow-lolly-ecaae8.netlify.app. My goal is to use Netlify Identity, and use a serverless function to save the user’s name, email, and user ID to a cloud database hosted on Planetscale.

First I installed netlify-identity-widget and added the button element to my site. I sent an invitation to myself to be the first user. (My site will be invite-only, so I used the Netlify website to configure the identity settings to not provide a “sign-up” tab on the identity modal.) I logged in and out, deleted and recreated my user, and this much worked fine so far.

The problem began when I created serverless functions, following the instructions in the Netlify Documentation:

First I tried putting this code in three different files in the /functions folder, one at a time: identity-validate.js, identity-signup.js, and identity-login.js. (You might say, wait a minute, you tried signup but you’re not allowing signups! But when I tried the others & they didn’t work, I thought I might learn through experimenting with signup, in case I misunderstood the descriptions in the documentation of where these three triggers actually fit in the auth flow.)

import { withPlanetscale } from "@netlify/planetscale";

export const handler: Handler = withPlanetscale(async (event, context) => {
  console.log('event ', event);
  console.log('context ', context);
  const { identity, user } = context.clientContext;
  console.dir('identity ', identity);
  console.dir('user ', user);

  const { email, id } = user;

  const name = user.metadata['name'];

  const {
    planetscale: { connection },
  } = context;

  await connection.execute(
    `INSERT INTO Users (id, email, name) VALUES (${id}, ${email}, ${name})`
  );

  return {
    statusCode: 200,
    body: "User created in Planetscale",
  };
});

The dev tools Network pane returned {"code":422,"msg":"Failed to handle signup webhook"}, and that message appears in the identity modal.

Here’s the log output from the Netlify Functions page:

context  {
  callbackWaitsForEmptyEventLoop: true,
  succeed: [Function (anonymous)],
  fail: [Function (anonymous)],
  done: [Function (anonymous)],
  functionVersion: '$LATEST',
  functionName: 'blah blah blah etcetera',
  memoryLimitInMB: '1024',
  logGroupName: '/aws/lambda/ blah blah blah etcetera',
  logStreamName: '2023/03/30/[$LATEST]blah blah blah etcetera',
  clientContext: {
    custom: {
      netlify: 'blah blah blah etcetera'
    },
    identity: {
      url: 'https://mellow-lolly-ecaae8.netlify.app/.netlify/identity',
      token: 'blah blah blah etcetera'
    }
  },
  identity: undefined,
  invokedFunctionArn: 'arn:aws:lambda:us-east-1:blah blah blah etcetera:function:blah blah blah etcetera',
  awsRequestId: 'blah blah blah etcetera',
  getRemainingTimeInMillis: [Function: getRemainingTimeInMillis],
  planetscale: { connection: Connection { session: null, config: [Object] } }
}

Mar 30, 03:02:46 PM: 'identity 'Mar 30, 03:02:46 PM: 'user 'Mar 30, 03:02:46 PM: 4223e65b ERROR  Invoke Error 	{
  "errorType":"TypeError",
  "errorMessage":"Cannot destructure property 'email' of 'user' as it is undefined.",
  "stack":[
    "TypeError: Cannot destructure property 'email' of 'user' as it is undefined.",
    "    at /var/task/functions/identity-login.js:5909:11",
    "    at /var/task/functions/identity-login.js:5898:12",
    "    at Generator.next (<anonymous>)",
    "    at /var/task/functions/identity-login.js:5886:67",
    "    at new Promise (<anonymous>)",
    "    at __awaiter (/var/task/functions/identity-login.js:5868:10)",
    "    at Runtime.handler (/var/task/functions/identity-login.js:5897:30)",
    "    at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1085:29)"
  ]
}

It seems to always give the same output. context.clientContext doesn’t actually have user, so user was undefined, and so, “Cannot destructure property ‘email’ of ‘user’ as it is undefined.”

I hoped I would find a user object somewhere in context or identity, but as you can see, it’s absent. What am I doing wrong?

Hi @MattArnold ,

The user object is only available in the context when a user is logged in and has an active session. Could you try adding some console.log statements in the function to see the exact object being passed in? Here is some more info about debugging functions:
Developing functions with Netlify CLI.

Hey there Sam, I appreciate your response. I already indicated that I put console logs in, and I showed you their output. As you can see in my code, above, I’m logging event and context that are being passed in to the serverless function, as well as logging identity and user. Is there something else I need to log?

The Netlify documentation, which I linked to, seems to make it clear that functions which trigger on identity-signup, identity-validate, and identity-login should be expected to have a user object. Am I looking for it in the wrong place?

These trigger when the user is currently signing up, currently validating their email, or in the process of logging in. Those are not times when the user is already logged in to an active sesison.

If I’m misunderstanding this, please help me to understand what I need to do differently.

I created a new serverless function which is called when I press a button on the page. It console logs out event and context. I removed the identity based serverless fuctions so that I could successfully log in without an error. I pressed the button while logged in to an active session. Here’s what I got in the function logs (there’s still no user):

event: {
  rawUrl: 'https://blah blah etcetera--mellow-lolly-ecaae8.netlify.app/.netlify/functions/getUser',
  rawQuery: '',
  path: '/.netlify/functions/getUser',
  httpMethod: 'POST',
  headers: {
    accept: '*/*,image/webp',
    'accept-encoding': 'br',
    'accept-language': 'en-US,en;q=0.9',
    'cdn-loop': 'netlify',
    'content-length': '0',
    cookie: 'nf_jwt=blah blah etcetera.blah blah etcetera.blah blah etcetera',
    host: 'blah blah etcetera--mellow-lolly-ecaae8.netlify.app',
    origin: 'https://blah blah etcetera--mellow-lolly-ecaae8.netlify.app',
    referer: 'https://blah blah etcetera--mellow-lolly-ecaae8.netlify.app/',
    'sec-ch-ua': '"Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"macOS"',
    'sec-fetch-dest': 'empty',
    'sec-fetch-mode': 'cors',
    'sec-fetch-site': 'same-origin',
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36',
    'x-country': 'US',
    'x-forwarded-for': 'blah blah etcetera',
    'x-forwarded-proto': 'https',
    'x-language': 'en-US',
    'x-nf-account-id': 'blah blah etcetera',
    'x-nf-client-connection-ip': 'blah blah etcetera',
    'x-nf-request-id': 'blah blah etcetera',
    'x-nf-site-id': 'blah blah etcetera'
  },
  multiValueHeaders: {
    Accept: [ '*/*,image/webp' ],
    'Accept-Encoding': [ 'br' ],
    'Accept-Language': [ 'en-US,en;q=0.9' ],
    'Cdn-Loop': [ 'netlify' ],
    'Content-Length': [ '0' ],
    Cookie: [
      'nf_jwt=blah blah etcetera.blah blah etcetera.blah blah etcetera'
    ],
    Origin: [
      'https://blah blah etcetera--mellow-lolly-ecaae8.netlify.app'
    ],
    Referer: [
      'https://blah blah etcetera--mellow-lolly-ecaae8.netlify.app/'
    ],
    'Sec-Ch-Ua': [
      '"Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"'
    ],
    'Sec-Ch-Ua-Mobile': [ '?0' ],
    'Sec-Ch-Ua-Platform': [ '"macOS"' ],
    'Sec-Fetch-Dest': [ 'empty' ],
    'Sec-Fetch-Mode': [ 'cors' ],
    'Sec-Fetch-Site': [ 'same-origin' ],
    'User-Agent': [
      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36'
    ],
    'X-Country': [ 'US' ],
    'X-Forwarded-For': [ 'blah blah etcetera, 100.64.0.167' ],
    'X-Forwarded-Proto': [ 'https' ],
    'X-Language': [ 'en-US' ],
    'X-Nf-Account-Id': [ 'blah blah etcetera' ],
    'X-Nf-Client-Connection-Ip': [ 'blah blah etcetera' ],
    'X-Nf-Request-Id': [ 'blah blah etcetera' ],
    'X-Nf-Site-Id': [ 'blah blah etcetera' ],
    host: [ 'blah blah etcetera--mellow-lolly-ecaae8.netlify.app' ]
  },
  queryStringParameters: {},
  multiValueQueryStringParameters: {},
  body: '',
  isBase64Encoded: false
}
context: {
  callbackWaitsForEmptyEventLoop: true,
  succeed: [Function (anonymous)],
  fail: [Function (anonymous)],
  done: [Function (anonymous)],
  functionVersion: '$LATEST',
  functionName: 'blah blah etcetera',
  memoryLimitInMB: '1024',
  logGroupName: '/aws/lambda/blah blah etcetera',
  logStreamName: '2023/04/02/[$LATEST]blah blah etcetera',
  clientContext: {
    custom: {
      netlify: 'blah blah etcetera'
    },
    identity: {
      url: 'https://blah blah etcetera--mellow-lolly-ecaae8.netlify.app/.netlify/identity',
      token: 'blah blah etcetera.blah blah etcetera.blah blah etcetera-blah blah etcetera'
    }
  },
  identity: undefined,
  invokedFunctionArn: 'arn:aws:lambda:us-east-1:837451400009:function:blah blah etcetera',
  awsRequestId: 'blah blah etcetera',
  getRemainingTimeInMillis: [Function: getRemainingTimeInMillis],
  planetscale: { connection: Connection { session: [Object], config: [Object] } }
}

In my last comment, I mentioned I created a new serverless function which is called when I press a button on the page. I am now also setting a listener like this:

netlifyIdentity.on("login", async user => {
    console.log('Logging in. user: ', user);
    const bodyObject = {
        id: user?.id,
        name: user?.user_metadata.full_name,
        email: user?.email,
    }
    await setUser(bodyObject);
    // Close the login modal
    await netlifyIdentity.close();
    await fetchUser(bodyObject);
});

When I call that API, I am now passing in the user object from the browser instead of relying on Netlify to provide the identity on the back end:

async function fetchUser(user) {
    console.log('fetchUser. user: ', user);
     try {
        const response = await fetch("/.netlify/functions/getUser", {
            method: "POST",
            body: JSON.stringify(user),
         });
     const data = await response.json();
     console.log('data ', data);
     } catch (error) {
        console.error("error: ", error);
     }
 }

Here’s what it logs to the browser console for user:

user:  
api: e {apiURL: '/.netlify/identity', _sameOrigin: true, defaultHeaders: {…}}
app_metadata: {provider: 'email'}
aud: ""
audience: undefined
confirmed_at: "2023-03-29T21:25:04Z"
created_at: "2023-03-29T21:23:41Z"
email: "matt.mattarn@gmail.com"
id: "3f33f91d-dea9-4cf6-ae8b-135d29cf8849"
invited_at: "2023-03-29T21:23:41Z"
role: ""
token: {access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2O…nt9fQ.zEmfXJwzAwiP8X7vieryx2b_tHpIokbdzghWuigKUsY', token_type: 'bearer', expires_in: 3600, refresh_token: 'b2orReb7QlbS1fx10uMBdA', expires_at: 1680554235000}
updated_at: "2023-03-29T21:23:41Z"
url: "/.netlify/identity"
user_metadata: [[Prototype]]: Object
admin: (...)
_details: (...)
[[Prototype]]: Object

No metadata. When I accepted the invitation email and signed up, I entered my name in the netlify-identity-widget signup modal. Why isn’t my name captured in Netlify’s identity service? The documentation said to expect user_metadata to have full_name.

This particular error was happening because you need to return the user object to the client instead of the string that you were returning. Try returning JSON.stringify(user).

The value would be populated only if you pass authorization: Bearer <jwt-given-by-Identity> header to the Function call.

I can see user_metadata being available here:

What am I missing on that?