PATCH user.app_metadata isn't behaving as documented

The mgmt api docs state that app_metadata will be merged with a user update however, it seems that is not always the case if the update happens immediately after create in some situations. Auth0 Management API v2

I’m sure a simpler repro exists but I’m not set up to create one, so I’ve ripped out some of the relevant code from my app and posted it here in a gist with some comments: app_metadata is replaced, and not merged · GitHub

Pre-req: custom db with scripts that set app_metadata on create, login, get_user, etc.

(Docs on db scripts are lacking so I’m not entirely sure where the correct spot is to inject app_metadata so I set app_metadata everywhere – always to the same value.)

Using that gist a reference, the short of what’s happening is:

  1. [Works] Create a new auth0 user with authentication api: app_metadata is replaced, and not merged · GitHub

  2. [Works] Auth0 calls the custom db scripts which adds some initial app_metadata

  3. [Works] Poll the mgmt api for consistency (i only added this when this problem arose. ideally it’d be unnecessary): app_metadata is replaced, and not merged · GitHub

  4. [Doesn’t work] This creates a new identity for the user and is supposed to add a new property in app_metadata (however it ends up replacing it): app_metadata is replaced, and not merged · GitHub

After 4 runs, the app_metadata value REPLACES user.app_metadata – and does not merge. e.g.:

Before #4 user.app_metadata is: app_metadata: { set_in_db_script: 'hello from db' }

4 does PATCH to: app_metadata: { set_elsewhere: 'world' }

Expected result: app_metadata: { set_in_db_script: 'hello from db', set_elsewhere: 'world' }

Actual result: app_metadata: { set_elsewhere: 'world' }

The user_metadata and app_metadata is a concept available in all users, no matter how they authenticated or to be even more specific, no matter the identity provider used. In this case the identity provider is your custom database while for social connections the identity provider would be Google or Facebook or some other.

The identity providers will issue attributes of their own and then on the Auth0 side you can augment the user with metadata. What this means is that, in general, the metadata properties are not meant to be directly used by the identity provider. With a custom database you control the database so you also control the returned identity provider attributes and, although, you are able to return attributes named user_metadata and app_metadata it is probably not recommended for you to do so as it will likely not get what you want. In particular, the act of a user login would re-run your custom database script and override those properties again.

If you want to return some metadata information directly from the custom database script in order to save an extra request and in a way that you can then later add new metadata properties in addition to the ones returned then my suggestion would be for you to return that data in a custom property and then use a rule to migrate it to the proper notion of user metadata (not directly associated with an identity provider). In this way, you’re very explicit about how things work and you have complete control.

For example, first, you could return some initial metadata from your custom script in a property named db_metadata and that would be an object containing more than one property; second, you could have a rule that does something like the following:

user.app_metadata = user.app_metadata || {};
user.user_metadata = user.user_metadata || {};

if (user.app_metadata.initialized) { return callback(null, user, context); }

user.user_metadata.language = user.db_metadata.language;
user.app_metadata.reference = user.db_metadata.reference;
user.app_metadata.initialized = true;

var appMetadataPromise  = auth0.users.updateAppMetadata(user.user_id, user.app_metadata);
var userMetadataPromise = auth0.users.updateUserMetadata(user.user_id, user.user_metadata);

q.all([userMetadataPromise, appMetadataPromise])
  .then(function(){
    callback(null, user, context);
  })
  .catch(function(err){
    callback(err);
  });

You could then enrich the initial metadata with additional properties through Management API calls and the documented merge behavior would work as intended. If you wanted to sill be able to conditionally update the metadata from data coming from the custom database you could update the rule logic with additional conditional logic based on information returned by the database, for example, sending a db_metadata: { override: true, /* ... */ }. This has the down-side of introducing some data duplication, but assuming the amount of data in question is not overwhelming this may not be an issue.

Disclaimer: I tested this with users being created just through the login script of a custom database connection so you may want to test-drive this approach a bit more to see it it fits your scenario.

1 Like