Custom Function authentication and POST method

Hi everyone, I apologize in advance if my question may be trivial, but I am now learning how to use MongoDB, and some parts of the documentation are difficult for me to understand.
In my application I don’t have the possibility to implement the Realm SDK, so I was thinking to manage the user/database communication via Webhooks.
First I thought to create a Custom Function authentication, in order to authenticate a user. I was trying to create a simple authentication function like this one:

        exports = async function(loginPayload) {
      // Get a handle for the app.users collection
      const users = context.services
        .get("mongodb-atlas")
        .db("app")
        .collection("users");

      // Parse out custom data from the FunctionCredential

      const { username } = loginPayload;


      // Query for an existing user document with the specified username

      const user = await users.findOne({ username });


      if (user) {
        // If the user document exists, return its unique ID

        return user._id.toString();

      } else {
        // If the user document does not exist, create it and then return its unique ID
        const result = await users.insertOne({ username });

        return result.insertedId.toString();

      }
    };

And then recall it via a Webhook.
Is this correct as a reasoning? Or am I adopting the wrong method?
If everything is correct I will proceed by explaining my current problem. I tried to create a new HTTP Webhook by selecting the “POST” method (since I have to communicate to the function the username of the player to be searched in the database), but I have some difficulties to create the url and the Webhook function.
How should the parameters of the url indicated by the Webhook be set? How can I read this data inside the function and then recall the custom authentication function?
Searching inside the documentation I think I have understood that I have to recall the custom function in this way:
const loginResult = context.functions.execute(“authFunc”, arg1);
return loginResult;
But unfortunately I can’t test this code since, as I wrote earlier, I can’t understand how to correctly read the data received by the “POST” method and assign the user’s username to the variable “arg1”.

Can someone give me some indication about this?
Thanks in advance!

Hi @Andrea,

You do not need to build a webhook to perform http POST authentication with a custom-function.

Basically any provider can be authenticated via the following url:

 https://realm.mongodb.com/api/client/v2.0/app/<yourappid-abcde>/auth/providers/<provider type>/login

For custom function you can do:

curl --location --request POST 'https://realm.mongodb.com/api/client/v2.0/app/myapp-abcde/auth/providers/custom-function/login' \
  --header 'Content-Type: application/json' \
  --data-raw '{

   "username" : "myuser@exmaple.com"

  }'

This will result in a successful authentication and the access token to use for other services (eg. graphql).

Remember to change your appId and provide the relevant payload for the auth provider, in your case a username field.

Please let me know if you have any additional questions.

Best regards,
Pavel

2 Likes

@Pavel_Duchovny Thank you very much for your answer!
I just did a test run, and it’s not really necessary to create a Webhook for authentication like I did.
If I understand correctly the last parameter of the url must be “login” and not the name of the authentication function (“login” is directly associated to the custom authentication function). Right?
When I did a test, I received this data as answer: “access_token”, “refresh_token”, “user_id” and “device_id”.
Where can I find information about the use of these values? I only managed to understand that “user_id” refers to the value of the “_id” field in the db document that refers to the user.
Moreover I have noticed that the “device_id” field is always the same as “00000000000000000000”. I don’t know if this is normal or if I need to do some specific operation to fill the field correctly.

Hi @Andrea

If I understand correctly the last parameter of the url must be “login” and not the name of the authentication function (“login” is directly associated to the custom authentication function). Right?

Correct only 1 auth function can exist per application.

The output you got is the expected output for successful authentication.

Now you can use this token to authenticate services via a Bearer header just as explained for graphql queries here:
https://docs.mongodb.com/realm/reference/authenticate-http-client-requests/#authenticate-http-client-requests

Please let me know if you have any additional questions.

Best regards,
Pavel

1 Like

Thank you for your answer. Now everything is clearer to me!
One last question (I tried looking in the documentation but found nothing about it): what does the value “device_id” refer to? What is its functionality? Or rather, what is it usually used for?

Hi @Andrea,
I am not sure but maybe some idenrifier for your user agent , I think custom function auth will never track it.

Best
Pavel

Thanks again for your answer. It’s ok, mine was just curiosity, for what I have to do I don’t need to trace the id of the device.
I was completing my custom authentication function, but I noticed a small problem. To better clarify my situation I placed part of my custom function:

exports = async function(loginPayload) {
  // Get a handle for the app.users collection
  const users = context.services
  .get("mongodb-atlas")
  .db("magika_db")
  .collection("users");
  
  //console.log(loginPayload);
  const username = loginPayload.toString();

  const user = await users.findOne( {"userData.username": username} );

  if (user) {
    // If the user document exists, return its unique ID
    return user._id.toString();
  } else {
    // If the user document does not exist, create it and then return its unique ID
    const newDocument = {
      "userData": {
        "username": username,
        "email": ""
      },
      "collection": [{
        "name": "",
        "goldIncrase": 5
      }]
    };
    
    const result = await users.insertOne(newDocument);
    
    return result.insertedId.toString();
  }
};

