Create/register user on backend with Realm function

Is it possible to register a new user from Functions on the realm backend instead of doing it on the client with realm SDK ?? if not then how can I add custom user data during registration process ? using email/password registration.

3 Likes

Hey @Mohammed_Ramadan,

I did something similar by saving all the user information during the sign up in a different document in Atlas and once sign up is verified we can sync this information back to the user document using a trigger.

do you have a coding example with steps ?
the register user function on the realm SDK takes only email & password and what I want is like giving an additional attribute/param to be stored during registration

Hi @Mohammed_Ramadan,

The documentation explains how to do that: the custom function can only return a unique id and a name, and you’re responsible to ensure your user is a valid one, by creating a new one or ensuring that, given the same login, the same id is returned. You can set additional attributes by implementing Custom User Data, that will store them in a specific collection in the DB.

Hi @Paolo_Manna @Mohit_Sharma

thank you for following up. Let me explain what I have to do based on my reading. But there are some keywords on the documentation are confusing like ( Custom Function Authentication ) but I’m asking about creating new user with custom data in the same time like sending email, password, fullname, gender, etc… in one POST request.
and the phrase Custom Function Authentication is about checking if username and password match and user exist or not. But, after reading the document you have sent I feel that it should be like this

  • On the client side register new user by calling login function on the SDK not register
  • on the custom auth function I do the logic to create new user if doesn’t exist in the users collection I handle with all the data I need!
    But if that is what needs to be done, I have to return ID string to have the user authenticated, in this case I will be returning the _id after converting it to string because it’s ObjectId, right ?
    but
    does that mean I need to create an additional field that stores the ObjectId of newly created user as string value ? and call that field userId for instance ? because I have to mention the linked field for Custom User Data.
    are all of that right ?
1 Like

Hi @Mohammed_Ramadan,

One solution could have been to let the email/password registration happen, and after the first login add the required custom data by calling a specific function explicitly.

Then a Custom Authentication function is necessary: there isn’t a registration process separate from the login process, you call the same function every time, and create or login according to the check for existence.
Please note that, by using a Custom Authentication Function with a password, you’ll be responsible to ensure that its storage will be secure.

Correct.

Correct: it’s also advised to return a name property, so that you can identify the user in a list, i.e. a possible response for the Authentication Function in your case could be

  return { "id": id.toString(), "name": email };

The process here is a bit more complicated than that: when you create a new user in your collection that will store the Custom User Data, the Realm User Id (that’s the one you’ll need when referring to the user from anywhere in the Realm Functions and bind the Custom User Data) is not known yet, and it will not be the same than your _id. Yes, you’ll need to have it registered in your collection for the binding to work, but you just don’t know it!

A possible solution is then to have an Authentication Trigger on the Create operation on the users collection: when the trigger is fired, the user is known, hence you can bind the custom data to it.

A sample code for the triggered function, that sets the realmUserId variable as the field to link, can be:

exports = async function(authEvent) {
  const { user } = authEvent;
  const userCollection = context.services.get("mongodb-atlas").db("<database>").collection("<users>");
  
  try {
    // The _id of the user created in Auth Function is in the identities array
    const customDataId = new BSON.ObjectId(user.identities[0].id);
    
    await userCollection.updateOne(
      { "_id": customDataId },
      { "$set": {"realmUserId": user.id} }
    );
  } catch (error) {
    console.error(`Couldn't update user ${JSON.stringify(user)}: ${error}`);
  }
};
1 Like

ahaaaaaaaaaaaaaaaaaaaaaaaaaaa :smiley: :smiley: now I got it. Thanks a lot

full example
Hi @Paolo_Manna
I think this is how I do it, right ?
on the realm-web

const credentials = Realm.Credentials.function(custom);
const user = await app.logIn(credentials);

custom auth function

exports = async function(loginPayload) {
  // Get a handle for the app.users collection
  const users = context.services
    .get("mongodb-atlas")
    .db("realm-training")
    .collection("users");
    
  // VALIDATE email
  // ----------------
  // Parse out custom data from the FunctionCredential
  const { email, fullName, age, password } = loginPayload;

  // Query for an existing user document with the specified username
  const user = await users.findOne({ email });

  if (user) {
    // If the user document exists
    // VALIDATE password
    const hash = utils.crypto.hmac(password, 'SECRET', 'sha256', 'hex');
    const isMatch = hash === user.hash;
    if(!isMatch){
      return false;
    }
    // return its unique ID
    return user._id.toString();
  } else {
    // If the user document does not exist
    // VALIDATE user data based on user schema
    // ----------------
    // create it and then return its unique ID
    const hash = utils.crypto.hmac(password, 'SECRET', 'sha256', 'hex');
    const result = await users.insertOne({ email, fullName, age, hash });
    
    return {id: result.insertedId.toString(), name: email }
  }
};

trigger function for create user event

exports = async function(authEvent) {
  const { user } = authEvent;
  const userCollection = context.services.get("mongodb-atlas").db("realm-training").collection("users");

  try {
    // The _id of the user created in Auth Function is in the identities array
    const customDataId = new BSON.ObjectId(user.identities[0].id);
    
    await userCollection.updateOne(
      { "_id": customDataId },
      { "$set": {"realmUserId": user.id} }
    );
  } catch (error) {
    console.error(`Couldn't update user ${JSON.stringify(user)}: ${error}`);
  }
};

1 Like

@Paolo_Manna I added bcrypt to do the hashing but it didn’t work.
I got this error

bcrypt failed to execute source for 'node_modules/bcrypt/bcrypt.js

and I used utils.crypto because I found it on the documentation but is this one secure and one time hash ? like no going back aka decryption ?

Hi @Mohammed_Ramadan,

Correct, assuming that custom has the structure the function expects, i.e.

{ email, fullName, age, password }

I think it’s better to keep consistent results, and return the same format that you return when creating a user, i.e.

return {id: user._id.toString(), name: user.email }

The HMAC functionality in the utils.crypto library complies with RFC2104

1 Like

@Paolo_Manna Thanks a lot :). One more thing, in Realm documentation there is no reference to let me know if the custom user data key I define for realm is indexed or not for faster queries, so I think I have to index that key myself for faster query, am I right ?

The collection used for Custom User Data doesn’t need to be visible to Realm on the client side, the related content will be transmitted as a part of the user at login. If you prefer to have indexes, then yes, you need to define them at the MongoDB level.

1 Like

@Mohammed_Ramadan: Hope all queries are resolved and from the message trail I see question/answers can help any developer to understand how to implement custom registration.

And adding to this, I would like you to showcase this knowledge to our DevHub Knowledge base & Medium section, would be you be interested?

Hi, sure I can do that.

2 Likes