When I call the authentication function via https://realm.mongodb.com/api/client/v2.0/app/myapp-abcde/auth/providers/custom-function/login I get “access_token”, “refresh_token”, “user_id” and “device_id”. I thought that “user_id” was the id that returns the function (so the unique id of the document where the function finds the controlled username), but I noticed that this is not the case. So “user_id” is a unique id created by authentication to authenticate the user?
If I need to receive the return value of the custom authentication function as an answer (in addition to the authentication success values), how can I do it?

Hi @Andrea,

This will be more tricky to implement as a user is not expected to use the function internal returned id for any user logic.

The idea of a custom function auth is that you implement your logic to your external 3rd party application provider and return a value that maps with a Realm User internally, the example in the docs is simple just for the idea presentation.

But for the sake of my point, since your logic provide a unique username and will always retrieve same realm user_id you should treat it as the user id. If you wish to store some additional information in that collection index the username field and query it through user name.

Now to implement collection rules you can still provide a filter based on user_id of realm.

If you wish to still retrieve this ID anyway you need to consider save it in custom user data or have a webhook to get it after the login from the collection.

Best
Pavel

1 Like

First of all I want to thank you again for your time. Thanks to your answers I am learning many new things! :blush:

So, if I understand correctly, the custom authentication function must return an ID string that identifies a unique user (in the case of my function I return the _id of the document containing the user’s username, then a unique ID string). Realm uses this string to check the IDs of internal users, and returns “user_id”. Then by authenticating with the usual _id of the document, Realm will always return the same “user_id”. Do I understand correctly?

If I understood what you wrote, your advice is to save the “user_id” returned by the authentication, and use that value for future interactions with the db (instead of the _id, which I wanted to save from the function and use). If the answer is yes, how should I proceed to “associate” “user_id” to a user’s document? Once I received the authentication answer, should I save the “user_id” value inside my app, and then tell my db (via a Webhook) to save the “user_id” value inside the user’s document with that username?

Hi @Andrea,

You understand correctly.

Actually, the way to use the authentication object in realm services is not always aligned across services.

It really depends on the services, for example graphql uses the access_token as a bearer header for its http request.

However, webhooks should use a script authentication to use the user_id you got.

If you want a detailed example please let me build one and I will provide it in upcoming days.

Best
Pavel

Thank you very much! I think I understand how it works, but with an example created by someone experienced, I think I will be able to learn and understand many new things!

Hi @Andrea,

Ok so the idea is once you get the authentication object from the query you can use it in a webhook payload to authticate the webhook via script function method. For example my webhook of storing post comments:


Script

exports = function(payload) {
  const authInput = JSON.parse(payload.body.text());
  
  if (authInput.user_id)
  {
    return authInput.user_id;
  }
};

The trick is only the required user will be returned from payload so anyone who calls the webhook can execute it via a specific user only if it knows it Realm id (consider the user id to operate as sort of apiKey here)

** Webhook body and parsing **

// This function is the webhook's request handler.
exports = function(payload, response) {
    // Data can be extracted from the request as follows:

    // Query params, e.g. '?arg1=hello&arg2=world' => {arg1: "hello", arg2: "world"}
    const {arg1, arg2} = payload.query;

    // Headers, e.g. {"Content-Type": ["application/json"]}
    const contentTypes = payload.headers["Content-Type"];

    // Raw request body (if the client sent one).
    // This is a binary object that can be accessed as a string using .text()
    const body = JSON.parse(payload.body.text());


    // Querying a mongodb service:
     const comments = context.services.get("mongodb-atlas").db("feed").collection("comments");



    return doc.updateOne({comment_id : body.comment_id, post_id :body.post_id, user_id : body.user_id },body,{ "upsert" : true});
    
};

Now the field provided in my webhook call will save it with user under the user_id field:

curl \
-H "Content-Type: application/json" \
-d '{"user_id":"5fa7105a871d206bd6739a4", "comment_id" : 1, "post_id" : 1, comment : "great post!" }' \
https://webhooks.mongodb-realm.com/api/client/v2.0/app/app-abcd/service/myTest/incoming_webhook/storeComment

My rules for comments are write only permitted to user owned objects and read is for everyone. Therefore if webhook tries to access a comment that is not written by the user it will not allow it to edit that comment.

As you can see Realm will use the user_id in my comment collection to filter and get the correct permissions. And my webhook require this field to authenticate. This field must be the same value as my custom function. Hope now it all make sense.

Please let me know if you have any additional questions.

Best regards,
Pavel

2 Likes

Thank you very much!
Your example is really clear and easy to understand! I was able to make everything work properly!

I just have one more simple question:


Usually, is the execution of the initial authentication function (i.e. the script that returns Realm’s user_id) called with a System authentication method (i.e. with all privileges)? I think the answer is yes, since in my case, when I call that function, I haven’t yet received the “user_id” that I can use to identify the user, but I don’t know if it could be a problem in terms of security.

Thank you.

1 Like

The authentication function should be run as system thats correct.

It should not be run from anywhere but the authentication flow and should be marked as Private I believe…

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.